@nx/js 23.1.0-pr.36127.8b9019b → 23.1.0-rc.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.
@@ -1,5 +1,7 @@
1
1
  import { type ExecutorContext } from '@nx/devkit';
2
+ import { type PackageJson } from 'nx/src/utils/package-json';
2
3
  import { type PruneLockfileOptions } from './schema';
3
4
  export default function pruneLockfileExecutor(schema: PruneLockfileOptions, context: ExecutorContext): Promise<{
4
5
  success: boolean;
5
6
  }>;
7
+ export declare function resolveCatalogReferences(packageJson: PackageJson): PackageJson;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = pruneLockfileExecutor;
4
+ exports.resolveCatalogReferences = resolveCatalogReferences;
4
5
  const devkit_1 = require("@nx/devkit");
6
+ const internal_1 = require("@nx/devkit/internal");
5
7
  const fs_1 = require("fs");
6
8
  const path_1 = require("path");
7
9
  const utils_1 = require("nx/src/tasks-runner/utils");
@@ -13,7 +15,8 @@ const strip_glob_to_base_dir_1 = require("../../utils/strip-glob-to-base-dir");
13
15
  async function pruneLockfileExecutor(schema, context) {
14
16
  devkit_1.logger.log('Pruning lockfile...');
15
17
  const outputDirectory = getOutputDir(schema, context);
16
- const packageJson = getPackageJson(schema, context);
18
+ const packageJson = resolveCatalogReferences(getPackageJson(schema, context));
19
+ mergeAllowScripts(packageJson);
17
20
  const packageManager = (0, devkit_1.detectPackageManager)(devkit_1.workspaceRoot);
18
21
  if (packageManager === 'bun') {
19
22
  devkit_1.logger.warn('Bun lockfile generation is not supported. Only package.json will be generated. Run "bun install" in the output directory if needed.');
@@ -48,6 +51,55 @@ function createPrunedLockfile(packageJson, graph) {
48
51
  lockFile,
49
52
  };
50
53
  }
54
+ /**
55
+ * npm reads the `allowScripts` install-script allowlist only from the install
56
+ * root, but `npm approve-scripts` writes it to the workspace root, so it never
57
+ * lives in the project package.json the prune output is built from. Carry the
58
+ * root allowlist over, with project-level entries preserved and winning on
59
+ * conflict. Mirrors the `pnpm.allowBuilds` handling in createPackageJson.
60
+ */
61
+ function mergeAllowScripts(packageJson) {
62
+ const rootPackageJson = (0, devkit_1.readJsonFile)((0, path_1.join)(devkit_1.workspaceRoot, 'package.json'));
63
+ if (!rootPackageJson.allowScripts) {
64
+ return;
65
+ }
66
+ packageJson.allowScripts = {
67
+ ...rootPackageJson.allowScripts,
68
+ ...packageJson.allowScripts,
69
+ };
70
+ }
71
+ function resolveCatalogReferences(packageJson) {
72
+ const manager = (0, internal_1.getCatalogManager)(devkit_1.workspaceRoot);
73
+ if (!manager) {
74
+ return packageJson;
75
+ }
76
+ const sections = [
77
+ 'dependencies',
78
+ 'optionalDependencies',
79
+ 'devDependencies',
80
+ 'peerDependencies',
81
+ ];
82
+ const resolved = { ...packageJson };
83
+ for (const section of sections) {
84
+ const deps = packageJson[section];
85
+ if (!deps) {
86
+ continue;
87
+ }
88
+ const resolvedDeps = { ...deps };
89
+ for (const [packageName, version] of Object.entries(deps)) {
90
+ if (!manager.isCatalogReference(version)) {
91
+ continue;
92
+ }
93
+ const resolvedVersion = manager.resolveCatalogReference(devkit_1.workspaceRoot, packageName, version);
94
+ if (!resolvedVersion) {
95
+ throw new Error(`Could not resolve catalog reference for package ${packageName}@${version}.`);
96
+ }
97
+ resolvedDeps[packageName] = resolvedVersion;
98
+ }
99
+ resolved[section] = resolvedDeps;
100
+ }
101
+ return resolved;
102
+ }
51
103
  function getPackageJson(schema, context) {
52
104
  const target = (0, devkit_1.parseTargetString)(schema.buildTarget, context);
53
105
  const project = context.projectGraph.nodes[target.project].data;
@@ -55,8 +55,12 @@ function createTypeScriptCompilationOptions(normalizedOptions, context) {
55
55
  outputPath: (0, devkit_1.joinPathFragments)(normalizedOptions.outputPath),
56
56
  projectName: context.projectName,
57
57
  projectRoot: normalizedOptions.projectRoot,
58
- rootDir: (0, devkit_1.joinPathFragments)(normalizedOptions.rootDir),
59
- tsConfig: (0, devkit_1.joinPathFragments)(normalizedOptions.tsConfig),
58
+ // Keep the Windows drive letter on rootDir/tsConfig (only forward-slash them).
59
+ // joinPathFragments strips it via normalizePath, leaving them drive-less while
60
+ // the generated path mappings stay absolute drive-full, so alias-resolved files
61
+ // fail TypeScript's rootDir containment check (TS6059) on Windows.
62
+ rootDir: normalizedOptions.rootDir.replace(/\\/g, '/'),
63
+ tsConfig: normalizedOptions.tsConfig.replace(/\\/g, '/'),
60
64
  watch: normalizedOptions.watch,
61
65
  deleteOutputPath: normalizedOptions.clean,
62
66
  getCustomTransformers: (0, lib_1.getCustomTrasformersFactory)(normalizedOptions.transformers),
@@ -249,7 +249,5 @@ function updatePackageJson(tree, projectName, projectRoot, main, outputPath, roo
249
249
  function mergeTargetDefaults(tree, project, buildTarget) {
250
250
  const nxJson = (0, devkit_1.readNxJson)(tree);
251
251
  const projectTarget = project.targets[buildTarget];
252
- return (0, devkit_internals_1.mergeTargetConfigurations)(projectTarget, (projectTarget.executor
253
- ? nxJson.targetDefaults?.[projectTarget.executor]
254
- : undefined) ?? nxJson.targetDefaults?.[buildTarget]);
252
+ return (0, devkit_internals_1.mergeTargetConfigurations)(projectTarget, (0, internal_1.readTargetDefaultsForTarget)(buildTarget, nxJson.targetDefaults, projectTarget.executor));
255
253
  }
@@ -35,30 +35,50 @@ async function default_1(tree) {
35
35
  if (!nxJson.targetDefaults) {
36
36
  return;
37
37
  }
38
- for (const [targetOrExecutor, targetConfig] of Object.entries(nxJson.targetDefaults)) {
39
- if (!exports.executors.includes(targetOrExecutor) &&
40
- !exports.executors.includes(targetConfig.executor)) {
41
- continue;
42
- }
43
- if (targetConfig.options) {
44
- delete targetConfig.options.external;
45
- delete targetConfig.options.externalBuildTargets;
46
- if (!Object.keys(targetConfig.options).length) {
47
- delete targetConfig.options;
38
+ const cleanEntry = (entry) => {
39
+ if (entry.options) {
40
+ delete entry.options.external;
41
+ delete entry.options.externalBuildTargets;
42
+ if (!Object.keys(entry.options).length) {
43
+ delete entry.options;
48
44
  }
49
45
  }
50
- Object.entries(targetConfig.configurations ?? {}).forEach(([, config]) => {
46
+ Object.values(entry.configurations ?? {}).forEach((config) => {
51
47
  delete config.external;
52
48
  delete config.externalBuildTargets;
53
49
  });
54
- if (!Object.keys(targetConfig).length ||
55
- (Object.keys(targetConfig).length === 1 &&
56
- Object.keys(targetConfig)[0] === 'executor')) {
57
- delete nxJson.targetDefaults[targetOrExecutor];
50
+ };
51
+ const targetDefaults = nxJson.targetDefaults;
52
+ for (const key of Object.keys(targetDefaults)) {
53
+ const value = targetDefaults[key];
54
+ const entries = Array.isArray(value)
55
+ ? value
56
+ : [value];
57
+ const usesExecutor = (entry) => exports.executors.includes(key) ||
58
+ exports.executors.includes(entry.executor) ||
59
+ exports.executors.includes(entry.filter?.executor);
60
+ const kept = entries.filter((entry) => {
61
+ if (usesExecutor(entry)) {
62
+ cleanEntry(entry);
63
+ }
64
+ // Drop entries left with nothing but their `filter`/`executor` locator.
65
+ return Object.keys(entry).some((k) => k !== 'filter' && k !== 'executor');
66
+ });
67
+ if (kept.length === 0) {
68
+ delete targetDefaults[key];
58
69
  }
59
- if (!Object.keys(nxJson.targetDefaults).length) {
60
- delete nxJson.targetDefaults;
70
+ else if (kept.length === 1 && kept[0].filter === undefined) {
71
+ // A lone unfiltered entry is stored as the plain object form (which
72
+ // omits `filter`).
73
+ const { filter: _filter, ...config } = kept[0];
74
+ targetDefaults[key] = config;
61
75
  }
76
+ else {
77
+ targetDefaults[key] = kept;
78
+ }
79
+ }
80
+ if (Object.keys(targetDefaults).length === 0) {
81
+ delete nxJson.targetDefaults;
62
82
  }
63
83
  (0, devkit_1.updateNxJson)(tree, nxJson);
64
84
  await (0, devkit_1.formatFiles)(tree);
@@ -1,28 +1,43 @@
1
1
  import { type Tree } from '@nx/devkit';
2
2
  /**
3
- * Two independent passes over every tsconfig*.json in the workspace:
3
+ * Runs on TypeScript 6 workspaces (gated by `requires` in migrations.json) in
4
+ * two passes over every `tsconfig*.json`.
4
5
  *
5
- * 1. ignoreDeprecations pass - adds `ignoreDeprecations: "6.0"` to any
6
- * compilerOptions (or ts-node.compilerOptions) block that directly carries a
7
- * TS6 hard-deprecated option value (moduleResolution node/node10/classic,
8
- * baseUrl, target es5, esModuleInterop false, outFile, module
9
- * amd/umd/system/none, alwaysStrict false, allowSyntheticDefaultImports
10
- * false, downlevelIteration set to any value).
6
+ * Pass 1 writes what pass 2's `extends` resolution reads back:
7
+ * - Default-preserving pins: on every chain root (no `extends`) that is not a
8
+ * pure solution container (`files: []` with no `include`), pin the TS6
9
+ * defaults that changed in a breaking way, but only where the root leaves
10
+ * them unset:
11
+ * - `strict: false`. TS6 treats an absent `strict` as true, TS5 as false.
12
+ * - `noUncheckedSideEffectImports: false`. TS6 defaults it true, turning a
13
+ * bare side-effect import of an asset without an ambient declaration
14
+ * (`import './styles.css'`) into a hard TS2882; a semantic diagnostic,
15
+ * not a deprecation, so `ignoreDeprecations` cannot silence it.
16
+ * - `types: ["*"]`. TS6 loads no @types when `types` is unset (TS5 loaded
17
+ * all); the wildcard restores that so a config relying on it (ts-node
18
+ * type-checking jest.config.ts) keeps finding @types/node.
19
+ * - `esModuleInterop: false`. TS6 flips the default false->true, changing
20
+ * `import * as x from '<cjs>'` call semantics at runtime; false preserves
21
+ * the pre-TS6 behavior. It is itself deprecated (removed in TS7), so pass
22
+ * 2 silences it, deferring the interop change to the eventual TS7 work.
23
+ * - Config-load flag: set `ignoreDeprecations: "6.0"` on every file named
24
+ * exactly `tsconfig.json`, the name jest/ts-node auto-resolve (walking up
25
+ * from the file they compile, such as jest.config.ts). ts-node injects a
26
+ * `target: es5` when the config leaves it unset, and es5 is a TS6-deprecated
27
+ * value (TS5107); the flag (which ts-node passes through) keeps that load
28
+ * silent. Set unconditionally, since any `tsconfig.json` is a potential
29
+ * loader target, and inert when nothing is deprecated. It cannot silence a
30
+ * module/moduleResolution mismatch (TS5110) when ts-node's forced
31
+ * `module: commonjs` meets an inherited `nodenext` resolution.
11
32
  *
12
- * 2. default-preserving pass - for every chain root (no "extends" key), pins
13
- * the TS6 compiler-option defaults that flipped to a stricter value back to
14
- * their pre-TS6 value, but only when the root does not set them explicitly:
15
- * - "strict": false - TS6 treats an absent "strict" as true; TS5 as false.
16
- * - "noUncheckedSideEffectImports": false - TS6 defaults it to true, which
17
- * turns a bare side-effect import of an asset lacking an ambient
18
- * declaration (e.g. `import './styles.css'`) into a hard TS2882 error; it
19
- * is a semantic diagnostic, not a deprecation, so `ignoreDeprecations`
20
- * cannot silence it.
21
- * Files with "extends" inherit from their chain root and are left untouched.
22
- * Pure solution-style containers (root has `"files": []` and no "include")
23
- * select no source files, so pinning there is noise and they are skipped.
24
- *
25
- * Only runs on TS6 workspaces (gated by `requires` in migrations.json), because
26
- * `ignoreDeprecations: "6.0"` is itself a hard error (TS5103) on TS 5.x.
33
+ * Pass 2 silences the remaining hard-deprecated values. For each config,
34
+ * TypeScript resolves its `extends`-merged compiler options; when they carry a
35
+ * value TS6 hard-deprecates (see `hasDeprecatedOption`) and their effective
36
+ * `ignoreDeprecations` is not already `"6.0"`, add the flag. Because the check
37
+ * runs on the merged options, it covers a value the config inherits from a base
38
+ * this migration never edits, respects an inherited `"6.0"` (no redundant flag),
39
+ * and upgrades a stale local `"5.0"` that overrides one. The
40
+ * `ts-node.compilerOptions` overlay, which `tsc` does not merge, is checked on
41
+ * its own.
27
42
  */
28
43
  export default function (tree: Tree): Promise<void>;
@@ -4,192 +4,256 @@ exports.default = default_1;
4
4
  const devkit_1 = require("@nx/devkit");
5
5
  const node_path_1 = require("node:path");
6
6
  const jsonc_parser_1 = require("jsonc-parser");
7
+ const ensure_typescript_1 = require("../../utils/typescript/ensure-typescript");
7
8
  const FORMATTING_OPTIONS = {
8
9
  formattingOptions: { keepLines: true, insertSpaces: true, tabSize: 2 },
9
10
  };
11
+ let tsModule;
10
12
  /**
11
- * Two independent passes over every tsconfig*.json in the workspace:
13
+ * Runs on TypeScript 6 workspaces (gated by `requires` in migrations.json) in
14
+ * two passes over every `tsconfig*.json`.
12
15
  *
13
- * 1. ignoreDeprecations pass - adds `ignoreDeprecations: "6.0"` to any
14
- * compilerOptions (or ts-node.compilerOptions) block that directly carries a
15
- * TS6 hard-deprecated option value (moduleResolution node/node10/classic,
16
- * baseUrl, target es5, esModuleInterop false, outFile, module
17
- * amd/umd/system/none, alwaysStrict false, allowSyntheticDefaultImports
18
- * false, downlevelIteration set to any value).
16
+ * Pass 1 writes what pass 2's `extends` resolution reads back:
17
+ * - Default-preserving pins: on every chain root (no `extends`) that is not a
18
+ * pure solution container (`files: []` with no `include`), pin the TS6
19
+ * defaults that changed in a breaking way, but only where the root leaves
20
+ * them unset:
21
+ * - `strict: false`. TS6 treats an absent `strict` as true, TS5 as false.
22
+ * - `noUncheckedSideEffectImports: false`. TS6 defaults it true, turning a
23
+ * bare side-effect import of an asset without an ambient declaration
24
+ * (`import './styles.css'`) into a hard TS2882; a semantic diagnostic,
25
+ * not a deprecation, so `ignoreDeprecations` cannot silence it.
26
+ * - `types: ["*"]`. TS6 loads no @types when `types` is unset (TS5 loaded
27
+ * all); the wildcard restores that so a config relying on it (ts-node
28
+ * type-checking jest.config.ts) keeps finding @types/node.
29
+ * - `esModuleInterop: false`. TS6 flips the default false->true, changing
30
+ * `import * as x from '<cjs>'` call semantics at runtime; false preserves
31
+ * the pre-TS6 behavior. It is itself deprecated (removed in TS7), so pass
32
+ * 2 silences it, deferring the interop change to the eventual TS7 work.
33
+ * - Config-load flag: set `ignoreDeprecations: "6.0"` on every file named
34
+ * exactly `tsconfig.json`, the name jest/ts-node auto-resolve (walking up
35
+ * from the file they compile, such as jest.config.ts). ts-node injects a
36
+ * `target: es5` when the config leaves it unset, and es5 is a TS6-deprecated
37
+ * value (TS5107); the flag (which ts-node passes through) keeps that load
38
+ * silent. Set unconditionally, since any `tsconfig.json` is a potential
39
+ * loader target, and inert when nothing is deprecated. It cannot silence a
40
+ * module/moduleResolution mismatch (TS5110) when ts-node's forced
41
+ * `module: commonjs` meets an inherited `nodenext` resolution.
19
42
  *
20
- * 2. default-preserving pass - for every chain root (no "extends" key), pins
21
- * the TS6 compiler-option defaults that flipped to a stricter value back to
22
- * their pre-TS6 value, but only when the root does not set them explicitly:
23
- * - "strict": false - TS6 treats an absent "strict" as true; TS5 as false.
24
- * - "noUncheckedSideEffectImports": false - TS6 defaults it to true, which
25
- * turns a bare side-effect import of an asset lacking an ambient
26
- * declaration (e.g. `import './styles.css'`) into a hard TS2882 error; it
27
- * is a semantic diagnostic, not a deprecation, so `ignoreDeprecations`
28
- * cannot silence it.
29
- * Files with "extends" inherit from their chain root and are left untouched.
30
- * Pure solution-style containers (root has `"files": []` and no "include")
31
- * select no source files, so pinning there is noise and they are skipped.
32
- *
33
- * Only runs on TS6 workspaces (gated by `requires` in migrations.json), because
34
- * `ignoreDeprecations: "6.0"` is itself a hard error (TS5103) on TS 5.x.
43
+ * Pass 2 silences the remaining hard-deprecated values. For each config,
44
+ * TypeScript resolves its `extends`-merged compiler options; when they carry a
45
+ * value TS6 hard-deprecates (see `hasDeprecatedOption`) and their effective
46
+ * `ignoreDeprecations` is not already `"6.0"`, add the flag. Because the check
47
+ * runs on the merged options, it covers a value the config inherits from a base
48
+ * this migration never edits, respects an inherited `"6.0"` (no redundant flag),
49
+ * and upgrades a stale local `"5.0"` that overrides one. The
50
+ * `ts-node.compilerOptions` overlay, which `tsc` does not merge, is checked on
51
+ * its own.
35
52
  */
36
53
  async function default_1(tree) {
37
- let deprecationCount = 0;
54
+ tsModule ??= (0, ensure_typescript_1.ensureTypescript)();
55
+ const ts = tsModule;
56
+ // Tree-backed host so TypeScript resolves `extends` against pass 1's pending
57
+ // writes; the `readDirectory` no-op skips the source-file scan, since only the
58
+ // merged compiler options matter here, not the file list.
59
+ const parseHost = {
60
+ ...ts.sys,
61
+ readFile: (filePath) => tree.read(filePath, 'utf-8') ?? undefined,
62
+ readDirectory: () => [],
63
+ };
64
+ const tsconfigPaths = await (0, devkit_1.globAsync)(tree, ['**/tsconfig*.json']);
65
+ // Pass 1: pins and config-load flags, so pass 2's inheritance check sees them.
38
66
  let defaultsPinCount = 0;
39
- (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
40
- const name = (0, node_path_1.basename)(filePath);
41
- if (!name.startsWith('tsconfig') || !name.endsWith('.json')) {
42
- return;
43
- }
44
- if (addIgnoreDeprecations(tree, filePath)) {
45
- deprecationCount += 1;
46
- }
47
- if (pinPreTs6Defaults(tree, filePath)) {
67
+ let configLoadCount = 0;
68
+ for (const tsconfigPath of tsconfigPaths) {
69
+ const { pinned, flagged } = applyDefaultsAndConfigLoadFlag(tree, tsconfigPath);
70
+ if (pinned)
48
71
  defaultsPinCount += 1;
72
+ if (flagged)
73
+ configLoadCount += 1;
74
+ }
75
+ // Pass 2: silence a deprecated value the config carries or inherits.
76
+ let deprecationCount = 0;
77
+ for (const tsconfigPath of tsconfigPaths) {
78
+ if (silenceDeprecations(tree, ts, parseHost, tsconfigPath)) {
79
+ deprecationCount += 1;
49
80
  }
50
- });
81
+ }
82
+ if (configLoadCount > 0) {
83
+ devkit_1.logger.info(`Ensured "ignoreDeprecations": "6.0" on ${configLoadCount} "tsconfig.json" file(s) so config loaders (jest/ts-node) keep working on TypeScript 6.`);
84
+ }
51
85
  if (deprecationCount > 0) {
52
86
  devkit_1.logger.info(`Added "ignoreDeprecations": "6.0" to ${deprecationCount} tsconfig file(s) carrying TS6-deprecated options.`);
53
87
  }
54
88
  if (defaultsPinCount > 0) {
55
- devkit_1.logger.info(`Pinned pre-TS6 compiler option defaults ("strict", "noUncheckedSideEffectImports") on ${defaultsPinCount} tsconfig chain root(s) to preserve existing behavior.`);
89
+ devkit_1.logger.info(`Pinned pre-TS6 compiler option defaults ("strict", "noUncheckedSideEffectImports", "types", "esModuleInterop") on ${defaultsPinCount} tsconfig chain root(s).`);
56
90
  }
57
91
  await (0, devkit_1.formatFiles)(tree);
58
92
  }
59
- function addIgnoreDeprecations(tree, tsconfigPath) {
93
+ // The pre-TS6 default values pinned on chain roots that leave them unset; pass 1
94
+ // above explains why each one changed in a breaking way.
95
+ const DEFAULT_PRESERVING_PINS = [
96
+ ['strict', false],
97
+ ['noUncheckedSideEffectImports', false],
98
+ ['types', ['*']],
99
+ ['esModuleInterop', false],
100
+ ];
101
+ // Pass 1 for one file: pin pre-TS6 defaults on a chain root and set the
102
+ // config-load flag on a `tsconfig.json`, combined into a single write.
103
+ function applyDefaultsAndConfigLoadFlag(tree, tsconfigPath) {
60
104
  const original = tree.read(tsconfigPath, 'utf-8');
61
105
  if (!original) {
106
+ return { pinned: false, flagged: false };
107
+ }
108
+ const root = (0, jsonc_parser_1.parseTree)(original);
109
+ if (!root || root.type !== 'object') {
110
+ return { pinned: false, flagged: false };
111
+ }
112
+ const own = (0, jsonc_parser_1.getNodeValue)(root);
113
+ const compilerOptions = own.compilerOptions;
114
+ // A present-but-non-object compilerOptions can't receive keys; leave the file
115
+ // alone so modify() doesn't throw and abort the whole migration.
116
+ if (compilerOptions !== undefined && !isObject(compilerOptions)) {
117
+ return { pinned: false, flagged: false };
118
+ }
119
+ const options = (compilerOptions ?? {});
120
+ let contents = original;
121
+ let pinned = false;
122
+ let flagged = false;
123
+ // Only chain roots; a file with "extends" inherits the defaults. Skip pure
124
+ // solution containers ("files": [] and no "include"): they select no sources.
125
+ const isSolutionContainer = Array.isArray(own.files) && own.files.length === 0 && !('include' in own);
126
+ if (own.extends === undefined && !isSolutionContainer) {
127
+ for (const [key, value] of DEFAULT_PRESERVING_PINS) {
128
+ if (key in options)
129
+ continue; // An explicit value means the user opted in.
130
+ contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', key], value, FORMATTING_OPTIONS));
131
+ pinned = true;
132
+ }
133
+ }
134
+ if ((0, node_path_1.basename)(tsconfigPath) === 'tsconfig.json' &&
135
+ options.ignoreDeprecations !== '6.0') {
136
+ contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS));
137
+ flagged = true;
138
+ }
139
+ if (pinned || flagged) {
140
+ tree.write(tsconfigPath, contents);
141
+ }
142
+ return { pinned, flagged };
143
+ }
144
+ // Pass 2 for one file: silence a deprecated value it carries or inherits, in the
145
+ // main block and the ts-node overlay, combined into a single write.
146
+ function silenceDeprecations(tree, ts, parseHost, tsconfigPath) {
147
+ const original = tree.read(tsconfigPath, 'utf-8');
148
+ if (!original) {
149
+ return false;
150
+ }
151
+ const root = (0, jsonc_parser_1.parseTree)(original);
152
+ if (!root || root.type !== 'object') {
153
+ return false;
154
+ }
155
+ const own = (0, jsonc_parser_1.getNodeValue)(root);
156
+ const mainBlock = own.compilerOptions;
157
+ const mainIsObject = isObject(mainBlock);
158
+ const mainFlag = mainIsObject
159
+ ? mainBlock.ignoreDeprecations
160
+ : undefined;
161
+ const tsNodeBlock = isObject(own['ts-node'])
162
+ ? own['ts-node'].compilerOptions
163
+ : undefined;
164
+ const tsNodeIsObject = isObject(tsNodeBlock);
165
+ const tsNodeFlag = tsNodeIsObject
166
+ ? tsNodeBlock.ignoreDeprecations
167
+ : undefined;
168
+ // No own options and no `extends` to inherit through, and no overlay to check:
169
+ // nothing can be deprecated here, so skip without parsing.
170
+ if (!mainIsObject && own.extends === undefined && !tsNodeIsObject) {
62
171
  return false;
63
172
  }
64
- // Each entry targets a distinct compilerOptions block within the same file.
65
- const blocks = [
66
- ['compilerOptions'],
67
- ['ts-node', 'compilerOptions'],
68
- ];
173
+ // Main is already silenced and there is no overlay: nothing to do, skip the
174
+ // parse entirely (the common case for a config-load-flagged `tsconfig.json`).
175
+ if (mainFlag === '6.0' && !tsNodeIsObject) {
176
+ return false;
177
+ }
178
+ // The `extends`-merged main options, normalized by TypeScript. Passing the
179
+ // config object (not the path) keeps TypeScript from re-reading this file; it
180
+ // reads only the ancestors.
181
+ const mainOptions = ts.parseJsonConfigFileContent({ extends: own.extends, compilerOptions: mainIsObject ? mainBlock : {} }, parseHost, (0, node_path_1.dirname)(tsconfigPath)).options;
182
+ const mainDeprecated = hasDeprecatedOption(ts, mainOptions);
69
183
  let contents = original;
70
184
  let changed = false;
71
- for (const blockPath of blocks) {
72
- // Re-parse each iteration: a prior edit shifts offsets in `contents`.
73
- const root = (0, jsonc_parser_1.parseTree)(contents);
74
- const blockNode = root && (0, jsonc_parser_1.findNodeAtLocation)(root, blockPath);
75
- if (!blockNode || blockNode.type !== 'object') {
76
- continue;
77
- }
78
- const compilerOptions = (0, jsonc_parser_1.getNodeValue)(blockNode);
79
- if (!hasDeprecatedValue(compilerOptions)) {
80
- continue;
81
- }
82
- // An existing non-"6.0" value (e.g. "5.0") does NOT silence 6.0-class
83
- // deprecations, so upgrade it; only "6.0" is already correct.
84
- if (compilerOptions.ignoreDeprecations === '6.0') {
85
- continue;
86
- }
87
- const edits = (0, jsonc_parser_1.modify)(contents, [...blockPath, 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS);
88
- contents = (0, jsonc_parser_1.applyEdits)(contents, edits);
185
+ // Main block: silence a deprecated value it carries or inherits, unless an
186
+ // effective "6.0" (own or inherited) already covers it. A stale local "5.0"
187
+ // that overrides an inherited "6.0" shows through the merge and is upgraded.
188
+ // An absent block is created; a present-but-non-object one is left alone so
189
+ // modify() neither corrupts it nor throws.
190
+ const mainWritable = mainBlock === undefined || mainIsObject;
191
+ if (mainWritable &&
192
+ mainFlag !== '6.0' &&
193
+ mainDeprecated &&
194
+ mainOptions.ignoreDeprecations !== '6.0') {
195
+ contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS));
89
196
  changed = true;
90
197
  }
198
+ // ts-node overlay: `tsc` does not merge it, so check it directly. ts-node
199
+ // overlays the resolved main options at runtime; a "6.0" the main block just
200
+ // received does not reliably reach the overlay across ts-node versions, so
201
+ // silence it on its own whenever it or the resolved main carries a deprecated
202
+ // value.
203
+ if (tsNodeIsObject && tsNodeFlag !== '6.0') {
204
+ const overlayOptions = ts.parseJsonConfigFileContent({ compilerOptions: tsNodeBlock }, parseHost, (0, node_path_1.dirname)(tsconfigPath)).options;
205
+ if (hasDeprecatedOption(ts, overlayOptions) || mainDeprecated) {
206
+ contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['ts-node', 'compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS));
207
+ changed = true;
208
+ }
209
+ }
91
210
  if (changed) {
92
211
  tree.write(tsconfigPath, contents);
93
212
  }
94
213
  return changed;
95
214
  }
96
- // Values that compile silently on TS 5.8 but are hard deprecation errors
97
- // (TS5101/TS5107) on TS 6.0 - derived by differential 5.8-vs-6.0 probing.
98
- function hasDeprecatedValue(compilerOptions) {
99
- const moduleResolution = asLowerString(compilerOptions.moduleResolution);
100
- if (moduleResolution === 'node' ||
101
- moduleResolution === 'node10' ||
102
- moduleResolution === 'classic') {
215
+ // Whether TypeScript-parsed, `extends`-merged compiler options carry a value
216
+ // that compiles silently on TS 5.8 but is a hard deprecation error (TS5101/
217
+ // TS5107) on TS 6.0, derived by differential 5.8-vs-6.0 probing. Options are
218
+ // read from the normalized enums TypeScript produces; an unset option stays
219
+ // undefined (TypeScript does not fill in the module-derived `moduleResolution`
220
+ // default here), so an implied resolution is not misread as deprecated.
221
+ function hasDeprecatedOption(ts, options) {
222
+ const moduleResolution = options.moduleResolution;
223
+ if (moduleResolution === ts.ModuleResolutionKind.Node10 ||
224
+ moduleResolution === ts.ModuleResolutionKind.Classic) {
103
225
  return true;
104
226
  }
105
- // `baseUrl`/`outFile` only deprecate when set to a real value; `null`/unset
106
- // is inert on TS6.
107
- if (compilerOptions.baseUrl != null) {
227
+ if (options.baseUrl != null) {
108
228
  return true;
109
229
  }
110
- if (asLowerString(compilerOptions.target) === 'es5') {
230
+ if (options.target === ts.ScriptTarget.ES5) {
111
231
  return true;
112
232
  }
113
- if (compilerOptions.esModuleInterop === false) {
233
+ if (options.esModuleInterop === false) {
114
234
  return true;
115
235
  }
116
- if (compilerOptions.outFile != null) {
236
+ if (options.outFile != null) {
117
237
  return true;
118
238
  }
119
- const moduleValue = asLowerString(compilerOptions.module);
120
- if (moduleValue === 'amd' ||
121
- moduleValue === 'umd' ||
122
- moduleValue === 'system' ||
123
- moduleValue === 'none') {
239
+ const module = options.module;
240
+ if (module === ts.ModuleKind.AMD ||
241
+ module === ts.ModuleKind.UMD ||
242
+ module === ts.ModuleKind.System ||
243
+ module === ts.ModuleKind.None) {
124
244
  return true;
125
245
  }
126
- // Only the explicit `false` value triggers TS5107 for these two flags.
127
- if (compilerOptions.alwaysStrict === false) {
246
+ if (options.alwaysStrict === false) {
128
247
  return true;
129
248
  }
130
- if (compilerOptions.allowSyntheticDefaultImports === false) {
249
+ if (options.allowSyntheticDefaultImports === false) {
131
250
  return true;
132
251
  }
133
- // A real boolean triggers TS5101; `null`/unset is inert. `!= null` also flags
134
- // non-boolean JSON, but that only adds a harmless no-op `ignoreDeprecations`.
135
- if (compilerOptions.downlevelIteration != null) {
252
+ if (options.downlevelIteration != null) {
136
253
  return true;
137
254
  }
138
255
  return false;
139
256
  }
140
- // TS6 compiler-option defaults that flipped to a stricter value. We pin each
141
- // back to its pre-TS6 value on chain roots that don't set it, so existing
142
- // workspaces keep building without adopting a legit TS6 setup.
143
- const DEFAULT_PRESERVING_PINS = [
144
- ['strict', false],
145
- ['noUncheckedSideEffectImports', false],
146
- ];
147
- function pinPreTs6Defaults(tree, tsconfigPath) {
148
- const original = tree.read(tsconfigPath, 'utf-8');
149
- if (!original) {
150
- return false;
151
- }
152
- const root = (0, jsonc_parser_1.parseTree)(original);
153
- if (!root || root.type !== 'object') {
154
- return false;
155
- }
156
- const rootValue = (0, jsonc_parser_1.getNodeValue)(root);
157
- // Only touch chain roots - files with "extends" inherit from their ancestor.
158
- if ('extends' in rootValue) {
159
- return false;
160
- }
161
- // Skip pure solution-style containers: root has "files": [] and no "include".
162
- // They select no source files, so pinning defaults there is noise.
163
- const filesNode = (0, jsonc_parser_1.findNodeAtLocation)(root, ['files']);
164
- const hasEmptyFiles = filesNode?.type === 'array' && filesNode.children?.length === 0;
165
- if (hasEmptyFiles && !('include' in rootValue)) {
166
- return false;
167
- }
168
- const compilerOptionsNode = (0, jsonc_parser_1.findNodeAtLocation)(root, ['compilerOptions']);
169
- // A present-but-non-object compilerOptions can't receive pinned keys; bailing
170
- // avoids modify() throwing and aborting the whole migration.
171
- if (compilerOptionsNode && compilerOptionsNode.type !== 'object') {
172
- return false;
173
- }
174
- const compilerOptions = compilerOptionsNode
175
- ? (0, jsonc_parser_1.getNodeValue)(compilerOptionsNode)
176
- : {};
177
- let contents = original;
178
- let changed = false;
179
- for (const [key, value] of DEFAULT_PRESERVING_PINS) {
180
- // An explicit value (true or false) means the user opted in - leave it.
181
- if (key in compilerOptions) {
182
- continue;
183
- }
184
- const edits = (0, jsonc_parser_1.modify)(contents, ['compilerOptions', key], value, FORMATTING_OPTIONS);
185
- contents = (0, jsonc_parser_1.applyEdits)(contents, edits);
186
- changed = true;
187
- }
188
- if (changed) {
189
- tree.write(tsconfigPath, contents);
190
- }
191
- return changed;
192
- }
193
- function asLowerString(value) {
194
- return typeof value === 'string' ? value.toLowerCase() : undefined;
257
+ function isObject(value) {
258
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
195
259
  }
@@ -1,11 +1,12 @@
1
1
  #### Keep Existing Workspaces Compiling on TypeScript 6
2
2
 
3
- TypeScript 6 turns several long-deprecated compiler options into hard errors and flips a few option defaults to stricter values. So that an existing workspace keeps compiling on TypeScript 6 without being migrated to a full TypeScript 6 setup, this migration makes two edits to the `tsconfig*.json` files in the workspace:
3
+ TypeScript 6 turns several long-deprecated compiler options into hard errors and flips a few option defaults to stricter values. So that an existing workspace keeps compiling on TypeScript 6 without being migrated to a full TypeScript 6 setup, this migration edits the `tsconfig*.json` files in the workspace:
4
4
 
5
- - Adds `"ignoreDeprecations": "6.0"` to any `compilerOptions` (or `ts-node.compilerOptions`) block that directly sets a TypeScript 6 deprecated option - for example `moduleResolution` set to `node`/`node10`/`classic`, `baseUrl`, `target` set to `es5`, `esModuleInterop: false`, `outFile`, `module` set to `amd`/`umd`/`system`/`none`, `alwaysStrict: false`, `allowSyntheticDefaultImports: false`, or `downlevelIteration`.
6
- - Pins `"strict": false` and `"noUncheckedSideEffectImports": false` on every chain-root tsconfig (one without an `extends`) that does not already set them. TypeScript 6 treats an absent `strict` as `true` (it was `false` when unset before) and defaults `noUncheckedSideEffectImports` to `true`, which turns a bare side-effect import such as `import './styles.css'` without an ambient module declaration into an error. Pinning both preserves the pre-TypeScript 6 behavior.
5
+ - Adds `"ignoreDeprecations": "6.0"` to any `compilerOptions` (or `ts-node.compilerOptions`) block whose effective options carry a TypeScript 6 deprecated option, set directly or inherited through `extends` (including from a base this migration does not edit, such as a package-provided config). Examples: `moduleResolution` set to `node`/`node10`/`classic`, `baseUrl`, `target` set to `es5`, `esModuleInterop: false`, `outFile`, `module` set to `amd`/`umd`/`system`/`none`, `alwaysStrict: false`, `allowSyntheticDefaultImports: false`, or `downlevelIteration`. A block that already inherits an effective `"6.0"` and sets no local value is left alone; a stale local value such as `"5.0"` is upgraded to `"6.0"`, since it would otherwise override the inherited flag and still error.
6
+ - Pins `"strict": false`, `"noUncheckedSideEffectImports": false`, `"types": ["*"]`, and `"esModuleInterop": false` on every chain-root tsconfig (one without an `extends`) that does not already set them. TypeScript 6 treats an absent `strict` as `true` (it was `false` when unset before), defaults `noUncheckedSideEffectImports` to `true` (which turns a bare side-effect import such as `import './styles.css'` without an ambient module declaration into an error), no longer auto-loads every `@types` package when `types` is unset the way TypeScript 5 did (the `"*"` wildcard restores that last default), and flips `esModuleInterop` from `false` to `true` (so an `import * as x from '<cjs>'` binds a non-callable namespace object and a call or `new` on that import fails at runtime). Pinning all four preserves the pre-TypeScript 6 behavior. Because `esModuleInterop: false` is itself a TypeScript 6 deprecated value, this pin runs before the `ignoreDeprecations` edit above, so the added `false` is silenced in the same run.
7
+ - Adds `"ignoreDeprecations": "6.0"` to every `tsconfig.json` (the exact file name jest and ts-node auto-resolve), even one that carries no deprecated option of its own. Those loaders compile config files such as `jest.config.ts`, and ts-node injects a default `target: es5` when the config leaves it unset. TypeScript 6 rejects `es5` as a deprecated value, so the flag keeps that load working. It is inert wherever nothing is actually deprecated.
7
8
 
8
- Files that use `extends` inherit these settings from their chain root and are left untouched, and pure solution-style tsconfigs (`"files": []` with no `include`) are skipped. The migration only runs when the workspace is on TypeScript 6.
9
+ Files that use `extends` inherit the pinned settings from their chain root, so the pins are not repeated on them, and pure solution-style tsconfigs (`"files": []` with no `include`) receive no pins either, though a solution-style `tsconfig.json` still gets the config-load flag. The migration only runs when the workspace is on TypeScript 6.
9
10
 
10
11
  #### Sample Code Changes
11
12
 
@@ -23,15 +24,17 @@ Files that use `extends` inherit these settings from their chain root and are le
23
24
 
24
25
  ##### After
25
26
 
26
- ```json title="tsconfig.json" {6-8}
27
+ ```json title="tsconfig.json" {6-10}
27
28
  {
28
29
  "compilerOptions": {
29
30
  "target": "es5",
30
31
  "module": "esnext",
31
32
  "moduleResolution": "bundler",
32
- "ignoreDeprecations": "6.0",
33
33
  "strict": false,
34
- "noUncheckedSideEffectImports": false
34
+ "noUncheckedSideEffectImports": false,
35
+ "types": ["*"],
36
+ "esModuleInterop": false,
37
+ "ignoreDeprecations": "6.0"
35
38
  }
36
39
  }
37
40
  ```
@@ -0,0 +1,26 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ /**
3
+ * TypeScript 6.0 changed the implicit `rootDir`. Before 6.0 an unset `rootDir`
4
+ * was inferred as the common directory of a program's non-declaration input
5
+ * files; 6.0 defaults it to the tsconfig's own directory instead. A program
6
+ * whose files resolve outside that directory (most commonly a spec/e2e tsconfig
7
+ * importing another project's source through a `paths` alias) then hard-fails
8
+ * with TS5011 or TS6059.
9
+ *
10
+ * For every project `tsconfig*.json` that lacks an explicit `rootDir`, this pins
11
+ * `rootDir` to exactly the directory TypeScript 5 would have inferred, so both
12
+ * compilation and emit layout are unchanged under 6.0. The value is computed by
13
+ * the compiler itself: a throwaway program built with `configFilePath` cleared
14
+ * takes the file-derived branch of `getCommonSourceDirectory`, so it matches
15
+ * `tsc` exactly, including project-reference redirects. Configs already at the
16
+ * 6.0 default are left untouched: their inferred directory equals the tsconfig
17
+ * directory, or they are composite (which defaulted there before 6.0 too).
18
+ *
19
+ * Each config is written on its own; nothing is written to a shared `extends`
20
+ * base, so a config never inherits a `rootDir` computed for a sibling. The one
21
+ * case that needs care is a config that both needs a `rootDir` and is itself an
22
+ * `extends` base: its own-directory children would inherit the new value and
23
+ * shift their emit layout, so each such child is pinned to its own directory to
24
+ * block it. Runs only on TypeScript 6 workspaces (gated by `requires`).
25
+ */
26
+ export default function (tree: Tree): Promise<void>;
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const node_path_1 = require("node:path");
6
+ const jsonc_parser_1 = require("jsonc-parser");
7
+ const ensure_typescript_1 = require("../../utils/typescript/ensure-typescript");
8
+ const FORMATTING_OPTIONS = {
9
+ formattingOptions: { keepLines: true, insertSpaces: true, tabSize: 2 },
10
+ };
11
+ let tsModule;
12
+ /**
13
+ * TypeScript 6.0 changed the implicit `rootDir`. Before 6.0 an unset `rootDir`
14
+ * was inferred as the common directory of a program's non-declaration input
15
+ * files; 6.0 defaults it to the tsconfig's own directory instead. A program
16
+ * whose files resolve outside that directory (most commonly a spec/e2e tsconfig
17
+ * importing another project's source through a `paths` alias) then hard-fails
18
+ * with TS5011 or TS6059.
19
+ *
20
+ * For every project `tsconfig*.json` that lacks an explicit `rootDir`, this pins
21
+ * `rootDir` to exactly the directory TypeScript 5 would have inferred, so both
22
+ * compilation and emit layout are unchanged under 6.0. The value is computed by
23
+ * the compiler itself: a throwaway program built with `configFilePath` cleared
24
+ * takes the file-derived branch of `getCommonSourceDirectory`, so it matches
25
+ * `tsc` exactly, including project-reference redirects. Configs already at the
26
+ * 6.0 default are left untouched: their inferred directory equals the tsconfig
27
+ * directory, or they are composite (which defaulted there before 6.0 too).
28
+ *
29
+ * Each config is written on its own; nothing is written to a shared `extends`
30
+ * base, so a config never inherits a `rootDir` computed for a sibling. The one
31
+ * case that needs care is a config that both needs a `rootDir` and is itself an
32
+ * `extends` base: its own-directory children would inherit the new value and
33
+ * shift their emit layout, so each such child is pinned to its own directory to
34
+ * block it. Runs only on TypeScript 6 workspaces (gated by `requires`).
35
+ */
36
+ async function default_1(tree) {
37
+ tsModule ??= (0, ensure_typescript_1.ensureTypescript)();
38
+ const ts = tsModule;
39
+ const parseConfigHost = {
40
+ ...ts.sys,
41
+ onUnRecoverableConfigFileDiagnostic: () => { },
42
+ };
43
+ const relPaths = collectProjectTsconfigs(tree);
44
+ // Phase 1: analyze every candidate against the on-disk configs.
45
+ const candidates = relPaths.map((relPath) => {
46
+ const absPath = toPosix((0, node_path_1.join)(tree.root, relPath));
47
+ return {
48
+ relPath,
49
+ dirAbs: toPosix((0, node_path_1.dirname)(absPath)),
50
+ analysis: analyze(ts, absPath, parseConfigHost),
51
+ };
52
+ });
53
+ // Phase 2: pin each config that needs a `rootDir` on its own.
54
+ let changed = 0;
55
+ for (const c of candidates) {
56
+ if (c.analysis.kind === 'write') {
57
+ const rootDir = toRelativeRootDir(c.dirAbs, c.analysis.dirAbs);
58
+ if (setRootDir(tree, c.relPath, rootDir)) {
59
+ changed++;
60
+ }
61
+ }
62
+ }
63
+ // Phase 3: shield own-dir configs that now inherit a pinned `rootDir` from a
64
+ // base. Re-parse each from the tree (so Phase 2's writes are visible); when
65
+ // TypeScript reports an inherited `rootDir` where there was none, pin the
66
+ // config to its own directory so its emit layout does not shift. Repeat until
67
+ // stable, since shielding one config can turn it into a base for another. The
68
+ // tree-backed host lets TypeScript resolve `extends`; the `readDirectory`
69
+ // no-op skips the unused file scan.
70
+ const readFile = (filePath) => tree.read(filePath, 'utf-8') ?? undefined;
71
+ const treeParseHost = {
72
+ ...ts.sys,
73
+ readFile,
74
+ readDirectory: () => [],
75
+ };
76
+ const shielded = new Set();
77
+ let added = true;
78
+ while (added) {
79
+ added = false;
80
+ for (const c of candidates) {
81
+ if (c.analysis.kind !== 'own-dir' || shielded.has(c.relPath)) {
82
+ continue;
83
+ }
84
+ if (inheritsRootDir(ts, treeParseHost, readFile, c.relPath)) {
85
+ if (setRootDir(tree, c.relPath, '.')) {
86
+ changed++;
87
+ }
88
+ shielded.add(c.relPath);
89
+ added = true;
90
+ }
91
+ }
92
+ }
93
+ if (changed > 0) {
94
+ await (0, devkit_1.formatFiles)(tree);
95
+ }
96
+ }
97
+ function analyze(ts, absPath, parseConfigHost) {
98
+ const parsed = ts.getParsedCommandLineOfConfigFile(absPath, undefined, parseConfigHost);
99
+ if (!parsed) {
100
+ return { kind: 'inert' };
101
+ }
102
+ const { options, fileNames, projectReferences } = parsed;
103
+ if (options.rootDir != null) {
104
+ return { kind: 'has-rootDir' };
105
+ }
106
+ if (!setsEmitGate(options) || fileNames.length === 0) {
107
+ return { kind: 'inert' };
108
+ }
109
+ // Composite already defaults `rootDir` to the tsconfig's own directory in both
110
+ // 5.x and 6.0, so the 6.0 change leaves it alone. Treat it as own-dir: never
111
+ // pin a file-derived value, and shield it from a `rootDir` this migration pins
112
+ // on an `extends` base.
113
+ if (options.composite) {
114
+ return { kind: 'own-dir' };
115
+ }
116
+ const commonDir = computeCommonSourceDirectory(ts, fileNames, options, projectReferences);
117
+ if (!commonDir) {
118
+ return { kind: 'inert' };
119
+ }
120
+ if (equalPaths(ts, commonDir, (0, node_path_1.dirname)(absPath))) {
121
+ return { kind: 'own-dir' };
122
+ }
123
+ return { kind: 'write', dirAbs: toPosix(commonDir).replace(/\/+$/, '') };
124
+ }
125
+ // True when TypeScript resolves an inherited `rootDir` for the config. An own-dir
126
+ // config sets none of its own, so any `rootDir` reported here comes from a base
127
+ // this migration just pinned, and inheriting it would shift the config's emit
128
+ // layout.
129
+ function inheritsRootDir(ts, parseHost, readFile, relPath) {
130
+ const config = ts.readConfigFile(relPath, readFile).config;
131
+ if (!config) {
132
+ return false;
133
+ }
134
+ const { options } = ts.parseJsonConfigFileContent(config, parseHost, (0, node_path_1.dirname)(relPath));
135
+ return options.rootDir != null;
136
+ }
137
+ // Any of these makes TypeScript run the source-file containment / common-source
138
+ // computation that emits TS5011 / TS6059 when `rootDir` is absent.
139
+ function setsEmitGate(o) {
140
+ return !!(o.outDir ||
141
+ o.outFile ||
142
+ o.sourceRoot ||
143
+ o.mapRoot ||
144
+ (o.declaration && o.declarationDir));
145
+ }
146
+ function computeCommonSourceDirectory(ts, rootNames, parsedOptions, projectReferences) {
147
+ // Clearing `configFilePath` makes `getCommonSourceDirectory` take the
148
+ // file-derived branch (the pre-6.0 inference) instead of returning the
149
+ // tsconfig's own directory. `noLib` skips lib.d.ts (declarations, irrelevant
150
+ // to the calc). `projectReferences` preserves reference-redirect semantics, so
151
+ // a source file provided by a referenced project is excluded via its `.d.ts`.
152
+ const options = {
153
+ ...parsedOptions,
154
+ noLib: true,
155
+ configFilePath: undefined,
156
+ };
157
+ // A fresh host per program so each config resolves on its own, with no parse or
158
+ // module-resolution state shared across configs. Letting the program resolve
159
+ // modules itself (no custom `resolveModuleNameLiterals`) keeps resolution
160
+ // mode-aware under node16/nodenext/bundler, matching tsc.
161
+ const host = ts.createCompilerHost(options, /* setParentNodes */ false);
162
+ const program = ts.createProgram({
163
+ rootNames,
164
+ options,
165
+ host,
166
+ projectReferences,
167
+ });
168
+ // `getCommonSourceDirectory` is a real Program method but is absent from the
169
+ // public `Program` type, so widen it here.
170
+ const commonSourceDirectory = program.getCommonSourceDirectory();
171
+ return commonSourceDirectory || undefined;
172
+ }
173
+ function collectProjectTsconfigs(tree) {
174
+ const projectRoots = [...(0, devkit_1.getProjects)(tree).values()].map((p) => p.root === '.' ? '' : p.root);
175
+ const tsconfigs = [];
176
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
177
+ const name = (0, node_path_1.basename)(filePath);
178
+ if (!name.startsWith('tsconfig') || !name.endsWith('.json')) {
179
+ return;
180
+ }
181
+ const normalized = toPosix(filePath);
182
+ // Only touch tsconfigs owned by a project; never a workspace-root base
183
+ // whose (unknown) extenders we would silently affect.
184
+ if (projectRoots.some((root) => root === '' || normalized.startsWith(`${root}/`))) {
185
+ tsconfigs.push(filePath);
186
+ }
187
+ });
188
+ return tsconfigs;
189
+ }
190
+ function setRootDir(tree, tsconfigPath, rootDir) {
191
+ const contents = tree.read(tsconfigPath, 'utf-8');
192
+ if (!contents) {
193
+ return false;
194
+ }
195
+ const edits = (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'rootDir'], rootDir, FORMATTING_OPTIONS);
196
+ if (edits.length === 0) {
197
+ return false;
198
+ }
199
+ tree.write(tsconfigPath, (0, jsonc_parser_1.applyEdits)(contents, edits));
200
+ return true;
201
+ }
202
+ function toRelativeRootDir(tsconfigDir, commonDir) {
203
+ const rel = node_path_1.posix.relative(toPosix(tsconfigDir), toPosix(commonDir));
204
+ return rel === '' ? '.' : rel;
205
+ }
206
+ function equalPaths(ts, a, b) {
207
+ const normalize = (p) => {
208
+ const stripped = toPosix(p).replace(/\/+$/, '');
209
+ return ts.sys.useCaseSensitiveFileNames ? stripped : stripped.toLowerCase();
210
+ };
211
+ return normalize(a) === normalize(b);
212
+ }
213
+ function toPosix(p) {
214
+ return p.split('\\').join('/');
215
+ }
@@ -0,0 +1,36 @@
1
+ #### Pin the Inferred `rootDir` for TypeScript 6
2
+
3
+ Before TypeScript 6, a tsconfig that did not set `rootDir` had it inferred as the common directory of the program's non-declaration input files. TypeScript 6 changed that default to the tsconfig's own directory. A program whose files resolve outside that directory (most commonly a spec or e2e tsconfig that imports another project's source through a `paths` alias) now hard-fails with `TS5011` or `TS6059` because a file falls outside the assumed root.
4
+
5
+ For every project `tsconfig*.json` that does not already set `rootDir` (directly or through `extends`), this migration pins `rootDir` to exactly the directory TypeScript 5 would have inferred, so both compilation and emit layout stay the same under TypeScript 6. The value is computed by the TypeScript compiler itself, so it matches `tsc` exactly, including project-reference redirects. Configs that cannot hit the error are skipped: ones without an output option (`outDir`, `outFile`, `sourceRoot`, `mapRoot`, or `declaration` + `declarationDir`), ones with no input files, composite projects (whose `rootDir` already defaulted to the tsconfig directory before TypeScript 6), and ones whose inferred directory already equals the tsconfig directory. The migration only runs when the workspace is on TypeScript 6.
6
+
7
+ Each config is updated on its own; the migration never writes `rootDir` to a shared `extends` base, so a config never inherits a value computed for a sibling. The one case that needs care is a config that both needs a `rootDir` and is itself an `extends` base: its self-contained children would inherit the new value and emit to the wrong place, so each such child is pinned to its own directory to keep its layout. This includes a composite child, whose own-directory default would otherwise be overridden by the inherited value.
8
+
9
+ #### Sample Code Changes
10
+
11
+ ##### Before
12
+
13
+ A `libs/products/e2e/tsconfig.json` whose specs import a shared library's source:
14
+
15
+ ```json title="libs/products/e2e/tsconfig.json"
16
+ {
17
+ "extends": "../../../tsconfig.base.json",
18
+ "compilerOptions": {
19
+ "outDir": "../../../dist/out-tsc"
20
+ },
21
+ "include": ["src/**/*.ts"]
22
+ }
23
+ ```
24
+
25
+ ##### After
26
+
27
+ ```json title="libs/products/e2e/tsconfig.json" {4}
28
+ {
29
+ "extends": "../../../tsconfig.base.json",
30
+ "compilerOptions": {
31
+ "rootDir": "../../..",
32
+ "outDir": "../../../dist/out-tsc"
33
+ },
34
+ "include": ["src/**/*.ts"]
35
+ }
36
+ ```
@@ -282,7 +282,15 @@ function updatePaths(dependencies, paths) {
282
282
  const { root } = dep.node.data;
283
283
  // Update nested mappings to point to the dependency's output paths
284
284
  mappedPaths = mappedPaths.concat(paths[path].flatMap((p) => dep.outputs.flatMap((output) => {
285
- const basePath = p.replace(root, output);
285
+ // Re-map the root prefix to the output. Match root only as a
286
+ // leading segment (after an optional `./`) so a root that
287
+ // also appears later in the value (e.g. output `dist/libs/base`
288
+ // for root `base`) isn't doubled.
289
+ const dotPrefix = p.startsWith('./') ? './' : '';
290
+ const value = dotPrefix ? p.slice(2) : p;
291
+ const basePath = value === root || value.startsWith(`${root}/`)
292
+ ? `${dotPrefix}${output}${value.slice(root.length)}`
293
+ : p;
286
294
  return [
287
295
  // extension-less path to support compiled output
288
296
  basePath.replace(new RegExp(`${(0, path_1.extname)(basePath)}$`, 'gi'), ''),
@@ -5,14 +5,23 @@ exports.generatePrettierSetup = generatePrettierSetup;
5
5
  exports.resolvePrettierConfigPath = resolvePrettierConfigPath;
6
6
  const devkit_1 = require("@nx/devkit");
7
7
  const versions_1 = require("./versions");
8
- async function resolveUserExistingPrettierConfig() {
9
- let prettier;
8
+ // Prettier v3 (ESM) exposes its API as named exports; v2 (CJS) exposes it under
9
+ // `.default` when loaded via `import()`. Return whichever carries the API, or
10
+ // null if prettier isn't installed.
11
+ async function importPrettier() {
10
12
  try {
11
- prettier = await import('prettier');
13
+ const imported = await import('prettier');
14
+ return (imported.resolveConfig ? imported : imported.default);
12
15
  }
13
16
  catch {
14
17
  return null;
15
18
  }
19
+ }
20
+ async function resolveUserExistingPrettierConfig() {
21
+ const prettier = await importPrettier();
22
+ if (!prettier) {
23
+ return null;
24
+ }
16
25
  try {
17
26
  const filepath = await prettier.resolveConfigFile();
18
27
  if (!filepath) {
@@ -76,18 +85,13 @@ function generatePrettierSetup(tree, options) {
76
85
  : (0, devkit_1.addDependenciesToPackageJson)(tree, {}, { prettier: versions_1.prettierVersion });
77
86
  }
78
87
  async function resolvePrettierConfigPath(tree) {
79
- let prettier;
80
- try {
81
- prettier = await import('prettier');
82
- }
83
- catch {
88
+ const prettier = await importPrettier();
89
+ if (!prettier) {
84
90
  return null;
85
91
  }
86
- if (prettier) {
87
- const filePath = await prettier.resolveConfigFile();
88
- if (filePath) {
89
- return filePath;
90
- }
92
+ const configFilePath = await prettier.resolveConfigFile();
93
+ if (configFilePath) {
94
+ return configFilePath;
91
95
  }
92
96
  if (!tree) {
93
97
  return null;
@@ -44,7 +44,10 @@ function getTypeCheckOptions(normalizedOptions) {
44
44
  };
45
45
  if (watch) {
46
46
  typeCheckOptions.incremental = true;
47
- typeCheckOptions.cacheDir = devkit_1.cacheDir;
47
+ // Scope the incremental .tsbuildinfo per project (in its own subdir, not
48
+ // alongside Nx's cache files) so concurrent watch type-checks don't collide
49
+ // on a single file.
50
+ typeCheckOptions.cacheDir = (0, node_path_1.join)(devkit_1.cacheDir, 'swc', projectRoot);
48
51
  }
49
52
  if (normalizedOptions.isTsSolutionSetup && normalizedOptions.skipTypeCheck) {
50
53
  typeCheckOptions.ignoreDiagnostics = true;
@@ -43,6 +43,10 @@ async function runTypeCheck(options) {
43
43
  options: {
44
44
  ...compilerOptions,
45
45
  incremental: true,
46
+ // Set after the spread so it overrides any user-set tsBuildInfoFile.
47
+ // This is a dedicated type-check program with Nx-injected options that
48
+ // differ from the real build, so its build info must not share a file
49
+ // with the build, or it corrupts the build's incremental cache.
46
50
  tsBuildInfoFile: path.join(cacheDir, '.tsbuildinfo'),
47
51
  },
48
52
  });
package/migrations.json CHANGED
@@ -30,13 +30,22 @@
30
30
  "documentation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.md"
31
31
  },
32
32
  "23-1-0-add-ignore-deprecations-for-ts6": {
33
- "version": "23.1.0-beta.0",
34
- "description": "Adds `\"ignoreDeprecations\": \"6.0\"` to tsconfig files whose compilerOptions (or ts-node.compilerOptions) directly carry a TypeScript 6 deprecated option value (e.g. moduleResolution node/node10/classic, baseUrl, target es5, esModuleInterop false, outFile, module amd/umd/system/none, alwaysStrict false, allowSyntheticDefaultImports false, downlevelIteration set). Also pins `\"strict\": false` and `\"noUncheckedSideEffectImports\": false` in chain-root tsconfigs (no \"extends\") that lack each key, preserving pre-TS6 behavior where their defaults were less strict.",
33
+ "version": "23.1.0-beta.8",
34
+ "description": "Adds `\"ignoreDeprecations\": \"6.0\"` to tsconfig files whose compilerOptions (or ts-node.compilerOptions) carry a TypeScript 6 deprecated option value, set directly or inherited through `extends` including from a base this migration does not edit (e.g. moduleResolution node/node10/classic, baseUrl, target es5, esModuleInterop false, outFile, module amd/umd/system/none, alwaysStrict false, allowSyntheticDefaultImports false, downlevelIteration set). Also pins `\"strict\": false`, `\"noUncheckedSideEffectImports\": false`, `\"types\": [\"*\"]`, and `\"esModuleInterop\": false` in chain-root tsconfigs (no \"extends\") that lack each key, preserving pre-TS6 behavior where these defaults changed; `esModuleInterop` false is itself deprecated (removed in TS7) so it also receives `ignoreDeprecations`, deferring the interop change to the TS7 migration. Also adds `ignoreDeprecations` to every `tsconfig.json` (the exact name jest/ts-node auto-resolve when compiling a config file; ts-node injects a deprecated default `target: es5` there, so the load hits a TS6 error even on a clean config), even one with no deprecated value of its own; a stale local `ignoreDeprecations` (e.g. `5.0`) that would otherwise override the inherited flag is upgraded.",
35
35
  "requires": {
36
36
  "typescript": ">=6.0.0"
37
37
  },
38
38
  "factory": "./dist/src/migrations/update-23-1-0/add-ignore-deprecations-for-ts6",
39
39
  "documentation": "./dist/src/migrations/update-23-1-0/add-ignore-deprecations-for-ts6.md"
40
+ },
41
+ "23-1-0-set-tsconfig-root-dir-for-ts6": {
42
+ "version": "23.1.0-beta.8",
43
+ "description": "Sets an explicit `rootDir` on project `tsconfig*.json` files that lack one, pinned to the source directory TypeScript 5 inferred implicitly, so programs whose input files span more than one directory (for example a spec tsconfig importing another project's source through a `paths` alias) keep compiling under TypeScript 6, which otherwise hard-fails with TS5011 or TS6059. The value is computed by the compiler, so emit layout is unchanged. Each config is written on its own; the migration never writes to a shared `extends` base, so a config never inherits a value computed for a sibling. Composite projects are not given an inferred `rootDir`, since it already defaulted to the tsconfig directory before TypeScript 6; a composite that extends a base receiving a pinned `rootDir` is itself pinned to its own directory to preserve that default.",
44
+ "requires": {
45
+ "typescript": ">=6.0.0"
46
+ },
47
+ "factory": "./dist/src/migrations/update-23-1-0/set-tsconfig-root-dir-for-ts6",
48
+ "documentation": "./dist/src/migrations/update-23-1-0/set-tsconfig-root-dir-for-ts6.md"
40
49
  }
41
50
  },
42
51
  "packageJsonUpdates": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/js",
3
- "version": "23.1.0-pr.36127.8b9019b",
3
+ "version": "23.1.0-rc.0",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "files": [
@@ -144,11 +144,11 @@
144
144
  "source-map-support": "0.5.19",
145
145
  "tinyglobby": "^0.2.12",
146
146
  "tslib": "^2.3.0",
147
- "@nx/workspace": "23.1.0-pr.36127.8b9019b",
148
- "@nx/devkit": "23.1.0-pr.36127.8b9019b"
147
+ "@nx/workspace": "23.1.0-rc.0",
148
+ "@nx/devkit": "23.1.0-rc.0"
149
149
  },
150
150
  "devDependencies": {
151
- "nx": "23.1.0-pr.36127.8b9019b"
151
+ "nx": "23.1.0-rc.0"
152
152
  },
153
153
  "peerDependencies": {
154
154
  "@swc/cli": ">=0.6.0 <0.9.0",