UNPKG

nx

Version:

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

251 lines (250 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = update; const executor_utils_1 = require("../../command-line/run/executor-utils"); const format_changed_files_1 = require("../../generators/internal-utils/format-changed-files"); const json_1 = require("../../generators/utils/json"); const project_configuration_1 = require("../../generators/utils/project-configuration"); const path_1 = require("../../utils/path"); // TODO(v24): remove this migration alongside the runtime fallback it exists to // retire (`isLegacyCachedTarget` in `target-normalization.ts`), along with its // registration in `packages/nx/migrations.json`. /** * Target defaults resolve to a single key rather than merging, so an executor * key hides the target name key entirely. Before Nx 23 a hidden `cache: true` * still took effect, because cacheability was also derived from target *names* * via `cacheableOperations`; Nx 23 removed that derivation and those targets * silently stopped being cacheable. * * Write the intent into the executor key so it no longer depends on the * deprecated fallback that restores the old behavior at run time. */ async function update(tree) { const nxJson = (0, project_configuration_1.readNxJson)(tree); const targetDefaults = nxJson?.targetDefaults; if (!targetDefaults) { return; } const projects = (0, project_configuration_1.getProjects)(tree); const projectsMap = {}; // `cache` on an executor key reaches every target that resolves through it, // not just the one whose target name key enables caching. Collect them all // per key so the decision below can be made for the key as a whole. The // collection is a list rather than a map keyed by target name: two projects // routinely declare the same target name through the same executor, and only // one of them may be continuous. const targetsByExecutorKey = new Map(); const collect = (targetName, target) => { // `hasOwnProperty` rather than a lookup: an executor named `__proto__` or // `constructor` resolves through the prototype chain to a truthy object, so // a plain lookup would collect the target and stamp `Object.prototype`. if (!target?.executor || !Object.prototype.hasOwnProperty.call(targetDefaults, target.executor)) { return; } const targets = targetsByExecutorKey.get(target.executor) ?? []; targets.push([targetName, target]); targetsByExecutorKey.set(target.executor, targets); }; for (const [projectName, project] of projects) { projectsMap[projectName] = project; for (const [targetName, target] of Object.entries(project.targets ?? {})) { collect(targetName, target); } for (const [targetName, target] of Object.entries(packageJsonTargets(tree, project.root))) { collect(targetName, target); } } let changed = false; for (const [key, targets] of targetsByExecutorKey) { if (!canEnableCache(targetDefaults, key, targets, tree.root, projectsMap)) { continue; } catchAllConfig(targetDefaults[key]).cache = true; changed = true; } if (!changed) { return; } (0, project_configuration_1.updateNxJson)(tree, nxJson); await (0, format_changed_files_1.formatChangedFiles)(tree); } /** * Whether `cache: true` can be written onto `key` without changing behavior for * any target that resolves through it. * * The executor key outranks every target name key, so stamping it caches all of * `targets` — including any that is continuous, which makes `nx.json` fail graph * construction outright. The key is only safe when every target through it * independently wants caching and none of them can be continuous. Anything else * is left to the runtime fallback, which decides per target after normalization * and can therefore see continuity this migration cannot. */ function canEnableCache(targetDefaults, key, targets, workspaceRoot, projectsMap) { // Targets written with `command` are rewritten to `nx:run-commands` and // `package.json` scripts to `nx:run-script` before key selection, so neither // names the key it resolves through. Plugins also infer continuous targets // (`watch-deps`, dev servers) into projects whose config never mentions them. // The target list here can never be trusted to be complete for these keys. if (key === 'nx:run-commands' || key === 'nx:run-script') { return false; } // Already decided; never override the user's value. A filtered entry counts // as decided because its value can't be evaluated without project context. if (declaresAnyCache(targetDefaults[key])) { return false; } // `continuous` is a valid field on a target default, and this key applies it // to every target that resolves through it. Adding `cache` would make each of // them both cacheable and continuous, which fails graph construction — so no // target list can make this key safe. if (declaresAnyContinuous(targetDefaults[key])) { return false; } // Only an unfiltered entry can be amended. Appending a catch-all to an // all-filtered key would make it match targets that previously fell through // to the target name key, silently dropping that key's `dependsOn`/`inputs`. if (!catchAllConfig(targetDefaults[key])) { return false; } // Continuity declared by the executor's schema is resolved at graph // construction for every target through this key, so one lookup covers them // all. For what an executor that cannot be resolved here costs, see // {@link executorDeclaresContinuous}. if (executorDeclaresContinuous(key, workspaceRoot, projectsMap)) { return false; } for (const [targetName, target] of targets) { if (!declaresCacheTrue(targetDefaults[targetName])) { return false; } if (target.continuous || isLongRunningTargetName(targetName)) { return false; } } return true; } /** * Whether the target name key declares `cache: true` on an entry that always * applies. Mirrors `declaresCacheTrue` in `target-normalization.ts`: a filtered * entry declaring `cache` makes the value unknowable without project context, * so nothing is claimed. Among unfiltered entries the last wins. */ function declaresCacheTrue(value) { if (!value) { return false; } let declared; for (const entry of configEntries(value)) { if (entry.cache === undefined) continue; if (entry.filter) return false; declared = entry.cache; } return declared === true; } /** * Whether any entry declares `cache`, filtered or not — i.e. whether the value * is already decided and must be left alone. Deliberately broader than * {@link declaresCacheTrue}: a filtered `cache` blocks here rather than being * read past, so the two never disagree about a filtered entry. */ function declaresAnyCache(value) { if (!value) { return false; } return configEntries(value).some((entry) => entry.cache !== undefined); } /** * Whether any entry declares `continuous: true`. Filtered entries count: which * projects the filter matches is unknowable here, and the only case that * matters is the one where it matches. */ function declaresAnyContinuous(value) { if (!value) { return false; } return configEntries(value).some((entry) => entry.continuous === true); } /** * The config blocks of a `targetDefaults` value. Entries that are not config * objects are dropped rather than inspected: `nx.json` is hand-edited, and a * `null` or scalar entry would otherwise throw while reading `.cache` off it. */ function configEntries(value) { const entries = Array.isArray(value) ? value : [value]; return entries.filter((entry) => !!entry && typeof entry === 'object'); } /** * The targets a project's `package.json` contributes to the graph. * * {@link getProjects} builds a root's configuration from its `project.json` * alone whenever one exists, but the package.json plugin still creates the * sibling `package.json`'s targets — it accepts any `package.json` that is * either in the package manager workspaces or next to a `project.json` * (`plugins/package-json/create-nodes.ts`). Those targets resolve through the * same executor keys, so they belong in this decision. * * Targets derived from `scripts` are still not covered; they resolve through * `nx:run-script`, which is never stamped for exactly that reason. */ function packageJsonTargets(tree, projectRoot) { const packageJsonPath = (0, path_1.joinPathFragments)(projectRoot, 'package.json'); if (!tree.exists(packageJsonPath)) { return {}; } // A parse failure is left to throw: `readJson` names the offending file, // which is the one thing a user can act on. Shapes that parse but aren't the // one this reads contribute no targets rather than crashing the run. const targets = (0, json_1.readJson)(tree, packageJsonPath)?.nx?.targets; if (!targets || typeof targets !== 'object' || Array.isArray(targets)) { return {}; } return targets; } /** * The unfiltered config block of a `targetDefaults` value, or undefined when the * array form carries only filtered entries and when the value is not a config * object at all. Never creates one — see {@link canEnableCache}. */ function catchAllConfig(value) { // The filter check applies to both spellings: the object and single-element // array forms of the same config have to reach the same verdict. return configEntries(value).find((entry) => entry.filter === undefined); } /** * Whether the executor's schema marks its targets continuous, resolved through * the same `getExecutorInformation` call `normalizeTarget` makes at graph * construction, so the two cannot drift on what continuous means. * * An executor that cannot be resolved is read as not continuous. That is the * same answer `normalizeTarget` gives, but not the same trade: its lookup is * repeated on every graph construction, so a failure there corrects itself, * whereas this one decides a permanent edit to `nx.json`. An executor that is * unresolvable while the migration runs and resolvable afterwards therefore * keeps its stamped `cache` while the runtime starts marking its targets * continuous — which `normalizeTargets` rejects with a `WorkspaceValidityError`, * failing graph construction until `nx.json` is edited by hand. */ function executorDeclaresContinuous(executor, workspaceRoot, projectsMap) { try { const [nodeModule, name] = (0, executor_utils_1.parseExecutor)(executor); const { schema } = (0, executor_utils_1.getExecutorInformation)(nodeModule, name, workspaceRoot, projectsMap); return !!schema.continuous; } catch { return false; } } /** * Target names the pre-23 `longRunningTask` guard excluded from caching. * Mirrors `isLongRunningTarget` in `target-normalization.ts`. */ function isLongRunningTargetName(targetName) { return (targetName.endsWith(':watch') || targetName.endsWith('-watch') || targetName === 'serve' || targetName === 'dev' || targetName === 'start'); }