Structural Headfull Components
DL#162 — Structural Headfull Components (No Code File)
Written for AI agents. See Log Methodology Note below for details.
Background
Headfull full-stack components currently require three files: .jay-html, .jay-contract, and .ts. The .ts file provides the component definition via makeJayStackComponent, including render phase implementations.
Many headfull components are purely structural — shared headers, footers, layout wrappers. They have props but no data logic, no services, no render phases. Their .ts file is boilerplate:
export const header = makeJayStackComponent<HeaderContract>()
.withProps<HeaderProps>()
.withFastRender(async (props) => phaseOutput({ logoUrl: props.logoUrl }, {}));
This creates a role boundary problem: the designer creates the template and contract, but needs the developer to write trivial passthrough code before the component works.
Problem
- Structural headfull components require boilerplate
.tsfiles - The designer role cannot create a complete headfull component independently
- Agents frequently get the
.tswrong (missing.withProps(), wrong types) - The contract already declares everything needed — props and their types
Design
Allow headfull imports without src
When a headfull import has contract but no src, treat it as a structural component. The framework generates a passthrough component automatically:
<!-- No src attribute — structural component -->
<script
type="application/jay-headfull"
src="../components/site-header/site-header"
names="SiteHeader"
contract="../components/site-header/site-header.jay-contract"
></script>
Wait — the src currently points at the .ts file AND identifies the component's jay-html directory. We need src for the jay-html location. The question is whether src must resolve to a .ts file.
Approach: auto-generate component when .ts is missing
Keep src required (it locates the component directory and jay-html). But when the .ts file doesn't exist at that path:
- Parser — skip
analyzeExportedTypes(), derive types from contract only - Compiler — generate an inline passthrough component instead of importing from the module
- Dev server — create a synthetic component definition that passes props → ViewState
- Production build — same synthetic component
The synthetic component behaves as:
makeJayStackComponent<Contract>()
.withProps<Props>()
.withFastRender(async (props) => phaseOutput(props, {}));
All contract props become ViewState fields at fast phase. No slow phase, no interactive phase, no services.
What the designer creates (two files only)
src/components/site-header/
site-header.jay-html # template
site-header.jay-contract # props declaration
No .ts file needed. The src attribute in the import still points to ../components/site-header/site-header — the framework looks for .ts there, and when it's absent, generates the passthrough automatically.
Implementation Plan
Phase 1: Parser
compiler-jay-html/lib/jay-target/jay-html-parser.ts — in parseHeadfullFSImports():
- When resolving
src, check if the.tsfile exists - If missing, skip
analyzeExportedTypes() - Create a synthetic
codeLinkwith a generated component name - Derive ViewState type from contract props (all props → fast phase data tags)
Phase 2: Dev server loading
stack-server-runtime/lib/load-page-parts.ts:
- When loading headfull component module, check if file exists
- If missing, create a synthetic
compDefinitionwith fast render that passes props through
Phase 3: Production build
production-server/lib/builder/load-production-parts.ts:
- Same synthetic component fallback
Phase 4: Compiler code generation
compiler-jay-html/lib/jay-target/jay-html-compiler.ts:
- When no code module exists, generate inline component definition instead of import statement
- The generated code creates a passthrough component in-place
Phase 5: Validation
stack-cli/lib/validate.ts:
- Don't error when headfull component has no
.tsfile (if contract exists) - Validate that structural components don't declare render phases they can't have
Questions
- Should the developer be able to add a
.tslater to upgrade a structural component to a full component? (Yes — if.tsexists, use it; if not, use passthrough) - Should we support interactive refs in structural components? (No — refs require component code to handle events. Structural components are render-only.)
Trade-offs
| Choice | Pro | Con |
|---|---|---|
| Auto-generate from contract | Designer-independent, zero boilerplate | Less explicit, "magic" behavior |
Keep requiring .ts |
Explicit, no special cases | Role boundary problem, boilerplate |
Generate .ts on agent-kit |
File exists for inspection | Generated code to maintain |
Implementation Results
Key insight: structural = template fragment, not component
The original design proposed a synthetic passthrough component (props → ViewState). This was wrong. A structural component has no props, no tags, no ViewState. It's a reusable template fragment — the <jay:> tag is unwrapped and its content becomes plain HTML in the parent.
Data inside a structural component comes from headless imports declared in its own <head>, not from the parent via props.
What changed (simplified from original plan)
Parser (
jay-html-parser.ts):- Check if
.tsfile exists after resolvingsrc - If missing: inject the component's jay-html template into the parent body, then unwrap the
<jay:>tag (replace with its innerHTML) continue— noheadlessImportentry is created, no contract types are built
- Check if
Compiler (
jay-html-compiler.ts):- Structural components are excluded from
headlessContractNames— the compiler never sees them as component tags (they were unwrapped to plain HTML by the parser) - The inline passthrough code from the original plan was removed — not needed
- Structural components are excluded from
Runtime (
load-page-parts.ts,load-production-parts.ts):structuralflag onJayHeadlessImportsis kept defensively but never set (structural componentscontinuebefore pushing)- No synthetic component code — not needed
What was removed from original plan
- Synthetic passthrough component (
phaseOutput(props, {})) - Props → ViewState mapping
- Inline component generation in compiler
structuralentries in page-parts.json
Contract for structural components
Minimal — just a name:
name: InfoBox
No props, no tags. The contract exists only to name the component for the import.
Smoke test
examples/jay-stack/smoke-test/src/components/info-box/ — two files only (.jay-html + .jay-contract), used via <jay:infoBox> in the headfull page. Verified in both dev and production modes.
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.