Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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;
}
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
1 change: 0 additions & 1 deletion tools/workspace-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"@nx/conformance": "5.0.4",
"@nx/devkit": "23.0.0-beta.4",
"@nx/js": "23.0.0-beta.4",
"@nx/plugin": "23.0.0-beta.4",
"@xmldom/xmldom": "^0.8.10",
"glob": "7.1.4",
"semver": "catalog:",
Expand Down
Loading
Loading