Convert TypeScript package to ESM - #261
Open
Arechii wants to merge 5 commits into
Open
Conversation
Switches the npm package from CommonJS to ESM-only output. - tsconfig.json: module/moduleResolution NodeNext, target ES2022 - package.json: adds "type": "module" and "exports" map; drops "main" - CI Generate: adds esModuleInterop=true and importSuffix=.js so ts-proto emits relative imports with .js extensions for NodeNext Breaking change for CommonJS consumers (require() is blocked by the exports map); a major version bump should accompany this before publishing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR migrates the @blueyerobotics/protocol-definitions npm package build output from CommonJS to ESM-only, aligning the TypeScript compiler and ts-proto generation with Node’s ESM/NodeNext requirements.
Changes:
- Update TypeScript compilation to
module/moduleResolution: NodeNextand raisetargettoES2022. - Convert the npm package to ESM-only via
"type": "module"and anexportsmap (dropping the previousmainentrypoint). - Adjust CI generation to emit
.js-suffixed relative imports and enableesModuleInteropin ts-proto output.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tsconfig.json | Switches TS compiler settings to NodeNext ESM output and ES2022 target. |
| package.json | Declares ESM-only package semantics and defines an exports map for the entrypoint/types. |
| .github/workflows/ci-typescript.yaml | Updates ts-proto generation options to be compatible with NodeNext ESM resolution. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Bump to 4.0.0: the output format change is breaking, and the `3.2.0-<sha>` prerelease scheme means a `^3.2.0-<sha>` range would otherwise let `npm update` move consumers onto the ESM build silently. - Use a `default` export condition instead of `import`, so `require()` still resolves on Node 22.12+ via `require(esm)` instead of failing with ERR_PACKAGE_PATH_NOT_EXPORTED. - Restore deep imports via a `./dist/*` subpath export, preserving the specifiers that worked before the `exports` map was introduced. - Move the protoc invocation into a `generate` npm script so CI and the docs cannot drift; CLAUDE.md documented flags that no longer compiled under NodeNext. - Add a CI step that packs the tarball and verifies both the ESM import and the CJS require against the real `exports` map. - Document the module format in README.npm.md and ignore `dist/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"ESM only" reads as "you cannot require this", which is not true here — require() resolves on Node 22.12+ via require(esm). Lead with what both loaders actually do instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Match the README wording — "ESM-only" implies require() is unavailable, which it is not. Also record why the exports map uses a `default` condition, so it does not get "corrected" back to `import` later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The published README (README.npm.md, copied over README.md at publish time) never mentioned that this implements protocol v3, so npm consumers had no signal beyond inferring it from the leading version digit. That inference was never reliable — the NuGet package sits at 5.4.0 for the same protocol v3 — so state it explicitly and note that the package version is a separate axis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
package.json:16
- This wildcard maps
@blueyerobotics/protocol-definitions/dist/telemetryliterally to./dist/telemetry; unlike the old extensionless CommonJS path resolution, package export targets do not search for.js, so that previously valid deep import now fails. If preserving deep-import compatibility is intended, add an extensionless compatibility mapping (while retaining the.jsform), or document this additional breaking change instead of claiming the old specifiers remain supported.
"./dist/*": "./dist/*"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Publishes
@blueyerobotics/protocol-definitionsas an ESM package, while keeping CommonJS consumers working on modern Node.tsconfig.json:module/moduleResolutionset toNodeNext,targetraised toES2022. This makes TypeScript's resolution match what Node actually does at runtime.package.json: adds"type": "module"and anexportsmap; dropsmain. Version bumped to 4.0.0.generatenpm script, withesModuleInterop=trueandimportSuffix=.jsso ts-proto emits the explicit.jsextensions NodeNext requires.exportsmap.Compatibility
The
.entry uses adefaultcondition rather thanimport:importmatches ESM callers only, sorequire()would fall through the map and fail withERR_PACKAGE_PATH_NOT_EXPORTED.defaultmatches any loader, and Node 22.12+ / 20.19+ canrequire()an ES module (require(esm)) as long as the graph has no top-level await — generated protobuf code has none. Verified locally:import { blueye } from "@blueyerobotics/protocol-definitions"— worksrequire("@blueyerobotics/protocol-definitions")— returns['blueye', 'google']module: nodenext(TS 5.9) — cleanOn Node older than 22.12,
require()fails withERR_REQUIRE_ESM.The
./dist/*subpath keeps deep imports working with the same specifiers as before theexportsmap existed.Why 4.0.0
This does not mean protocol v4. The package still implements protocol v3; the npm version tracks releases of this package, not the protocol generation. The two were never coupled — the NuGet package is at
5.4.0for the same protocol v3, and the CMake project at3.0.0. Since the published npm README never actually stated the protocol version, this PR adds it toREADME.npm.mdexplicitly, which is a firmer signal than a leading digit.CI publishes
3.2.0-<sha>on every master push under thelatesttag. A consumer who rannpm installhas^3.2.0-<sha>in their manifest, and that caret range does match other3.2.0-*prereleases, since prerelease identifiers compare lexically. Without a major bump,npm updatecould walk a consumer onto the ESM build with no signal.4.0.0breaks the range and forces an explicit upgrade.Notes
@bufbuild/protobufdual-publishes, and the previous CJS build already worked for ESM consumers viacjs-module-lexernamed-export detection. The wins here are correct NodeNext type resolution and roughly 12% smaller bundles (583.7 KB → 515.8 KB for a one-message esbuild bundle); tree-shaking is limited because ts-proto's encode/decode closures keep most of the graph reachable.instanceofchecks that silently fail across the boundary.CLAUDE.mddocumented a protoc command without the new flags, which producedTS2835errors on every file under the new tsconfig. Folding the flags into thegeneratescript keeps CI and the docs in sync.🤖 Generated with Claude Code