|
| 1 | +# Upgrade a plugin package to ESM-only (packages/<name>) |
| 2 | + |
| 3 | +Upgrade a single plugin under `packages/<name>` to publish ESM-only output with TypeScript-emitted JS and declarations. |
| 4 | + |
| 5 | +## Prerequisites |
| 6 | + |
| 7 | +- Repo already contains shared config at `.config/tsconfig.base.json` and `.config/tsconfig.plugin.json` and (optionally) `.config/vitest.config.mts` from prior migrations. |
| 8 | +- Scope constraint: make changes only inside the target package directory (e.g., `packages/alias`). Do not add or edit files outside `packages/<name>`. |
| 9 | +- Local Node 20.19+ to run builds and tests. |
| 10 | + |
| 11 | +## Steps |
| 12 | + |
| 13 | +1. Identify the target package |
| 14 | + - Set a shell variable for reuse: `PKG=packages/<name>`. |
| 15 | + |
| 16 | +2. Package metadata: ESM-only and minimums |
| 17 | + - Edit `$PKG/package.json`: |
| 18 | + - Set `"type": "module"`. |
| 19 | + - Replace legacy `main/module/exports.require` with ESM-only exports: |
| 20 | + ```json |
| 21 | + "exports": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, |
| 22 | + "types": "./dist/index.d.ts" |
| 23 | + ``` |
| 24 | + - Set minimums: `"engines": { "node": ">=20.19.0" }` and `"peerDependencies": { "rollup": ">=4.0.0" }`. |
| 25 | + - Keep `rollup` as a devDependency only if tests use it. Otherwise remove it. |
| 26 | + - Ensure published files include dist but exclude maps: |
| 27 | + ```json |
| 28 | + "files": ["dist", "!dist/**/*.map", "README.md", "LICENSE"] |
| 29 | + ``` |
| 30 | + |
| 31 | +3. Build scripts: TypeScript emit to dist |
| 32 | + - Prefer a tsc-only build for packages that do not need bundling: |
| 33 | + - In `$PKG/package.json`, set scripts: |
| 34 | + ```json |
| 35 | + "prebuild": "del-cli dist", |
| 36 | + "build": "tsc --project tsconfig.json", |
| 37 | + "pretest": "pnpm build", |
| 38 | + "prerelease": "pnpm build", |
| 39 | + "prepare": "if [ ! -d 'dist' ]; then pnpm build; fi" |
| 40 | + ``` |
| 41 | + - If this package still needs bundling for tests/examples, keep its Rollup config but point inputs at the TypeScript output in `dist/` instead of sources. |
| 42 | + |
| 43 | +4. TypeScript config: emit ESM to `dist/` |
| 44 | + - Create or update `$PKG/tsconfig.json` to extend the shared plugin config and emit declarations: |
| 45 | + ```json |
| 46 | + { |
| 47 | + "extends": "../../.config/tsconfig.base.json", |
| 48 | + "compilerOptions": { |
| 49 | + "noEmit": false, |
| 50 | + "outDir": "dist", |
| 51 | + "rootDir": "src", |
| 52 | + "declaration": true, |
| 53 | + "declarationMap": true |
| 54 | + }, |
| 55 | + "include": ["src/**/*"] |
| 56 | + } |
| 57 | + ``` |
| 58 | + - Delete any package-local `rollup` build scripts that produced CJS, and remove any `types/` folder if declarations were hand-authored (they will now be generated). |
| 59 | + |
| 60 | +5. Source: convert to pure ESM and modern Node APIs |
| 61 | + - Replace `require`, `module.exports`, and `__dirname` patterns with ESM equivalents. |
| 62 | + - Use `node:` specifiers for built-ins (e.g., `import path from 'node:path'`). |
| 63 | + - Prefer URL utilities where needed (`fileURLToPath(new URL('.', import.meta.url))`). |
| 64 | + - Inline and export public types from `src/index.ts`; avoid separate `types/` unless unavoidable. |
| 65 | + |
| 66 | +6. Tests: drop CJS branches; ESM everywhere |
| 67 | + - Remove CJS-specific branches/assertions from tests. |
| 68 | + - Keep the existing runner (AVA) if it already handles ESM in Node 20. If the package already uses Vitest in this repo, keep that pattern. |
| 69 | + - Ensure Rollup bundles created in tests are `await bundle.close()`-d to avoid leaks. |
| 70 | + |
| 71 | +7. Clean up package artifacts |
| 72 | + - Remove obsolete files that are no longer used by ESM-only publishing (examples): |
| 73 | + - `$PKG/rollup.config.*` if switching to tsc-only. |
| 74 | + - `$PKG/types/**` once declarations are generated to `dist/`. |
| 75 | + |
| 76 | +## Verify |
| 77 | + |
| 78 | +- Build succeeds and emits JS and d.ts to `dist/`: |
| 79 | + ```bash |
| 80 | + pnpm -C $PKG build |
| 81 | + tree $PKG/dist | sed -n '1,80p' |
| 82 | + ``` |
| 83 | +- Type declarations resolve for consumers: |
| 84 | + ```bash |
| 85 | + jq -r '.types, .exports.types, .exports.import' $PKG/package.json |
| 86 | + ``` |
| 87 | +- Runtime smoke (Node ESM import works): |
| 88 | + ```bash |
| 89 | + node -e "import('file://$PWD/$PKG/dist/index.js').then(() => console.log('ok'))" |
| 90 | + ``` |
| 91 | +- Tests pass for the package (runner may be AVA or Vitest depending on the package): |
| 92 | + ```bash |
| 93 | + pnpm -C $PKG test |
| 94 | + ``` |
| 95 | + |
| 96 | +## Rollback |
| 97 | + |
| 98 | +- Revert the package directory to the previous commit: |
| 99 | + ```bash |
| 100 | + git checkout -- $PKG |
| 101 | + git restore -SW $PKG |
| 102 | + ``` |
| 103 | +- If needed, `git reset --hard HEAD~1` when this package’s change is isolated on a feature branch. |
| 104 | + |
| 105 | +## References |
| 106 | + |
| 107 | +- Alias migration (ESM-only) — PR #1926: feat(alias)!: ESM only. Update Node and Rollup minimum versions |
| 108 | +- Task spec used for alias — Issue #1925 |
| 109 | +- Shared TS configs used by packages — `.config/tsconfig.base.json`, `.config/tsconfig.plugin.json` |
0 commit comments