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
8 changes: 7 additions & 1 deletion packages/devkit/migrations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"generators": {},
"generators": {
"update-devkit-deep-imports": {
"version": "23.0.0-beta.6",
"description": "Rewrite imports from `@nx/devkit/src/...` to `@nx/devkit` (for public symbols) or `@nx/devkit/internal` (for the rest), since deep imports are no longer reachable through the package's `exports` map.",
"implementation": "./dist/src/migrations/update-23-0-0/update-deep-imports"
}
},
"packageJsonUpdates": {},
"version": "0.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#### Update `@nx/devkit` deep imports

`@nx/devkit` now ships a strict `exports` map, so deep imports like `@nx/devkit/src/utils/...` and `@nx/devkit/src/generators/...` are no longer reachable through Node module resolution.

This migration scans every `.ts`, `.tsx`, `.cts`, and `.mts` file in your workspace and rewrites those deep imports to one of the supported entry points:

- Symbols that are part of the stable `@nx/devkit` public API are routed to `@nx/devkit`.
- Symbols that were previously only reachable through deep imports are routed to `@nx/devkit/internal`.

After rewriting, the migration **collapses duplicate imports** so a file never ends up with two `import ... from '@nx/devkit'` (or `@nx/devkit/internal`) lines — including merging into any matching import you already had.

#### Sample Code Changes

##### Before

```ts
import { Tree } from '@nx/devkit';
import { dasherize, names } from '@nx/devkit/src/utils/string-utils';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
```

##### After

```ts
import { Tree, names } from '@nx/devkit';
import { dasherize, addPlugin } from '@nx/devkit/internal';
```

`names` was already in the public API, so it joins the existing `@nx/devkit` import. `dasherize` and `addPlugin` move to `@nx/devkit/internal`, and the two `/internal` imports are collapsed into one.

#### 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.

```ts
// Before
const { dasherize } = require('@nx/devkit/src/utils/string-utils');

// After
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migration, {
DEVKIT_INTERNAL_SYMBOLS,
rewriteDevkitDeepImports,
} from './update-deep-imports';

describe('update-deep-imports migration', () => {
let tree: Tree;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

describe('rewriteDevkitDeepImports', () => {
it('routes a single internal symbol to @nx/devkit/internal', () => {
const input = `import { dasherize } from '@nx/devkit/src/utils/string-utils';\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`import { dasherize } from '@nx/devkit/internal';\n`
);
});

it('routes a single public symbol to @nx/devkit', () => {
const input = `import { names } from '@nx/devkit/src/utils/names';\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`import { names } from '@nx/devkit';\n`
);
});

it('splits mixed public and internal symbols across two declarations', () => {
const input = `import { dasherize, names } from '@nx/devkit/src/utils/string-utils';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { names } from '@nx/devkit';`);
expect(output).toContain(
`import { dasherize } from '@nx/devkit/internal';`
);
});

it('preserves `as` aliases', () => {
const input = `import { dasherize as toKebab } from '@nx/devkit/src/utils/string-utils';\n`;
expect(rewriteDevkitDeepImports(input)).toContain(
`import { dasherize as toKebab } from '@nx/devkit/internal';`
);
});

it('handles `import type` declarations', () => {
const input = `import type { FileExtensionType } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';\n`;
expect(rewriteDevkitDeepImports(input)).toContain(
`import type { FileExtensionType } from '@nx/devkit/internal';`
);
});

it('preserves inline `type` modifiers on individual specifiers', () => {
const input = `import { type FileExtensionType, addPlugin } from '@nx/devkit/src/x';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import { type FileExtensionType, addPlugin } from '@nx/devkit/internal';`
);
});

it('drops redundant inline `type` when the whole import is already type-only', () => {
const input = `import type { type FileExtensionType } from '@nx/devkit/src/x';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import type { FileExtensionType } from '@nx/devkit/internal';`
);
});

it('handles multi-line named imports', () => {
const input = `import {\n dasherize,\n names,\n classify,\n} from '@nx/devkit/src/utils/string-utils';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { names } from '@nx/devkit';`);
expect(output).toContain(
`import { dasherize, classify } from '@nx/devkit/internal';`
);
});

it('rewrites multiple deep imports in one file', () => {
const input =
`import { dasherize } from '@nx/devkit/src/utils/string-utils';\n` +
`import { names } from '@nx/devkit/src/utils/names';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import { dasherize } from '@nx/devkit/internal';`
);
expect(output).toContain(`import { names } from '@nx/devkit';`);
});

it('falls back to /internal for side-effect imports', () => {
const input = `import '@nx/devkit/src/utils/some-side-effect';\n`;
expect(rewriteDevkitDeepImports(input)).toContain(
`import '@nx/devkit/internal';`
);
});

it('falls back to /internal for default imports', () => {
const input = `import x from '@nx/devkit/src/utils/foo';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`'@nx/devkit/internal'`);
expect(output).toContain(`import x from`);
});

it('falls back to /internal for namespace imports', () => {
const input = `import * as devkit from '@nx/devkit/src/utils/foo';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import * as devkit from '@nx/devkit/internal';`
);
});

it('falls back to /internal for require()', () => {
const input = `const x = require('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`const x = require('@nx/devkit/internal');\n`
);
});

