Skip to content

feat: reduce declarative template size#7498

Merged
janechu merged 21 commits intoreleases/fast-element-v3from
users/janechu/reduce-declarative-element-size
Apr 28, 2026
Merged

feat: reduce declarative template size#7498
janechu merged 21 commits intoreleases/fast-element-v3from
users/janechu/reduce-declarative-element-size

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented Apr 25, 2026

Pull Request

📖 Description

Reduces the declarative template entrypoint size by making optional attribute/observer map and hydration lifecycle support tree-shakable through schema transforms, replacing the custom TemplateElement FASTElement with a native <f-template> publisher, and updating declarative docs, generated API docs, fixtures, benchmarks, size docs, and the change file.

Optional helpers now use dedicated flat @microsoft/fast-element subpaths and are no longer exported from the root entrypoint or the old nested helper paths. The package export map keeps the public paths flat while targeting the actual source and output file locations for types, test, and default.

Examples of the flat subpaths:

  • @microsoft/fast-element/attribute-map.js
  • @microsoft/fast-element/observer-map.js
  • @microsoft/fast-element/children.js
  • @microsoft/fast-element/ref.js
  • @microsoft/fast-element/slotted.js
  • @microsoft/fast-element/when.js
  • @microsoft/fast-element/repeat.js
  • @microsoft/fast-element/node-observation.js
  • @microsoft/fast-element/two-way.js
  • @microsoft/fast-element/signal.js
  • @microsoft/fast-element/declarative-utilities.js

Additional dedicated paths include updates.js, observable.js, attr.js, volatile.js, css.js, html.js, array-observer.js, binding.js, dom.js, schema.js, templating.js, render.js, and hydration.js.

FASTElementDefinition.schema is optional; declarative templates assign or augment it during template resolution, and non-declarative/manual schema users can use observerMap({ schema }) or provide a schema on the definition. Explicitly supplied schemas are preserved.

This also addresses PR review feedback by replacing shell-string API document generation with argument-based execFile usage.

📑 Test Plan

  • npm run build:tsc -w @microsoft/fast-element -- --pretty false
  • npm run doc:exports -w @microsoft/fast-element
  • npm run doc:exports:ci -w @microsoft/fast-element
  • npm run build:sizes -w @microsoft/fast-element
  • npm run prebuild -w @microsoft/fast-site
  • npm run biome:check
  • npm run checkchange
  • node -c sites/website/scripts/generate-docs.cjs
  • git diff --check
  • Targeted Chromium declarative template bridge tests
  • Flat subpath positive resolution checks for default and test conditions
  • Stale nested export/import path searches

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

janechu and others added 4 commits April 24, 2026 22:09
Reduce declarative template entrypoint size by moving optional map and lifecycle support behind schema hooks, replacing the custom template element with a native publisher, and updating docs/fixtures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose attributeMap and observerMap from extension subpaths, keep declarative re-exports, and support schema-driven usage with optional definition schemas.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread sites/website/scripts/generate-docs.cjs Fixed
janechu and others added 3 commits April 25, 2026 21:14
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread packages/fast-element/src/declarative/template.ts
janechu and others added 5 commits April 25, 2026 22:58
Adds remaining optional export subpaths for binding, DOM, schema, templating, render, hydration, and node observation, while keeping Observable/observable together and controller/definition internals on the root entrypoint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the public fast-element export paths flat while resolving package export types, test, and default targets to the original source and output locations.

Also address review feedback by preserving explicit declarative schemas and avoiding shell-string API doc generation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the generated size tables focused on the previously tracked exports plus the requested attributeMap and observerMap subpath entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-export the runtime APIs that moved to subpaths from the rollup-only entries so the CDN bundle keeps its previous surface and size while the package root remains trimmed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as ready for review April 27, 2026 03:49
Comment thread packages/fast-element/src/declarative/attribute-map.ts Outdated
Comment thread packages/fast-element/src/declarative/observer-map.ts Outdated
Comment thread packages/fast-element/scripts/measure-sizes.js
Copy link
Copy Markdown
Collaborator Author

