Skip to content

fix(jco): re-export browser bindgen from @bytecodealliance/jco-transpile#1751

Open
zacharywhitley wants to merge 1 commit into
bytecodealliance:mainfrom
zacharywhitley:fix/ship-obj-in-tarball
Open

fix(jco): re-export browser bindgen from @bytecodealliance/jco-transpile#1751
zacharywhitley wants to merge 1 commit into
bytecodealliance:mainfrom
zacharywhitley:fix/ship-obj-in-tarball

Conversation

@zacharywhitley

@zacharywhitley zacharywhitley commented Jul 21, 2026

Copy link
Copy Markdown

Problem

@bytecodealliance/jco@1.25.2 (published 2026-07-09) ships a broken browser build. packages/jco/src/browser.js imports the bindgen artifact from ../obj/js-component-bindgen-component.js, but packages/jco/package.json's files array is only ["lib","src","types"], so npm publish drops the entire obj/ directory and any bundler resolving the browser conditional export fails:

[plugin:vite:import-analysis] Failed to resolve import
"../obj/js-component-bindgen-component.js"
from "node_modules/@bytecodealliance/jco/src/browser.js".

Issue #646 tracked the same class of regression in 1.11.0. It has re-regressed since the vendoring reorg (#1732 / #1744) — obj/ is no longer produced next to browser.js at publish time.

Fix (revised per maintainer guidance)

Per @vados-cosmonic in #1751 (comment), this PR now wires the browser entry through @bytecodealliance/jco-transpile rather than re-adding the obj/* globs to packages/jco's files list. The sibling jco-transpile package already vendors the bindgen under vendor/js-component-bindgen-component.js and ships it via its files array, so exposing it as a subpath export makes it consumable from jco (and any other downstream) without duplicating the artifact.

  • packages/jco-transpile/package.json: add a ./component subpath to exports pointing at ./vendor/js-component-bindgen-component.{js,d.ts}. Version bumped to 0.4.3 (patch — non-breaking additive export).
  • packages/jco/src/browser.js: replace import ... from "../obj/js-component-bindgen-component.js" with import ... from "@bytecodealliance/jco-transpile/component". Public exports (generate, generateTypes, transpile, and their $init-await wrapping) are preserved for backwards compatibility.
  • packages/jco/package.json: repoint the ./component types export from the (unshipped) ./obj/js-component-bindgen-component.d.ts to ./types/browser.d.ts, which is already produced by build:ts and covered by the types entry in files. obj/ is no longer referenced from anywhere the tarball needs to see.

Note: packages/jco/package.json's @bytecodealliance/jco-transpile dependency is left at ^0.4.2 — that satisfies workspace-linked 0.4.3 in local dev, and once jco-transpile@0.4.3 is on the registry, npm/pnpm consumers of a future jco release automatically resolve to a version that has the new subpath. Maintainers may prefer to bump the range explicitly at release time.

Regression test

