From 62407ababe5fabdec1088afd0deb1ce0bf311dc3 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Tue, 25 Jul 2023 09:51:10 -0500 Subject: [PATCH] feat(core): expand support for projectRoot token to include project.json --- e2e/nx-run/src/run.test.ts | 67 +++++++ packages/nx/src/config/workspaces.spec.ts | 55 ------ packages/nx/src/config/workspaces.ts | 78 ++++---- .../build-nodes/workspace-projects.spec.ts | 179 +++++++++++++++++- .../build-nodes/workspace-projects.ts | 25 ++- 5 files changed, 305 insertions(+), 99 deletions(-) diff --git a/e2e/nx-run/src/run.test.ts b/e2e/nx-run/src/run.test.ts index 792d482c26f687..fed424a9625901 100644 --- a/e2e/nx-run/src/run.test.ts +++ b/e2e/nx-run/src/run.test.ts @@ -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', () => { diff --git a/packages/nx/src/config/workspaces.spec.ts b/packages/nx/src/config/workspaces.spec.ts index ed9e39558cfde6..e80b063bc6e082 100644 --- a/packages/nx/src/config/workspaces.spec.ts +++ b/packages/nx/src/config/workspaces.spec.ts @@ -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', () => { diff --git a/packages/nx/src/config/workspaces.ts b/packages/nx/src/config/workspaces.ts index 8febcdb540a542..7a5ff30c2f8d3f 100644 --- a/packages/nx/src/config/workspaces.ts +++ b/packages/nx/src/config/workspaces.ts @@ -88,7 +88,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, @@ -393,53 +393,57 @@ export function mergeTargetConfigurations( !targetConfiguration.executor || targetDefaults.executor === targetConfiguration.executor ) { - result.options = mergeOptions( - defaultOptions, - targetConfiguration.options ?? {}, - projectConfiguration, - target - ); + result.options = mergeOptions(defaultOptions, targetConfiguration.options); result.configurations = mergeConfigurations( defaultConfigurations, - targetConfiguration.configurations, - projectConfiguration, - target + targetConfiguration.configurations ); } return result as TargetConfiguration; } -function mergeOptions( - defaults: T, - options: T, - project: ProjectConfiguration, - key: string -): T { - return { - ...resolvePathTokensInOptions(defaults, project, key), - ...options, - }; +function mergeOptions(defaults: T, options: T): T { + const result: T = { ...defaults }; + for (const key in options) { + // Deep merge options objects, but not arrays. + if ( + typeof options[key] === 'object' && + typeof defaults?.[key] === 'object' && + !Array.isArray(options[key]) && + !Array.isArray(defaults?.[key]) + ) { + result[key] = mergeOptions(defaults[key], options[key]); + } else { + // The types are either: + // - primitive (string, number, boolean, etc.) + // - arrays + // - different types (e.g. string vs array) + // In any case, we want to override the default with the options. + result[key] = options[key]; + } + } + return result; } function mergeConfigurations( defaultConfigurations: Record, - projectDefinedConfigurations: Record, - project: ProjectConfiguration, - targetName: string + projectDefinedConfigurations: Record ): Record { - const configurations: Record = { ...projectDefinedConfigurations }; - for (const configuration in defaultConfigurations) { - configurations[configuration] = mergeOptions( - defaultConfigurations[configuration], - configurations[configuration], - project, - `${targetName}.${configuration}` + const result: Record = {}; + const configurations = new Set([ + ...Object.keys(defaultConfigurations ?? {}), + ...Object.keys(projectDefinedConfigurations ?? {}), + ]); + for (const configuration of configurations) { + result[configuration] = mergeOptions( + defaultConfigurations?.[configuration] ?? ({} as T), + projectDefinedConfigurations?.[configuration] ?? ({} as T) ); } - return configurations; + return result; } -function resolvePathTokensInOptions>( +export function resolveNxTokensInOptions>( object: T, project: ProjectConfiguration, key: string @@ -447,10 +451,10 @@ function resolvePathTokensInOptions>( 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\}\//, ''); - } - if (value.includes('{workspaceRoot}')) { + const workspaceRootMatch = /^(\{workspaceRoot\}\/?)/.exec(value); + if (workspaceRootMatch?.length) { + value = value.replace(workspaceRootMatch[0], ''); + } else if (value.includes('{workspaceRoot}')) { throw new Error( `${NX_PREFIX} The {workspaceRoot} token is only valid at the beginning of an option. (${key})` ); @@ -458,7 +462,7 @@ function resolvePathTokensInOptions>( 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('.') diff --git a/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts b/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts index 56858c0f718e15..34ac097dafd84d 100644 --- a/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts +++ b/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts @@ -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 = { @@ -75,4 +78,178 @@ 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 merge 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', + c: 'c', + }, + }); + }); + + 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' }); + }); + }); }); diff --git a/packages/nx/src/project-graph/build-nodes/workspace-projects.ts b/packages/nx/src/project-graph/build-nodes/workspace-projects.ts index 8112a779930e22..20c6d578d04a60 100644 --- a/packages/nx/src/project-graph/build-nodes/workspace-projects.ts +++ b/packages/nx/src/project-graph/build-nodes/workspace-projects.ts @@ -20,6 +20,7 @@ import { NX_PREFIX } from '../../utils/logger'; import { mergeTargetConfigurations, readTargetDefaultsForTarget, + resolveNxTokensInOptions, } from '../../config/workspaces'; export async function buildWorkspaceProjectNodes( @@ -123,7 +124,7 @@ export async function buildWorkspaceProjectNodes( /** * Apply target defaults and normalization */ -function normalizeProjectTargets( +export function normalizeProjectTargets( project: ProjectConfiguration, targetDefaults: NxJsonConfiguration['targetDefaults'], projectName: string @@ -145,24 +146,36 @@ function normalizeProjectTargets( targets[target] = mergeTargetConfigurations(project, target, defaults); } - const config = targets[target]; - if (config.command) { + const { command, ...config } = targets[target]; + if (command) { if (config.executor) { throw new Error( `${NX_PREFIX} ${projectName}: ${target} should not have executor and command both configured.` ); } else { targets[target] = { - ...targets[target], + ...config, executor, options: { ...config.options, - command: config.command, + command: 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; }