nx/packages/devkit/src/generators/project-configuration.ts
Craigory Coppola bdcbac4445
feat(core): standalone project configuration is default (#7351)
* feat(core): standalone project configuration is default

* chore(core): changes to e2e tests

* fix(core): intuit projects when inside their directories when using project.json files

* chore(core): e2e changes

* fix(core): standalone configurations work with npm-project generator

* chore(core): e2e tests should use standalone configurations

* chore(core): e2e fixes per review
2021-10-16 00:07:12 +02:00

422 lines
11 KiB
TypeScript

import {
ProjectConfiguration,
RawWorkspaceJsonConfiguration,
reformattedWorkspaceJsonOrNull,
toNewFormat,
WorkspaceJsonConfiguration,
} from '@nrwl/tao/src/shared/workspace';
import {
getWorkspaceLayout,
getWorkspacePath,
} from '../utils/get-workspace-layout';
import { readJson, updateJson, writeJson } from '../utils/json';
import { joinPathFragments } from '../utils/path';
import type { Tree } from '@nrwl/tao/src/shared/tree';
import type { NxJsonConfiguration } from '@nrwl/tao/src/shared/nx';
export type WorkspaceConfiguration = Omit<
WorkspaceJsonConfiguration,
'projects'
> &
Partial<NxJsonConfiguration>;
/**
* Adds project configuration to the Nx workspace.
*
* The project configuration is stored in workspace.json or the associated project.json file.
* The utility will update either files.
*
* @param tree - the file system tree
* @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib)
* @param projectConfiguration - project configuration
* @param standalone - should the project use package.json? If false, the project config is inside workspace.json
*/
export function addProjectConfiguration(
tree: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration,
standalone?: boolean
): void {
const workspaceLayout = getWorkspaceLayout(tree);
standalone = standalone ?? workspaceLayout.standaloneAsDefault;
setProjectConfiguration(
tree,
projectName,
projectConfiguration,
'create',
standalone
);
}
/**
* Updates the configuration of an existing project.
*
* The project configuration is stored in workspace.json or the associated project.json file.
* The utility will update either files.
*
* @param tree - the file system tree
* @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib)
* @param projectConfiguration - project configuration
*/
export function updateProjectConfiguration(
tree: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration
): void {
setProjectConfiguration(tree, projectName, projectConfiguration, 'update');
}
/**
* Removes the configuration of an existing project.
*
* The project configuration is stored in workspace.json or the associated project.json file.
* The utility will update either file.
*/
export function removeProjectConfiguration(
tree: Tree,
projectName: string
): void {
setProjectConfiguration(tree, projectName, undefined, 'delete');
}
/**
* Get a map of all projects in a workspace.
*
* Use {@link readProjectConfiguration} if only one project is needed.
*/
export function getProjects(tree: Tree): Map<string, ProjectConfiguration> {
const workspace = readWorkspace(tree);
return new Map(
Object.keys(workspace.projects || {}).map((projectName) => {
return [projectName, getProjectConfiguration(projectName, workspace)];
})
);
}
/**
* Read general workspace configuration such as the default project or cli settings
*
* This does _not_ provide projects configuration, use {@link readProjectConfiguration} instead.
*/
export function readWorkspaceConfiguration(tree: Tree): WorkspaceConfiguration {
const workspace = readRawWorkspaceJson(tree);
delete workspace.projects;
let nxJson = readNxJson(tree);
if (nxJson === null) {
return workspace;
}
const nxJsonExtends = readNxJsonExtends(tree, nxJson as any);
if (nxJsonExtends) {
nxJson = { ...nxJsonExtends, ...nxJson };
}
return {
...workspace,
...nxJson,
};
}
/**
* Update general workspace configuration such as the default project or cli settings.
*
* This does _not_ update projects configuration, use {@link updateProjectConfiguration} or {@link addProjectConfiguration} instead.
*/
export function updateWorkspaceConfiguration(
tree: Tree,
workspaceConfig: WorkspaceConfiguration
): void {
const {
// Workspace Json Properties
version,
// Nx Json Properties
cli,
defaultProject,
generators,
implicitDependencies,
plugins,
npmScope,
targetDependencies,
workspaceLayout,
tasksRunnerOptions,
affected,
} = workspaceConfig;
const workspace: Omit<Required<WorkspaceJsonConfiguration>, 'projects'> = {
version,
};
const nxJson: Required<NxJsonConfiguration> = {
implicitDependencies,
plugins,
npmScope,
targetDependencies,
workspaceLayout,
tasksRunnerOptions,
affected,
cli,
generators,
defaultProject,
};
updateJson<WorkspaceJsonConfiguration>(
tree,
getWorkspacePath(tree),
(json) => {
return {
...json,
...(reformattedWorkspaceJsonOrNull(workspace) ?? workspace),
};
}
);
if (tree.exists('nx.json')) {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
const nxJsonExtends = readNxJsonExtends(tree, json as any);
if (nxJsonExtends) {
const changedPropsOfNxJson = {};
Object.keys(nxJson).forEach((prop) => {
if (
JSON.stringify(nxJson[prop], null, 2) !=
JSON.stringify(nxJsonExtends[prop], null, 2)
) {
changedPropsOfNxJson[prop] = nxJson[prop];
}
});
return { ...json, ...changedPropsOfNxJson };
} else {
return { ...json, ...nxJson };
}
});
}
}
function readNxJsonExtends(tree: Tree, nxJson: { extends?: string }) {
if (nxJson.extends) {
const extendsPath = nxJson.extends;
try {
return JSON.parse(
tree.read(joinPathFragments('node_modules', extendsPath), 'utf-8')
);
} catch (e) {
throw new Error(`Unable to resolve nx.json extends. Error: ${e.message}`);
}
} else {
return null;
}
}
/**
* Reads a project configuration.
*
* The project configuration is stored in workspace.json or the associated project.json file.
* The utility will read from either file.
*
* @param tree - the file system tree
* @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib)
* @throws If supplied projectName cannot be found
*/
export function readProjectConfiguration(
tree: Tree,
projectName: string
): ProjectConfiguration {
const workspace = readWorkspace(tree);
if (!workspace.projects[projectName]) {
throw new Error(
`Cannot find configuration for '${projectName}' in ${getWorkspacePath(
tree
)}.`
);
}
const nxJson = readNxJson(tree);
return getProjectConfiguration(projectName, workspace);
}
export function readNxJson(tree: Tree): NxJsonConfiguration | null {
if (!tree.exists('nx.json')) {
return null;
}
let nxJson = readJson<NxJsonConfiguration>(tree, 'nx.json');
const nxJsonExtends = readNxJsonExtends(tree, nxJson as any);
if (nxJsonExtends) {
nxJson = { ...nxJsonExtends, ...nxJson };
}
return nxJson;
}
/**
* Returns if a project has a standalone configuration (project.json).
*
* @param tree - the file system tree
* @param project - the project name
*/
export function isStandaloneProject(tree: Tree, project: string): boolean {
const rawWorkspace = readJson<RawWorkspaceJsonConfiguration>(
tree,
getWorkspacePath(tree)
);
const projectConfig = rawWorkspace.projects?.[project];
return typeof projectConfig === 'string';
}
function getProjectConfiguration(
projectName: string,
workspace: WorkspaceJsonConfiguration
): ProjectConfiguration {
return {
...readWorkspaceSection(workspace, projectName),
};
}
function readWorkspaceSection(
workspace: WorkspaceJsonConfiguration,
projectName: string
) {
return workspace.projects[projectName];
}
function setProjectConfiguration(
tree: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration,
mode: 'create' | 'update' | 'delete',
standalone: boolean = false
): void {
if (mode === 'delete') {
addProjectToWorkspaceJson(tree, projectName, undefined, mode);
return;
}
if (!projectConfiguration) {
throw new Error(
`Cannot ${mode} "${projectName}" with value ${projectConfiguration}`
);
}
addProjectToWorkspaceJson(
tree,
projectName,
projectConfiguration,
mode,
standalone
);
}
function addProjectToWorkspaceJson(
tree: Tree,
projectName: string,
project: ProjectConfiguration,
mode: 'create' | 'update' | 'delete',
standalone: boolean = false
) {
const path = getWorkspacePath(tree);
const workspaceJson = readJson<RawWorkspaceJsonConfiguration>(tree, path);
validateWorkspaceJsonOperations(mode, workspaceJson, projectName);
const configFile =
mode === 'create' && standalone
? joinPathFragments(project.root, 'project.json')
: getProjectFileLocation(tree, projectName);
if (configFile) {
if (mode === 'delete') {
tree.delete(configFile);
delete workspaceJson.projects[projectName];
} else {
writeJson(tree, configFile, project);
}
if (mode === 'create') {
workspaceJson.projects[projectName] = project.root;
}
} else if (mode === 'delete') {
delete workspaceJson.projects[projectName];
} else {
workspaceJson.projects[projectName] = project;
}
writeJson(
tree,
path,
reformattedWorkspaceJsonOrNull(workspaceJson) ?? workspaceJson
);
}
/**
* Read the workspace configuration, including projects.
*/
export function readWorkspace(tree: Tree): WorkspaceJsonConfiguration {
const workspaceJson = inlineProjectConfigurationsWithTree(tree);
const originalVersion = workspaceJson.version;
return {
...toNewFormat(workspaceJson),
version: originalVersion,
};
}
/**
* This has to be separate from the inline functionality inside tao,
* as the functionality in tao does not use a Tree. Changes made during
* a generator would not be present during runtime execution.
* @returns
*/
function inlineProjectConfigurationsWithTree(
tree: Tree
): WorkspaceJsonConfiguration {
const workspaceJson = readRawWorkspaceJson(tree);
Object.entries(workspaceJson.projects || {}).forEach(([project, config]) => {
if (typeof config === 'string') {
const configFileLocation = joinPathFragments(config, 'project.json');
workspaceJson.projects[project] = readJson<ProjectConfiguration>(
tree,
configFileLocation
);
}
});
return workspaceJson as WorkspaceJsonConfiguration;
}
function readRawWorkspaceJson(tree: Tree): RawWorkspaceJsonConfiguration {
const path = getWorkspacePath(tree);
return readJson<RawWorkspaceJsonConfiguration>(tree, path);
}
/**
* @description Determine where a project's configuration is located.
* @returns file path if separate from root config, null otherwise.
*/
function getProjectFileLocation(tree: Tree, project: string): string | null {
const rawWorkspace = readRawWorkspaceJson(tree);
const projectConfig = rawWorkspace.projects?.[project];
return typeof projectConfig === 'string'
? joinPathFragments(projectConfig, 'project.json')
: null;
}
function validateWorkspaceJsonOperations(
mode: 'create' | 'update' | 'delete',
workspaceJson: RawWorkspaceJsonConfiguration | WorkspaceJsonConfiguration,
projectName: string
) {
if (mode == 'create' && workspaceJson.projects[projectName]) {
throw new Error(
`Cannot create Project '${projectName}'. It already exists.`
);
}
if (mode == 'update' && !workspaceJson.projects[projectName]) {
throw new Error(
`Cannot update Project '${projectName}'. It does not exist.`
);
}
if (mode == 'delete' && !workspaceJson.projects[projectName]) {
throw new Error(
`Cannot delete Project '${projectName}'. It does not exist.`
);
}
}