Dev-Only Actions & Production Exclusion
Design Log #180 — Dev-Only Actions & Production Exclusion of Dev-Only Surfaces
Written for AI agents. See Log Methodology Note below for details.
Enables plugin settings pages: a
devOnlyroute whose interactive form calls plugindevOnlyactions, where the action handlers may use the compiler and are fully excluded from production. Completes DL#171's deferred production-exclusion, and is the mechanism that lets DL#179's compiler-freeindex.jsinvariant hold for compiler-using settings UIs.
Background
- DL#130 — plugins can provide full pages (routes: jay-html + page component), served by the dev server (admin dashboards, builder UIs).
- DL#171 —
routes[].devOnlymarks a route as dev-server tooling. Production-build exclusion was explicitly deferred ("Phase 2 — Deferred: Production build excludesdevOnlyplugin routes from manifest and client bundles"). TodaydevOnlyis metadata only: the dev server serves it, sitemap skips it (generate-sitemap.ts:17), and production-build still compiles it intoallRoutes(build-pipeline.ts:274). production-server has nodevOnlyhandling. - DL#179 — runtime (
., compiler-free) vs tools (./tools, compiler-OK) entry split. Actions are the serving primitive (., compiler-free); commands are the tools primitive (./tools). - DL#173 / #142 / #145 — AIditor settings contributions, CLI commands, validators.
Problem
A plugin like design-system-validator wants a settings page: a devOnly route with an
interactive UI whose buttons run analysis (runDesignSystemAnalysis, catalog regeneration, …). That
analysis needs the compiler (run-design-analysis.ts → parseJayFile, JAY_IMPORT_RESOLVER
from compiler-jay-html).
Two forces collide:
- Settings pages need actions, not commands. The page runs in the browser and invokes server
handlers via the action RPC. CLI commands (
jay-stack run …) can't be called from a page. So the analysis handlers must be actions. - DL#179 forbids compiler in serve actions. A normal action loads from
.(compiler-free). A compiler-using action would poisonindex.jsand the deploy bundle.
There is no way today to have a browser-callable server handler that (a) may use the compiler and
(b) never ships to production. devOnly exists for routes but does nothing at build time, and there
is no devOnly for actions at all.
Real-world cases (scanned 2026-09-03)
The pattern is rare but real — only two plugins across the in-repo, wix, and aiditor repos route the compiler through a browser-callable action/page (everything else that leaks does so through a validator, handled by DL#179 alone):
design-system-validator(in-repo) — the driving case. Settings page →runDesignSystemAnalysisaction →run-design-analysis.ts→parseJayFile/JAY_IMPORT_RESOLVER(compiler-jay-html).aiditor(../aiditor/packages) —getContractInspectorTagsAction(re-exported fromindex.tsand imported by the/aiditorpage component) →resolve-contract-tags-server.ts→parseContract/ContractTagType(compiler-jay-html) +checkValidationErrors(compiler-shared). aiditor assumes a full dev environment (it's a design tool), so its/aiditorroute should be markeddevOnly: trueandgetContractInspectorTagsActionbecomes adevOnlyaction with its handler in./tools. This is the second real driving case for this DL, not an edge case: the entire surface is dev/tools-only, so the compiler stays in./toolsand never enters a production serve bundle.
Reference shape (but compiler-free): wix-media (../wix/packages) already ships the exact
settings-page structure this DL formalizes — /wix-media/settings (devOnly: true, component
mediaSettingsPage) backed by 5 settings actions (getMediaSettingsStatus, rebuildMediaCatalog, …).
Its actions don't use the compiler, so it doesn't need the devOnly-action mechanism for #179
reasons — but it does benefit from Phase 3's production exclusion so its admin handlers stop shipping.
It's the proof that the settings-page pattern is already in use and worth first-classing.
Questions and Answers
Q1. Why not make the analysis handlers CLI commands (DL#179's "compiler ⇒ command" rule)?
A: Commands are CLI-triggered; a settings page can't invoke them via the action RPC. Reclassifying
would break the page UX. Settings pages need actions. devOnly is the missing axis — orthogonal to
action-vs-command: it says "this handler exists only under the toolchain / dev server."
Q2. Where do dev-only action handlers live?
A: In ./tools (DL#179's compiler-allowed entry). Regular (production) actions stay in . and
must be compiler-free. This reuses the exact split DL#179 already establishes — no new entry.
Q3. How are dev-only surfaces kept out of production?
A: Two levers, both already feasible because production externalizes plugin packages and
dispatches actions/routes through the plugin's . entry (server-code-build.ts:176):
- devOnly action handlers live only in
./tools, so production'simport(plugin)(.) never reaches them; and production action registration skipsdevOnlyactions so they aren't dispatchable. - devOnly routes are filtered out before compilation in production-build (finishing DL#171 Phase 2), so the compiler-using page component is never compiled or bundled.
Q4. Does the dev server change how it serves these?
A: No behavior change for the author: the dev server registers all actions (devOnly from
./tools, regular from .) and serves all routes (DL#171: no HTTP gate). The settings page works
end-to-end in dev with the compiler present.
Q5. Is devOnly inferred?
A: No — explicit, consistent with DL#171. Author sets devOnly: true on the routes[] and
actions[] entries.
Q6. Does a devOnly action have to use the compiler?
A: No — devOnly is orthogonal to compiler use. Its meaning is "admin/tooling surface, excluded from
production", not "compiler-using." wix-media's settings actions are compiler-free yet still want
production exclusion so admin handlers (rebuildMediaCatalog, …) never ship to a live site. A
compiler-free devOnly action still lives in ./tools (that's where devOnly surfaces are excluded
from production), it just doesn't need to for #179 leak reasons.
Q7. Should there be a plugin-level devOnly for whole-tool plugins?
A: No — per-surface only (routes[].devOnly + actions[].devOnly). The scan of the in-repo, wix, and
aiditor repos found exactly one plugin that is fully dev-only and has browser surfaces:
aiditor (route + 37 actions). Every other fully-tools-only plugin (a11y-validator, seo-validator,
wix-deploy, aiditor-quill) has no routes/actions at all — DL#179's move-to-./tools handles them
with no devOnly flag. With n=1 benefiting, a plugin-level flag isn't worth the second axis every
consumer (dev-server, production-build, validator) must reason about alongside per-surface devOnly.
aiditor marks its route + actions individually.
Design
devOnly as a cross-cutting "dev/tools-only runtime surface" flag
| Surface | Flag | Handler/component entry | Production | Dev server |
|---|---|---|---|---|
| Route | routes[].devOnly (existing) |
./tools when compiler-using |
excluded (new) | served |
| Action | actions[].devOnly (new) |
./tools |
excluded (new) | registered |
Rule: a devOnly action's handler must be exported from ./tools; a non-devOnly action's handler
must be compiler-free on . (DL#179 leak scan enforces the latter). A devOnly route whose page
component uses the compiler resolves its component from ./tools.
Schema
compiler-shared/lib/plugin-resolution.ts:
// ActionManifestEntry gains a devOnly flag on the object form
export type ActionManifestEntry = string | { name: string; action?: string; devOnly?: boolean };
normalizeActionEntry returns devOnly alongside { name, action }.
plugin.yaml:
routes:
- path: /design-system/settings
jayHtml: ./dist/pages/settings/page.jay-html
component: designSystemSettingsPage
devOnly: true
actions:
- name: runDesignSystemAnalysis
action: run-analysis.jay-action
devOnly: true # ← handler in ./tools, excluded from production
- name: fontFallback
action: font-fallback.jay-action # normal production action, stays on `.`
Loading & registration
- Dev server (
dev-server/lib/service-lifecycle.ts→action-discovery.ts): register every action. FordevOnlyactions, import the handler from${packageName}/tools(npm) or the tools module (local); regular actions load from.as today. devOnly route components resolve from./tools. - Production build:
production-build/.../plugin-routes.ts+build-pipeline.ts: filterdevOnlyroutes out ofallRoutesbeforecompileRouteServerElement/compileRouteHydrateScript. (Currently only sitemap skips them.)- Plugin action registration for the deploy: skip
devOnlyactions — they are neither dispatched nor traced (their handlers aren't on.).
- Production server: devOnly routes/actions are absent from the manifest → naturally unserved. No special-casing needed once the build excludes them.
Validation (plugin-validator)
actions[].devOnlymust be a boolean (mirror the existingroutes[].devOnlycheck atvalidate-plugin.ts:477).- A
devOnly: trueaction requires a./toolsexport (same rule DL#179 adds for other tools capabilities). - Advisory: a
devOnlyroute paired with compiler-using code but nodevOnlyactions likely means the analysis runs inline in the page component — fine for dev, but flag if the same component is reachable from a non-devOnly route.
Diagram
Implementation Plan
Phase 1 — Schema
ActionManifestEntrygainsdevOnly?: boolean;normalizeActionEntrysurfaces it.
Phase 2 — Dev server
action-discovery.ts: loaddevOnlyaction handlers from./tools(npm) / tools module (local); threaddevOnlyfrom the manifest through discovery.- devOnly route component resolution from
./toolswhen present.
Phase 3 — Production exclusion (finishes DL#171 Phase 2)
plugin-routes.ts/build-pipeline.ts: excludedevOnlyroutes from compiledallRoutesand the route manifest.- Plugin action registration for deploy: skip
devOnlyactions. - Confirm production-server serves nothing devOnly (manifest-driven; add a guard test).
Phase 4 — Validation + agent-kit
validate-plugin: boolean check onactions[].devOnly; require./toolswhen anydevOnlyaction declared.- Agent-kit
plugin/plugin-routes.md+ a settings-page guide: document the settings-page pattern (devOnly route + devOnly actions in./tools), and that devOnly actions may use the compiler and never ship to production.
Phase 5 — Migrate design-system-validator (the driving case)
- Mark the analysis actions
devOnly: true; move their handlers to./tools(keepfontFallbacka normal action on.); mark the settings routedevOnly: true(already is). - Verify:
dist/index.jscompiler-free (DL#179 leak scan); settings page + analysis work on the dev server; production build excludes the route + devOnly actions.
Examples
✅ Settings page backed by a dev-only, compiler-using action:
routes:
- {
path: /design-system/settings,
jayHtml: ./dist/pages/settings/page.jay-html,
component: designSystemSettingsPage,
devOnly: true,
}
actions:
- { name: runDesignSystemAnalysis, action: run-analysis.jay-action, devOnly: true }
// tools.ts (./tools) — compiler allowed, excluded from production
export { runDesignSystemAnalysis } from './actions/run-analysis.js'; // uses parseJayFile
export { designSystemSettingsPage } from './pages/settings/page.js';
❌ Compiler-using action left as a normal production action:
actions:
- { name: runDesignSystemAnalysis, action: run-analysis.jay-action } # no devOnly → must be on `.` → DL#179 leak-scan error
Trade-offs
- + Settings pages get first-class support: browser-callable handlers that may use the compiler and never ship to production.
- + Reuses DL#179's
./toolsentry;devOnlybecomes one coherent flag across routes and actions. - + Finishes DL#171's long-deferred production exclusion; production stops compiling dev tooling.
- − New manifest field + touches dev-server, production-build, production-server, validator.
- −
devOnlyis now load-bearing for correctness (production exclusion), not just a UI hint — needs solid tests so a regression can't silently ship dev tooling. - Alternative rejected: reclassify settings handlers as CLI commands — breaks the browser action RPC the settings page depends on.
Verification Criteria
-
actions[].devOnly: truehandler loads from./toolson the dev server and is callable from a plugin page via the action RPC. - Production build excludes
devOnlyroutes (not compiled/bundled) anddevOnlyactions (not registered/dispatchable). -
design-system-validatordist/index.jscontains no@jay-framework/compiler-import (DL#179 scan) while its settings page + analysis work in dev. -
plugin-validatorerrors on a non-booleandevOnlyand on adevOnlyaction with no./toolsexport. - production-server serves no devOnly route/action (manifest-driven).
Relationship to other logs
- Depends on / pairs with DL#179 (the
./toolsentry is where devOnly handlers live). - Completes DL#171 Phase 2 (production exclusion of devOnly routes).
- Supports DL#173 settings contributions (AIditor settings pages become production-safe).
Implementation Results
Implemented on branch dl179-180-compiler-free-runtime (paired with DL#179). yarn confirm passes
clean.
actions[].devOnly schema
compiler-shared/lib/plugin-resolution.ts — ActionManifestEntry is now
string | { name; action?; devOnly? } and normalizeActionEntry carries devOnly through. A
devOnly action's handler lives in the plugin's ./tools entry (DL#179), so it may use the compiler;
regular actions stay compiler-free on ..
Dev server serves devOnly surfaces normally
dev-server/lib/dev-server.ts—resolvePluginModulegained anexportKeyparam; a route withdevOnly: trueresolves its component from./toolsinstead of..stack-server-build/lib/action-discovery.ts— both the NPM path (registerNpmPluginActions) and the local path (discoverPluginActions) lazily load a tools module (<pkg>/tools, or a siblingtools.ts/tools.jsfor local plugins) only when a declared action isdevOnly; plugins without devOnly actions never require a./toolsexport. Local plugins missing the tools module get a clear warning.
Production excludes devOnly routes + actions
production-build/lib/builder/build-pipeline.ts— devOnly routes are filtered out ofallRoutesbefore compilation/bundling/manifest, with an info log naming the excluded routes. (Finishes DL#171 Phase 2.)production-build/lib/builder/route-manifest.ts—discoverActionsfilters devOnly actions out of the deploy manifest vianormalizeActionEntry, logging the excluded count. A plugin whose actions are all devOnly contributes no action entry.
validate-plugin devOnly rules (Phase 4)
validate-plugin.ts — non-boolean devOnly → schema error; a devOnly action's handler is
checked against ./tools (regular actions against .); route components resolve against ./tools
when route.devOnly. Requirement of a ./tools export when any devOnly action exists is folded into
DL#179's needsToolsEntry. Tests cover the boolean check and the routing.
Migration
design-system-validator's settings-page action is marked devOnly and its handler moved to
lib/tools.ts; the settings route is already devOnly. Result: dist/index.js is compiler-free
(DL#179 scan) while the settings page + analysis still work under the dev server.
Deviations from the original design
- Skipped the optional advisory rule. The design floated an advisory: warn when a plugin has a
devOnlyroute whose (compiler-using) component is reachable, yet declares nodevOnlyactions, and the component is also reachable from a non-devOnly route. This is genuinely advisory, requires route-reachability analysis, and has low value now — the two required Phase-4 rules (boolean validation +./toolsrequirement) plus the DL#179 leak scan already prevent the failure mode. Not implemented.
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.