🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

nx

Package Overview
Dependencies
Maintainers
8
Versions
2646
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nx - npm Package Compare versions

Comparing version
23.1.0
to
23.1.1
+5
dist/src/ai/configure-ai-agents-disclaimer.d.ts
import { AgentStatusInfo } from '../daemon/message-types/configure-ai-agents';
/**
* Whether to show the "configure-ai-agents is outdated" banner after a task run.
*/
export declare function shouldPrintConfigureAiAgentsDisclaimer(outdatedAgents: AgentStatusInfo[], workspaceRoot: string): boolean;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.shouldPrintConfigureAiAgentsDisclaimer = shouldPrintConfigureAiAgentsDisclaimer;
const fs_1 = require("fs");
const detect_ai_agent_1 = require("./detect-ai-agent");
const constants_1 = require("./constants");
/**
* Whether to show the "configure-ai-agents is outdated" banner after a task run.
*/
function shouldPrintConfigureAiAgentsDisclaimer(outdatedAgents, workspaceRoot) {
if (outdatedAgents.length === 0) {
return false;
}
const detectedAgent = (0, detect_ai_agent_1.detectAiAgent)();
if (detectedAgent) {
return outdatedAgents.some((agent) => agent.name === detectedAgent);
}
// Unsupported agents (e.g. qwen) cannot be configured via `nx configure-ai-agents`.
// If the repo already has Nx rules in AGENTS.md, skip the misleading warning.
const agentsMd = (0, constants_1.agentsMdPath)(workspaceRoot);
if (!(0, fs_1.existsSync)(agentsMd)) {
return true;
}
try {
const content = (0, fs_1.readFileSync)(agentsMd, 'utf-8');
return !constants_1.rulesRegex.test(content);
}
catch {
return true;
}
}
import { MigrationsJson } from '../../config/misc-interfaces';
import { FileChange } from '../../generators/tree';
import { ArrayPackageGroup, NxMigrationsConfiguration, PackageJson } from '../../utils/package-json';
interface PackageMigrationConfig extends NxMigrationsConfiguration {
packageJson: PackageJson;
packageGroup: ArrayPackageGroup;
}
export declare function readPackageMigrationConfig(packageName: string, dir: string): PackageMigrationConfig;
export declare function runInstall(nxWorkspaceRoot?: string, phase?: MigrationInstallPhase): Promise<void>;
export type MigrationInstallPhase = 'pre-migration' | 'post-migration';
export declare class NpmPeerDepsInstallError extends Error {
constructor();
}
/**
* Detects npm peer-dependency resolution failures. Keyed on the `ERESOLVE`
* error code, which npm consistently emits for this class of failure across
* v7+ (`npm ERR! code ERESOLVE` / `npm error code ERESOLVE`). Falls back to a
* small set of stable phrases in case the code line is missing from the
* captured output.
*/
export declare function isNpmPeerDepsError(stderr: string): boolean;
export declare function logNpmPeerDepsError(phase: MigrationInstallPhase): void;
export declare class ChangedDepInstaller {
private readonly root;
private readonly shouldSkipInstall;
private initialDeps;
private _skippedInstall;
constructor(root: string, shouldSkipInstall?: boolean);
get skippedInstall(): boolean;
installDepsIfChanged(): Promise<void>;
}
export declare function runNxOrAngularMigration(root: string, migration: {
package: string;
name: string;
description?: string;
version: string;
}, isVerbose: boolean, captureGeneratorOutput?: boolean, resolvedCollection?: {
collection: MigrationsJson;
collectionPath: string;
}): Promise<{
changes: FileChange[];
nextSteps: string[];
agentContext: string[];
logs: string;
madeChanges: boolean;
}>;
export declare function getStringifiedPackageJsonDeps(root: string): string;
export declare function runNxMigration(root: string, collectionPath: string, collection: MigrationsJson, name: string, migrationVersion: string | undefined, captureGeneratorOutput: boolean): Promise<{
changes: FileChange[];
nextSteps: string[];
agentContext: string[];
logs: string;
}>;
export declare function parseMigrationReturn(value: unknown): {
nextSteps: string[];
agentContext: string[];
};
export declare function filterStrings(value: unknown): string[];
export declare function readMigrationCollection(packageName: string, root: string): {
collection: MigrationsJson;
collectionPath: string;
};
export declare function getImplementationPath(collection: MigrationsJson, collectionPath: string, name: string, migrationVersion?: string): {
path: string;
fnSymbol: string;
};
export declare class MigrationImplementationMissingError extends Error {
constructor(baseMessage: string, collectionPath: string, migrationVersion: string | undefined);
}
export declare function isAngularMigration(collection: MigrationsJson, name: string): import("../../config/misc-interfaces").MigrationsJsonEntry;
export declare const getNgCompatLayer: () => Promise<typeof import("../../adapter/ngcli-adapter")>;
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNgCompatLayer = exports.MigrationImplementationMissingError = exports.ChangedDepInstaller = exports.NpmPeerDepsInstallError = void 0;
exports.readPackageMigrationConfig = readPackageMigrationConfig;
exports.runInstall = runInstall;
exports.isNpmPeerDepsError = isNpmPeerDepsError;
exports.logNpmPeerDepsError = logNpmPeerDepsError;
exports.runNxOrAngularMigration = runNxOrAngularMigration;
exports.getStringifiedPackageJsonDeps = getStringifiedPackageJsonDeps;
exports.runNxMigration = runNxMigration;
exports.parseMigrationReturn = parseMigrationReturn;
exports.filterStrings = filterStrings;
exports.readMigrationCollection = readMigrationCollection;
exports.getImplementationPath = getImplementationPath;
exports.isAngularMigration = isAngularMigration;
const tslib_1 = require("tslib");
const pc = tslib_1.__importStar(require("picocolors"));
const child_process_1 = require("child_process");
const path_1 = require("path");
const semver_1 = require("semver");
const handle_import_1 = require("../../utils/handle-import");
const tree_1 = require("../../generators/tree");
const fileutils_1 = require("../../utils/fileutils");
const logger_1 = require("../../utils/logger");
const package_json_1 = require("../../utils/package-json");
const package_manager_1 = require("../../utils/package-manager");
const output_1 = require("../../utils/output");
const fs_1 = require("fs");
const installation_directory_1 = require("../../utils/installation-directory");
const project_graph_1 = require("../../project-graph/project-graph");
const version_utils_1 = require("./version-utils");
function readPackageMigrationConfig(packageName, dir) {
const { path: packageJsonPath, packageJson: json } = (0, package_json_1.readModulePackageJson)(packageName, (0, installation_directory_1.getNxRequirePaths)(dir));
const config = (0, package_json_1.readNxMigrateConfig)(json);
if (!config) {
return { packageJson: json, migrations: null, packageGroup: [] };
}
try {
const migrationFile = require.resolve(config.migrations, {
paths: [(0, path_1.dirname)(packageJsonPath)],
});
return {
packageJson: json,
migrations: migrationFile,
packageGroup: config.packageGroup,
supportsOptionalMigrations: config.supportsOptionalMigrations,
};
}
catch {
return {
packageJson: json,
migrations: null,
packageGroup: config.packageGroup,
supportsOptionalMigrations: config.supportsOptionalMigrations,
};
}
}
function runInstall(nxWorkspaceRoot, phase = 'pre-migration') {
const cwd = nxWorkspaceRoot ?? process.cwd();
const packageManager = (0, package_manager_1.detectPackageManager)(cwd);
const pmCommands = (0, package_manager_1.getPackageManagerCommand)(packageManager, cwd);
const installCommand = `${pmCommands.install} ${pmCommands.ignoreScriptsFlag ?? ''}`;
output_1.output.log({
title: `Running '${installCommand}' to make sure necessary packages are installed`,
});
return new Promise((resolve, reject) => {
// For npm, pipe stderr so we can detect peer dependency errors while still
// mirroring it live to the user's terminal. Other package managers inherit
// stderr directly since we don't need to inspect their output.
const shouldCaptureStderr = packageManager === 'npm';
const child = (0, child_process_1.spawn)(installCommand, {
shell: true,
stdio: ['inherit', 'inherit', shouldCaptureStderr ? 'pipe' : 'inherit'],
windowsHide: true,
cwd,
});
const stderrChunks = [];
child.stderr?.on('data', (chunk) => {
process.stderr.write(chunk);
stderrChunks.push(chunk);
});
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) {
resolve();
return;
}
if (shouldCaptureStderr) {
const stderr = Buffer.concat(stderrChunks).toString().trim();
if (isNpmPeerDepsError(stderr)) {
// Log the remediation guidance here so every caller of `runInstall`
// (CLI migrate, `nx repair`, single-migration runner, etc.) surfaces
// it consistently. Top-level callers catch `NpmPeerDepsInstallError`
// and return a non-zero exit code without re-logging.
logNpmPeerDepsError(phase);
reject(new NpmPeerDepsInstallError());
return;
}
}
reject(new Error(`Command failed: ${installCommand}`));
});
});
}
class NpmPeerDepsInstallError extends Error {
constructor() {
super('npm install failed due to peer dependency conflicts.');
this.name = 'NpmPeerDepsInstallError';
}
}
exports.NpmPeerDepsInstallError = NpmPeerDepsInstallError;
/**
* Detects npm peer-dependency resolution failures. Keyed on the `ERESOLVE`
* error code, which npm consistently emits for this class of failure across
* v7+ (`npm ERR! code ERESOLVE` / `npm error code ERESOLVE`). Falls back to a
* small set of stable phrases in case the code line is missing from the
* captured output.
*/
function isNpmPeerDepsError(stderr) {
if (/\bERESOLVE\b/.test(stderr)) {
return true;
}
const lowerStderr = stderr.toLowerCase();
return (lowerStderr.includes('unable to resolve dependency tree') ||
lowerStderr.includes('could not resolve dependency') ||
lowerStderr.includes('conflicting peer dependency'));
}
function logNpmPeerDepsError(phase) {
const peerDepsResolutionSteps = [
'Recommended approaches (in order of preference):',
'',
'1. Use "overrides" in package.json to force compatible versions across the dependency tree.',
' See https://docs.npmjs.com/cli/configuring-npm/package-json#overrides',
'2. Persist legacy peer deps resolution in the project ".npmrc":',
' npm config set legacy-peer-deps=true --location=project',
' (bypasses peer dependency resolution; use with caution)',
'3. As a last resort, force the installation by running "npm install --force".',
' (does not persist and may produce broken installs)',
];
const manualInstallHint = [
'If you installed the dependencies manually, pass "--skip-install" to avoid re-installing them:',
' nx migrate --run-migrations --skip-install',
];
if (phase === 'pre-migration') {
output_1.output.error({
title: 'You need to resolve the peer dependency conflicts before the migration can continue',
bodyLines: [
...peerDepsResolutionSteps,
'',
'Once the conflicts are resolved, re-run the migrations:',
' nx migrate --run-migrations',
'',
...manualInstallHint,
],
});
}
else {
output_1.output.error({
title: 'Some migrations have been applied, but installing the updated dependencies failed',
bodyLines: [
...peerDepsResolutionSteps,
'',
'Once the conflicts are resolved, run "npm install" to install the updated dependencies.',
'If the migration was interrupted before completing, re-run the remaining migrations:',
' nx migrate --run-migrations',
'',
...manualInstallHint,
],
});
}
}
class ChangedDepInstaller {
constructor(root, shouldSkipInstall = false) {
this.root = root;
this.shouldSkipInstall = shouldSkipInstall;
this._skippedInstall = false;
this.initialDeps = getStringifiedPackageJsonDeps(root);
}
get skippedInstall() {
return this._skippedInstall;
}
async installDepsIfChanged() {
const currentDeps = getStringifiedPackageJsonDeps(this.root);
if (this.initialDeps !== currentDeps) {
if (this.shouldSkipInstall) {
this._skippedInstall = true;
}
else {
await runInstall(this.root, 'post-migration');
}
}
this.initialDeps = currentDeps;
}
}
exports.ChangedDepInstaller = ChangedDepInstaller;
async function runNxOrAngularMigration(root, migration, isVerbose, captureGeneratorOutput = false, resolvedCollection) {
const { collection, collectionPath } = resolvedCollection ?? readMigrationCollection(migration.package, root);
let changes = [];
let nextSteps = [];
let agentContext = [];
let logs = '';
// Angular's `ngResult.changes` is synthesized from the schematic's
// DryRunEvent stream so Nx and Angular paths can share commit/validation
// gating via `changes.length > 0`.
let madeChanges = false;
logger_1.logger.info(pc.dim('→ Running generator…'));
if (!isAngularMigration(collection, migration.name)) {
({ nextSteps, changes, agentContext, logs } = await runNxMigration(root, collectionPath, collection, migration.name, migration.version, captureGeneratorOutput));
madeChanges = changes.length > 0;
logger_1.logger.info(`Ran ${migration.name} from ${migration.package}`);
if (migration.description) {
logger_1.logger.info(` ${migration.description}`);
}
logger_1.logger.info('');
if (!madeChanges) {
logger_1.logger.info(`No changes were made\n`);
return { changes, nextSteps, agentContext, logs, madeChanges };
}
logger_1.logger.info('Changes:');
(0, tree_1.printChanges)(changes, ' ');
logger_1.logger.info('');
}
else {
const ngCliAdapter = await (0, exports.getNgCompatLayer)();
const migrationProjectGraph = await (0, project_graph_1.createProjectGraphAsync)();
const ngResult = await ngCliAdapter.runMigration(root, migration.package, migration.name, (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(migrationProjectGraph).projects, isVerbose, migrationProjectGraph);
changes = ngResult.changes;
madeChanges = ngResult.madeChanges;
logs = ngResult.loggingQueue.join('\n');
logger_1.logger.info(`Ran ${migration.name} from ${migration.package}`);
if (migration.description) {
logger_1.logger.info(` ${migration.description}`);
}
logger_1.logger.info('');
if (!madeChanges) {
logger_1.logger.info(`No changes were made\n`);
return { changes, nextSteps, agentContext, logs, madeChanges };
}
logger_1.logger.info('Changes:');
ngResult.loggingQueue.forEach((log) => logger_1.logger.info(' ' + log));
logger_1.logger.info('');
}
return { changes, nextSteps, agentContext, logs, madeChanges };
}
function getStringifiedPackageJsonDeps(root) {
try {
const { dependencies, devDependencies } = (0, fileutils_1.readJsonFile)((0, path_1.join)(root, 'package.json'));
return JSON.stringify([dependencies, devDependencies]);
}
catch {
// We don't really care if the .nx/installation property changes,
// whenever nxw is invoked it will handle the dep updates.
return '';
}
}
async function runNxMigration(root, collectionPath, collection, name, migrationVersion, captureGeneratorOutput) {
const { path: implPath, fnSymbol } = getImplementationPath(collection, collectionPath, name, migrationVersion);
const fn = require(implPath)[fnSymbol];
const host = new tree_1.FsTree(root, process.env.NX_VERBOSE_LOGGING === 'true', `migration ${collection.name}:${name}`);
let result;
let logs = '';
if (captureGeneratorOutput) {
const { withGeneratorOutputCapture } = require('./agentic/capture-generator-output');
({ result, logs } = await withGeneratorOutputCapture(() => fn(host, {})));
}
else {
result = await fn(host, {});
}
const { nextSteps, agentContext } = parseMigrationReturn(result);
host.lock();
const changes = host.listChanges();
(0, tree_1.flushChanges)(root, changes);
return { changes, nextSteps, agentContext, logs };
}
function parseMigrationReturn(value) {
if (Array.isArray(value)) {
return { nextSteps: filterStrings(value), agentContext: [] };
}
if (value && typeof value === 'object') {
const obj = value;
return {
nextSteps: filterStrings(obj.nextSteps),
agentContext: filterStrings(obj.agentContext),
};
}
// Catches `void`, mistakenly-returned generator callbacks, malformed values.
return { nextSteps: [], agentContext: [] };
}
// Bucket-level tolerance: a single non-string entry shouldn't discard the
// whole `nextSteps` / `agentContext` array. Migration authors occasionally
// push `null` / `undefined` / a number into the array; we drop the bad entries
// and keep the rest so end-of-run guidance isn't silently lost.
function filterStrings(value) {
if (!Array.isArray(value))
return [];
return value.filter((v) => typeof v === 'string');
}
function readMigrationCollection(packageName, root) {
const collectionPath = readPackageMigrationConfig(packageName, root).migrations;
const collection = (0, fileutils_1.readJsonFile)(collectionPath);
collection.name ??= packageName;
return {
collection,
collectionPath,
};
}
function getImplementationPath(collection, collectionPath, name, migrationVersion) {
const g = collection.generators?.[name] || collection.schematics?.[name];
if (!g) {
throw new MigrationImplementationMissingError(`Unable to determine implementation path for "${collectionPath}:${name}"`, collectionPath, migrationVersion);
}
const implRelativePathAndMaybeSymbol = g.implementation || g.factory;
const [implRelativePath, fnSymbol = 'default'] = implRelativePathAndMaybeSymbol.split('#');
let implPath;
try {
implPath = require.resolve(implRelativePath, {
paths: [(0, path_1.dirname)(collectionPath)],
});
}
catch (e) {
try {
// workaround for a bug in node 12
implPath = require.resolve(`${(0, path_1.dirname)(collectionPath)}/${implRelativePath}`);
}
catch {
throw new MigrationImplementationMissingError(`Could not resolve implementation for migration "${name}" from "${collectionPath}"`, collectionPath, migrationVersion ?? g.version);
}
}
return { path: implPath, fnSymbol };
}
class MigrationImplementationMissingError extends Error {
constructor(baseMessage, collectionPath, migrationVersion) {
super(buildMigrationMissingMessage(baseMessage, collectionPath, migrationVersion));
this.name = 'MigrationImplementationMissingError';
}
}
exports.MigrationImplementationMissingError = MigrationImplementationMissingError;
function buildMigrationMissingMessage(baseMessage, collectionPath, migrationVersion) {
if (!migrationVersion) {
return baseMessage;
}
try {
const packageJsonPath = (0, path_1.join)((0, path_1.dirname)(collectionPath), 'package.json');
if (!(0, fs_1.existsSync)(packageJsonPath)) {
return baseMessage;
}
const packageJson = (0, fileutils_1.readJsonFile)(packageJsonPath);
const installedVersion = packageJson.version;
if (installedVersion &&
(0, semver_1.lt)((0, version_utils_1.normalizeVersion)(installedVersion), (0, version_utils_1.normalizeVersion)(migrationVersion))) {
const packageManager = (0, package_manager_1.detectPackageManager)();
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
const overrideFieldName = getOverrideFieldName(packageManager);
return (`${baseMessage}\n\n` +
`The installed version of "${packageJson.name}" is ${installedVersion}, ` +
`but this migration requires version ${migrationVersion}. ` +
`This likely means the package version is being held back by an ${overrideFieldName} ` +
`in your package.json. ` +
`Remove the ${overrideFieldName} and run "${pmc.install}" to install the correct version.`);
}
}
catch {
// Fall through to return the base message if we can't read package info
}
return baseMessage;
}
function getOverrideFieldName(packageManager) {
switch (packageManager) {
case 'pnpm':
return '"pnpm.overrides"';
case 'yarn':
return '"resolutions"';
case 'npm':
case 'bun':
return '"overrides"';
}
}
function isAngularMigration(collection, name) {
return !collection.generators?.[name] && collection.schematics?.[name];
}
exports.getNgCompatLayer = (() => {
let _ngCliAdapter;
return async function getNgCompatLayer() {
if (!_ngCliAdapter) {
_ngCliAdapter = await (0, handle_import_1.handleImport)('../../adapter/ngcli-adapter.js', __dirname);
require('../../adapter/compat');
}
return _ngCliAdapter;
};
})();
import { type NxJsonConfiguration } from '../config/nx-json';
import type { ProjectGraph } from '../config/project-graph';
import type { HashInputs } from '../native';
/**
* A project graph the caller has already built. `nx show target` resolves one to
* find the target in the first place, and `createProjectGraphAsync` is not
* memoized, so without this the CLI would build the graph a second time.
*/
export interface TaskFileCheckSeed {
projectGraph: ProjectGraph;
nxJson: NxJsonConfiguration;
}
/** The rule that made a value an input for a task. */
export type InputCategory = 'files' | 'depOutputs' | 'dependentTasksOutputFiles' | 'runtime' | 'environment' | 'external';
export interface InputCandidate {
/** The value as supplied — matched verbatim against environment/runtime/external. */
value: string;
/** Workspace-relative path form of `value` — matched against the path categories. */
path: string;
}
/**
* Check which values are legitimate inputs for the given task. A value matches
* when it is:
* - a declared environment variable, runtime input, or external dependency;
* - a file in the task's declared input file list;
* - a file in the task's materialized `depOutputs` (upstream has run);
* - a file matching a `dependentTasksOutputFiles` glob declared on the task
* that lies inside the declared outputs of an upstream task in the task
* graph (static — works even when upstream tasks have not yet run).
*
* `categories` records the rule each matched value satisfied. Paths may be
* workspace-relative or absolute; absolute ones are relativized against the
* workspace root, and a path outside the workspace simply matches nothing. A
* caller resolving paths against a cwd passes an {@link InputCandidate} so that
* names are still matched verbatim.
*/
export declare function checkFilesAreInputs(taskId: string, files: Array<string | InputCandidate>): Promise<{
matched: string[];
unmatched: string[];
categories: Map<string, InputCategory>;
}>;
/**
* Check which files match the output globs declared for the given task.
* Uses the same path-matching logic as the task runner (directory containment
* + glob matching through the native `globset` engine), including negated
* (`!`-prefixed) patterns acting as exclusions over the whole pattern set.
*
* Paths may be workspace-relative or absolute; absolute ones are relativized
* against the workspace root. An output pattern whose `{options.*}` token has no
* value resolves to nothing — exactly as the task runner drops it — so a file it
* would have covered is reported `unmatched`, like any other non-output.
*
* That last case makes `unmatched` two answers in one: "not an output" and
* "the outputs could not be determined". A consumer judging sandbox violations
* cannot tell them apart, and would call the second one illegal. `getTaskOutputs`
* already computes the `unresolved` list this would need; surfacing it here is
* deliberately deferred until a consumer's contract asks for the distinction.
*/
export declare function checkFilesAreOutputs(taskId: string, files: string[]): Promise<{
matched: string[];
unmatched: string[];
}>;
/**
* Returns the full hash inputs for a task (files + runtime + env + depOutputs
* + external). Used internally by the `nx show target --inputs` renderer.
*/
export declare function getTaskRawInputs(taskId: string, seed?: TaskFileCheckSeed): Promise<HashInputs | null>;
export interface TaskOutputs {
/** Output patterns after token substitution — what the task runner will cache. */
resolved: string[];
/** `resolved`, expanded against the files currently on disk. */
expanded: string[];
/** Configured outputs left out of `resolved` because an option had no value. */
unresolved: string[];
}
/**
* Returns the outputs declared for a task, resolved against its effective
* configuration. Used internally by the `nx show target --outputs` renderer.
*/
export declare function getTaskOutputs(taskId: string, seed?: TaskFileCheckSeed): Promise<TaskOutputs>;
/**
* Resets all module-level caches. Call this in `beforeEach` when testing so
* each test gets a fresh context load. Not part of the public API.
* @internal
*/
export declare function _resetContextForTesting(): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkFilesAreInputs = checkFilesAreInputs;
exports.checkFilesAreOutputs = checkFilesAreOutputs;
exports.getTaskRawInputs = getTaskRawInputs;
exports.getTaskOutputs = getTaskOutputs;
exports._resetContextForTesting = _resetContextForTesting;
const path_1 = require("path");
const nx_json_1 = require("../config/nx-json");
const native_1 = require("../native");
const project_graph_1 = require("../project-graph/project-graph");
const create_task_graph_1 = require("../tasks-runner/create-task-graph");
const utils_1 = require("../tasks-runner/utils");
const path_2 = require("../utils/path");
const project_graph_utils_1 = require("../utils/project-graph-utils");
const split_target_1 = require("../utils/split-target");
const workspace_root_1 = require("../utils/workspace-root");
const hash_plan_inspector_1 = require("./hash-plan-inspector");
const task_hasher_1 = require("./task-hasher");
let cachedContext = null;
function getContext(seed) {
// Only a fulfilled context is cached — caching a rejected promise would
// poison the resolver for the rest of the process after one transient failure.
return (cachedContext ??= loadContext(seed).catch((e) => {
cachedContext = null;
throw e;
}));
}
async function loadContext(seed) {
const projectGraph = seed?.projectGraph ?? (await (0, project_graph_1.createProjectGraphAsync)());
const nxJson = seed?.nxJson ?? (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot) ?? {};
let inspector = null;
const getInspector = () =>
// As with the context itself, a rejection is not cached.
(inspector ??= initInspector(projectGraph, nxJson).catch((e) => {
inspector = null;
throw e;
}));
return { projectGraph, nxJson, getInspector };
}
async function initInspector(projectGraph, nxJson) {
const inspector = new hash_plan_inspector_1.HashPlanInspector(projectGraph, workspace_root_1.workspaceRoot, nxJson);
await inspector.init();
return inspector;
}
const identityCache = new Map();
const hashInputsCache = new Map();
const outputsCache = new Map();
const taskGraphCache = new Map();
const depsOutputsCache = new Map();
// ── Internal resolution helpers ──────────────────────────────────────────────
function resolveIdentity(taskId, projectGraph) {
const cached = identityCache.get(taskId);
if (cached)
return cached;
const [project, target, configuration] = (0, split_target_1.splitTarget)(taskId, projectGraph);
if (!project || !target) {
throw new Error(`Invalid taskId "${taskId}" — expected "project:target[:configuration]"`);
}
const projectNode = projectGraph.nodes[project];
if (!projectNode) {
throw new Error(`Invalid taskId "${taskId}" — project "${project}" does not exist in the project graph.`);
}
const targetConfig = projectNode.data?.targets?.[target];
if (!targetConfig) {
throw new Error(`Invalid taskId "${taskId}" — project "${project}" has no target "${target}".`);
}
// Substituting defaultConfiguration for a configuration that does not exist
// would answer confidently about a *different* task — and configurations
// routinely change `outputPath`, so the answer could be wrong in either
// direction. `nx run` errors here; so do we.
if (configuration &&
!(0, project_graph_utils_1.projectHasTargetAndConfiguration)(projectNode, target, configuration)) {
const available = Object.keys(targetConfig.configurations ?? {});
throw new Error(`Invalid taskId "${taskId}" — target "${target}" of project "${project}" has no configuration "${configuration}".` +
(available.length
? ` Available configurations: ${available.join(', ')}.`
: ' It has no configurations.'));
}
const effectiveConfiguration = configuration ?? targetConfig.defaultConfiguration;
const identity = {
project,
target,
configuration: effectiveConfiguration,
canonicalTaskId: (0, utils_1.createTaskId)(project, target, effectiveConfiguration),
projectNode,
};
identityCache.set(taskId, identity);
return identity;
}
async function getRawInputs(taskId, { projectGraph, getInspector }) {
if (hashInputsCache.has(taskId)) {
return hashInputsCache.get(taskId) ?? null;
}
const { project, target, configuration, canonicalTaskId } = resolveIdentity(taskId, projectGraph);
const inspector = await getInspector();
// `null` means "this task is absent from the hash plan" — any other failure
// is a real error and propagates to the caller.
const planResult = inspector.inspectTaskInputs({
project,
target,
configuration,
});
const result = planResult[canonicalTaskId] ?? null;
hashInputsCache.set(taskId, result);
return result;
}
function getOutputs(taskId, projectGraph) {
const cached = outputsCache.get(taskId);
if (cached !== undefined)
return cached;
const { project, target, configuration, projectNode } = resolveIdentity(taskId, projectGraph);
const outputs = (0, utils_1.getOutputsForTargetAndConfiguration)({ project, target, configuration }, {}, projectNode).map(path_2.normalizePath);
outputsCache.set(taskId, outputs);
return outputs;
}
/**
* Configured outputs that `getOutputsForTargetAndConfiguration` dropped because
* an `{options.x}` token had no value to interpolate. The resolver discards them
* silently, so they have to be recovered from the target configuration.
*/
function getUnresolvedOutputs(taskId, projectGraph) {
const { target, configuration, projectNode } = resolveIdentity(taskId, projectGraph);
const targetConfig = projectNode.data.targets[target];
const options = {
...targetConfig.options,
...(configuration
? targetConfig.configurations?.[configuration]
: undefined),
};
return (targetConfig.outputs ?? []).filter((output) => [...output.matchAll(/\{options\.([^}]+)\}/g)].some(([, key]) => {
const value = key.split('.').reduce((acc, k) => acc?.[k], options);
return value === undefined;
}));
}
function getTaskGraph(taskId, projectGraph) {
const cached = taskGraphCache.get(taskId);
if (cached)
return cached;
const { project, target, configuration } = resolveIdentity(taskId, projectGraph);
const taskGraph = (0, create_task_graph_1.createTaskGraph)(projectGraph, {}, [project], [target], configuration, {}, false);
taskGraphCache.set(taskId, taskGraph);
return taskGraph;
}
function getDepsOutputs(taskId, { projectGraph, nxJson }) {
if (depsOutputsCache.has(taskId))
return depsOutputsCache.get(taskId);
const { project, target } = resolveIdentity(taskId, projectGraph);
const result = (0, task_hasher_1.getInputs)({ target: { project, target } }, projectGraph, nxJson)
.depsOutputs ?? [];
depsOutputsCache.set(taskId, result);
return result;
}
function collectUpstreamTaskIds(taskGraph, rootTaskId, transitive) {
const direct = taskGraph.dependencies[rootTaskId] ?? [];
if (!transitive)
return [...direct];
const collected = new Set();
const walk = (id) => {
for (const dep of taskGraph.dependencies[id] ?? []) {
if (collected.has(dep))
continue;
collected.add(dep);
walk(dep);
}
};
walk(rootTaskId);
return [...collected];
}
/**
* Matches a single path against a task's whole output pattern list using the
* native glob engine (`globset`) that the task runner's expand_outputs also
* builds on: non-glob patterns match themselves and anything nested under them,
* and negated (`!`-prefixed) patterns act as exclusions over the full set.
*/
function isOutput(taskId, path, projectGraph) {
const patterns = getOutputs(taskId, projectGraph);
return (0, native_1.matchOutputPaths)(patterns, [(0, path_2.normalizePath)(path)])[0];
}
function matchesDependentTaskOutputs(taskId, path, ctx) {
const normalized = (0, path_2.normalizePath)(path);
const depsOutputs = getDepsOutputs(taskId, ctx);
if (depsOutputs.length === 0)
return false;
const taskGraph = getTaskGraph(taskId, ctx.projectGraph);
const { canonicalTaskId } = resolveIdentity(taskId, ctx.projectGraph);
if (!taskGraph.tasks[canonicalTaskId])
return false;
for (const { dependentTasksOutputFiles, transitive } of depsOutputs) {
const glob = (0, path_2.normalizePath)(dependentTasksOutputFiles);
if (!(0, native_1.matchGlobPaths)([glob], [normalized])[0])
continue;
const upstreamIds = collectUpstreamTaskIds(taskGraph, canonicalTaskId, !!transitive);
for (const upstreamId of upstreamIds) {
if (isOutput(upstreamId, normalized, ctx.projectGraph))
return true;
}
}
return false;
}
/**
* Coerces a caller-supplied path to the workspace-relative, forward-slashed
* form the hash plan and output patterns are expressed in. A path outside the
* workspace stays outside (`../…`) and simply matches nothing — it cannot be a
* declared input or output, so "unmatched" is the true answer rather than an
* error.
*/
function toWorkspaceRelativePath(candidatePath) {
// Backslash separators must be split *before* anchoring: on POSIX a backslash
// is an ordinary filename character, so `dep\..\..` would ride through
// join/relative as one opaque segment and only become a live `..` traversal
// after the swap. Swapped directly rather than via normalizePath, which also
// strips a Windows drive letter: isAbsolute accepts the drive-less form too,
// but relative() then resolves it against the cwd's drive, so a path on
// another drive (`D:\…`) could be relativized to *inside* the workspace — a
// Windows-only fail-open no POSIX test can catch.
const forwardSlashed = candidatePath.replace(/\\/g, '/');
// Anchoring a relative path to the workspace root before relativizing it back
// resolves any `..` segments. Left in, they would traverse *through* a pattern
// that globset had already matched — `dist/libs/dep/../../../secrets.env`
// matching an output of `dist/libs/dep`.
const absolute = (0, path_1.isAbsolute)(forwardSlashed)
? forwardSlashed
: (0, path_1.join)(workspace_root_1.workspaceRoot, forwardSlashed);
return (0, path_2.normalizePath)((0, path_1.relative)(workspace_root_1.workspaceRoot, absolute));
}
/**
* The task's hash inputs, or an error. A task missing from its own hash plan is
* a failure to *determine* the inputs — reporting every file as unmatched would
* tell a sandbox-violation consumer that all of them are illegal.
*/
async function requireRawInputs(taskId, ctx) {
const raw = await getRawInputs(taskId, ctx);
if (!raw) {
throw new Error(`Could not determine the inputs of task "${taskId}" — it is not present in its own hash plan.`);
}
return raw;
}
function classifyInput(taskId, candidate, raw, ctx) {
// `environment`, `runtime` and `external` hold names rather than paths, so
// they are matched against the value exactly as the caller supplied it.
if (raw.environment.includes(candidate.value))
return 'environment';
if (raw.runtime.includes(candidate.value))
return 'runtime';
if (raw.external.includes(candidate.value))
return 'external';
const path = toWorkspaceRelativePath(candidate.path);
if (raw.files.includes(path))
return 'files';
if (raw.depOutputs.includes(path))
return 'depOutputs';
return matchesDependentTaskOutputs(taskId, path, ctx)
? 'dependentTasksOutputFiles'
: null;
}
/**
* Check which values are legitimate inputs for the given task. A value matches
* when it is:
* - a declared environment variable, runtime input, or external dependency;
* - a file in the task's declared input file list;
* - a file in the task's materialized `depOutputs` (upstream has run);
* - a file matching a `dependentTasksOutputFiles` glob declared on the task
* that lies inside the declared outputs of an upstream task in the task
* graph (static — works even when upstream tasks have not yet run).
*
* `categories` records the rule each matched value satisfied. Paths may be
* workspace-relative or absolute; absolute ones are relativized against the
* workspace root, and a path outside the workspace simply matches nothing. A
* caller resolving paths against a cwd passes an {@link InputCandidate} so that
* names are still matched verbatim.
*/
async function checkFilesAreInputs(taskId, files) {
const ctx = await getContext();
// Resolve the task and its hash plan eagerly, so an unknown task or an
// undeterminable plan errors even when the file list is empty — rather than
// being reported as "none of these files are inputs".
resolveIdentity(taskId, ctx.projectGraph);
const raw = await requireRawInputs(taskId, ctx);
const matched = [];
const unmatched = [];
const categories = new Map();
// Results are keyed by value, so a value given two path forms could land in
// both matched and unmatched at once. Exact duplicates are collapsed instead.
const seenPaths = new Map();
for (const file of files) {
const candidate = typeof file === 'string' ? { value: file, path: file } : file;
const seenPath = seenPaths.get(candidate.value);
if (seenPath !== undefined) {
if (seenPath !== candidate.path) {
throw new Error(`Value "${candidate.value}" was given conflicting path forms "${seenPath}" and "${candidate.path}".`);
}
continue;
}
seenPaths.set(candidate.value, candidate.path);
const category = classifyInput(taskId, candidate, raw, ctx);
if (category) {
matched.push(candidate.value);
categories.set(candidate.value, category);
}
else {
unmatched.push(candidate.value);
}
}
return { matched, unmatched, categories };
}
/**
* Check which files match the output globs declared for the given task.
* Uses the same path-matching logic as the task runner (directory containment
* + glob matching through the native `globset` engine), including negated
* (`!`-prefixed) patterns acting as exclusions over the whole pattern set.
*
* Paths may be workspace-relative or absolute; absolute ones are relativized
* against the workspace root. An output pattern whose `{options.*}` token has no
* value resolves to nothing — exactly as the task runner drops it — so a file it
* would have covered is reported `unmatched`, like any other non-output.
*
* That last case makes `unmatched` two answers in one: "not an output" and
* "the outputs could not be determined". A consumer judging sandbox violations
* cannot tell them apart, and would call the second one illegal. `getTaskOutputs`
* already computes the `unresolved` list this would need; surfacing it here is
* deliberately deferred until a consumer's contract asks for the distinction.
*/
async function checkFilesAreOutputs(taskId, files) {
const ctx = await getContext();
// Validate taskId eagerly so callers always get an error for an unknown or
// malformed task, even when the file list is empty.
resolveIdentity(taskId, ctx.projectGraph);
const patterns = getOutputs(taskId, ctx.projectGraph);
const results = (0, native_1.matchOutputPaths)(patterns, files.map(toWorkspaceRelativePath));
const matched = [];
const unmatched = [];
files.forEach((file, i) => {
if (results[i]) {
matched.push(file);
}
else {
unmatched.push(file);
}
});
return { matched, unmatched };
}
// ── Renderer helpers (used by `nx show target`) ──────────────────────────────
/**
* Returns the full hash inputs for a task (files + runtime + env + depOutputs
* + external). Used internally by the `nx show target --inputs` renderer.
*/
async function getTaskRawInputs(taskId, seed) {
const ctx = await getContext(seed);
return getRawInputs(taskId, ctx);
}
/**
* Returns the outputs declared for a task, resolved against its effective
* configuration. Used internally by the `nx show target --outputs` renderer.
*/
async function getTaskOutputs(taskId, seed) {
const ctx = await getContext(seed);
const resolved = getOutputs(taskId, ctx.projectGraph);
return {
resolved,
expanded: (0, native_1.expandOutputs)(workspace_root_1.workspaceRoot, resolved),
unresolved: getUnresolvedOutputs(taskId, ctx.projectGraph),
};
}
// ── Test utilities ───────────────────────────────────────────────────────────
/**
* Resets all module-level caches. Call this in `beforeEach` when testing so
* each test gets a fresh context load. Not part of the public API.
* @internal
*/
function _resetContextForTesting() {
cachedContext = null;
identityCache.clear();
hashInputsCache.clear();
outputsCache.clear();
taskGraphCache.clear();
depsOutputsCache.clear();
}
import type { Tree } from '../generators/tree';
import type { PackageManager } from './package-manager';
/**
* Records build-script decisions for dependencies that are about to be
* installed, in whatever form the given package manager understands.
*
* Only pnpm needs this today: pnpm 11+ refuses to install a dependency whose
* build scripts are neither allowed nor denied, so the generator or command
* that introduces such a dependency records the decision up front. Other
* package managers run build scripts unconditionally, so this is a no-op for
* them.
*/
export declare function acknowledgeBuildScripts(treeOrRoot: Tree | string, packageManager: PackageManager, entries: Record<string, boolean>): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.acknowledgeBuildScripts = acknowledgeBuildScripts;
const fs_1 = require("fs");
const path_1 = require("path");
const yaml_1 = require("yaml");
const semver_1 = require("semver");
const package_manager_1 = require("./package-manager");
const fileutils_1 = require("./fileutils");
const json_1 = require("./json");
const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
/**
* Records build-script decisions for dependencies that are about to be
* installed, in whatever form the given package manager understands.
*
* Only pnpm needs this today: pnpm 11+ refuses to install a dependency whose
* build scripts are neither allowed nor denied, so the generator or command
* that introduces such a dependency records the decision up front. Other
* package managers run build scripts unconditionally, so this is a no-op for
* them.
*/
function acknowledgeBuildScripts(treeOrRoot, packageManager, entries) {
if (packageManager !== 'pnpm') {
return;
}
acknowledgePnpmBuildScripts(treeOrRoot, entries);
}
/**
* Records `allowBuilds` decisions in pnpm-workspace.yaml, creating the file
* when missing (mirroring `pnpm approve-builds` in single-package repos).
*
* Comment-preserving. Existing entries are never overwritten, so user
* decisions always win. No-op for pnpm < 11, which warns instead of erroring
* and does not read `allowBuilds`.
*/
function acknowledgePnpmBuildScripts(treeOrRoot, entries) {
const host = createHost(treeOrRoot);
const pnpmVersion = getPnpmVersion(host);
if (!pnpmVersion || !(0, semver_1.gte)(pnpmVersion, '11.0.0')) {
return;
}
const parsed = (0, yaml_1.parseDocument)(host.exists(PNPM_WORKSPACE_FILE) ? host.read(PNPM_WORKSPACE_FILE) : '');
// A file that doesn't parse cleanly or whose root isn't a mapping is
// malformed for pnpm; leave it alone rather than crashing or replacing the
// user's content. pnpm's own error on the file is the actionable signal.
// Empty and comment-only files have no contents at all; setIn creates the
// mapping for them while keeping whatever comments they carry.
if (parsed.errors.length > 0 ||
(parsed.contents != null && !(parsed.contents instanceof yaml_1.YAMLMap))) {
return;
}
let changed = false;
for (const [pkg, allowed] of Object.entries(entries)) {
// Only a real boolean is a user decision. pnpm's non-strict installs stub
// undecided packages with a placeholder string ("set this to true or
// false"), which would fail the next strict install if left in place.
if (typeof parsed.getIn(['allowBuilds', pkg]) !== 'boolean') {
parsed.setIn(['allowBuilds', pkg], allowed);
changed = true;
}
}
if (changed) {
host.write(PNPM_WORKSPACE_FILE, parsed.toString());
}
}
function createHost(treeOrRoot) {
if (typeof treeOrRoot === 'string') {
return {
root: treeOrRoot,
exists: (p) => (0, fs_1.existsSync)((0, path_1.join)(treeOrRoot, p)),
read: (p) => (0, fs_1.readFileSync)((0, path_1.join)(treeOrRoot, p), 'utf-8'),
write: (p, c) => (0, fs_1.writeFileSync)((0, path_1.join)(treeOrRoot, p), c),
readJson: (p) => (0, fileutils_1.readJsonFile)((0, path_1.join)(treeOrRoot, p)),
};
}
return {
root: treeOrRoot.root,
exists: (p) => treeOrRoot.exists(p),
read: (p) => treeOrRoot.read(p, 'utf-8'),
write: (p, c) => treeOrRoot.write(p, c),
readJson: (p) => (0, json_1.parseJson)(treeOrRoot.read(p, 'utf-8')),
};
}
function getPnpmVersion(host) {
// The host's packageManager field wins: during workspace creation the
// in-flight package.json only exists in the tree, not on disk.
if (host.exists('package.json')) {
const { packageManager } = host.readJson('package.json');
const version = (0, package_manager_1.parseVersionFromPackageManagerField)('pnpm', typeof packageManager === 'string' ? packageManager : undefined);
if (version) {
return version;
}
}
try {
return (0, package_manager_1.getPackageManagerVersion)('pnpm', host.root);
}
catch {
// The version cannot be probed (e.g. pnpm is not on PATH). Leave the
// workspace file untouched; pnpm's own install error remains actionable.
return null;
}
}
/**
* Git parses any argument beginning with `-` as an option rather than a
* revision, so a flag-shaped revision could smuggle options such as
* `--upload-pack` into the git commands Nx runs.
*/
export declare function assertValidGitRevision(revision: string): void;
/**
* For refs Nx itself recorded from `git rev-parse` and later read back off
* disk, where anything other than a commit sha means the value was tampered
* with rather than that the user picked an unusual revision.
*/
export declare function assertValidGitSha(sha: string): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertValidGitRevision = assertValidGitRevision;
exports.assertValidGitSha = assertValidGitSha;
/**
* Git parses any argument beginning with `-` as an option rather than a
* revision, so a flag-shaped revision could smuggle options such as
* `--upload-pack` into the git commands Nx runs.
*/
function assertValidGitRevision(revision) {
if (revision.startsWith('-')) {
throw new Error(`Invalid git revision: "${revision}". Git revisions cannot start with "-".`);
}
}
const COMMIT_SHA = /^[0-9a-f]{7,40}$/i;
/**
* For refs Nx itself recorded from `git rev-parse` and later read back off
* disk, where anything other than a commit sha means the value was tampered
* with rather than that the user picked an unusual revision.
*/
function assertValidGitSha(sha) {
if (!COMMIT_SHA.test(sha)) {
throw new Error(`Invalid git commit sha: "${sha}". Expected a hexadecimal commit sha.`);
}
}
import type { NxJsonConfiguration } from '../config/nx-json';
/**
* The workspace's analytics identity: the Nx Cloud id when the workspace has
* one (most stable — it survives repo moves and renames), else the repo key.
* Null when neither is available, in which case nothing is reported.
*/
export declare function generateWorkspaceId(root: string, nxJson: NxJsonConfiguration | null): string | null;
/**
* Derive the stable, unsalted key identifying this workspace in the
* repoTelemetry registry: `sha256(<repo identity> + '#' + <workspace path
* relative to the git root>)`.
*
* The repo identity is the normalized `domain/slug` from the git remote
* (protocol-independent: ssh, https, and token-authenticated URLs of the
* same repo produce the same key), falling back to the first-commit SHA
* when no remote exists. Returns null when no identity is derivable — not
* a git repository, or a shallow clone without a remote.
*/
export declare function deriveRepoKey(directory: string): string | null;
export declare function computeRepoKey(identity: string, relativePath: string): string;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateWorkspaceId = generateWorkspaceId;
exports.deriveRepoKey = deriveRepoKey;
exports.computeRepoKey = computeRepoKey;
const crypto_1 = require("crypto");
const git_utils_1 = require("./git-utils");
/**
* The workspace's analytics identity: the Nx Cloud id when the workspace has
* one (most stable — it survives repo moves and renames), else the repo key.
* Null when neither is available, in which case nothing is reported.
*/
function generateWorkspaceId(root, nxJson) {
const nxCloudId = nxJson?.nxCloudId ?? nxJson?.nxCloudAccessToken;
if (nxCloudId) {
return nxCloudId;
}
return deriveRepoKey(root);
}
/**
* Derive the stable, unsalted key identifying this workspace in the
* repoTelemetry registry: `sha256(<repo identity> + '#' + <workspace path
* relative to the git root>)`.
*
* The repo identity is the normalized `domain/slug` from the git remote
* (protocol-independent: ssh, https, and token-authenticated URLs of the
* same repo produce the same key), falling back to the first-commit SHA
* when no remote exists. Returns null when no identity is derivable — not
* a git repository, or a shallow clone without a remote.
*/
function deriveRepoKey(directory) {
const identity = getRepoIdentity(directory);
if (!identity) {
return null;
}
return computeRepoKey(identity, (0, git_utils_1.getGitRootRelativePath)(directory) ?? '');
}
function computeRepoKey(identity, relativePath) {
return (0, crypto_1.createHash)('sha256')
.update(`${identity}#${relativePath}`)
.digest('hex');
}
function getRepoIdentity(directory) {
const remote = (0, git_utils_1.getVcsRemoteInfo)(directory);
if (remote) {
// Hosts route case-insensitively and hold one canonical casing, so a
// hand-typed remote must key the same as the canonical one the claim
// flow derives from the host's API.
return `${remote.domain}/${remote.slug}`.toLowerCase().replace(/\/+$/, '');
}
// Without a remote the first commit is the only stable identity, and a
// shallow clone doesn't have a real one.
return (0, git_utils_1.isShallowRepository)(directory) ? null : (0, git_utils_1.getFirstCommitSha)(directory);
}
+2
-1

