diff --git a/e2e/nx-run/src/run.test.ts b/e2e/nx-run/src/run.test.ts index fbe87f7e8b6f61..9c206679fc3ae3 100644 --- a/e2e/nx-run/src/run.test.ts +++ b/e2e/nx-run/src/run.test.ts @@ -275,12 +275,14 @@ describe('Nx Running Tests', () => { JSON.stringify({ name: lib, targets: { - target: {}, + [target]: {}, }, }) ); - expect(runCLI(`${target} ${lib}`)).toContain(`Hello from ${target}`); + expect(runCLI(`${target} ${lib} --verbose`)).toContain( + `Hello from ${target}` + ); }); it('should be able to pull options from targetDefaults based on executor', () => { @@ -289,7 +291,7 @@ describe('Nx Running Tests', () => { updateJson('nx.json', (nxJson) => { nxJson.targetDefaults ??= {}; - nxJson.targetDefaults[`*|nx:run-commands}`] = { + nxJson.targetDefaults[`nx:run-commands`] = { options: { command: `echo Hello from ${target}`, }, @@ -302,14 +304,16 @@ describe('Nx Running Tests', () => { JSON.stringify({ name: lib, targets: { - target: { + [target]: { executor: 'nx:run-commands', }, }, }) ); - expect(runCLI(`${target} ${lib}`)).toContain(`Hello from ${target}`); + expect(runCLI(`${target} ${lib} --verbose`)).toContain( + `Hello from ${target}` + ); }); }); diff --git a/packages/nx/schemas/nx-schema.json b/packages/nx/schemas/nx-schema.json index ecc85c0795dc66..1e30d4ea878143 100644 --- a/packages/nx/schemas/nx-schema.json +++ b/packages/nx/schemas/nx-schema.json @@ -209,6 +209,26 @@ "type": "object", "description": "Target defaults", "properties": { + "executor": { + "description": "The function that Nx will invoke when you run this target", + "type": "string" + }, + "options": { + "type": "object" + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "configurations": { + "type": "object", + "description": "provides extra sets of values that will be merged into the options map", + "additionalProperties": { + "type": "object" + } + }, "inputs": { "$ref": "#/definitions/inputs" }, @@ -243,32 +263,8 @@ } ] } - }, - "outputs": { - "type": "array", - "items": { - "type": "string" - } - }, - "executor": { - "description": "The function that Nx will invoke when you run this target", - "type": "string" - }, - "options": { - "type": "object" - }, - "configurations": { - "type": "object", - "description": "provides extra sets of values that will be merged into the options map", - "additionalProperties": { - "type": "object" - } } }, - "dependentRequired": { - "configurations": ["executor"], - "options": ["executor"] - }, "additionalProperties": false } } diff --git a/packages/nx/src/config/workspaces.spec.ts b/packages/nx/src/config/workspaces.spec.ts index 5cfebe5dda2a89..f02d528357cc17 100644 --- a/packages/nx/src/config/workspaces.spec.ts +++ b/packages/nx/src/config/workspaces.spec.ts @@ -145,52 +145,55 @@ describe('Workspaces', () => { }); describe('target defaults', () => { - const nxJson = { - targetDefaults: { - 'build|nx:run-commands': { - options: { - key: 't:e', - }, + const targetDefaults = { + 'build#nx:run-commands': { + options: { + key: 't:e', }, - '*|nx:run-commands': { - options: { - key: '*:e', - }, + }, + 'nx:run-commands': { + options: { + key: '*:e', }, - build: { - options: { - key: 't', - }, + }, + build: { + options: { + key: 't', }, }, }; - it('should prefer target|executor key', () => { + it('should prefer target#executor key', () => { expect( - readTargetDefaultsForTarget('build', nxJson, 'run-commands').options[ - 'key' - ] + readTargetDefaultsForTarget('build', targetDefaults, 'nx:run-commands') + .options['key'] ).toEqual('t:e'); }); - it('should prefer *|executor key', () => { + it('should prefer executor key', () => { expect( - readTargetDefaultsForTarget('other-target', nxJson, 'run-commands') - .options['key'] + readTargetDefaultsForTarget( + 'other-target', + targetDefaults, + 'nx:run-commands' + ).options['key'] ).toEqual('*:e'); }); it('should fallback to target key', () => { expect( - readTargetDefaultsForTarget('build', nxJson, 'other-executor').options[ - 'key' - ] + readTargetDefaultsForTarget('build', targetDefaults, 'other-executor') + .options['key'] ).toEqual('t'); }); it('should return undefined if not found', () => { expect( - readTargetDefaultsForTarget('other-target', nxJson, 'other-executor') + readTargetDefaultsForTarget( + 'other-target', + targetDefaults, + 'other-executor' + ) ).toBeNull(); }); @@ -200,11 +203,17 @@ describe('Workspaces', () => { expect( mergeTargetConfigurations( { - executor: 'target', - [property]: { - a: {}, + root: '.', + targets: { + build: { + executor: 'target', + [property]: { + a: {}, + }, + }, }, }, + 'build', { executor: 'target', [property]: { @@ -223,11 +232,17 @@ describe('Workspaces', () => { expect( mergeTargetConfigurations( { - executor: 'target', - [property]: { - a: {}, + root: '', + targets: { + build: { + executor: 'target', + [property]: { + a: {}, + }, + }, }, }, + 'build', { [property]: { b: {}, @@ -244,10 +259,16 @@ describe('Workspaces', () => { expect( mergeTargetConfigurations( { - [property]: { - a: {}, + root: '', + targets: { + build: { + [property]: { + a: {}, + }, + }, }, }, + 'build', { executor: 'target', [property]: { @@ -265,10 +286,16 @@ describe('Workspaces', () => { expect( mergeTargetConfigurations( { - [property]: { - a: {}, + root: '', + targets: { + build: { + [property]: { + a: {}, + }, + }, }, }, + 'build', { executor: 'target', [property]: { diff --git a/packages/nx/src/config/workspaces.ts b/packages/nx/src/config/workspaces.ts index 34afc9bbe06989..1460096bcf498b 100644 --- a/packages/nx/src/config/workspaces.ts +++ b/packages/nx/src/config/workspaces.ts @@ -132,14 +132,14 @@ export class Workspaces { const projectTargetDefinition = proj.targets[targetName]; const defaults = readTargetDefaultsForTarget( targetName, - nxJson, + nxJson.targetDefaults, projectTargetDefinition.executor ); if (defaults) { - const defaults = nxJson.targetDefaults[targetName]; proj.targets[targetName] = mergeTargetConfigurations( - projectTargetDefinition, + proj, + targetName, defaults ); } @@ -921,9 +921,12 @@ export function buildWorkspaceConfigurationFromGlobs( } export function mergeTargetConfigurations( - targetConfiguration: Partial, + projectConfiguration: ProjectConfiguration, + target: string, targetDefaults: TargetDefaults[string] ): TargetConfiguration { + const { targets, root } = projectConfiguration; + const targetConfiguration = targets?.[target] ?? {}; const { configurations: defaultConfigurations, options: defaultOptions, @@ -946,7 +949,7 @@ export function mergeTargetConfigurations( targetDefaults.executor === targetConfiguration.executor ) { result.options = { - ...defaultOptions, + ...resolvePathTokensInOptions(defaultOptions, root, target), ...targetConfiguration.options, }; result.configurations = { @@ -957,28 +960,55 @@ export function mergeTargetConfigurations( return result as TargetConfiguration; } +function resolvePathTokensInOptions( + object: T, + projectRoot: string, + key: string +): T { + for (let [opt, value] of Object.entries(object ?? {})) { + if (typeof value === 'string') { + if (value.startsWith('{workspaceRoot}/')) { + value = value.replace(/^\{workspaceRoot\}\//, ''); + } + if (value.includes('{workspaceRoot}')) { + output.error({ + title: `${NX_PREFIX}: {workspaceRoot} is only valid at the beginning of an option`, + bodyLines: [`Found {workspaceRoot} in property: ${key}`], + }); + throw new Error( + '{workspaceRoot} is only valid at the beginning of an option.' + ); + } + object[opt] = value.replace('{projectRoot}', projectRoot); + } else if (typeof value === 'object' && value) { + resolvePathTokensInOptions(value, projectRoot, [key, opt].join('.')); + } + } + return object; +} + export function readTargetDefaultsForTarget( targetName: string, - nxJson: NxJsonConfiguration<'*' | string[]>, + targetDefaults: TargetDefaults, executor?: string ): TargetDefaults[string] { if (executor) { // If an executor is defined in project.json, defaults should be read // from the most specific key that matches that executor. // e.g. If executor === run-commands, and the target is named build: - // Use build|run-commands if it is present - // If not, use *|run-commands if it is present + // Use build#nx:run-commands if it is present + // If not, use nx:run-commands if it is present // If not, use build if it is present. - const separator = '|'; + const separator = '#'; const key = [ `${targetName}${separator}${executor}`, - `*${separator}${executor}`, + executor, targetName, - ].find((x) => nxJson.targetDefaults[x]); - return key ? nxJson.targetDefaults[key] : null; + ].find((x) => targetDefaults?.[x]); + return key ? targetDefaults?.[key] : null; } else { // If the executor is not defined, the only key we have is the target name. - return nxJson.targetDefaults[targetName]; + return targetDefaults?.[targetName]; } } 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 72a4d123752b6c..9e781b32d83877 100644 --- a/packages/nx/src/project-graph/build-nodes/workspace-projects.ts +++ b/packages/nx/src/project-graph/build-nodes/workspace-projects.ts @@ -11,8 +11,15 @@ import { ProjectGraphBuilder } from '../project-graph-builder'; import { PackageJson } from '../../utils/package-json'; import { readJsonFile } from '../../utils/fileutils'; import { NxJsonConfiguration } from '../../config/nx-json'; -import { TargetConfiguration } from '../../config/workspace-json-project-json'; +import { + ProjectConfiguration, + TargetConfiguration, +} from '../../config/workspace-json-project-json'; import { NX_PREFIX } from '../../utils/logger'; +import { + mergeTargetConfigurations, + readTargetDefaultsForTarget, +} from '../../config/workspaces'; export function buildWorkspaceProjectNodes( ctx: ProjectGraphProcessorContext, @@ -47,14 +54,14 @@ export function buildWorkspaceProjectNodes( } } - p.targets = normalizeProjectTargets(p.targets, nxJson.targetDefaults, key); - p.targets = mergePluginTargetsWithNxTargets( p.root, p.targets, loadNxPlugins(ctx.workspace.plugins) ); + p.targets = normalizeProjectTargets(p, nxJson.targetDefaults, key); + // TODO: remove in v16 const projectType = p.projectType === 'application' @@ -98,26 +105,27 @@ export function buildWorkspaceProjectNodes( * Apply target defaults and normalization */ function normalizeProjectTargets( - targets: Record, - defaultTargets: NxJsonConfiguration['targetDefaults'], + project: ProjectConfiguration, + targetDefaults: NxJsonConfiguration['targetDefaults'], projectName: string ) { - for (const targetName in defaultTargets) { - const target = targets?.[targetName]; - if (!target) { - continue; - } - if (defaultTargets[targetName].inputs && !target.inputs) { - target.inputs = defaultTargets[targetName].inputs; - } - if (defaultTargets[targetName].dependsOn && !target.dependsOn) { - target.dependsOn = defaultTargets[targetName].dependsOn; - } - if (defaultTargets[targetName].outputs && !target.outputs) { - target.outputs = defaultTargets[targetName].outputs; - } - } + const targets = project.targets; for (const target in targets) { + const executor = + targets[target].executor ?? targets[target].command + ? 'nx:run-commands' + : null; + + const defaults = readTargetDefaultsForTarget( + target, + targetDefaults, + executor + ); + + if (defaults) { + targets[target] = mergeTargetConfigurations(project, target, defaults); + } + const config = targets[target]; if (config.command) { if (config.executor) { @@ -127,12 +135,13 @@ function normalizeProjectTargets( } else { targets[target] = { ...targets[target], - executor: 'nx:run-commands', + executor, options: { ...config.options, command: config.command, }, }; + delete config.command; } } }