Summary
stx already knows how to emit its runtimes as external assets: injectSignalsRuntime and injectRouter each have a serve branch (<script src="/_stx/runtime.js">) and a compile branch (/__stx/runtime.__STX_HASH__.js, fingerprinted by the production builder). The SSG sets neither, so it falls through to the inline branch and every generated page carries a full, byte-identical copy of both runtimes. There is no external script, no fingerprinted filename and no source map anywhere in the output.
Impact (real app)
Measured on the analyticshq static build (dist/, 35 HTML files):
| block | size | md5 | pages |
|---|---|---|---|
| signals runtime | 118,394 B | a16acf78 | account, dashboard, index, login, pricing, register |
| router runtime | 31,876 B | 2d35441c | all 11 measured pages |
| third scoped block | 7,883 B | fe68e775 | 6 pages |
dist/login.html is 169,987 bytes, of which 158,153 is duplicated runtime. dist/dashboard.html is 182,245 bytes. find dist -type f -name '*.js' returns nothing — there is no src= to any local script, no hashed filename, and no .map file for any client script in the app.
The app had no lever to pull: config/ui.ts has no buildMode key and nothing in the Stacks build path (bunx --bun @stacksjs/stx build --pages resources/views --out dist) passes one.
Evidence in stx
packages/stx/src/runtime-injection.ts
:193if (options.buildMode === 'serve')→<script data-stx-runtime src="/_stx/runtime.js"></script>:208if (options.buildMode === 'compile')→<script data-stx-runtime src="/__stx/runtime.__STX_HASH__.js"></script>:221-224fallback →<script data-stx-scoped data-stx-runtime>${runtime}</script>(the whole thing, inline):289-293the same three-way split for the router bundle
Who sets buildMode:
packages/stx/src/template-compiler.ts:144→'compile'packages/bun-plugin/src/serve.ts:1832→'serve'packages/stx/src/ssg.ts:645-650→ nothing:
const stxOptions = {
...stxConfig,
debug: false,
cache: false,
autoShell: true,
}The replacement machinery already exists on the other side — packages/stx/src/production-builder.ts:227-228 rewrites runtime.__STX_HASH__.js / router.__STX_HASH__.js to the real fingerprinted filenames. The SSG simply never opts in.
Prior art
Vite, Nuxt and Next all emit shared runtime chunks as hashed files (_nuxt/entry.<hash>.js) linked from every page: downloaded once, cached across navigations, and shipped with source maps. Nothing in those pipelines duplicates the framework runtime per document.
Ask
- Have
generateStaticSite()setbuildMode: 'compile'(or a new'ssg'mode with the same external-asset semantics) in thestxOptionsit passes toprocessDirectivesatssg.ts:645-650. - Emit
dist/__stx/runtime.<hash>.jsanddist/__stx/router.<hash>.jsfrom the SSG using the same hashing passproduction-builder.ts:227-228already implements, and rewrite the placeholders in every generated page. - Emit source maps for the runtimes and for bundled client scripts in the build output.
- Expose it as a config key (
build.inlineRuntime: boolean) so a single-file/offline deployment can still opt back into inlining.
Related: #1814