Skip to content
136 changes: 136 additions & 0 deletions .charlie/playbooks/upgrade-plugin-to-esm-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Upgrade a plugin package to ESM-only (packages/<name>)

Upgrade a single plugin under `packages/<name>` to publish ESM-only output with TypeScript-emitted JS and declarations.

## Prerequisites

- Repo already contains shared config at `.config/tsconfig.base.json` and `.config/tsconfig.plugin.json` and (optionally) `.config/vitest.config.mts` from prior migrations.
- Scope constraint: make changes only inside the target package directory (e.g., `packages/alias`). Do not add or edit files outside `packages/<name>`.
- Local Node 20.19+ to run builds and tests.

## Steps

1. Identify the target package

- Set a shell variable for reuse: `PKG=packages/<name>`.

2. Package metadata: ESM-only and minimums

- Edit `$PKG/package.json`:

- Set `"type": "module"`.
- Replace legacy `main/module/exports.require` with an ESM-only export mapped via the explicit `"."` entry for broad tooling compatibility:
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"types": "./dist/index.d.ts"
}
```
- Set minimums: `"engines": { "node": ">=20.19.0" }` and `"peerDependencies": { "rollup": ">=4.0.0" }`.
- Keep `rollup` as a devDependency only if tests use it. Otherwise remove it.
- Ensure published files include build output and standard docs:
```json
"files": ["dist", "README.md", "LICENSE"]
```
Note: `package.json` `files` does not support negation patterns. To exclude maps from the published package, add an `.npmignore` entry:

```
dist/**/*.map
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CharlieHelps packages should never omit *.map from "files" period.

Copy link
Contributor Author

@charliecreates charliecreates bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand this to see my work.
  • Submitted a review on PR #1934 noting in .charlie/playbooks/upgrade-plugin-to-esm-only.md that files must include *.map and source maps must be published.
  • Pinpointed the conflicting guidance in .charlie/playbooks/upgrade-plugin-to-esm-only.md near the files block (lines ~41–49) that suggests excluding dist/**/*.map via .npmignore and disabling sourceMap/declarationMap.
  • Drafted corrected guidance: always publish maps; remove !dist/**/*.map from files; do not exclude dist/**/*.map in .npmignore; avoid disabling sourceMap/declarationMap for published packages.
  • Edited .charlie/playbooks/upgrade-plugin-to-esm-only.md to replace map-exclusion advice with: "Always publish source maps. Do not exclude dist/**/*.map via .npmignore, and do not disable sourceMap/declarationMap"; also clarified that files negation is not supported.
  • Ran Prettier to format the repository; .charlie/playbooks/upgrade-plugin-to-esm-only.md is formatted and saved cleanly.
  • Verified working tree with git status --porcelain: 1 modified file (.charlie/playbooks/upgrade-plugin-to-esm-only.md).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — updated the playbook to always publish source maps and removed the guidance to exclude .map files or disable map emission. Commit: ca2ba63.

Changes
  • .charlie/playbooks/upgrade-plugin-to-esm-only.md
    • Replaced the “exclude maps via .npmignore / disable maps in tsconfig” advice with a clear policy: always publish source maps.
    • Kept the note to remove any existing "!dist/**/*.map" entry since files negation isn’t supported.
Verification
# Prettier: OK
$ pnpm prettier:check
All matched files use Prettier code style!

# ESLint: 0 errors, 10 warnings
$ pnpm lint

Re-requested review from @shellscape and @CharlieHelps.

