nx 23.3.0 → 24.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -6
- package/dist/src/adapter/ngcli-adapter.js +1 -0
- package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
- package/dist/src/command-line/graph/graph.js +3 -5
- package/dist/src/command-line/init/command-object.js +0 -12
- package/dist/src/command-line/init/init-v2.js +0 -2
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
- package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
- package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
- package/dist/src/command-line/run/executor-utils.js +1 -0
- package/dist/src/command-line/run/run-one.d.ts +20 -1
- package/dist/src/command-line/run/run-one.js +153 -5
- package/dist/src/command-line/show/show-target/utils.js +1 -0
- package/dist/src/command-line/watch/command-object.js +3 -22
- package/dist/src/command-line/watch/watch.d.ts +0 -7
- package/dist/src/command-line/watch/watch.js +1 -1
- package/dist/src/config/misc-interfaces.d.ts +12 -0
- package/dist/src/config/workspace-json-project-json.d.ts +5 -0
- package/dist/src/daemon/client/client.d.ts +9 -0
- package/dist/src/daemon/client/client.js +89 -37
- package/dist/src/daemon/server/shutdown-utils.js +0 -3
- package/dist/src/daemon/socket-utils.d.ts +9 -4
- package/dist/src/daemon/socket-utils.js +12 -49
- package/dist/src/daemon/tmp-dir.d.ts +19 -14
- package/dist/src/daemon/tmp-dir.js +142 -354
- package/dist/src/devkit-internals.d.ts +1 -1
- package/dist/src/devkit-internals.js +1 -0
- package/dist/src/hasher/file-hasher.js +9 -7
- package/dist/src/hasher/hash-task.js +4 -0
- package/dist/src/hasher/native-task-hasher-impl.js +1 -1
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
- package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
- package/dist/src/native/index.d.ts +127 -8
- package/dist/src/native/native-bindings.js +5 -0
- package/dist/src/native/nx.wasi-browser.js +1 -0
- package/dist/src/native/nx.wasi.cjs +1 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/nx-cloud/update-manager.d.ts +7 -0
- package/dist/src/nx-cloud/update-manager.js +237 -50
- package/dist/src/project-graph/affected/affected-tasks.js +1 -0
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
- package/dist/src/tasks-runner/create-task-graph.d.ts +10 -0
- package/dist/src/tasks-runner/create-task-graph.js +37 -2
- package/dist/src/tasks-runner/task-env.d.ts +10 -0
- package/dist/src/tasks-runner/task-env.js +26 -5
- package/dist/src/tasks-runner/task-orchestrator.d.ts +10 -4
- package/dist/src/tasks-runner/task-orchestrator.js +87 -53
- package/dist/src/tasks-runner/utils.d.ts +20 -5
- package/dist/src/tasks-runner/utils.js +80 -29
- package/dist/src/ultracache/overrides.js +4 -0
- package/dist/src/utils/owned-private-dir.d.ts +1 -17
- package/dist/src/utils/owned-private-dir.js +10 -62
- package/dist/src/utils/perf-hooks.d.ts +1 -0
- package/dist/src/utils/plugins/core-plugins.js +1 -1
- package/dist/src/utils/string-similarity.d.ts +38 -0
- package/dist/src/utils/string-similarity.js +108 -0
- package/migrations.json +7 -0
- package/package.json +11 -20
- package/readme-template.md +3 -3
- package/schemas/nx-schema.json +5 -0
- package/schemas/project-schema.json +5 -0
- package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
- package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
|
@@ -78,11 +78,12 @@ class TaskOrchestrator {
|
|
|
78
78
|
? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
|
|
79
79
|
: null;
|
|
80
80
|
this.taskInvocationTracker = !native_1.IS_WASM
|
|
81
|
-
? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(),
|
|
81
|
+
? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), (0, task_env_1.getInvocationRootPid)(), (0, task_env_1.getInvocationAncestorPids)())
|
|
82
82
|
: null;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
83
|
+
// Task ids this process has already registered. Recursive code paths (e.g.
|
|
84
|
+
// applyFromCacheOrRunBatch looping on incomplete batches) re-enter the
|
|
85
|
+
// detector for the same task; skipping them saves a DB round trip. A task id
|
|
86
|
+
// is unique within one process, so it is enough to identify the row.
|
|
86
87
|
this.registeredInvocations = new Set();
|
|
87
88
|
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
|
|
88
89
|
// region internal state
|
|
@@ -316,39 +317,59 @@ class TaskOrchestrator {
|
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
/**
|
|
319
|
-
* Registers a task invocation and checks for loops across nested Nx
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
320
|
+
* Registers a task invocation and checks for loops across nested Nx
|
|
321
|
+
* processes. registerTask() returns the invocation chain only when an
|
|
322
|
+
* *ancestor* Nx process is already running this task — a genuine loop.
|
|
323
|
+
* Sibling processes running the same task are legitimate and register
|
|
324
|
+
* without complaint.
|
|
325
|
+
*
|
|
326
|
+
* The overrides hash rides along with the task id so the tracker can tell an
|
|
327
|
+
* ancestor re-invoking a task with different arguments from one repeating
|
|
328
|
+
* the same invocation.
|
|
323
329
|
*/
|
|
324
330
|
detectTaskInvocationLoop(task) {
|
|
325
331
|
if (!this.taskInvocationTracker)
|
|
326
332
|
return;
|
|
327
333
|
if (this.registeredInvocations.has(task.id))
|
|
328
334
|
return;
|
|
335
|
+
let chain;
|
|
329
336
|
try {
|
|
330
|
-
this.taskInvocationTracker.registerTask(process.pid, task.id);
|
|
337
|
+
chain = this.taskInvocationTracker.registerTask(process.pid, task.id, (0, utils_1.hashTaskOverrides)(task));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Loop detection is diagnostic only; a DB failure must not fail the run
|
|
341
|
+
// or be mistaken for a loop.
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!chain) {
|
|
331
345
|
this.registeredInvocations.add(task.id);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
|
|
349
|
+
output_1.output.error({
|
|
350
|
+
title: 'Recursive task invocation detected',
|
|
351
|
+
bodyLines: [
|
|
352
|
+
`Nx detected a recursive loop of task invocations:`,
|
|
353
|
+
``,
|
|
354
|
+
` ${chainDisplay} -> ${task.id}`,
|
|
355
|
+
``,
|
|
356
|
+
`Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
|
|
357
|
+
`This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
|
|
358
|
+
`triggers a chain of tasks that eventually re-invokes itself.`,
|
|
359
|
+
``,
|
|
360
|
+
`To fix this, review the command configuration for the tasks in the chain above.`,
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
releaseTaskInvocation(task) {
|
|
366
|
+
if (!this.registeredInvocations.delete(task.id))
|
|
367
|
+
return;
|
|
368
|
+
try {
|
|
369
|
+
this.taskInvocationTracker?.unregisterTask(process.pid, task.id);
|
|
332
370
|
}
|
|
333
371
|
catch {
|
|
334
|
-
//
|
|
335
|
-
const chain = this.taskInvocationTracker.getInvocationChain();
|
|
336
|
-
const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
|
|
337
|
-
output_1.output.error({
|
|
338
|
-
title: 'Recursive task invocation detected',
|
|
339
|
-
bodyLines: [
|
|
340
|
-
`Nx detected a recursive loop of task invocations:`,
|
|
341
|
-
``,
|
|
342
|
-
` ${chainDisplay} -> ${task.id}`,
|
|
343
|
-
``,
|
|
344
|
-
`Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
|
|
345
|
-
`This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
|
|
346
|
-
`triggers a chain of tasks that eventually re-invokes itself.`,
|
|
347
|
-
``,
|
|
348
|
-
`To fix this, review the command configuration for the tasks in the chain above.`,
|
|
349
|
-
],
|
|
350
|
-
});
|
|
351
|
-
process.exit(1);
|
|
372
|
+
// Diagnostic only, like registration. A leftover row is swept as stale.
|
|
352
373
|
}
|
|
353
374
|
}
|
|
354
375
|
// endregion Processing Scheduled Tasks
|
|
@@ -1186,31 +1207,45 @@ class TaskOrchestrator {
|
|
|
1186
1207
|
await this.scheduleNextTasksAndReleaseThreads();
|
|
1187
1208
|
return runningTask;
|
|
1188
1209
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
//
|
|
1193
|
-
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
1194
|
-
// Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
|
|
1195
|
-
// `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
|
|
1196
|
-
// only reports at `endCommand`, which never runs for a task that does not
|
|
1197
|
-
// end - so suppressing here would leave a `nx serve` under `summary` with a
|
|
1198
|
-
// permanently silent terminal and no file to read yet. Streaming is the only
|
|
1199
|
-
// channel a continuous task has.
|
|
1200
|
-
const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
|
|
1201
|
-
? false
|
|
1202
|
-
: (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
|
|
1203
|
-
let env = pipeOutput
|
|
1204
|
-
? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
|
|
1205
|
-
: (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
|
|
1206
|
-
this.detectTaskInvocationLoop(task);
|
|
1207
|
-
const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
|
|
1210
|
+
// Claim the task before doing any of the work below. getRunningTasks()
|
|
1211
|
+
// above is a check-then-act: until this row exists, a sibling Nx process
|
|
1212
|
+
// asking the same question is told the task is not running and starts a
|
|
1213
|
+
// duplicate of it.
|
|
1208
1214
|
this.runningTasksService?.addRunningTask(task.id);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1215
|
+
let childProcess;
|
|
1216
|
+
try {
|
|
1217
|
+
const taskSpecificEnv = await this.processedTasks.get(task.id);
|
|
1218
|
+
await this.preRunSteps([task], { groupId });
|
|
1219
|
+
const pipeOutput = await this.pipeOutputCapture(task);
|
|
1220
|
+
// obtain metadata
|
|
1221
|
+
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
1222
|
+
// Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
|
|
1223
|
+
// `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
|
|
1224
|
+
// only reports at `endCommand`, which never runs for a task that does not
|
|
1225
|
+
// end - so suppressing here would leave a `nx serve` under `summary` with a
|
|
1226
|
+
// permanently silent terminal and no file to read yet. Streaming is the only
|
|
1227
|
+
// channel a continuous task has.
|
|
1228
|
+
const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
|
|
1229
|
+
? false
|
|
1230
|
+
: (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
|
|
1231
|
+
let env = pipeOutput
|
|
1232
|
+
? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
|
|
1233
|
+
: (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
|
|
1234
|
+
this.detectTaskInvocationLoop(task);
|
|
1235
|
+
childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
|
|
1236
|
+
this.runningContinuousTasks.set(task.id, {
|
|
1237
|
+
runningTask: childProcess,
|
|
1238
|
+
groupId,
|
|
1239
|
+
ownsRunningTasksService: true,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
catch (e) {
|
|
1243
|
+
// Nothing owns the claim until runningContinuousTasks holds it — the
|
|
1244
|
+
// release paths (completeContinuousTask, the signal handler) both iterate
|
|
1245
|
+
// that map. Drop it here or siblings wait on a task no one is running.
|
|
1246
|
+
this.runningTasksService?.removeRunningTask(task.id);
|
|
1247
|
+
throw e;
|
|
1248
|
+
}
|
|
1214
1249
|
this.continuousTaskExitHandled.set(task.id, new Promise((resolve) => {
|
|
1215
1250
|
childProcess.onExit(async (code) => {
|
|
1216
1251
|
await this.handleContinuousTaskExit(code, task, groupId, true);
|
|
@@ -1425,8 +1460,7 @@ class TaskOrchestrator {
|
|
|
1425
1460
|
if (this.completedTasks.has(task.id))
|
|
1426
1461
|
continue;
|
|
1427
1462
|
this.completedTasks.set(task.id, status);
|
|
1428
|
-
this.
|
|
1429
|
-
this.registeredInvocations.delete(task.id);
|
|
1463
|
+
this.releaseTaskInvocation(task);
|
|
1430
1464
|
if (this.tuiEnabled) {
|
|
1431
1465
|
this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
|
|
1432
1466
|
}
|
|
@@ -2,7 +2,6 @@ import { CustomHasher, ExecutorConfig } from '../config/misc-interfaces';
|
|
|
2
2
|
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
|
|
3
3
|
import { Task, TaskGraph } from '../config/task-graph';
|
|
4
4
|
import { ProjectConfiguration, TargetConfiguration, TargetDependencyConfig } from '../config/workspace-json-project-json';
|
|
5
|
-
import { LegacyDependsOnLocation } from './legacy-depends-on-warning';
|
|
6
5
|
export type NormalizedTargetDependencyConfig = TargetDependencyConfig & {
|
|
7
6
|
projects: string[];
|
|
8
7
|
};
|
|
@@ -10,9 +9,8 @@ export declare function getDependencyConfigs({ project, target }: {
|
|
|
10
9
|
project: string;
|
|
11
10
|
target: string;
|
|
12
11
|
}, extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>, projectGraph: ProjectGraph, allTargetNames: string[]): NormalizedTargetDependencyConfig[] | undefined;
|
|
13
|
-
export
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function normalizeDependencyConfigProjects(dependencyConfig: TargetDependencyConfig, currentProject: string, graph: ProjectGraph, location?: DependsOnEntryLocation): NormalizedTargetDependencyConfig;
|
|
12
|
+
export declare function normalizeDependencyConfigDefinition(definition: string | TargetDependencyConfig, currentProject: string, graph: ProjectGraph, allTargetNames: string[]): NormalizedTargetDependencyConfig[];
|
|
13
|
+
export declare function normalizeDependencyConfigProjects(dependencyConfig: TargetDependencyConfig, currentProject: string, graph: ProjectGraph): NormalizedTargetDependencyConfig;
|
|
16
14
|
export declare function expandDependencyConfigSyntaxSugar(dependencyConfigString: string | TargetDependencyConfig, graph: ProjectGraph, currentProject?: string): TargetDependencyConfig;
|
|
17
15
|
export declare function expandWildcardTargetConfiguration(dependencyConfig: NormalizedTargetDependencyConfig, allTargetNames: string[]): NormalizedTargetDependencyConfig[];
|
|
18
16
|
export declare function readProjectAndTargetFromTargetString(targetString: string, projects: Record<string, ProjectGraphProjectNode>, currentProject?: string): {
|
|
@@ -20,7 +18,7 @@ export declare function readProjectAndTargetFromTargetString(targetString: strin
|
|
|
20
18
|
target: string;
|
|
21
19
|
};
|
|
22
20
|
export declare function getOutputs(p: Record<string, ProjectGraphProjectNode>, target: Task['target'], overrides: Task['overrides']): string[];
|
|
23
|
-
export declare function normalizeTargetDependencyWithStringProjects(dependencyConfig: TargetDependencyConfig
|
|
21
|
+
export declare function normalizeTargetDependencyWithStringProjects(dependencyConfig: TargetDependencyConfig): Omit<TargetDependencyConfig, 'projects'> & {
|
|
24
22
|
projects?: string[];
|
|
25
23
|
};
|
|
26
24
|
export declare function validateOutputs(outputs: string[]): void;
|
|
@@ -49,6 +47,8 @@ export declare function getExecutorForTask(task: Task, projects: Record<string,
|
|
|
49
47
|
isNxExecutor: boolean;
|
|
50
48
|
};
|
|
51
49
|
export declare function getCustomHasher(task: Task, projects: Record<string, ProjectConfiguration>): CustomHasher | null;
|
|
50
|
+
/** Test-only: forget which executors have been warned about. */
|
|
51
|
+
export declare function resetCustomHasherDeprecationWarnings(): void;
|
|
52
52
|
/**
|
|
53
53
|
* `taskGraph` pruned to `required`, which must already hold its dependencies.
|
|
54
54
|
* Unknown ids are ignored: a sync generator can remove a task after selection.
|
|
@@ -64,3 +64,18 @@ export declare function shouldStreamOutput(task: Task, initiatingProject: string
|
|
|
64
64
|
export declare function isCacheableTask(task: Task): boolean;
|
|
65
65
|
export declare function unparse(options: Object): string[];
|
|
66
66
|
export declare function createTaskId(project: string, target: string, configuration: string | undefined): string;
|
|
67
|
+
/**
|
|
68
|
+
* Distinguishes two invocations of the same task for the recursive invocation
|
|
69
|
+
* detector. The task id alone would report `nx run app:build --flag` invoked
|
|
70
|
+
* from within `app:build` as a loop, so the detector pairs the id with this
|
|
71
|
+
* hash and only treats an exact repeat as an immediate loop.
|
|
72
|
+
*
|
|
73
|
+
* The hash cannot decide recursion on its own. Arguments that accumulate
|
|
74
|
+
* through forwarding (`app:build` -> `app:build hello` -> `app:build hello
|
|
75
|
+
* hello`) hash differently on every hop, so the tracker also bounds how many
|
|
76
|
+
* ancestors may be running one task id.
|
|
77
|
+
*
|
|
78
|
+
* `__overrides_unparsed__` is dropped, matching how the task hasher treats it:
|
|
79
|
+
* it restates the other keys as raw argv, down to flag order and spelling.
|
|
80
|
+
*/
|
|
81
|
+
export declare function hashTaskOverrides(task: Task): string;
|
|
@@ -17,6 +17,7 @@ exports.getExecutorNameForTask = getExecutorNameForTask;
|
|
|
17
17
|
exports.expandInitiatingTasksThroughNoop = expandInitiatingTasksThroughNoop;
|
|
18
18
|
exports.getExecutorForTask = getExecutorForTask;
|
|
19
19
|
exports.getCustomHasher = getCustomHasher;
|
|
20
|
+
exports.resetCustomHasherDeprecationWarnings = resetCustomHasherDeprecationWarnings;
|
|
20
21
|
exports.pruneToSelectedTasks = pruneToSelectedTasks;
|
|
21
22
|
exports.removeTasksFromTaskGraph = removeTasksFromTaskGraph;
|
|
22
23
|
exports.calculateReverseDeps = calculateReverseDeps;
|
|
@@ -28,37 +29,35 @@ exports.shouldStreamOutput = shouldStreamOutput;
|
|
|
28
29
|
exports.isCacheableTask = isCacheableTask;
|
|
29
30
|
exports.unparse = unparse;
|
|
30
31
|
exports.createTaskId = createTaskId;
|
|
32
|
+
exports.hashTaskOverrides = hashTaskOverrides;
|
|
31
33
|
const minimatch_1 = require("minimatch");
|
|
32
34
|
const node_path_1 = require("node:path");
|
|
33
35
|
const posix_1 = require("node:path/posix");
|
|
34
36
|
const executor_utils_1 = require("../command-line/run/executor-utils");
|
|
37
|
+
const file_hasher_1 = require("../hasher/file-hasher");
|
|
35
38
|
const native_1 = require("../native");
|
|
36
39
|
const fileutils_1 = require("../utils/fileutils");
|
|
37
40
|
const find_matching_projects_1 = require("../utils/find-matching-projects");
|
|
38
|
-
const legacy_depends_on_warning_1 = require("./legacy-depends-on-warning");
|
|
39
41
|
const globs_1 = require("../utils/globs");
|
|
40
42
|
const long_running_target_1 = require("../utils/long-running-target");
|
|
43
|
+
const output_1 = require("../utils/output");
|
|
41
44
|
const path_1 = require("../utils/path");
|
|
42
45
|
const serialize_overrides_into_command_line_1 = require("../utils/serialize-overrides-into-command-line");
|
|
43
46
|
const split_target_1 = require("../utils/split-target");
|
|
44
47
|
const workspace_root_1 = require("../utils/workspace-root");
|
|
45
48
|
const is_tui_enabled_1 = require("./is-tui-enabled");
|
|
46
49
|
function getDependencyConfigs({ project, target }, extraTargetDependencies, projectGraph, allTargetNames) {
|
|
47
|
-
|
|
48
|
-
const dependencyConfigs = (projectGraph.nodes[project].data?.targets[target]?.dependsOn ??
|
|
50
|
+
return (projectGraph.nodes[project].data?.targets[target]?.dependsOn ??
|
|
49
51
|
// This is passed into `run-command` from programmatic invocations
|
|
50
52
|
extraTargetDependencies[target] ??
|
|
51
|
-
[]).flatMap((config
|
|
52
|
-
if (legacyViolations.length) {
|
|
53
|
-
(0, legacy_depends_on_warning_1.flushLegacyDependsOnViolations)(project, target, legacyViolations, projectGraph.nodes[project]?.data?.root);
|
|
54
|
-
}
|
|
55
|
-
return dependencyConfigs;
|
|
53
|
+
[]).flatMap((config) => normalizeDependencyConfigDefinition(config, project, projectGraph, allTargetNames));
|
|
56
54
|
}
|
|
57
|
-
function normalizeDependencyConfigDefinition(definition, currentProject, graph, allTargetNames
|
|
58
|
-
return expandWildcardTargetConfiguration(normalizeDependencyConfigProjects(expandDependencyConfigSyntaxSugar(definition, graph, currentProject), currentProject, graph
|
|
55
|
+
function normalizeDependencyConfigDefinition(definition, currentProject, graph, allTargetNames) {
|
|
56
|
+
return expandWildcardTargetConfiguration(normalizeDependencyConfigProjects(expandDependencyConfigSyntaxSugar(definition, graph, currentProject), currentProject, graph), allTargetNames);
|
|
59
57
|
}
|
|
60
|
-
function normalizeDependencyConfigProjects(dependencyConfig, currentProject, graph
|
|
61
|
-
|
|
58
|
+
function normalizeDependencyConfigProjects(dependencyConfig, currentProject, graph) {
|
|
59
|
+
assertNotLegacyProjectsString(dependencyConfig, currentProject, graph);
|
|
60
|
+
const noStringConfig = normalizeTargetDependencyWithStringProjects(dependencyConfig);
|
|
62
61
|
if (noStringConfig.projects) {
|
|
63
62
|
dependencyConfig.projects = (0, find_matching_projects_1.findMatchingProjects)(noStringConfig.projects, graph.nodes);
|
|
64
63
|
}
|
|
@@ -67,6 +66,13 @@ function normalizeDependencyConfigProjects(dependencyConfig, currentProject, gra
|
|
|
67
66
|
}
|
|
68
67
|
return dependencyConfig;
|
|
69
68
|
}
|
|
69
|
+
function assertNotLegacyProjectsString(dependencyConfig, currentProject, graph) {
|
|
70
|
+
const projects = dependencyConfig.projects;
|
|
71
|
+
if ((projects === 'self' || projects === 'dependencies') &&
|
|
72
|
+
!graph.nodes[projects]) {
|
|
73
|
+
throw new Error(`${currentProject} has a dependsOn entry using projects: '${projects}', which is no longer supported: ${JSON.stringify(dependencyConfig)}\nPlease run "nx repair" to repair your configuration`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
70
76
|
function expandDependencyConfigSyntaxSugar(dependencyConfigString, graph, currentProject) {
|
|
71
77
|
if (typeof dependencyConfigString !== 'string') {
|
|
72
78
|
return dependencyConfigString;
|
|
@@ -133,23 +139,9 @@ function readProjectAndTargetFromTargetString(targetString, projects, currentPro
|
|
|
133
139
|
function getOutputs(p, target, overrides) {
|
|
134
140
|
return getOutputsForTargetAndConfiguration(target, overrides, p[target.project]);
|
|
135
141
|
}
|
|
136
|
-
function normalizeTargetDependencyWithStringProjects(dependencyConfig
|
|
142
|
+
function normalizeTargetDependencyWithStringProjects(dependencyConfig) {
|
|
137
143
|
if (typeof dependencyConfig.projects === 'string') {
|
|
138
|
-
|
|
139
|
-
// The v16 `update-depends-on-to-tokens` migration already rewrites
|
|
140
|
-
// these to the modern shape, and `nx repair` will re-run it on demand.
|
|
141
|
-
if (dependencyConfig.projects === 'self') {
|
|
142
|
-
(0, legacy_depends_on_warning_1.warnLegacyDependsOnMagicString)(currentProject, dependencyConfig, location);
|
|
143
|
-
delete dependencyConfig.projects;
|
|
144
|
-
}
|
|
145
|
-
else if (dependencyConfig.projects === 'dependencies') {
|
|
146
|
-
(0, legacy_depends_on_warning_1.warnLegacyDependsOnMagicString)(currentProject, dependencyConfig, location);
|
|
147
|
-
dependencyConfig.dependencies = true;
|
|
148
|
-
delete dependencyConfig.projects;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
dependencyConfig.projects = [dependencyConfig.projects];
|
|
152
|
-
}
|
|
144
|
+
dependencyConfig.projects = [dependencyConfig.projects];
|
|
153
145
|
}
|
|
154
146
|
return dependencyConfig;
|
|
155
147
|
}
|
|
@@ -341,9 +333,33 @@ function getExecutorForTask(task, projects) {
|
|
|
341
333
|
const [nodeModule, executorName] = (0, executor_utils_1.parseExecutor)(executor);
|
|
342
334
|
return (0, executor_utils_1.getExecutorInformation)(nodeModule, executorName, workspace_root_1.workspaceRoot, projects);
|
|
343
335
|
}
|
|
336
|
+
// TODO(v25): remove custom hasher support.
|
|
344
337
|
function getCustomHasher(task, projects) {
|
|
345
338
|
const factory = getExecutorForTask(task, projects).hasherFactory;
|
|
346
|
-
|
|
339
|
+
if (!factory) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
warnCustomHasherDeprecation(projects[task.target.project].targets[task.target.target].executor);
|
|
343
|
+
return factory();
|
|
344
|
+
}
|
|
345
|
+
const customHasherDeprecationWarned = new Set();
|
|
346
|
+
function warnCustomHasherDeprecation(executor) {
|
|
347
|
+
if (customHasherDeprecationWarned.has(executor)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
customHasherDeprecationWarned.add(executor);
|
|
351
|
+
output_1.output.warn({
|
|
352
|
+
title: `The "${executor}" executor uses a custom hasher, which is deprecated and will be removed in Nx 25.`,
|
|
353
|
+
bodyLines: [
|
|
354
|
+
'Replace the custom hasher with target inputs, following the migration guide:',
|
|
355
|
+
'https://nx.dev/docs/kb/local-executors#replace-a-custom-hasher-with-inputs',
|
|
356
|
+
'If the executor comes from a third-party plugin, report this to its maintainers.',
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** Test-only: forget which executors have been warned about. */
|
|
361
|
+
function resetCustomHasherDeprecationWarnings() {
|
|
362
|
+
customHasherDeprecationWarned.clear();
|
|
347
363
|
}
|
|
348
364
|
/**
|
|
349
365
|
* `taskGraph` pruned to `required`, which must already hold its dependencies.
|
|
@@ -355,13 +371,29 @@ function pruneToSelectedTasks(taskGraph, required) {
|
|
|
355
371
|
}
|
|
356
372
|
function removeTasksFromTaskGraph(graph, ids) {
|
|
357
373
|
const newGraph = removeIdsFromTaskGraph(graph, ids, graph.tasks);
|
|
374
|
+
const continuousDependenciesWithoutInputs = pruneEdges(graph.continuousDependenciesWithoutInputs, newGraph.mapWithIds);
|
|
358
375
|
return {
|
|
359
376
|
dependencies: newGraph.dependencies,
|
|
360
377
|
continuousDependencies: newGraph.continuousDependencies,
|
|
378
|
+
...(continuousDependenciesWithoutInputs
|
|
379
|
+
? { continuousDependenciesWithoutInputs }
|
|
380
|
+
: {}),
|
|
361
381
|
roots: newGraph.roots,
|
|
362
382
|
tasks: newGraph.mapWithIds,
|
|
363
383
|
};
|
|
364
384
|
}
|
|
385
|
+
function pruneEdges(edges, kept) {
|
|
386
|
+
let pruned;
|
|
387
|
+
for (const [id, deps] of Object.entries(edges ?? {})) {
|
|
388
|
+
if (!(id in kept))
|
|
389
|
+
continue;
|
|
390
|
+
const keptDeps = deps.filter((dep) => dep in kept);
|
|
391
|
+
if (keptDeps.length > 0) {
|
|
392
|
+
(pruned ??= {})[id] = keptDeps;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return pruned;
|
|
396
|
+
}
|
|
365
397
|
function removeIdsFromTaskGraph(graph, ids, mapWithIds) {
|
|
366
398
|
const filteredMapWithIds = {};
|
|
367
399
|
const dependencies = {};
|
|
@@ -457,3 +489,22 @@ function createTaskId(project, target, configuration) {
|
|
|
457
489
|
}
|
|
458
490
|
return id;
|
|
459
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Distinguishes two invocations of the same task for the recursive invocation
|
|
494
|
+
* detector. The task id alone would report `nx run app:build --flag` invoked
|
|
495
|
+
* from within `app:build` as a loop, so the detector pairs the id with this
|
|
496
|
+
* hash and only treats an exact repeat as an immediate loop.
|
|
497
|
+
*
|
|
498
|
+
* The hash cannot decide recursion on its own. Arguments that accumulate
|
|
499
|
+
* through forwarding (`app:build` -> `app:build hello` -> `app:build hello
|
|
500
|
+
* hello`) hash differently on every hop, so the tracker also bounds how many
|
|
501
|
+
* ancestors may be running one task id.
|
|
502
|
+
*
|
|
503
|
+
* `__overrides_unparsed__` is dropped, matching how the task hasher treats it:
|
|
504
|
+
* it restates the other keys as raw argv, down to flag order and spelling.
|
|
505
|
+
*/
|
|
506
|
+
function hashTaskOverrides(task) {
|
|
507
|
+
const overrides = { ...task.overrides };
|
|
508
|
+
delete overrides['__overrides_unparsed__'];
|
|
509
|
+
return (0, file_hasher_1.hashObject)(overrides);
|
|
510
|
+
}
|
|
@@ -19,6 +19,7 @@ function customHasherTaskIds(projectGraph, taskGraph) {
|
|
|
19
19
|
return memoized;
|
|
20
20
|
}
|
|
21
21
|
const projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(projectGraph).projects;
|
|
22
|
+
// TODO(v25): remove custom hasher support.
|
|
22
23
|
const ids = Object.values(taskGraph.tasks)
|
|
23
24
|
.filter((task) => {
|
|
24
25
|
try {
|
|
@@ -48,6 +49,9 @@ function getUltracacheSettings(taskGraph) {
|
|
|
48
49
|
function ultracacheEligibilityOptions(projectGraph, taskGraph) {
|
|
49
50
|
return {
|
|
50
51
|
customHasherTaskIds: customHasherTaskIds(projectGraph, taskGraph),
|
|
52
|
+
continuousTaskIds: Object.values(taskGraph.tasks)
|
|
53
|
+
.filter((task) => task.continuous)
|
|
54
|
+
.map((task) => task.id),
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
@@ -25,7 +25,7 @@ export type OwnedRealDir = string & {
|
|
|
25
25
|
export type OwnedPrivateDir = string & {
|
|
26
26
|
readonly [ownedPrivateDirBrand]: true;
|
|
27
27
|
};
|
|
28
|
-
/** Why a guard refused a directory. Data, not a sentence
|
|
28
|
+
/** Why a guard refused a directory. Data, not a sentence. */
|
|
29
29
|
export type DirRefusal = {
|
|
30
30
|
kind: 'not-created';
|
|
31
31
|
dir: string;
|
|
@@ -65,8 +65,6 @@ export type GuardResult<T> = {
|
|
|
65
65
|
};
|
|
66
66
|
/** The user-facing sentence for a refusal. The only place wording is decided. */
|
|
67
67
|
export declare function describeRefusal(r: DirRefusal): string;
|
|
68
|
-
/** What the user can do about a refusal, or `undefined` when there is nothing. */
|
|
69
|
-
export declare function remedyFor(r: DirRefusal): string | undefined;
|
|
70
68
|
/** One refusal as an `Error`, so several can travel in an `AggregateError`. */
|
|
71
69
|
export declare class DirectoryRefusedError extends Error {
|
|
72
70
|
readonly refusal: DirRefusal;
|
|
@@ -98,20 +96,6 @@ export declare function canonicalDir(dir: string): string;
|
|
|
98
96
|
* verified private directory beneath it.
|
|
99
97
|
*/
|
|
100
98
|
export declare function isSafeSharedRoot(dir: string): GuardResult<SafeSharedRoot>;
|
|
101
|
-
/**
|
|
102
|
-
* Whether other users on this machine can write into `dir`. Gates whether a
|
|
103
|
-
* refusal message may cite other users, so it must not over-report: `false` on
|
|
104
|
-
* Windows, where libuv synthesizes `st_mode` from the READONLY attribute and
|
|
105
|
-
* reports an ordinary per-account directory as `0666`, and `false` on a path
|
|
106
|
-
* that cannot be inspected.
|
|
107
|
-
*
|
|
108
|
-
* `statSync`, not `lstatSync`: the question is about the directory that will be
|
|
109
|
-
* used, not the link pointing at it. Link modes mislead in both directions —
|
|
110
|
-
* Linux creates symlinks `0777`, macOS applies the umask. Nothing here decides
|
|
111
|
-
* whether a path is accepted, so following the link costs nothing the guards
|
|
112
|
-
* do not already re-check.
|
|
113
|
-
*/
|
|
114
|
-
export declare function isPeerWritable(dir: string): boolean;
|
|
115
99
|
/**
|
|
116
100
|
* Create a shared container as sticky + world-writable if it does not exist,
|
|
117
101
|
* and report whether the resulting path is safe for the current user.
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DirectoryRefusedError = void 0;
|
|
4
4
|
exports.describeRefusal = describeRefusal;
|
|
5
|
-
exports.remedyFor = remedyFor;
|
|
6
5
|
exports.canonicalDir = canonicalDir;
|
|
7
6
|
exports.isSafeSharedRoot = isSafeSharedRoot;
|
|
8
|
-
exports.isPeerWritable = isPeerWritable;
|
|
9
7
|
exports.ensureSafeSharedRoot = ensureSafeSharedRoot;
|
|
10
8
|
exports.isOwnedRealDirectory = isOwnedRealDirectory;
|
|
11
9
|
exports.getUserSegment = getUserSegment;
|
|
12
10
|
exports.ensureOwnedPrivateDir = ensureOwnedPrivateDir;
|
|
11
|
+
/**
|
|
12
|
+
* Directory guards for Nx's runtime state: create a directory only when it is
|
|
13
|
+
* ours and unreachable by other users on this machine.
|
|
14
|
+
*
|
|
15
|
+
* `native/utils/owned_dir.rs` implements the same rules and must change with
|
|
16
|
+
* this file. This copy exists because the native binding loader has to place
|
|
17
|
+
* and lock down the `.node` before Rust can be called, and `cache-directory.ts`,
|
|
18
|
+
* `git-utils.ts` and `nx-tmp-dir.ts` share it. Node builtins only, for the
|
|
19
|
+
* loader's sake.
|
|
20
|
+
*/
|
|
13
21
|
const node_fs_1 = require("node:fs");
|
|
14
22
|
const node_os_1 = require("node:os");
|
|
15
23
|
const node_path_1 = require("node:path");
|
|
@@ -56,42 +64,6 @@ function describeRefusal(r) {
|
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
|
-
/** Single-quoted for a shell, so a path with a space or quote survives a paste. */
|
|
60
|
-
const shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
61
|
-
/** What the user can do about a refusal, or `undefined` when there is nothing. */
|
|
62
|
-
function remedyFor(r) {
|
|
63
|
-
switch (r.kind) {
|
|
64
|
-
case 'not-a-directory':
|
|
65
|
-
return r.symlink
|
|
66
|
-
? `${r.dir} is a symlink where Nx expects a directory. If you did not create it, treat it as hostile: remove the link itself (not what it points at) and run the command again.`
|
|
67
|
-
: undefined;
|
|
68
|
-
case 'foreign-owner':
|
|
69
|
-
return `${r.dir} belongs to another user on this machine, so Nx cannot keep its own directory there. Set NX_SOCKET_DIR to a short directory your user owns, or move it aside — which you can do yourself if you own the directory it sits in, and otherwise needs an administrator.`;
|
|
70
|
-
case 'not-tightenable':
|
|
71
|
-
// Both producers reach here having already established that the directory
|
|
72
|
-
// is ours, so `chmod` is the user's to run. Names no cause: one producer
|
|
73
|
-
// is a mount that discards the mode, the other is any `chmod` failure.
|
|
74
|
-
return `${r.dir} is reachable by other users (mode ${asMode(r.mode)}) and Nx could not restrict it. Run \`chmod 0700 ${shellQuote(r.dir)}\` and try again; if the mode does not stick, set NX_SOCKET_DIR to a short directory on a filesystem that keeps POSIX permissions.`;
|
|
75
|
-
case 'foreign-shared-container':
|
|
76
|
-
// Unreachable today: `isSafeSharedRoot` denies with this kind only when
|
|
77
|
-
// `stats.uid !== 0`, so a root-owned container never reaches here.
|
|
78
|
-
if (r.uid === 0) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
const q = shellQuote(r.dir);
|
|
82
|
-
return `${r.dir} belongs to another user on this machine, so Nx cannot keep a private directory beneath it. Ask an administrator to hand it to root with \`sudo chown root ${q} && sudo chmod 1777 ${q}\`; every user can then keep their own directory under it.`;
|
|
83
|
-
case 'not-created':
|
|
84
|
-
case 'not-inspectable':
|
|
85
|
-
case 'peer-writable-not-sticky':
|
|
86
|
-
return undefined;
|
|
87
|
-
default: {
|
|
88
|
-
// Matches `describeRefusal`'s arm, so a new kind cannot ship with a
|
|
89
|
-
// sentence and no advice — which is how `not-tightenable` went unadvised.
|
|
90
|
-
const unhandled = r;
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
67
|
/** One refusal as an `Error`, so several can travel in an `AggregateError`. */
|
|
96
68
|
class DirectoryRefusedError extends Error {
|
|
97
69
|
constructor(refusal) {
|
|
@@ -207,30 +179,6 @@ function isSafeSharedRoot(dir) {
|
|
|
207
179
|
return deny({ kind: 'not-inspectable', dir, code: e?.code });
|
|
208
180
|
}
|
|
209
181
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Whether other users on this machine can write into `dir`. Gates whether a
|
|
212
|
-
* refusal message may cite other users, so it must not over-report: `false` on
|
|
213
|
-
* Windows, where libuv synthesizes `st_mode` from the READONLY attribute and
|
|
214
|
-
* reports an ordinary per-account directory as `0666`, and `false` on a path
|
|
215
|
-
* that cannot be inspected.
|
|
216
|
-
*
|
|
217
|
-
* `statSync`, not `lstatSync`: the question is about the directory that will be
|
|
218
|
-
* used, not the link pointing at it. Link modes mislead in both directions —
|
|
219
|
-
* Linux creates symlinks `0777`, macOS applies the umask. Nothing here decides
|
|
220
|
-
* whether a path is accepted, so following the link costs nothing the guards
|
|
221
|
-
* do not already re-check.
|
|
222
|
-
*/
|
|
223
|
-
function isPeerWritable(dir) {
|
|
224
|
-
if (process.platform === 'win32') {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
return !!((0, node_fs_1.statSync)(dir).mode & 0o022);
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
182
|
/**
|
|
235
183
|
* Create a shared container as sticky + world-writable if it does not exist,
|
|
236
184
|
* and report whether the resulting path is safe for the current user.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the Levenshtein edit distance between two strings, i.e. the minimum
|
|
3
|
+
* number of single-character insertions, deletions, or substitutions required
|
|
4
|
+
* to turn `a` into `b`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function levenshteinDistance(a: string, b: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Ranks every candidate by how closely it resembles `input`, from closest to
|
|
9
|
+
* furthest, tie-broken alphabetically so the order is deterministic. Each entry
|
|
10
|
+
* keeps its computed distance so callers can reuse it (e.g. to both pick the
|
|
11
|
+
* closest match and order a list) without recomputing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rankByDistance(input: string, candidates: readonly string[]): {
|
|
14
|
+
candidate: string;
|
|
15
|
+
distance: number;
|
|
16
|
+
}[];
|
|
17
|
+
/**
|
|
18
|
+
* Whether a candidate at edit `distance` from `input` is close enough to be a
|
|
19
|
+
* useful suggestion. Exposed so callers that already have a distance (e.g. from
|
|
20
|
+
* `rankByDistance`) can gate on it without recomputing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isWithinSuggestionThreshold(input: string, candidate: string, distance: number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Ranks the candidates that are similar enough to `input` to be worth
|
|
25
|
+
* suggesting, closest first (ties broken alphabetically). Each entry keeps its
|
|
26
|
+
* distance so callers can compare suggestions found for different forms of the
|
|
27
|
+
* same input without recomputing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function rankSuggestions(input: string, candidates: readonly string[]): {
|
|
30
|
+
candidate: string;
|
|
31
|
+
distance: number;
|
|
32
|
+
}[];
|
|
33
|
+
/**
|
|
34
|
+
* Returns up to `limit` candidates that most closely resemble `input`, sorted
|
|
35
|
+
* from closest to furthest. Candidates that are not similar enough to be a
|
|
36
|
+
* useful suggestion are excluded.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findClosestMatches(input: string, candidates: readonly string[], limit?: number): string[];
|