Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .claude/skills/run-nx-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,6 @@ nx generate @nx/workspace-plugin:bump-maven-version \

This automates all the version bumping instead of manual file edits.

### Creating a New Plugin

For creating a new create-nodes plugin:

```bash
nx generate @nx/workspace-plugin:create-nodes-plugin \
--name my-custom-plugin
```

## When to Use This Skill

Use this skill when you need to:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { dasherize, addPlugin } from '@nx/devkit/internal';

#### Fallback for non-named imports

For deep-import shapes that can't be split by symbol — default imports, namespace imports, side-effect imports, `require(...)` calls, and dynamic `import(...)` — the migration rewrites the specifier to `@nx/devkit/internal` as a best guess, since most symbols that previously lived under `@nx/devkit/src/...` ended up there.
For deep-import shapes that can't be split by symbol — default imports, namespace imports, side-effect imports, `require(...)` calls, dynamic `import(...)`, and `jest.mock(...)` / `vi.mock(...)`-style mock-helper calls — the migration rewrites the specifier to `@nx/devkit/internal` as a best guess, since most symbols that previously lived under `@nx/devkit/src/...` ended up there.

```ts
// Before
Expand All @@ -40,4 +40,4 @@ const { dasherize } = require('@nx/devkit/src/utils/string-utils');
const { dasherize } = require('@nx/devkit/internal');
```

If the symbol you're after is part of the stable public API instead, the rewritten import will fail to resolve against `@nx/devkit/internal` — switch it to `@nx/devkit` by hand.
If the symbol you're after is part of the stable public API instead, the rewritten import will fail to resolve against `@nx/devkit/internal` — switch it to `@nx/devkit` by hand. The migration also leaves `typeof import('@nx/devkit/src/...')` type queries and any deep-import strings inside template literals or comments untouched, so you'll need to update those by hand.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,80 @@ describe('update-deep-imports migration', () => {
});
});