@janechu janechu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the source-code changes (core runtime, components/hydration, declarative + observer-map-utilities, package exports, tests/fixtures, consumer updates).

Overall the refactor is well-executed:

  • Flat subpath exports are consistent; types-first ordering in the export map is correct; every new entry point has a matching api-extractor.<name>.json.
  • Hydration is properly isolated: element-controller.ts and view.ts do not import the new hydration runtime, preserving tree-shakability.
  • The shell→execFile change in sites/website/scripts/generate-docs.cjs correctly closes a real command-injection vector (no shell: true, args passed as array).
  • Changefiles accurately reflect the breaking nature of the path restructuring.
  • Test fixture updates and the new declarative-no-hydration ecosystem fixture are reasonable; bundle-size validation is appropriately deferred to SIZES.md tracking.

One concrete issue worth addressing inline. I also looked carefully at deepMerge in observer-map-utilities.ts (the nestedChanged === false branch is correct because nextTarget is a copy of targetValue and deepEqual short-circuits earlier when source/target are equivalent), the addPropertiesToAContext/anyOf access in components/schema.ts (the splitPath.length > 1 and else if (childRef) paths are mutually exclusive, and schema.properties[splitPath[0]] is initialized to {} immediately above), and the polyfill removal (no remaining importers found). Those checked out.

Comment thread packages/fast-element/src/declarative/observer-map-utilities.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as draft April 27, 2026 18:06
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .github/skills/typescript/SKILL.md Outdated
Comment thread packages/fast-element/docs/arrays/api-report.api.md Outdated
Comment thread packages/fast-element/docs/declarative/api-report.api.md Outdated
Comment thread packages/fast-element/docs/declarative/api-report.api.md Outdated
Comment thread packages/fast-element/docs/hydration/api-report.api.md Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
janechu and others added 2 commits April 27, 2026 13:05
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md Outdated
Comment thread packages/fast-element/README.md
Comment thread packages/fast-element/SIZES.md Outdated
Comment thread packages/fast-element/SIZES.md Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator Author

@janechu janechu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary (fleet review)

Thanks for this substantial cleanup! The export-map flatten and the schema-transform tree-shaking are well executed overall — flat subpaths resolve cleanly (no stale nested imports remain in packages/, sites/, examples/, or .github/), sideEffects: false is genuinely defensible given the new modules are pure re-exports, and the 600+ line drop in declarative/utilities.ts is fully accounted for (functions relocated to observer-map-utilities.ts, template-bridge.ts, etc.).

I reviewed in four parallel passes (exports, schema/observer-map refactor, template-bridge/hydration, and the execFile/docs/change-file pass). Findings below; inline comments cover the higher-impact ones where the line falls in the diff.

