Compiler-Free Production Runtime
Design Log #178 — Compiler-Free Production Runtime
Written for AI agents. See Log Methodology Note below for details.
Background
The production server has two modes: serve (main server handles requests) and rebuild (renderer server re-runs slow phase when data changes). Neither mode genuinely needs the compiler — all compilation happens during jay-stack build. But compiler packages are pulled into the serve/rebuild bundle through accidental import chains, inflating the deploy artifact.
This matters for Wix BaaS deployment where the serve bundle must be under 20MB. Currently we hack around it by stripping compiler packages from the bundle post-build.
Problem
Two packages each mix build-time and serve-time code in a single bundle:
stack-server-runtime
Compiles to a single dist/index.js that imports compiler packages at the top level. Dev-server-only modules (load-page-parts.ts, generate-ssr-response.ts) import compiler-jay-html, but serve-time modules (fast-changing-runner.ts, slowly-changing-runner.ts) are bundled alongside them. Any import from stack-server-runtime triggers loading the compiler.
Two small utility functions are genuinely needed at serve time but live in compiler-shared:
computeForEachInstanceKey— ~10-line pure function for forEach instance coordinationloadPluginManifest— reads and parsesplugin.yamlfiles
production-server (currently)
Has a serve-index.ts entry point intended to exclude build deps, but Vite's bundler creates a shared chunk containing both loadProductionPageParts (build-only, imports compiler) and loadPagePartsFromConfig (serve-only, compiler-free) because they share a file.
Rebuild also unnecessarily re-parses jay-html via loadProductionPageParts when it could read the pre-built page-parts.json.
Design
Two package splits with parallel naming
Split each mixed package into a runtime package (zero compiler deps) and a build package (compiler + vite deps):
| Runtime (serve + rebuild) | Build (compile + bundle) |
|---|---|
@jay-framework/production-server |
@jay-framework/production-build |
@jay-framework/stack-server-runtime |
@jay-framework/stack-server-build |
stack-server-runtime → stack-server-runtime + stack-server-build
@jay-framework/stack-server-runtime — pure runtime, zero compiler deps:
slowly-changing-runner.ts,fast-changing-runner.ts,instance-slow-render.tsresolve-instance-props.ts,services.tsplugin-scanner.ts(with inlinedloadPluginManifest)parseCookies- Inlined
computeForEachInstanceKey(copied from compiler-shared)
@jay-framework/stack-server-build — dev-server and build tooling, depends on compiler:
load-page-parts.ts(needs Vite + compiler-jay-html)generate-ssr-response.ts(needs compiler for dev-server SSR)action-metadata.ts,action-discovery.ts,contract-materializer.ts- Depends on
stack-server-runtime+ compiler packages
production-server → production-server + production-build
@jay-framework/production-server — serve + rebuild, zero compiler deps:
main-server.ts,renderer-server.tsfetch-page-handler.ts,fetch-action-handler.ts,fetch-static-handler.tsroute-matcher.ts,import-map.ts,artifact-store.tsinit-services.tsrebuild.ts(usingloadPagePartsFromConfig, not re-parsing)generate-sitemap.ts- Depends on
stack-server-runtime(notstack-server-build)
@jay-framework/production-build — build pipeline, depends on compiler + vite:
build-pipeline.ts,instance-pipeline.ts,instance-client-build.tsserver-element-compile.ts,shared-chunks-build.ts,server-code-build.tsload-production-parts.ts(the build version withloadProductionPageParts)param-routing.ts,route-manifest.ts- Depends on
production-server+stack-server-build+ compiler + vite
Make rebuild compiler-free
Currently rebuild.ts calls buildInstance() which calls loadProductionPageParts() (re-parses jay-html). But rebuild only re-runs the slow phase when data changes — the jay-html hasn't changed.
Create rebuildInstance() in production-server that uses loadPagePartsFromConfig() (reads pre-built page-parts.json) instead of re-parsing. buildInstance() stays in production-build for the initial build.
Inline utilities
Copy two small functions into stack-server-runtime to break the compiler-shared dependency:
computeForEachInstanceKey— pure hash function, ~10 lines, no depsloadPluginManifest— YAML file reader, inline intoplugin-scanner.ts
Consumer mapping
| Consumer | Depends on |
|---|---|
| Dev server | stack-server-runtime + stack-server-build |
CLI build (jay-stack build) |
production-build |
CLI serve (jay-stack serve) |
production-server |
| CLI validate/agent-kit/etc. | stack-server-build (via stack-cli) |
| Wix BaaS deploy | production-server only |
| jay-fetch-handler | production-server |
Dependency graph
stack-server-runtime ←── production-server (both zero compiler deps)
↑ ↑
stack-server-build ←─── production-build (both have compiler + vite deps)
↑
stack-cli
Implementation Plan
Phase 1: Create stack-server-build package
- Create
packages/jay-stack/stack-server-build/withpackage.json - Move build-only modules from
stack-server-runtime - Depends on
stack-server-runtime+ compiler packages - Update dev-server imports
Phase 2: Clean stack-server-runtime
- Inline
computeForEachInstanceKeyandloadPluginManifest - Remove all compiler imports
- Verify zero compiler dependencies in built output
Phase 3: Rename current production-server to production-build
- Rename package directory and
package.jsonname - Keep all build code
- Update
stack-clibuild imports
Phase 4: Create new production-server package (serve + rebuild)
- Create
packages/jay-stack/production-server/withpackage.json - Move serve-time modules from
production-build - Move
loadPagePartsFromConfig(serve-only) - Create
rebuildInstance()usingpage-parts.json - Move
rebuild.tsto userebuildInstance() - Depends on
stack-server-runtimeonly
Phase 5: Update all consumers
stack-clirunServe: import fromproduction-serverstack-clirunBuild: import fromproduction-buildjay-fetch-handler: import fromproduction-server- Wix deploy plugin: depend on
production-server - Dev-server: import build symbols from
stack-server-build
Phase 6: Verify
production-serverhas zero deps on compiler-jay-html, compiler-shared, compiler-jay-stack, vitestack-server-runtimehas zero compiler depsjay-stack servestarts cleanly- Rebuild via
/_jay/rebuildworks usingpage-parts.json jay-stack buildworks viaproduction-build- Wix BaaS deploy bundle under 5MB
- Smoke tests pass for dev, production self-hosted, production CDN
Implementation Results
Plugin-facing types must live in a compiler-free package (deviation)
The original plan (Phase 5) said "Dev-server: import build symbols from stack-server-build" and treated all consumers of the moved build symbols the same way. This is correct for build/dev tools (dev-server, stack-cli) — they legitimately depend on stack-server-build.
It is wrong for plugins. Plugin packages (ui-kit, gemini-agent, design-system-validator) are runtime artifacts and must not depend on a build package (would re-introduce compiler/vite into their bundles). Yet they import the plugin-facing contract types — PluginSetupContext, PluginSetupResult, PluginAgentKitContext, PluginAgentKitResult (and transitively PluginSetupPrompt/handler types) — which had moved into stack-server-build/plugin-setup.ts.
Resolution: extract the pure, type-only plugin contract into stack-server-runtime/lib/plugin-setup-types.ts (compiler-free, and already the package plugins depend on for actionRegistry/registerService/ActionMetadata). stack-server-build/plugin-setup.ts now imports those types from stack-server-runtime and keeps only the build-time discovery/execution logic (discoverPluginsWith*, executePlugin*, SetupNeedsAnswerError, PluginWith*). Plugin source needed zero changes — they already imported these types from @jay-framework/stack-server-runtime.
full-stack-component was considered as the shared home but rejected: it is about component authoring, whereas these are server/plugin-lifecycle contracts, and keeping them in stack-server-runtime (next to ActionMetadata) required no plugin-code churn.
Symbol relocation for build/dev tools
- dev-server (
dev-server.ts,dev-server-service.ts,service-lifecycle.ts,hydration.test.ts): build symbols (SlowRenderCache,SlowRenderCacheEntry,loadPageParts,LoadedPageParts,generateClientScript,generateSSRPageHtml,generateFrozenPageHtml,clearServerElementCache,materializeContracts,discoverAndRegisterActions,discoverAllPluginActions,ProjectClientInitInfo) now import fromstack-server-build; runtime symbols stay onstack-server-runtime. Added@jay-framework/stack-server-builddependency. - stack-cli (
run-setup.ts,run-agent-kit.ts,run-action.ts,run-command.ts,index.ts,setup-prompts.ts): contract-materializer, action-discovery, plugin-commands, plugin-setup discover/execute, andSetupNeedsAnswerErrornow import fromstack-server-build. Added@jay-framework/stack-server-builddependency.
Other fixes surfaced by yarn confirm
production-build/lib/index.tsnow re-exportsRouteInfo,ParamPart,MaterializedEntry(movedparam-routing.test.tsneeded the type).- Moved tests in
stack-server-buildimportActionRegistry/PluginClientInitInfofrom@jay-framework/stack-server-runtime(were stale../lib/../lib/plugin-init-discoverypaths). production-server/lib/types.tsimportsJayHtmlHeadMetafromstack-server-runtimeinstead ofcompiler-shared— removes the last (type-only) compiler coupling;headMetaToHeadTagsalready consumes the runtime definition.- Added
production-server/test/route-matcher.test.ts(serve-only, compiler-free unit tests formatchRequest+buildImportMap) so the package is independently verified andvitestdoesn't exit 1 on an empty suite.
Verification: yarn confirm passes end-to-end (69 packages build, build:check-types exit 0, full test suite exit 0, format clean). production-server and stack-server-runtime package.json carry zero compiler/vite dependencies.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.