feat(core): expand support for projectRoot token to include project.json (#18302)

This commit is contained in:
Craigory Coppola 2023-08-03 16:44:09 -05:00 committed by GitHub
parent c175fc3138
commit 1a1cb4f2dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 366 additions and 117 deletions

View File

@ -115,6 +115,73 @@ describe('Nx Running Tests', () => {
updateProjectConfig(mylib, (c) => original);
}, 1000000);
describe('tokens support', () => {
let app: string;
beforeAll(() => {
app = uniq('myapp');
runCLI(`generate @nx/web:app ${app}`);
});
it('should support using {projectRoot} in options blocks in project.json', async () => {
updateProjectConfig(app, (c) => {
c.targets['echo'] = {
command: `node -e 'console.log("{projectRoot}")'`,
};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(`apps/${app}`);
});
it('should support using {projectName} in options blocks in project.json', async () => {
updateProjectConfig(app, (c) => {
c.targets['echo'] = {
command: `node -e 'console.log("{projectName}")'`,
};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(app);
});
it('should support using {projectRoot} in targetDefaults', async () => {
updateJson(`nx.json`, (json) => {
json.targetDefaults = {
echo: {
command: `node -e 'console.log("{projectRoot}")'`,
},
};
return json;
});
updateProjectConfig(app, (c) => {
c.targets['echo'] = {};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(`apps/${app}`);
});
it('should support using {projectName} in targetDefaults', async () => {
updateJson(`nx.json`, (json) => {
json.targetDefaults = {
echo: {
command: `node -e 'console.log("{projectName}")'`,
},
};
return json;
});
updateProjectConfig(app, (c) => {
c.targets['echo'] = {};
return c;
});
const output = runCLI(`echo ${app}`);
expect(output).toContain(app);
});
});
});
describe('Nx Bail', () => {

View File

@ -256,30 +256,6 @@ describe('Workspaces', () => {
).options
).toEqual({ a: 'project-value' });
});
it('should resolve workspaceRoot and projectRoot tokens', () => {
expect(
mergeTargetConfigurations(
{
root: 'my/project',
targets: {
build: {
options: {
a: '{workspaceRoot}',
},
},
},
},
'build',
{
executor: 'target',
options: {
b: '{workspaceRoot}/dist/{projectRoot}',
},
}
).options
).toEqual({ a: '{workspaceRoot}', b: 'dist/my/project' });
});
});
describe('configurations', () => {
@ -392,37 +368,6 @@ describe('Workspaces', () => {
).configurations
).toEqual(projectConfigurations);
});
it('should resolve workspaceRoot and projectRoot tokens', () => {
expect(
mergeTargetConfigurations(
{
root: 'my/project',
targets: {
build: {
configurations: {
dev: {
a: '{workspaceRoot}',
},
},
},
},
},
'build',
{
executor: 'target',
configurations: {
prod: {
a: '{workspaceRoot}/dist/{projectRoot}',
},
},
}
).configurations
).toEqual({
dev: { a: '{workspaceRoot}' },
prod: { a: 'dist/my/project' },
});
});
});
describe('defaultConfiguration', () => {

View File

@ -84,7 +84,7 @@ export class Workspaces {
) {
for (const proj of Object.values(projects)) {
if (proj.targets) {
for (const targetName of Object.keys(proj.targets)) {
for (const targetName of Object.keys(proj.targets ?? {})) {
const projectTargetDefinition = proj.targets[targetName];
const defaults = readTargetDefaultsForTarget(
targetName,
@ -389,53 +389,34 @@ export function mergeTargetConfigurations(
!targetConfiguration.executor ||
targetDefaults.executor === targetConfiguration.executor
) {
result.options = mergeOptions(
defaultOptions,
targetConfiguration.options ?? {},
projectConfiguration,
target
);
result.options = { ...defaultOptions, ...targetConfiguration?.options };
result.configurations = mergeConfigurations(
defaultConfigurations,
targetConfiguration.configurations,
projectConfiguration,
target
targetConfiguration.configurations
);
}
return result as TargetConfiguration;
}
function mergeOptions<T extends Object>(
defaults: T,
options: T,
project: ProjectConfiguration,
key: string
): T {
return {
...resolvePathTokensInOptions(defaults, project, key),
...options,
};
}
function mergeConfigurations<T extends Object>(
defaultConfigurations: Record<string, T>,
projectDefinedConfigurations: Record<string, T>,
project: ProjectConfiguration,
targetName: string
projectDefinedConfigurations: Record<string, T>
): Record<string, T> {
const configurations: Record<string, T> = { ...projectDefinedConfigurations };
for (const configuration in defaultConfigurations) {
configurations[configuration] = mergeOptions(
defaultConfigurations[configuration],
configurations[configuration],
project,
`${targetName}.${configuration}`
);
const result: Record<string, T> = {};
const configurations = new Set([
...Object.keys(defaultConfigurations ?? {}),
...Object.keys(projectDefinedConfigurations ?? {}),
]);
for (const configuration of configurations) {
result[configuration] = {
...(defaultConfigurations?.[configuration] ?? ({} as T)),
...(projectDefinedConfigurations?.[configuration] ?? ({} as T)),
};
}
return configurations;
return result;
}
function resolvePathTokensInOptions<T extends Object | Array<unknown>>(
export function resolveNxTokensInOptions<T extends Object | Array<unknown>>(
object: T,
project: ProjectConfiguration,
key: string
@ -443,8 +424,9 @@ function resolvePathTokensInOptions<T extends Object | Array<unknown>>(
const result: T = Array.isArray(object) ? ([...object] as T) : { ...object };
for (let [opt, value] of Object.entries(object ?? {})) {
if (typeof value === 'string') {
if (value.startsWith('{workspaceRoot}/')) {
value = value.replace(/^\{workspaceRoot\}\//, '');
const workspaceRootMatch = /^(\{workspaceRoot\}\/?)/.exec(value);
if (workspaceRootMatch?.length) {
value = value.replace(workspaceRootMatch[0], '');
}
if (value.includes('{workspaceRoot}')) {
throw new Error(
@ -454,7 +436,7 @@ function resolvePathTokensInOptions<T extends Object | Array<unknown>>(
value = value.replace(/\{projectRoot\}/g, project.root);
result[opt] = value.replace(/\{projectName\}/g, project.name);
} else if (typeof value === 'object' && value) {
result[opt] = resolvePathTokensInOptions(
result[opt] = resolveNxTokensInOptions(
value,
project,
[key, opt].join('.')

View File

@ -1,5 +1,8 @@
import { ProjectGraphProjectNode } from '../../config/project-graph';
import { normalizeImplicitDependencies } from './workspace-projects';
import {
normalizeImplicitDependencies,
normalizeProjectTargets,
} from './workspace-projects';
describe('workspace-projects', () => {
let projectGraph: Record<string, ProjectGraphProjectNode> = {
@ -75,4 +78,228 @@ describe('workspace-projects', () => {
).toEqual(['b', 'b-1', 'b-2']);
});
});
describe('normalizeTargets', () => {
it('should apply target defaults', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: 'a',
},
},
},
},
{
build: {
executor: 'target',
options: {
b: 'b',
},
},
},
'build'
).build.options
).toEqual({ a: 'a', b: 'b' });
});
it('should overwrite target defaults when type doesnt match or provided an array', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: 'a',
b: ['project-value'],
c: 'project-value',
},
},
},
},
{
build: {
executor: 'target',
options: {
a: 1,
b: ['default-value'],
c: ['default-value'],
},
},
},
'build'
).build.options
).toEqual({ a: 'a', b: ['project-value'], c: 'project-value' });
});
it('should overwrite object options from target defaults', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: 'a',
b: {
a: 'a',
b: 'project-value',
},
},
},
},
},
{
build: {
executor: 'target',
options: {
b: {
b: 'default-value',
c: 'c',
},
},
},
},
'build'
).build.options
).toEqual({
a: 'a',
b: {
a: 'a',
b: 'project-value',
},
});
});
it('should convert command property to run-commands executor', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
command: 'echo',
},
},
},
{},
'build'
).build
).toEqual({
executor: 'nx:run-commands',
options: {
command: 'echo',
},
});
});
it('should support {projectRoot}, {workspaceRoot}, and {projectName} tokens', () => {
expect(
normalizeProjectTargets(
{
name: 'project',
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: '{projectRoot}',
b: '{workspaceRoot}',
c: '{projectName}',
},
},
},
},
{},
'build'
).build.options
).toEqual({ a: 'my/project', b: '', c: 'project' });
});
it('should suppport {projectRoot} token in targetDefaults', () => {
expect(
normalizeProjectTargets(
{
name: 'project',
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: 'a',
},
},
},
},
{
build: {
executor: 'target',
options: {
b: '{projectRoot}',
},
},
},
'build'
).build.options
).toEqual({ a: 'a', b: 'my/project' });
});
it('should not merge options when targets use different executors', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
executor: 'target',
options: {
a: 'a',
},
},
},
},
{
build: {
executor: 'different-target',
options: {
b: 'c',
},
},
},
'build'
).build.options
).toEqual({ a: 'a' });
});
it('should not merge options when either target or target defaults use `command`', () => {
expect(
normalizeProjectTargets(
{
root: 'my/project',
targets: {
build: {
command: 'echo',
},
},
},
{
build: {
executor: 'target',
options: {
b: 'c',
},
},
},
'build'
).build.options
).toEqual({ command: 'echo' });
});
});
});