Headline issues

  1. The advertised execFile security fix is not actually present. The PR description says shell-string command generation was replaced by argument-based execFile usage, but the final commit 8a2b3218f "Restrict fast-element path exports" reverted sites/website/scripts/generate-docs.cjs back to exec(\api-documenter markdown -i ${tempAPIDir} -o ${markdownAPIDir}`) at lines 216 and 233. The net diff for that file is empty. Earlier commits (8acba4a, 2cff100) had the runAPIDocumenter()helper usingexecFile(cmd, ["markdown", "-i", input, "-o", output])— that helper needs to be restored before merge.markdownAPIDiris built fromprocess.argv[2]` (line 13) without sanitisation, so this is a real injection surface, not just a hardening preference.

  2. <f-template>.publishTemplate rejects legitimate nested <template> contentgetElementsByTagName("template") walks all descendants, so any user template that contains nested <template> elements (declarative shadow DOM, sub-template patterns, even content like a <table> row template) trips the multi-template guard. Inline comment on template.ts.

  3. ObserverMap ${prop}Changed wrapping is not idempotent — re-running the immediate-execution path against the same prototype nests wrappers indefinitely. Important footgun for the new transform-driven flow that may apply on republish or hot-reload. Inline comment on observer-map.ts.

  4. ObserverMap.applyConfigToSchema mutates a shared schema in place with $observe: false stamps. A caller that reuses one Schema instance across elements, or calls observerMap() twice with progressively-permissive configs, can never re-include previously-excluded nodes (stampObserveFalse only writes false). Now relevant because schemas are commonly shared via definition.schema.

  5. Removed deferred-template guard in ElementController.connect (the if (!this.template && definition.templateOptions === TemplateOptions.deferAndHydrate) return; early-return). With the new template-bridge flow, async-resolved templates make connect() proceed with template === null, which permanently resolves the throwaway controller's _resolvePrerendered/_resolveHydrated to false — even though the replacement controller installed by the template-change subscription will then hydrate correctly. Consumers that read $fastController.isPrerendered before the second controller arrives observe a misleading false. Lifecycle hooks also fire twice.

Medium issues

  • attributeMap() silently no-ops when called without a schema and without a template resolver, while observerMap() throws at the same location (observer-map.ts:109). Inconsistent failure modes — please mirror.
  • HydrationTracker.started latches to true permanently (hydration-tracker.ts:33). Subsequent SSR batches (per-route hydration, dynamically inserted SSR fragments) silently bypass hydrationStarted/hydrationComplete. Either reset on completion or document single-batch.
  • ensureHydrationRuntime mutates ViewTemplate.prototype globally and irreversibly via Object.defineProperty(..., { configurable: false }) (hydration/runtime.ts:22). Once any code path calls enableHydration() in a process, every ViewTemplate becomes hydratable everywhere — including parallel Playwright workers sharing a context. No teardown / per-instance scoping.
  • MutationObserver installed by observeLateAttributes is never disconnected (element-controller.ts:644). It keeps observing while the element is detached and there's no disposal hook on ElementController.
  • forCustomElement re-subscribes a new template and shadowOptions listener on every override invocation (element-controller.ts:865, 875). This was a pre-existing concern in v3, but the new declarative-template hand-off makes the override = true recursion routine, so the leak path is now hit in normal flow rather than rare hot-swap.
  • Change-file review: change/@microsoft-fast-element-22265526-…json correctly uses major. However change/@microsoft-fast-router-de58ce36-…json is the only file in this PR using prerelease — verify that's a valid type for the releases/fast-element-v3 branch in beachball.config.js; everything else uses major/minor/none.

Doc inconsistencies

  • MIGRATION.md:225–227 references @microsoft/fast-element/install-hydratable-view-templates.js as "still available", but that path is not in the export map (only ./hydration.js exists). Either add the export or delete the sentence.
  • MIGRATION.md:162–163 prose says "attributeMap() and observerMap() are imported from @microsoft/fast-element for declarative templates" — but the surrounding code samples (and the actual export map) require /attribute-map.js and /observer-map.js. Prose contradicts examples.
  • MIGRATION.md:65 and migration-guide.md:145 mix one hydration row pointing at /hydration.js into a table whose other rows are all root-package imports. Worth splitting or footnoting.
  • A few stale references to TemplateElement.options() / TemplateElement.config() in DECLARATIVE_DESIGN.md and DECLARATIVE_SCHEMA_OBSERVER_MAP.md — names removed in this PR.

Test coverage gaps (template-bridge.pw.spec.ts)

The new 649-line spec is solid, but I'd add:

  • Message.noTemplateProvided (an <f-template> with no <template> child)
  • Message.moreThanOneTemplateProvided (multiple direct <template> children in one <f-template>)
  • A nested-<template> inside the user template — this would surface issue (2) above
  • A repeat / when directive inside a declarative template
  • An end-to-end hydration test (declarative template + SSR markers + enableHydration + assert hydrationStage === 'hydrated')
  • <slot> elements inside a declarative template
  • Race: FASTElement.define() resolving before the <f-template> is parsed and connected (currently weakly covered by "waits for definition-first markup")

The unit-level observer-map.spec.ts only exercises the immediate-execution path. The transform/lazy/merge path that is the central change of this PR is only covered indirectly by the Playwright spec. A unit test that asserts (a) deterministic transform ordering under attributeMap + observerMap, (b) idempotency under double-application, (c) definition.schema = userSchema is preserved by reference when declarativeTemplate() augments it, would catch regressions much faster.

Smaller observations (non-blocking)

  • package.json: the test condition is set on most flat subpaths but missing on ./debug.js, ./utilities.js, ./state.js, ./context.js, ./di.js, ./dom-policy.js. Either add for symmetry or document the rule.
  • test/tsconfig.json paths block is missing several flat subpaths (binding.js, dom.js, schema.js, templating.js, render.js, …). A new test importing those will silently bypass the path map.
  • src/arrays.ts re-exports SubscriberSet as a type-only export, but it's a runtime class. With verbatimModuleSyntax, consumers cannot construct it through this subpath. If intentional (runtime is via ./observable.js), drop the export from ./arrays.js.
  • dist/declarative/definition-options.* artifacts appear to exist but setDefinitionSchemaTransform is not surfaced as a public subpath. Either expose it (if third-party schema transforms are supported) or remove the stale dist files.
  • template.ts:48 uses bare new Error(...) for a definition-conflict while the rest of the module uses FAST.error(Message.*) — adding a Message enum entry keeps debug-stripped builds usable.
  • template.ts:167 attributeChangedCallback has parameters named previousName / nextName but they are attribute values, not names.

I'll leave the inline comments below for the higher-impact items where the GitHub API supports inline placement on the diff.

Comment thread packages/fast-element/src/declarative/template.ts
Comment thread packages/fast-element/src/declarative/attribute-map.ts Outdated
Comment thread packages/fast-element/MIGRATION.md Outdated
Comment thread packages/fast-element/MIGRATION.md Outdated
Comment thread packages/fast-element/src/components/hydration-tracker.ts
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented Apr 27, 2026

Scope correction

A few items in my review summary above turned out to be in code that is already on releases/fast-element-v3 (landed via PR #7497 / commit b78786a) rather than introduced by this PR. My local clone's base ref was a few commits behind origin, which led the review to over-attribute. Specifically, the following are pre-existing and out of scope for this PR — flagging them only as separate issues you may want to file:

  • ElementController.connect deferred-template guard removal (element-controller.ts:492)
  • ElementController.observeLateAttributes MutationObserver never disconnected (element-controller.ts:644)
  • ElementController.forCustomElement re-subscribe leak on the template/shadowOptions notifiers (element-controller.ts:865, :875)
  • ensureHydrationRuntime mutating ViewTemplate.prototype globally and irreversibly (hydration/runtime.ts:22)
  • HydrationTracker.started latch (hydration-tracker.ts:33) — this PR only touches the JSDoc on this file

The rest of the findings in the summary above (the execFile regression in generate-docs.cjs, the getElementsByTagName bug in template.ts, the ObserverMap ${prop}Changed non-idempotency, applyConfigToSchema shared-schema mutation, attributeMap silent no-op, the change-file prerelease type oddity, and the MIGRATION.md inconsistencies) are in this PR's diff and stand.

Comment thread packages/fast-element/README.md Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread sites/website/src/docs/3.x/getting-started/html-templates.md Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as ready for review April 27, 2026 21:18
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu merged commit a165e94 into releases/fast-element-v3 Apr 28, 2026
11 checks passed
@janechu janechu deleted the users/janechu/reduce-declarative-element-size branch April 28, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants