UNPKG

@nx/js

Version:

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

184 lines (183 loc) 8.58 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 FORMATTING_OPTIONS = { formattingOptions: { keepLines: true, insertSpaces: true, tabSize: 2 }, }; let tsModule; /** * TypeScript 6.0 changed the implicit `rootDir`. Before 6.0 an unset `rootDir` * was inferred as the common directory of a program's non-declaration input * files; 6.0 defaults it to the tsconfig's own directory instead. A program * whose files resolve outside that directory (most commonly a spec/e2e tsconfig * importing another project's source through a `paths` alias) then hard-fails * with TS5011 or TS6059. * * For every project `tsconfig*.json` that lacks an explicit `rootDir`, this pins * `rootDir` to exactly the directory TypeScript 5 would have inferred, so both * compilation and emit layout are unchanged under 6.0. The value is computed by * the compiler itself: a throwaway program built with `configFilePath` cleared * takes the file-derived branch of `getCommonSourceDirectory`, so it matches * `tsc` exactly, including project-reference redirects. The pin is written even * when the inferred directory already equals the tsconfig directory: inference * is per-program, and tools like ts-jest's `isolatedModules` compile single * files against the same config, collapsing the common directory below the * config dir and failing with TS5011. * * Composite configs are pinned too, to the tsconfig's own directory. Under `tsc` * a composite `rootDir` already defaults there (for any file subset), so `"."` * is a no-op — but ts-jest strips `composite` for its per-file transpile, and * TypeScript 6 only exempts genuinely-composite programs from the containment * check (`!options.composite`), so a composite spec config compiled by ts-jest * hits TS5011 all the same. The explicit `"."` survives the strip. Pinning the * own directory (not a deeper file-derived value) keeps a real composite build's * emit layout unchanged. * * Each config is written on its own; nothing is written to a shared `extends` * base, so a config never inherits a `rootDir` computed for a sibling — and * because every config that emits is given its own explicit `rootDir`, none * inherits a value pinned on a base. Runs only on TypeScript 6 workspaces * (gated by `requires`). */ async function default_1(tree) { tsModule ??= (0, ensure_typescript_1.ensureTypescript)(); const ts = tsModule; const parseConfigHost = { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => { }, }; const relPaths = collectProjectTsconfigs(tree); // Phase 1: analyze every candidate against the on-disk configs. const candidates = relPaths.map((relPath) => { const absPath = toPosix((0, node_path_1.join)(tree.root, relPath)); return { relPath, dirAbs: toPosix((0, node_path_1.dirname)(absPath)), analysis: analyze(ts, absPath, parseConfigHost), }; }); // Phase 2: pin each config that needs a `rootDir` on its own. Every config // that emits is given its own explicit value here — including composites, // pinned to their own directory — so none is left to inherit a `rootDir` this // migration pins on a base. let changed = 0; for (const c of candidates) { if (c.analysis.kind === 'write') { const rootDir = toRelativeRootDir(c.dirAbs, c.analysis.dirAbs); if (setRootDir(tree, c.relPath, rootDir)) { changed++; } } } if (changed > 0) { await (0, devkit_1.formatFiles)(tree); } } function analyze(ts, absPath, parseConfigHost) { const parsed = ts.getParsedCommandLineOfConfigFile(absPath, undefined, parseConfigHost); if (!parsed) { return { kind: 'inert' }; } const { options, fileNames, projectReferences } = parsed; if (options.rootDir != null) { return { kind: 'has-rootDir' }; } if (!setsEmitGate(options) || fileNames.length === 0) { return { kind: 'inert' }; } // Composite: pin the tsconfig's own directory, not a file-derived value. // `rootDir` already defaults there under `tsc`, so `"."` preserves a real // composite build's emit layout — but ts-jest strips `composite` for its // per-file transpile, and TS6 only skips the containment check for genuinely // composite programs, so an unpinned composite spec config still fails with // TS5011 under ts-jest. The explicit pin survives the strip. if (options.composite) { return { kind: 'write', dirAbs: toPosix((0, node_path_1.dirname)(absPath)) }; } const commonDir = computeCommonSourceDirectory(ts, fileNames, options, projectReferences); if (!commonDir) { return { kind: 'inert' }; } // Written even when `commonDir` is the tsconfig's own directory (the 6.0 // default): a tool compiling a subset of the config's files — ts-jest with // `isolatedModules` builds a program per test file — re-infers the common // directory from that subset alone, and without an explicit `rootDir` TS6 // fails with TS5011. return { kind: 'write', dirAbs: toPosix(commonDir).replace(/\/+$/, '') }; } // Any of these makes TypeScript run the source-file containment / common-source // computation that emits TS5011 / TS6059 when `rootDir` is absent. function setsEmitGate(o) { return !!(o.outDir || o.outFile || o.sourceRoot || o.mapRoot || (o.declaration && o.declarationDir)); } function computeCommonSourceDirectory(ts, rootNames, parsedOptions, projectReferences) { // Clearing `configFilePath` makes `getCommonSourceDirectory` take the // file-derived branch (the pre-6.0 inference) instead of returning the // tsconfig's own directory. `noLib` skips lib.d.ts (declarations, irrelevant // to the calc). `projectReferences` preserves reference-redirect semantics, so // a source file provided by a referenced project is excluded via its `.d.ts`. const options = { ...parsedOptions, noLib: true, configFilePath: undefined, }; // A fresh host per program so each config resolves on its own, with no parse or // module-resolution state shared across configs. Letting the program resolve // modules itself (no custom `resolveModuleNameLiterals`) keeps resolution // mode-aware under node16/nodenext/bundler, matching tsc. const host = ts.createCompilerHost(options, /* setParentNodes */ false); const program = ts.createProgram({ rootNames, options, host, projectReferences, }); // `getCommonSourceDirectory` is a real Program method but is absent from the // public `Program` type, so widen it here. const commonSourceDirectory = program.getCommonSourceDirectory(); return commonSourceDirectory || undefined; } function collectProjectTsconfigs(tree) { const projectRoots = [...(0, devkit_1.getProjects)(tree).values()].map((p) => p.root === '.' ? '' : p.root); const tsconfigs = []; (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => { const name = (0, node_path_1.basename)(filePath); if (!name.startsWith('tsconfig') || !name.endsWith('.json')) { return; } const normalized = toPosix(filePath); // Only touch tsconfigs owned by a project; never a workspace-root base // whose (unknown) extenders we would silently affect. if (projectRoots.some((root) => root === '' || normalized.startsWith(`${root}/`))) { tsconfigs.push(filePath); } }); return tsconfigs; } function setRootDir(tree, tsconfigPath, rootDir) { const contents = tree.read(tsconfigPath, 'utf-8'); if (!contents) { return false; } const edits = (0, jsonc_parser_1.modify)(contents, ['compilerOptions', 'rootDir'], rootDir, FORMATTING_OPTIONS); if (edits.length === 0) { return false; } tree.write(tsconfigPath, (0, jsonc_parser_1.applyEdits)(contents, edits)); return true; } function toRelativeRootDir(tsconfigDir, commonDir) { const rel = node_path_1.posix.relative(toPosix(tsconfigDir), toPosix(commonDir)); return rel === '' ? '.' : rel; } function toPosix(p) { return p.split('\\').join('/'); }