@nx/detox
Version:
224 lines (223 loc) • 9.62 kB
JavaScript
;
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;
}