Skip to content

fix(txe): emit the CLI bundle at a path tsc-only rebuilds cannot clobber - #38

Open
vezenovm wants to merge 5 commits into
mainfrom
mv/node24-lazy-json-imports
Open

fix(txe): emit the CLI bundle at a path tsc-only rebuilds cannot clobber#38
vezenovm wants to merge 5 commits into
mainfrom
mv/node24-lazy-json-imports

Conversation

@vezenovm

@vezenovm vezenovm commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

TXE crashed at startup with ERR_IMPORT_ATTRIBUTE_MISSING loading the Schnorr artifact via @aztec/accounts/schnorr/lazy.

Root cause: a build-artifact collision, not a missing import attribute. TXE only runs correctly from its esbuild bundles — esbuild resolves dynamic JSON imports at build time, so Node's loader never sees them. But the CLI bundle was emitted at dest/bin/index.js, the same path swc writes the unbundled entrypoint to. A root-only yarn build (tsgo + swc, no package esbuild) overwrote the bundle with unbundled output, which does hit Node's loader. CI never sees this because bootstrap always runs each package's full build script.

Why not add with { type: 'json' } instead

Removed on purpose in AztecProtocol/aztec-packages#12352: vite's dev server serves JSON as text/javascript and only strips import attributes from static imports, so an attributed dynamic import fails the browser's MIME check (vitejs/vite#19095, still open). The playground and external wallet apps consume these lazy entrypoints through vite, so they stay browser-only by design.

Fix

  • Emit the TXE CLI bundle as dest/bin/index.bundle.js (matches worker.bundle.js/server.bundle.js) — a path swc never writes — and repoint bin, start/dev, the files list, the size guard, and the bootstrap TXE launcher.
  • Reword the lazy import comments to say why the attribute can't be added (vite dev MIME check) instead of the stale pre-attributes wording.
  • Rebundle in start/dev (~150ms) so a root-only yarn build can never leave a launched TXE on stale bundles — also fixes worker.bundle.js silently going stale in that case.

Node 24 rejects dynamic imports of JSON modules without
`with { type: 'json' }`, so the TXE crashed at startup with
ERR_IMPORT_ATTRIBUTE_MISSING when dispatcher_pool loaded the Schnorr
account artifact through @aztec/accounts/schnorr/lazy. The static
imports already carry the attribute; this adds it to the lazy dynamic
imports as well.
esbuild used to overwrite the swc-emitted dest/bin/index.js, so a tsc-only
rebuild (e.g. `yarn build` at the yarn-project root) re-emitted the unbundled
entrypoint over the bundle. The unbundled tree reaches dynamic JSON imports
that Node's ESM loader rejects (ERR_IMPORT_ATTRIBUTE_MISSING via
@aztec/accounts/schnorr/lazy in dispatcher_pool), crashing TXE at startup.

Emit the CLI bundle as dest/bin/index.bundle.js instead — a path tsc never
writes, matching worker.bundle.js and server.bundle.js — and point bin,
start/dev scripts, the publish files list, the size guard, and the root
bootstrap TXE launcher at it.

Also reword the stale comments on the lazy JSON imports: "cannot assert this
import" used the pre-attributes terminology, and the attribute cannot be
added because vite's dev server serves JSON as a JS module and only strips
attributes from static imports, leaving these imports Node-incompatible
unless bundled.
@vezenovm vezenovm changed the title fix(yarn-project): add JSON import attributes to lazy artifact imports fix(txe): emit the CLI bundle at a path tsc-only rebuilds cannot clobber Aug 20, 2026
A root-only `yarn build` (tsgo + swc, no package esbuild steps) leaves the
esbuild bundles — including worker.bundle.js, where foreign calls actually
execute — stale. Rebundling takes ~150ms, negligible against TXE cold start,
so run esbuild.config.mjs before launching in the start and dev scripts.
…he attribute syntax

Say `with { type: 'json' }` explicitly instead of "assert this import" so
the comment names the actual syntax that can't be added, without otherwise
changing the reasoning.
@vezenovm
vezenovm marked this pull request as ready for review August 20, 2026 16:16

@nchamo nchamo 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 not my strong suit, so the review is mostly AI-driven. I would be more comfortable if we got another set of eyes on this PR

}
},
"bin": "./dest/bin/index.js",
"bin": "./dest/bin/index.bundle.js",

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.

yarn.lock still has txe: ./dest/bin/index.js. Should we commit the regenerated lockfile? yarn install is immutable in CI, so it'd fail on the first node_modules cache miss rather than here.

Comment thread bootstrap.sh
port=$((txe_base_port + i))
kill_port $port
dump_fail "LOG_LEVEL=info TXE_PORT=$port retry 'node --no-warnings ./yarn-project/txe/dest/bin/index.js'" &
dump_fail "LOG_LEVEL=info TXE_PORT=$port retry 'node --no-warnings ./yarn-project/txe/dest/bin/index.bundle.js'" &

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 one doesn't go through yarn start, so it misses the rebundle you added. After a root-only yarn build it runs whatever the last esbuild left behind, and after a yarn clean the file isn't there at all. Should start_txes bundle first too?

// yarn-project root) must not be able to replace a bundle with unbundled output, because the
// unbundled tree reaches dynamic JSON imports that Node's ESM loader rejects
// (ERR_IMPORT_ATTRIBUTE_MISSING) — TXE only runs correctly from the bundles.
'bin/index.bundle': 'src/bin/index.ts',

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.

Now that esbuild stops overwriting it, swc's dest/bin/index.js sticks around as a broken copy of the CLI. Should we stop emitting it, so the old path fails as a missing file instead of a confusing crash?

},
"files": [
"dest/bin/index.js",
"dest/bin/index.bundle.js",

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.

"dest" on line 109 already publishes the whole directory. Should we drop the redundant entries rather than keep renaming them?

*/
export async function getStubSchnorrAccountContractArtifact() {
// Cannot assert this import as it's incompatible with bundlers like vite
// Cannot add `with { type: 'json' }` to this import as it's incompatible with bundlers like vite

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.

Nit: the generator that emits this same block was missed — generate_client_artifacts_helper.ts:100 and :150 still say "Cannot assert this import". They're the last two copies of the old wording left.

// https://github.com/vitejs/vite/issues/19095#issuecomment-2566074352
// Even if now supported by all major browsers, the MIME type is replaced with
// "text/javascript"
// In the meantime, this lazy import is INCOMPATIBLE WITH NODEJS

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.

@aztec/noir-protocol-circuits-types is external, so its client/lazy JSON imports still reach Node's loader unbundled — same error, just not called today. Should the generator emit with { type: 'json' }? One change covers ~150 sites.

* inside `dest/`) or bundled into `dest/bin/index.js` (one directory deeper). `import.meta.url`
* refers to whichever module the calling code actually lives in; we try both relative locations
* and use whichever exists.
* inside `dest/`) or bundled into `dest/bin/index.bundle.js` (one directory deeper).

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.

With splitting on, this code ends up in a dest/chunk-*.js at the dest/ root rather than dest/bin/, so the ../worker.bundle.js candidate never wins. Is it still needed?

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