UNPKG

@nx/devkit

Version:

The Nx Devkit is used to customize Nx for different technologies and use cases. It contains many utility functions for reading and writing files, updating configuration, working with Abstract Syntax Trees(ASTs), and more. Learn more about [extending Nx by

200 lines (199 loc) 8.54 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CREATE_NODES_V2_TYPE_RENAMES = void 0; exports.default = renameCreateNodesV2Types; exports.rewriteCreateNodesV2Types = rewriteCreateNodesV2Types; const devkit_exports_1 = require("nx/src/devkit-exports"); const format_files_1 = require("../../generators/format-files"); const visit_not_ignored_files_1 = require("../../generators/visit-not-ignored-files"); const package_json_1 = require("../../utils/package-json"); const string_change_1 = require("../../utils/string-change"); const versions_1 = require("../../utils/versions"); const TS_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts']; const DEVKIT_SPECIFIER = '@nx/devkit'; // The `*V2` plugin types were renamed to their canonical un-suffixed names in // Nx 23. The `V2` names remain as `@deprecated` aliases, so this migration only // moves callers onto the canonical names. (`CreateNodesResultV2` is renamed to // `CreateNodesResultArray`, not `CreateNodesResult`, which is an unrelated type.) exports.CREATE_NODES_V2_TYPE_RENAMES = new Map([ ['CreateNodesV2', 'CreateNodes'], ['CreateNodesContextV2', 'CreateNodesContext'], ['CreateNodesResultV2', 'CreateNodesResultArray'], ['CreateNodesFunctionV2', 'CreateNodesFunction'], ['NxPluginV2', 'NxPlugin'], ]); let ts; async function renameCreateNodesV2Types(tree) { let touchedCount = 0; (0, visit_not_ignored_files_1.visitNotIgnoredFiles)(tree, '.', (filePath) => { if (!TS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) { return; } const original = tree.read(filePath, 'utf-8'); if (!original || !original.includes('V2')) { return; } const updated = rewriteCreateNodesV2Types(original); if (updated !== original) { tree.write(filePath, updated); touchedCount += 1; } }); if (touchedCount > 0) { devkit_exports_1.logger.info(`Renamed deprecated CreateNodes V2 type imports from @nx/devkit in ${touchedCount} file(s).`); } await (0, format_files_1.formatFiles)(tree); } /** * Rewrites named imports and re-exports of the deprecated `*V2` plugin types * from `@nx/devkit` to their canonical names. Only the named bindings are * touched — the module specifier, the `import`/`export` keyword, any `type` * modifier, and any default import are left untouched. */ function rewriteCreateNodesV2Types(source) { ts ??= (0, package_json_1.ensurePackage)('typescript', versions_1.typescriptVersion); const sourceFile = ts.createSourceFile('tmp.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true, ts.ScriptKind.TSX); const changes = []; // Local bindings whose name changes as a result of rewriting a non-aliased // import specifier (e.g. `import { CreateNodesV2 }` -> `import { CreateNodes }`). // Every in-body reference to such a binding must be renamed too, otherwise the // rewritten import leaves a dangling reference to the old name. const localRenames = new Map(); for (const stmt of sourceFile.statements) { if (ts.isImportDeclaration(stmt)) { collectImportRewrite(sourceFile, stmt, changes, localRenames); } else if (ts.isExportDeclaration(stmt)) { collectExportRewrite(sourceFile, stmt, changes); } } if (localRenames.size > 0) { collectUsageRewrites(sourceFile, localRenames, changes); } return changes.length > 0 ? (0, string_change_1.applyChangesToString)(source, changes) : source; } function isDevkitSpecifier(node) { return ts.isStringLiteral(node) && node.text === DEVKIT_SPECIFIER; } function collectImportRewrite(sourceFile, stmt, changes, localRenames) { if (!isDevkitSpecifier(stmt.moduleSpecifier)) { return; } const namedBindings = stmt.importClause?.namedBindings; if (!namedBindings || !ts.isNamedImports(namedBindings)) { return; } // A non-aliased specifier (`{ CreateNodesV2 }`) renames the local binding, so // its in-body references must be rewritten as well. An aliased specifier // (`{ CreateNodesV2 as Foo }`) keeps the local name `Foo`, so it does not. for (const el of namedBindings.elements) { if (el.propertyName) { continue; } const canonical = exports.CREATE_NODES_V2_TYPE_RENAMES.get(el.name.text); if (canonical) { localRenames.set(el.name.text, canonical); } } rewriteNamedBindings(sourceFile, namedBindings, changes); } /** * Renames in-body references (type annotations, value usages) of bindings that * were renamed by an import rewrite. References inside import/export * declarations are left to the binding rewrite, and member positions * (`foo.CreateNodesV2`, `NS.CreateNodesV2`) are not standalone references to the * imported binding, so they are skipped. */ function collectUsageRewrites(sourceFile, localRenames, changes) { const visit = (node) => { if (ts.isIdentifier(node) && localRenames.has(node.text) && isRenameableReference(node)) { const start = node.getStart(sourceFile); changes.push({ type: string_change_1.ChangeType.Delete, start, length: node.getEnd() - start }, { type: string_change_1.ChangeType.Insert, index: start, text: localRenames.get(node.text), }); } ts.forEachChild(node, visit); }; ts.forEachChild(sourceFile, visit); } function isRenameableReference(id) { const parent = id.parent; // `foo.CreateNodesV2` — the member name is not the imported binding. if (ts.isPropertyAccessExpression(parent) && parent.name === id) { return false; } // `NS.CreateNodesV2` in a type position — same reasoning. if (ts.isQualifiedName(parent) && parent.right === id) { return false; } // Anything inside an import/export declaration is handled by the binding // rewrite; skip it here to avoid touching the specifier twice. for (let n = id; n; n = n.parent) { if (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) { return false; } } return true; } function collectExportRewrite(sourceFile, stmt, changes) { if (!stmt.moduleSpecifier || !isDevkitSpecifier(stmt.moduleSpecifier)) { return; } if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) { return; } rewriteNamedBindings(sourceFile, stmt.exportClause, changes); } /** * Re-renders the `{ ... }` of a named import/export, renaming any deprecated * `*V2` type to its canonical name. If renaming would collide with a canonical * name that is already present, the duplicate is dropped. Returns without * recording a change when the binding list contains none of the renamed types. */ function rewriteNamedBindings(sourceFile, namedBindings, changes) { const elements = namedBindings.elements; const hasRenamed = elements.some((el) => exports.CREATE_NODES_V2_TYPE_RENAMES.has((el.propertyName ?? el.name).text)); if (!hasRenamed) { return; } const seen = new Set(); const rendered = []; for (const el of elements) { const text = renderSpecifier(el); if (!seen.has(text)) { seen.add(text); rendered.push(text); } } const start = namedBindings.getStart(sourceFile); changes.push({ type: string_change_1.ChangeType.Delete, start, length: namedBindings.getEnd() - start, }, { type: string_change_1.ChangeType.Insert, index: start, text: `{ ${rendered.join(', ')} }`, }); } function renderSpecifier(el) { const typePrefix = el.isTypeOnly ? 'type ' : ''; const rename = (name) => exports.CREATE_NODES_V2_TYPE_RENAMES.get(name) ?? name; // `{ name }` — no alias, so the local binding follows the rename. if (!el.propertyName) { return `${typePrefix}${rename(el.name.text)}`; } // `{ propertyName as name }` — only the imported (left) side is renamed; the // local alias is preserved. A now-redundant alias such as // `CreateNodesV2 as CreateNodes` collapses to `CreateNodes`. const canonicalImported = rename(el.propertyName.text); const localName = el.name.text; return canonicalImported === localName ? `${typePrefix}${localName}` : `${typePrefix}${canonicalImported} as ${localName}`; }