// These tests guard against an earlier implementation that string-replaced
// every `'@nx/devkit/src/...'` literal in the file. That over-eager rewrite
// mangled test fixtures inside template literals, type queries, comments,
// and other non-runtime usages. The current AST-based pass must leave them
// alone.
describe('non-runtime string literals', () => {
it('does not rewrite deep-import paths inside template literals', () => {
const input = `const fixture = \`import { dasherize } from '@nx/devkit/src/utils/string-utils';\\n\`;\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('does not rewrite deep-import paths inside `typeof import(...)` type queries', () => {
const input = `type Devkit = typeof import('@nx/devkit/src/executors/parse-target-string');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('does not rewrite deep-import paths inside block comments', () => {
const input = `/* see @nx/devkit/src/utils/foo */\nconst x = 1;\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('does not rewrite deep-import paths inside line comments', () => {
const input = `// see '@nx/devkit/src/utils/foo'\nconst x = 1;\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('does not rewrite arbitrary string-literal arguments to unrelated calls', () => {
const input = `someUnrelatedFn('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});
});

describe('mock helper calls', () => {
it('rewrites jest.mock(...) targets', () => {
const input = `jest.mock('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`jest.mock('@nx/devkit/internal');\n`
);
});

it('rewrites jest.requireActual(...) targets', () => {
const input = `const real = jest.requireActual('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`const real = jest.requireActual('@nx/devkit/internal');\n`
);
});

it('rewrites vi.mock(...) targets', () => {
const input = `vi.mock('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`vi.mock('@nx/devkit/internal');\n`
);
});

it('rewrites vi.importActual(...) targets', () => {
const input = `const real = await vi.importActual('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`const real = await vi.importActual('@nx/devkit/internal');\n`
);
});

it('rewrites paired import + jest.mock together', () => {
const input =
`import * as cfg from '@nx/devkit/src/utils/config-utils';\n` +
`jest.mock('@nx/devkit/src/utils/config-utils', () => ({\n` +
` ...jest.requireActual('@nx/devkit/src/utils/config-utils'),\n` +
`}));\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import * as cfg from '@nx/devkit/internal';`);
expect(output).toContain(`jest.mock('@nx/devkit/internal',`);
expect(output).toContain(`jest.requireActual('@nx/devkit/internal')`);
});
});

describe('migration runner', () => {
it('rewrites deep imports across .ts/.tsx/.cts/.mts files', async () => {
tree.write(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { logger, type Tree } from 'nx/src/devkit-exports';
import type {
CallExpression,
ImportDeclaration,
ImportSpecifier,
NamedImports,
Node,
SourceFile,
} from 'typescript';
import { formatFiles } from '../../generators/format-files';
Expand Down Expand Up @@ -69,7 +71,19 @@ export const DEVKIT_INTERNAL_SYMBOLS: ReadonlySet<string> = new Set([
'dasherize',
]);

const FALLBACK_RE = /(['"])@nx\/devkit\/src\/[^'"\n]+?\1/g;
// Methods on `jest` and `vi` that take a module specifier as their first
// argument. Calls like `jest.mock('@nx/devkit/src/...')` are rewritten so the
// mock target lines up with the rewritten import.
const MOCK_HELPER_METHODS: ReadonlySet<string> = new Set([
'mock',
'unmock',
'doMock',
'dontMock',
'requireActual',
'requireMock',
'importActual',
'importMock',
]);

let ts: typeof import('typescript') | undefined;

Expand Down Expand Up @@ -134,19 +148,17 @@ export function rewriteDevkitDeepImports(source: string): string {
);
}

// Pass 2: rewrite `require('@nx/devkit/src/...')`, dynamic
// `import('@nx/devkit/src/...')`, and `jest.mock(...)` / `vi.mock(...)`-style
// calls. We can't bucket these by symbol (no named binding to inspect), so
// we route them at `/internal` as a best guess. Walking the AST instead of
// string-replacing keeps us out of unrelated string literals — template
// strings, `typeof import('...')` type queries, comments, etc.
collectCallExpressionRewrites(sourceFile, changes);

let updated =
changes.length > 0 ? applyChangesToString(source, changes) : source;

// Fallback: any remaining `@nx/devkit/src/...` specifiers (default imports,
// namespace imports, side-effect imports, dynamic `import(...)`, `require(...)`
// calls, etc.) get rewritten to `/internal`. We can't bucket by symbol for
// those forms, so `/internal` is the safe default since it re-exports every
// symbol that was deep-importable.
updated = updated.replace(
FALLBACK_RE,
(_match, quote: string) => `${quote}${INTERNAL_SPECIFIER}${quote}`
);

// Final pass: collapse any duplicate `@nx/devkit` and `@nx/devkit/internal`
// named-only imports into a single declaration. This handles both the
// imports we just emitted AND any that the user already had, so we never
Expand Down Expand Up @@ -341,3 +353,60 @@ function renderImport(
function source(decl: ImportDeclaration, sourceFile: SourceFile): string {
return sourceFile.text.slice(decl.getStart(sourceFile), decl.getEnd());
}

function collectCallExpressionRewrites(
sourceFile: SourceFile,
changes: StringChange[]
): void {
const visit = (node: Node): void => {
if (
ts!.isCallExpression(node) &&
shouldRewriteCallExpression(node) &&
node.arguments.length >= 1 &&
ts!.isStringLiteral(node.arguments[0]) &&
node.arguments[0].text.startsWith(DEEP_IMPORT_PREFIX)
) {
const arg = node.arguments[0];
const start = arg.getStart(sourceFile);
const end = arg.getEnd();
const quote = sourceFile.text.charAt(start);
changes.push(
{
type: ChangeType.Delete,
start,
length: end - start,
},
{
type: ChangeType.Insert,
index: start,
text: `${quote}${INTERNAL_SPECIFIER}${quote}`,
}
);
}
ts!.forEachChild(node, visit);
};
visit(sourceFile);
}

function shouldRewriteCallExpression(call: CallExpression): boolean {
const callee = call.expression;
// `require('...')`
if (ts!.isIdentifier(callee) && callee.text === 'require') return true;
// dynamic `import('...')` — the runtime form parses as a CallExpression
// whose callee is the `import` keyword. The type-position form
// (`typeof import('...')`) parses as `ImportTypeNode`, not a CallExpression,
// so we don't touch it.
if (callee.kind === ts!.SyntaxKind.ImportKeyword) return true;
// `jest.mock(...)` / `vi.mock(...)` and friends.
if (ts!.isPropertyAccessExpression(callee)) {
const obj = callee.expression;
if (
ts!.isIdentifier(obj) &&
(obj.text === 'jest' || obj.text === 'vi') &&
MOCK_HELPER_METHODS.has(callee.name.text)
) {
return true;
}
}
return false;
}
5 changes: 0 additions & 5 deletions tools/workspace-plugin/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
"schema": "./src/generators/remove-migrations/schema.json",
"description": "remove-migrations generator"
},
"create-nodes-plugin": {
"factory": "./src/generators/create-nodes-plugin/generator",
"schema": "./src/generators/create-nodes-plugin/schema.json",
"description": "Workspace Generator to create a create-nodes-plugin"
},
"bump-maven-version": {
"factory": "./src/generators/bump-maven-version/generator",
"schema": "./src/generators/bump-maven-version/schema.json",
Expand Down
Loading
Loading