UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

266 lines (265 loc) • 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = default_1; const devkit_1 = require("@nx/devkit"); const node_path_1 = require("node:path"); const jsonc_parser_1 = require("jsonc-parser"); const ensure_typescript_1 = require("../../utils/typescript/ensure-typescript"); const ts_config_1 = require("../../utils/typescript/ts-config"); const FORMATTING_OPTIONS = { formattingOptions: { keepLines: true, insertSpaces: true, tabSize: 2 }, }; let tsModule; /** * Runs on TypeScript 6 workspaces (gated by `requires` in migrations.json) in * two passes over every `tsconfig*.json`. * * Pass 1 writes what pass 2's `extends` resolution reads back: * - Default-preserving pins: on every chain root (no `extends`) that is not a * pure solution container (`files: []` with no `include`), pin the TS6 * defaults that changed in a breaking way, but only where the root leaves * them unset: * - `strict: false`. TS6 treats an absent `strict` as true, TS5 as false. * - `noUncheckedSideEffectImports: false`. TS6 defaults it true, turning a * bare side-effect import of an asset without an ambient declaration * (`import './styles.css'`) into a hard TS2882; a semantic diagnostic, * not a deprecation, so `ignoreDeprecations` cannot silence it. * - `types: ["*"]`. TS6 loads no @types when `types` is unset (TS5 loaded * all); the wildcard restores that so a config relying on it (ts-node * type-checking jest.config.ts) keeps finding @types/node. * - `esModuleInterop: false`. TS6 flips the default false->true, changing * `import * as x from '<cjs>'` call semantics at runtime; false preserves * the pre-TS6 behavior. It is itself deprecated (removed in TS7), so pass * 2 silences it, deferring the interop change to the eventual TS7 work. * - Config-load flag: set `ignoreDeprecations: "6.0"` on every file named * exactly `tsconfig.json`, the name ts-node auto-resolves when jest compiles * a config file like jest.config.ts (walking up from the working directory, * not the config file's own directory). ts-node injects a * `target: es5` when the config leaves it unset, and es5 is a TS6-deprecated * value (TS5107); the flag (which ts-node passes through) keeps that load * silent. Set unconditionally, since any `tsconfig.json` is a potential * loader target, and inert when nothing is deprecated. It cannot silence a * module/moduleResolution mismatch (TS5110) when ts-node's forced * `module: commonjs` meets an inherited `nodenext` resolution. * * Pass 2 silences the remaining hard-deprecated values. For each config, * TypeScript resolves its `extends`-merged compiler options; when they carry a * value TS6 hard-deprecates (see `hasDeprecatedOption`) and their effective * `ignoreDeprecations` is not already `"6.0"`, add the flag. Because the check * runs on the merged options, it covers a value the config inherits from a base * this migration never edits, respects an inherited `"6.0"` (no redundant flag), * and upgrades a stale local `"5.0"` that overrides one. The * `ts-node.compilerOptions` overlay, which `tsc` does not merge, is checked on * its own. */ async function default_1(tree) { tsModule ??= (0, ensure_typescript_1.ensureTypescript)(); const ts = tsModule; // Tree-faithful host so TypeScript resolves `extends` against pass 1's pending // writes and against package-provided bases (a naive `tree.read` host misses // the latter). The host's `readDirectory` no-op skips the source-file scan, // since only the merged compiler options matter here, not the file list. const parseHost = (0, ts_config_1.createTreeParseConfigHost)(tree); const tsconfigPaths = await (0, devkit_1.globAsync)(tree, ['**/tsconfig*.json']); // Pass 1: pins and config-load flags, so pass 2's inheritance check sees them. let defaultsPinCount = 0; let configLoadCount = 0; for (const tsconfigPath of tsconfigPaths) { const { pinned, flagged } = applyDefaultsAndConfigLoadFlag(tree, tsconfigPath); if (pinned) defaultsPinCount += 1; if (flagged) configLoadCount += 1; } // Pass 2: silence a deprecated value the config carries or inherits. let deprecationCount = 0; for (const tsconfigPath of tsconfigPaths) { if (silenceDeprecations(tree, ts, parseHost, tsconfigPath)) { deprecationCount += 1; } } if (configLoadCount > 0) { 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.`); } if (deprecationCount > 0) { devkit_1.logger.info(`Added "ignoreDeprecations": "6.0" to ${deprecationCount} tsconfig file(s) carrying TS6-deprecated options.`); } if (defaultsPinCount > 0) { devkit_1.logger.info(`Pinned pre-TS6 compiler option defaults ("strict", "noUncheckedSideEffectImports", "types", "esModuleInterop") on ${defaultsPinCount} tsconfig chain root(s).`); } await (0, devkit_1.formatFiles)(tree); } // The pre-TS6 default values pinned on chain roots that leave them unset; pass 1 // above explains why each one changed in a breaking way. const DEFAULT_PRESERVING_PINS = [ ['strict', false], ['noUncheckedSideEffectImports', false], ['types', ['*']], ['esModuleInterop', false], ]; // Pass 1 for one file: pin pre-TS6 defaults on a chain root and set the // config-load flag on a `tsconfig.json`, combined into a single write. function applyDefaultsAndConfigLoadFlag(tree, tsconfigPath) { const original = tree.read(tsconfigPath, 'utf-8'); if (!original) { return { pinned: false, flagged: false }; } const root = (0, jsonc_parser_1.parseTree)(original); if (!root || root.type !== 'object') { return { pinned: false, flagged: false }; } const own = (0, jsonc_parser_1.getNodeValue)(root); const compilerOptions = own.compilerOptions; // A present-but-non-object compilerOptions can't receive keys; leave the file // alone so modify() doesn't throw and abort the whole migration. if (compilerOptions !== undefined && !isObject(compilerOptions)) { return { pinned: false, flagged: false }; } const options = (compilerOptions ?? {}); let contents = original; let pinned = false; let flagged = false; // Only chain roots; a file with "extends" inherits the defaults. Skip pure // solution containers ("files": [] and no "include"): they select no sources. const isSolutionContainer = Array.isArray(own.files) && own.files.length === 0 && !('include' in own); if (own.extends === undefined && !isSolutionContainer) { for (const [key, value] of DEFAULT_PRESERVING_PINS) { if (key in options) continue; // An explicit value means the user opted in. contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', key], value, FORMATTING_OPTIONS)); pinned = true; } } if ((0, node_path_1.basename)(tsconfigPath) === 'tsconfig.json' && options.ignoreDeprecations !== '6.0') { contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS)); flagged = true; } if (pinned || flagged) { tree.write(tsconfigPath, contents); } return { pinned, flagged }; } // Pass 2 for one file: silence a deprecated value it carries or inherits, in the // main block and the ts-node overlay, combined into a single write. function silenceDeprecations(tree, ts, parseHost, tsconfigPath) { const original = tree.read(tsconfigPath, 'utf-8'); if (!original) { return false; } const root = (0, jsonc_parser_1.parseTree)(original); if (!root || root.type !== 'object') { return false; } const own = (0, jsonc_parser_1.getNodeValue)(root); const mainBlock = own.compilerOptions; const mainIsObject = isObject(mainBlock); const mainFlag = mainIsObject ? mainBlock.ignoreDeprecations : undefined; const tsNodeBlock = isObject(own['ts-node']) ? own['ts-node'].compilerOptions : undefined; const tsNodeIsObject = isObject(tsNodeBlock); const tsNodeFlag = tsNodeIsObject ? tsNodeBlock.ignoreDeprecations : undefined; // No own options and no `extends` to inherit through, and no overlay to check: // nothing can be deprecated here, so skip without parsing. if (!mainIsObject && own.extends === undefined && !tsNodeIsObject) { return false; } // Main is already silenced and there is no overlay: nothing to do, skip the // parse entirely (the common case for a config-load-flagged `tsconfig.json`). if (mainFlag === '6.0' && !tsNodeIsObject) { return false; } // The `extends`-merged main options, normalized by TypeScript. Passing the // config object (not the path) keeps TypeScript from re-reading this file; it // reads only the ancestors. const mainParsed = ts.parseJsonConfigFileContent({ extends: own.extends, compilerOptions: mainIsObject ? mainBlock : {} }, parseHost, (0, node_path_1.dirname)(tsconfigPath)); // A base that neither the tree nor `tsc` can read leaves the merged options // incomplete, so a value it deprecates would be missed. The config does not // compile in that state anyway; surface it rather than silently guessing. if ((0, ts_config_1.extendsResolutionFailed)(mainParsed.errors)) { devkit_1.logger.warn(`Could not resolve the "extends" chain of "${tsconfigPath}"; a TypeScript 6 deprecated option inherited from the unresolved base may have been missed. Make sure the config compiles.`); } const mainOptions = mainParsed.options; const mainDeprecated = hasDeprecatedOption(ts, mainOptions); let contents = original; let changed = false; // Main block: silence a deprecated value it carries or inherits, unless an // effective "6.0" (own or inherited) already covers it. A stale local "5.0" // that overrides an inherited "6.0" shows through the merge and is upgraded. // An absent block is created; a present-but-non-object one is left alone so // modify() neither corrupts it nor throws. const mainWritable = mainBlock === undefined || mainIsObject; if (mainWritable && mainFlag !== '6.0' && mainDeprecated && mainOptions.ignoreDeprecations !== '6.0') { contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS)); changed = true; } // ts-node overlay: `tsc` does not merge it, so check it directly. ts-node // overlays the resolved main options at runtime; a "6.0" the main block just // received does not reliably reach the overlay across ts-node versions, so // silence it on its own whenever it or the resolved main carries a deprecated // value. if (tsNodeIsObject && tsNodeFlag !== '6.0') { const overlayOptions = ts.parseJsonConfigFileContent({ compilerOptions: tsNodeBlock }, parseHost, (0, node_path_1.dirname)(tsconfigPath)).options; if (hasDeprecatedOption(ts, overlayOptions) || mainDeprecated) { contents = (0, jsonc_parser_1.applyEdits)(contents, (0, jsonc_parser_1.modify)(contents, ['ts-node', 'compilerOptions', 'ignoreDeprecations'], '6.0', FORMATTING_OPTIONS)); changed = true; } } if (changed) { tree.write(tsconfigPath, contents); } return changed; } // Whether TypeScript-parsed, `extends`-merged compiler options carry a value // that compiles silently on TS 5.8 but is a hard deprecation error (TS5101/ // TS5107) on TS 6.0, derived by differential 5.8-vs-6.0 probing. Options are // read from the normalized enums TypeScript produces; an unset option stays // undefined (TypeScript does not fill in the module-derived `moduleResolution` // default here), so an implied resolution is not misread as deprecated. function hasDeprecatedOption(ts, options) { const moduleResolution = options.moduleResolution; if (moduleResolution === ts.ModuleResolutionKind.Node10 || moduleResolution === ts.ModuleResolutionKind.Classic) { return true; } if (options.baseUrl != null) { return true; } if (options.target === ts.ScriptTarget.ES5) { return true; } if (options.esModuleInterop === false) { return true; } if (options.outFile != null) { return true; } const module = options.module; if (module === ts.ModuleKind.AMD || module === ts.ModuleKind.UMD || module === ts.ModuleKind.System || module === ts.ModuleKind.None) { return true; } if (options.alwaysStrict === false) { return true; } if (options.allowSyntheticDefaultImports === false) { return true; } if (options.downlevelIteration != null) { return true; } return false; } function isObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }