Repository navigation
Conversation
hlenke
force-pushed
the
proto/esbuild-bundle
branch
from
October 15, 2026 21:26
2c03bba to
ee69664
Compare
EXPERIMENTAL prototype, opened as a discardable draft PR — see the PR description for measurements, trade-offs and sequencing notes. - tsc output and 39 runtime dependencies are replaced by esbuild-bundled entries under dist/bundle/ (per-command entries + shared chunks, so commands stay lazy), reducing runtime deps to oxc-parser, @typescript-eslint/typescript-estree and jiti (none of which can be bundled: native binding, user-project resolution, runtime self-requires). - @oclif/core and the two oclif plugins move to devDependencies and are folded into the bundle; plugin-help was fully shadowed already. - bin/run.cjs enables Node's V8 compile cache (best effort, old-syntax safe for the version preflight). - New packageRoot() util anchors package-relative paths (ai-context, assets) that used to depend on module depth in dist/. - New startup-budget CI check (scripts/startup-check.mjs) fails the lint job if --version p50 exceeds 400ms (currently ~50ms).
The deploy preview prints construct.constructor.name, and error messages and write-back checks name construct classes too. Without keepNames, esbuild renames classes whose name collides with their export binding in chunk scope (ApiCheck = class _ApiCheck), so the packed CLI rendered '_ApiCheck' where main renders 'ApiCheck' — caught by the deploy e2e on CI (unit tests run from source and never saw it). Also move the exports 'types' conditions first, where esbuild expects them, silencing build warnings.
Roughly halves the bundle (7.4 MB -> 3.5 MB, tarball 1.69 -> 1.32 MB gzipped, within 8% of the unbundled main tarball). keepNames keeps class and function names readable in stack traces; only local identifiers are mangled. Sourcemaps stay unshipped: split-bundle maps weigh ~13 MB and Node only applies them with --enable-source-maps anyway.
#1543 moved write-back and preview parsing to oxc-parser and made @typescript-eslint/typescript-estree a devDependency (reference parser of ts-parser-parity.spec.ts only). No runtime imports remain, so the bundle no longer needs it external: runtime dependencies are down to jiti and oxc-parser, and installs no longer pull the ~24 MB typescript peer — node_modules drops from 42 MB/18 packages to 12 MB/5 packages. Also regenerates the lockfile after the rebase onto main.
hlenke
force-pushed
the
proto/esbuild-bundle
branch
from
October 16, 2026 18:11
ee69664 to
98434fb
Compare
This branch has not been deployed
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.
Sequencing note: remaining dependencies on #1530 and #1532
#1543 has landed and this branch is rebased on top of it — runtime deps are now 2 (
jiti,oxc-parser) and installs no longer pull the ~24 MBtypescriptpeer. Two open PRs still change what this branch should look like when it grows up:src/hooks/check-update.ts. If it lands first, this branch should drop that hook and take theirs.Affected Components
What this does, in plain language
Today, installing the CLI downloads 39 npm packages plus everything those packages depend on — roughly 224 packages, 7,800 files, 95 MB — before the CLI can run. Almost all of that code is ours or is used in exactly one way.
This branch has esbuild (the same bundler the Shopify CLI uses) combine everything into a small number of pre-built files that ship inside the checkly package itself. After that:
typescriptthat npm auto-installs as a peer, same as on main).bin/run.cjsnow enables Node's V8 compile cache so warm runs skip re-parsing the bundle entirely.One deliberate deviation from the Shopify pattern, with measurements: Shopify uses oclif's "explicit" command strategy — one file importing all commands, which oclif loads eagerly. I built that first; it made every invocation import all 60 command classes, and warm
--versionregressed 96 ms → 225 ms. So this branch instead gives each command its own bundle entry, with shared code split into chunks by esbuild — runningcheckly env lsnever even parses the mqtt or archiver code. oclif keeps its default lazy command discovery, just pointed at the bundled files. Shopify's bundling without Shopify's startup tax.Performance numbers
Measured on the same idle macOS machine, from real
npm installs of the packed tarballs — both columns on currentmain(after #1543 landed), p50 of 30 runs:checkly --versionwarmcheckly --helpwarmThe tarball is within 8% of main. The bundles are minified (halving them from 7.4 MB to 3.5 MB) with esbuild's
keepNames, so class and function names stay readable in stack traces — only local identifiers are mangled. Sourcemaps are not shipped: the split-bundle maps weigh ~13 MB, and Node only appliessourceMappingURLwith--enable-source-mapsanyway.Runtime dependencies: 39 → 2. The two that remain each genuinely cannot be bundled:
oxc-parser— native Rust bindings, resolved per platformjiti— lazy-requires its own package files at runtime(
@typescript-eslint/typescript-estreewas the third until #1543 landed; it is now a devDependency used only by the parser-parity test, and nothing imports it at runtime.)@oclif/core,@oclif/plugin-helpand@oclif/plugin-warn-if-update-availablebecome devDependencies (bundled in). plugin-help turned out to be fully shadowed: help output is byte-identical without it.How the build works now
prepare:dist=tsc --build(still the source of truth for types and.d.tsfiles) →scripts/bundle.mjs(esbuild).dist/bundle/...paths.dist/; those now use a smallpackageRoot()walker (src/services/package-root.ts) so they resolve identically from the bundle, from tsc output, and from source.pnpm --filter checkly run startup:checkin the lint job) fails the build if--versionp50 exceeds a generous 400 ms budget (currently ~50 ms) — so startup regressions from new dependencies fail a check instead of going unnoticed.Testing done
--help/help/topics/-h/-v, unknown-command hook (exit 127), parse-error hook,skills,runtimes, live authenticated API commands (env add/rm, including agent mode), plus ESM and CJS imports of the public API from the installed copy.pnpm run prepare(clean → tsc → bundle → AI-context generation) runs end-to-end; the AI-context step itself executes the bundled CLI.packageRoot()and the fixture installs — are exactly what CI on this draft would validate for free (unit + fixture suites run on Windows in CI). That is a big part of why this draft exists.Known trade-offs / open questions
keepNames, no shipped sourcemaps (see above).keepNames).src/hooks/check-update.ts(the folded update-check plugin) should be replaced by perf(cli): replace warn-if-update-available with a lightweight hook #1532 when it lands.