nx/packages/devkit/src/generators/project-configuration.ts

379 lines
11 KiB
TypeScript

import type { Tree } from '@nrwl/tao/src/shared/tree';
import {
ProjectConfiguration,
RawWorkspaceJsonConfiguration,
toNewFormat,
WorkspaceJsonConfiguration,
} from '@nrwl/tao/src/shared/workspace';
import { readJson, updateJson, writeJson } from '../utils/json';
import type {
NxJsonConfiguration,
NxJsonProjectConfiguration,
} from '@nrwl/tao/src/shared/nx';
import {
getWorkspaceLayout,
getWorkspacePath,
} from '../utils/get-workspace-layout';
import { joinPathFragments } from '../utils/path';
export type WorkspaceConfiguration = Omit<
WorkspaceJsonConfiguration,
'projects'
> &
Omit<NxJsonConfiguration, 'projects'>;
/**
* Adds project configuration to the Nx workspace.
*
* The project configuration is stored in workspace.json and nx.json. The utility will update
* both files.
*
* @param host - 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(
host: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration,
standalone: boolean = false
): void {
standalone = standalone || getWorkspaceLayout(host).standaloneAsDefault;
setProjectConfiguration(
host,
projectName,
projectConfiguration,
'create',
standalone
);
}
/**
* Updates the configuration of an existing project.
*
* The project configuration is stored in workspace.json and nx.json. The utility will update
* both files.
*
* @param host - 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(
host: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration
): void {
setProjectConfiguration(host, projectName, projectConfiguration, 'update');
}
/**
* Removes the configuration of an existing project.
*
* The project configuration is stored in workspace.json and nx.json.
* The utility will update both files.
*/
export function removeProjectConfiguration(
host: Tree,
projectName: string
): void {
setProjectConfiguration(host, projectName, undefined, 'delete');
}
/**
* Get a map of all projects in a workspace.
*
* Use {@link readProjectConfiguration} if only one project is needed.
*/
export function getProjects(
host: Tree
): Map<string, ProjectConfiguration & NxJsonProjectConfiguration> {
const workspace = readWorkspace(host);
const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
return new Map(
Object.keys(workspace.projects || {}).map((projectName) => {
return [
projectName,
getProjectConfiguration(host, projectName, workspace, nxJson),
];
})
);
}
/**
* 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(host: Tree): WorkspaceConfiguration {
const workspace = readWorkspace(host);
delete workspace.projects;
const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
delete nxJson.projects;
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(
host: Tree,
workspaceConfig: WorkspaceConfiguration
): void {
const { version, cli, defaultProject, generators, ...nxJson } =
workspaceConfig;
const workspace: Omit<Required<WorkspaceJsonConfiguration>, 'projects'> = {
version,
cli,
defaultProject,
generators,
};
updateJson<WorkspaceJsonConfiguration>(
host,
getWorkspacePath(host),
(json) => {
return { ...json, ...workspace };
}
);
updateJson<NxJsonConfiguration>(host, 'nx.json', (json) => {
return { ...json, ...nxJson };
});
}
/**
* Reads a project configuration.
*
* The project configuration is stored in workspace.json and nx.json. The utility will read
* both files.
*
* @param host - 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(
host: Tree,
projectName: string
): ProjectConfiguration & NxJsonProjectConfiguration {
const workspace = readWorkspace(host);
if (!workspace.projects[projectName]) {
throw new Error(
`Cannot find configuration for '${projectName}' in ${getWorkspacePath(
host
)}.`
);
}
const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
// TODO: Remove after confirming that nx.json should be optional.
// if (!nxJson.projects[projectName]) {
// throw new Error(
// `Cannot find configuration for '${projectName}' in nx.json`
// );
// }
return getProjectConfiguration(host, projectName, workspace, nxJson);
}
function getProjectConfiguration(
host: Tree,
projectName: string,
workspace: WorkspaceJsonConfiguration,
nxJson: NxJsonConfiguration
): ProjectConfiguration & NxJsonProjectConfiguration {
return {
...readWorkspaceSection(host, workspace, projectName),
...readNxJsonSection(nxJson, projectName),
};
}
function readWorkspaceSection(
host: Tree,
workspace: WorkspaceJsonConfiguration,
projectName: string
) {
const config = workspace.projects[projectName];
return config;
}
function readNxJsonSection(nxJson: NxJsonConfiguration, projectName: string) {
return nxJson.projects[projectName];
}
function setProjectConfiguration(
host: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration,
mode: 'create' | 'update' | 'delete',
standalone: boolean = false
) {
if (mode === 'delete') {
addProjectToNxJson(host, projectName, undefined, mode);
addProjectToWorkspaceJson(host, projectName, undefined, mode);
return;
}
if (!projectConfiguration) {
throw new Error(
`Cannot ${mode} "${projectName}" with value ${projectConfiguration}`
);
}
const { tags, implicitDependencies } = projectConfiguration;
addProjectToWorkspaceJson(
host,
projectName,
projectConfiguration,
mode,
standalone
);
addProjectToNxJson(
host,
projectName,
{
tags,
implicitDependencies,
},
mode
);
}
function addProjectToWorkspaceJson(
host: Tree,
projectName: string,
project: ProjectConfiguration & NxJsonProjectConfiguration,
mode: 'create' | 'update' | 'delete',
standalone: boolean = false
) {
const path = getWorkspacePath(host);
const workspaceJson = readJson<RawWorkspaceJsonConfiguration>(host, path);
validateWorkspaceJsonOperations(mode, workspaceJson, projectName);
const configFile =
mode === 'create' && standalone
? joinPathFragments(project.root, 'project.json')
: getProjectFileLocation(host, projectName);
if (configFile) {
if (mode === 'delete') {
host.delete(configFile);
} else {
writeJson(host, configFile, project);
}
if (mode === 'create') {
workspaceJson.projects[projectName] = project.root;
writeJson(host, path, workspaceJson);
}
} else {
let workspaceConfiguration: ProjectConfiguration;
if (project) {
const { tags, implicitDependencies, ...c } = project;
workspaceConfiguration = c;
}
workspaceJson.projects[projectName] = workspaceConfiguration;
writeJson(host, path, workspaceJson);
}
}
function addProjectToNxJson(
host: Tree,
projectName: string,
config: NxJsonProjectConfiguration,
mode: 'create' | 'update' | 'delete'
) {
// distributed project files do not use nx.json,
// so only proceed if the project does not use them.
if (!getProjectFileLocation(host, projectName)) {
const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
if (mode === 'delete') {
delete nxJson.projects[projectName];
} else {
nxJson.projects[projectName] = {
...{
tags: [],
},
...(config || {}),
};
}
writeJson(host, 'nx.json', nxJson);
}
}
function readWorkspace(host: Tree): WorkspaceJsonConfiguration {
const workspaceJson = inlineProjectConfigurationsWithTree(host);
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(
host: Tree
): WorkspaceJsonConfiguration {
const path = getWorkspacePath(host);
const workspaceJson = readJson<RawWorkspaceJsonConfiguration>(host, path);
Object.entries(workspaceJson.projects || {}).forEach(([project, config]) => {
if (typeof config === 'string') {
const configFileLocation = joinPathFragments(config, 'project.json');
workspaceJson.projects[project] = readJson<
ProjectConfiguration & NxJsonProjectConfiguration
>(host, configFileLocation);
}
});
return workspaceJson as WorkspaceJsonConfiguration;
}
/**
* @description Determine where a project's configuration is located.
* @returns file path if separate from root config, null otherwise.
*/
function getProjectFileLocation(host: Tree, project: string): string | null {
const rawWorkspace = readJson<RawWorkspaceJsonConfiguration>(
host,
getWorkspacePath(host)
);
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.`
);
}
}