```

If you must disable map emission, either update the shared `.config/tsconfig.plugin.json` (affects all packages) or create a package-local `tsconfig.build.json` that extends it with `"sourceMap": false` and `"declarationMap": false`, then change the build script to `tsc --project tsconfig.build.json`.

If an existing `package.json` contains `"files": [ ..., "!dist/**/*.map", ... ]`, remove the negated entry—negation is not supported and will be ignored.

3. Build scripts: TypeScript emit to dist

- Prefer a tsc-only build for packages that do not need bundling:
- In `$PKG/package.json`, set scripts:
```json
"prebuild": "del-cli dist",
"build": "tsc --project tsconfig.json",
"pretest": "pnpm build",
"prerelease": "pnpm build",
"prepare": "if [ ! -d 'dist' ]; then pnpm build; fi"
```
- 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.

4. TypeScript config: use the shared plugin config (symlink)

- 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:
```bash
# from repo root
ln -snf ../../.config/tsconfig.plugin.json "$PKG/tsconfig.json"
git add "$PKG/tsconfig.json"
```
On Windows PowerShell, you can run:
```powershell
# from repo root
$pkg = 'packages/<name>'
New-Item -ItemType SymbolicLink -Path "$pkg/tsconfig.json" -Target (Resolve-Path ".config/tsconfig.plugin.json") -Force
git add "$pkg/tsconfig.json"
```
The shared config content lives at `.config/tsconfig.plugin.json`.
- 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).

5. Source: convert to pure ESM and modern Node APIs

- Replace `require`, `module.exports`, and `__dirname` patterns with ESM equivalents.
- Use `node:` specifiers for built-ins (e.g., `import path from 'node:path'`).
- Prefer URL utilities where needed (`fileURLToPath(new URL('.', import.meta.url))`).
- Inline and export public types from `src/index.ts`; avoid separate `types/` unless unavoidable.

6. Tests: drop CJS branches; ESM everywhere

- Remove CJS-specific branches/assertions from tests.
- 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.
- Ensure Rollup bundles created in tests are `await bundle.close()`-d to avoid leaks.

7. Clean up package artifacts
- Remove obsolete files that are no longer used by ESM-only publishing (examples):
- `$PKG/rollup.config.*` if switching to tsc-only.
- `$PKG/types/**` once declarations are generated to `dist/`.

## Verify

- Build succeeds and emits JS and d.ts to `dist/`:
```bash
pnpm -C $PKG build
tree $PKG/dist | sed -n '1,80p'
```
Comment on lines +116 to +119
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree is not universally available (missing on macOS by default and many CI images). The verify step should use a more portable alternative (POSIX and PowerShell options).

Suggestion

Suggest portable alternatives:

  • POSIX:
find "$PKG/dist" -maxdepth 2 -type f | sed -n '1,80p'
  • Windows PowerShell (assuming $pkg = 'packages/<name>' as in the symlink step):
Get-ChildItem -Recurse -Depth 2 "$pkg/dist" | Select-Object -First 80

Reply with "@CharlieHelps yes please" if you’d like me to update the Verify section with these alternatives.

- Symlink exists and points at the shared config:
```bash
test -L "$PKG/tsconfig.json" && ls -l "$PKG/tsconfig.json" || (echo "tsconfig.json symlink missing" && exit 1)
```
- Type declarations resolve for consumers:
```bash
jq -r '.types, .exports["."].types, .exports["."].import' $PKG/package.json
```
Comment on lines +124 to +127
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verify step checks types and import but not default, even though the exports["."] example includes it. Also, jq isn’t common on Windows; a Node alternative would improve portability.

Suggestion

Expand the check and add a Node-based alternative:

  • jq (POSIX):
jq -r '.types, .exports["."].types, .exports["."].import, .exports["."].default' "$PKG/package.json"
  • Node (cross-platform):
node -e "const fs=require('node:fs');const pkg=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));console.log(pkg.types,pkg.exports?.['.']?.types,pkg.exports?.['.']?.import,pkg.exports?.['.']?.default)" "$PKG/package.json"

Reply with "@CharlieHelps yes please" if you want me to apply these updates.

- Runtime smoke (Node ESM import works):
```bash
node -e "import('file://$PWD/$PKG/dist/index.js').then(() => console.log('ok'))"
```
Comment on lines +128 to +131
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime smoke test uses $PWD and a Bash‑style expansion that’s not Windows‑friendly. A path‑safe Node snippet avoids quoting issues across shells and OSes.

Suggestion

Replace the command with a cross‑platform Node one‑liner that resolves the file path safely:

node -e "const {resolve}=require('node:path');const {pathToFileURL}=require('node:url');import(pathToFileURL(resolve(process.argv[1]))).then(()=>console.log('ok'))" "$PKG/dist/index.js"

Reply with "@CharlieHelps yes please" and I’ll update the Verify snippet accordingly.

- Tests pass for the package (runner may be AVA or Vitest depending on the package):
```bash
pnpm -C $PKG test
```

## Rollback

- Revert the package directory to the previous commit (modern Git):
```bash
git restore -SW $PKG
```
- If needed, `git reset --hard HEAD~1` when this package’s change is isolated on a feature branch.

## References

- Alias migration (ESM-only) — PR #1926: feat(alias)!: ESM only. Update Node and Rollup minimum versions
- Task spec used for alias — Issue #1925
- Shared TS configs used by packages — `.config/tsconfig.base.json`, `.config/tsconfig.plugin.json`
Loading