View File

@ -14,12 +14,16 @@ import { ProjectGraphBuilder } from '../project-graph-builder';
import { PackageJson } from '../../utils/package-json';
import { readJsonFile } from '../../utils/fileutils';
import { NxJsonConfiguration } from '../../config/nx-json';
import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import {
ProjectConfiguration,
TargetConfiguration,
} from '../../config/workspace-json-project-json';
import { findMatchingProjects } from '../../utils/find-matching-projects';
import { NX_PREFIX } from '../../utils/logger';
import {
mergeTargetConfigurations,
readTargetDefaultsForTarget,
resolveNxTokensInOptions,
} from '../../config/workspaces';
export async function buildWorkspaceProjectNodes(
@ -123,7 +127,7 @@ export async function buildWorkspaceProjectNodes(
/**
* Apply target defaults and normalization
*/
function normalizeProjectTargets(
export function normalizeProjectTargets(
project: ProjectConfiguration,
targetDefaults: NxJsonConfiguration['targetDefaults'],
projectName: string
@ -135,33 +139,31 @@ function normalizeProjectTargets(
? 'nx:run-commands'
: null;
const defaults = readTargetDefaultsForTarget(
target,
targetDefaults,
executor
const defaults = resolveCommandSyntacticSugar(
readTargetDefaultsForTarget(target, targetDefaults, executor),
`targetDefaults:${target}`
);
targets[target] = resolveCommandSyntacticSugar(
targets[target],
`${projectName}:${target}`
);
if (defaults) {
targets[target] = mergeTargetConfigurations(project, target, defaults);
}
const config = targets[target];
if (config.command) {
if (config.executor) {
throw new Error(
`${NX_PREFIX} ${projectName}: ${target} should not have executor and command both configured.`
);
} else {
targets[target] = {
...targets[target],
executor,
options: {
...config.options,
command: config.command,
},
};
delete config.command;
}
targets[target].options = resolveNxTokensInOptions(
targets[target].options,
project,
`${projectName}:${target}`
);
for (const configuration in targets[target].configurations ?? {}) {
targets[target].configurations[configuration] = resolveNxTokensInOptions(
targets[target].configurations[configuration],
project,
`${projectName}:${target}:${configuration}`
);
}
}
return targets;
@ -184,3 +186,29 @@ export function normalizeImplicitDependencies(
.concat(implicitDependencies.filter((x) => x.startsWith('!')))
);
}
function resolveCommandSyntacticSugar(
target: TargetConfiguration,
key: string
): TargetConfiguration {
const { command, ...config } = target ?? {};
if (!command) {
return target;
}
if (config.executor) {
throw new Error(
`${NX_PREFIX} ${key} should not have executor and command both configured.`
);
} else {
return {
...config,
executor: 'nx:run-commands',
options: {
...config.options,
command: command,
},
};
}
}