Conversation
Coverage Report for CI Build 34905065268Coverage decreased (-0.2%) to 94.689%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
There was a problem hiding this comment.
🟡 Changes recommended
Three unresolved findings remain, including one critical strictNullChecks compatibility issue and two moderate inference issues.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds an optional type-only layout registry for inferring nested layout contracts, page types, variables, and render results without runtime changes.
Changes:
- Adds registry and recursive type-inference helpers.
- Adds TypeScript/JSDoc compile-time coverage.
- Updates the basic example and layout documentation.
File summaries
| File | Summary |
|---|---|
types.ts |
Adds registry helpers. Findings: moderate (3 votes) dataDeps must be excluded; moderate (2 votes) union defaults can be over-reported as required; critical (1 vote) parent detection breaks without strictNullChecks. |
test-cases/type-exports/layout-registry.test.ts |
Adds registry inference and invalid-contract tests. |
scripts/test-packed-types.js |
Validates packaged TypeScript and JSDoc consumers. |
examples/basic/type-checks.ts |
Adds compile-time example assertions. |
examples/basic/tsconfig.json |
Includes JavaScript and type-check sources. |
examples/basic/src/layouts/root.layout.ts |
Registers the root layout and defaults. |
examples/basic/src/layouts/child.layout.ts |
Registers the nested layout and defaults. |
examples/basic/src/js-page/page.js |
Uses inferred JSDoc page contracts. |
examples/basic/src/js-page/loose-assets/page.ts |
Uses inferred root-layout contracts. |
examples/basic/src/global.vars.ts |
Adds typed global variables. |
examples/basic/README.md |
Documents the working example. |
docs/layouts/README.md |
Documents registry usage and constraints. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| : undefined extends Parent | ||
| ? InvalidParentReference | ||
| : [Parent] extends [string] |
There was a problem hiding this comment.
Confirmed with a compiler probe: with strictNullChecks disabled, the root chain resolved but the literal child-to-root chain became never. Addressed in 359a74e using the explicit-requirement option: registry helpers now require strictNullChecks, documented on LayoutRegistry and in the layout guide, with a central guard that consistently returns never for chain-dependent helpers when it is disabled. The existing explicit LayoutFunction, PageFunction, GeneratedPageDefinition, and PagesFunction APIs remain unchanged.
Added an isolated packed consumer with strictNullChecks: false that verifies the behavior for root and child across all registry helpers and demonstrates that the explicit APIs remain usable. This passed with Node typings 22, 24, and 26; the normal strict TS/JSDoc matrix also passes. Leaving this thread unresolved for your review.
| RequiredKeys<Vars>, | ||
| DefinitelyRequiredKeys<MergeLayoutDefaults<Chain>> | ||
| >, |
There was a problem hiding this comment.
Confirmed with a failing regression: a layout whose renderer vars and defaults are both { a: string } | { b: number } rejected {} as its external requirements. Fixed in 359a74e by calculating compatible renderer choices separately for each defaults branch, then combining obligations across all possible defaults branches. Fully supplied alternatives now require {}, while shared missing keys and branch-specific obligations are retained; an empty, optional, or incompatible defaults branch is not silently discarded.
Added registry-required-vars.test.ts covering the reported case, shared missing fields, optional/undefined defaults, differing obligations, conflicting obligations, and multiple renderer choices per branch, plus packed-consumer assertions. Source type checks, lint, packed tests with Node typings 22/24/26, the basic example checks/build, and the full serial Node suite passed. Leaving this thread unresolved for your review.
| MergeRight<MergeRendererVars<Chain>, GlobalVars>, | ||
| MergeLayoutDefaults<Chain> | ||
| >, | ||
| PageVars |
There was a problem hiding this comment.
Confirmed with failing regressions: PageForLayout exposed vars.dataDeps, and LayoutProvidedVars retained layout subscription metadata. Fixed in 359a74e by stripping dataDeps distributively before merging layout defaults and page vars, and removing it from the renderer-requirement baseline. This preserves union-specific fields and applies consistently to the provided/required/chain/page helpers as well as the generated-page helpers. Renderer alternatives that require vars.dataDeps are rejected because runtime cannot supply it; global vars containing dataDeps are also rejected, matching runtime. Raw exports and ValidatePageVars results retain subscription metadata for its actual purpose.
Added registry-subscriptions.test.ts covering page and async ancestor metadata, union preservation, required/optional metadata declarations, global metadata rejection, and unchanged page data isolation. Packed TS and JSDoc consumers now exercise metadata-bearing layouts and reject vars.dataDeps access. All source, lint, packed, basic-example, and serial Node checks passed. Leaving this thread unresolved for your review.
359a74e to
e528fb0
Compare
|
Rebased onto master at 6cf1c4c (including #323) and pushed with force-with-lease. Commit e528fb0 adds PageOutputsForRenderer, compile-time and packed-consumer coverage, and a working assets.json hook in the basic example. The PR description now includes before/after hook types and the compatibility details. Lint, TypeScript, packed consumers, basic example checks/build, JSON/link assertions, and the full sequential Node suite passed. Generated declarations were cleaned. Existing review threads remain unresolved. |
Summary
Implements the optional type-only registry proposed in #313 for review, without changing filesystem discovery, rendering, subscriptions, or runtime validation.
LayoutRegistryto the public type entry, with registrations referencing actual layout exports throughtypeof.PageForLayoutto derive page renderer contracts while keeping page data separate from every layout data contract.Before and after: typing a page through nested layouts
Consider a site that renders
root(article(page())). The page produces astring;articletransforms it into aFrame;rootaccepts thatFrameand produces the document string. Both layouts receive the same resolved vars, but each renderer has its own data contract.Before: explicit types, but no connection through the selected layout name
The existing APIs correctly describe each renderer in isolation:
A page author then describes the resolved vars and repeats the expected inner content type:
Here, the page annotation manually connects
ArticleVarsandstringto the runtime choicelayout: "article". It does not derive either from that name. If the selected layout changes its children type, this page annotation can become stale without a type error at the page. Likewise, changing an article renderer to returnnumberinstead ofFramecan satisfy its own revised annotation without TypeScript discovering that its declared parent still accepts onlyFrame. Authors can manually construct more elaborate types today, but each consumer must reconstruct those relationships.After: register each layout once, then derive the page contract
Keep the explicit renderer annotations above. Add a type-only registration alongside each layout, referencing the actual exports rather than copying their contracts:
The page no longer imports the layout vars contract or repeats
stringas its output type:PageForLayout<"article">is also sufficient when a page only needs the inferred layout contract. The extra parameters above account for this page’s own vars, its own data contract, and the known global vars without inventing another source of truth. As with the existing API, these types do not create exports, provide missing values, or declare runtime subscriptions.The same registry exposes the individual contracts for tooling or other typed consumers:
Why prefer this for statically known layout chains?
Framefrom the page is rejected because the article layout expects astring; changing the article result tonumbermakes the registered chain invalid because root expects aFrame.theme: "light", article overrides it with"dark", and the result is"dark", not an impossible intersection. A known override outside the renderers’ accepted"light" | "dark"contract is rejected. Required renderer vars remain distinct from defaults that layouts actually provide.The tradeoff is a one-time registration per layout and more advanced compile-time machinery. This is most useful when many pages share statically named layouts, especially nested layouts with different intermediate render values. Small sites, unregistered layouts, and dynamic/union selections can keep using the simpler existing explicit APIs. Registrations must be included in the site TypeScript program; separate sites sharing one program must avoid registry name collisions. Layout signatures should remain explicitly annotated rather than deriving themselves recursively from their own registry entry.
Design choices
The existing
LayoutVars<T>API is unchanged; the inferred helper is namedLayoutChainVars. Existing explicitLayoutFunctionandPageFunctionAPIs remain available for unregistered layouts and dynamic selections.Registry helpers require a single literal selected name and literal parent names. Invalid or ambiguous chains resolve to
never, with a 32-layout recursion limit. Registration is scoped to the site TypeScript program and does not introduce a runtime registry or watch dependencies.Renderer declarations remain independent of the registry to avoid circular inference. Renderer requirements compose as simultaneous constraints, while actual vars sources merge in runtime precedence order. Required vars still need real global, page, or builder sources; these helpers describe contracts rather than supplying values.
Working example: basic site
The existing basic example now demonstrates inferred contracts in both JavaScript/JSDoc and TypeScript, with visible global, root, child, and page variable contributions.
typeof parentLayoutand its explicit renderer type.PageForLayoutusingtypeof varsandAwaited<ReturnType<typeof globalVars>>, overrides the theme and whole badge object, and adds its own topics array. JSDoc@satisfiespreserves the async function’s Promise return type.Record<string, never>for their own data contracts; global vars are not subscribed global data.Variable precedence shown in the built page
themedarklightdarklightlightlocaleenenfooterreadingMinutes4number, value4badgetopicsRenderer requirements accept compatible alternatives such as light/dark themes and info/tip badges, while inferred page vars retain the winning source’s narrower type. Nested objects are replaced shallowly, not deep-merged.
LayoutRequiredVarsidentifies title, site name, and locale as values still needed from outside the layouts.The example includes compile-time checks against its actual exports for precedence, async defaults, required vars, literal unions, page-only fields, and rejection of invalid themes, string reading times, incomplete badge overrides, and incompatible page content. The checks live outside
srcand are not included in the built site. Its TypeScript program checks both JS and TS consumers.The render chain remains unchanged: the JavaScript page produces a
HtmlResult, child renders it to a string, and root wraps that string into the document. The pages now display their inferred vars, and root emits locale/theme attributes and its footer so runtime composition can be inspected.Additional validation:
npm run build:declarationnpm --workspace @domstack/basic-example testnpm --workspace @domstack/basic-example run buildgit diff --checkSupplied-vars validation and generated-page support
This PR also closes the two immediate coverage gaps identified during review: checking actual supplied vars and connecting generated definitions/factories to registered layouts. It remains a layout-chain registry, not a site-wide registry for manifests, templates, build configuration, or page introspection.
Validate actual exports
ValidatePageVars<Name, PageVars, GlobalVars>returns the original supplied page-vars type only if the actual known global vars, layout defaults, and page vars satisfy every renderer. It resolves toneverfor missing required values or incompatible final overrides. UnlikePageForLayout, it does not treat renderer requirements as assumed values.For the registered example earlier in this description, omitting the title or failing to supply the required global site name now fails at this export boundary. The name specifies the chain to validate; the actual layout export must still select that chain. Reserved
dataDepsremains in the supplied export but is excluded from the vars passed to renderers, matching runtime behavior.Generate checked definitions
GeneratedPageForLayout<Name, PageVars, PageData, GlobalVars>separates supplied definition vars from the fully merged vars available to inline children. It requires an explicit literalvars.layout, validates supplied requirements automatically, and checks static or inline content against the innermost layout.PagesForLayout<Name, PageVars, GlobalVars, FactoryData, PageData>adds the corresponding factory contract for sync functions, async functions, and async generators. Factory vars contain only effective globals; inline-page vars include layout and page contributions. Factory and page data contracts are independent, and declaring a type does not create subscriptions.Generated-content validation models omitted/static-nullish content becoming an empty string, awaited inline results, and function-valued children requiring a wrapper. Static objects conservatively reserve callable/thenable member names to prevent structural function or promise values from bypassing the runtime boundary. Existing explicit generated-page APIs remain unchanged.
Working examples and coverage
The basic example now validates both existing source-page exports and adds
src/guides.pages.ts, producing one async inline guide and one static-content guide through the registered child/root chain. Their HTML confirms inherited dark theme, locale, reading time, badge, footer, and generated URL. Source compile-time tests and packed TypeScript/JSDoc tests cover missing required vars, incompatible unions/overrides, subscription metadata, selector enforcement, factory/page isolation, nullish content, mixed awaited unions, and structural callable/thenable cases.Latest validation passed:
node --test --test-concurrency=1 --test-reporter=dotgit diff --checkThe default concurrent Node test run encountered intermittent watch failures: the JSX-client update assertion on one run and generated-page layout-asset watching on another. The full suite passed with file concurrency disabled; no runtime code or unrelated watch tests were changed. An initial concurrently run ESLint process also encountered a temporary watch-fixture file being deleted; standalone lint passed.
Validation
npm run test:tscnpm run test:neostandardnpm run test:node-testpassed earlier in the PR; see the latest sequential-run results and watch-test caveat above.npm run test:packed-types— TypeScript and JSDoc consumers with Node typings 22, 24, and 26git diff --checkCompile-time regressions cover mixed and asynchronous render values, vars precedence, optional and union sources, required/default distinctions, incompatible overrides, missing names, cycles, ambiguous selections, renderer boundaries, and data isolation. Generated declarations were cleaned up after validation.
Rebased page-outputs integration
Rebased onto
masterat6cf1c4c, including #323, and addedPageOutputsForRenderer<Renderer>ine528fb0. This type-only helper derives a hook’s vars and its own renderer’s data contract while reusing the upstream restrictedPageOutputsPagehandle and string-only output envelope. No library runtime behavior changes.Before, a hook alongside a registry-derived page had to repeat its vars/data types or manually extract them:
After, the existing renderer contract is the single source of truth:
This avoids repeated extraction and keeps hook typings synchronized with the page or layout without accidentally exposing renderer methods, children, client assets, or another renderer’s subscriptions. Generated pages still skip all page-output hooks, including inherited layout hooks.
The basic loose-assets page now emits
assets.jsonusing its inferred title, global site name/locale, winning root theme, asset-kind union, and page URL, with a visible download link. Generated-page docs also distinguish upstream runtime support for promised array entries from the existing conservative definition-only array types.Validation after rebase: repository lint and TypeScript checks; packed TypeScript/JSDoc consumers with Node typings 22/24/26; basic example type checks/build; assertions on emitted JSON values and its HTML link; full Node suite with file concurrency disabled; declaration cleanup; and
git diff --checkall passed. The three existing review threads remain unresolved as requested.Closes #313