Added a browser-bundle-smoke job to .github/workflows/main.yml (release gate — runs on every PR):

  1. Restores the existing build / jco-transpile-vendor artifacts.
  2. Builds TypeScript for both packages so types/ and dist/ are in place.
  3. npm pack --ignore-scripts for both packages into a scratch dir.
  4. Asserts the jco tarball does not ship an obj/ tree (guards against re-adding obj/* to files "just in case").
  5. Installs both tarballs into an ephemeral project and runs esbuild --bundle --platform=browser on import '@bytecodealliance/jco/component'.
  6. Fails the CI check if bundling fails or if out.js is empty.

node:* is marked external in the smoke test because the vendored bindgen has runtime feature detection with a dynamic await import('node:fs/promises') for host env use; a browser code path never reaches that import and the smoke test only needs to prove import resolution and bundling succeed. This is the same class of check upstream would need for a real browser E2E test but doesn't require puppeteer / a browser runtime, so it runs on every PR.

Verification

Locally, before the fix, esbuild --bundle --platform=browser on import '@bytecodealliance/jco/component' fails at the ../obj/... import. After the fix, packing both packages, installing into /tmp/jco-bundler-smoke, and running:

npx esbuild main.js --bundle --format=esm --platform=browser --outfile=out.js --external:'node:*'

produces a 410 KB out.js and exits 0. The packed bytecodealliance-jco-1.25.2.tgz contains zero entries under obj/ (confirmed with tar tzf ... | grep -c obj0).

References

Branch-name note

Branch is still fix/ship-obj-in-tarball — the name predates the maintainer's redirect to jco-transpile and is kept as-is to preserve this PR's URL. The commit and description reflect the actual approach.

@vados-cosmonic

vados-cosmonic commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Hey @zacharywhitley thanks for the report -- this is certainly a regression. The fix here should be to use the exports from jco-transpile, which is where all this functionality lives now, rather than trying to include the obj files directly in Jco itself.

Would you be up for trying to make those changes?

(and a regression test would be appreciated too, just to avoid this happening again -- Jco in the browser has not gotten enough attention as of late and a E2E test or even a release gate like we have in CI would be incredibly helpful)

@zacharywhitley

Copy link
Copy Markdown
Author

Absolutely, I'll give it a go this evening. Thanks

@vados-cosmonic

Copy link
Copy Markdown
Collaborator

Appreciate it 🙇 Thanks for banging on this and helping fix it -- Jco in the browser definitely needs way more attention than we've (at the very least I) been able to give it up until now.

The `@bytecodealliance/jco` browser entry (`src/browser.js`) imports the
transpiled `js-component-bindgen-component.js` from `../obj/`, but
`packages/jco/package.json`'s `files` array only lists `lib`, `src`, and
`types`. As a result, `npm publish` drops the entire `obj/` directory and
any bundler resolving jco's `browser` conditional export fails with
`Failed to resolve import "../obj/js-component-bindgen-component.js"`.

Per maintainer guidance in bytecodealliance#1751, wire the browser
entry through `@bytecodealliance/jco-transpile` instead of trying to ship
`obj/` from jco itself. `jco-transpile` already vendors the bindgen under
`vendor/` and lists it in its `files` array, so a new `./component`
subpath export makes that artifact reachable from downstream packages:

- `packages/jco-transpile/package.json`: add `./component` to `exports`
  pointing at `./vendor/js-component-bindgen-component.{js,d.ts}`; bump
  to `0.4.3` to expose the new subpath.
- `packages/jco/src/browser.js`: import `$init`, `generate`, and
  `generateTypes` from `@bytecodealliance/jco-transpile/component`
  instead of `../obj/js-component-bindgen-component.js`. Public exports
  and their $init-await wrapping are unchanged.
- `packages/jco/package.json`: repoint the `./component` types export
  from the (unshipped) `./obj/js-component-bindgen-component.d.ts` to
  `./types/browser.d.ts`, which is already produced by `build:ts` and
  covered by the `types` entry in `files`.

Also add a `browser-bundle-smoke` job to `.github/workflows/main.yml`
that packs both packages, installs the tarballs into a scratch project,
and bundles `import '@bytecodealliance/jco/component'` with
`esbuild --platform=browser`. The job additionally asserts the jco
tarball does not ship an `obj/` tree. This is the class of regression
that bytecodealliance#646 originally fixed for 1.11.1 and that recurred in 1.25.2; a
release gate against packed tarballs catches it without needing a real
browser test harness.
@zacharywhitley
zacharywhitley force-pushed the fix/ship-obj-in-tarball branch from 81fac20 to 2b00711 Compare July 22, 2026 00:31
@zacharywhitley zacharywhitley changed the title fix(jco): ship obj/ dir with the browser bindgen artifact fix(jco): re-export browser bindgen from @bytecodealliance/jco-transpile Jul 22, 2026
@zacharywhitley

Copy link
Copy Markdown
Author

Pushed the redirect. Approach (Option B): browser entry now imports $init, generate, generateTypes from a new @bytecodealliance/jco-transpile/component subpath, which points at the already-vendored vendor/js-component-bindgen-component.js. jco-transpile bumped to 0.4.3 for the additive export; jco's ./component types pointer moved to the shipped types/browser.d.ts.

Regression test: .github/workflows/main.yml gains a browser-bundle-smoke job that packs both packages, refuses to publish if the jco tarball contains an obj/ tree, and bundles import '@bytecodealliance/jco/component' with esbuild --platform=browser against those tarballs. That catches the exact failure mode (relative import → missing file after npm publish) without needing a real browser test harness. Verified locally end-to-end: bundle succeeds (410 KB output, exit 0); rolling back browser.js to the old ../obj/... import breaks the same job (esbuild can't resolve the import).

Two notes worth flagging:

  • I left jco's @bytecodealliance/jco-transpile dependency range at ^0.4.2 on the assumption you'd prefer to bump it explicitly at release time. If you'd rather the PR bumps it to ^0.4.3 now (or switches to workspace:^0.4.3), happy to amend.
  • The smoke test marks node:* external because the vendored bindgen has a dynamic await import('node:fs/promises') for its Node-host code path; a browser build never reaches it, but a naive esbuild invocation without --external:'node:*' would still surface the warning. If you'd rather the gate use a bundler that handles this via its own browser polyfills (Vite, rolldown, etc.), let me know and I'll swap it.

Ready for another look @vados-cosmonic.

@vados-cosmonic vados-cosmonic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is almost there, thanks for making the changes, @zacharywhitley -- I've left just a few more points to address/think about.

So basically I think when I release this, I'm going to:

  • release the new version of jco-transpile first (leaving the workspace: dep in place for jco)
  • remove the workspace: dep for jco, replacing it with the updated jco-transpile
  • release jco

Comment on lines +528 to +531
mkdir -p /tmp/jco-pack
(cd packages/jco-transpile && npm pack --pack-destination /tmp/jco-pack --ignore-scripts)
(cd packages/jco && npm pack --pack-destination /tmp/jco-pack --ignore-scripts)
ls -la /tmp/jco-pack

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You should be able to use pnpm --filter <project> pack --pack-destination <path> for this I think

fi

- name: Bundle the browser entry with esbuild against the packed tarballs
run: |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What do you think about compress all these intermediate steps of this into one JS script (scripts/build-browser-bundle.mjs?)

{
"name": "@bytecodealliance/jco-transpile",
"version": "0.4.2",
"version": "0.4.3",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yeah let's leave this unchanged for now -- right now our release scripts will do this update automatically (since we usually don't know how many breaking changes got in until we cut a release)...

It's a bit weird, but we do stay on the previous version until the new release actually happens.

@vados-cosmonic

Copy link
Copy Markdown
Collaborator

Pushed the redirect. Approach (Option B): browser entry now imports $init, generate, generateTypes from a new @bytecodealliance/jco-transpile/component subpath, which points at the already-vendored vendor/js-component-bindgen-component.js. jco-transpile bumped to 0.4.3 for the additive export; jco's ./component types pointer moved to the shipped types/browser.d.ts.

Regression test: .github/workflows/main.yml gains a browser-bundle-smoke job that packs both packages, refuses to publish if the jco tarball contains an obj/ tree, and bundles import '@bytecodealliance/jco/component' with esbuild --platform=browser against those tarballs. That catches the exact failure mode (relative import → missing file after npm publish) without needing a real browser test harness. Verified locally end-to-end: bundle succeeds (410 KB output, exit 0); rolling back browser.js to the old ../obj/... import breaks the same job (esbuild can't resolve the import).

Two notes worth flagging:

* I left jco's `@bytecodealliance/jco-transpile` dependency range at `^0.4.2` on the assumption you'd prefer to bump it explicitly at release time. If you'd rather the PR bumps it to `^0.4.3` now (or switches to `workspace:^0.4.3`), happy to amend.

* The smoke test marks `node:*` external because the vendored bindgen has a dynamic `await import('node:fs/promises')` for its Node-host code path; a browser build never reaches it, but a naive esbuild invocation without `--external:'node:*'` would still surface the warning. If you'd rather the gate use a bundler that handles this via its own browser polyfills (Vite, rolldown, etc.), let me know and I'll swap it.

Ready for another look @vados-cosmonic.

As far as the things you flagged:

  1. that's great for me -- we bump explicitly at release time -- but this extends also to jco-transpile, unfortunately (I left a comment).

  2. Thanks for adding an early smoke test. As in my comment the only thing I think I'd ask for is that the build actually just is implemented as a script that we can run rather than chunks of bash.

Comment on lines +553 to +555
npm install --no-save --no-audit --no-fund \
/tmp/jco-pack/bytecodealliance-jco-transpile-*.tgz \
/tmp/jco-pack/bytecodealliance-jco-*.tgz
@vados-cosmonic

Copy link
Copy Markdown
Collaborator

Oh and one more thing -- if we could use the pnpm equivalents everywhere (pnpm exec, etc) that would be great -- we switched to pnpm a while ago for it's security posture, and though npm is better now I think it's better to just be pnpm-first/pnpm only going forward.

@zacharywhitley

Copy link
Copy Markdown
Author

Will do. Happy to be contributing. I have another project that I'm working on that works with the CM. It was originally for implementing the CM for Endive but it turned out to be generally applicable and I'm porting it over to the browser. It comes from a very different direction than jco. I don't know if there could be any cross pollination or if you're just interested but anything that furthers the CM couldn't be bad. I just have to finish a few things and then I'll open it up. It implements most of the CM in webassembly itself.

@vados-cosmonic

Copy link
Copy Markdown
Collaborator

I don't know if there could be any cross pollination or if you're just interested but anything that furthers the CM couldn't be bad.

Definitely interested in getting whatever changes we can learn from in here! Jco is aiming to be toolkit, so multiple runtimes, multiple host implementations (this should be possible soon once we externalize the runtime) are all welcome.

I just have to finish a few things and then I'll open it up. It implements most of the CM in webassembly itself.

Well that sounds amazing -- my interest is piqued and I'm sure almost all of the folks on Zulip would be too! is this some sort of cross of https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-dylib and a new interface that covers the CM but with only core types? Very interesting.

@zacharywhitley

Copy link
Copy Markdown
Author

Well that sounds amazing -- my interest is piqued and I'm sure almost all of the folks on Zulip would be too! is this some sort of cross of https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-dylib and a new interface that covers the CM but with only core types? Very interesting.

Thanks for the link. That might slot in nicely. You nailed it. I've had a bit of a change in how I view the webassembly runtime. I used to think of it as a hard split between guest and host with the guest being webassembly and the host being whatever the embedding language was but then I realized that you can fold most of the host side code into guest side implementations and then guest vs host becomes more about the direction you're traversing the WIT interface than where the implementation lives. Now your host side code is portable and sandboxed (minus the explicit interface you expose). I've been pulling on that thread and it goes interesting places

@vados-cosmonic

Copy link
Copy Markdown
Collaborator

That's certainly super interesting -- there are some guests built with wit-dylib (e.g. componentize-js), so if those codebases/generated components & WAT are interesting to you, feel free to peruse them! Of course for compiled languages native bindings are much more efficient but really interested to see what you're working on, feel free to discuss/ask questions in the Zulip if you ever have any, I'm sure others would also be interesting/would like to theory craft on what is possible.

There has been discussion lately of distilling the interface needed by modules (essentially to make it as easy as possible for people to implement the CM) down as well, so this fits in there, but I'm not sure how much of it is well documented yet.

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.

3 participants