Skip to content

Convert TypeScript package to ESM - #261

Open
Arechii wants to merge 8 commits into
masterfrom
typescript-esm-only
Open

Arechii wants to merge 8 commits into
masterfrom
typescript-esm-only

Conversation

@Arechii

@Arechii Arechii commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Publishes @blueyerobotics/protocol-definitions as native ES modules. Existing import specifiers keep working, and require() still works on current Node.

  • tsconfig.json: module/moduleResolution set to NodeNext, target raised to ES2022.
  • package.json: adds "type": "module" and an exports map, drops main, bumps the version to 3.4.0.
  • Generation: the protoc invocation moves into a generate npm script with esModuleInterop=true and importSuffix=.js (NodeNext requires explicit .js extensions). CI and CLAUDE.md now share the script, so the flags can't drift apart.
  • CI: a new step packs the tarball and checks the root import and require(), both deep-import forms, and the package.json subpath against the real exports map. The TypeScript workflow now also runs on pull requests, with the npm publish and the blueye-ts dispatch limited to pushes to master, so these checks run before merge instead of after.

Why

This is the last step of the 2026-04-30 ESM migration, after BluEye-Robotics/jszmq#2 and BluEye-Robotics/blueye-ts#7. While this package is CommonJS, ESM tooling has to guess its named exports, and Vite's dependency optimizer guesses wrong for export *: it kept only default, so import { blueye } from "@blueyerobotics/blueye-ts" failed in the browser. blueye-ts worked around it with an explicit re-export (dffbc78d). Publishing ESM removes that whole class of interop bug for any future consumer. As a side effect, a one-message esbuild bundle drops by about 12% (583.7 KB → 515.8 KB).

Compatibility

"exports": {
  ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
  "./dist/*.js": "./dist/*.js",
  "./dist/*": "./dist/*.js",
  "./package.json": "./package.json"
}
  • Root import: unchanged, import { blueye } from "@blueyerobotics/protocol-definitions".
  • require(): the . entry uses a default condition rather than import, so require() resolves on Node 22.12+ / 20.19+ through require(esm). The generated code has no top-level await. On older Node, or under Jest in CommonJS mode, require() fails.
  • Deep imports: exports targets never get an extension appended, so ./dist/* maps to ./dist/*.js. That keeps extensionless specifiers like …/dist/mission_planning working, which BlueyeCloud uses in about 18 files. ./dist/*.js takes precedence for specifiers that already end in .js.
  • package.json: exported explicitly, since the exports map would otherwise block it.

Shipping only ESM, rather than dual-publishing, also avoids the dual package hazard.

Dependents

Consumer Status
blueye-ts Already ESM and imports only from the root. It picks this release up through the existing automatic protocol-bump dispatch. No changes needed.
BlueyeCloud Already ESM (Vite). Pinned to 3.2.0-d6c9260a, and its extensionless deep imports resolve under the new map. No changes needed to upgrade.
p2-django, sonar-mosaic, dive-viewer ESM and use the package only through blueye-ts. Unaffected.

Why 3.4.0 and not 4.0.0

CI publishes every master push as a prerelease X.Y.Z-<sha>, and semver ranges never match prereleases of a different X.Y.Z, so ^3.3.0-<sha> doesn't match 3.4.0-<sha>. A minor bump therefore already stops npm update from moving anyone onto the ESM build unannounced, and both internal consumers pin exact versions anyway. The only breaks are require() on Node older than 22.12 / 20.19 and CommonJS-mode Jest, neither of which applies to any known consumer, so a major version isn't needed.

🤖 Generated with Claude Code

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>
@follesoe
follesoe requested review from jp-pino and a lite review from Copilot May 22, 2026 06:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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: NodeNext and raise target to ES2022.
  • Convert the npm package to ESM-only via "type": "module" and an exports map (dropping the previous main entrypoint).
  • Adjust CI generation to emit .js-suffixed relative imports and enable esModuleInterop in 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.

Comment thread package.json Outdated
Comment thread package.json Outdated
- 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>
@Arechii Arechii changed the title Convert TypeScript package to ESM-only Convert TypeScript package to ESM Aug 18, 2026
Arechii and others added 3 commits August 18, 2026 21:04
"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>
Copilot AI review requested due to automatic review settings August 21, 2026 10:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/telemetry literally 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 .js form), or document this additional breaking change instead of claiming the old specifiers remain supported.
    "./dist/*": "./dist/*"

Arechii and others added 3 commits September 25, 2026 15:19
Resolve the package.json version conflict as 3.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`exports` targets never get an extension appended, so `"./dist/*": "./dist/*"`
broke specifiers like `…/dist/mission_planning` that resolved before the
exports map existed — BlueyeCloud imports this way in ~18 files. Map
`./dist/*` to `./dist/*.js`, with a `./dist/*.js` entry taking precedence
for specifiers that already carry the extension. Also export
`./package.json`, which the exports map otherwise blocks.

The CI smoke test now covers both deep-import forms and the package.json
subpath.

Drop the protocol-version section from the npm README; it existed to
explain a 4.0.0 bump that is no longer happening.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workflow only ran on pushes to master, so generation, compilation
and the entrypoint smoke test were first exercised after merge. Run
them on pull requests too, and limit the npm publish and the blueye-ts
bump dispatch to pushes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants