feat(core): expand support for projectRoot token to include project.json (#18302)
This commit is contained in:
parent
c175fc3138
commit
1a1cb4f2dc
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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('.')
|
||||
|
||||
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user