perf(build): bundle CLI entry point with esbuild#3108
Conversation
Adds a build:bundle step that uses esbuild to produce dist/lib/cli.bundled.js from the compiled dist/lib/cli.js, collapsing typedoc internal module resolution at cold start while keeping node_modules dependencies external (so plugins and the unbundled cli.js fallback continue to work). bin/typedoc now prefers cli.bundled.js when present and falls back to cli.js otherwise. A small esbuild plugin in scripts/build_bundle.js rewrites per-module import.meta.url so source-relative path lookups (locales, TYPEDOC_ROOT, theme assets) continue to resolve correctly inside the bundle. tsconfig.bundle.json builds the source without the test tree so the bundle input compiles cleanly.
|
Unfortunately, as CI is hinting, this is still subtly broken. Bundling is a good idea, but typedoc needs to be bundled in a way that only one copy of the files exist on disc to avoid issues where there are multiple instances of the library loaded (breaks Thankfully, we (mostly) don't need to worry about plugins importing deep paths because TypeDoc uses the exports key in package.json to specify which paths may be imported, and very few paths are in that list. I think to make this fully work there needs to be a few bundles:
The cli can then be implemented by importing the lib bundle, and typedoc will only need 4 files instead of the 160ish it currently uses. Not quite as good as 1 file, but it's still a major improvement. Ideally I'd like to have only a single copy of the built source, so it's potentially time to enable noEmit permanently in tsconfig and just use esbuild's built bundle for everything. |
|
ok, happy to make the change don't entirely understand yet so i might need a couple of tries, patience is appreciated |
What
Adds a `build:bundle` step that produces `dist/lib/cli.bundled.js` — a single-file bundle of typedoc's CLI entry path, produced by esbuild. `bin/typedoc` (kept as CommonJS to honor `bin/package.json`'s `"type": "commonjs"` override) now prefers the bundle when present and falls back to the unbundled `dist/lib/cli.js` otherwise. The unbundled `dist/` tree stays in place so plugin imports keep working.
Why
Profiling typedoc cold-start attributes ~440 ms of self-time across `detectModuleFormat`, `internalModuleStat`, `getPackageScopeConfig`, and `cjs-module-lexer` parsing — Node's per-file ESM/CJS resolution machinery touching every typedoc-owned JS module. A single-file bundle collapses those resolutions into one file load. Plugins import from `typedoc` (and deep paths under `typedoc/dist/...`) which still resolve through normal Node machinery against the unbundled tree, so the bundle is a CLI-startup optimization only and not a packaging change.
How
This was nearly an embarrassment. Naively running esbuild with `--bundle --packages=external --format=esm` produces a bundle that looks like it works (`bin/typedoc --version` prints "TypeDoc 0.28.19") but is actually broken — typedoc uses `import.meta.url` in 3 places to compute source-relative paths (`loadTranslations`, `TYPEDOC_ROOT`, `AssetsPlugin`), and after bundling every `import.meta.url` collapses to `cli.bundled.js`. Translation lookups then recurse infinitely, the project root becomes wrong, and theme asset paths break.
The fix is in `scripts/build_bundle.js`: a small esbuild API script with an `onLoad` plugin that rewrites `import.meta.url` per-module to a string literal of each module's original file URL (`pathToFileURL(args.path).href`). That preserves the source-relative semantics the unbundled tree depends on. `--showConfig` (which exercises i18n and `TYPEDOC_ROOT`) now runs cleanly through the bundle.
Perf impact
Estimated ~440 ms one-time cold-start reduction per CLI invocation, on machines with cold filesystem cache. No effect on subsequent phases (conversion, render). Most visible after `pnpm install` or after a reboot when the FS cache is empty. CI measurements will land in this description after a confirmation run.
Bundle size: 1,046,753 bytes (1022 KB). Runtime deps (`@gerrit0/mini-shiki`, `lunr`, `markdown-it`, `minimatch`, `yaml`, `typescript` peer) stay external.
Test plan
Risks worth a reviewer's attention
Rollback
Revert this PR. The unbundled `dist/lib/cli.js` continues to work as before; `bin/typedoc`'s existence check falls back automatically when no `cli.bundled.js` is present.
Stack context
Part of the typedoc speedup stack tracked by #3103. Group B PR — independent of the async-git stack (#3104) and the other Group B PRs (#3105 page writes, #3106 caches, #3107 JSX string buffer).
Blocks #3103