|
| 1 | +--- |
| 2 | +name: dist-build-migration |
| 3 | +description: Migrate an Nx package to build to a local dist/ directory with nodenext module resolution, exports map, and @nx/nx-source condition. |
| 4 | +allowed-tools: Bash, Read, Glob, Grep, Agent, Edit, Write |
| 5 | +--- |
| 6 | + |
| 7 | +# Migrate Package to Local Dist Build |
| 8 | + |
| 9 | +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`. |
| 10 | + |
| 11 | +## Argument |
| 12 | + |
| 13 | +The user provides a package name (e.g., `js`, `webpack`, `angular`). The package lives at `packages/<name>/`. |
| 14 | + |
| 15 | +## Steps |
| 16 | + |
| 17 | +### 1. Read current state |
| 18 | + |
| 19 | +Read these files for the target package: |
| 20 | + |
| 21 | +- `packages/<name>/package.json` |
| 22 | +- `packages/<name>/project.json` |
| 23 | +- `packages/<name>/tsconfig.lib.json` |
| 24 | +- `packages/<name>/tsconfig.spec.json` (if exists) |
| 25 | +- `packages/<name>/.eslintrc.json` (if exists) |
| 26 | +- `packages/<name>/assets.json` (if exists) |
| 27 | +- `packages/<name>/.npmignore` (if exists) |
| 28 | +- `packages/<name>/.gitignore` (if exists) |
| 29 | + |
| 30 | +Also read the reference implementations: |
| 31 | + |
| 32 | +- `packages/devkit/tsconfig.lib.json` |
| 33 | +- `packages/devkit/package.json` |
| 34 | +- `packages/devkit/project.json` |
| 35 | +- `packages/devkit/.npmignore` |
| 36 | + |
| 37 | +Run `pnpm nx show target <name>:build-base` to see the inferred build target. |
| 38 | +Run `pnpm nx show target <name>:build` to see the full build target. |
| 39 | + |
| 40 | +### 2. Identify entry points |
| 41 | + |
| 42 | +Look at the package's root `.ts` files and any existing `exports` field. Common entry points: |
| 43 | + |
| 44 | +- `index.ts` (main) |
| 45 | +- `testing.ts` |
| 46 | +- `internal.ts` |
| 47 | +- `ngcli-adapter.ts` |
| 48 | +- Any other `.ts` files at the package root that re-export from `src/` |
| 49 | + |
| 50 | +Also check for `migrations.json` and `generators.json`/`executors.json` — these need exports entries too. |
| 51 | + |
| 52 | +### 3. Update `tsconfig.lib.json` |
| 53 | + |
| 54 | +Transform from the old pattern to the new pattern: |
| 55 | + |
| 56 | +**Before:** |
| 57 | + |
| 58 | +```json |
| 59 | +{ |
| 60 | + "compilerOptions": { |
| 61 | + "module": "commonjs", |
| 62 | + "outDir": "../../dist/packages/<name>", |
| 63 | + "tsBuildInfoFile": "../../dist/packages/<name>/tsconfig.tsbuildinfo" |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +**After:** |
| 69 | + |
| 70 | +```json |
| 71 | +{ |
| 72 | + "compilerOptions": { |
| 73 | + "outDir": "dist", |
| 74 | + "rootDir": ".", |
| 75 | + "declarationDir": "dist", |
| 76 | + "declarationMap": false, |
| 77 | + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", |
| 78 | + "types": ["node"], |
| 79 | + "composite": true, |
| 80 | + "module": "nodenext", |
| 81 | + "moduleResolution": "nodenext", |
| 82 | + "esModuleInterop": true, |
| 83 | + "allowSyntheticDefaultImports": true |
| 84 | + }, |
| 85 | + "exclude": ["node_modules", "dist", ...existing excludes, ".eslintrc.json"], |
| 86 | + "include": ["*.ts", "src/**/*.ts"] |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +**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. |
| 91 | + |
| 92 | +### 4. Update `tsconfig.spec.json` (if exists) |
| 93 | + |
| 94 | +Change `outDir` from `../../dist/packages/<name>/spec` to `dist/spec`. |
| 95 | + |
| 96 | +### 5. Update `package.json` |
| 97 | + |
| 98 | +Key changes: |
| 99 | + |
| 100 | +- Add `"type": "commonjs"` near the top (after `private`) |
| 101 | +- Change `"main"` to `"./dist/index.js"` |
| 102 | +- Change `"types"` to `"./dist/index.d.ts"` |
| 103 | +- Add `"typesVersions"` for backwards compatibility with `moduleResolution: "node"` consumers |
| 104 | +- Add `"exports"` map with entries for each entry point |
| 105 | + |
| 106 | +Each export entry follows this pattern: |
| 107 | + |
| 108 | +```json |
| 109 | +"./entry-name": { |
| 110 | + "@nx/nx-source": "./entry-name.ts", |
| 111 | + "types": "./entry-name.d.ts", |
| 112 | + "default": "./dist/entry-name.js" |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +The main entry (`.`) uses `./index.ts`, `./index.d.ts`, `./dist/index.js`. |
| 117 | + |
| 118 | +Always include: |
| 119 | + |
| 120 | +```json |
| 121 | +"./package.json": "./package.json" |
| 122 | +``` |
| 123 | + |
| 124 | +Include `"./migrations.json": "./migrations.json"` if the package has migrations. |
| 125 | + |
| 126 | +**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). |
| 127 | + |
| 128 | +Add a `typesVersions` field for consumers using `moduleResolution: "node"` (which doesn't read `exports`): |
| 129 | + |
| 130 | +```json |
| 131 | +"typesVersions": { |
| 132 | + "*": { |
| 133 | + "testing": ["dist/testing.d.ts"], |
| 134 | + "ngcli-adapter": ["dist/ngcli-adapter.d.ts"] |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +Add an entry for each subpath export (excluding `.`, `./package.json`, and `./migrations.json`). |
| 140 | + |
| 141 | +### 6. Update `project.json` |
| 142 | + |
| 143 | +Add these sections: |
| 144 | + |
| 145 | +```json |
| 146 | +{ |
| 147 | + "release": { |
| 148 | + "version": { |
| 149 | + "generator": "@nx/js:release-version", |
| 150 | + "preserveLocalDependencyProtocols": true, |
| 151 | + "manifestRootsToUpdate": ["packages/{projectName}"] |
| 152 | + } |
| 153 | + }, |
| 154 | + "targets": { |
| 155 | + "nx-release-publish": { |
| 156 | + "options": { |
| 157 | + "packageRoot": "packages/{projectName}" |
| 158 | + } |
| 159 | + }, |
| 160 | + "build-base": { |
| 161 | + "outputs": [ |
| 162 | + "{projectRoot}/dist/**/*.{js,cjs,mjs,d.ts}", |
| 163 | + "{projectRoot}/*.d.ts", |
| 164 | + "{projectRoot}/src/**/*.d.ts" |
| 165 | + ] |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +Update the existing `build` target's `outputs` if they reference `{workspaceRoot}/dist/packages/<name>` — they should now reference `{projectRoot}/dist/`. |
| 172 | + |
| 173 | +Also update `dependsOn` in the `build` target: replace `"^build"` with `"^build"` if it isn't already, and make sure `"build-base"` is listed. |
| 174 | + |
| 175 | +### 7. Update `.eslintrc.json` |
| 176 | + |
| 177 | +Add `"dist"` and `"*.d.ts"` to `ignorePatterns`: |
| 178 | + |
| 179 | +```json |
| 180 | +"ignorePatterns": ["!**/*", "node_modules", "dist", "*.d.ts"] |
| 181 | +``` |
| 182 | + |
| 183 | +### 8. Update `assets.json` (if exists) |
| 184 | + |
| 185 | +Change `outDir` from `"dist/packages/<name>"` to `"packages/<name>/dist"`. |
| 186 | + |
| 187 | +### 9. Add `files` field to `package.json` |
| 188 | + |
| 189 | +Instead of using `.npmignore`, add a `"files"` field to `package.json` (matching the `nx` package pattern). Remove `.npmignore` if it exists. |
| 190 | + |
| 191 | +```json |
| 192 | +"files": [ |
| 193 | + "dist", |
| 194 | + "!dist/tsconfig.tsbuildinfo", |
| 195 | + "migrations.json" |
| 196 | +] |
| 197 | +``` |
| 198 | + |
| 199 | +Adjust based on the package's needs: |
| 200 | + |
| 201 | +- Add `"executors.json"` and/or `"generators.json"` if the package has them |
| 202 | +- Add any other non-TS files that need to be published |
| 203 | +- npm always includes `package.json` and `README.md` automatically — no need to list them |
| 204 | + |
| 205 | +### 10. Rename README.md and update build command |
| 206 | + |
| 207 | +If the package has a `README.md` at its root and uses the `copy-readme.js` script in its build target: |
| 208 | + |
| 209 | +1. Rename `README.md` to `readme-template.md` (`git mv`) |
| 210 | +2. Update the build command to pass explicit paths: |
| 211 | + ``` |
| 212 | + node ./scripts/copy-readme.js <name> packages/<name>/readme-template.md packages/<name>/README.md |
| 213 | + ``` |
| 214 | +3. Update the build target `outputs` to `["{projectRoot}/README.md"]` |
| 215 | + |
| 216 | +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. |
| 217 | + |
| 218 | +### 11. Update root `.gitignore` |
| 219 | + |
| 220 | +Add two entries to the workspace root `.gitignore`: |
| 221 | + |
| 222 | +1. Under the section that lists generated README files (look for `packages/nx/README.md`), add: |
| 223 | + |
| 224 | + ``` |
| 225 | + packages/<name>/README.md |
| 226 | + ``` |
| 227 | + |
| 228 | +2. Under the section that lists generated `.d.ts` files (look for `packages/nx/**/*.d.ts`), add: |
| 229 | + ``` |
| 230 | + packages/<name>/**/*.d.ts |
| 231 | + ``` |
| 232 | + |
| 233 | +These are build outputs that shouldn't be committed. |
| 234 | + |
| 235 | +### 12. Update docs generation paths |
| 236 | + |
| 237 | +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`. |
| 238 | + |
| 239 | +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`. |
| 240 | + |
| 241 | +### 13. Update `scripts/nx-release.ts` |
| 242 | + |
| 243 | +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>/`. |
| 244 | + |
| 245 | +### 14. Update imports across the workspace |
| 246 | + |
| 247 | +Search for imports from `@nx/<name>/src/` across all other packages. These internal imports need to be updated: |
| 248 | + |
| 249 | +- 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 |
| 250 | +- If not, consider adding it to `internal.ts` or the appropriate entry point |
| 251 | + |
| 252 | +Use: `grep -r "from '@nx/<name>/src/" packages/ --include="*.ts" -l` to find affected files. |
| 253 | + |
| 254 | +Also check for imports in: |
| 255 | + |
| 256 | +- `e2e/` tests |
| 257 | +- `scripts/` |
| 258 | +- `tools/workspace-plugin/` |
| 259 | +- `astro-docs/` |
| 260 | +- `examples/` |
| 261 | + |
| 262 | +### 15. Verify |
| 263 | + |
| 264 | +Run: |
| 265 | + |
| 266 | +```bash |
| 267 | +pnpm nx run-many -t test,build,lint -p <name> |
| 268 | +``` |
| 269 | + |
| 270 | +Then: |
| 271 | + |
| 272 | +```bash |
| 273 | +pnpm nx affected -t build,test,lint |
| 274 | +``` |
| 275 | + |
| 276 | +### Summary of the pattern |
| 277 | + |
| 278 | +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. |
0 commit comments