Skip to content

fix: Schema.addPath replaces $defs instead of merging in fast-html #7366

@mcritzjam

Description

@mcritzjam

🐛 Bug Report

Schema.addPath in fast-html replaces the entire $defs object instead of merging when a new dotted-property binding
(e.g. {{strings.selectAll}}) is encountered inside a repeat context. This destroys previously created repeat contexts and
causes a TypeError crash during template compilation/hydration.

💻 Repro or Code Sample

Minimal template that crashes:

<f-repeat value="{{item in displayedItems}}">
  <span>{{strings.selectAll}}</span>
  <f-repeat value="{{child in item.children}}">
    <span>{{child.label}}</span>
  </f-repeat>
</f-repeat>

Steps to reproduce:

1. Clone the repro and run npm install
2. Run npm test (Node.js direct test) — prints the crash trace
3. Or run npm start and open http://localhost:8787 — crash in browser console

Node.js test (test-crash.mjs) directly exercises Schema.addPath and Schema.addContext to trigger the crash without a browser.

🤔 Expected Behavior

Template compilation and hydration should complete without errors. Adding a new entry to $defs via addPath should preserve all
previously created repeat contexts.

😯 Current Behavior

TypeError: Cannot read properties of undefined (reading '$fast_parent_contexts')
    at _Schema.getParentContexts (schema.ts:~160)
    at _Schema.addContext (schema.ts:~140)
    at _Schema.addPath (schema.ts:~120)
    at bindingResolver
    at _TemplateElement.resolveTemplateDirective
    at _TemplateElement.resolveInnerHTML

In minified builds: TypeError: Cannot read properties of undefined (reading 'at').

The custom element never completes hydration. The page renders the pre-rendered HTML shell but is completely non-interactive
(no event handlers bound).

💁 Possible Solution

The bug is in packages/fast-html/src/components/schema.ts lines 143–147, in the "access" case of Schema.addPath:

// CURRENT (buggy) — replaces entire $defs
if (!schema[defsPropertyName]?.[splitPath[0]]) {
    schema[defsPropertyName] = {
        [splitPath[0]]: {} as any,
    };
}

Fix — spread to preserve existing entries:

if (!schema[defsPropertyName]?.[splitPath[0]]) {
    schema[defsPropertyName] = {
        ...schema[defsPropertyName],
        [splitPath[0]]: {} as any,
    };
}

Crash sequence:

1. Outer <f-repeat> → addContext creates $defs["item"] with $fast_parent_contexts
2. {{strings.selectAll}} → addPath(type="access") → $defs["strings"] doesn't exist → $defs replaced with { "strings": {} } → 
$defs["item"] destroyed
3. Inner <f-repeat> → addContext("child", parentContext="item") → getParentContexts("item") → $defs["item"] is undefined → 💥

I'm happy to contribute the fix as a PR.

🔦 Context

This bug blocks the Microsoft Edge edge://history BTR (Build Time Rendering) conversion. The history page template uses 
{{strings.selectEntry}} (localized string) inside a <for each="group in displayedGroups"> loop, which crashes when followed by
a nested <for each="entry in group.entries">.

Current workaround: flatten nested data into a single array with row-type discriminators, avoiding nested repeat loops
entirely. This significantly increases component complexity and loses semantic grouping.

🌍 Your Environment

- OS & Device: Windows 11 on PC
- Browser: Microsoft Edge
 147.0.0.0 (developer build)
- @microsoft/fast-html:
 1.0.0-alpha.36 (also verified on alpha.44 — not fixed)
- @microsoft/fast-element:
 2.10.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions