UNPKG

@nx/detox

Version:

The Nx Plugin for Detox contains executors and generators for allowing your workspace to use the powerful Detox integration testing capabilities.

224 lines (223 loc) 9.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = migrateCreateNodesV2ToCreateNodes; exports.rewriteCreateNodesV2Imports = rewriteCreateNodesV2Imports; const devkit_1 = require("@nx/devkit"); const TS_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts']; const DEPRECATED_NAME = 'createNodesV2'; const CANONICAL_NAME = 'createNodes'; // Module specifiers from which `@nx/detox` publicly exposes `createNodesV2`. // A named import or re-export of `createNodesV2` from one of these is rewritten // to the canonical `createNodes` export. const TARGET_SPECIFIERS = new Set(['@nx/detox/plugin']); let ts; async function migrateCreateNodesV2ToCreateNodes(tree) { let touchedCount = 0; (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => { if (!TS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) { return; } const original = tree.read(filePath, 'utf-8'); if (!original || !original.includes(DEPRECATED_NAME)) { return; } const updated = rewriteCreateNodesV2Imports(original, TARGET_SPECIFIERS); if (updated !== original) { tree.write(filePath, updated); touchedCount += 1; } }); if (touchedCount > 0) { devkit_1.logger.info(`Renamed \`${DEPRECATED_NAME}\` imports to \`${CANONICAL_NAME}\` in ${touchedCount} file(s).`); } await (0, devkit_1.formatFiles)(tree); } /** * Rewrites named imports and re-exports of `createNodesV2` to `createNodes` * when they come from one of the given module specifiers. Only the named * bindings are touched — the module specifier, the `import`/`export` keyword, * any `type` modifier, and any default import are left untouched. */ function rewriteCreateNodesV2Imports(source, specifiers) { ts ??= (0, devkit_1.ensurePackage)('typescript', '*'); const sourceFile = ts.createSourceFile('tmp.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true, ts.ScriptKind.TSX); const changes = []; let renameLocalUsages = false; for (const stmt of sourceFile.statements) { if (ts.isImportDeclaration(stmt)) { renameLocalUsages = collectImportRewrite(sourceFile, stmt, specifiers, changes) || renameLocalUsages; } else if (ts.isExportDeclaration(stmt)) { collectExportRewrite(sourceFile, stmt, specifiers, changes); } } // Renaming a local `createNodesV2` import binding to `createNodes` (a lone // `{ createNodesV2 }`, or one deduped against an existing `createNodes`) // changes the name in scope, so value references to `createNodesV2` in the // file body must be renamed too — otherwise they dangle. Aliased imports and // re-exports keep their local name, so they never trigger this. if (renameLocalUsages) { collectValueUsageRewrites(sourceFile, changes); } return changes.length > 0 ? (0, devkit_1.applyChangesToString)(source, changes) : source; } function isTargetSpecifier(node, specifiers) { return ts.isStringLiteral(node) && specifiers.has(node.text); } function collectImportRewrite(sourceFile, stmt, specifiers, changes) { if (!isTargetSpecifier(stmt.moduleSpecifier, specifiers)) { return false; } const namedBindings = stmt.importClause?.namedBindings; // Only `import { ... }` carries renameable named bindings. `import x`, // `import * as ns`, and side-effect imports reference the module wholesale // and keep working through the `createNodesV2` runtime alias, so we leave // them be. A mixed `import def, { createNodesV2 }` still has its named // bindings rewritten below — the default binding is untouched. if (!namedBindings || !ts.isNamedImports(namedBindings)) { return false; } // The local `createNodesV2` binding only disappears when it is imported // without an alias — a lone `{ createNodesV2 }` or one deduped against an // existing `createNodes`. `{ createNodesV2 as x }` keeps the local `x`, so // its in-file usages are unaffected and must not be rewritten. const localBindingRenamed = namedBindings.elements.some((el) => el.name.text === DEPRECATED_NAME && (el.propertyName ?? el.name).text === DEPRECATED_NAME); rewriteNamedBindings(sourceFile, namedBindings, changes); return localBindingRenamed; } function collectExportRewrite(sourceFile, stmt, specifiers, changes) { if (!stmt.moduleSpecifier || !isTargetSpecifier(stmt.moduleSpecifier, specifiers)) { return; } // `export { ... } from '...'` can be rewritten; `export * from '...'` has no // named bindings to rename. if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) { return; } rewriteNamedBindings(sourceFile, stmt.exportClause, changes); } /** * Re-renders the `{ ... }` of a named import/export, renaming any * `createNodesV2` specifier to `createNodes`. If renaming would collide with a * `createNodes` that is already present (e.g. `{ createNodes, createNodesV2 }`), * the duplicate is dropped. Returns without recording a change when the binding * list contains no `createNodesV2`. */ function rewriteNamedBindings(sourceFile, namedBindings, changes) { const elements = namedBindings.elements; const hasDeprecated = elements.some((el) => (el.propertyName ?? el.name).text === DEPRECATED_NAME); if (!hasDeprecated) { 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: devkit_1.ChangeType.Delete, start, length: namedBindings.getEnd() - start, }, { type: devkit_1.ChangeType.Insert, index: start, text: `{ ${rendered.join(', ')} }`, }); } function renderSpecifier(el) { const typePrefix = el.isTypeOnly ? 'type ' : ''; const rename = (name) => name === DEPRECATED_NAME ? CANONICAL_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}`; } /** * Renames value references of `createNodesV2` to `createNodes` in the file * body. Only called once a local `createNodesV2` import binding has actually * been renamed, so these references would otherwise dangle. Occurrences that * are not references to that binding are skipped: the import/export * declarations themselves, property accesses (`x.createNodesV2`), qualified * type names, object-literal keys, and declaration names that shadow the * import. A shorthand property (`{ createNodesV2 }`) is expanded to * `{ createNodesV2: createNodes }` so the property key is preserved. Strings * and comments are never `Identifier` nodes, so they are left alone. */ function collectValueUsageRewrites(sourceFile, changes) { const visit = (node) => { if (ts.isIdentifier(node) && node.text === DEPRECATED_NAME && isRenamableValueUsage(node)) { const start = node.getStart(sourceFile); changes.push({ type: devkit_1.ChangeType.Delete, start, length: node.getEnd() - start, }, { type: devkit_1.ChangeType.Insert, index: start, text: ts.isShorthandPropertyAssignment(node.parent) ? `${DEPRECATED_NAME}: ${CANONICAL_NAME}` : CANONICAL_NAME, }); } node.forEachChild(visit); }; sourceFile.forEachChild(visit); } /** * Whether a `createNodesV2` identifier is a value reference to the renamed * import binding, as opposed to a position that must be left untouched. */ function isRenamableValueUsage(node) { const parent = node.parent; if (!parent) { return false; } // Import/export bindings — already handled by the declaration rewrite. if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent)) { return false; } // `x.createNodesV2` / `X.createNodesV2` — a member name, not the binding. if (ts.isPropertyAccessExpression(parent) && parent.name === node) { return false; } if (ts.isQualifiedName(parent) && parent.right === node) { return false; } // `{ createNodesV2: ... }` — an object-literal key, not the binding. if (ts.isPropertyAssignment(parent) && parent.name === node) { return false; } // A declaration whose name shadows the import (variable, param, etc.). if ((ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isBindingElement(parent) || ts.isFunctionDeclaration(parent) || ts.isClassDeclaration(parent)) && parent.name === node) { return false; } return true; }