it('falls back to /internal for dynamic import()', () => {
const input = `const x = await import('@nx/devkit/src/utils/foo');\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`const x = await import('@nx/devkit/internal');\n`
);
});

it('preserves quote style on fallback paths', () => {
const input = `const x = require("@nx/devkit/src/utils/foo");\n`;
expect(rewriteDevkitDeepImports(input)).toBe(
`const x = require("@nx/devkit/internal");\n`
);
});

it('does not touch unrelated @nx/devkit imports', () => {
const input = `import { Tree } from '@nx/devkit';\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('does not touch unrelated @nx/devkit/internal imports', () => {
const input = `import { dasherize } from '@nx/devkit/internal';\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});
});

describe('migration runner', () => {
it('rewrites deep imports across .ts/.tsx/.cts/.mts files', async () => {
tree.write(
'libs/foo/src/a.ts',
`import { dasherize } from '@nx/devkit/src/utils/string-utils';\n`
);
tree.write(
'libs/foo/src/b.tsx',
`import { addPlugin } from '@nx/devkit/src/utils/add-plugin';\n`
);
tree.write(
'libs/foo/src/c.cts',
`const { classify } = require('@nx/devkit/src/utils/string-utils');\n`
);
tree.write(
'libs/foo/src/d.mts',
`import { camelize } from '@nx/devkit/src/utils/string-utils';\n`
);

await migration(tree);

expect(tree.read('libs/foo/src/a.ts', 'utf-8')).toContain(
`'@nx/devkit/internal'`
);
expect(tree.read('libs/foo/src/b.tsx', 'utf-8')).toContain(
`'@nx/devkit/internal'`
);
expect(tree.read('libs/foo/src/c.cts', 'utf-8')).toContain(
`'@nx/devkit/internal'`
);
expect(tree.read('libs/foo/src/d.mts', 'utf-8')).toContain(
`'@nx/devkit/internal'`
);
});

it('does not rewrite deep-import strings inside non-TS files', async () => {
const md = `Example: \`import { x } from '@nx/devkit/src/utils/foo';\`\n`;
tree.write('docs/example.md', md);

await migration(tree);

// The deep-import literal must survive untouched in markdown — only TS
// sources should be rewritten. (formatFiles may normalize trailing
// whitespace, so we assert on substring rather than full equality.)
expect(tree.read('docs/example.md', 'utf-8')).toContain(
`'@nx/devkit/src/utils/foo'`
);
});

it('does not touch files that do not contain the deep prefix', async () => {
const content = `import { Tree } from '@nx/devkit';\n`;
tree.write('libs/foo/src/index.ts', content);

await migration(tree);

expect(tree.read('libs/foo/src/index.ts', 'utf-8')).toBe(content);
});
});

describe('collapse', () => {
it('merges a rewritten import into a pre-existing @nx/devkit import', () => {
const input =
`import { Tree } from '@nx/devkit';\n` +
`import { names } from '@nx/devkit/src/utils/names';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { Tree, names } from '@nx/devkit';`);
expect(output).not.toMatch(
/import \{[^}]*\} from '@nx\/devkit';[\s\S]*import \{[^}]*\} from '@nx\/devkit';/
);
});

it('merges a rewritten import into a pre-existing @nx/devkit/internal import', () => {
const input =
`import { dasherize } from '@nx/devkit/internal';\n` +
`import { classify } from '@nx/devkit/src/utils/string-utils';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import { dasherize, classify } from '@nx/devkit/internal';`
);
});

it('merges multiple rewritten imports that target the same specifier', () => {
const input =
`import { dasherize } from '@nx/devkit/src/utils/string-utils';\n` +
`import { classify } from '@nx/devkit/src/utils/string-utils';\n` +
`import { camelize } from '@nx/devkit/src/utils/string-utils';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import { dasherize, classify, camelize } from '@nx/devkit/internal';`
);
// Only one /internal declaration in the output.
expect((output.match(/from '@nx\/devkit\/internal'/g) ?? []).length).toBe(
1
);
});

it('keeps public and internal collapses independent', () => {
const input =
`import { Tree } from '@nx/devkit';\n` +
`import { dasherize, names } from '@nx/devkit/src/utils/string-utils';\n` +
`import { classify } from '@nx/devkit/src/utils/string-utils';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { Tree, names } from '@nx/devkit';`);
expect(output).toContain(
`import { dasherize, classify } from '@nx/devkit/internal';`
);
});

it('does not merge value imports with type-only imports', () => {
const input =
`import type { Tree } from '@nx/devkit';\n` +
`import { names } from '@nx/devkit/src/utils/names';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import type { Tree } from '@nx/devkit';`);
expect(output).toContain(`import { names } from '@nx/devkit';`);
});

it('merges duplicate specifiers without repeating them', () => {
const input =
`import { Tree } from '@nx/devkit';\n` +
`import { Tree, names } from '@nx/devkit';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { Tree, names } from '@nx/devkit';`);
expect((output.match(/Tree/g) ?? []).length).toBe(1);
});

it('does not touch a file that already has a single canonical import', () => {
const input = `import { Tree, names } from '@nx/devkit';\n`;
expect(rewriteDevkitDeepImports(input)).toBe(input);
});

it('leaves unrelated imports between merged declarations alone', () => {
const input =
`import { Tree } from '@nx/devkit';\n` +
`import { readFileSync } from 'node:fs';\n` +
`import { names } from '@nx/devkit/src/utils/names';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(`import { Tree, names } from '@nx/devkit';`);
expect(output).toContain(`import { readFileSync } from 'node:fs';`);
});
});

describe('symbol set sanity', () => {
it('treats every name in DEVKIT_INTERNAL_SYMBOLS as internal', () => {
for (const name of DEVKIT_INTERNAL_SYMBOLS) {
const input = `import { ${name} } from '@nx/devkit/src/x';\n`;
const output = rewriteDevkitDeepImports(input);
expect(output).toContain(
`import { ${name} } from '@nx/devkit/internal';`
);
}
});
});
});
Loading
Loading