|
| 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 | + |
| 15 | + - Set a shell variable for reuse: `PKG=packages/<name>`. |
| 16 | + |
| 17 | +2. Package metadata: ESM-only and minimums |
| 18 | + |
| 19 | + - Edit `$PKG/package.json`: |
| 20 | + |
| 21 | + - Set `"type": "module"`. |
| 22 | + - Replace legacy `main/module/exports.require` with an ESM-only export mapped via the explicit `"."` entry for broad tooling compatibility: |
| 23 | + ```json |
| 24 | + { |
| 25 | + "exports": { |
| 26 | + ".": { |
| 27 | + "types": "./dist/index.d.ts", |
| 28 | + "import": "./dist/index.js", |
| 29 | + "default": "./dist/index.js" |
| 30 | + } |
| 31 | + }, |
| 32 | + "types": "./dist/index.d.ts" |
| 33 | + } |
| 34 | + ``` |
| 35 | + - Set minimums: `"engines": { "node": ">=20.19.0" }` and `"peerDependencies": { "rollup": ">=4.0.0" }`. |
| 36 | + - Keep `rollup` as a devDependency only if tests use it. Otherwise remove it. |
| 37 | + - Ensure published files include build output and standard docs: |
| 38 | + ```json |
| 39 | + "files": ["dist", "README.md", "LICENSE"] |
| 40 | + ``` |
| 41 | + Notes: |
| 42 | + - `package.json` `files` does not support negation patterns. If an existing `package.json` contains "files": [ ..., "!dist/**/*.map", ... ], remove the negated entry—negation is not supported and will be ignored. |
| 43 | + - Always publish source maps. Do not exclude `dist/**/*.map` via `.npmignore`, and do not disable `sourceMap`/`declarationMap` for published packages. If a package currently excludes maps (via `.npmignore` or tsconfig), remove those exclusions so maps are included. |
| 44 | + |
| 45 | +3. Build scripts: TypeScript emit to dist |
| 46 | + |
| 47 | + - Prefer a tsc-only build for packages that do not need bundling: |
| 48 | + - In `$PKG/package.json`, set scripts: |
| 49 | + ```json |
| 50 | + "prebuild": "del-cli dist", |
| 51 | + "build": "tsc --project tsconfig.json", |
| 52 | + "pretest": "pnpm build", |
| 53 | + "prerelease": "pnpm build", |
| 54 | + "prepare": "if [ ! -d 'dist' ]; then pnpm build; fi" |
| 55 | + ``` |
| 56 | + - 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. |
| 57 | + |
| 58 | +4. TypeScript config: use the shared plugin config (symlink) |
| 59 | + |
| 60 | + - Replace any existing `$PKG/tsconfig.json` with a symlink to the shared plugin config (`.config/tsconfig.plugin.json`), which already enables emit to `dist/` and declaration maps: |
| 61 | + ```bash |
| 62 | + # from repo root |
| 63 | + ln -snf ../../.config/tsconfig.plugin.json "$PKG/tsconfig.json" |
| 64 | + git add "$PKG/tsconfig.json" |
| 65 | + ``` |
| 66 | + On Windows PowerShell, you can run: |
| 67 | + ```powershell |
| 68 | + # from repo root |
| 69 | + $pkg = 'packages/<name>' |
| 70 | + New-Item -ItemType SymbolicLink -Path "$pkg/tsconfig.json" -Target (Resolve-Path ".config/tsconfig.plugin.json") -Force |
| 71 | + git add "$pkg/tsconfig.json" |
| 72 | + ``` |
| 73 | + The shared config content lives at `.config/tsconfig.plugin.json`. |
| 74 | + - 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). |
| 75 | + |
| 76 | +5. Source: convert to pure ESM and modern Node APIs |
| 77 | + |
| 78 | + - Replace `require`, `module.exports`, and `__dirname` patterns with ESM equivalents. |
| 79 | + - Use `node:` specifiers for built-ins (e.g., `import path from 'node:path'`). |
| 80 | + - Prefer the latest ES APIs: use `import.meta.dirname` and `import.meta.filename` (Node ≥20.11) instead of re‑creating them via `fileURLToPath`. |
| 81 | + |
| 82 | + ```ts |
| 83 | + import path from 'node:path'; |
| 84 | + |
| 85 | + const here = import.meta.dirname; |
| 86 | + // const file = import.meta.filename; |
| 87 | + const pkgJson = path.join(here, 'package.json'); |
| 88 | + ``` |
| 89 | + |
| 90 | + Use URL utilities only when specifically needed (e.g., for non‑file module URLs): `fileURLToPath(new URL('.', import.meta.url))`. |
| 91 | + |
| 92 | + - Inline and export public types from `src/index.ts`; avoid separate `types/` unless unavoidable. |
| 93 | + - Conversion rules for file types: |
| 94 | + - Always convert any `.js` in `src/` to `.ts`. |
| 95 | + - Never convert files in test `fixtures/` to TypeScript—keep fixtures exactly as authored. |
| 96 | + - Always convert any `.js` in `test/` to `.ts` (test sources only; exclude `test/**/fixtures/**`). |
| 97 | + |
| 98 | +6. Tests: order of operations (AVA → Vitest) and ESM |
| 99 | + |
| 100 | + - Remove CJS-specific branches/assertions from tests. |
| 101 | + - Follow this sequence for reliability: |
| 102 | + 1. After JS→TS conversion in `src/` and `test/` and after any `package.json` changes, always run the AVA test suite. |
| 103 | + 2. Only after verifying AVA tests run without modifications, convert the AVA tests to Vitest. |
| 104 | + 3. After converting tests to Vitest, always run the Vitest suite after any subsequent `src/` change. |
| 105 | + - Note: If a package already uses Vitest, start at step 3 and skip AVA‑specific steps. |
| 106 | + - Ensure Rollup bundles created in tests are `await bundle.close()`-d to avoid leaks. |
| 107 | + |
| 108 | +7. Clean up package artifacts |
| 109 | + - Remove obsolete files that are no longer used by ESM-only publishing (examples): |
| 110 | + - `$PKG/rollup.config.*` if switching to tsc-only. |
| 111 | + - `$PKG/types/**` once declarations are generated to `dist/`. |
| 112 | + |
| 113 | +## Verify |
| 114 | + |
| 115 | +- Build succeeds and emits JS and d.ts to `dist/`: |
| 116 | + ```bash |
| 117 | + pnpm -C $PKG build |
| 118 | + tree $PKG/dist | sed -n '1,80p' |
| 119 | + ``` |
| 120 | +- Symlink exists and points at the shared config: |
| 121 | + ```bash |
| 122 | + test -L "$PKG/tsconfig.json" && ls -l "$PKG/tsconfig.json" || (echo "tsconfig.json symlink missing" && exit 1) |
| 123 | + ``` |
| 124 | +- Type declarations resolve for consumers: |
| 125 | + ```bash |
| 126 | + jq -r '.types, .exports["."].types, .exports["."].import' $PKG/package.json |
| 127 | + ``` |
| 128 | +- Runtime smoke (Node ESM import works): |
| 129 | + ```bash |
| 130 | + node -e "import('file://$PWD/$PKG/dist/index.js').then(() => console.log('ok'))" |
| 131 | + ``` |
| 132 | +- Tests pass for the package (runner may be AVA or Vitest depending on the package): |
| 133 | + ```bash |
| 134 | + pnpm -C $PKG test |
| 135 | + ``` |
| 136 | + |
| 137 | +## Rollback |
| 138 | + |
| 139 | +- Revert the package directory to the previous commit (modern Git): |
| 140 | + ```bash |
| 141 | + git restore -SW $PKG |
| 142 | + ``` |
| 143 | +- If needed, `git reset --hard HEAD~1` when this package’s change is isolated on a feature branch. |
| 144 | + |
| 145 | +## References |
| 146 | + |
| 147 | +- Alias migration (ESM-only) — PR #1926: feat(alias)!: ESM only. Update Node and Rollup minimum versions |
| 148 | +- Task spec used for alias — Issue #1925 |
| 149 | +- Shared TS configs used by packages — `.config/tsconfig.base.json`, `.config/tsconfig.plugin.json` |
0 commit comments