Context
Follow-up to #310 and #311 (explicit nested layouts), with the per-renderer data boundary from #294. This is a deferred design exploration, not a prerequisite or landing blocker for either PR.
The runtime layout registry is heterogeneous: one layout can accept a string and return an object that its parent consumes. Individual renderers retain explicit input/output generics, but a string-keyed runtime registry cannot currently connect those contracts across the selected chain. Page authors also have to describe types that could potentially be derived from their selected layout.
Proposal
Explore an optional, type-only layout registry that each layout extends through TypeScript module augmentation. DOMStack would export an augmentable LayoutRegistry interface from its public type entry point. Each entry would connect the discovered layout name to its actual parent, vars export, and render function types.
For example, alongside the existing exports in article.layout.ts:
// Proposed API; these registry types do not exist yet.
declare module '@domstack/static/types.js' {
interface LayoutRegistry {
article: {
parentLayout: typeof parentLayout
vars: typeof vars
render: typeof articleLayout
}
}
}
The root layout would register similarly, without a parent. Reference the actual exports with typeof instead of independently repeating the parent name, vars shape, and render signature. This would be a compile-time registry, not a runtime global object or a replacement for filesystem discovery.
With those registrations, provisional consumer helpers could look like:
import type {
LayoutVars,
PageForLayout,
} from '@domstack/static/types.js'
type ArticleVars = LayoutVars<'article'>
type ArticlePage = PageForLayout<'article'>
The names and exact generic parameters are illustrative. The desired outcome is that choosing a layout supplies useful inferred contracts without requiring consumers to reconstruct the chain.
Type composition rules
- Follow the literal
parentLayout declarations recursively.
- Derive layout vars contributions in outer-to-inner order, then account for global and page/frontmatter contributions where their types are known.
- Model override precedence rather than blindly intersecting conflicting properties into
never.
- Distinguish supplied defaults from the vars a renderer requires, and reject incompatible overrides where statically knowable.
- Infer the page content type expected by the innermost layout.
- Check each child layout's awaited render result against its parent's accepted children type.
- Derive the outermost renderer's result separately from DOMStack's final serialized HTML string.
Vars merge, but render types compose. For example, if article accepts a string and returns a Frame, and root accepts a Frame, the page must return a string. The content type is not an intersection of string and Frame.
Preserve the existing data boundary
The registry must not expose an ancestor's subscribed data to its children or to the page. Each renderer keeps its own declared data contract. The union of dataDeps across the page and layout chain remains an output-invalidation concern, not a merged renderer data object. Any registry support for data contracts should derive from the existing renderer declarations rather than creating another independent source of truth.
Constraints and questions to resolve
- Keep the current explicit generic APIs available for unregistered layouts and JavaScript consumers.
- Registration files must be included in the site's TypeScript program; type-only declarations must not create runtime imports or watch dependencies.
- Determine how JavaScript/JSDoc projects can opt in, potentially through a companion declaration file.
- Prevent circular inference when a renderer registers its own type; do not require its signature to depend recursively on that same registry entry.
- Decide how literal versus dynamically selected layout names affect precision and fallbacks.
- Retain runtime checks for actual exports, missing parents, and cycles; TypeScript does not validate Markdown/HTML frontmatter or arbitrary dynamic modules automatically.
- Consider registry name collisions when several sites share a TypeScript program.
- Keep recursive-type diagnostics and compiler performance practical.
- Evaluate generated registrations only if manual registration proves burdensome; code generation is not assumed by this proposal.
Validation for a future implementation
Add positive and negative compile-time tests covering mixed render values, asynchronous layouts, inherited vars, compatible and incompatible overrides, missing names, cycles, and page/layout boundary mismatches. Verify that each renderer's data remains isolated and that sites using the existing APIs continue to type-check. Include clear examples showing local registration and simplified final page usage.
Sequencing
Leave #311 and #294 focused on their current runtime features and fixes. Prototype this separately after those changes land, then decide whether the typing benefits justify the public API and compiler complexity.
Context
Follow-up to #310 and #311 (explicit nested layouts), with the per-renderer data boundary from #294. This is a deferred design exploration, not a prerequisite or landing blocker for either PR.
The runtime layout registry is heterogeneous: one layout can accept a string and return an object that its parent consumes. Individual renderers retain explicit input/output generics, but a string-keyed runtime registry cannot currently connect those contracts across the selected chain. Page authors also have to describe types that could potentially be derived from their selected layout.
Proposal
Explore an optional, type-only layout registry that each layout extends through TypeScript module augmentation. DOMStack would export an augmentable
LayoutRegistryinterface from its public type entry point. Each entry would connect the discovered layout name to its actual parent, vars export, and render function types.For example, alongside the existing exports in
article.layout.ts:The root layout would register similarly, without a parent. Reference the actual exports with
typeofinstead of independently repeating the parent name, vars shape, and render signature. This would be a compile-time registry, not a runtime global object or a replacement for filesystem discovery.With those registrations, provisional consumer helpers could look like:
The names and exact generic parameters are illustrative. The desired outcome is that choosing a layout supplies useful inferred contracts without requiring consumers to reconstruct the chain.
Type composition rules
parentLayoutdeclarations recursively.never.Vars merge, but render types compose. For example, if
articleaccepts a string and returns aFrame, androotaccepts aFrame, the page must return a string. The content type is not an intersection of string andFrame.Preserve the existing data boundary
The registry must not expose an ancestor's subscribed data to its children or to the page. Each renderer keeps its own declared
datacontract. The union ofdataDepsacross the page and layout chain remains an output-invalidation concern, not a merged renderer data object. Any registry support for data contracts should derive from the existing renderer declarations rather than creating another independent source of truth.Constraints and questions to resolve
Validation for a future implementation
Add positive and negative compile-time tests covering mixed render values, asynchronous layouts, inherited vars, compatible and incompatible overrides, missing names, cycles, and page/layout boundary mismatches. Verify that each renderer's data remains isolated and that sites using the existing APIs continue to type-check. Include clear examples showing local registration and simplified final page usage.
Sequencing
Leave #311 and #294 focused on their current runtime features and fixes. Prototype this separately after those changes land, then decide whether the typing benefits justify the public API and compiler complexity.