@@ -8,2 +8,3 @@ #!/usr/bin/env node

if (process.env.FORCE_COLOR === '0') {
process.env.NX_ORIGINAL_FORCE_COLOR = '0';
process.env.NO_COLOR = '1';

@@ -262,3 +263,3 @@ delete process.env.FORCE_COLOR;

: [];
bodyLines.push('For more information, see https://nx.dev/more-concepts/global-nx');
bodyLines.push('For more information, see https://nx.dev/docs/getting-started/installation#global-installation');
output_1.output.warn({

@@ -265,0 +266,0 @@ title: `It's time to update Nx 🎉`,

import type { EventDimensions } from '../native';
export declare const customDimensions: EventDimensions;
export type EventParameters = Partial<Record<EventDimensions[keyof EventDimensions], string | number | boolean>>;
/**
* Fraction of sessions that report perf spans. Stamping this rate on a
* measure's detail (as the sampleRate dimension) opts it into sampling; see
* is_sampled_in in native/telemetry/service.rs. Multiply GA counts by 1/rate.
*/
export declare const PERF_SPAN_SAMPLE_RATE = 0.1;
export declare function startAnalytics(): Promise<void>;

@@ -5,0 +11,0 @@ export declare function reportNxAddCommand(packageName: string, version: string): void;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.customDimensions = void 0;
exports.PERF_SPAN_SAMPLE_RATE = exports.customDimensions = void 0;
exports.startAnalytics = startAnalytics;

@@ -22,3 +22,3 @@ exports.reportNxAddCommand = reportNxAddCommand;

const is_ci_1 = require("../utils/is-ci");
const analytics_prompt_1 = require("../utils/analytics-prompt");
const workspace_id_1 = require("../utils/workspace-id");
const db_connection_1 = require("../utils/db-connection");

@@ -46,2 +46,8 @@ // Conditionally import telemetry functions only on non-WASM platforms

let _telemetryInitialized = false;
/**
* Fraction of sessions that report perf spans. Stamping this rate on a
* measure's detail (as the sampleRate dimension) opts it into sampling; see
* is_sampled_in in native/telemetry/service.rs. Multiply GA counts by 1/rate.
*/
exports.PERF_SPAN_SAMPLE_RATE = 0.1;
async function startAnalytics() {

@@ -57,7 +63,7 @@ // Analytics not supported on WASM

try {
if (!isAnalyticsEnabled()) {
const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot);
if (!isAnalyticsEnabled(nxJson)) {
return;
}
const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot);
const workspaceId = (0, analytics_prompt_1.generateWorkspaceId)();
const workspaceId = (0, workspace_id_1.generateWorkspaceId)(workspace_root_1.workspaceRoot, nxJson);
if (!workspaceId) {

@@ -68,3 +74,6 @@ // Not a git repo — no telemetry

const isNxCloud = !!(nxJson?.nxCloudId ?? nxJson?.nxCloudAccessToken);
const userId = await getTelemetryUserId(workspaceId);
// A CI fleet is not a user: shared images bake in /etc/machine-id, so a
// uid would collapse whole fleets into one GA "user" (and trip per-user
// collection caps). GA falls back to cid = workspace for CI traffic.
const userId = (0, is_ci_1.isCI)() ? undefined : await getTelemetryUserId(workspaceId);
const packageManagerInfo = getPackageManagerInfo();

@@ -233,4 +242,3 @@ const nodeVersion = (0, semver_1.parse)(process.version);

}
function isAnalyticsEnabled() {
const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot);
function isAnalyticsEnabled(nxJson) {
return nxJson?.analytics === true;

@@ -237,0 +245,0 @@ }

@@ -1,1 +0,1 @@

export { customDimensions, EventParameters, startAnalytics, reportNxAddCommand, reportNxGenerateCommand, reportCommandRunEvent, reportEvent, flushAnalytics, } from './analytics';
export { customDimensions, EventParameters, PERF_SPAN_SAMPLE_RATE, startAnalytics, reportNxAddCommand, reportNxGenerateCommand, reportCommandRunEvent, reportEvent, flushAnalytics, } from './analytics';
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flushAnalytics = exports.reportEvent = exports.reportCommandRunEvent = exports.reportNxGenerateCommand = exports.reportNxAddCommand = exports.startAnalytics = exports.customDimensions = void 0;
exports.flushAnalytics = exports.reportEvent = exports.reportCommandRunEvent = exports.reportNxGenerateCommand = exports.reportNxAddCommand = exports.startAnalytics = exports.PERF_SPAN_SAMPLE_RATE = exports.customDimensions = void 0;
var analytics_1 = require("./analytics");
Object.defineProperty(exports, "customDimensions", { enumerable: true, get: function () { return analytics_1.customDimensions; } });
Object.defineProperty(exports, "PERF_SPAN_SAMPLE_RATE", { enumerable: true, get: function () { return analytics_1.PERF_SPAN_SAMPLE_RATE; } });
Object.defineProperty(exports, "startAnalytics", { enumerable: true, get: function () { return analytics_1.startAnalytics; } });

@@ -7,0 +8,0 @@ Object.defineProperty(exports, "reportNxAddCommand", { enumerable: true, get: function () { return analytics_1.reportNxAddCommand; } });

@@ -41,5 +41,12 @@ "use strict";

// if we explicitly specify latest in yarn berry, it won't resolve the version
const command = pm === 'yarn' && (0, semver_1.gte)(pmv, '2.0.0') && version === 'latest'
let command = pm === 'yarn' && (0, semver_1.gte)(pmv, '2.0.0') && version === 'latest'
? `${pmc.addDev} ${pkgName}`
: `${pmc.addDev} ${pkgName}@${version}`;
// pnpm 11+ fails the install when the plugin's own dependency tree
// carries unacknowledged build scripts, and the plugin's generators can
// only record allowBuilds decisions after this install. Warn and skip
// for this one install, like pnpm 10 did.
if (pm === 'pnpm' && (0, semver_1.gte)(pmv, '11.0.0')) {
command += ' --config.strictDepBuilds=false';
}
await new Promise((resolve) => (0, child_process_1.exec)(command, {

@@ -46,0 +53,0 @@ windowsHide: true,

@@ -53,2 +53,3 @@ import { ProjectFileMap, ProjectGraph, ProjectGraphDependency, ProjectGraphProjectNode } from '../../config/project-graph';

}, affectedProjects: string[]): Promise<void>;
export declare function getExpandedTaskInputs(depGraphClientResponse: ProjectGraphClientResponse, expandedTaskInputsCache: Map<string, Record<string, string[]>>, taskId: string): Promise<Record<string, string[]>>;
/**

@@ -55,0 +56,0 @@ * The data type that `nx graph --file graph.json` or `nx build --graph graph.json` contains

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateGraph = generateGraph;
exports.getExpandedTaskInputs = getExpandedTaskInputs;
const tslib_1 = require("tslib");

@@ -12,3 +13,2 @@ const crypto_1 = require("crypto");

const node_url_1 = require("node:url");
const open_1 = tslib_1.__importDefault(require("open"));
const node_path_1 = require("node:path");

@@ -38,2 +38,3 @@ const net = tslib_1.__importStar(require("node:net"));

const nx_cloud_utils_1 = require("../../utils/nx-cloud-utils");
const split_target_1 = require("../../utils/split-target");
// maps file extention to MIME types

@@ -354,3 +355,7 @@ const mimeType = {

if (args.open) {
(0, open_1.default)(url.toString());
new Function('return import("open")')()
.then((m) => m.default(url.toString()))
.catch(() => {
// Ignore errors when opening browser (e.g. no browser available)
});
}

@@ -444,3 +449,3 @@ return new Promise((res) => {

res.writeHead(200, { 'Content-Type': 'application/json' });
const inputs = await getExpandedTaskInputs(taskId);
const inputs = await getExpandedTaskInputs(currentProjectGraphClientResponse, expandedTaskInputsCache, taskId);
node_perf_hooks_1.performance.mark('task input generation:end');

@@ -830,3 +835,3 @@ res.end(JSON.stringify({ [taskId]: inputs }));

}
async function getExpandedTaskInputs(taskId) {
async function getExpandedTaskInputs(depGraphClientResponse, expandedTaskInputsCache, taskId) {
// Check cache first

@@ -837,3 +842,7 @@ if (expandedTaskInputsCache.has(taskId)) {

// Use the optimized version that only creates the specific task graph needed
const [projectName, targetName, configuration] = taskId.split(':');
// Use colon-aware splitting so that target names containing colons
// (e.g. "test:integration") are parsed correctly instead of being
// mistaken for a target + configuration pair.
const projectNodes = Object.fromEntries(depGraphClientResponse.projects.map((p) => [p.name, p]));
const [projectName, targetName, configuration] = (0, split_target_1.splitTargetFromNodes)(taskId, projectNodes, { silent: true });
const taskGraphResponse = await createTaskGraphForTargetsAndProjects([targetName], [projectName], configuration);

@@ -844,3 +853,3 @@ const allWorkspaceFiles = await (0, all_file_data_1.allFileData)();

if (inputs) {
result = expandInputs(inputs, currentProjectGraphClientResponse.projects.find((p) => p.name === projectName), allWorkspaceFiles, currentProjectGraphClientResponse);
result = expandInputs(inputs, depGraphClientResponse.projects.find((p) => p.name === projectName), allWorkspaceFiles, depGraphClientResponse);
}

@@ -847,0 +856,0 @@ // Cache the result

@@ -492,3 +492,3 @@ "use strict";

});
(0, utils_1.runInstall)(workspace_root_1.workspaceRoot, (0, package_manager_1.getPackageManagerCommand)(packageManager));
(0, utils_1.runInstall)(workspace_root_1.workspaceRoot, packageManager, (0, package_manager_1.getPackageManagerCommand)(packageManager));
await destinationGitClient.amendCommit();

@@ -495,0 +495,0 @@ }

import { Agent } from '../../ai/utils';
export declare function determineAiAgents(aiAgents?: Agent[], interactive?: boolean): Promise<Agent[]>;
export declare function determineAiAgents(aiAgents?: (Agent | 'none')[], interactive?: boolean): Promise<Agent[]>;

@@ -11,12 +11,13 @@ "use strict";

async function determineAiAgents(aiAgents, interactive) {
if (aiAgents) {
const filtered = aiAgents.filter((a) => a !== 'none');
if (filtered.length > 0) {
return filtered;
}
return [];
}
if (interactive === false || (0, is_ci_1.isCI)()) {
if (aiAgents) {
return aiAgents;
}
const detected = (0, detect_ai_agent_1.detectAiAgent)();
return detected ? [detected] : [];
}
if (aiAgents) {
return aiAgents;
}
return await aiAgentsPrompt();

@@ -23,0 +24,0 @@ }

@@ -67,4 +67,12 @@ "use strict";

string: true,
description: 'List of AI agents to set up.',
choices: ['claude', 'codex', 'copilot', 'cursor', 'gemini', 'opencode'],
description: 'List of AI agents to set up. Use "none" to skip.',
choices: [
'claude',
'codex',
'copilot',
'cursor',
'gemini',
'opencode',
'none',
],
})

@@ -71,0 +79,0 @@ .option('plugins', {

@@ -28,4 +28,5 @@ "use strict";

if ((0, fs_1.existsSync)((0, path_1.join)(repoRoot, 'package.json'))) {
(0, utils_1.addDepsToPackageJson)(repoRoot, plugins);
(0, utils_1.runInstall)(repoRoot, pmc);
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager, plugins);
(0, utils_1.runInstall)(repoRoot, packageManager, pmc);
}

@@ -32,0 +33,0 @@ else {

@@ -10,2 +10,3 @@ "use strict";

const output_1 = require("../../../utils/output");
const package_manager_1 = require("../../../utils/package-manager");
const utils_1 = require("./utils");

@@ -80,5 +81,6 @@ const connect_to_nx_cloud_1 = require("../../nx-cloud/connect/connect-to-nx-cloud");

(0, utils_1.updateGitIgnore)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot);
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager);
output_1.output.log({ title: '📦 Installing dependencies' });
(0, utils_1.runInstall)(repoRoot);
(0, utils_1.runInstall)(repoRoot, packageManager);
if (nxCloudChoice === 'yes') {

@@ -85,0 +87,0 @@ output_1.output.log({ title: '🛠️ Setting up Nx Cloud' });

@@ -89,5 +89,6 @@ "use strict";

(0, utils_1.createNxJsonFile)(repoRoot, [], [...cacheableOperations, ...nestCacheableScripts], scriptOutputs);
const pmc = (0, package_manager_1.getPackageManagerCommand)();
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
(0, utils_1.updateGitIgnore)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager);
addNestPluginToPackageJson(repoRoot);

@@ -102,3 +103,3 @@ (0, utils_1.markRootPackageJsonAsNxProjectLegacy)(repoRoot, cacheableOperations, pmc);

output_1.output.log({ title: '📦 Installing dependencies' });
(0, utils_1.runInstall)(repoRoot);
(0, utils_1.runInstall)(repoRoot, packageManager, pmc);
if (nxCloudChoice === 'yes') {

@@ -105,0 +106,0 @@ output_1.output.log({ title: '🛠️ Setting up Nx Cloud' });

@@ -64,5 +64,6 @@ "use strict";

(0, utils_1.createNxJsonFile)(repoRoot, [], cacheableOperations, scriptOutputs);
const pmc = (0, package_manager_1.getPackageManagerCommand)();
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
(0, utils_1.updateGitIgnore)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager);
if (options.legacy) {

@@ -75,3 +76,3 @@ (0, utils_1.markRootPackageJsonAsNxProjectLegacy)(repoRoot, cacheableOperations, pmc);

output_1.output.log({ title: '📦 Installing dependencies' });
(0, utils_1.runInstall)(repoRoot, pmc);
(0, utils_1.runInstall)(repoRoot, packageManager, pmc);
if (nxCloudChoice === 'yes') {

@@ -78,0 +79,0 @@ output_1.output.log({ title: '🛠️ Setting up Nx Cloud' });

@@ -45,7 +45,8 @@ "use strict";

}
const pmc = (0, package_manager_1.getPackageManagerCommand)();
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
(0, utils_1.updateGitIgnore)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager);
output_1.output.log({ title: '📦 Installing dependencies' });
(0, utils_1.runInstall)(repoRoot, pmc);
(0, utils_1.runInstall)(repoRoot, packageManager, pmc);
}

@@ -10,2 +10,3 @@ "use strict";

const output_1 = require("../../../../utils/output");
const package_manager_1 = require("../../../../utils/package-manager");
const package_json_1 = require("../../../../utils/package-json");

@@ -90,5 +91,6 @@ const utils_1 = require("../utils");

function installDependencies() {
(0, utils_1.addDepsToPackageJson)(repoRoot);
const packageManager = (0, package_manager_1.detectPackageManager)(repoRoot);
(0, utils_1.addDepsToPackageJson)(repoRoot, packageManager);
addPluginDependencies();
(0, utils_1.runInstall)(repoRoot);
(0, utils_1.runInstall)(repoRoot, packageManager);
}

@@ -95,0 +97,0 @@ function addPluginDependencies() {

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupIntegratedWorkspace = setupIntegratedWorkspace;
const child_process_1 = require("child_process");
const package_manager_1 = require("../../../../utils/package-manager");
const child_process_1 = require("../../../../utils/child-process");
function setupIntegratedWorkspace() {
const pmc = (0, package_manager_1.getPackageManagerCommand)();
(0, child_process_1.execSync)(`${pmc.exec} nx g @nx/angular:ng-add`, {
(0, child_process_1.runNxSync)(`g @nx/angular:ng-add`, {
stdio: [0, 1, 2],
windowsHide: true,
});
}

@@ -110,2 +110,5 @@ "use strict";

output_1.output.log({ title: '📝 Setting up workspace' });
// Intentionally not runNxSync: legacyMigrationCommand is either the Angular
// CLI (`ng g ...:ng-add`) or a version-pinned `nx@<version> init`, neither
// of which is the local nx that runNxSync would resolve.
(0, child_process_1.execSync)(`${pmc.exec} ${legacyMigrationCommand}`, {

@@ -112,0 +115,0 @@ stdio: [0, 1, 2],

@@ -72,3 +72,6 @@ "use strict";

// This is needed when using a global nx with dot-nx, otherwise running any nx command using global command will fail due to missing modules.
// Pipe stderr so failures surface in telemetry instead of bare "Command failed: ./nx --version".
// Intentionally not runNxSync: when the repo has a package.json it would run
// `<pm> exec nx` instead of the wrapper, but this call must run the
// just-written wrapper itself so it bootstraps .nx/installation.
// Pipe stderr so failures surface in telemetry instead of bare "Command failed".
try {

@@ -75,0 +78,0 @@ (0, child_process_1.execSync)(getDotNxWrapperVersionCommand(), {

import { NxJsonConfiguration, TargetDefaultEntry, TargetDefaults } from '../../../config/nx-json';
import { PackageJson } from '../../../utils/package-json';
import { PackageManagerCommands } from '../../../utils/package-manager';
import { PackageManager, PackageManagerCommands } from '../../../utils/package-manager';
export declare function createNxJsonFile(repoRoot: string, topologicalTargets: string[], cacheableOperations: string[], scriptOutputs: {

@@ -16,5 +16,5 @@ [name: string]: string;

export declare function createNxJsonFromTurboJson(turboJson: Record<string, any>): NxJsonConfiguration;
export declare function addDepsToPackageJson(repoRoot: string, additionalPackages?: string[]): void;
export declare function addDepsToPackageJson(repoRoot: string, packageManager: PackageManager, additionalPackages?: string[]): void;
export declare function updateGitIgnore(root: string): void;
export declare function runInstall(repoRoot: string, pmc?: PackageManagerCommands): void;
export declare function runInstall(repoRoot: string, packageManager?: PackageManager, pmc?: PackageManagerCommands): void;
/**

@@ -21,0 +21,0 @@ * Coerce any thrown value into a non-empty telemetry string. The naive

@@ -21,5 +21,7 @@ "use strict";

const path_1 = require("path");
const semver_1 = require("semver");
const fileutils_1 = require("../../../utils/fileutils");
const output_1 = require("../../../utils/output");
const package_manager_1 = require("../../../utils/package-manager");
const acknowledge_build_scripts_1 = require("../../../utils/acknowledge-build-scripts");
const path_2 = require("../../../utils/path");

@@ -211,3 +213,3 @@ const versions_1 = require("../../../utils/versions");

}
function addDepsToPackageJson(repoRoot, additionalPackages) {
function addDepsToPackageJson(repoRoot, packageManager, additionalPackages) {
const path = (0, path_2.joinPathFragments)(repoRoot, `package.json`);

@@ -224,2 +226,5 @@ const json = (0, fileutils_1.readJsonFile)(path);

(0, fileutils_1.writeJsonFile)(path, json);
// nx has a postinstall script, which pnpm 11+ refuses to install
// unacknowledged.
(0, acknowledge_build_scripts_1.acknowledgeBuildScripts)(repoRoot, packageManager, { nx: true });
}

@@ -257,5 +262,19 @@ function updateGitIgnore(root) {

}
function runInstall(repoRoot, pmc = (0, package_manager_1.getPackageManagerCommand)()) {
function runInstall(repoRoot, packageManager = (0, package_manager_1.detectPackageManager)(repoRoot), pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager)) {
let command = pmc.install;
// Plugins added during init can pull build-script deps whose allowBuilds
// entries are only recorded by their init generators after this install;
// warn and skip for this one install, like pnpm 10 did.
if (packageManager === 'pnpm') {
try {
if ((0, semver_1.gte)((0, package_manager_1.getPackageManagerVersion)('pnpm', repoRoot), '11.0.0')) {
command += ' --config.strictDepBuilds=false';
}
}
catch {
// The version cannot be probed; run the install unmodified.
}
}
try {
(0, child_process_1.execSync)(pmc.install, {
(0, child_process_1.execSync)(command, {
stdio: ['ignore', 'ignore', 'pipe'],

@@ -262,0 +281,0 @@ encoding: 'utf8',

@@ -22,2 +22,3 @@ "use strict";

const path_1 = require("path");
const git_revision_1 = require("../../utils/git-revision");
const migrate_1 = require("./migrate");

@@ -55,2 +56,5 @@ Object.defineProperty(exports, "isHybridMigration", { enumerable: true, get: function () { return migrate_1.isHybridMigration; } });

const initialGitRef = parsedMigrationsJson['nx-console'].initialGitRef;
if (squashCommits && initialGitRef) {
(0, git_revision_1.assertValidGitSha)(initialGitRef.ref);
}
if ((0, fs_1.existsSync)(migrationsJsonPath)) {

@@ -64,9 +68,5 @@ (0, fs_1.rmSync)(migrationsJsonPath);

});
(0, child_process_1.execSync)(`git commit -m "${commitMessage}" --no-verify`, {
cwd: workspacePath,
encoding: 'utf-8',
windowsHide: true,
});
commit(workspacePath, commitMessage);
if (squashCommits && initialGitRef) {
(0, child_process_1.execSync)(`git reset --soft ${initialGitRef.ref}`, {
(0, child_process_1.execFileSync)('git', ['reset', '--soft', initialGitRef.ref], {
cwd: workspacePath,

@@ -76,9 +76,13 @@ encoding: 'utf-8',

});
(0, child_process_1.execSync)(`git commit -m "${commitMessage}" --no-verify`, {
cwd: workspacePath,
encoding: 'utf-8',
windowsHide: true,
});
commit(workspacePath, commitMessage);
}
}
function commit(workspacePath, commitMessage) {
(0, child_process_1.execSync)('git commit --no-verify -F -', {
cwd: workspacePath,
encoding: 'utf-8',
windowsHide: true,
input: commitMessage,
});
}
async function runSingleMigration(workspacePath, migration, configuration) {

@@ -349,3 +353,4 @@ try {

if (existing.changedFiles.length > 0) {
(0, child_process_1.execSync)(`git reset --hard ${existing.ref}^`, {
(0, git_revision_1.assertValidGitSha)(existing.ref);
(0, child_process_1.execFileSync)('git', ['reset', '--hard', `${existing.ref}^`], {
cwd: workspacePath,

@@ -352,0 +357,0 @@ encoding: 'utf-8',

import { MigrationsJson, PackageJsonUpdateForPackage as PackageUpdate } from '../../config/misc-interfaces';
import { NxJsonConfiguration } from '../../config/nx-json';
import { FileChange } from '../../generators/tree';
import { ArrayPackageGroup, PackageJson } from '../../utils/package-json';

@@ -13,2 +12,3 @@ import { PackageManagerCommands } from '../../utils/package-manager';

import { normalizeVersion } from './version-utils';
export * from './execute-migration';
export { normalizeVersion };

@@ -178,10 +178,2 @@ export interface ResolvedMigrationConfiguration extends MigrationsJson {

export declare function generateMigrationsJsonAndUpdatePackageJson(root: string, opts: GenerateMigrations, fetch?: MigratorOptions['fetch']): Promise<void>;
/**
* Detects npm peer-dependency resolution failures. Keyed on the `ERESOLVE`
* error code, which npm consistently emits for this class of failure across
* v7+ (`npm ERR! code ERESOLVE` / `npm error code ERESOLVE`). Falls back to a
* small set of stable phrases in case the code line is missing from the
* captured output.
*/
export declare function isNpmPeerDepsError(stderr: string): boolean;
type ExecutableMigration = {

@@ -245,30 +237,2 @@ package: string;

}>;
export declare class ChangedDepInstaller {
private readonly root;
private readonly shouldSkipInstall;
private initialDeps;
private _skippedInstall;
constructor(root: string, shouldSkipInstall?: boolean);
get skippedInstall(): boolean;
installDepsIfChanged(): Promise<void>;
}
export declare function runNxOrAngularMigration(root: string, migration: {
package: string;
name: string;
description?: string;
version: string;
}, isVerbose: boolean, captureGeneratorOutput?: boolean, resolvedCollection?: {
collection: MigrationsJson;
collectionPath: string;
}): Promise<{
changes: FileChange[];
nextSteps: string[];
agentContext: string[];
logs: string;
madeChanges: boolean;
}>;
export declare function parseMigrationReturn(value: unknown): {
nextSteps: string[];
agentContext: string[];
};
export declare function migrate(root: string, args: {

@@ -278,10 +242,2 @@ [k: string]: any;

export declare function runMigration(): Promise<number>;
export declare function readMigrationCollection(packageName: string, root: string): {
collection: MigrationsJson;
collectionPath: string;
};
export declare function getImplementationPath(collection: MigrationsJson, collectionPath: string, name: string, migrationVersion?: string): {
path: string;
fnSymbol: string;
};
/**

@@ -288,0 +244,0 @@ * Resolves a migration's collection once and derives everything the run loop

@@ -29,3 +29,2 @@ "use strict";

const ora = require('ora');
const open = require('open');
function onlyDefaultRunnerIsUsed(nxJson) {

@@ -165,2 +164,3 @@ const defaultRunner = nxJson.tasksRunnerOptions?.default?.runner;

await sleep(2000);
const { default: open } = await new Function('return import("open")')();
await open(connectCloudUrl);

@@ -167,0 +167,0 @@ cloudConnectSpinner.succeed();

@@ -42,2 +42,4 @@ "use strict";

const pmc = (0, package_manager_1.getPackageManagerCommand)();
// Intentionally not runNxSync: this invokes the separate `nx-cloud` binary,
// not the nx CLI that runNxSync resolves.
(0, child_process_1.execSync)(`${pmc.exec} nx-cloud upload-and-show-run-details`, {

@@ -44,0 +46,0 @@ stdio: [0, 1, 2],

@@ -446,4 +446,6 @@ "use strict";

}
// Extract issue references from commit body
for (const m of commit.body.matchAll(IssueRE)) {
// Extract issue references from commit body, only when linked via a closing
// keyword so that other repos' issue numbers mentioned in prose are not
// picked up (e.g. "web-infra-dev/rspack#2292")
for (const m of commit.body.matchAll(IssueClosingKeywordRE)) {
if (!references.some((i) => i.value === m[1])) {

@@ -477,2 +479,4 @@ references.push({ type: 'issue', value: m[1] });

const IssueRE = /(#\d+)/gm;
// GitHub style issue closing keywords, e.g. "Fixes #1234"
const IssueClosingKeywordRE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+(#\d+)/gim;
const ChangedFileRegex = /(A|M|D|R\d*|C\d*)\t([^\t\n]*)\t?(.*)?/gm;

@@ -479,0 +483,0 @@ const RevertHashRE = /This reverts commit (?<hash>[\da-f]{40})./gm;

@@ -89,4 +89,4 @@ import type { NxReleaseDockerConfiguration, NxReleaseVersionConfiguration } from '../../../config/nx-json';

type: string;
dependencyCollection: string;
rawVersionSpec: string;
dependencyCollection: string | null;
rawVersionSpec: string | null;
}[]>;

@@ -93,0 +93,0 @@ readonly releaseGroupToFilteredProjects: Map<ReleaseGroupWithName, Set<string>>;

@@ -238,3 +238,3 @@ "use strict";

}
const open = require('open');
const { default: open } = await new Function('return import("open")')();
await open(result.url)

@@ -241,0 +241,0 @@ .then(() => {

@@ -160,3 +160,3 @@ "use strict";

}
const open = require('open');
const { default: open } = await new Function('return import("open")')();
await open(result.url)

@@ -163,0 +163,0 @@ .then(() => {

@@ -27,4 +27,4 @@ import { ProjectGraph } from '../../../config/project-graph';

type: string;
dependencyCollection: string;
rawVersionSpec: string;
dependencyCollection: string | null;
rawVersionSpec: string | null;
}[];

@@ -31,0 +31,0 @@ }

@@ -119,4 +119,13 @@ "use strict";

const taskGraph = (0, create_task_graph_1.createTaskGraph)(graph, extraTargetDeps, [projectName], [targetName], configuration, {});
const rootId = (0, utils_1.createTaskId)(projectName, targetName, configuration);
const directDeps = taskGraph.dependencies[rootId] ?? [];
// `createTaskGraph` resolves the target's `defaultConfiguration`, so the real
// root task id may carry a `:config` suffix that `createTaskId` with the raw
// configuration would omit. Find the root task by (project, target) instead.
const rootTask = Object.values(taskGraph.tasks).find((task) => task.target.project === projectName && task.target.target === targetName);
const rootId = rootTask?.id ?? (0, utils_1.createTaskId)(projectName, targetName, configuration);
// Continuous dependencies (e.g. dev servers) live in a separate map but are
// direct dependencies just like the entries in `dependencies`.
const directDeps = [
...(taskGraph.dependencies[rootId] ?? []),
...(taskGraph.continuousDependencies[rootId] ?? []),
];
const directDepSet = new Set(directDeps);

@@ -123,0 +132,0 @@ const depSourceIndices = directDeps.map((depTaskId) => {

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.showTargetInputsHandler = showTargetInputsHandler;
const workspace_root_1 = require("../../../utils/workspace-root");
const handle_import_1 = require("../../../utils/handle-import");
const utils_1 = require("./utils");
const check_task_files_1 = require("../../../hasher/check-task-files");
const utils_1 = require("../../../tasks-runner/utils");
const utils_2 = require("./utils");
// ── Handler ─────────────────────────────────────────────────────────
async function showTargetInputsHandler(args) {
const t = await (0, utils_1.resolveTarget)(args);
const usesCustomHasher = (0, utils_1.hasCustomHasher)(t.projectName, t.targetName, t.graph);
if (usesCustomHasher) {
renderCustomHasherWarning(t.projectName, t.targetName, args);
const t = await (0, utils_2.resolveTarget)(args);
const { projectName, targetName, configuration } = t;
if ((0, utils_2.hasCustomHasher)(projectName, targetName, t.graph)) {
renderCustomHasherWarning(projectName, targetName, args);
process.exitCode = 1;
return;
}
const hashInputs = await resolveInputFiles(t);
const taskId = (0, utils_1.createTaskId)(projectName, targetName, configuration);
const hashInputs = await (0, check_task_files_1.getTaskRawInputs)(taskId, {
projectGraph: t.graph,
nxJson: t.nxJson,
});
if (!hashInputs) {
throw new Error(`Could not find hash plan for task "${taskId}".`);
}
if (args.check !== undefined) {
const checkItems = (0, utils_1.deduplicateFolderEntries)(args.check);
const results = checkItems.map((input) => resolveCheckFromInputs(input, t.projectName, t.targetName, hashInputs));
if (results.length >= 2) {
renderBatchCheckInputs(results, t.projectName, t.targetName);
}
else {
for (const data of results)
renderCheckInput(data);
}
for (const data of results) {
process.exitCode ||=
data.isInput || data.containedInputFiles.length ? 0 : 1;
}
const results = await checkInputs(taskId, args.check, hashInputs);
(0, utils_2.renderCheckResults)(results, projectName, targetName, 'input');
(0, utils_2.setCheckExitCode)(results);
return;
}
renderInputs({ project: t.projectName, target: t.targetName, ...hashInputs }, t.node.data.targets[t.targetName].inputs, args);
renderInputs({ project: projectName, target: targetName, ...hashInputs }, t.node.data.targets[targetName].inputs, args);
}
async function resolveInputFiles(t) {
const { projectName, targetName, configuration, graph, nxJson } = t;
const { HashPlanInspector } = (await (0, handle_import_1.handleImport)('../../../hasher/hash-plan-inspector.js', __dirname));
const inspector = new HashPlanInspector(graph, workspace_root_1.workspaceRoot, nxJson);
await inspector.init();
const plan = inspector.inspectTaskInputs({
project: projectName,
target: targetName,
configuration,
// ── Data resolution ─────────────────────────────────────────────────
async function checkInputs(taskId, check, hashInputs) {
// checkFilesAreInputs matches environment/runtime/external names against the
// raw argument and paths against the workspace-relative form, so both are
// passed through — it has no cwd of its own.
const candidates = (0, utils_2.deduplicateFolderEntries)(check).map((value) => ({
value,
path: (0, utils_2.normalizePath)(value),
}));
const { categories } = await (0, check_task_files_1.checkFilesAreInputs)(taskId, candidates);
return candidates.map(({ value, path }) => {
const category = categories.get(value);
return {
value,
file: path,
matched: !!category,
category,
contained: category ? [] : (0, utils_2.pathsUnder)(path, hashInputs.files),
};
});
const targetConfig = graph.nodes[projectName]?.data?.targets?.[targetName];
const effectiveConfig = configuration ?? targetConfig?.defaultConfiguration;
const taskId = effectiveConfig
? `${projectName}:${targetName}:${effectiveConfig}`
: `${projectName}:${targetName}`;
const result = plan[taskId];
if (!result) {
throw new Error(`Could not find hash plan for task "${taskId}". Available tasks: ${Object.keys(plan).join(', ')}`);
}
return result;
}
function resolveCheckFromInputs(rawValue, projectName, targetName, inputs) {
for (const [category, arr] of [
['environment', inputs.environment],
['runtime', inputs.runtime],
['external', inputs.external],
['depOutputs', inputs.depOutputs],
]) {
if (arr.includes(rawValue)) {
return {
value: rawValue,
file: rawValue,
project: projectName,
target: targetName,
isInput: true,
matchedCategory: category,
containedInputFiles: [],
};
}
}
const fileToCheck = (0, utils_1.normalizePath)(rawValue);
const isFile = inputs.files.includes(fileToCheck);
let containedInputFiles = [];
if (!isFile) {
if (fileToCheck === '') {
containedInputFiles = inputs.files;
}
else {
const dirPrefix = fileToCheck.endsWith('/')
? fileToCheck
: fileToCheck + '/';
containedInputFiles = inputs.files.filter((f) => f.startsWith(dirPrefix));
}
}
return {
value: rawValue,
file: fileToCheck,
project: projectName,
target: targetName,
isInput: isFile,
matchedCategory: isFile || containedInputFiles.length > 0
? 'files'
: null,
containedInputFiles,
};
}
// ── Render ──────────────────────────────────────────────────────────
function renderInputs(data, configuredInputs, args) {
if (args.json) {
const jsonData = data;
const result = {};
for (const [k, v] of Object.entries(jsonData)) {
if (Array.isArray(v) && v.length === 0)
continue;
result[k] = v;
}
console.log(JSON.stringify(result, null, 2));
(0, utils_2.printJson)(data);
return;
}
const c = (0, utils_1.pc)();
const c = (0, utils_2.pc)();
console.log(`${c.bold('Inputs for')} ${c.cyan(data.project)}:${c.green(data.target)}`);
if (configuredInputs && configuredInputs.length > 0) {
(0, utils_1.printList)('Configured inputs', configuredInputs.map((i) => typeof i === 'string' ? i : JSON.stringify(i)));
if (configuredInputs?.length) {
(0, utils_2.printList)('Configured inputs', configuredInputs.map((i) => typeof i === 'string' ? i : JSON.stringify(i)));
}
(0, utils_1.printList)('External dependencies', [...data.external].sort());
(0, utils_1.printList)('Runtime inputs', [...data.runtime].sort());
(0, utils_1.printList)('Environment variables', [...data.environment].sort());
(0, utils_1.printList)(`Files (${data.files.length})`, [...data.files, ...data.depOutputs].sort());
(0, utils_2.printList)('External dependencies', [...data.external].sort());
(0, utils_2.printList)('Runtime inputs', [...data.runtime].sort());
(0, utils_2.printList)('Environment variables', [...data.environment].sort());
(0, utils_2.printList)(`Files (${data.files.length})`, [...data.files, ...data.depOutputs].sort());
}
function renderCheckInput(data) {
const c = (0, utils_1.pc)();
const categoryLabel = data.matchedCategory
? ` (${data.matchedCategory})`
: '';
if (data.isInput) {
console.log(`${c.green('✓')} ${c.bold(data.value)} is an input for ${c.cyan(data.project)}:${c.green(data.target)}${categoryLabel}`);
}
else if (data.containedInputFiles.length > 0) {
console.log(`${c.yellow('~')} ${c.bold(data.file)} is a directory containing ${c.bold(String(data.containedInputFiles.length))} input file(s) for ${c.cyan(data.project)}:${c.green(data.target)}`);
for (const f of [...data.containedInputFiles].sort())
console.log(` ${f}`);
}
else {
console.log(`${c.red('✗')} ${c.bold(data.value)} is ${c.red('not')} an input for ${c.cyan(data.project)}:${c.green(data.target)}`);
}
}
function renderBatchCheckInputs(results, projectName, targetName) {
const c = (0, utils_1.pc)();
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
const matched = [];
const directories = [];
const unmatched = [];
for (const r of results) {
if (r.isInput) {
matched.push(r.value);
}
else if (r.containedInputFiles.length > 0) {
directories.push({ value: r.file, count: r.containedInputFiles.length });
}
else {
unmatched.push(r.value);
}
}
if (matched.length > 0 || directories.length > 0) {
console.log(`\n${c.green('✓')} These arguments were inputs for ${label}:`);
for (const v of matched)
console.log(` ${v}`);
for (const d of directories) {
console.log(` ${d.value} (directory containing ${d.count} input files)`);
}
}
if (unmatched.length > 0) {
console.log(`\n${c.red('✗')} These arguments were ${c.red('not')} inputs for ${label}:`);
for (const v of unmatched)
console.log(` ${v}`);
}
}
function renderCustomHasherWarning(projectName, targetName, args) {
const c = (0, utils_1.pc)();
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
const c = (0, utils_2.pc)();
if (args.json) {
console.log(JSON.stringify({
(0, utils_2.printJson)({
project: projectName,
target: targetName,
warning: 'This target uses a custom hasher. Configured inputs do not affect the cache hash.',
}, null, 2));
});
return;
}
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
console.log(`\n${c.yellow('⚠')} ${label} uses a ${c.yellow('custom hasher')}.`);

@@ -184,0 +81,0 @@ console.log(` Configured inputs do not affect the cache hash for this target.`);

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.showTargetOutputsHandler = showTargetOutputsHandler;
const check_task_files_1 = require("../../../hasher/check-task-files");
const utils_1 = require("../../../tasks-runner/utils");
const workspace_root_1 = require("../../../utils/workspace-root");
const utils_2 = require("./utils");

@@ -10,187 +10,62 @@ // ── Handler ─────────────────────────────────────────────────────────

const t = await (0, utils_2.resolveTarget)(args);
const outputsData = resolveOutputsData(t);
const { projectName, targetName, configuration } = t;
const taskId = (0, utils_1.createTaskId)(projectName, targetName, configuration);
const outputs = await (0, check_task_files_1.getTaskOutputs)(taskId, {
projectGraph: t.graph,
nxJson: t.nxJson,
});
if (args.check !== undefined) {
const checkItems = (0, utils_2.deduplicateFolderEntries)(args.check);
const results = checkItems.map((o) => resolveCheckOutputData(o, outputsData));
if (results.length >= 2) {
renderBatchCheckOutputs(results, outputsData.project, outputsData.target);
}
else {
for (const data of results)
renderCheckOutput(data);
}
for (const data of results) {
process.exitCode ||=
data.matchedOutput ||
data.containedOutputPaths.length ||
data.containedExpandedOutputs.length
? 0
: 1;
}
const results = await checkOutputs(taskId, args.check, outputs);
(0, utils_2.renderCheckResults)(results, projectName, targetName, 'output');
(0, utils_2.setCheckExitCode)(results);
return;
}
renderOutputs(outputsData, args);
renderOutputs(projectName, targetName, outputs, args);
}
function resolveOutputsData(t) {
const { projectName, targetName, configuration, node } = t;
const resolvedOutputs = (0, utils_1.getOutputsForTargetAndConfiguration)({ project: projectName, target: targetName, configuration }, {}, node);
const targetConfig = node.data.targets?.[targetName];
const configuredOutputs = targetConfig?.outputs ?? [];
const mergedOptions = {
...targetConfig?.options,
...(configuration
? targetConfig?.configurations?.[configuration]
: undefined),
};
const unresolvedOutputs = configuredOutputs.filter((o) => {
if (!/\{options\./.test(o))
return false;
const unresolved = o.match(/\{options\.([^}]+)\}/g);
return unresolved?.some((token) => {
const key = token.slice('{options.'.length, -1);
return mergedOptions[key] === undefined;
});
// ── Data resolution ─────────────────────────────────────────────────
async function checkOutputs(taskId, check, { resolved, expanded }) {
const checkItems = (0, utils_2.deduplicateFolderEntries)(check);
const paths = checkItems.map(utils_2.normalizePath);
// checkFilesAreOutputs handles exact, directory-prefix and glob matching (and
// `!` exclusions) through the same native engine the task runner uses.
const { matched } = await (0, check_task_files_1.checkFilesAreOutputs)(taskId, paths);
const matchedPaths = new Set(matched);
return checkItems.map((value, i) => {
const path = paths[i];
const isMatch = matchedPaths.has(path);
return {
value,
file: path,
matched: isMatch,
contained: isMatch
? []
: [
...new Set([
...(0, utils_2.pathsUnder)(path, resolved),
...(0, utils_2.pathsUnder)(path, expanded),
]),
],
};
});
let expandedOutputs;
try {
const { expandOutputs } = require('../../../native');
expandedOutputs = expandOutputs(workspace_root_1.workspaceRoot, resolvedOutputs);
}
catch {
expandedOutputs = resolvedOutputs;
}
return {
project: projectName,
target: targetName,
outputPaths: resolvedOutputs,
expandedOutputs,
unresolvedOutputs,
};
}
function resolveCheckOutputData(rawFileToCheck, outputsData) {
const fileToCheck = (0, utils_2.normalizePath)(rawFileToCheck);
const { outputPaths, expandedOutputs } = outputsData;
let matchedOutput = null;
for (const outputPath of outputPaths) {
const normalizedOutput = outputPath.replace(/\\/g, '/');
if (fileToCheck === normalizedOutput ||
fileToCheck.startsWith(normalizedOutput + '/')) {
matchedOutput = outputPath;
break;
}
}
if (!matchedOutput && expandedOutputs.includes(fileToCheck)) {
matchedOutput = fileToCheck;
}
let containedOutputPaths = [];
let containedExpandedOutputs = [];
if (!matchedOutput) {
if (fileToCheck === '') {
containedOutputPaths = [...outputPaths];
containedExpandedOutputs = [...expandedOutputs];
}
else {
const dirPrefix = fileToCheck.endsWith('/')
? fileToCheck
: fileToCheck + '/';
containedOutputPaths = outputPaths.filter((o) => o.replace(/\\/g, '/').startsWith(dirPrefix));
containedExpandedOutputs = expandedOutputs.filter((o) => o.replace(/\\/g, '/').startsWith(dirPrefix));
}
}
return {
value: rawFileToCheck,
file: fileToCheck,
project: outputsData.project,
target: outputsData.target,
matchedOutput,
containedOutputPaths,
containedExpandedOutputs,
};
}
// ── Render ──────────────────────────────────────────────────────────
function renderOutputs(data, args) {
function renderOutputs(project, target, { resolved, expanded, unresolved }, args) {
if (args.json) {
const jsonData = data;
const result = {};
for (const [k, v] of Object.entries(jsonData)) {
if (Array.isArray(v) && v.length === 0)
continue;
result[k] = v;
}
console.log(JSON.stringify(result, null, 2));
(0, utils_2.printJson)({
project,
target,
outputPaths: resolved,
expandedOutputs: expanded,
unresolvedOutputs: unresolved,
});
return;
}
const c = (0, utils_2.pc)();
console.log(`${c.bold('Output paths for')} ${c.cyan(data.project)}:${c.green(data.target)}`);
if (data.outputPaths.length > 0) {
(0, utils_2.printList)('Configured outputs', data.outputPaths);
}
if (data.expandedOutputs.length > 0) {
(0, utils_2.printList)('Resolved outputs', data.expandedOutputs);
}
if (data.unresolvedOutputs.length > 0) {
(0, utils_2.printList)(`${c.yellow('Unresolved outputs')} (option not set)`, data.unresolvedOutputs);
}
if (data.outputPaths.length === 0 && data.unresolvedOutputs.length === 0) {
console.log(`${c.bold('Output paths for')} ${c.cyan(project)}:${c.green(target)}`);
(0, utils_2.printList)('Configured outputs', resolved);
(0, utils_2.printList)('Resolved outputs', expanded);
(0, utils_2.printList)(`${c.yellow('Unresolved outputs')} (option not set)`, unresolved);
if (resolved.length === 0 && unresolved.length === 0) {
console.log(`\n No outputs configured for this target.`);
}
}
function renderCheckOutput(data) {
const c = (0, utils_2.pc)();
const displayPath = data.value || data.file;
if (data.matchedOutput) {
console.log(`${c.green('✓')} ${c.bold(displayPath)} is an output of ${c.cyan(data.project)}:${c.green(data.target)}`);
}
else if (data.containedOutputPaths.length > 0 ||
data.containedExpandedOutputs.length > 0) {
const uniquePaths = new Set([
...data.containedOutputPaths,
...data.containedExpandedOutputs,
]);
console.log(`${c.yellow('~')} ${c.bold(displayPath)} is a directory containing ${c.bold(String(uniquePaths.size))} output path(s) for ${c.cyan(data.project)}:${c.green(data.target)}`);
const extraExpanded = data.containedExpandedOutputs.filter((o) => !data.containedOutputPaths.includes(o));
if (extraExpanded.length > 0) {
(0, utils_2.printList)('Expanded outputs', extraExpanded);
}
}
else {
console.log(`${c.red('✗')} ${c.bold(displayPath)} is ${c.red('not')} an output of ${c.cyan(data.project)}:${c.green(data.target)}`);
}
}
function renderBatchCheckOutputs(results, projectName, targetName) {
const c = (0, utils_2.pc)();
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
const matched = [];
const directories = [];
const unmatched = [];
for (const r of results) {
if (r.matchedOutput) {
matched.push(r.value);
}
else {
const uniqueCount = new Set([
...r.containedOutputPaths,
...r.containedExpandedOutputs,
]).size;
if (uniqueCount > 0) {
directories.push({ value: r.file, count: uniqueCount });
}
else {
unmatched.push(r.value);
}
}
}
if (matched.length > 0 || directories.length > 0) {
console.log(`\n${c.green('✓')} These arguments were outputs of ${label}:`);
for (const v of matched)
console.log(` ${v}`);
for (const d of directories) {
console.log(` ${d.value} (directory containing ${d.count} output paths)`);
}
}
if (unmatched.length > 0) {
console.log(`\n${c.red('✗')} These arguments were ${c.red('not')} outputs of ${label}:`);
for (const v of unmatched)
console.log(` ${v}`);
}
}

@@ -27,1 +27,29 @@ import type { NxJsonConfiguration } from '../../../config/nx-json';

export declare function printList(header: string, items: unknown[], prefix?: string): void;
export declare function printJson(data: Record<string, unknown>): void;
/** The paths that live under `dir`. An empty `dir` is the workspace root. */
export declare function pathsUnder(dir: string, paths: string[]): string[];
export interface CheckResult {
/** The argument as the user typed it. */
value: string;
/** Workspace-relative form of `value`. */
file: string;
matched: boolean;
/** Which rule matched, when the caller can name one. */
category?: string;
/** Matches found underneath `value`, when it is a directory. */
contained: string[];
}
declare const NOUNS: {
readonly input: {
readonly preposition: 'for';
readonly contained: 'input file';
};
readonly output: {
readonly preposition: 'of';
readonly contained: 'output path';
};
};
type CheckNoun = keyof typeof NOUNS;
export declare function renderCheckResults(results: CheckResult[], project: string, target: string, noun: CheckNoun): void;
export declare function setCheckExitCode(results: CheckResult[]): void;
export {};

@@ -9,2 +9,6 @@ "use strict";

exports.printList = printList;
exports.printJson = printJson;
exports.pathsUnder = pathsUnder;
exports.renderCheckResults = renderCheckResults;
exports.setCheckExitCode = setCheckExitCode;
const path_1 = require("path");

@@ -33,9 +37,13 @@ const calculate_default_project_name_1 = require("../../../config/calculate-default-project-name");

const { projectName, targetName, configurationName } = resolveTargetIdentifier(args, graph, nxJson);
// `resolveProjectNode` accepts a pattern specifier (`my-*`), so the concrete
// name it resolved to is the one everything downstream has to use — it is a
// lookup key for the task id, not just a label.
const node = resolveProjectNode(projectName, graph);
const resolvedProjectName = node.name;
if (!node.data.targets?.[targetName]) {
reportTargetNotFound(projectName, targetName, node);
reportTargetNotFound(resolvedProjectName, targetName, node);
}
const configuration = configurationName ?? args.configuration;
if (configuration) {
validateConfiguration(projectName, targetName, configuration, node.data.targets[targetName]);
validateConfiguration(resolvedProjectName, targetName, configuration, node.data.targets[targetName]);
}

@@ -45,3 +53,3 @@ return {

nxJson,
projectName,
projectName: resolvedProjectName,
targetName,

@@ -189,1 +197,73 @@ configuration,

}
function printJson(data) {
const result = {};
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value) && value.length === 0)
continue;
result[key] = value;
}
console.log(JSON.stringify(result, null, 2));
}
/** The paths that live under `dir`. An empty `dir` is the workspace root. */
function pathsUnder(dir, paths) {
if (dir === '')
return [...paths];
const prefix = dir.endsWith('/') ? dir : dir + '/';
return paths.filter((p) => p.startsWith(prefix));
}
const NOUNS = {
input: { preposition: 'for', contained: 'input file' },
output: { preposition: 'of', contained: 'output path' },
};
function renderCheckResults(results, project, target, noun) {
if (results.length >= 2) {
renderBatchCheckResults(results, project, target, noun);
return;
}
for (const result of results) {
renderCheckResult(result, project, target, noun);
}
}
function setCheckExitCode(results) {
for (const result of results) {
process.exitCode ||= result.matched || result.contained.length ? 0 : 1;
}
}
function renderCheckResult(result, project, target, noun) {
const c = pc();
const { preposition, contained } = NOUNS[noun];
const label = `${c.cyan(project)}:${c.green(target)}`;
if (result.matched) {
const category = result.category ? ` (${result.category})` : '';
console.log(`${c.green('✓')} ${c.bold(result.value)} is an ${noun} ${preposition} ${label}${category}`);
}
else if (result.contained.length > 0) {
console.log(`${c.yellow('~')} ${c.bold(result.file)} is a directory containing ${c.bold(String(result.contained.length))} ${contained}(s) ${preposition} ${label}`);
for (const item of [...result.contained].sort())
console.log(` ${item}`);
}
else {
console.log(`${c.red('✗')} ${c.bold(result.value)} is ${c.red('not')} an ${noun} ${preposition} ${label}`);
}
}
function renderBatchCheckResults(results, project, target, noun) {
const c = pc();
const { preposition, contained } = NOUNS[noun];
const label = `${c.cyan(project)}:${c.green(target)}`;
const matched = results.filter((r) => r.matched);
const directories = results.filter((r) => !r.matched && r.contained.length);
const unmatched = results.filter((r) => !r.matched && !r.contained.length);
if (matched.length > 0 || directories.length > 0) {
console.log(`\n${c.green('✓')} These arguments were ${noun}s ${preposition} ${label}:`);
for (const r of matched)
console.log(` ${r.value}`);
for (const r of directories) {
console.log(` ${r.file} (directory containing ${r.contained.length} ${contained}s)`);
}
}
if (unmatched.length > 0) {
console.log(`\n${c.red('✗')} These arguments were ${c.red('not')} ${noun}s ${preposition} ${label}:`);
for (const r of unmatched)
console.log(` ${r.value}`);
}
}

@@ -1,2 +0,2 @@

import { Argv, ParserConfigurationOptions } from 'yargs';
import type { Argv, ParserConfigurationOptions } from 'yargs';
interface ExcludeOptions {

@@ -3,0 +3,0 @@ exclude: string[];

@@ -44,2 +44,7 @@ "use strict";

}
else {
for (const projectName of projectNames) {
dependencies[projectName] = [];
}
}
const roots = Object.keys(dependencies).filter((d) => dependencies[d].length === 0);

@@ -46,0 +51,0 @@ const commandGraph = {

@@ -57,4 +57,6 @@ import type ChangelogRenderer from '../../release/changelog-renderer';

* Restrict the default to targets originated by a specific plugin
* (e.g. `@nx/vite`). Matches against the plugin that wrote the target's
* `executor` or `command`.
* (e.g. `@nx/vite`). Matches against the plugin from nx.json's `plugins`
* that wrote the target's `executor` or `command`. Targets whose
* executor/command comes from `project.json` or `package.json` have no
* source plugin and never match.
*/

@@ -791,3 +793,3 @@ plugin?: string;

* Default options for `nx affected`
* @deprecated use {@link defaultBase} instead. For more information see https://nx.dev/deprecated/affected-config#affected-config
* @deprecated use {@link defaultBase} instead. For more information see https://nx.dev/docs/reference/nx-json#default-base
*/

@@ -794,0 +796,0 @@ affected?: NxAffectedConfig;

@@ -29,4 +29,2 @@ import { ChildProcess } from 'child_process';

private _daemonReady;
private _out;
private _err;
private fileWatcherMessenger;

@@ -33,0 +31,0 @@ private fileWatcherReconnecting;

@@ -6,3 +6,2 @@ "use strict";

const child_process_1 = require("child_process");
const promises_1 = require("fs/promises");
const net_1 = require("net");

@@ -16,2 +15,3 @@ const node_fs_1 = require("node:fs");

const error_types_1 = require("../../project-graph/error-types");
const typescript_1 = require("../../plugins/js/utils/typescript");
const project_graph_1 = require("../../project-graph/project-graph");

@@ -67,4 +67,2 @@ const consume_messages_from_socket_1 = require("../../utils/consume-messages-from-socket");

this._daemonReady = null;
this._out = null;
this._err = null;
this.fileWatcherReconnecting = false;

@@ -133,6 +131,2 @@ this.fileWatcherCallbacks = new Map();

this._enabled = undefined;
this._out?.close();
this._err?.close();
this._out = null;
this._err = null;
// Clean up file watcher and project graph listener connections

@@ -956,16 +950,17 @@ this.fileWatcherMessenger?.close();

}
// Open the log handles into locals first. If the previous daemon's
// socket close handler fires reset() while we're awaiting these opens,
// it would null out this._out/this._err and the spawn below would hit
// `Cannot read properties of null (reading 'fd')`.
const [out, err] = await Promise.all([
(0, promises_1.open)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a'),
(0, promises_1.open)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a'),
]);
this._out = out;
this._err = err;
// Redirect the detached daemon's stdout/stderr into the log file. The
// child dup's these descriptors at spawn, so we close ours right after
// instead of holding them for the life of this process (Node >=26 turns a
// file descriptor closed during garbage collection into a fatal error).
const outFd = (0, node_fs_1.openSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a');
const errFd = (0, node_fs_1.openSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a');
logger_1.clientLogger.log(`[Client] Starting new daemon server in background`);
const backgroundProcess = (0, child_process_1.spawn)(process.execPath, [(0, path_1.join)(__dirname, `../server/start.js`)], {
const backgroundProcess = (0, child_process_1.spawn)(process.execPath, [
// Spawn with the same resolve conditions Nx uses for plugin entries so a
// source-loaded plugin's transitive workspace imports resolve to source.
...(0, typescript_1.getPluginResolveConditionNodeArgs)(),
(0, path_1.join)(__dirname, `../server/start.js`),
], {
cwd: workspace_root_1.workspaceRoot,
stdio: ['ignore', out.fd, err.fd],
stdio: ['ignore', outFd, errFd],
detached: true,

@@ -976,2 +971,5 @@ windowsHide: true,

});
// The child now owns dup'd copies of the descriptors, so release ours.
(0, node_fs_1.closeSync)(outFd);
(0, node_fs_1.closeSync)(errFd);
// if this process is the process that spawned the daemon,

@@ -978,0 +976,0 @@ // the daemon env is already up to date

import type { PostTasksExecutionContext, PreTasksExecutionContext } from '../../project-graph/plugins/public-api';
export declare function handleRunPreTasksExecution(context: PreTasksExecutionContext): Promise<{
error?: undefined;
response: NodeJS.ProcessEnv[];
description: string;
error?: undefined;
} | {

@@ -7,0 +7,0 @@ response?: undefined;

@@ -363,2 +363,9 @@ "use strict";

async function startServer() {
// Watch before scan: a file written during boot must be visible to the
// watcher or the scan below. Scan-first left a blind window where such
// files stayed invisible to both until an unrelated change arrived.
if (!(0, shutdown_utils_1.getWatcherInstance)()) {
(0, shutdown_utils_1.storeWatcherInstance)(await (0, watcher_1.watchWorkspace)(server, handleWorkspaceChanges));
logger_1.serverLogger.watcherLog(`Subscribed to changes within: ${workspace_root_1.workspaceRoot} (native)`);
}
(0, workspace_context_1.setupWorkspaceContext)(workspace_root_1.workspaceRoot);

@@ -422,6 +429,2 @@ // Initialize analytics for daemon process

daemonIsOutdated();
if (!(0, shutdown_utils_1.getWatcherInstance)()) {
(0, shutdown_utils_1.storeWatcherInstance)(await (0, watcher_1.watchWorkspace)(server, handleWorkspaceChanges));
logger_1.serverLogger.watcherLog(`Subscribed to changes within: ${workspace_root_1.workspaceRoot} (native)`);
}
if (!(0, shutdown_utils_1.getOutputWatcherInstance)()) {

@@ -428,0 +431,0 @@ (0, shutdown_utils_1.storeOutputWatcherInstance)(await (0, watcher_1.watchOutputFiles)(server, handleOutputsChanges));

@@ -41,1 +41,4 @@ /**

export { resolvePrompt, PromptResolutionError, } from './command-line/migrate/prompt-files';
export { checkFilesAreInputs, checkFilesAreOutputs, } from './hasher/check-task-files';
export { getCatalogManager, getCatalogDependenciesFromPackageJson, } from './utils/catalog';
export { acknowledgeBuildScripts } from './utils/acknowledge-build-scripts';
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PromptResolutionError = exports.resolvePrompt = exports.SchemaResolutionError = exports.ImplementationResolutionError = exports.resolveSchema = exports.resolveImplementation = exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.handleImport = exports.signalToCode = exports.globalSpinner = exports.readYamlFile = exports.isUsingPrettierInTree = exports.isCI = exports.interpolate = exports.requireWithTsconfigFallback = exports.forceRegisterEsmLoader = exports.loadTsFile = exports.registerTsProject = exports.LoadedNxPlugin = exports.retrieveProjectConfigurations = exports.findProjectForPath = exports.createProjectRootMappings = exports.createProjectRootMappingsFromProjectConfigurations = exports.hashMultiGlobWithWorkspaceContext = exports.hashWithWorkspaceContext = exports.hashObject = exports.splitByColons = exports.installPackageToTmpAsync = exports.installPackageToTmp = exports.readModulePackageJson = exports.stripIndent = exports.sortObjectByKeys = exports.combineOptionsForExecutor = exports.splitTarget = exports.getIgnoreObjectForTree = exports.readTargetDefaultsForTarget = exports.findMatchingProjects = exports.findMatchingConfigFiles = exports.readProjectConfigurationsFromRootMap = exports.mergeTargetConfigurations = exports.retrieveProjectConfigurationsWithAngularProjects = exports.calculateDefaultProjectName = exports.readNxJsonFromDisk = exports.parseExecutor = exports.getExecutorInformation = exports.createTempNpmDirectory = void 0;
exports.checkFilesAreOutputs = exports.checkFilesAreInputs = exports.PromptResolutionError = exports.resolvePrompt = exports.SchemaResolutionError = exports.ImplementationResolutionError = exports.resolveSchema = exports.resolveImplementation = exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.handleImport = exports.signalToCode = exports.globalSpinner = exports.readYamlFile = exports.isUsingPrettierInTree = exports.isCI = exports.interpolate = exports.requireWithTsconfigFallback = exports.forceRegisterEsmLoader = exports.loadTsFile = exports.registerTsProject = exports.LoadedNxPlugin = exports.retrieveProjectConfigurations = exports.findProjectForPath = exports.createProjectRootMappings = exports.createProjectRootMappingsFromProjectConfigurations = exports.hashMultiGlobWithWorkspaceContext = exports.hashWithWorkspaceContext = exports.hashObject = exports.splitByColons = exports.installPackageToTmpAsync = exports.installPackageToTmp = exports.readModulePackageJson = exports.stripIndent = exports.sortObjectByKeys = exports.combineOptionsForExecutor = exports.splitTarget = exports.getIgnoreObjectForTree = exports.readTargetDefaultsForTarget = exports.findMatchingProjects = exports.findMatchingConfigFiles = exports.readProjectConfigurationsFromRootMap = exports.mergeTargetConfigurations = exports.retrieveProjectConfigurationsWithAngularProjects = exports.calculateDefaultProjectName = exports.readNxJsonFromDisk = exports.parseExecutor = exports.getExecutorInformation = exports.createTempNpmDirectory = void 0;
exports.acknowledgeBuildScripts = exports.getCatalogDependenciesFromPackageJson = exports.getCatalogManager = void 0;
const tslib_1 = require("tslib");

@@ -93,1 +94,9 @@ /**

Object.defineProperty(exports, "PromptResolutionError", { enumerable: true, get: function () { return prompt_files_1.PromptResolutionError; } });
var check_task_files_1 = require("./hasher/check-task-files");
Object.defineProperty(exports, "checkFilesAreInputs", { enumerable: true, get: function () { return check_task_files_1.checkFilesAreInputs; } });
Object.defineProperty(exports, "checkFilesAreOutputs", { enumerable: true, get: function () { return check_task_files_1.checkFilesAreOutputs; } });
var catalog_1 = require("./utils/catalog");
Object.defineProperty(exports, "getCatalogManager", { enumerable: true, get: function () { return catalog_1.getCatalogManager; } });
Object.defineProperty(exports, "getCatalogDependenciesFromPackageJson", { enumerable: true, get: function () { return catalog_1.getCatalogDependenciesFromPackageJson; } });
var acknowledge_build_scripts_1 = require("./utils/acknowledge-build-scripts");
Object.defineProperty(exports, "acknowledgeBuildScripts", { enumerable: true, get: function () { return acknowledge_build_scripts_1.acknowledgeBuildScripts; } });

@@ -45,5 +45,15 @@ "use strict";

try {
return (0, json_1.readJson)(tree, (0, path_1.relative)(tree.root, require.resolve(extendsPath, {
paths: [tree.root],
})));
let resolvedExtendsPath;
try {
resolvedExtendsPath = require.resolve(extendsPath, {
paths: [tree.root],
});
}
catch {
// Tree roots without a node_modules folder (e.g. the in-memory trees
// used in tests) can't anchor module resolution; fall back to
// resolving from the running nx package.
resolvedExtendsPath = require.resolve(extendsPath);
}
return (0, json_1.readJson)(tree, (0, path_1.relative)(tree.root, resolvedExtendsPath));
}

@@ -50,0 +60,0 @@ catch (e) {

@@ -329,2 +329,3 @@ /**

duration: string
sampleRate: string
taskCount: string

@@ -491,3 +492,3 @@ projectCount: string

*/
export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string
export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string

@@ -499,3 +500,3 @@ /**

*/
export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void
export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void

@@ -563,2 +564,20 @@ export interface InputsInput {

/**
* Checks which `paths` match the given `globs`, using the same glob engine
* as the task hasher (`build_glob_set`). Used to statically match
* `dependentTasksOutputFiles` globs against candidate paths.
*/
export declare function matchGlobPaths(globs: Array<string>, paths: Array<string>): Array<boolean>
/**
* Statically checks which `paths` would be captured by the given output
* `entries`, without touching the file system. Mirrors `expand_outputs`
* semantics: entries match themselves and anything nested under them (so a
* directory entry captures its contents), negated (`!`-prefixed) entries
* exclude matches from the whole entry set, and a non-empty list with only
* negated entries matches everything not excluded. An empty list matches
* nothing.
*/
export declare function matchOutputPaths(entries: Array<string>, paths: Array<string>): Array<boolean>
/** Combined metadata for groups and processes */

@@ -565,0 +584,0 @@ export interface Metadata {

@@ -628,2 +628,4 @@ // prettier-ignore

module.exports.logDebug = nativeBinding.logDebug
module.exports.matchGlobPaths = nativeBinding.matchGlobPaths
module.exports.matchOutputPaths = nativeBinding.matchOutputPaths
module.exports.parseTaskStatus = nativeBinding.parseTaskStatus

@@ -630,0 +632,0 @@ module.exports.remove = nativeBinding.remove

@@ -136,2 +136,18 @@ "use strict";

const dependencies = [];
// Memoizes semver `satisfies(version, range)` for this dependency walk. The
// same (version, range) pairs recur across many edges, so the range parse
// happens once per distinct pair instead of once per edge. Scoped to the walk
// (not module-global) so V8 collects it when dependency creation finishes
// rather than retaining it for the daemon's lifetime.
const versionSatisfiesCache = new Map();
const cachedSatisfies = (version, range) => {
const key = `${version}\n${range}`;
const cached = versionSatisfiesCache.get(key);
if (cached !== undefined) {
return cached;
}
const result = (0, semver_1.satisfies)(version, range);
versionSatisfiesCache.set(key, result);
return result;
};
if (data.lockfileVersion > 1) {

@@ -151,3 +167,3 @@ Object.entries(data.packages).forEach(([path, snapshot]) => {

Object.entries(section).forEach(([name, versionRange]) => {
const target = findTarget(path, keyMap, name, versionRange);
const target = findTarget(path, keyMap, name, versionRange, cachedSatisfies);
if (target) {

@@ -169,3 +185,3 @@ const dep = {

Object.entries(data.dependencies).forEach(([packageName, snapshot]) => {
addV1NodeDependencies(`node_modules/${packageName}`, snapshot, dependencies, keyMap, ctx);
addV1NodeDependencies(`node_modules/${packageName}`, snapshot, dependencies, keyMap, ctx, cachedSatisfies);
});

@@ -175,3 +191,3 @@ }

}
function findTarget(sourcePath, keyMap, targetName, versionRange,
function findTarget(sourcePath, keyMap, targetName, versionRange, cachedSatisfies,
// When a package is found at a path but its version doesn't satisfy the

@@ -193,3 +209,4 @@ // range (e.g. due to npm overrides), we keep it as a fallback. npm already

const depVersion = versionRange.slice(versionRange.indexOf('@', 5) + 1);
if (nodeVersion === depVersion || (0, semver_1.satisfies)(nodeVersion, depVersion)) {
if (nodeVersion === depVersion ||
cachedSatisfies(nodeVersion, depVersion)) {
return child;

@@ -199,3 +216,3 @@ }

else if (child.data.version === versionRange ||
(0, semver_1.satisfies)(child.data.version, versionRange)) {
cachedSatisfies(child.data.version, versionRange)) {
return child;

@@ -212,9 +229,13 @@ }

}
return findTarget(sourcePath.split('node_modules/').slice(0, -1).join('node_modules/'), keyMap, targetName, versionRange, fallback);
// Walk one level up the nesting chain by dropping the trailing
// `node_modules/<pkg>` segment. Slash-index arithmetic avoids the
// split/slice/join array allocation on every hop.
const lastNodeModules = sourcePath.lastIndexOf('node_modules/');
return findTarget(lastNodeModules === -1 ? '' : sourcePath.substring(0, lastNodeModules), keyMap, targetName, versionRange, cachedSatisfies, fallback);
}
function addV1NodeDependencies(path, snapshot, dependencies, keyMap, ctx) {
function addV1NodeDependencies(path, snapshot, dependencies, keyMap, ctx, cachedSatisfies) {
if (keyMap.has(path) && snapshot.requires) {
const source = keyMap.get(path).name;
Object.entries(snapshot.requires).forEach(([name, versionRange]) => {
const target = findTarget(path, keyMap, name, versionRange);
const target = findTarget(path, keyMap, name, versionRange, cachedSatisfies);
if (target) {

@@ -233,3 +254,3 @@ const dep = {

Object.entries(snapshot.dependencies).forEach(([depName, depSnapshot]) => {
addV1NodeDependencies(`${path}/node_modules/${depName}`, depSnapshot, dependencies, keyMap, ctx);
addV1NodeDependencies(`${path}/node_modules/${depName}`, depSnapshot, dependencies, keyMap, ctx, cachedSatisfies);
});

@@ -241,3 +262,3 @@ }

Object.entries(peerDependencies).forEach(([depName, depSpec]) => {
const target = findTarget(path, keyMap, depName, depSpec);
const target = findTarget(path, keyMap, depName, depSpec, cachedSatisfies);
if (target) {

@@ -370,6 +391,7 @@ const dep = {

const remappedPackages = new Map();
const packageIndex = buildV3Index(rootLockFile.packages);
// add first level children
Object.values(graph.externalNodes).forEach((node) => {
if (node.name === `npm:${node.data.packageName}`) {
const mappedPackage = mapPackage(rootLockFile, node.data.packageName, node.data.version);
const mappedPackage = mapPackage(rootLockFile, packageIndex, node.data.packageName, node.data.version);
remappedPackages.set(mappedPackage.path, mappedPackage);

@@ -388,3 +410,3 @@ visitedNodes.set(node, {

const invertedGraph = (0, operators_1.reverse)(graph);
nestMappedPackages(invertedGraph, remappedPackages, nestedNodes, visitedNodes, rootLockFile);
nestMappedPackages(invertedGraph, remappedPackages, nestedNodes, visitedNodes, rootLockFile, packageIndex);
// initially we naively map package paths to topParent/../parent/child

@@ -399,3 +421,3 @@ // but some of those should be nested higher up the tree

}
function mapPackage(rootLockFile, packageName, version, parentPath = '') {
function mapPackage(rootLockFile, packageIndex, packageName, version, parentPath = '') {
const lockfileVersion = rootLockFile.lockfileVersion;

@@ -407,3 +429,3 @@ let valueV3, valueV1;

if (lockfileVersion > 1) {
valueV3 = findMatchingPackageV3(rootLockFile.packages, packageName, version);
valueV3 = findMatchingPackageV3(packageIndex, packageName, version);
}

@@ -417,3 +439,3 @@ return {

}
function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile) {
function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile, packageIndex) {
const initialSize = nestedNodes.size;

@@ -438,3 +460,3 @@ if (!initialSize) {

visitedNodes.get(targetNode).packagePaths.forEach((path) => {
const mappedPackage = mapPackage(rootLockFile, node.data.packageName, node.data.version, path + '/');
const mappedPackage = mapPackage(rootLockFile, packageIndex, node.data.packageName, node.data.version, path + '/');
result.set(mappedPackage.path, mappedPackage);

@@ -457,3 +479,3 @@ visitedNodes.get(node).packagePaths.add(mappedPackage.path);

else {
nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile);
nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile, packageIndex);
}

@@ -514,12 +536,34 @@ }

}
function findMatchingPackageV3(packages, name, version) {
for (const [key, { dev, peer, ...snapshot }] of Object.entries(packages)) {
if (key.endsWith(`node_modules/${name}`)) {
if ([
snapshot.version,
snapshot.resolved,
`npm:${snapshot.name}@${snapshot.version}`,
].includes(version)) {
return snapshot;
}
// Bucket packages by their trailing "node_modules/<name>" segment so a lookup
// scans only that name's copies instead of every package (was O(nodes *
// allPackages)). Mirrors the old `key.endsWith(node_modules/<name>)` match:
// the name is whatever follows the last "node_modules/" in the key.
function buildV3Index(packages) {
const index = new Map();
if (!packages)
return index;
const marker = 'node_modules/';
for (const key of Object.keys(packages)) {
const i = key.lastIndexOf(marker);
if (i === -1)
continue; // root "" / workspace paths never matched endsWith
const name = key.slice(i + marker.length);
let bucket = index.get(name);
if (!bucket)
index.set(name, (bucket = []));
bucket.push(packages[key]);
}
return index;
}
function findMatchingPackageV3(packageIndex, name, version) {
const bucket = packageIndex.get(name);
if (!bucket)
return undefined;
for (const { dev, peer, ...snapshot } of bucket) {
if ([
snapshot.version,
snapshot.resolved,
`npm:${snapshot.name}@${snapshot.version}`,
].includes(version)) {
return snapshot;
}

@@ -526,0 +570,0 @@ }

@@ -145,3 +145,8 @@ "use strict";

const patchInfo = data.patchedDependencies[specifier];
if (patchInfo && typeof patchInfo === 'object' && 'hash' in patchInfo) {
const patchHash = typeof patchInfo === 'string'
? patchInfo
: patchInfo && typeof patchInfo === 'object' && 'hash' in patchInfo
? patchInfo.hash
: undefined;
if (patchHash) {
const packageName = extractNameFromKey(specifier, false);

@@ -154,3 +159,3 @@ const versionSpecifier = getVersion(specifier, packageName) || null;

versionSpecifier,
hash: patchInfo.hash,
hash: patchHash,
});

@@ -183,3 +188,4 @@ }

const packageNames = new Set();
for (const [key, snapshot] of Object.entries(data.packages)) {
// pnpm omits the packages block for workspace-only lockfiles (no external deps)
for (const [key, snapshot] of Object.entries(data.packages ?? {})) {
let packageNameObj;

@@ -365,3 +371,4 @@ const originalPackageName = extractNameFromKey(key, isV5);

const results = [];
Object.keys(data.packages).forEach((key) => {
// pnpm omits the packages block for workspace-only lockfiles (no external deps)
Object.keys(data.packages ?? {}).forEach((key) => {
const snapshot = data.packages[key];

@@ -398,5 +405,8 @@ const nodes = keyMap.get(key);

const data = (0, pnpm_normalizer_1.parseAndNormalizePnpmLockfile)(rootLockFileContent);
const { lockfileVersion, packages, importers } = data;
const { snapshot: rootSnapshot, importers: requiredImporters } = mapRootSnapshot(packageJson, importers, packages, graph, +lockfileVersion, workspaceRoot);
const snapshots = mapSnapshots(data.packages, graph.externalNodes, +lockfileVersion);
const { lockfileVersion, importers } = data;
// pnpm omits the packages block for workspace-only lockfiles (no external deps)
const packages = data.packages ?? {};
const packageIndex = indexPackagesByName(packages, +lockfileVersion);
const { snapshot: rootSnapshot, importers: requiredImporters } = mapRootSnapshot(packageJson, importers, packages, packageIndex, graph, +lockfileVersion, workspaceRoot);
const snapshots = mapSnapshots(packages, packageIndex, graph.externalNodes, +lockfileVersion);
const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);

@@ -463,8 +473,6 @@ // Walk transitive workspace deps so every package copy-workspace-modules

}
function mapSnapshots(packages, nodes, lockfileVersion) {
function mapSnapshots(packages, packageIndex, nodes, lockfileVersion) {
const result = {};
Object.values(nodes).forEach((node) => {
const matchedKeys = findOriginalKeys(packages, node, lockfileVersion, {
returnFullKey: true,
});
const matchedKeys = findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey: true });
// the package manager doesn't check for types of dependencies

@@ -509,6 +517,31 @@ // so we can safely set all to prod

}
function findOriginalKeys(packages, { data: { packageName, version } }, lockfileVersion, { returnFullKey } = {}) {
// Bucket package keys by their package name so a node only scans its own name's
// versions instead of every key (was O(nodes * allPackages)). v5 is excluded
// below and keeps the full scan: its standard keys use a "/" separator while
// tarball keys use "@", so a single name index would misfile v5 tarballs.
function indexPackagesByName(packages, lockfileVersion) {
const isV5 = lockfileVersion < 6;
const index = new Map();
for (const key of Object.keys(packages)) {
const name = extractNameFromKey(key, isV5);
let bucket = index.get(name);
if (!bucket)
index.set(name, (bucket = []));
bucket.push([key, packages[key]]);
}
return index;
}
// npm alias version is "npm:<name>@<ver>"; extract <name> the same way
// versionIsAlias does so the index lookup matches the alias branch below.
function aliasTargetName(version) {
return version.slice('npm:'.length, version.indexOf('@', 'npm:'.length + 1));
}
const NO_CANDIDATES = [];
function findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey } = {}) {
const { data: { packageName, version }, } = node;
const candidates = lockfileVersion >= 6
? (packageIndex.get(version.startsWith('npm:') ? aliasTargetName(version) : packageName) ?? NO_CANDIDATES)
: Object.entries(packages);
const matchedKeys = [];
for (const key of Object.keys(packages)) {
const snapshot = packages[key];
for (const [key, snapshot] of candidates) {
// tarball package

@@ -572,6 +605,7 @@ if (key.startsWith(`${packageName}@${version}`) &&

}
function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVersion, workspaceRoot) {
function mapRootSnapshot(packageJson, rootImporters, packages, packageIndex, graph, lockfileVersion, workspaceRoot) {
const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);
const snapshot = { specifiers: {} };
const importers = {};
const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
[

@@ -586,3 +620,2 @@ 'dependencies',

let version = packageJson[depType][packageName];
const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
if (manager?.isCatalogReference(version)) {

@@ -624,3 +657,3 @@ version = manager.resolveCatalogReference(workspaceRoot, packageName, version);

snapshot[section] = snapshot[section] || {};
snapshot[section][packageName] = findOriginalKeys(packages, node, lockfileVersion)[0][0];
snapshot[section][packageName] = findOriginalKeys(packages, packageIndex, node, lockfileVersion)[0][0];
}

@@ -627,0 +660,0 @@ });

@@ -41,5 +41,5 @@ "use strict";

};
const manager = (0, catalog_1.getCatalogManager)(workspaceRootPath);
Object.entries(combinedDependencies).forEach(([packageName, versionRange]) => {
let resolvedVersionRange = versionRange;
const manager = (0, catalog_1.getCatalogManager)(workspaceRootPath);
if (manager?.isCatalogReference(versionRange)) {

@@ -46,0 +46,0 @@ resolvedVersionRange = manager.resolveCatalogReference(workspaceRootPath, packageName, versionRange);

@@ -57,2 +57,5 @@ "use strict";

function extractMainLockfileDocument(content) {
// Lockfiles written on Windows may use CRLF line endings, which would never
// match the LF-only document markers.
content = content.replace(/\r\n/g, '\n');
if (!content.startsWith(YAML_DOCUMENT_START)) {

@@ -59,0 +62,0 @@ return content;

@@ -317,5 +317,6 @@ "use strict";

const groupedDependencies = groupDependencies(rootDependencies, isBerry);
const keyIndex = buildYarnKeyIndex(groupedDependencies);
// collect snapshots and their matching keys
Object.values(nodes).forEach((node) => {
const foundOriginalKeys = findOriginalKeys(groupedDependencies, node, workspaceModules);
const foundOriginalKeys = findOriginalKeys(groupedDependencies, keyIndex, node, workspaceModules);
if (!foundOriginalKeys) {

@@ -353,3 +354,3 @@ throw new Error(`Original key(s) not found for "${node.data.packageName}@${node.data.version}" while pruning yarn.lock.`);

// look for patched versions
const patch = findPatchedKeys(groupedDependencies, node, resolutions[node.data.packageName]);
const patch = findPatchedKeys(groupedDependencies, keyIndex, node, resolutions[node.data.packageName]);
if (patch) {

@@ -429,4 +430,30 @@ const [matchedKeys, snapshot] = patch;

}
function findOriginalKeys(dependencies, node, workspaceModules) {
// Bucket grouped-dependency key expressions by the package names they contain
// so a node scans only its own name's entries (was O(nodes * allEntries)). A
// key like "foo@npm:1.0" indexes under "foo"; the inner match checks below are
// unchanged, so candidates are exactly the entries the old full scan would not
// have skipped and the result is identical.
function extractYarnKeyName(key) {
const at = key.indexOf('@', 1);
return at === -1 ? key : key.slice(0, at);
}
function buildYarnKeyIndex(dependencies) {
const index = new Map();
for (const keyExpr of Object.keys(dependencies)) {
const seen = new Set();
for (const k of keyExpr.split(', ')) {
const name = extractYarnKeyName(k);
if (seen.has(name))
continue;
seen.add(name);
let bucket = index.get(name);
if (!bucket)
index.set(name, (bucket = []));
bucket.push(keyExpr);
}
}
return index;
}
function findOriginalKeys(dependencies, keyIndex, node, workspaceModules) {
for (const keyExpr of keyIndex.get(node.data.packageName) ?? []) {
const snapshot = dependencies[keyExpr];

@@ -456,4 +483,4 @@ const keys = keyExpr.split(', ');

}
function findPatchedKeys(dependencies, node, resolutionVersion) {
for (const keyExpr of Object.keys(dependencies)) {
function findPatchedKeys(dependencies, keyIndex, node, resolutionVersion) {
for (const keyExpr of keyIndex.get(node.data.packageName) ?? []) {
const snapshot = dependencies[keyExpr];

@@ -460,0 +487,0 @@ const keys = keyExpr.split(', ');

@@ -164,9 +164,17 @@ "use strict";

const version = (0, semver_1.clean)(externalPackageJson.version);
let matchingExternalNode = this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
if (!matchingExternalNode) {
// check if it's a package alias, where the resolved package key is used as the version
const isAliasImport = packageName !== externalPackageJson.name;
let matchingExternalNode = null;
if (isAliasImport) {
// Prefer the alias node when both the alias import and the resolved package
// exist in the graph, otherwise generated package.json files lose the alias key.
const aliasNpmProjectKey = `npm:${packageName}@npm:${externalPackageJson.name}@${version}`;
matchingExternalNode = this.npmProjects[aliasNpmProjectKey];
matchingExternalNode =
this.npmProjects[aliasNpmProjectKey] ??
this.npmProjects[`npm:${packageName}`];
}
if (!matchingExternalNode) {
matchingExternalNode =
this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
}
if (!matchingExternalNode) {
// Fallback to package name as key. This can happen if the version in project graph is not the same as in the resolved package.json.

@@ -173,0 +181,0 @@ // e.g. Version in project graph is a git remote, but the resolved version is semver.

@@ -100,2 +100,20 @@ import type { TsConfigOptions } from 'ts-node';

/**
* Make Node's module resolver honor the given export conditions for the rest of
* the process, via `module.registerHooks()`. Used when a local plugin is loaded
* from source in-process (no child process to pass `--conditions` to at spawn):
* the hook appends the conditions to every resolution so the plugin's transitive
* workspace imports resolve to source the same way the plugin entry did.
*
* No-op when:
* - `conditions` is empty (nothing to inject);
* - every target condition is already active at startup (a spawned plugin
* worker or daemon was launched with the full `--conditions` set, so Node's
* resolver already honors them and the per-resolve hook would be redundant);
* - `module.registerHooks` is unavailable (Node < 22.15 / < 23.5). Those
* runtimes keep the `NODE_OPTIONS=--conditions` escape hatch.
*
* Idempotent and best-effort.
*/
export declare function ensureResolveConditionsInjected(conditions: string[]): void;
/**
* Whether Nx will defer to Node's native TypeScript stripping for the next

@@ -102,0 +120,0 @@ * `.ts` load. Mirrors the gate used by `loadTsFile`/`registerTsProject` so

@@ -8,2 +8,3 @@ "use strict";

exports.ensureCjsResolverPatched = ensureCjsResolverPatched;
exports.ensureResolveConditionsInjected = ensureResolveConditionsInjected;
exports.isNativeStripPreferred = isNativeStripPreferred;

@@ -392,3 +393,83 @@ exports.registerTsProject = registerTsProject;

})();
let resolveConditionsInjected = false;
/**
* Make Node's module resolver honor the given export conditions for the rest of
* the process, via `module.registerHooks()`. Used when a local plugin is loaded
* from source in-process (no child process to pass `--conditions` to at spawn):
* the hook appends the conditions to every resolution so the plugin's transitive
* workspace imports resolve to source the same way the plugin entry did.
*
* No-op when:
* - `conditions` is empty (nothing to inject);
* - every target condition is already active at startup (a spawned plugin
* worker or daemon was launched with the full `--conditions` set, so Node's
* resolver already honors them and the per-resolve hook would be redundant);
* - `module.registerHooks` is unavailable (Node < 22.15 / < 23.5). Those
* runtimes keep the `NODE_OPTIONS=--conditions` escape hatch.
*
* Idempotent and best-effort.
*/
function ensureResolveConditionsInjected(conditions) {
if (resolveConditionsInjected)
return;
resolveConditionsInjected = true;
if (!conditions.length)
return;
// Skip only when Node already honors every target condition (e.g. a worker or
// daemon spawned with the full `--conditions` set); a partial overlap still
// needs the hook to add the missing ones.
const activeConditions = getConditionsActiveAtStartup();
if (conditions.every((condition) => activeConditions.has(condition)))
return;
const module = require('node:module');
const registerHooks = module.registerHooks;
if (typeof registerHooks !== 'function')
return;
try {
registerHooks.call(module, {
resolve(specifier, context, nextResolve) {
const merged = context.conditions
? [...context.conditions, ...conditions]
: conditions;
return nextResolve(specifier, { ...context, conditions: merged });
},
});
}
catch {
// Best-effort: leave Node's native resolution in place rather than failing.
}
}
/**
* Export conditions this process was started with, parsed from `--conditions`
* (or its `-C` alias) in `process.execArgv` and `NODE_OPTIONS`. Node's resolver
* already honors these, so the injected hook only needs to cover target
* conditions not present here.
*/
function getConditionsActiveAtStartup() {
const active = new Set();
const collect = (tokens) => {
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === '--conditions' || token === '-C') {
const value = tokens[i + 1];
if (value) {
active.add(value);
i++;
}
}
else if (token.startsWith('--conditions=')) {
active.add(token.slice('--conditions='.length));
}
else if (token.startsWith('-C=')) {
active.add(token.slice('-C='.length));
}
}
};
collect(process.execArgv ?? []);
const nodeOptions = process.env.NODE_OPTIONS;
if (nodeOptions)
collect(nodeOptions.split(/\s+/).filter(Boolean));
return active;
}
/**
* Whether the current Node.js runtime exposes native TypeScript type

@@ -395,0 +476,0 @@ * stripping. This is the authoritative gate - it correctly handles every

@@ -23,2 +23,11 @@ import type * as ts from 'typescript';

export declare function getRootTsConfigResolveExportsConditions(root?: string): string[];
/**
* Node `--conditions <name>` CLI args for spawning a plugin worker or the daemon
* with the plugin-resolution conditions active at startup. Mirrors the set Nx
* uses to resolve the plugin entry (`getRootTsConfigResolveExportsConditions`)
* so the entry and the plugin's transitive workspace imports resolve the same
* way; Node's own resolver otherwise ignores TypeScript `customConditions` and a
* source-loaded plugin's imports land on their unbuilt `dist`.
*/
export declare function getPluginResolveConditionNodeArgs(root?: string): string[];
export declare function findNodes(node: Node, kind: SyntaxKind | SyntaxKind[], max?: number): Node[];

@@ -11,2 +11,3 @@ "use strict";

exports.getRootTsConfigResolveExportsConditions = getRootTsConfigResolveExportsConditions;
exports.getPluginResolveConditionNodeArgs = getPluginResolveConditionNodeArgs;
exports.findNodes = findNodes;

@@ -117,2 +118,16 @@ const workspace_root_1 = require("../../../utils/workspace-root");

}
/**
* Node `--conditions <name>` CLI args for spawning a plugin worker or the daemon
* with the plugin-resolution conditions active at startup. Mirrors the set Nx
* uses to resolve the plugin entry (`getRootTsConfigResolveExportsConditions`)
* so the entry and the plugin's transitive workspace imports resolve the same
* way; Node's own resolver otherwise ignores TypeScript `customConditions` and a
* source-loaded plugin's imports land on their unbuilt `dist`.
*/
function getPluginResolveConditionNodeArgs(root = workspace_root_1.workspaceRoot) {
return getRootTsConfigResolveExportsConditions(root).flatMap((c) => [
'--conditions',
c,
]);
}
function findNodes(node, kind, max = Infinity) {

@@ -119,0 +134,0 @@ if (!node || max == 0) {

@@ -10,2 +10,3 @@ "use strict";

const perf_hooks_1 = require("perf_hooks");
const analytics_1 = require("../analytics");
const configuration_1 = require("../config/configuration");

@@ -226,3 +227,8 @@ const file_hasher_1 = require("../hasher/file-hasher");

end: `${plugin.name}:createDependencies - end`,
detail: { track: true },
detail: {
track: true,
...(analytics_1.customDimensions && {
[analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
}),
},
});

@@ -229,0 +235,0 @@ }));

@@ -245,5 +245,44 @@ "use strict";

}
// Plugins pass through whatever value they caught, which is not
// guaranteed to be an Error. Coerce so formatting can rely on
// message and stack being present.
for (const errorTuple of errors) {
errorTuple[1] = coerceToError(errorTuple[1]);
}
}
}
exports.AggregateCreateNodesError = AggregateCreateNodesError;
function coerceToError(value) {
if (value instanceof Error) {
return value;
}
let message;
let stack;
if (typeof value === 'object' && value !== null) {
const candidate = value;
if (typeof candidate.message === 'string') {
message = candidate.message;
if (typeof candidate.stack === 'string') {
stack = candidate.stack;
}
}
else {
try {
message = JSON.stringify(value);
}
catch {
// Circular structures cannot be stringified.
message = String(value);
}
}
}
else {
message = String(value);
}
const error = new Error(message);
// A synthesized stack would point at this coercion site rather than
// the original failure, so prefer the original stack or just the message.
error.stack = stack ?? message;
return error;
}
function formatAggregateCreateNodesError(error, pluginName) {

@@ -274,3 +313,4 @@ const errorCount = error.errors.length > 1 ? `${error.errors.length} errors` : 'An error';

const messageLines = e.message.split('\n');
const stackLines = e.stack.split('\n');
// Errors deserialized from a plugin worker may arrive without a stack.
const stackLines = (e.stack ?? e.message).split('\n');
if (file) {

@@ -277,0 +317,0 @@ errorBodyLines.push(...messageLines.map((line) => ` ${line}`));

@@ -15,2 +15,3 @@ "use strict";

const fileutils_1 = require("../utils/fileutils");
const git_revision_1 = require("../utils/git-revision");
const ignore_1 = require("../utils/ignore");

@@ -120,2 +121,5 @@ const json_diff_1 = require("../utils/json-diff");

function defaultReadFileAtRevision(file, revision) {
if (revision) {
(0, git_revision_1.assertValidGitRevision)(revision);
}
try {

@@ -125,3 +129,3 @@ const filePathInGitRepository = getFilePathInGitRepository(file);

? (0, fs_1.readFileSync)(file, 'utf-8')
: (0, child_process_1.execSync)(`git show ${revision}:${filePathInGitRepository}`, {
: (0, child_process_1.execFileSync)('git', ['show', `${revision}:${filePathInGitRepository}`], {
maxBuffer: exports.TEN_MEGABYTES,

@@ -146,2 +150,3 @@ stdio: ['pipe', 'pipe', 'ignore'],

}
(0, git_revision_1.assertValidGitRevision)(revision);
const filePathInGitRepository = getFilePathInGitRepository(file);

@@ -148,0 +153,0 @@ const tempDirectory = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'nx-bun-lock-'));

@@ -10,2 +10,3 @@ "use strict";

const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket");
const typescript_1 = require("../../../plugins/js/utils/typescript");
const installation_directory_1 = require("../../../utils/installation-directory");

@@ -357,2 +358,5 @@ const logger_2 = require("../../../utils/logger");

const worker = (0, child_process_1.spawn)(process.execPath, [
// Spawn the worker with the same resolve conditions Nx uses for plugin
// entries so the plugin's transitive workspace imports resolve to source.
...(0, typescript_1.getPluginResolveConditionNodeArgs)(),
...(isWorkerTypescript ? ['--require', 'ts-node/register'] : []),

@@ -359,0 +363,0 @@ workerPath,

@@ -66,2 +66,3 @@ "use strict";

[analytics_1.customDimensions.projectCount]: projectCount,
[analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
}),

@@ -68,0 +69,0 @@ },

@@ -28,2 +28,6 @@ "use strict";

}
// Align Node's runtime resolution with the source-first plugin entry so the
// plugin's transitive workspace imports don't fall through to unbuilt `dist`.
// A no-op inside a plugin worker/daemon already spawned with `--conditions`.
(0, register_1.ensureResolveConditionsInjected)((0, typescript_1.getRootTsConfigResolveExportsConditions)(workspace_root_1.workspaceRoot));
if ((0, register_1.isNativeStripPreferred)()) {

@@ -30,0 +34,0 @@ // Native strip handles `.ts` syntax but doesn't rewrite NodeNext-style

@@ -247,2 +247,3 @@ "use strict";

[analytics_1.customDimensions.projectCount]: Object.keys(currentProjectGraph.nodes).length,
[analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
}),

@@ -314,2 +315,3 @@ },

.length,
[analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
}),

@@ -338,2 +340,3 @@ },

[analytics_1.customDimensions.projectCount]: Object.keys(projectGraphAndSourceMaps.projectGraph.nodes).length,
[analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
}),

@@ -340,0 +343,0 @@ },

@@ -63,15 +63,18 @@ import { NxJsonConfiguration } from '../../config/nx-json';

*
* Specified plugin results are merged once into the manager. Default
* plugin results are first staged into an intermediate rootMap (with
* `'...'` spreads deferred) so that synthesis can read each layer's
* contribution without re-running the merge. The synthetic result from
* `createTargetDefaultsResults` is then merged into the manager, and
* the staged intermediate is replayed on top — that replay is where
* deferred spreads expand against the final (specified + synth) base.
* Every layer merges into the manager through the same source-map-aware
* merge, in precedence order:
*
* Synthesis itself doesn't materialize a second rootMap. Per
* (root, target) it does an on-the-fly merge of the two layered
* contributions to learn the eventual executor/command, then matches
* defaults against that merged shape. This keeps specified-plugin
* merge work to a single pass.
* specified plugins → synthetic target defaults → default plugins
*
* so field-level provenance is decided by the merge itself for all three
* layers — whichever layer a field's final value came from owns its
* attribution, and `'...'` spreads in default-plugin configs resolve against
* the accumulated specified + target-defaults base.
*
* Target-default synthesis needs the *merged* shape of the default layer
* (to predict each target's eventual executor/command) before that layer
* merges into the manager. To get it, default results are first staged into
* a throwaway intermediate rootMap with unresolvable `'...'` spreads
* deferred. The staging output feeds only `createTargetDefaultsResults`; the
* default plugins then merge into the manager from their original results.
*/

@@ -78,0 +81,0 @@ export declare function mergeCreateNodesResults(specifiedResults: CreateNodesResultEntry[][], defaultResults: CreateNodesResultEntry[][], nxJsonConfiguration: NxJsonConfiguration, workspaceRoot: string, errors: MergeError[]): {

@@ -16,6 +16,6 @@ "use strict";

const error_types_1 = require("../error-types");
const source_maps_1 = require("./project-configuration/source-maps");
const target_defaults_1 = require("./project-configuration/target-defaults");
var target_merging_1 = require("./project-configuration/target-merging");
Object.defineProperty(exports, "mergeTargetConfigurations", { enumerable: true, get: function () { return target_merging_1.mergeTargetConfigurations; } });
const target_merging_2 = require("./project-configuration/target-merging");
var target_defaults_2 = require("./project-configuration/target-defaults");

@@ -127,10 +127,9 @@ Object.defineProperty(exports, "readTargetDefaultsForTarget", { enumerable: true, get: function () { return target_defaults_2.readTargetDefaultsForTarget; } });

* 1. Every project node in every plugin result is handed to `mergeFn`,
* which decides where it lands (the manager's rootMap, an
* intermediate rootMap, etc.). Any failure is collected into
* `errors`; processing keeps going. External nodes are accumulated
* onto the shared `externalNodes` record.
* which merges it into the manager's rootMap. Any failure is
* collected into `errors`; processing keeps going. External nodes
* are accumulated onto the shared `externalNodes` record.
* 2. After every project in the batch has been merged, name-reference
* sentinels for the batch are registered against `nameRefRootMap` —
* the rootMap the batch was merged into — so sentinels point at the
* target objects that actually received the merges.
* sentinels for the batch are registered against the manager's
* rootMap, so sentinels point at the target objects that actually
* received the merges.
*

@@ -143,3 +142,7 @@ * The two passes can't be collapsed: a sentinel registered too early

*/
function mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeFn, nodesManager, nameRefRootMap, externalNodes, errors) {
function mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeFn, nodesManager, externalNodes, errors) {
mergeSinglePluginResults(pluginResults, mergeFn, externalNodes, errors);
registerNameRefsFromSinglePlugin(pluginResults, nodesManager, errors);
}
function mergeSinglePluginResults(pluginResults, mergeFn, externalNodes, errors) {
for (const result of pluginResults) {

@@ -162,2 +165,4 @@ const [pluginName, file, nodes, pluginIndex] = result;

}
}
function registerNameRefsFromSinglePlugin(pluginResults, nodesManager, errors) {
for (const result of pluginResults) {

@@ -167,3 +172,3 @@ const [pluginName, file, nodes, pluginIndex] = result;

try {
nodesManager.registerNameRefs(projectNodes, nameRefRootMap);
nodesManager.registerNameRefs(projectNodes);
}

@@ -178,15 +183,18 @@ catch (error) {

*
* Specified plugin results are merged once into the manager. Default
* plugin results are first staged into an intermediate rootMap (with
* `'...'` spreads deferred) so that synthesis can read each layer's
* contribution without re-running the merge. The synthetic result from
* `createTargetDefaultsResults` is then merged into the manager, and
* the staged intermediate is replayed on top — that replay is where
* deferred spreads expand against the final (specified + synth) base.
* Every layer merges into the manager through the same source-map-aware
* merge, in precedence order:
*
* Synthesis itself doesn't materialize a second rootMap. Per
* (root, target) it does an on-the-fly merge of the two layered
* contributions to learn the eventual executor/command, then matches
* defaults against that merged shape. This keeps specified-plugin
* merge work to a single pass.
* specified plugins → synthetic target defaults → default plugins
*
* so field-level provenance is decided by the merge itself for all three
* layers — whichever layer a field's final value came from owns its
* attribution, and `'...'` spreads in default-plugin configs resolve against
* the accumulated specified + target-defaults base.
*
* Target-default synthesis needs the *merged* shape of the default layer
* (to predict each target's eventual executor/command) before that layer
* merges into the manager. To get it, default results are first staged into
* a throwaway intermediate rootMap with unresolvable `'...'` spreads
* deferred. The staging output feeds only `createTargetDefaultsResults`; the
* default plugins then merge into the manager from their original results.
*/

@@ -198,87 +206,57 @@ function mergeCreateNodesResults(specifiedResults, defaultResults, nxJsonConfiguration, workspaceRoot, errors) {

const configurationSourceMaps = {};
const intermediateDefaultRootMap = {};
// Kept separate so the intermediate merge doesn't clobber
// specified/TD attribution on fields the defaults don't touch.
const defaultConfigurationSourceMaps = {};
const mergeToManager = (project, sourceInfo) => nodesManager.mergeProjectNode(project, configurationSourceMaps, sourceInfo);
const mergeToIntermediate = (project, sourceInfo) => {
(0, project_nodes_manager_1.mergeProjectConfigurationIntoRootMap)(intermediateDefaultRootMap, project, defaultConfigurationSourceMaps, sourceInfo);
};
for (const pluginResults of specifiedResults) {
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToManager, nodesManager, nodesManager.getRootMap(), externalNodes, errors);
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToManager, nodesManager, externalNodes, errors);
}
for (const pluginResults of defaultResults) {
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToIntermediate, nodesManager, intermediateDefaultRootMap, externalNodes, errors);
}
const targetDefaultsResults = (0, target_defaults_1.createTargetDefaultsResults)(nodesManager.getRootMap(), intermediateDefaultRootMap, nxJsonConfiguration, configurationSourceMaps, defaultConfigurationSourceMaps);
if (targetDefaultsResults.length > 0) {
mergeCreateNodesResultsFromSinglePlugin(targetDefaultsResults, mergeToManager, nodesManager, nodesManager.getRootMap(), externalNodes, errors);
}
// Apply the intermediate default rootMap as a single layer. Preserved
// spread sentinels resolve here against the real specified + TD base.
// Source maps are intentionally not written — TD attribution for
// fields that yield to the base (e.g. keys before `...`) stays intact.
for (const root in intermediateDefaultRootMap) {
const project = intermediateDefaultRootMap[root];
try {
nodesManager.mergeProjectNode(project, undefined, undefined);
// Without target defaults there is nothing to synthesize, and the staging
// pass exists only to feed synthesis — skip straight to the default-plugin
// merge.
if (Object.keys(nxJsonConfiguration.targetDefaults ?? {}).length > 0) {
// Throwaway staging area: the default layer's merged shape (unresolvable
// `'...'` spreads deferred), read only by target-default synthesis. The
// default plugins merge into the manager from their original results, not
// from this map. No source maps are kept for it — synthesis attributes
// targets without them (a default plugin can never be named by a
// `filter.plugin`); the real default merge writes the manager's.
const intermediateDefaultRootMap = {};
// The rootMap merge adopts input arrays/objects by reference and grows
// them in place (e.g. `mergeMetadata`), so staging works on deep clones —
// handing it the plugin results themselves would corrupt them before the
// real merge below reads them.
const mergeToIntermediate = (project, sourceInfo) => {
(0, project_nodes_manager_1.mergeProjectConfigurationIntoRootMap)(intermediateDefaultRootMap, (0, target_merging_2.deepClone)(project), undefined, sourceInfo, false, true);
};
// Stage the default layer for synthesis. Merge errors are discarded and
// external nodes land in a scratch object — the same results merge into
// the manager below, where both surface once with proper plugin context.
// The discard is safe because every merge throw is base-independent in
// both its condition and its reachability: each throw's condition reads
// only the plugin's own config, and the spread-ambiguity throws fire even
// when a key is dropped by a base-owns-key shortcut, because those
// shortcuts eagerly validate the dropped value (`assertNoIntegerLikeSpreadKey`).
// So a given config raises the same error in this pass and the real merge
// below despite their bases differing.
// Name references are NOT registered here: `applySubstitutions` sweeps only
// the manager's `rootMap`, so sentinels registered against this throwaway
// rootMap would never be visited and would never resolve.
const stagingErrors = [];
const stagingExternalNodes = {};
for (const pluginResults of defaultResults) {
mergeSinglePluginResults(pluginResults, mergeToIntermediate, stagingExternalNodes, stagingErrors);
}
catch (error) {
errors.push(new error_types_1.MergeNodesError({
file: 'nx.json',
pluginName: 'nx/default-plugins',
error,
pluginIndex: undefined,
}));
const targetDefaultsResults = (0, target_defaults_1.createTargetDefaultsResults)(nodesManager.getRootMap(), intermediateDefaultRootMap, nxJsonConfiguration, configurationSourceMaps);
if (targetDefaultsResults.length > 0) {
mergeCreateNodesResultsFromSinglePlugin(targetDefaultsResults, mergeToManager, nodesManager, externalNodes, errors);
}
}
// The intermediate apply may have rebuilt dependsOn / inputs arrays
// via spread merges, leaving sentinels inserted against the
// intermediate rootMap pointing at now-orphaned arrays. Re-walking
// the final merged targets rebinds each encountered sentinel's
// `parent` to the current array (see
// ProjectNameInNodePropsManager#processInputs / processDependsOn).
nodesManager.registerNameRefs(intermediateDefaultRootMap);
// Overlay default-plugin attribution onto the main source maps using
// "only fill missing" semantics. Any key already present in
// configurationSourceMaps was written by a specified plugin or by
// target defaults, and that attribution is strictly more correct:
// - For fields the default plugin never shadowed, the existing entry
// already matches what the default plugin would overlay.
// - For fields where a default plugin placed `...` after other keys,
// those keys yielded to the base during the single-layer apply
// above. The stale default-plugin entry in
// `defaultConfigurationSourceMaps` must NOT clobber the base
// attribution that the specified plugin / TD already recorded.
const mainRootMap = nodesManager.getRootMap();
for (const root in defaultConfigurationSourceMaps) {
const existing = (configurationSourceMaps[root] ??= {});
const incoming = defaultConfigurationSourceMaps[root];
// A default plugin's targets are synthesized into the main rootmap by
// target defaults *before* the default layer is applied, so target
// defaults wrote the main source map's entry for the target node and its
// identity fields first — even though it never authored them (it only
// stamps them as a merge guard and cannot bring a target into existence).
// The identity fields are the executor/command plus, for run-commands, the
// `options.command`/`options.commands` the synthetic copies from the winner
// to stay compatible (#36067). For those keys, the real default plugin's
// attribution must override the target-defaults stamp.
const identityKeys = new Set();
for (const targetName in mainRootMap[root]?.targets ?? {}) {
const base = (0, source_maps_1.targetSourceMapKey)(targetName);
identityKeys.add(base);
identityKeys.add(`${base}.executor`);
identityKeys.add(`${base}.command`);
identityKeys.add(`${base}.options.command`);
identityKeys.add(`${base}.options.commands`);
}
for (const key in incoming) {
if (existing[key] === undefined) {
existing[key] = incoming[key];
}
else if (identityKeys.has(key) &&
existing[key][1] === source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME) {
existing[key] = incoming[key];
}
}
// Merge the default plugins into the manager on top of the specified + TD
// base, from their original results. This is the same source-map-aware path
// the other layers take, so every field a default plugin wins — including
// fields it overrides on a specified/TD target — is attributed to it by the
// merge itself, `'...'` spreads resolve against the real base (keys a spread
// lets the base win keep their base attribution), and identity provenance
// follows the node-ownership rules in `recordTargetIdentitySourceMapInfo` /
// `getMergeValueResult`.
for (const pluginResults of defaultResults) {
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToManager, nodesManager, externalNodes, errors);
}

@@ -285,0 +263,0 @@ const projectRootMap = nodesManager.getRootMap();

@@ -7,4 +7,3 @@ import { ProjectConfiguration } from '../../../config/workspace-json-project-json';

* forward refs, promoted to `RootRef` in place when the name is
* identified). `parent` + `key` let the final pass write the resolved
* name back; `targetPart` preserves the `:target` suffix from
* identified). `targetPart` preserves the `:target` suffix from
* `dependsOn` strings.

@@ -14,6 +13,4 @@ */

value: string;
parent: unknown;
key: string | undefined;
targetPart: string | undefined;
constructor(value: string, parent: unknown, key: string | undefined, targetPart: string | undefined);
constructor(value: string, targetPart: string | undefined);
}

@@ -32,12 +29,13 @@ export declare class RootRef extends NameRef {

* Tracking by array position breaks once `'...'` spreads shuffle indices,
* so each ref becomes a sentinel object. Arrays spread-merge by pushing
* element references, so sentinel identity survives any downstream
* merges — the final pass walks a flat registry and writes the resolved
* name back through each sentinel's `parent` back-reference. Orphaned
* sentinels (from arrays dropped by a full-replace) write harmlessly.
* so each ref becomes a sentinel object. Merges copy sentinels by
* reference — one sentinel can end up in many arrays (e.g. a pattern
* target's dependsOn applied to every matching target) — so the final
* pass sweeps the merged rootMap and resolves every sentinel where it
* actually sits. Sentinels in arrays dropped by a full-replace are never
* visited and vanish with their array.
*/
export declare class ProjectNameInNodePropsManager {
private getNameMap;
private allRefs;
private pendingByName;
private nameHistory;
constructor(getNameMap?: () => Record<string, ProjectConfiguration>);

@@ -51,4 +49,4 @@ registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>): void;

applySubstitutions(rootMap: Record<string, ProjectConfiguration>): void;
private substituteInArray;
private resolveFinalName;
private writeReplacement;
}

@@ -14,11 +14,8 @@ "use strict";

* forward refs, promoted to `RootRef` in place when the name is
* identified). `parent` + `key` let the final pass write the resolved
* name back; `targetPart` preserves the `:target` suffix from
* identified). `targetPart` preserves the `:target` suffix from
* `dependsOn` strings.
*/
class NameRef {
constructor(value, parent, key, targetPart) {
constructor(value, targetPart) {
this.value = value;
this.parent = parent;
this.key = key;
this.targetPart = targetPart;

@@ -48,12 +45,15 @@ }

* Tracking by array position breaks once `'...'` spreads shuffle indices,
* so each ref becomes a sentinel object. Arrays spread-merge by pushing
* element references, so sentinel identity survives any downstream
* merges — the final pass walks a flat registry and writes the resolved
* name back through each sentinel's `parent` back-reference. Orphaned
* sentinels (from arrays dropped by a full-replace) write harmlessly.
* so each ref becomes a sentinel object. Merges copy sentinels by
* reference — one sentinel can end up in many arrays (e.g. a pattern
* target's dependsOn applied to every matching target) — so the final
* pass sweeps the merged rootMap and resolves every sentinel where it
* actually sits. Sentinels in arrays dropped by a full-replace are never
* visited and vanish with their array.
*/
class ProjectNameInNodePropsManager {
constructor(getNameMap) {
this.allRefs = new Set();
this.pendingByName = new Map();
// name → root for every name a project has ever been identified by, so refs
// to a since-renamed name still bind to the right root (see createRef).
this.nameHistory = new Map();
this.getNameMap = getNameMap ?? (() => ({}));

@@ -87,8 +87,4 @@ }

const entry = inputs[i];
// Existing sentinel: spread merges may have copied it out of its
// original array, so rebind parent to this one.
if (isNameRef(entry)) {
entry.parent = inputs;
if (isNameRef(entry))
continue;
}
if (!entry || typeof entry !== 'object')

@@ -100,10 +96,8 @@ continue;

const projects = element.projects;
if (isNameRef(projects)) {
// Object-parent sentinel — element identity is stable across spread.
if (isNameRef(projects))
continue;
}
if (typeof projects === 'string') {
if (projects === 'self' || projects === 'dependencies')
continue;
element.projects = this.createRef(projects, element, 'projects');
element.projects = this.createRef(projects);
}

@@ -118,8 +112,4 @@ else if (Array.isArray(projects)) {

const dep = dependsOn[i];
// Existing sentinel: rebind parent to this array in case a spread
// merge copied it out of its original.
if (isNameRef(dep)) {
dep.parent = dependsOn;
if (isNameRef(dep))
continue;
}
if (typeof dep === 'string') {

@@ -134,3 +124,3 @@ // `^target` and same-project targets aren't cross-project refs.

const targetPart = rest.join(':');
dependsOn[i] = this.createRef(maybeProject, dependsOn, undefined, targetPart);
dependsOn[i] = this.createRef(maybeProject, targetPart);
continue;

@@ -151,3 +141,3 @@ }

}
element.projects = this.createRef(projects, element, 'projects');
element.projects = this.createRef(projects);
}

@@ -162,6 +152,4 @@ else if (Array.isArray(projects)) {

const name = projects[j];
if (isNameRef(name)) {
name.parent = projects;
if (isNameRef(name))
continue;
}
if (typeof name !== 'string')

@@ -171,12 +159,16 @@ continue;

continue;
projects[j] = this.createRef(name, projects, undefined);
projects[j] = this.createRef(name);
}
}
// Builds a sentinel and registers it.
createRef(referencedName, parent, key, targetPart) {
const referencedRoot = this.getNameMap()[referencedName]?.root;
// Builds a sentinel and registers it. When `referencedName` isn't in the
// current name map, fall back to the rename history: a batch may reference a
// project by a name that a project earlier in the same batch already renamed
// (refs register after the whole batch merges), and the old name still
// identifies the same root.
createRef(referencedName, targetPart) {
const referencedRoot = this.getNameMap()[referencedName]?.root ??
this.nameHistory.get(referencedName);
const ref = referencedRoot !== undefined
? new RootRef(referencedRoot, parent, key, targetPart)
: new UsageRef(referencedName, parent, key, targetPart);
this.allRefs.add(ref);
? new RootRef(referencedRoot, targetPart)
: new UsageRef(referencedName, targetPart);
if (ref instanceof UsageRef) {

@@ -196,2 +188,7 @@ let set = this.pendingByName.get(referencedName);

identifyProjectWithRoot(root, name) {
// Every name a root has ever gone by, including pre-rename names. A later
// ref to a stale name resolves to the root it identified at the time.
// (If two roots use the same name over a graph construction, the later
// one wins — matching how the live name map would have resolved it.)
this.nameHistory.set(name, root);
const pending = this.pendingByName.get(name);

@@ -208,4 +205,8 @@ if (!pending)

}
// Writes each sentinel's current resolved name back into its owning slot.
// Called once after all plugin results have been merged.
// Resolves every sentinel in the merged rootMap in place. Sweeping the
// final config (rather than writing through back-references held by the
// sentinels) covers sentinels that merges copied into arrays other than
// the one they were created in, e.g. a pattern target's dependsOn applied
// to every matching target. Called once after all plugin results have
// been merged.
applySubstitutions(rootMap) {

@@ -216,14 +217,58 @@ const nameByRoot = {};

}
for (const ref of this.allRefs) {
const finalName = this.resolveFinalName(ref, nameByRoot);
if (finalName === undefined)
for (const root in rootMap) {
const targets = rootMap[root]?.targets;
if (!targets)
continue;
const replacement = ref.targetPart !== undefined
? `${finalName}:${ref.targetPart}`
: finalName;
this.writeReplacement(ref, replacement);
for (const targetName in targets) {
const targetConfig = targets[targetName];
if (!targetConfig || typeof targetConfig !== 'object')
continue;
if (Array.isArray(targetConfig.inputs)) {
this.substituteInArray(targetConfig.inputs, nameByRoot);
}
if (Array.isArray(targetConfig.dependsOn)) {
this.substituteInArray(targetConfig.dependsOn, nameByRoot);
}
}
}
this.allRefs.clear();
this.pendingByName.clear();
this.nameHistory.clear();
}
substituteInArray(entries, nameByRoot) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (isNameRef(entry)) {
const finalName = this.resolveFinalName(entry, nameByRoot);
if (finalName !== undefined) {
entries[i] =
entry.targetPart !== undefined
? `${finalName}:${entry.targetPart}`
: finalName;
}
continue;
}
if (!entry || typeof entry !== 'object' || !('projects' in entry)) {
continue;
}
const element = entry;
if (isNameRef(element.projects)) {
const finalName = this.resolveFinalName(element.projects, nameByRoot);
if (finalName !== undefined) {
element.projects = finalName;
}
}
else if (Array.isArray(element.projects)) {
const projects = element.projects;
for (let j = 0; j < projects.length; j++) {
const name = projects[j];
if (!isNameRef(name))
continue;
const finalName = this.resolveFinalName(name, nameByRoot);
if (finalName !== undefined) {
projects[j] = finalName;
}
}
}
}
}
resolveFinalName(ref, nameByRoot) {

@@ -236,18 +281,3 @@ if (ref instanceof RootRef) {

}
writeReplacement(ref, replacement) {
const parent = ref.parent;
if (Array.isArray(parent)) {
// One sentinel may appear at multiple indices (e.g. `[..., ...]`
// pushed the same reference twice via spread), so replace all.
for (let i = 0; i < parent.length; i++) {
if (parent[i] === ref)
parent[i] = replacement;
}
return;
}
if (parent && typeof parent === 'object' && ref.key !== undefined) {
parent[ref.key] = replacement;
}
}
}
exports.ProjectNameInNodePropsManager = ProjectNameInNodePropsManager;

@@ -31,11 +31,8 @@ import { ProjectConfiguration } from '../../../config/workspace-json-project-json';

* Inserts project-name sentinels into `inputs` and `dependsOn` on the
* merged objects from `mergedRootMap` (defaulting to this manager's
* rootMap). Walking the merged entries matters because a spread-produced
* array is a fresh instance.
*
* Pass a different `mergedRootMap` for the default-plugin intermediate
* pass, then call again with `this.rootMap` after it's applied so
* sentinel parents rebind onto the final arrays.
* merged objects in this manager's rootMap. Walking the merged entries —
* not the plugin results — matters because a spread-produced array is a
* fresh instance, and because sentinels written into plugin-result arrays
* would corrupt them for any later merge that re-reads the results.
*/
registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>, mergedRootMap?: Record<string, ProjectConfiguration>): void;
registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>): void;
/**

@@ -42,0 +39,0 @@ * Applies all pending name substitutions. Call once after all plugin

@@ -132,6 +132,6 @@ "use strict";

if (sourceMap) {
// A target default stamping fields onto an existing target must not
// steal provenance for the target's existence; real plugins still win
// last. Field-level attribution is handled inside
// `mergeTargetConfigurations`.
// Claims the node key for a newly-created target (or reclaims it from
// a weak target-defaults stamp). Whether an *existing* target changes
// owners is decided inside `mergeTargetConfigurations`, which knows
// whether this merge changed the target's identity.
(0, source_maps_1.recordTargetIdentitySourceMapInfo)(sourceMap, (0, source_maps_1.targetSourceMapKey)(targetName), sourceInformation);

@@ -261,11 +261,8 @@ }

* Inserts project-name sentinels into `inputs` and `dependsOn` on the
* merged objects from `mergedRootMap` (defaulting to this manager's
* rootMap). Walking the merged entries matters because a spread-produced
* array is a fresh instance.
*
* Pass a different `mergedRootMap` for the default-plugin intermediate
* pass, then call again with `this.rootMap` after it's applied so
* sentinel parents rebind onto the final arrays.
* merged objects in this manager's rootMap. Walking the merged entries —
* not the plugin results — matters because a spread-produced array is a
* fresh instance, and because sentinels written into plugin-result arrays
* would corrupt them for any later merge that re-reads the results.
*/
registerNameRefs(pluginResultProjects, mergedRootMap = this.rootMap) {
registerNameRefs(pluginResultProjects) {
if (!pluginResultProjects)

@@ -275,4 +272,4 @@ return;

for (const root in pluginResultProjects) {
if (mergedRootMap[root]) {
scoped[root] = mergedRootMap[root];
if (this.rootMap[root]) {
scoped[root] = this.rootMap[root];
}

@@ -279,0 +276,0 @@ }

@@ -12,8 +12,15 @@ /** [file, plugin] that contributed a configuration property. */

/**
* Write the source for a target's "identity" key (the target node itself, or
* its executor/command). Real plugins win last; a target-defaults stamp only
* claims the key when no real plugin already recorded it — target defaults
* never bring a target into existence, they only layer fields onto one.
* Write the source for the target node key (`targets.<name>`). Ownership of a
* target follows its identity, not the last writer:
*
* - An unowned key goes to whoever writes it first (the creator).
* - A target-defaults stamp is weak — it never authors a target's existence,
* so any real plugin reclaims the key from it, and it can never take the
* key from a real plugin.
* - Between real plugins, the key only changes hands when the merge changed
* the target's identity (executor/command) — a plugin that merely layers
* fields (dependsOn, options, …) onto an existing target does not become
* its owner.
*/
export declare function recordTargetIdentitySourceMapInfo(sourceMap: Record<string, SourceInformation>, key: string, sourceInfo: SourceInformation): void;
export declare function recordTargetIdentitySourceMapInfo(sourceMap: Record<string, SourceInformation>, key: string, sourceInfo: SourceInformation, identityChanged?: boolean): void;
/** Source map per project root. */

@@ -20,0 +27,0 @@ export type ConfigurationSourceMaps = Record<string, Record<string, SourceInformation>>;

@@ -24,12 +24,26 @@ "use strict";

/**
* Write the source for a target's "identity" key (the target node itself, or
* its executor/command). Real plugins win last; a target-defaults stamp only
* claims the key when no real plugin already recorded it — target defaults
* never bring a target into existence, they only layer fields onto one.
* Write the source for the target node key (`targets.<name>`). Ownership of a
* target follows its identity, not the last writer:
*
* - An unowned key goes to whoever writes it first (the creator).
* - A target-defaults stamp is weak — it never authors a target's existence,
* so any real plugin reclaims the key from it, and it can never take the
* key from a real plugin.
* - Between real plugins, the key only changes hands when the merge changed
* the target's identity (executor/command) — a plugin that merely layers
* fields (dependsOn, options, …) onto an existing target does not become
* its owner.
*/
function recordTargetIdentitySourceMapInfo(sourceMap, key, sourceInfo) {
if (sourceInfo[1] !== exports.TARGET_DEFAULTS_PLUGIN_NAME ||
sourceMap[key] === undefined) {
function recordTargetIdentitySourceMapInfo(sourceMap, key, sourceInfo, identityChanged = false) {
const existing = sourceMap[key];
if (existing === undefined) {
sourceMap[key] = sourceInfo;
return;
}
if (sourceInfo[1] === exports.TARGET_DEFAULTS_PLUGIN_NAME) {
return;
}
if (existing[1] === exports.TARGET_DEFAULTS_PLUGIN_NAME || identityChanged) {
sourceMap[key] = sourceInfo;
}
}

@@ -36,0 +50,0 @@ // Iterates `${prefixKey}.0`, `${prefixKey}.1`, ... for each index of `array`.

@@ -22,3 +22,3 @@ import { NormalizedTargetDefaults, NxJsonConfiguration, TargetDefaults } from '../../../config/nx-json';

*/
export declare function createTargetDefaultsResults(specifiedPluginRootMap: Record<string, ProjectConfiguration>, defaultPluginRootMap: Record<string, ProjectConfiguration>, nxJsonConfiguration: NxJsonConfiguration, specifiedSourceMaps?: ConfigurationSourceMaps, defaultSourceMaps?: ConfigurationSourceMaps): CreateNodesResultEntry[];
export declare function createTargetDefaultsResults(specifiedPluginRootMap: Record<string, ProjectConfiguration>, defaultPluginRootMap: Record<string, ProjectConfiguration>, nxJsonConfiguration: NxJsonConfiguration, specifiedSourceMaps?: ConfigurationSourceMaps): CreateNodesResultEntry[];
/**

@@ -25,0 +25,0 @@ * Public reader that resolves the target defaults applying to a given

@@ -24,3 +24,3 @@ "use strict";

*/
function createTargetDefaultsResults(specifiedPluginRootMap, defaultPluginRootMap, nxJsonConfiguration, specifiedSourceMaps, defaultSourceMaps) {
function createTargetDefaultsResults(specifiedPluginRootMap, defaultPluginRootMap, nxJsonConfiguration, specifiedSourceMaps) {
const targetDefaultsConfig = nxJsonConfiguration.targetDefaults;

@@ -69,3 +69,3 @@ if (!targetDefaultsConfig) {

const sourcePlugin = needsSourcePlugin
? resolveSourcePlugin(root, targetName, specifiedSourceMaps, defaultSourceMaps)
? resolveSourcePlugin(root, targetName, defaultTargets[targetName], specifiedSourceMaps)
: undefined;

@@ -377,30 +377,29 @@ const syntheticTargets = buildSyntheticTargetsForRoot(targetName, root, effective, targetDefaults, projectName, projectNode, sourcePlugin);

}
function resolveSourcePlugin(root, targetName, specifiedSourceMaps, defaultSourceMaps) {
// Default-plugin attribution overrides specified-plugin attribution in the
// merge, so check it first. The executor/command keys carry the plugin that
// *created* the target, which is what `filter.plugin` ("targets originated by
// X") means. The top-level `targets.<name>` node key is deliberately NOT used
// as a fallback: it tracks the last writer, so a later plugin augmenting the
// target would mis-attribute it. The trade-off is that a target with neither
// an executor nor a command (a rare, non-runnable shape) resolves to no
// source plugin and won't match a `filter.plugin` default — accepted, since
// every runnable target carries one of these keys.
const executorKey = `${(0, source_maps_1.targetSourceMapKey)(targetName)}.executor`;
const commandKey = `${(0, source_maps_1.targetSourceMapKey)(targetName)}.command`;
const candidates = [
pluginFromSourceMap(defaultSourceMaps, root, executorKey),
pluginFromSourceMap(defaultSourceMaps, root, commandKey),
pluginFromSourceMap(specifiedSourceMaps, root, executorKey),
pluginFromSourceMap(specifiedSourceMaps, root, commandKey),
];
for (const candidate of candidates) {
if (candidate && candidate !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME)
return candidate;
function resolveSourcePlugin(root, targetName, defaultTarget, specifiedSourceMaps) {
// `filter.plugin` ("targets originated by X") can only name a plugin from
// nx.json's `plugins` — the specified set. When the default layer authors
// the target's identity (its merged config carries an executor or command,
// which the merge lets win), the originator is a default plugin and the
// target has no matchable source plugin; no source maps are needed to see
// that. Otherwise the specified layer's executor/command attribution names
// the originator. The top-level `targets.<name>` node key is deliberately
// NOT used as a fallback: it tracks ownership, so a later plugin augmenting
// the target would mis-attribute it. The trade-off is that a target with
// neither an executor nor a command (a rare, non-runnable shape) resolves to
// no source plugin and won't match a `filter.plugin` default — accepted,
// since every runnable target carries one of these keys.
if (defaultTarget &&
(defaultTarget.executor !== undefined ||
defaultTarget.command !== undefined)) {
return undefined;
}
const sourceMap = specifiedSourceMaps?.[root];
for (const identityKey of ['executor', 'command']) {
const plugin = sourceMap?.[`${(0, source_maps_1.targetSourceMapKey)(targetName)}.${identityKey}`]?.[1];
if (plugin && plugin !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME) {
return plugin;
}
}
return undefined;
}
function pluginFromSourceMap(maps, root, key) {
const entry = maps?.[root]?.[key];
return entry?.[1];
}
// Builds a name → MatcherProjectNode view for `findMatchingProjects` to

@@ -407,0 +406,0 @@ // consult, across both layered rootMaps. Tags are unioned across layers. A

@@ -173,2 +173,7 @@ "use strict";

if (baseHasConfig) {
// Base wins, so the incoming config is dropped without reaching
// `mergeConfigurationValue`. Validate its nested spread here so an
// integer-like-key ambiguity throws regardless of which side owns
// the config name (mirrors the base-independent target-level check).
(0, utils_1.assertNoIntegerLikeSpreadKey)(newConfigurations?.[configName], configIdentifier ? `Object at "${configIdentifier}"` : 'Object');
mergedConfigurations[configName] = baseConfigurations[configName];

@@ -286,4 +291,7 @@ }

// Integer-like keys get hoisted to targetKeys[0], making their position
// relative to '...' unrecoverable.
if (hasSpread &&
// relative to '...' unrecoverable. This is a property of the authored
// config, not of the base, so it throws regardless of compatibility —
// which also keeps the error identical between the target-defaults staging
// merge and the real merge, whose bases differ.
if (spreadPosInTarget >= 0 &&
targetKeys[0] &&

@@ -309,2 +317,9 @@ utils_1.INTEGER_LIKE_KEY_PATTERN.test(targetKeys[0])) {

// Before '...': base wins; fall through to target only if base lacks it.
// When base wins, the incoming `target[key]` is dropped without ever
// reaching `getMergeValueResult`, so validate its nested spread here —
// the integer-like-key ambiguity is a property of the authored value,
// not of which side owns the key, and must throw either way.
(0, utils_1.assertNoIntegerLikeSpreadKey)(target[key], projectConfigSourceMap
? `Object at "${targetIdentifier}.${key}"`
: 'Object');
result[key] =

@@ -387,9 +402,16 @@ key in mergeBase

}
// Update source map once after loop. Real plugins win last, but a target
// default — which only stamps fields onto an existing target and never
// authors its existence — must not steal the node key from the plugin that
// introduced the target. An incompatible replace clears these keys above, so
// a replacing real plugin still re-owns the node.
// Update the node key once after the loop. Ownership follows identity: this
// merge claims `targets.<name>` only when it changed the target's identity
// (a new/different executor or command, or an incompatible replace — whose
// key purge above empties the slot anyway). A plugin that only layers fields
// onto an existing target leaves the node with its creator; weak
// target-defaults stamps are always reclaimable.
if (projectConfigSourceMap) {
(0, source_maps_1.recordTargetIdentitySourceMapInfo)(projectConfigSourceMap, targetIdentifier, sourceInformation);
const identityChanged = !isCompatible ||
(target.executor !== undefined &&
target.executor !== baseTarget?.executor) ||
(target.command !== undefined &&
target.command !== baseTarget?.command) ||
suppliesNewOptionsIdentity(baseTarget, target);
(0, source_maps_1.recordTargetIdentitySourceMapInfo)(projectConfigSourceMap, targetIdentifier, sourceInformation, identityChanged);
}

@@ -450,2 +472,22 @@ // merge options if there are any

}
/**
* Run-commands and run-script targets carry their runnable identity in
* `options` (see {@link isCompatibleTarget}). A layer that supplies that
* identity where the base had none changed what the target runs — the same
* identity change as setting an executor on a target that had none.
*/
function suppliesNewOptionsIdentity(baseTarget, target) {
const executor = target.executor ?? baseTarget?.executor;
if (executor === 'nx:run-commands') {
const baseCommand = baseTarget?.options?.command ??
baseTarget?.options?.commands?.join(' && ');
const targetCommand = target.options?.command ?? target.options?.commands?.join(' && ');
return !!targetCommand && targetCommand !== baseCommand;
}
if (executor === 'nx:run-script') {
const targetScript = target.options?.script;
return !!targetScript && targetScript !== baseTarget?.options?.script;
}
return false;
}
function resolveNxTokensInOptions(object, project, key) {

@@ -452,0 +494,0 @@ const result = Array.isArray(object) ? [...object] : { ...object };

@@ -118,5 +118,18 @@ "use strict";

// the resulting project to change names from earlier plugins...
if (!project.name &&
(0, node_fs_1.existsSync)((0, path_1.join)(workspaceRoot, project.root, 'project.json'))) {
project.name = (0, to_project_name_1.toProjectName)((0, path_1.join)(root, 'project.json'));
if (!project.name) {
const projectJsonPath = (0, path_1.join)(workspaceRoot, project.root, 'project.json');
if ((0, node_fs_1.existsSync)(projectJsonPath)) {
// The project.json plugin may not have run (e.g. when a single
// plugin is run in isolation via `addPlugin` from a generator), so
// prefer the name declared in project.json before deriving one from
// the directory name.
let nameFromProjectJson;
try {
nameFromProjectJson =
(0, fileutils_1.readJsonFile)(projectJsonPath).name;
}
catch { }
project.name =
nameFromProjectJson ?? (0, to_project_name_1.toProjectName)((0, path_1.join)(root, 'project.json'));
}
}

@@ -123,0 +136,0 @@ try {

@@ -11,2 +11,17 @@ import { type SourceInformation } from './source-maps';

}
/**
* Throws `IntegerLikeSpreadKeyError` when `value` is a `'...'` spread object
* whose enumeration hoists an integer-like key ahead of the spread, making its
* authored position ambiguous.
*
* The ambiguity is a property of the authored value alone, not of any merge
* base. `mergeObjectWithSpread` runs this the moment it merges such a value —
* but a merge layer that lets the base win a key drops the incoming value
* without merging it, so the check must also be run eagerly at those
* base-owns-key shortcuts. Otherwise the error would surface or vanish
* depending on which side owns the key (e.g. it fires in the target-defaults
* staging merge but not the real merge), which is exactly the divergence that
* makes discarding staging errors unsafe.
*/
export declare function assertNoIntegerLikeSpreadKey(value: unknown, errorContext: string): void;
type SourceMapContext = {

@@ -13,0 +28,0 @@ sourceMap: Record<string, SourceInformation>;

@@ -5,2 +5,3 @@ "use strict";

exports.uniqueKeysInObjects = uniqueKeysInObjects;
exports.assertNoIntegerLikeSpreadKey = assertNoIntegerLikeSpreadKey;
exports.getMergeValueResult = getMergeValueResult;

@@ -35,2 +36,25 @@ const source_maps_1 = require("./source-maps");

/**
* Throws `IntegerLikeSpreadKeyError` when `value` is a `'...'` spread object
* whose enumeration hoists an integer-like key ahead of the spread, making its
* authored position ambiguous.
*
* The ambiguity is a property of the authored value alone, not of any merge
* base. `mergeObjectWithSpread` runs this the moment it merges such a value —
* but a merge layer that lets the base win a key drops the incoming value
* without merging it, so the check must also be run eagerly at those
* base-owns-key shortcuts. Otherwise the error would surface or vanish
* depending on which side owns the key (e.g. it fires in the target-defaults
* staging merge but not the real merge), which is exactly the divergence that
* makes discarding staging errors unsafe.
*/
function assertNoIntegerLikeSpreadKey(value, errorContext) {
if (!isObject(value) || value[exports.NX_SPREAD_TOKEN] !== true) {
return;
}
const keys = Object.keys(value);
if (keys[0] && exports.INTEGER_LIKE_KEY_PATTERN.test(keys[0])) {
throw new IntegerLikeSpreadKeyError(keys[0], errorContext);
}
}
/**
* `"..."` in `newValue` (as an array element or a key set to `true`)

@@ -57,3 +81,9 @@ * expands the base at that position; otherwise `newValue` replaces

// or a different one) still attributes to the new layer.
if (!isUnchangedPrimitive(newValue, baseValue)) {
//
// The exception cuts the other way too: when the *existing* attribution is a
// target-defaults stamp (TD merges before the plugin whose value it
// predicted, so its stamp lands first), a real plugin re-stating that value
// is the genuine author and reclaims the key.
if (!isUnchangedPrimitive(newValue, baseValue) ||
isReclaimableTargetDefaultsStamp(sourceMapContext)) {
writeTopLevelSourceMap(sourceMapContext);

@@ -63,2 +93,10 @@ }

}
// Whether the key's current attribution is a weak target-defaults stamp that
// the (non-target-defaults) writer should reclaim despite the value being
// unchanged.
function isReclaimableTargetDefaultsStamp(ctx) {
return (!!ctx &&
ctx.sourceMap[ctx.key]?.[1] === source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME &&
ctx.sourceInformation[1] !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME);
}
// Whether `newValue` leaves a defined primitive base untouched. Objects fall

@@ -127,8 +165,6 @@ // through (always re-attributed) to preserve the existing replace semantics —

: 'Object';
// Integer-like keys are hoisted to the front of enumeration, so one
// alongside `'...'` makes its authored position ambiguous.
assertNoIntegerLikeSpreadKey(newValue, errorContext);
const newKeys = Object.keys(newValue);
// Integer-like keys are hoisted to the front of enumeration, so if one
// exists alongside `'...'` it must be newKeys[0].
if (newKeys[0] && exports.INTEGER_LIKE_KEY_PATTERN.test(newKeys[0])) {
throw new IntegerLikeSpreadKeyError(newKeys[0], errorContext);
}
// Base per-key sources captured lazily — only for shared keys the new

@@ -135,0 +171,0 @@ // object overwrites before `'...'`, since writing their new source

@@ -32,2 +32,10 @@ import { TaskGraph } from '../../config/task-graph';

}
/**
* A task and how long it ran (ms). The unit the report's task lists are built from —
* shared with the renderers so the producer and the formatters can't drift.
*/
export interface TaskDurationRow {
id: string;
duration: number;
}
export interface PerformanceSummary {

@@ -38,6 +46,3 @@ runDuration: number;

/** Longest critical-path tasks that ran (desc, capped at a few), cache hits excluded; empty when the path was fully cached. */
criticalPathTop: Array<{
id: string;
duration: number;
}>;
criticalPathTop: TaskDurationRow[];
/** Ids of tasks that failed (slowest first), for the GitHub Actions summary's failed-tasks list. Continuous tasks and tasks without a complete window are excluded. */

@@ -65,2 +70,4 @@ failedTasks: string[];

remoteCacheEnabled: boolean;
/** The workspace opted out of Nx Cloud (`neverConnectToCloud` / NX_NO_CLOUD) — never recommend it. */
cloudOptedOut: boolean;
}

@@ -67,0 +74,0 @@ /** Construction-time inputs for {@link PerformanceLifeCycle}. */

@@ -21,2 +21,4 @@ "use strict";

const PRE_DISPATCH_HASH_GAP = 1000;
/** A critical-path task shorter than this fraction of the path is noise, not a speed-up target. */
const CRITICAL_PATH_TOP_MIN_FRACTION = 0.2;
/**

@@ -175,4 +177,4 @@ * Pure analysis over one finished run's collected timings — no lifecycle state and no

*/
computeCriticalPathTop(criticalPathTasks, durations) {
return criticalPathTasks
computeCriticalPathTop(criticalPathTasks, durations, criticalPathDuration) {
return (criticalPathTasks
.filter((id) => {

@@ -183,4 +185,6 @@ const status = this.statuses.get(id);

.map((id) => ({ id, duration: durations.get(id) ?? 0 }))
// Only tasks that meaningfully shape the path are worth speeding up.
.filter((t) => t.duration >= CRITICAL_PATH_TOP_MIN_FRACTION * criticalPathDuration)
.sort((a, b) => b.duration - a.duration)
.slice(0, 3);
.slice(0, 3));
}

@@ -250,3 +254,3 @@ /** Cache outcome: tasks restored (`cacheHits`) and the total with a cache outcome (`cacheableCount` = hits + ran). No-status tasks count for neither. */

});
const criticalPathTop = this.computeCriticalPathTop(criticalPathTasks, durations);
const criticalPathTop = this.computeCriticalPathTop(criticalPathTasks, durations, criticalPathDuration);
const { cacheHits, cacheableCount } = this.computeCacheStats();

@@ -257,2 +261,5 @@ // `skipNxCache` already folds in NX_SKIP_NX_CACHE / NX_DISABLE_NX_CACHE

const remoteCacheEnabled = this.remoteCacheEnabled();
const cloudOptedOut = this.options.nxJson
? !!(0, nx_cloud_utils_1.isNxCloudDisabled)(this.options.nxJson)
: false;
// Coordinator-dominated: hashing/scheduling outweighs task work by >3x the

@@ -279,4 +286,5 @@ // critical path, which keeps cold runs critical-path-bound.

isCI,
// Can only start distributing in CI when not already doing so.
canDistribute: isCI && !distributing,
// Can only start distributing in CI when not already doing so — and never
// suggest Nx Agents to a workspace that opted out of Nx Cloud.
canDistribute: isCI && !distributing && !cloudOptedOut,
distributing,

@@ -288,2 +296,3 @@ coordinatorDominated,

remoteCacheEnabled,
cloudOptedOut,
};

@@ -290,0 +299,0 @@ }

@@ -145,4 +145,5 @@ "use strict";

console.log((0, performance_report_1.formatReport)(summary));
// In GitHub Actions, also append the report (plus a per-task table) to the job
// summary page. Independent of the console.log above so neither masks the other.
// In GitHub Actions, also append the report to the job summary page — the same stats
// as above, led by the run's outcome (a failed-tasks list, or a success line).
// Independent of the console.log above so neither masks the other.
writePerformanceReportToGitHubActions(summary);

@@ -161,4 +162,4 @@ }

* Actions (`$GITHUB_STEP_SUMMARY` is set there and nowhere else). No-op otherwise. The
* per-task table is computed lazily so non-CI runs don't pay for it. Best-effort: a
* write failure must never affect the run.
* Markdown is rendered below the guard, so non-CI runs don't pay to format a report
* nothing reads. Best-effort: a write failure must never affect the run.
*

@@ -165,0 +166,0 @@ * Skipped for a nested run (one nx command invoked by another nx task's command), so only

import type { PerformanceSummaryPayload } from '../../native';
import type { PerformanceSummary } from './performance-analysis';
import type { PerformanceSummary, TaskDurationRow } from './performance-analysis';
/**
* A recommendation built from structured parts so the link text comes from the
* link definition (not a substring scanned out of the assembled report). A part
* is either literal text or a {@link RecLink}; the renderers below project the
* same parts to the terminal string, the payload string, and the popup links.
* A recommendation built from structured parts so the link text comes from the link
* definition (not a substring scanned out of the assembled report). A part is literal
* text, a {@link RecLink}, or a {@link RecTaskRows}, projected to each output string by
* {@link renderRecommendation}.
*
* String parts must be single-line — multi-line content needs its own structured part (as
* {@link RecTaskRows} is). TS can't enforce this: `string` is already a handled member, so
* a multi-line one compiles and then breaks the Markdown nested list.
*/
type RecPart = string | RecLink;
type RecPart = string | RecLink | RecTaskRows;
export type Recommendation = RecPart[];

@@ -18,8 +22,12 @@ /**

interface RecLink {
/** Visible label: the sentence that links. */
visible: string;
/** OSC 8 click target / appended URL: the utm-tagged URL. */
href: string;
}
/**
* The critical path's longest tasks as data, so each renderer formats them natively:
* the terminal and payload as space-aligned columns, Markdown as a nested list
* (HTML collapses space runs, so aligned columns don't survive rendering there).
*/
type RecTaskRows = TaskDurationRow[];
/**
* The recommendation string the napi payload ships and the Rust popup matches against.

@@ -29,2 +37,4 @@ * Links are URL-less (the popup re-links them from {@link PerformanceSummaryPayload.links}).

export declare function recommendationToPayloadString(rec: Recommendation): string;
/** Below this run duration (ms), the run is already fast — recommend nothing. */
export declare const MIN_RECOMMENDATION_RUN_DURATION = 30000;
/** Below this (ms) overhead is noise, not worth a recommendation. */

@@ -31,0 +41,0 @@ export declare const MEANINGFUL_OVERHEAD = 1000;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MEANINGFUL_OVERHEAD = void 0;
exports.MEANINGFUL_OVERHEAD = exports.MIN_RECOMMENDATION_RUN_DURATION = void 0;
exports.recommendationToPayloadString = recommendationToPayloadString;

@@ -28,6 +28,33 @@ exports.buildRecommendations = buildRecommendations;

}
// Discriminate positively — test for what each part *is*. A `!isRecTaskRows` catch-all
// would misclassify a future `RecPart` member as a link; TS can't catch that (it never
// checks a predicate body), so `recommendationLinks`' `.filter(isRecLink)` would ship
// `{text: undefined, href: undefined}` to the popup.
function isRecLink(part) {
return typeof part !== 'string';
return typeof part !== 'string' && 'href' in part;
}
function isRecTaskRows(part) {
return Array.isArray(part);
}
/**
* Project a recommendation to a string, formatting each non-text part with the caller's
* renderers. The three output targets (payload, terminal, Markdown) share this one dispatch.
* After the string and task-rows branches a part is a {@link RecLink}, so `render.link`
* takes it directly — and a new {@link RecPart} member that is neither would fail to satisfy
* that `RecLink` parameter, turning "forgot to handle it" into a compile error right here.
*/
function renderRecommendation(rec, render) {
return rec
.map((part) => {
if (typeof part === 'string') {
return part;
}
if (isRecTaskRows(part)) {
return render.taskRows(part);
}
return render.link(part);
})
.join('');
}
/**
* The recommendation string the napi payload ships and the Rust popup matches against.

@@ -37,4 +64,11 @@ * Links are URL-less (the popup re-links them from {@link PerformanceSummaryPayload.links}).

function recommendationToPayloadString(rec) {
return rec.map((part) => (!isRecLink(part) ? part : part.visible)).join('');
return renderRecommendation(rec, {
link: (link) => link.visible,
taskRows: taskRowsToText,
});
}
/** Task rows as the text block the terminal and payload embed: newline-led, space-aligned columns. */
function taskRowsToText(tasks) {
return ['', ...formatTopTaskRows(tasks)].join('\n');
}
/**

@@ -47,12 +81,8 @@ * The recommendation as a terminal string. With OSC 8 the phrase becomes a hyperlink

function recommendationToTerminalString(rec, hyperlinks) {
return rec
.map((part) => {
if (!isRecLink(part)) {
return part;
}
return hyperlinks
? (0, terminal_link_1.terminalLink)(part.visible, part.href)
: `${part.visible} → ${part.href}`;
})
.join('');
return renderRecommendation(rec, {
link: (link) => hyperlinks
? (0, terminal_link_1.terminalLink)(link.visible, link.href)
: `${link.visible} → ${link.href}`,
taskRows: taskRowsToText,
});
}

@@ -65,2 +95,4 @@ /** The popup links (phrase + href) for every link in a recommendation list, for OSC 8 re-linking. */

}
/** Below this run duration (ms), the run is already fast — recommend nothing. */
exports.MIN_RECOMMENDATION_RUN_DURATION = 30_000;
/** At/below this hit rate, recommend remote cache (if off); above it caching works. */

@@ -82,3 +114,4 @@ const LOW_CACHE_HIT_RATE = 0.1;

function formatTopTaskRows(tasks) {
// The only caller returns early when empty, so `tasks` is non-empty here.
// Non-empty by construction: the only recommendation carrying task rows requires
// `criticalPathTop.length > 0` to apply, so no empty array reaches the widths below.
const idWidth = Math.max(...tasks.map((t) => t.id.length));

@@ -144,4 +177,6 @@ const durations = tasks.map((t) => (0, native_1.formatDuration)(t.duration));

// Barely-used cache with no remote: set up Nx Cloud. Whole-phrase link; the payload
// string stays URL-less (the popup re-links the phrase).
// string stays URL-less (the popup re-links the phrase). Never pushed at a
// workspace that opted out of Nx Cloud.
isApplicable: (c) => !c.cacheSkipped &&
!c.cloudOptedOut &&
c.cacheableCount > 0 &&

@@ -177,6 +212,4 @@ !c.remoteCacheEnabled &&

build: (c) => [
[
`Speed up or split the longest tasks on the critical path:`,
...formatTopTaskRows(c.criticalPathTop),
].join('\n'),
`Speed up or split the longest tasks on the critical path:`,
c.criticalPathTop,
],

@@ -191,2 +224,6 @@ },

function buildRecommendations(s) {
// A fast run has nothing worth optimizing — stats only, no advice.
if (s.runDuration < exports.MIN_RECOMMENDATION_RUN_DURATION) {
return [];
}
const c = {

@@ -205,2 +242,3 @@ recoverableByParallel: s.recoverableByParallel,

remoteCacheEnabled: s.remoteCacheEnabled,
cloudOptedOut: s.cloudOptedOut,
};

@@ -264,14 +302,13 @@ return RECOMMENDATIONS.filter((r) => r.isApplicable(c)).map((r) => r.build(c));

* A recommendation as Markdown: every link becomes `[phrase](href)` (no OSC 8, unlike the
* terminal renderer) — the whole sentence reads as prose and is the link text. The
* critical-path rec embeds newline-separated, space-aligned task rows; collapse them to
* `<br>`-joined lines so they render inside the list item.
* terminal renderer) — the whole sentence reads as prose and is the link text. Task rows
* become a nested list under the recommendation's bullet (space-aligned columns don't
* survive HTML's whitespace collapsing).
*/
function recommendationToMarkdownString(rec) {
return rec
.map((part) => !isRecLink(part) ? part : `[${part.visible}](${part.href})`)
.join('')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.join('<br>');
return renderRecommendation(rec, {
link: (link) => `[${link.visible}](${link.href})`,
taskRows: (rows) => rows
.map((t) => `\n - \`${t.id}\` — ${(0, native_1.formatDuration)(t.duration)}`)
.join(''),
});
}

@@ -278,0 +315,0 @@ /**

@@ -28,2 +28,3 @@ "use strict";

const output_1 = require("../utils/output");
const configure_ai_agents_disclaimer_1 = require("../ai/configure-ai-agents-disclaimer");
const sync_generators_1 = require("../utils/sync-generators");

@@ -414,5 +415,6 @@ const workspace_root_1 = require("../utils/workspace-root");

const { outdatedAgents } = await client_1.daemonClient.getConfigureAiAgentsStatus();
if (outdatedAgents.length > 0) {
output_1.output.logRawLine(output_1.output.dim('Your AI agent configuration is outdated. Run "nx configure-ai-agents" to update.'));
if (!(0, configure_ai_agents_disclaimer_1.shouldPrintConfigureAiAgentsDisclaimer)(outdatedAgents, workspace_root_1.workspaceRoot)) {
return;
}
output_1.output.logRawLine(output_1.output.dim('Your AI agent configuration is outdated. Run "nx configure-ai-agents" to update.'));
}

@@ -419,0 +421,0 @@ catch {

import { ProjectGraph } from '../config/project-graph';
import { Task } from '../config/task-graph';
/**
* Resolves the FORCE_COLOR value for forked child processes.
*
* When the user sets FORCE_COLOR=0, bin/nx.ts deletes it from process.env
* (workaround for picocolors treating "0" as truthy) and saves the original
* value in NX_ORIGINAL_FORCE_COLOR. Without this check, the undefined
* FORCE_COLOR would default to 'true', re-enabling colors in all children.
*/
export declare function getForceColorForChild(): string;
export declare function getEnvVariablesForBatchProcess(skipNxCache: boolean, captureStderr: boolean): NodeJS.ProcessEnv;

@@ -4,0 +13,0 @@ export declare function getTaskSpecificEnv(task: Task, graph: ProjectGraph): NodeJS.ProcessEnv;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getForceColorForChild = getForceColorForChild;
exports.getEnvVariablesForBatchProcess = getEnvVariablesForBatchProcess;

@@ -14,9 +15,29 @@ exports.getTaskSpecificEnv = getTaskSpecificEnv;

const task_env_paths_1 = require("./task-env-paths");
/**
* Resolves the FORCE_COLOR value for forked child processes.
*
* When the user sets FORCE_COLOR=0, bin/nx.ts deletes it from process.env
* (workaround for picocolors treating "0" as truthy) and saves the original
* value in NX_ORIGINAL_FORCE_COLOR. Without this check, the undefined
* FORCE_COLOR would default to 'true', re-enabling colors in all children.
*/
function getForceColorForChild() {
if (process.env.FORCE_COLOR !== undefined) {
return process.env.FORCE_COLOR;
}
if (process.env.NX_ORIGINAL_FORCE_COLOR === '0') {
return '0';
}
return 'true';
}
function getEnvVariablesForBatchProcess(skipNxCache, captureStderr) {
return {
const res = {
// User Process Env Variables override Dotenv Variables
...process.env,
// Nx Env Variables overrides everything
...getNxEnvVariablesForForkedProcess(process.env.FORCE_COLOR === undefined ? 'true' : process.env.FORCE_COLOR, skipNxCache, captureStderr),
...getNxEnvVariablesForForkedProcess(getForceColorForChild(), skipNxCache, captureStderr),
};
// NX_ORIGINAL_FORCE_COLOR is an internal signal and should not leak into child processes
delete res.NX_ORIGINAL_FORCE_COLOR;
return res;
}

@@ -62,2 +83,5 @@ // The orchestrator now calls this eagerly during the coordinator pre-hash

delete res.NX_SET_CLI;
// NX_ORIGINAL_FORCE_COLOR is an internal signal used by getForceColorForChild()
// and should not leak into child processes
delete res.NX_ORIGINAL_FORCE_COLOR;
return res;

@@ -64,0 +88,0 @@ }

@@ -38,2 +38,3 @@ import { NxJsonConfiguration } from '../config/nx-json';

private processedTasks;
private cacheMissedHashes;
private completedTasks;

@@ -104,3 +105,5 @@ private waitingForTasks;

* their own cache lookup on the assumption that this has already
* confirmed them as misses. Don't add length-based bails.
* confirmed them as misses. Excluding cacheMissedHashes preserves that
* invariant — every dispatched hash was queried exactly once — but
* don't add other length-based bails.
*/

@@ -107,0 +110,0 @@ private resolveCachedTasksBulk;

@@ -10,7 +10,1 @@ /**

export declare function promptForAnalyticsPreference(): Promise<boolean>;
/**
* Generates a deterministic workspace ID.
* Priority: nxCloudId > git remote URL (hashed).
* Returns null if neither is available (no telemetry).
*/
export declare function generateWorkspaceId(cwd?: string): string | null;

@@ -5,5 +5,2 @@ "use strict";

exports.promptForAnalyticsPreference = promptForAnalyticsPreference;
exports.generateWorkspaceId = generateWorkspaceId;
const crypto_1 = require("crypto");
const child_process_1 = require("child_process");
const fs_1 = require("fs");

@@ -89,49 +86,1 @@ const enquirer_1 = require("enquirer");

}
/**
* Generates a deterministic workspace ID.
* Priority: nxCloudId > git remote URL (hashed).
* Returns null if neither is available (no telemetry).
*/
function generateWorkspaceId(cwd) {
const root = cwd ?? workspace_root_1.workspaceRoot;
// Use nxCloudId if available — most stable identifier
const nxJson = (0, nx_json_1.readNxJson)(root);
const nxCloudId = nxJson?.nxCloudId ?? nxJson?.nxCloudAccessToken;
if (nxCloudId) {
return nxCloudId;
}
// Fall back to git remote URL hash
try {
const remoteUrl = (0, child_process_1.execSync)('git remote get-url origin', {
stdio: 'pipe',
cwd: root,
windowsHide: true,
})
.toString()
.trim();
if (remoteUrl) {
return (0, crypto_1.createHash)('sha256').update(remoteUrl).digest('hex').slice(0, 32);
}
}
catch {
// No git remote available
}
// Fall back to first commit SHA — already a hash
try {
const firstCommit = (0, child_process_1.execSync)('git rev-list --max-parents=0 HEAD', {
stdio: 'pipe',
cwd: root,
windowsHide: true,
})
.toString()
.trim()
.split('\n')[0];
if (firstCommit) {
return firstCommit;
}
}
catch {
// Not a git repo
}
return null;
}
import type { Tree } from '../../generators/tree';
import type { CatalogDefinitions } from './types';
export declare function readCatalogConfigFromFs(filename: string, fullPath: string): CatalogDefinitions | null;
export declare function readCatalogConfigFromTree(filename: string, tree: Tree): CatalogDefinitions | null;
export declare function readCatalogDefinitions(filename: string, treeOrRoot: Tree | string, cache: Map<string, CatalogDefinitions | null>): CatalogDefinitions | null;
export declare function updateCatalogVersionsInFile(filename: string, treeOrRoot: Tree | string, updates: Array<{

@@ -6,0 +5,0 @@ packageName: string;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readCatalogConfigFromFs = readCatalogConfigFromFs;
exports.readCatalogConfigFromTree = readCatalogConfigFromTree;
exports.readCatalogDefinitions = readCatalogDefinitions;
exports.updateCatalogVersionsInFile = updateCatalogVersionsInFile;
// Keep in sync with packages/devkit/src/utils/catalog/manager-utils.ts; the body
// below the imports is duplicated because @nx/devkit supports a range of nx majors
// and this logic isn't part of the nx surface it can import across that range.
const js_yaml_1 = require("@zkochan/js-yaml");

@@ -122,2 +118,23 @@ const node_fs_1 = require("node:fs");

}
// Managers are created per operation (getCatalogManager news one up), so the
// fs (string-root) branch is cached to read the file once per pass instead of
// once per catalog reference. The Tree branch stays live since the tree is
// mutable within a generator.
function readCatalogDefinitions(filename, treeOrRoot, cache) {
if (typeof treeOrRoot === 'string') {
if (cache.has(treeOrRoot)) {
return cache.get(treeOrRoot);
}
const configPath = (0, node_path_1.join)(treeOrRoot, filename);
const defs = (0, node_fs_1.existsSync)(configPath)
? readCatalogConfigFromFs(filename, configPath)
: null;
cache.set(treeOrRoot, defs);
return defs;
}
if (!treeOrRoot.exists(filename)) {
return null;
}
return readCatalogConfigFromTree(filename, treeOrRoot);
}
function updateCatalogVersionsInFile(filename, treeOrRoot, updates) {

@@ -124,0 +141,0 @@ let checkExists;

@@ -10,2 +10,3 @@ import type { Tree } from '../../generators/tree';

readonly catalogProtocol = "catalog:";
private definitionsByRoot;
isCatalogReference(version: string): boolean;

@@ -12,0 +13,0 @@ parseCatalogReference(version: string): CatalogReference | null;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PnpmCatalogManager = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const manager_1 = require("./manager");

@@ -16,2 +14,4 @@ const manager_utils_1 = require("./manager-utils");

this.catalogProtocol = 'catalog:';
// Parsed fs-root definitions, cached per pass. See readCatalogDefinitions.
this.definitionsByRoot = new Map();
}

@@ -37,15 +37,3 @@ isCatalogReference(version) {

getCatalogDefinitions(treeOrRoot) {
if (typeof treeOrRoot === 'string') {
const configPath = (0, node_path_1.join)(treeOrRoot, PNPM_WORKSPACE_FILENAME);
if (!(0, node_fs_1.existsSync)(configPath)) {
return null;
}
return (0, manager_utils_1.readCatalogConfigFromFs)(PNPM_WORKSPACE_FILENAME, configPath);
}
else {
if (!treeOrRoot.exists(PNPM_WORKSPACE_FILENAME)) {
return null;
}
return (0, manager_utils_1.readCatalogConfigFromTree)(PNPM_WORKSPACE_FILENAME, treeOrRoot);
}
return (0, manager_utils_1.readCatalogDefinitions)(PNPM_WORKSPACE_FILENAME, treeOrRoot, this.definitionsByRoot);
}

@@ -52,0 +40,0 @@ resolveCatalogReference(treeOrRoot, packageName, version) {

@@ -10,2 +10,3 @@ import type { Tree } from '../../generators/tree';

readonly catalogProtocol = "catalog:";
private definitionsByRoot;
isCatalogReference(version: string): boolean;

@@ -12,0 +13,0 @@ parseCatalogReference(version: string): CatalogReference | null;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YarnCatalogManager = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const manager_1 = require("./manager");

@@ -16,2 +14,4 @@ const manager_utils_1 = require("./manager-utils");

this.catalogProtocol = 'catalog:';
// Parsed fs-root definitions, cached per pass. See readCatalogDefinitions.
this.definitionsByRoot = new Map();
}

@@ -37,15 +37,3 @@ isCatalogReference(version) {

getCatalogDefinitions(treeOrRoot) {
if (typeof treeOrRoot === 'string') {
const configPath = (0, node_path_1.join)(treeOrRoot, YARNRC_FILENAME);
if (!(0, node_fs_1.existsSync)(configPath)) {
return null;
}
return (0, manager_utils_1.readCatalogConfigFromFs)(YARNRC_FILENAME, configPath);
}
else {
if (!treeOrRoot.exists(YARNRC_FILENAME)) {
return null;
}
return (0, manager_utils_1.readCatalogConfigFromTree)(YARNRC_FILENAME, treeOrRoot);
}
return (0, manager_utils_1.readCatalogDefinitions)(YARNRC_FILENAME, treeOrRoot, this.definitionsByRoot);
}

@@ -52,0 +40,0 @@ resolveCatalogReference(treeOrRoot, packageName, version) {

@@ -14,2 +14,3 @@ "use strict";

const child_process_1 = require("child_process");
const git_revision_1 = require("./git-revision");
const workspace_root_1 = require("./workspace-root");

@@ -196,26 +197,23 @@ const shared_options_1 = require("../command-line/yargs-utils/shared-options");

function getUncommittedFiles() {
return parseGitOutput(`git diff --name-only --no-renames --relative HEAD .`);
return parseGitOutput([
'diff',
'--name-only',
'--no-renames',
'--relative',
'HEAD',
'.',
]);
}
function getUntrackedFiles() {
return parseGitOutput(`git ls-files --others --exclude-standard`);
return parseGitOutput(['ls-files', '--others', '--exclude-standard']);
}
function getMergeBase(base, head = 'HEAD') {
(0, git_revision_1.assertValidGitRevision)(base);
(0, git_revision_1.assertValidGitRevision)(head);
try {
return (0, child_process_1.execSync)(`git merge-base "${base}" "${head}"`, {
maxBuffer: file_utils_1.TEN_MEGABYTES,
cwd: workspace_root_1.workspaceRoot,
stdio: 'pipe',
windowsHide: true,
})
.toString()
.trim();
return runGit(['merge-base', base, head]).toString().trim();
}
catch {
try {
return (0, child_process_1.execSync)(`git merge-base --fork-point "${base}" "${head}"`, {
maxBuffer: file_utils_1.TEN_MEGABYTES,
cwd: workspace_root_1.workspaceRoot,
stdio: 'pipe',
windowsHide: true,
})
return runGit(['merge-base', '--fork-point', base, head])
.toString()

@@ -230,6 +228,15 @@ .trim();

function getFilesUsingBaseAndHead(base, head) {
return parseGitOutput(`git diff --name-only --no-renames --relative "${base}" "${head}"`);
(0, git_revision_1.assertValidGitRevision)(base);
(0, git_revision_1.assertValidGitRevision)(head);
return parseGitOutput([
'diff',
'--name-only',
'--no-renames',
'--relative',
base,
head,
]);
}
function parseGitOutput(command) {
return (0, child_process_1.execSync)(command, {
function runGit(args) {
return (0, child_process_1.execFileSync)('git', args, {
maxBuffer: file_utils_1.TEN_MEGABYTES,

@@ -239,3 +246,6 @@ cwd: workspace_root_1.workspaceRoot,

windowsHide: true,
})
});
}
function parseGitOutput(args) {
return runGit(args)
.toString('utf-8')

@@ -242,0 +252,0 @@ .split('\n')

@@ -31,5 +31,3 @@ export declare function cloneFromUpstream(url: string, destination: string, { originName, depth }?: {

filterBranch(source: string, destination: string, branchName: string): Promise<void>;
private execAsync;
private quotePath;
private quoteArg;
private execGit;
}

@@ -42,2 +40,17 @@ export interface VcsRemoteInfo {

export declare function getVcsRemoteInfo(directory?: string): VcsRemoteInfo | null;
export declare function getGitRootPath(cwd?: string): string;
/**
* Path of `directory` relative to its git root, posix-separated so it is
* identical on every OS, and '' when the directory is the git root itself.
* Null outside a git repository.
*/
export declare function getGitRootRelativePath(directory: string): string | null;
/** A shallow clone's truncated history has no stable root commit. */
export declare function isShallowRepository(directory?: string): boolean;
/**
* SHA of the repository's first commit. Merged unrelated histories leave
* several root commits — the sorted-first one is picked so every clone
* agrees. Null when there are no commits, or outside a git repository.
*/
export declare function getFirstCommitSha(directory?: string): string | null;
export declare function isGitRepository(directory?: string): boolean;

@@ -44,0 +57,0 @@ export declare function hasUncommittedChanges(directory?: string): boolean;

@@ -7,2 +7,6 @@ "use strict";

exports.getVcsRemoteInfo = getVcsRemoteInfo;
exports.getGitRootPath = getGitRootPath;
exports.getGitRootRelativePath = getGitRootRelativePath;
exports.isShallowRepository = isShallowRepository;
exports.getFirstCommitSha = getFirstCommitSha;
exports.isGitRepository = isGitRepository;

@@ -20,5 +24,5 @@ exports.hasUncommittedChanges = hasUncommittedChanges;

const logger_1 = require("./logger");
function execAsync(command, execOptions) {
function execFileAsync(file, args, execOptions) {
return new Promise((res, rej) => {
(0, child_process_1.exec)(command, { ...execOptions, windowsHide: true }, (err, stdout, stderr) => {
(0, child_process_1.execFile)(file, args, { ...execOptions, windowsHide: true }, (err, stdout) => {
if (err) {

@@ -34,3 +38,10 @@ return rej(err);

}) {
await execAsync(`git clone ${url} ${destination} ${depth ? `--depth ${depth}` : ''} --origin ${originName}`, {
await execFileAsync('git', [
'clone',
url,
destination,
...(depth ? ['--depth', `${depth}`] : []),
'--origin',
originName,
], {
cwd: (0, path_1.dirname)(destination),

@@ -47,21 +58,21 @@ maxBuffer: 10 * 1024 * 1024,

getGitRootPath(cwd) {
return (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
cwd,
windowsHide: true,
})
.toString()
.trim();
return getGitRootPath(cwd);
}
async hasUncommittedChanges() {
const data = await this.execAsync(`git status --porcelain`);
const data = await this.execGit(['status', '--porcelain']);
return data.trim() !== '';
}
async addFetchRemote(remoteName, branch) {
return await this.execAsync(`git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"`);
return await this.execGit([
'config',
'--add',
`remote.${remoteName}.fetch`,
`+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}`,
]);
}
async showStat() {
return await this.execAsync(`git show --stat`);
return await this.execGit(['show', '--stat']);
}
async listBranches() {
return (await this.execAsync(`git ls-remote --heads --quiet`))
return (await this.execGit(['ls-remote', '--heads', '--quiet']))
.trim()

@@ -77,3 +88,3 @@ .split('\n')

// This avoids problems with special characters in file names.
return (await this.execAsync(`git ls-files -z ${path}`))
return (await this.execGit(['ls-files', '-z', '--', path]))
.trim()

@@ -85,34 +96,47 @@ .split('\x00')

async reset(ref) {
return await this.execAsync(`git reset ${ref} --hard`);
return await this.execGit(['reset', '--hard', ref]);
}
async mergeUnrelatedHistories(ref, message) {
return await this.execAsync(`git merge ${ref} -X ours --allow-unrelated-histories -m "${message}"`);
return await this.execGit([
'merge',
ref,
'-X',
'ours',
'--allow-unrelated-histories',
'-m',
message,
]);
}
async fetch(remote, ref) {
return await this.execAsync(`git fetch ${remote}${ref ? ` ${ref}` : ''}`);
return await this.execGit(['fetch', remote, ...(ref ? [ref] : [])]);
}
async checkout(branch, opts) {
return await this.execAsync(`git checkout ${opts.new ? '-b ' : ' '}${branch}${opts.base ? ' ' + opts.base : ''}`);
return await this.execGit([
'checkout',
...(opts.new ? ['-b'] : []),
branch,
...(opts.base ? [opts.base] : []),
]);
}
async move(path, destination) {
return await this.execAsync(`git mv ${this.quotePath(path)} ${this.quotePath(destination)}`);
return await this.execGit(['mv', '--', path, destination]);
}
async push(ref, remoteName) {
return await this.execAsync(`git push -u -f ${remoteName} ${ref}`);
return await this.execGit(['push', '-u', '-f', remoteName, ref]);
}
async commit(message) {
return await this.execAsync(`git commit -am "${message}"`);
return await this.execGit(['commit', '-am', message]);
}
async amendCommit() {
return await this.execAsync(`git commit --amend -a --no-edit`);
return await this.execGit(['commit', '--amend', '-a', '--no-edit']);
}
async deleteGitRemote(name) {
return await this.execAsync(`git remote rm ${name}`);
return await this.execGit(['remote', 'rm', name]);
}
async addGitRemote(name, url) {
return await this.execAsync(`git remote add ${name} ${url}`);
return await this.execGit(['remote', 'add', name, url]);
}
async hasFilterRepoInstalled() {
try {
await this.execAsync(`git filter-repo --help`);
await this.execGit(['filter-repo', '--help']);
return true;

@@ -130,9 +154,16 @@ }

const destinationPosixPath = destination.split(path_1.sep).join(path_1.posix.sep);
await this.execAsync(`git filter-repo -f ${source !== '' ? `--path ${this.quotePath(sourcePosixPath)}` : ''} ${source !== destination
? `--path-rename ${this.quotePath(sourcePosixPath, true)}:${this.quotePath(destinationPosixPath, true)}`
: ''}`);
const sourcePath = ensureTrailingSlash(sourcePosixPath);
const destinationPath = ensureTrailingSlash(destinationPosixPath);
await this.execGit([
'filter-repo',
'-f',
...(source !== '' ? ['--path', sourcePosixPath] : []),
...(source !== destination
? ['--path-rename', `${sourcePath}:${destinationPath}`]
: []),
]);
}
async filterBranch(source, destination, branchName) {
// We need non-ASCII file names to not be quoted, or else filter-branch will exclude them.
await this.execAsync(`git config core.quotepath false`);
await this.execGit(['config', 'core.quotepath', 'false']);
// NOTE: filter-repo requires POSIX path to work

@@ -143,4 +174,12 @@ const sourcePosixPath = source.split(path_1.sep).join(path_1.posix.sep);

if (source !== '') {
const indexFilterCommand = this.quoteArg(`node ${(0, path_1.join)(__dirname, 'git-utils.index-filter.js')}`);
await this.execAsync(`git filter-branch -f --index-filter ${indexFilterCommand} --prune-empty -- ${branchName}`, {
const indexFilterCommand = `node ${quoteForShell((0, path_1.join)(__dirname, 'git-utils.index-filter.js'))}`;
await this.execGit([
'filter-branch',
'-f',
'--index-filter',
indexFilterCommand,
'--prune-empty',
'--',
branchName,
], {
NX_IMPORT_SOURCE: sourcePosixPath,

@@ -152,4 +191,11 @@ NX_IMPORT_DESTINATION: destinationPosixPath,

if (source === '' || source !== destination) {
const treeFilterCommand = this.quoteArg(`node ${(0, path_1.join)(__dirname, 'git-utils.tree-filter.js')}`);
await this.execAsync(`git filter-branch -f --tree-filter ${treeFilterCommand} -- ${branchName}`, {
const treeFilterCommand = `node ${quoteForShell((0, path_1.join)(__dirname, 'git-utils.tree-filter.js'))}`;
await this.execGit([
'filter-branch',
'-f',
'--tree-filter',
treeFilterCommand,
'--',
branchName,
], {
NX_IMPORT_SOURCE: sourcePosixPath,

@@ -160,4 +206,4 @@ NX_IMPORT_DESTINATION: destinationPosixPath,

}
execAsync(command, env) {
return execAsync(command, {
execGit(args, env) {
return execFileAsync('git', args, {
cwd: this.root,

@@ -171,21 +217,10 @@ maxBuffer: 10 * 1024 * 1024,

}
quotePath(path, ensureTrailingSlash) {
return this.quoteArg(ensureTrailingSlash && path !== '' && !path.endsWith('/')
? `${path}/`
: path);
}
quoteArg(arg) {
return process.platform === 'win32'
? // Windows/CMD only understands double-quotes, single-quotes are treated as part of the file name
// Bash and other shells will substitute `$` in file names with a variable value.
`"${arg
// Need to keep two slashes for Windows or else the path will be invalid.
// e.g. 'C:\Users\bob\projects\repo' is invalid, but 'C:\\Users\\bob\\projects\\repo' is valid
.replaceAll('\\', '\\\\')}"`
: // e.g. `git mv "$$file.txt" "libs/a/$$file.txt"` will not work since `$$` is swapped with the PID of the last process.
// Using single-quotes prevents this substitution.
`'${arg}'`;
}
}
exports.GitRepository = GitRepository;
function ensureTrailingSlash(path) {
return path !== '' && !path.endsWith('/') ? `${path}/` : path;
}
function quoteForShell(arg) {
return `'${arg.replaceAll("'", "'\"'\"'")}'`;
}
function parseVcsRemoteUrl(url) {

@@ -274,2 +309,61 @@ // Remove whitespace and handle common URL formats

}
function getGitRootPath(cwd) {
return (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
cwd,
windowsHide: true,
})
.toString()
.trim();
}
/**
* Path of `directory` relative to its git root, posix-separated so it is
* identical on every OS, and '' when the directory is the git root itself.
* Null outside a git repository.
*/
function getGitRootRelativePath(directory) {
try {
return (0, path_1.relative)(getGitRootPath(directory), directory)
.split(path_1.sep)
.join(path_1.posix.sep);
}
catch {
return null;
}
}
/** A shallow clone's truncated history has no stable root commit. */
function isShallowRepository(directory) {
try {
return ((0, child_process_1.execFileSync)('git', ['rev-parse', '--is-shallow-repository'], {
encoding: 'utf8',
stdio: 'pipe',
cwd: directory,
windowsHide: true,
}).trim() === 'true');
}
catch {
return false;
}
}
/**
* SHA of the repository's first commit. Merged unrelated histories leave
* several root commits — the sorted-first one is picked so every clone
* agrees. Null when there are no commits, or outside a git repository.
*/
function getFirstCommitSha(directory) {
try {
const roots = (0, child_process_1.execFileSync)('git', ['rev-list', '--max-parents=0', 'HEAD'], {
encoding: 'utf8',
stdio: 'pipe',
cwd: directory,
windowsHide: true,
})
.trim()
.split(/\r?\n/)
.filter(Boolean);
return roots.sort()[0] ?? null;
}
catch {
return null;
}
}
function isGitRepository(directory) {

@@ -276,0 +370,0 @@ try {

@@ -91,11 +91,13 @@ "use strict";

}
// Extracts the cooldown keys from pnpm's reported config. pnpm reports the
// kebab-case keys; the exclude list comes back as a JSON array (set in a yaml
// surface) or a comma-joined string (set via .npmrc / env).
// Extracts the cooldown keys from pnpm's reported config. pnpm 11 reports them
// camelCase; pnpm 10 reported them kebab-case, so read both forms. The exclude
// list comes back as a JSON array (set in a yaml surface) or a comma-joined
// string (set via .npmrc / env).
function parseCooldownConfig(config) {
const read = (camelKey, kebabKey) => config[camelKey] ?? config[kebabKey];
return {
windowMinutes: toNumber(config['minimum-release-age']) ?? undefined,
excludes: parseExcludeValue(config['minimum-release-age-exclude']),
strictExplicit: toBoolean(config['minimum-release-age-strict']),
ignoreMissingTimeExplicit: toBoolean(config['minimum-release-age-ignore-missing-time']),
windowMinutes: toNumber(read('minimumReleaseAge', 'minimum-release-age')) ?? undefined,
excludes: parseExcludeValue(read('minimumReleaseAgeExclude', 'minimum-release-age-exclude')),
strictExplicit: toBoolean(read('minimumReleaseAgeStrict', 'minimum-release-age-strict')),
ignoreMissingTimeExplicit: toBoolean(read('minimumReleaseAgeIgnoreMissingTime', 'minimum-release-age-ignore-missing-time')),
};

@@ -102,0 +104,0 @@ }

@@ -228,15 +228,19 @@ "use strict";

const preInstallCommand = pmCommands.preInstall;
// Omit peer dependencies from the temp install. `ensurePackage` puts the
// Keep peer dependencies out of the temp install. `ensurePackage` puts the
// workspace's `node_modules` on `NODE_PATH`, so a loaded package resolves its
// peers from the workspace instead of pulling its own (possibly incompatible)
// copies into the temp dir.
const omitPeerDependenciesFlag = packageManager === 'npm' || packageManager === 'bun'
? '--omit=peer'
: packageManager === 'pnpm'
? '--config.auto-install-peers=false'
: '';
//
// npm needs `--legacy-peer-deps` rather than `--omit=peer`: npm marks a package
// as a peer if anything in the tree peer-depends on it, so `--omit=peer` also
// prunes packages that are real dependencies. Bun's `--omit=peer` does not.
const skipPeerDependenciesFlags = {
npm: '--legacy-peer-deps',
bun: '--omit=peer',
pnpm: '--config.auto-install-peers=false',
};
const installCommand = [
pmCommands.addDev,
`${pkg}@${requiredVersion}`,
omitPeerDependenciesFlag,
skipPeerDependenciesFlags[packageManager],
pmCommands.ignoreScriptsFlag,

@@ -243,0 +247,0 @@ ]

@@ -124,3 +124,5 @@ export type PackageManager = 'yarn' | 'pnpm' | 'npm' | 'bun';

}): Promise<string>;
export declare function packageRegistryPack(cwd: string, pkg: string, version: string): Promise<{
export declare function packageRegistryPack(cwd: string, pkg: string, version: string, options?: {
bypassMinReleaseAge?: boolean;
}): Promise<{
tarballPath: string;

@@ -127,0 +129,0 @@ }>;

@@ -168,4 +168,8 @@ "use strict";

updateLockFile: 'pnpm install --lockfile-only',
add: isPnpmWorkspace ? 'pnpm add -w' : 'pnpm add',
addDev: isPnpmWorkspace ? 'pnpm add -Dw' : 'pnpm add -D',
add: isPnpmWorkspace
? 'pnpm add -w --config.frozen-lockfile=false'
: 'pnpm add --config.frozen-lockfile=false',
addDev: isPnpmWorkspace
? 'pnpm add -Dw --config.frozen-lockfile=false'
: 'pnpm add -D --config.frozen-lockfile=false',
rm: 'pnpm rm',

@@ -349,2 +353,16 @@ exec: modernPnpm ? 'pnpm exec' : 'pnpx',

doc.delete('patchedDependencies');
// link:/file: overrides (e.g. written by `pnpm link`) point at paths that
// don't exist in the temp dir, and an override would hijack an exact-version
// add (`pnpm add pkg@x.y.z` would install the linked dir instead).
const overrides = doc.toJS()?.overrides;
if (overrides && typeof overrides === 'object') {
for (const [name, spec] of Object.entries(overrides)) {
if (typeof spec === 'string' && /^(link|file):/.test(spec)) {
doc.deleteIn(['overrides', name]);
}
}
if (Object.keys(doc.toJS()?.overrides ?? {}).length === 0) {
doc.delete('overrides');
}
}
return doc.toString();

@@ -520,3 +538,3 @@ }

}
async function packageRegistryPack(cwd, pkg, version) {
async function packageRegistryPack(cwd, pkg, version, options) {
/**

@@ -535,3 +553,9 @@ * Only `npm pack` supports downloading a tarball of a specified remote

// download working in workspaces that pin a non-npm manager (onFail: error).
env: { ...process.env, npm_config_force: 'true' },
env: {
...process.env,
npm_config_force: 'true',
...(options?.bypassMinReleaseAge
? { npm_config_min_release_age: '0' }
: {}),
},
});

@@ -538,0 +562,0 @@ const tarballPath = stdout.trim();

@@ -25,4 +25,14 @@ "use strict";

const result = await (0, package_manager_1.packageRegistryView)(packageName, packageVersion, '--json --silent');
const npmViewResult = JSON.parse(result);
const attURL = npmViewResult.dist?.attestations?.url;
const parsed = JSON.parse(result);
// `npm view <pkg>@<spec> --json` returns a bare object on npm <= 11 but an
// array on npm 12 and pnpm, even for a single resolved version. A version
// range matches several versions and the registry lists all of them
// (including deprecated ones the installer skips), so we cannot tell which
// one will actually be installed; refuse rather than verify the wrong
// artifact.
if (Array.isArray(parsed) && parsed.length > 1) {
throw new ProvenanceError(packageName, packageVersion, 'Provenance can only be verified for a single version, but this version resolved to multiple candidates. Specify an exact version.');
}
const npmViewResult = Array.isArray(parsed) ? parsed[0] : parsed;
const attURL = npmViewResult?.dist?.attestations?.url;
if (!attURL)

@@ -29,0 +39,0 @@ throw new ProvenanceError(packageName, packageVersion, 'No attestation URL found');

@@ -163,4 +163,10 @@ {

"description": "Adds .claude/settings.local.json to .gitignore",
"implementation": "./dist/src/migrations/update-17-3-0/update-nxw"
"implementation": "./dist/src/migrations/update-22-6-0/add-claude-settings-local-to-git-ignore"
},
"22-6-0-enable-analytics-prompt": {
"cli": "nx",
"version": "22.6.0-beta.11",
"description": "Prompts to enable usage analytics",
"implementation": "./dist/src/migrations/update-22-6-0/enable-analytics-prompt"
},
"22-7-0-add-self-healing-to-gitignore": {

@@ -167,0 +173,0 @@ "cli": "nx",

{
"name": "nx",
"version": "23.1.0",
"version": "23.1.1",
"private": false,

@@ -64,8 +64,9 @@ "type": "commonjs",

"asynckit": "0.4.0",
"axios": "1.16.1",
"axios": "1.18.1",
"balanced-match": "4.0.3",
"base64-js": "1.5.1",
"bl": "4.1.0",
"brace-expansion": "5.0.6",
"brace-expansion": "5.0.8",
"buffer": "5.7.1",
"bundle-name": "4.1.0",
"call-bind-apply-helpers": "1.0.2",

@@ -81,4 +82,6 @@ "chalk": "4.1.2",

"debug": "4.4.3",
"default-browser": "5.2.1",
"default-browser-id": "5.0.0",
"defaults": "1.0.4",
"define-lazy-prop": "2.0.0",
"define-lazy-prop": "3.0.0",
"delayed-stream": "1.0.0",

@@ -116,7 +119,8 @@ "dotenv": "16.4.7",

"inherits": "2.0.4",
"is-docker": "2.2.1",
"is-docker": "3.0.0",
"is-fullwidth-code-point": "3.0.0",
"is-inside-container": "1.0.0",
"is-interactive": "1.0.0",
"is-unicode-supported": "0.1.0",
"is-wsl": "2.2.0",
"is-wsl": "3.1.0",
"isexe": "2.0.0",

@@ -137,3 +141,3 @@ "json5": "2.2.3",

"onetime": "5.1.2",
"open": "8.4.2",
"open": "10.1.0",
"ora": "5.4.1",

@@ -147,2 +151,3 @@ "path-key": "3.1.1",

"restore-cursor": "3.1.0",
"run-applescript": "7.0.0",
"safe-buffer": "5.2.1",

@@ -184,12 +189,12 @@ "semver": "7.8.4",

"optionalDependencies": {
"@nx/nx-darwin-arm64": "23.1.0",
"@nx/nx-darwin-x64": "23.1.0",
"@nx/nx-freebsd-x64": "23.1.0",
"@nx/nx-linux-arm-gnueabihf": "23.1.0",
"@nx/nx-linux-arm64-gnu": "23.1.0",
"@nx/nx-linux-arm64-musl": "23.1.0",
"@nx/nx-linux-x64-gnu": "23.1.0",
"@nx/nx-linux-x64-musl": "23.1.0",
"@nx/nx-win32-arm64-msvc": "23.1.0",
"@nx/nx-win32-x64-msvc": "23.1.0"
"@nx/nx-darwin-arm64": "23.1.1",
"@nx/nx-darwin-x64": "23.1.1",
"@nx/nx-freebsd-x64": "23.1.1",
"@nx/nx-linux-arm-gnueabihf": "23.1.1",
"@nx/nx-linux-arm64-gnu": "23.1.1",
"@nx/nx-linux-arm64-musl": "23.1.1",
"@nx/nx-linux-x64-gnu": "23.1.1",
"@nx/nx-linux-x64-musl": "23.1.1",
"@nx/nx-win32-arm64-msvc": "23.1.1",
"@nx/nx-win32-x64-msvc": "23.1.1"
},

@@ -196,0 +201,0 @@ "nx-migrations": {

import { Tree } from '../../generators/tree';
export default function nxReleasePath(tree: Tree): void;
export declare function visitNotIgnoredFiles(tree: Tree, dirPath: string, visitor: (path: string) => void): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = nxReleasePath;
exports.visitNotIgnoredFiles = visitNotIgnoredFiles;
const node_path_1 = require("node:path");
const ignore_1 = require("../../utils/ignore");
function nxReleasePath(tree) {
visitNotIgnoredFiles(tree, '', (file) => {
const contents = tree.read(file).toString('utf-8');
if (
// the deep import usage should be replaced by the new location
contents.includes('nx/src/command-line/release') ||
// changelog-renderer has moved into nx/release
contents.includes('nx/changelog-renderer')) {
const finalContents = contents
// replace instances of old changelog renderer location
.replace(/nx\/changelog-renderer/g, 'nx/release/changelog-renderer')
// replace instances of deep import for programmatic API (only perform the replacement if an actual import by checking for trailing ' or ")
.replace(/nx\/src\/command-line\/release(['"])/g, 'nx/release$1');
tree.write(file, finalContents);
}
});
}
// Adapted from devkit
function visitNotIgnoredFiles(tree, dirPath = tree.root, visitor) {
const ig = (0, ignore_1.getIgnoreObject)();
dirPath = normalizePathRelativeToRoot(dirPath, tree.root);
if (dirPath !== '' && ig?.ignores(dirPath)) {
return;
}
for (const child of tree.children(dirPath)) {
const fullPath = (0, node_path_1.join)(dirPath, child);
if (ig?.ignores(fullPath)) {
continue;
}
if (tree.isFile(fullPath)) {
visitor(fullPath);
}
else {
visitNotIgnoredFiles(tree, fullPath, visitor);
}
}
}
// Copied from devkit
function normalizePathRelativeToRoot(path, root) {
return (0, node_path_1.relative)(root, (0, node_path_1.join)(root, path)).split(node_path_1.sep).join('/');
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display