UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

458 lines (457 loc) 21.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deepClone = deepClone; exports.resolveCommandSyntacticSugar = resolveCommandSyntacticSugar; exports.mergeMetadata = mergeMetadata; exports.mergeTargetConfigurations = mergeTargetConfigurations; exports.isCompatibleTarget = isCompatibleTarget; exports.resolveNxTokensInOptions = resolveNxTokensInOptions; const logger_1 = require("../../../utils/logger"); const source_maps_1 = require("./source-maps"); const utils_1 = require("./utils"); function deepClone(obj) { return structuredClone(obj); } function resolveCommandSyntacticSugar(target, key) { const { command, ...config } = target ?? {}; if (!command) { return target; } if (config.executor) { throw new Error(`${logger_1.NX_PREFIX} Project at ${key} should not have executor and command both configured.`); } else { return { ...config, executor: 'nx:run-commands', options: { ...config.options, command: command, }, }; } } function mergeMetadata(sourceMap, sourceInformation, baseSourceMapPath, metadata, matchingMetadata) { const result = { ...(matchingMetadata ?? {}), }; for (const [metadataKey, value] of Object.entries(metadata)) { const existingValue = matchingMetadata?.[metadataKey]; if (Array.isArray(value) && Array.isArray(existingValue)) { const startIndex = result[metadataKey].length; result[metadataKey].push(...value); if (sourceMap) { (0, source_maps_1.recordSourceMapKeysByIndex)(sourceMap, `${baseSourceMapPath}.${metadataKey}`, result[metadataKey], sourceInformation, startIndex); } } else if (Array.isArray(value) && existingValue === undefined) { result[metadataKey] ??= value; if (sourceMap) { sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation; (0, source_maps_1.recordSourceMapKeysByIndex)(sourceMap, `${baseSourceMapPath}.${metadataKey}`, value, sourceInformation); } } else if (typeof value === 'object' && typeof existingValue === 'object') { for (const key in value) { const existingValue = matchingMetadata?.[metadataKey]?.[key]; if (Array.isArray(value[key]) && Array.isArray(existingValue)) { const startIndex = result[metadataKey][key].length; result[metadataKey][key].push(...value[key]); if (sourceMap) { (0, source_maps_1.recordSourceMapKeysByIndex)(sourceMap, `${baseSourceMapPath}.${metadataKey}.${key}`, result[metadataKey][key], sourceInformation, startIndex); } } else { result[metadataKey][key] = value[key]; if (sourceMap) { sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation; } } } } else { result[metadataKey] = value; if (sourceMap) { sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation; if (typeof value === 'object') { for (const k in value) { sourceMap[`${baseSourceMapPath}.${metadataKey}.${k}`] = sourceInformation; if (Array.isArray(value[k])) { (0, source_maps_1.recordSourceMapKeysByIndex)(sourceMap, `${baseSourceMapPath}.${metadataKey}.${k}`, value[k], sourceInformation); } } } } } } return result; } function mergeOptions(newOptions, baseOptions, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase) { // `'...'` at the options level uses object-spread semantics. if (newOptions?.[utils_1.NX_SPREAD_TOKEN] === true) { return (0, utils_1.getMergeValueResult)(baseOptions, newOptions, projectConfigSourceMap ? { sourceMap: projectConfigSourceMap, key: `${targetIdentifier}.options`, sourceInformation, } : undefined, deferSpreadsWithoutBase); } const mergedOptionKeys = (0, utils_1.uniqueKeysInObjects)(baseOptions ?? {}, newOptions ?? {}); const mergedOptions = {}; for (const optionKey of mergedOptionKeys) { mergedOptions[optionKey] = (0, utils_1.getMergeValueResult)(baseOptions ? baseOptions[optionKey] : undefined, newOptions ? newOptions[optionKey] : undefined, projectConfigSourceMap ? { sourceMap: projectConfigSourceMap, key: `${targetIdentifier}.options.${optionKey}`, sourceInformation, } : undefined, deferSpreadsWithoutBase); } return mergedOptions; } // Merges a single named configuration, keyed under its own identifier // (e.g. `targets.build.configurations.prod`) rather than under `.options`. // Source-map correctness for the spread case is handled inside // `getMergeValueResult`'s object-spread path — no post-merge fix-up needed. function mergeConfigurationValue(newConfig, baseConfig, projectConfigSourceMap, sourceInformation, configIdentifier, deferSpreadsWithoutBase) { if (newConfig?.[utils_1.NX_SPREAD_TOKEN] === true) { return (0, utils_1.getMergeValueResult)(baseConfig, newConfig, projectConfigSourceMap && configIdentifier ? { sourceMap: projectConfigSourceMap, key: configIdentifier, sourceInformation, } : undefined, deferSpreadsWithoutBase); } const mergedKeys = (0, utils_1.uniqueKeysInObjects)(baseConfig ?? {}, newConfig ?? {}); const merged = {}; for (const key of mergedKeys) { merged[key] = (0, utils_1.getMergeValueResult)(baseConfig ? baseConfig[key] : undefined, newConfig ? newConfig[key] : undefined, projectConfigSourceMap && configIdentifier ? { sourceMap: projectConfigSourceMap, key: `${configIdentifier}.${key}`, sourceInformation, } : undefined, deferSpreadsWithoutBase); } return merged; } function mergeConfigurations(newConfigurations, baseConfigurations, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase) { const mergedConfigurations = {}; // Keys before '...' let base win for shared names; keys after '...' // (or when there's no spread) merge normally with new winning. const newKeys = Object.keys(newConfigurations ?? {}); const spreadPosInNew = newKeys.indexOf(utils_1.NX_SPREAD_TOKEN); const hasSpread = spreadPosInNew >= 0; const keysBeforeSpread = hasSpread ? new Set(newKeys.slice(0, spreadPosInNew)) : new Set(); // Integer-like keys get hoisted to newKeys[0], making their position // relative to '...' unrecoverable. if (hasSpread && newKeys[0] && utils_1.INTEGER_LIKE_KEY_PATTERN.test(newKeys[0])) { throw new utils_1.IntegerLikeSpreadKeyError(newKeys[0], targetIdentifier ? `Configurations at "${targetIdentifier}.configurations"` : 'Configurations'); } // Preserving the unresolved `'...'` sentinel in authored position lets // a later merge layer (which actually has a base) classify the keys as // pre/post-spread correctly. const preserveSpreadSentinel = hasSpread && deferSpreadsWithoutBase && baseConfigurations === undefined; const processConfigName = (configName) => { const configIdentifier = targetIdentifier ? `${targetIdentifier}.configurations.${configName}` : undefined; const baseHasConfig = configName in (baseConfigurations ?? {}); const newHasConfig = !!newConfigurations && configName in newConfigurations; if (hasSpread && keysBeforeSpread.has(configName)) { // Before '...': base wins for shared names. Keep base's source-map // entries when it owns the config. if (baseHasConfig) { mergedConfigurations[configName] = baseConfigurations[configName]; } else { mergedConfigurations[configName] = mergeConfigurationValue(newConfigurations?.[configName], undefined, projectConfigSourceMap, sourceInformation, configIdentifier, deferSpreadsWithoutBase); if (projectConfigSourceMap && configIdentifier) { projectConfigSourceMap[configIdentifier] = sourceInformation; } } return; } mergedConfigurations[configName] = mergeConfigurationValue(newConfigurations?.[configName], baseConfigurations?.[configName], projectConfigSourceMap, sourceInformation, configIdentifier, deferSpreadsWithoutBase); // Only reattribute the config name when the new plugin introduced it. if (projectConfigSourceMap && configIdentifier && newHasConfig && !baseHasConfig) { projectConfigSourceMap[configIdentifier] = sourceInformation; } }; if (hasSpread) { // Authored positions of new's own keys relative to `'...'` drive // pre/post-spread classification, so those keys go in authored order. // Base-only keys land right before `'...'` — they weren't authored by // the new layer, so default semantics places them with the pre-spread // keys (the "base layer" slot). const baseOnlyKeys = baseConfigurations ? Object.keys(baseConfigurations).filter((k) => k !== utils_1.NX_SPREAD_TOKEN && !(k in (newConfigurations ?? {}))) : []; let baseOnlyInserted = false; const insertBaseOnlyKeys = () => { if (baseOnlyInserted) return; baseOnlyInserted = true; for (const configName of baseOnlyKeys) processConfigName(configName); }; for (const configName of newKeys) { if (configName === utils_1.NX_SPREAD_TOKEN) { insertBaseOnlyKeys(); if (preserveSpreadSentinel) { mergedConfigurations[utils_1.NX_SPREAD_TOKEN] = true; } continue; } processConfigName(configName); } insertBaseOnlyKeys(); } else { // No spread — classic `{ ...base, ...new }` ordering: base keys // first, new-only keys after. Shared configs stay at base's position. if (baseConfigurations) { for (const configName of Object.keys(baseConfigurations)) { if (configName === utils_1.NX_SPREAD_TOKEN) continue; processConfigName(configName); } } for (const configName of newKeys) { if (configName === utils_1.NX_SPREAD_TOKEN) continue; if (configName in mergedConfigurations) continue; processConfigName(configName); } } return mergedConfigurations; } /** * Merges two targets. * * Most properties from `target` will overwrite any properties from `baseTarget`. * Options and configurations are treated differently - they are merged together if the executor definition is compatible. * * @param target The target definition with higher priority * @param baseTarget The target definition that should be overwritten. Can be undefined, in which case the target is returned as-is. * @param projectConfigSourceMap The source map to be filled with metadata about where each property came from * @param sourceInformation The metadata about where the new target was defined * @param targetIdentifier The identifier for the target to merge, used for source map * @returns A merged target configuration */ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase) { const { configurations: defaultConfigurations, options: defaultOptions, ...baseTargetProperties } = baseTarget ?? {}; // Target is "compatible", e.g. executor is defined only once or is the same // in both places. This means that it is likely safe to merge const isCompatible = isCompatibleTarget(baseTarget ?? {}, target); if (!isCompatible && projectConfigSourceMap) { // if the target is not compatible, we will simply override the options // we have to delete old entries from the source map for (const key in projectConfigSourceMap) { if (key.startsWith(`${targetIdentifier}`)) { delete projectConfigSourceMap[key]; } } } // merge top level properties if they're compatible const result = {}; const mergeBase = isCompatible ? baseTargetProperties : {}; // Keys before '...' let base win; keys after '...' (or when there's no // spread) merge normally with target winning. const targetKeys = Object.keys(target); const spreadPosInTarget = targetKeys.indexOf(utils_1.NX_SPREAD_TOKEN); const hasSpread = isCompatible && spreadPosInTarget >= 0; const keysBeforeSpread = hasSpread ? new Set(targetKeys.slice(0, spreadPosInTarget)) : new Set(); // Integer-like keys get hoisted to targetKeys[0], making their position // relative to '...' unrecoverable. if (hasSpread && targetKeys[0] && utils_1.INTEGER_LIKE_KEY_PATTERN.test(targetKeys[0])) { throw new utils_1.IntegerLikeSpreadKeyError(targetKeys[0], targetIdentifier ? `Target at "${targetIdentifier}"` : 'Target'); } // Preserving the unresolved `'...'` sentinel in authored position lets a // later merge layer (which actually has a base) classify sibling keys as // pre/post-spread correctly. const preserveSpreadSentinel = spreadPosInTarget >= 0 && deferSpreadsWithoutBase && baseTarget === undefined; const skipForOwnMerge = new Set([ 'options', 'configurations', utils_1.NX_SPREAD_TOKEN, ]); const processKey = (key) => { if (skipForOwnMerge.has(key)) return; if (hasSpread && keysBeforeSpread.has(key)) { // Before '...': base wins; fall through to target only if base lacks it. result[key] = key in mergeBase ? mergeBase[key] : (0, utils_1.getMergeValueResult)(undefined, target[key], projectConfigSourceMap ? { sourceMap: projectConfigSourceMap, key: `${targetIdentifier}.${key}`, sourceInformation, } : undefined, deferSpreadsWithoutBase); return; } if (key in target) { result[key] = (0, utils_1.getMergeValueResult)(mergeBase[key], target[key], projectConfigSourceMap ? { sourceMap: projectConfigSourceMap, key: `${targetIdentifier}.${key}`, sourceInformation, } : undefined, deferSpreadsWithoutBase); } else { result[key] = mergeBase[key]; } }; if (isCompatible) { if (hasSpread) { // Authored positions of the target's own keys relative to `'...'` // drive pre/post-spread classification, so those keys go in // authored order. Base-only keys land right before `'...'` — they // weren't authored, so default semantics ("base layer that yields // to a higher-priority layer") places them with the rest of the // pre-spread keys. const baseOnlyKeys = Object.keys(baseTargetProperties).filter((k) => !skipForOwnMerge.has(k) && !(k in target)); let baseOnlyInserted = false; const insertBaseOnlyKeys = () => { if (baseOnlyInserted) return; baseOnlyInserted = true; for (const key of baseOnlyKeys) processKey(key); }; for (const key of targetKeys) { if (key === utils_1.NX_SPREAD_TOKEN) { insertBaseOnlyKeys(); if (preserveSpreadSentinel) { result[utils_1.NX_SPREAD_TOKEN] = true; } continue; } if (skipForOwnMerge.has(key)) continue; processKey(key); } // Safety for a sentinel-less iteration (shouldn't happen when // hasSpread is true, but keeps the base-only keys emitted). insertBaseOnlyKeys(); } else { // No spread — classic `{ ...base, ...target }` ordering: base keys // first (preserving their own-key order), target-only keys after. // Shared keys stay at base's position with per-key merged value. const mergedKeys = (0, utils_1.uniqueKeysInObjects)(baseTargetProperties, target); for (const key of mergedKeys) { if (skipForOwnMerge.has(key)) continue; processKey(key); } } } else { for (const key of targetKeys) { if (skipForOwnMerge.has(key)) continue; processKey(key); } } // Update source map once after loop if (projectConfigSourceMap) { projectConfigSourceMap[targetIdentifier] = sourceInformation; } // merge options if there are any // if the targets aren't compatible, we simply discard the old options during the merge if (target.options || defaultOptions) { result.options = mergeOptions(target.options, isCompatible ? defaultOptions : undefined, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase); if (projectConfigSourceMap && target.options) { projectConfigSourceMap[`${targetIdentifier}.options`] = sourceInformation; } } // merge configurations if there are any // if the targets aren't compatible, we simply discard the old configurations during the merge if (target.configurations || defaultConfigurations) { result.configurations = mergeConfigurations(target.configurations, isCompatible ? defaultConfigurations : undefined, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase); if (projectConfigSourceMap && target.configurations) { projectConfigSourceMap[`${targetIdentifier}.configurations`] = sourceInformation; } } if (target.metadata) { result.metadata = mergeMetadata(projectConfigSourceMap, sourceInformation, `${targetIdentifier}.metadata`, target.metadata, baseTarget?.metadata); } return result; } /** * Checks if targets options are compatible - used when merging configurations * to avoid merging options for @nx/js:tsc into something like @nx/webpack:webpack. * * If the executors are both specified and don't match, the options aren't considered * "compatible" and shouldn't be merged. */ function isCompatibleTarget(a, b) { const oneHasNoExecutor = !a.executor || !b.executor; const bothHaveSameExecutor = a.executor === b.executor; if (oneHasNoExecutor) return true; if (!bothHaveSameExecutor) return false; const isRunCommands = a.executor === 'nx:run-commands'; if (isRunCommands) { const aCommand = a.options?.command ?? a.options?.commands?.join(' && '); const bCommand = b.options?.command ?? b.options?.commands?.join(' && '); const oneHasNoCommand = !aCommand || !bCommand; const hasSameCommand = aCommand === bCommand; return oneHasNoCommand || hasSameCommand; } const isRunScript = a.executor === 'nx:run-script'; if (isRunScript) { const aScript = a.options?.script; const bScript = b.options?.script; const oneHasNoScript = !aScript || !bScript; const hasSameScript = aScript === bScript; return oneHasNoScript || hasSameScript; } return true; } function resolveNxTokensInOptions(object, project, key) { const result = Array.isArray(object) ? [...object] : { ...object }; for (let [opt, value] of Object.entries(object ?? {})) { if (typeof value === 'string') { const workspaceRootMatch = /^(\{workspaceRoot\}\/?)/.exec(value); if (workspaceRootMatch?.length) { value = value.replace(workspaceRootMatch[0], ''); } if (value.includes('{workspaceRoot}')) { throw new Error(`${logger_1.NX_PREFIX} The {workspaceRoot} token is only valid at the beginning of an option. (${key})`); } value = value.replace(/\{projectRoot\}/g, project.root); result[opt] = value.replace(/\{projectName\}/g, project.name); } else if (typeof value === 'object' && value) { result[opt] = resolveNxTokensInOptions(value, project, [key, opt].join('.')); } } return result; }