Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
804c18c
chore(devkit): build devkit to local dist and use nodenext
FrozenPandaz Mar 21, 2026
4aaa5ea
fix(devkit): point docs generation at dist/ for .d.ts files
FrozenPandaz Mar 27, 2026
e296ba8
fix(devkit): point docs generation at package root for d.ts files
FrozenPandaz Mar 28, 2026
28669c8
fix(devkit): put d.ts files in dist and update all references
FrozenPandaz Mar 28, 2026
eba85f1
chore(devkit): retrigger ci
FrozenPandaz Mar 28, 2026
4b860ff
fix(devkit): include dist d.ts files in typedoc tsconfig for docs gen…
FrozenPandaz Apr 7, 2026
584159b
fix(devkit): use files field instead of .npmignore
FrozenPandaz Apr 7, 2026
51a3ee2
fix(devkit): add main, typesVersions for moduleResolution node compat
FrozenPandaz Apr 7, 2026
4f73cf4
fix(devkit): update e2e-nx-build test to use new dist output path
FrozenPandaz Apr 8, 2026
3a48183
fix(devkit): update e2e-nx-build test to use new dist output path [Se…
nx-cloud[bot] Apr 8, 2026
2554a32
cleanup(devkit): simplify typedoc tsconfig setup
FrozenPandaz May 1, 2026
08eed91
fix(devkit): include devkit dist d.ts in typedoc tsconfig
FrozenPandaz May 1, 2026
cdf330a
fix(angular): make eslint config replacement quote-agnostic in lint e2e
FrozenPandaz May 1, 2026
8f4eaec
cleanup(angular-rspack): drop @nx/devkit require patch in examples
FrozenPandaz May 1, 2026
40a726c
cleanup(devkit): drop redundant '**/*.d.ts' from eslint ignores
FrozenPandaz May 1, 2026
b8afee4
fix(testing): remove unused @nx/devkit/internal import in jest migration
FrozenPandaz May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
278 changes: 278 additions & 0 deletions .claude/skills/dist-build-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
---
name: dist-build-migration
description: Migrate an Nx package to build to a local dist/ directory with nodenext module resolution, exports map, and @nx/nx-source condition.
allowed-tools: Bash, Read, Glob, Grep, Agent, Edit, Write
---

# Migrate Package to Local Dist Build

You are migrating an Nx monorepo package from building to `../../dist/packages/<name>` to building locally to `packages/<name>/dist/`. This matches the pattern already used by `nx` and `devkit`.

## Argument

The user provides a package name (e.g., `js`, `webpack`, `angular`). The package lives at `packages/<name>/`.

## Steps

### 1. Read current state

Read these files for the target package:

- `packages/<name>/package.json`
- `packages/<name>/project.json`
- `packages/<name>/tsconfig.lib.json`
- `packages/<name>/tsconfig.spec.json` (if exists)
- `packages/<name>/.eslintrc.json` (if exists)
- `packages/<name>/assets.json` (if exists)
- `packages/<name>/.npmignore` (if exists)
- `packages/<name>/.gitignore` (if exists)

Also read the reference implementations:

- `packages/devkit/tsconfig.lib.json`
- `packages/devkit/package.json`
- `packages/devkit/project.json`
- `packages/devkit/.npmignore`

Run `pnpm nx show target <name>:build-base` to see the inferred build target.
Run `pnpm nx show target <name>:build` to see the full build target.

### 2. Identify entry points

Look at the package's root `.ts` files and any existing `exports` field. Common entry points:

- `index.ts` (main)
- `testing.ts`
- `internal.ts`
- `ngcli-adapter.ts`
- Any other `.ts` files at the package root that re-export from `src/`

Also check for `migrations.json` and `generators.json`/`executors.json` — these need exports entries too.

### 3. Update `tsconfig.lib.json`

Transform from the old pattern to the new pattern:

**Before:**

```json
{
"compilerOptions": {
"module": "commonjs",
"outDir": "../../dist/packages/<name>",
"tsBuildInfoFile": "../../dist/packages/<name>/tsconfig.tsbuildinfo"
}
}
```

**After:**

```json
{
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"declarationDir": "dist",
"declarationMap": false,
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"types": ["node"],
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist", ...existing excludes, ".eslintrc.json"],
"include": ["*.ts", "src/**/*.ts"]
}
```

**Important**: Adjust `include` based on the package's actual structure. If the package has directories like `bin/`, `plugins/`, etc. at the root level (like `nx` does), include those too.

### 4. Update `tsconfig.spec.json` (if exists)

Change `outDir` from `../../dist/packages/<name>/spec` to `dist/spec`.

### 5. Update `package.json`

Key changes:

- Add `"type": "commonjs"` near the top (after `private`)
- Change `"main"` to `"./dist/index.js"`
- Change `"types"` to `"./dist/index.d.ts"`
- Add `"typesVersions"` for backwards compatibility with `moduleResolution: "node"` consumers
- Add `"exports"` map with entries for each entry point

Each export entry follows this pattern:

```json
"./entry-name": {
"@nx/nx-source": "./entry-name.ts",
"types": "./entry-name.d.ts",
"default": "./dist/entry-name.js"
}
```

The main entry (`.`) uses `./index.ts`, `./index.d.ts`, `./dist/index.js`.

Always include:

```json
"./package.json": "./package.json"
```

Include `"./migrations.json": "./migrations.json"` if the package has migrations.

**Note**: The `@nx/nx-source` condition is a custom condition used for source-level resolution within the workspace (so other packages import from source, not dist).

Add a `typesVersions` field for consumers using `moduleResolution: "node"` (which doesn't read `exports`):

```json
"typesVersions": {
"*": {
"testing": ["dist/testing.d.ts"],
"ngcli-adapter": ["dist/ngcli-adapter.d.ts"]
}
}
```

Add an entry for each subpath export (excluding `.`, `./package.json`, and `./migrations.json`).

### 6. Update `project.json`

Add these sections:

```json
{
"release": {
"version": {
"generator": "@nx/js:release-version",
"preserveLocalDependencyProtocols": true,
"manifestRootsToUpdate": ["packages/{projectName}"]
}
},
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "packages/{projectName}"
}
},
"build-base": {
"outputs": [
"{projectRoot}/dist/**/*.{js,cjs,mjs,d.ts}",
"{projectRoot}/*.d.ts",
"{projectRoot}/src/**/*.d.ts"
]
}
}
}
```

Update the existing `build` target's `outputs` if they reference `{workspaceRoot}/dist/packages/<name>` — they should now reference `{projectRoot}/dist/`.

Also update `dependsOn` in the `build` target: replace `"^build"` with `"^build"` if it isn't already, and make sure `"build-base"` is listed.

### 7. Update `.eslintrc.json`

Add `"dist"` and `"*.d.ts"` to `ignorePatterns`:

```json
"ignorePatterns": ["!**/*", "node_modules", "dist", "*.d.ts"]
```

### 8. Update `assets.json` (if exists)

Change `outDir` from `"dist/packages/<name>"` to `"packages/<name>/dist"`.

### 9. Add `files` field to `package.json`

Instead of using `.npmignore`, add a `"files"` field to `package.json` (matching the `nx` package pattern). Remove `.npmignore` if it exists.

```json
"files": [
"dist",
"!dist/tsconfig.tsbuildinfo",
"migrations.json"
]
```

Adjust based on the package's needs:

- Add `"executors.json"` and/or `"generators.json"` if the package has them
- Add any other non-TS files that need to be published
- npm always includes `package.json` and `README.md` automatically — no need to list them

### 10. Rename README.md and update build command

If the package has a `README.md` at its root and uses the `copy-readme.js` script in its build target:

1. Rename `README.md` to `readme-template.md` (`git mv`)
2. Update the build command to pass explicit paths:
```
node ./scripts/copy-readme.js <name> packages/<name>/readme-template.md packages/<name>/README.md
```
3. Update the build target `outputs` to `["{projectRoot}/README.md"]`

The script's default behavior reads `packages/<name>/README.md` and writes to `dist/packages/<name>/README.md` — both wrong for the new layout. Passing explicit args fixes both.

### 11. Update root `.gitignore`

Add two entries to the workspace root `.gitignore`:

1. Under the section that lists generated README files (look for `packages/nx/README.md`), add:

```
packages/<name>/README.md
```

2. Under the section that lists generated `.d.ts` files (look for `packages/nx/**/*.d.ts`), add:
```
packages/<name>/**/*.d.ts
```

These are build outputs that shouldn't be committed.

### 12. Update docs generation paths

Check `astro-docs/src/plugins/utils/` for any code that references `.d.ts` files from the package. The docs generation reads `.d.ts` entry points to build API reference pages. Paths that previously pointed to `dist/packages/<name>/foo.d.ts` (workspace root dist) or `packages/<name>/foo.d.ts` (package root) now need to point to `packages/<name>/dist/foo.d.ts`.

For example, `devkit-generation.ts` had to be updated to look for `packages/devkit/dist/index.d.ts` instead of `packages/devkit/index.d.ts`.

### 13. Update `scripts/nx-release.ts`

If the package has special release handling in `scripts/nx-release.ts` (like devkit's `hackFixForDevkitPeerDependencies`), update any paths from `./dist/packages/<name>/` to `./packages/<name>/`.

### 14. Update imports across the workspace

Search for imports from `@nx/<name>/src/` across all other packages. These internal imports need to be updated:

- If the imported thing is re-exported through a public entry point (index.ts, internal.ts, etc.), update the import to use that entry point
- If not, consider adding it to `internal.ts` or the appropriate entry point

Use: `grep -r "from '@nx/<name>/src/" packages/ --include="*.ts" -l` to find affected files.

Also check for imports in:

- `e2e/` tests
- `scripts/`
- `tools/workspace-plugin/`
- `astro-docs/`
- `examples/`

### 15. Verify

Run:

```bash
pnpm nx run-many -t test,build,lint -p <name>
```

Then:

```bash
pnpm nx affected -t build,test,lint
```

### Summary of the pattern

The core idea is simple: instead of building to a shared `dist/packages/<name>/` at the workspace root, each package builds to its own `packages/<name>/dist/`. The `exports` map with `@nx/nx-source` condition lets workspace packages resolve to `.ts` source files during development, while external consumers get the built `.js` from `dist/`. This is like giving each package its own "output mailbox" instead of sharing one big mailbox.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ packages/angular-rspack-compiler/README.md
packages/dotnet/README.md
packages/maven/README.md
packages/nx/README.md
packages/devkit/README.md

test-output
test-results
Expand Down
4 changes: 2 additions & 2 deletions astro-docs/src/plugins/utils/devkit-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export async function loadDevkitPackage(
// generate main @nx/devkit docs
const devkitEntryPoint = join(
workspaceRoot,
'dist',
'packages',
'devkit',
'dist',
'index.d.ts'
);
if (existsSync(devkitEntryPoint)) {
Expand All @@ -47,9 +47,9 @@ export async function loadDevkitPackage(
// generate ngcli docs in same dir
const ngcliEntryPoint = join(
workspaceRoot,
'dist',
'packages',
'devkit',
'dist',
'ngcli-adapter.d.ts'
);
if (existsSync(ngcliEntryPoint)) {
Expand Down
24 changes: 4 additions & 20 deletions astro-docs/src/plugins/utils/typedoc/typedoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,12 @@ export function setupTypeDoc(logger: LoaderContext['logger']) {
join(projectRoot, 'node_modules', '@types'),
];

// This ensures that nx and @nx/<plugin> modules resolve to `dist` rather than what's installed in node_modules.
// TODO(jack,caleb): If we move outDir from `dist/packages/nx` to `packages/nx/dist` like standard TS solution setup,
// then this isn't needed anymore since we should have devDependencies that resolve to local
// `node_modules` not the root one.
tsconfigObj.compilerOptions.baseUrl = workspaceRoot;
tsconfigObj.compilerOptions.paths = {
'nx/*': ['dist/packages/nx/*', 'packages/nx/src/*'],
'@nx/*': ['dist/packages/*', 'packages/*/src/*'],
};
// TypeDoc requires its entry points to be referenced by `include` or
// `files` in the tsconfig. Point at devkit's compiled .d.ts files.
tsconfigObj.include = [join(devkitPath, 'dist', '**', '*.d.ts')];

tsconfigObj.exclude = [
...(tsconfigObj.exclude || []),
...(tsconfigObj.exclude || []).filter((e: string) => e !== 'dist'),
'**/*.spec.ts',
'**/*.test.ts',
'**/test/**',
Expand All @@ -95,16 +89,6 @@ export function setupTypeDoc(logger: LoaderContext['logger']) {
'node_modules/@types/jest/**',
];

// The tsconfig now lives in tempDir but it operates on devkit's compiled
// dist (entry point is dist/packages/devkit/index.d.ts). Resolve include
// patterns to absolute paths anchored at the dist directory so TypeDoc
// picks up the .d.ts files instead of looking for sources next to the temp
// tsconfig.
const distDevkitDir = join(workspaceRoot, 'dist', 'packages', 'devkit');
tsconfigObj.include = (tsconfigObj.include || ['**/*.ts']).map(
(pattern: string) => join(distDevkitDir, pattern)
);

writeFileSync(generatedTsconfigPath, JSON.stringify(tsconfigObj, null, 2));

rmSync(outDir, { recursive: true, force: true });
Expand Down
3 changes: 2 additions & 1 deletion e2e/angular/src/misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
uniq,
updateFile,
} from '@nx/e2e-utils';
import { classify } from '@nx/devkit/src/utils/string-utils';
import { names } from '@nx/devkit';
const classify = (s: string) => names(s).className;

describe('Move Angular Project', () => {
let proj: string;
Expand Down
19 changes: 11 additions & 8 deletions e2e/angular/src/projects-linting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ describe('Angular Projects - Linting', () => {
it('should lint correctly with eslint and handle external HTML files and inline templates', async () => {
const { app1, lib1 } = setup;

// disable the prefer-standalone rule for app1 which is not standalone
let app1EslintConfig = readFile(`${app1}/eslint.config.mjs`);
app1EslintConfig = app1EslintConfig.replace(
`'@angular-eslint/directive-selector': [`,
`'@angular-eslint/prefer-standalone': 'off',
'@angular-eslint/directive-selector': [`
);
updateFile(`${app1}/eslint.config.mjs`, app1EslintConfig);
// disable the prefer-standalone rule for app1 and lib1 which are not standalone.
// Use a regex so we match regardless of whether the generated config uses
// single or double quotes around the rule name.
for (const project of [app1, lib1]) {
let eslintConfig = readFile(`${project}/eslint.config.mjs`);
eslintConfig = eslintConfig.replace(
/(['"])@angular-eslint\/directive-selector\1:\s*\[/,
`"@angular-eslint/prefer-standalone": "off",\n "@angular-eslint/directive-selector": [`
);
updateFile(`${project}/eslint.config.mjs`, eslintConfig);
}

// check apps and lib pass linting for initial generated code
runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`);
Expand Down
Loading
Loading