@adia-ai/adia-ui-forge 0.8.61 → 0.8.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +309 -274
- package/README.md +54 -18
- package/__init__.py +8 -8
- package/agents/a2ui-maintenance-agent.corpus.json +7 -7
- package/agents/a2ui-maintenance-agent.md +6 -6
- package/agents/a2ui-planner-agent.md +11 -11
- package/agents/demo-audit-agent.corpus.json +4 -4
- package/agents/demo-audit-agent.md +8 -9
- package/agents/framework-checker.md +10 -11
- package/agents/framework-planner-agent.md +9 -10
- package/agents/gen-ui-review-agent.corpus.json +4 -4
- package/agents/gen-ui-review-agent.md +8 -8
- package/agents/package-release-agent.corpus.json +4 -4
- package/agents/package-release-agent.md +9 -10
- package/agents/primitive-authoring-agent.corpus.json +1 -1
- package/agents/primitive-authoring-agent.md +5 -5
- package/agents/routing-corpus.json +46 -46
- package/codex.interface.json +1 -1
- package/commands/gen-ui-review.md +1 -1
- package/commands/package-release.md +2 -2
- package/package.json +1 -1
- package/plugin.yaml +2 -2
- package/prompts/gen-ui-review.md +1 -1
- package/prompts/package-release.md +2 -2
- package/references/contracts/a2ui-mcp-surface.md +7 -7
- package/references/contracts/migration-guide-format.md +6 -7
- package/references/shared/content-trust.md +3 -5
- package/skills/a2ui-maintenance/SKILL.md +13 -16
- package/skills/a2ui-maintenance/evals/evals.json +2 -2
- package/skills/a2ui-maintenance/evals/routing-corpus.json +1 -1
- package/skills/a2ui-maintenance/references/anti-patterns.md +10 -10
- package/skills/a2ui-maintenance/references/chunk-authoring.md +11 -12
- package/skills/a2ui-maintenance/references/corpus-discipline.md +13 -14
- package/skills/a2ui-maintenance/references/data-model-reactivity.md +22 -25
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +21 -21
- package/skills/a2ui-maintenance/references/format-extension-decisions.md +12 -13
- package/skills/a2ui-maintenance/references/leverage-rules.md +6 -6
- package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +9 -10
- package/skills/a2ui-maintenance/references/mcp-tool-reference.md +11 -12
- package/skills/a2ui-maintenance/references/pipeline-overview.md +33 -38
- package/skills/a2ui-maintenance/references/semantic-fail-lifting.md +13 -14
- package/skills/a2ui-maintenance/references/strategy-engines.md +14 -16
- package/skills/a2ui-maintenance/references/surface-lifecycle.md +10 -11
- package/skills/a2ui-maintenance/references/zettel-calibration.md +14 -16
- package/skills/component-md-authoring/SKILL.md +20 -21
- package/skills/component-md-authoring/agents/openai.yaml +1 -1
- package/skills/cross-harness-authoring-standards/SKILL.md +19 -21
- package/skills/cross-harness-authoring-standards/agents/openai.yaml +1 -1
- package/skills/cross-harness-compatibility-standards/SKILL.md +34 -35
- package/skills/cross-harness-compatibility-standards/agents/openai.yaml +1 -1
- package/skills/demo-audit/SKILL.md +38 -38
- package/skills/demo-audit/evals/routing-corpus.json +1 -1
- package/skills/demo-audit/references/admin-shell-anatomy.md +21 -25
- package/skills/demo-audit/references/app-shell-pitfalls.md +11 -13
- package/skills/demo-audit/references/auto-fix-allowlist.md +3 -3
- package/skills/demo-audit/references/card-anatomy-sweep.md +10 -11
- package/skills/demo-audit/references/chat-shell-anatomy.md +25 -26
- package/skills/demo-audit/references/editor-shell-anatomy.md +25 -28
- package/skills/demo-audit/references/html-attr-sweep.md +12 -13
- package/skills/demo-audit/references/mode7-status-battery.md +4 -4
- package/skills/demo-audit/references/native-leak-annotations.md +8 -8
- package/skills/demo-audit/references/output-contract-worked-example.md +8 -8
- package/skills/demo-audit/references/probe-discipline-and-escalation.md +2 -2
- package/skills/demo-audit/references/visual-probe-triage.md +21 -23
- package/skills/gen-ui-review/SKILL.md +27 -27
- package/skills/gen-ui-review/agents/openai.yaml +1 -1
- package/skills/gen-ui-review/evals/routing-corpus.json +1 -1
- package/skills/gen-ui-review/references/corpus-html-patterns.md +28 -29
- package/skills/gen-ui-review/references/exit-gate-mechanics.md +5 -5
- package/skills/gen-ui-review/references/lookup-maintenance.md +2 -2
- package/skills/gen-ui-review/references/loop-protocol.md +25 -25
- package/skills/gen-ui-review/references/rubric-cosmetic.md +10 -10
- package/skills/gen-ui-review/references/rubric-decompose.md +15 -16
- package/skills/gen-ui-review/references/rubric-score.md +23 -24
- package/skills/gen-ui-review/references/scorecard-worked-examples.md +12 -12
- package/skills/gen-ui-review/references/scores.schema.json +2 -2
- package/skills/llm-client-maintenance/SKILL.md +12 -12
- package/skills/llm-client-maintenance/evals/routing-corpus.json +1 -1
- package/skills/llm-client-maintenance/references/adapter-contract.md +27 -27
- package/skills/llm-client-maintenance/references/add-a-provider.md +25 -25
- package/skills/llm-client-maintenance/references/bridge-facade.md +31 -31
- package/skills/llm-client-maintenance/references/browser-proxy-boundary.md +17 -17
- package/skills/llm-client-maintenance/references/model-registry.md +14 -14
- package/skills/llm-client-maintenance/references/streaming-sse.md +21 -21
- package/skills/package-release/SKILL.md +21 -23
- package/skills/package-release/evals/evals.json +2 -2
- package/skills/package-release/evals/routing-corpus.json +2 -2
- package/skills/package-release/references/authorization-model.md +6 -8
- package/skills/package-release/references/changelog-discipline.md +25 -27
- package/skills/package-release/references/cut-procedure.md +129 -136
- package/skills/package-release/references/gates-catalog.md +42 -43
- package/skills/package-release/references/independent-package-release.md +14 -15
- package/skills/package-release/references/invariants-detail.md +9 -9
- package/skills/package-release/references/mechanization.md +11 -12
- package/skills/package-release/references/migration-guide-authoring.md +20 -20
- package/skills/package-release/references/notes-authoring.md +20 -20
- package/skills/package-release/references/recovery-paths.md +28 -28
- package/skills/primitive-authoring/SKILL.md +19 -19
- package/skills/primitive-authoring/agents/openai.yaml +1 -1
- package/skills/primitive-authoring/evals/evals.json +2 -2
- package/skills/primitive-authoring/evals/routing-corpus.json +2 -2
- package/skills/primitive-authoring/references/INDEX.md +22 -22
- package/skills/primitive-authoring/references/anti-patterns.md +76 -77
- package/skills/primitive-authoring/references/api-contract.md +63 -65
- package/skills/primitive-authoring/references/authoring-cycle.md +47 -50
- package/skills/primitive-authoring/references/canonical-pattern-index.md +118 -118
- package/skills/primitive-authoring/references/code-style.md +50 -52
- package/skills/primitive-authoring/references/common-gotchas.md +23 -23
- package/skills/primitive-authoring/references/composite-demo-protocol.md +42 -45
- package/skills/primitive-authoring/references/css-patterns.md +97 -97
- package/skills/primitive-authoring/references/for-attribute-event-contract.md +12 -12
- package/skills/primitive-authoring/references/form-control-sizing.md +16 -18
- package/skills/primitive-authoring/references/lifecycle-patterns.md +42 -42
- package/skills/primitive-authoring/references/llm-bridge.md +21 -21
- package/skills/primitive-authoring/references/module-promotion.md +51 -51
- package/skills/primitive-authoring/references/primitive-audit.md +16 -16
- package/skills/primitive-authoring/references/shell-patterns.md +81 -81
- package/skills/primitive-authoring/references/svg-authoring.md +62 -66
- package/skills/primitive-authoring/references/token-contract.md +45 -49
- package/skills/primitive-authoring/references/trait-pages.md +8 -8
- package/skills/primitive-authoring/references/worked-example.md +39 -39
- package/skills/primitive-authoring/references/yaml-contract.md +139 -143
- package/skills/site-deployment/SKILL.md +25 -26
- package/skills/site-deployment/agents/openai.yaml +1 -1
- package/skills/site-deployment/evals/routing-corpus.json +1 -1
- package/skills/site-deployment/references/deploy-playbooks.md +51 -53
- package/skills/site-docs-authoring/SKILL.md +14 -14
- package/skills/site-docs-authoring/agents/openai.yaml +1 -1
- package/skills/site-docs-authoring/evals/evals.json +1 -1
- package/skills/site-docs-authoring/evals/routing-corpus.json +1 -1
- package/skills/site-docs-authoring/intent.md +18 -18
- package/skills/ssr-compatibility/SKILL.md +24 -25
- package/skills/ssr-compatibility/agents/openai.yaml +1 -1
- package/skills/ssr-compatibility/evals/evals.json +1 -1
- package/skills/ssr-compatibility/evals/routing-corpus.json +1 -1
- package/skills/ssr-compatibility/references/consumer-workarounds.md +22 -23
- package/skills/ssr-compatibility/references/failure-shapes.md +84 -91
- package/skills/ssr-compatibility/references/guard-patterns.md +54 -58
- package/skills/ssr-compatibility/references/status-ledger.md +17 -17
- package/skills/ssr-compatibility/references/test-without-linkedom.md +26 -27
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Guard patterns
|
|
1
|
+
# Guard patterns, the established fix shape per failure shape
|
|
2
2
|
|
|
3
3
|
Cites the actual shipped code (gh#285 PR #292, gh#286 PR #290) as the pattern to copy
|
|
4
|
-
for a NEW instance of the same failure class
|
|
4
|
+
for a NEW instance of the same failure class, don't re-derive the shape, match it.
|
|
5
5
|
|
|
6
6
|
## §1 · A browser-only API constructor/method call
|
|
7
7
|
|
|
@@ -9,16 +9,16 @@ for a NEW instance of the same failure class — don't re-derive the shape, matc
|
|
|
9
9
|
`IntersectionObserver`, `MutationObserver`, `PerformanceObserver`) or `typeof
|
|
10
10
|
this.<method> === 'function'` (for instance methods: `attachInternals`) or `'<prop>'
|
|
11
11
|
in <object>` (for object properties: `document.adoptedStyleSheets`) guards the call;
|
|
12
|
-
the ELSE branch is picked by what downstream code does with the reference
|
|
12
|
+
the ELSE branch is picked by what downstream code does with the reference, there is
|
|
13
13
|
no one boilerplate to paste everywhere.
|
|
14
14
|
|
|
15
15
|
**Three concrete shapes, all live in the repo:**
|
|
16
16
|
|
|
17
|
-
**(a) Field assigned, used later with `?.` on teardown
|
|
17
|
+
**(a) Field assigned, used later with `?.` on teardown**, the majority case. Guard the
|
|
18
18
|
construction; every later reference to the field must already (or now needs to) use
|
|
19
19
|
optional chaining, since the field can legitimately stay unset:
|
|
20
20
|
```js
|
|
21
|
-
// packages/web-modules/shell/admin-sidebar/admin-sidebar.js
|
|
21
|
+
// packages/web-modules/shell/admin-sidebar/admin-sidebar.js, #setupChildResizeObserver
|
|
22
22
|
#setupChildResizeObserver() {
|
|
23
23
|
this.#childRO = new ResizeObserver((entries) => { ... });
|
|
24
24
|
this.#childRO.observe(this);
|
|
@@ -31,7 +31,7 @@ if (typeof ResizeObserver !== 'undefined') {
|
|
|
31
31
|
// disconnectedCallback: this.#childRO?.disconnect(); ← already needs the `?.`
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
**(b) Early-return no-op
|
|
34
|
+
**(b) Early-return no-op**, when the entire trait/method's purpose IS the observer;
|
|
35
35
|
nothing else in the function is worth running without it:
|
|
36
36
|
```js
|
|
37
37
|
// packages/web-components/traits/resize-observer/resize-observer.js
|
|
@@ -41,7 +41,7 @@ setup({ host }) {
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**(c) Module-level `const`, ternary at declaration
|
|
44
|
+
**(c) Module-level `const`, ternary at declaration**, when the observer is a
|
|
45
45
|
singleton constructed at import time, not per-instance:
|
|
46
46
|
```js
|
|
47
47
|
// packages/web-components/core/data-stream.js
|
|
@@ -51,10 +51,9 @@ const observer = typeof MutationObserver !== 'undefined'
|
|
|
51
51
|
// every later use: observer?.observe(...) / bootstrap() no-ops via the same check
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
**(d) Feature, not environment
|
|
54
|
+
**(d) Feature, not environment, `typeof window` is NOT a guard (gh#1430).**
|
|
55
55
|
linkedom defines `window` (an object) but neither `matchMedia` nor a numeric
|
|
56
|
-
`innerWidth`, so `typeof window !== 'undefined'` passes and the call still throws
|
|
57
|
-
at MODULE scope that takes every importer off the server. Test the member:
|
|
56
|
+
`innerWidth`, so `typeof window !== 'undefined'` passes and the call still throws, at MODULE scope that takes every importer off the server. Test the member:
|
|
58
57
|
```js
|
|
59
58
|
// packages/web-components/core/responsive.js
|
|
60
59
|
const w = typeof window !== 'undefined' ? window.innerWidth : undefined;
|
|
@@ -62,7 +61,7 @@ if (typeof w !== 'number') return 'lg'; // fal
|
|
|
62
61
|
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { … }
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
**(e) `instanceof <DOM constructor
|
|
64
|
+
**(e) `instanceof <DOM constructor>`, use `core/dom.js` (gh#1436).** Only
|
|
66
65
|
`HTMLElement` is a global under custom-elements-ssr; `v instanceof Node` /
|
|
67
66
|
`Element` / `Text` / `DocumentFragment` is a `ReferenceError`, not `false`.
|
|
68
67
|
`isNode(v)` / `isElement(v)` test `nodeType` structurally (and also accept nodes
|
|
@@ -73,13 +72,13 @@ if (isNode(result)) cell.replaceChildren(result); // renderer output
|
|
|
73
72
|
const row = isElement(e.target) ? e.target.closest('…') : null; // event target
|
|
74
73
|
```
|
|
75
74
|
`isElement` is deliberately the SUPERSET of a former `instanceof HTMLElement` (it
|
|
76
|
-
admits SVG elements too)
|
|
75
|
+
admits SVG elements too), every swept site only needed "an element", none needed
|
|
77
76
|
"an HTML element specifically". Absent-method form of the same idea:
|
|
78
|
-
`typeof this.getBoundingClientRect === 'function' && …` (nav-ui)
|
|
77
|
+
`typeof this.getBoundingClientRect === 'function' && …` (nav-ui), linkedom has no
|
|
79
78
|
rect API at all, so an unguarded call is a TypeError, not a zero rect (that zero-rect
|
|
80
79
|
case is §3).
|
|
81
80
|
|
|
82
|
-
**The `this.internals` special case
|
|
81
|
+
**The `this.internals` special case, a shim, not `undefined`.** `UIElement`'s
|
|
83
82
|
constructor (`packages/web-components/core/element.js`) needed a DIFFERENT answer
|
|
84
83
|
than "guard and leave unset," because ~12 files across the framework call
|
|
85
84
|
`this.internals.setValidity(...)` / `.setFormValue(...)` unconditionally (form
|
|
@@ -108,33 +107,33 @@ returned object/value is always truthy and call methods on it unconditionally?"
|
|
|
108
107
|
yes, a no-op shim matching the ACTUAL consumed surface (grep for it, don't guess) is
|
|
109
108
|
the right answer, not leaving the field `undefined`. If the API is purely
|
|
110
109
|
fire-and-forget infrastructure with no return value anyone depends on (most
|
|
111
|
-
Observers), a plain construction guard (shapes a/b/c above) is sufficient
|
|
110
|
+
Observers), a plain construction guard (shapes a/b/c above) is sufficient, no shim
|
|
112
111
|
needed.
|
|
113
112
|
|
|
114
|
-
## §2 · Destructive `stamp()` on connect (gh#284
|
|
113
|
+
## §2 · Destructive `stamp()` on connect (gh#284, narrowed to a static audit, not a stamp() patch)
|
|
115
114
|
|
|
116
115
|
There is no `stamp()`/`connectedCallback` fix, and per the 2026-07-17 narrowing
|
|
117
|
-
there isn't meant to be one
|
|
116
|
+
there isn't meant to be one: the mechanism is real but zero shipped components
|
|
118
117
|
are exposed (see [`failure-shapes.md`](failure-shapes.md) §2 for the full survey).
|
|
119
118
|
The shipped guard is `scripts/dev/audit-template-child-conflict.mjs`: it flags a
|
|
120
119
|
NEW component that pairs a non-null `static template` with a yaml `slots.default`
|
|
121
|
-
entry (critical) or a body-text usage example (advisory)
|
|
120
|
+
entry (critical) or a body-text usage example (advisory), i.e. it prevents the
|
|
122
121
|
conflict shape from being reintroduced, rather than patching the render lifecycle
|
|
123
122
|
every component goes through (150 at the 2026-07 survey; the census grows). **A component author who hits this
|
|
124
123
|
audit's finding fixes it by making the template `() => null`** (the pattern every
|
|
125
|
-
current children-accepting component already uses
|
|
124
|
+
current children-accepting component already uses, compose via CSS + `render()`'s
|
|
126
125
|
own surgical DOM manipulation, matching `avatar-ui`'s `#imgEl`/`#initialsEl`
|
|
127
|
-
pattern)
|
|
126
|
+
pattern), NOT by inventing a component-local stamp-skip flag or a marker
|
|
128
127
|
attribute; either of those drifts from the established convention and isn't
|
|
129
128
|
covered by this audit's "safe" classification. If a genuinely new component NEEDS
|
|
130
129
|
both a non-null template AND real consumer children (unlike anything shipped
|
|
131
|
-
today), route through `primitive-authoring` before authoring it
|
|
130
|
+
today), route through `primitive-authoring` before authoring it, that's a real design
|
|
132
131
|
question, not a mechanical fix.
|
|
133
132
|
|
|
134
133
|
## §2b · Reflected properties not initialized from pre-existing attributes on upgrade (gh#284's REAL fix, PR #309)
|
|
135
134
|
|
|
136
135
|
**Shape:** a `reflect: true` property stays at its class default after a LATE
|
|
137
|
-
custom-element upgrade
|
|
136
|
+
custom-element upgrade: the element was parsed/appended (or server-rendered)
|
|
138
137
|
with its attribute already present, but the tag wasn't `customElements.define`d
|
|
139
138
|
until afterward. `<nav-item-ui text="Profile">` renders an empty label; any
|
|
140
139
|
other reflected prop on any component is at equal risk under the same
|
|
@@ -143,10 +142,10 @@ sequence.
|
|
|
143
142
|
**Root cause:** the custom-elements spec's "upgrade an element" algorithm
|
|
144
143
|
(§4.13.5 step 6) requires replaying `attributeChangedCallback` for every
|
|
145
144
|
attribute already on the element BEFORE `connectedCallback` fires on upgrade.
|
|
146
|
-
happy-dom (this repo's test DOM) does not do this
|
|
145
|
+
happy-dom (this repo's test DOM) does not do this, confirmed directly with a
|
|
147
146
|
bare `HTMLElement` subclass, no AdiaUI code involved
|
|
148
147
|
(`packages/web-components/core/element.test.js`,
|
|
149
|
-
`describe('UIElement
|
|
148
|
+
`describe('UIElement: SSR attribute-upgrade replay (gh#284)')`, first test).
|
|
150
149
|
linkedom is a similarly from-scratch custom-elements registry, so the same gap
|
|
151
150
|
is the working hypothesis there too pending direct confirmation.
|
|
152
151
|
|
|
@@ -165,29 +164,28 @@ for (const [key, cfg] of Object.entries(ctor.properties)) {
|
|
|
165
164
|
|
|
166
165
|
Safe on every environment: `connectedCallback` fires reliably everywhere
|
|
167
166
|
(unlike the shimmed `attributeChangedCallback` replay), and the resync is a
|
|
168
|
-
no-op where the replay already happened correctly
|
|
167
|
+
no-op where the replay already happened correctly: the property setter's
|
|
169
168
|
`Object.is` check short-circuits when the value already matches, so no extra
|
|
170
|
-
render pass. This is a FRAMEWORK-LEVEL fix, already shipped
|
|
169
|
+
render pass. This is a FRAMEWORK-LEVEL fix, already shipped, a component
|
|
171
170
|
author hitting this shape doesn't need to do anything component-local; if a
|
|
172
171
|
reflected prop still looks wrong after upgrade on a version carrying this fix,
|
|
173
|
-
that's a new, different bug
|
|
172
|
+
that's a new, different bug, don't assume it's this one recurring.
|
|
174
173
|
|
|
175
174
|
## §3 · A connect-time measurement is "unknown," not "confirmed"
|
|
176
175
|
|
|
177
176
|
**Shape:** treat a zero (or otherwise clearly-bogus) synchronous read as "no signal
|
|
178
177
|
yet" and DEFER the decision to whatever async correction mechanism the component
|
|
179
|
-
already has
|
|
178
|
+
already has, don't add a new one if an existing `ResizeObserver`/similar already
|
|
180
179
|
watches the same dimension.
|
|
181
180
|
```js
|
|
182
|
-
// packages/web-modules/shell/admin-sidebar/admin-sidebar.js
|
|
181
|
+
// packages/web-modules/shell/admin-sidebar/admin-sidebar.js, #syncCollapsedFromWidth
|
|
183
182
|
#syncCollapsedFromWidth() {
|
|
184
183
|
const w = this.getBoundingClientRect().width;
|
|
185
|
-
if (w === 0) return; // ← unknown, not "confirmed collapsed"
|
|
186
|
-
this.collapsed = w <= SNAP_THRESHOLD; // leaves whatever connected() already
|
|
184
|
+
if (w === 0) return; // ← unknown, not "confirmed collapsed", this.collapsed = w <= SNAP_THRESHOLD; // leaves whatever connected() already
|
|
187
185
|
} // restored (persisted state, or default)
|
|
188
186
|
|
|
189
187
|
// the component's EXISTING ResizeObserver callback (already watching this.host for
|
|
190
|
-
// an unrelated reason
|
|
188
|
+
// an unrelated reason, flipping select-ui placement) gains ONE more line, so its
|
|
191
189
|
// first REAL tick (post-hydration, or after the display:none ancestor is revealed)
|
|
192
190
|
// resolves what connect-time couldn't:
|
|
193
191
|
#setupChildResizeObserver() {
|
|
@@ -201,25 +199,24 @@ watches the same dimension.
|
|
|
201
199
|
}
|
|
202
200
|
```
|
|
203
201
|
The key move: reuse the observer that's ALREADY there rather than adding a
|
|
204
|
-
purpose-built one
|
|
202
|
+
purpose-built one, a second observer watching the same element for two unrelated
|
|
205
203
|
reasons is a maintenance smell, and per §1 above, a NEW observer needs its own
|
|
206
204
|
construction guard anyway.
|
|
207
205
|
|
|
208
|
-
## §4 · Adopt-or-diff
|
|
206
|
+
## §4 · Adopt-or-diff, value-diff before mutating, never guess or half-adopt (gh#1678, gh#1687)
|
|
209
207
|
|
|
210
208
|
**Shape:** don't mutate a DOM position until a comparison proves it actually
|
|
211
209
|
needs to change. `setAttribute()` queues a mutation record even when the new
|
|
212
210
|
value is byte-identical to the old one (confirmed directly against both
|
|
213
211
|
happy-dom and linkedom); `removeAttribute()` on an already-absent attribute
|
|
214
|
-
does NOT (the spec's own asymmetry
|
|
212
|
+
does NOT (the spec's own asymmetry, no compare-first guard needed there).
|
|
215
213
|
Two concrete shapes, both shipped, picked by whether the reconciled unit
|
|
216
214
|
carries a stable identity key:
|
|
217
215
|
|
|
218
|
-
**(a) A flat, keyed list
|
|
216
|
+
**(a) A flat, keyed list, seed the keyed-reconcile map from the adopted
|
|
219
217
|
DOM (`pagination-ui`, gh#1687).** `connected()` adopts a pre-existing
|
|
220
218
|
server-rendered structural child (`this.#nav = this.querySelector(':scope
|
|
221
|
-
> nav[slot="nav"]')`) instead of unconditionally creating a fresh one
|
|
222
|
-
but adopting the CONTAINER alone isn't sufficient: `reconcile()`'s own
|
|
219
|
+
> nav[slot="nav"]')`) instead of unconditionally creating a fresh one, but adopting the CONTAINER alone isn't sufficient: `reconcile()`'s own
|
|
223
220
|
keyed diff (`core/element.js`) keys off a `parent[KEY_MAP]` populated by
|
|
224
221
|
this element's OWN prior render calls, which a freshly-parsed SSR fragment
|
|
225
222
|
never has. Without seeding it, the very first render treats every adopted
|
|
@@ -230,13 +227,13 @@ mismatch (`#childMatchesItem()` checking tag + marker, not just count) is
|
|
|
230
227
|
left unseeded so reconcile falls back to a genuine clean rebuild instead of
|
|
231
228
|
miskeying a wrong-tagged survivor in place.
|
|
232
229
|
|
|
233
|
-
**(b) An arbitrary positional child with no stable key
|
|
230
|
+
**(b) An arbitrary positional child with no stable key, compare via
|
|
234
231
|
`Node.isEqualNode()` (`table-ui`, gh#1678).** Table cells have no identity
|
|
235
232
|
key the way a pagination item does (no natural "this is always the id
|
|
236
233
|
column" marker independent of position), so `adoptOrDiffChildren()`
|
|
237
234
|
(`table.class.js`) builds each fresh candidate exactly as before, then
|
|
238
235
|
compares it against the existing child at that position with
|
|
239
|
-
`existing.isEqualNode(fresh)
|
|
236
|
+
`existing.isEqualNode(fresh)`, standard DOM, present under every
|
|
240
237
|
environment this framework runs in (browsers, happy-dom, linkedom), never
|
|
241
238
|
one of §1's browser-only APIs:
|
|
242
239
|
```js
|
|
@@ -248,12 +245,12 @@ function adoptOrDiffChildren(container, freshChildren) {
|
|
|
248
245
|
if (!existing) container.appendChild(fresh);
|
|
249
246
|
else if (RENDERER_OWNED.has(fresh) || !existing.isEqualNode(fresh)) container.replaceChild(fresh, existing);
|
|
250
247
|
// else: matches byte-for-byte AND carries no renderer-owned runtime
|
|
251
|
-
// state (§4.1 below)
|
|
248
|
+
// state (§4.1 below), adopt in place, touch nothing.
|
|
252
249
|
}
|
|
253
250
|
}
|
|
254
251
|
```
|
|
255
252
|
A match adopts in place (zero mutation); a mismatch replaces the position
|
|
256
|
-
wholesale exactly as the pre-fix code always did
|
|
253
|
+
wholesale exactly as the pre-fix code always did, never a partial patch of
|
|
257
254
|
a mismatched node's individual attributes, which would risk leaving a
|
|
258
255
|
wrong-tagged or wrong-shaped survivor "fixed" in place instead of really
|
|
259
256
|
rebuilt (the same failure mode (a) above guards against via
|
|
@@ -261,7 +258,7 @@ rebuilt (the same failure mode (a) above guards against via
|
|
|
261
258
|
|
|
262
259
|
### §4.1 · `isEqualNode()` proves structural safety, never runtime-state safety (gh#1678 CodeRabbit follow-up, closed on PR #1756 before merge)
|
|
263
260
|
|
|
264
|
-
**The gap:** (b) above is a *structural* diff
|
|
261
|
+
**The gap:** (b) above is a *structural* diff, tag, attributes, text,
|
|
265
262
|
descendants. It's a necessary adoption test but not a sufficient one: it
|
|
266
263
|
cannot see an event listener (or any other runtime/imperative state) that a
|
|
267
264
|
renderer attached to the candidate node it returned. `table.class.js`'s
|
|
@@ -269,16 +266,16 @@ renderer attached to the candidate node it returned. `table.class.js`'s
|
|
|
269
266
|
consumer function; or a built-in cell-type renderer, `typeDef.render`)
|
|
270
267
|
BEFORE the candidate ever reaches `adoptOrDiffChildren()`. If that renderer
|
|
271
268
|
attached a listener, the candidate can still be `isEqualNode()`-identical to
|
|
272
|
-
the existing (e.g. listener-less, SSR-parsed) DOM at that position
|
|
269
|
+
the existing (e.g. listener-less, SSR-parsed) DOM at that position, the
|
|
273
270
|
guard reports a match, adopts the OLD node, discards the fresh one, and the
|
|
274
271
|
listener silently never lands. The pre-adopt-or-diff, unconditional-
|
|
275
272
|
`replaceChild()` code never had this bug, because it always installed
|
|
276
273
|
whatever the renderer had just built, every time.
|
|
277
274
|
|
|
278
|
-
**The fix
|
|
275
|
+
**The fix, an explicit renderer-owned marker, not a deeper structural
|
|
279
276
|
check.** A `Node.isEqualNode()`-shaped fix can only ever prove declarative
|
|
280
277
|
shape; it structurally cannot see a listener, so the fix isn't "compare
|
|
281
|
-
harder"
|
|
278
|
+
harder", it's "know which candidates a comparison can't clear in the first
|
|
282
279
|
place, and never let structural equality alone adopt one of those." A
|
|
283
280
|
module-level `RENDERER_OWNED` `WeakSet` tags exactly the cells built by
|
|
284
281
|
something free to attach listeners/runtime state:
|
|
@@ -288,7 +285,7 @@ const RENDERER_OWNED = new WeakSet();
|
|
|
288
285
|
if (typeof col.render === 'function') {
|
|
289
286
|
const result = col.render(value, data, cell, dataIndex);
|
|
290
287
|
// ... apply result to cell ...
|
|
291
|
-
RENDERER_OWNED.add(cell); // arbitrary code
|
|
288
|
+
RENDERER_OWNED.add(cell); // arbitrary code, always tag
|
|
292
289
|
} else if (typeof col.format !== 'function') {
|
|
293
290
|
const typeDef = cellTypes[col.type || 'text'];
|
|
294
291
|
if (typeDef?.render) {
|
|
@@ -297,19 +294,19 @@ if (typeof col.render === 'function') {
|
|
|
297
294
|
}
|
|
298
295
|
}
|
|
299
296
|
```
|
|
300
|
-
`col.render()` is always tagged
|
|
297
|
+
`col.render()` is always tagged, it's opaque consumer code, impossible to
|
|
301
298
|
introspect for safety. A built-in cell-type renderer is tagged only when
|
|
302
|
-
its own registration declares `attachesListeners: true
|
|
299
|
+
its own registration declares `attachesListeners: true`, in this
|
|
303
300
|
framework, currently just `cellTypes.actions` (`cell-types.js`), the one
|
|
304
301
|
built-in type that calls `addEventListener()` directly on a node it builds.
|
|
305
302
|
Every OTHER built-in cell type (text/number/currency/percent/date/datetime/
|
|
306
303
|
boolean/badge/avatar/link/markdown/progress) only sets attributes on
|
|
307
|
-
already-declarative custom elements or plain nodes
|
|
304
|
+
already-declarative custom elements or plain nodes, no listeners, so
|
|
308
305
|
tagging them buys nothing and costs the zero-mutation benefit for the
|
|
309
306
|
overwhelmingly common case. **This was measured, not assumed**: an earlier
|
|
310
307
|
draft of this fix tagged every `typeDef.render` cell unconditionally and
|
|
311
308
|
broke the AC-004a zero-mutation test (§ above) for plain text/number/date
|
|
312
|
-
cells
|
|
309
|
+
cells, narrowed to the declared-flag form before merge.
|
|
313
310
|
|
|
314
311
|
**Applying this pattern to a NEW adopt-or-diff instance (elsewhere in this
|
|
315
312
|
framework, or #1755/#1754 if either goes this direction):** before trusting
|
|
@@ -317,17 +314,17 @@ framework, or #1755/#1754 if either goes this direction):** before trusting
|
|
|
317
314
|
invokes to build a candidate node is free to attach a listener or stash
|
|
318
315
|
other runtime state on it. If yes, that candidate needs its own
|
|
319
316
|
`RENDERER_OWNED`-shaped tag (or equivalent) and must always be replaced,
|
|
320
|
-
never adopted on structural equality alone
|
|
317
|
+
never adopted on structural equality alone, an activation step that tries
|
|
321
318
|
to re-attach the listener onto the ADOPTED node instead is a fragile
|
|
322
319
|
protocol this fix deliberately did not attempt (unclear how to discover
|
|
323
320
|
"what would the renderer have attached" without re-running the renderer,
|
|
324
321
|
at which point you already have the fresh node to just use).
|
|
325
322
|
|
|
326
|
-
**(c) Idempotent attribute writes
|
|
323
|
+
**(c) Idempotent attribute writes, the write itself must be guarded, not
|
|
327
324
|
just the value.** Any attribute set that runs on EVERY render/connect
|
|
328
325
|
regardless of whether the value changed (a host's `role`/`tabindex`, an
|
|
329
326
|
inline computed style, a row's `data-index`/`aria-selected`) needs a
|
|
330
|
-
compare-before-write wrapper
|
|
327
|
+
compare-before-write wrapper, `setAttrIfChanged(el, name, value)`, because
|
|
331
328
|
a bare `setAttribute(el, name, sameValue)` still mutates. `removeAttribute`
|
|
332
329
|
needs no equivalent guard (already a no-op on an absent attribute per
|
|
333
330
|
spec, confirmed directly).
|
|
@@ -338,9 +335,8 @@ of cells/fields with no such per-position identity → (b). Both fall back to
|
|
|
338
335
|
the SAME principle on a mismatch: rebuild for real, never guess and never
|
|
339
336
|
half-adopt (leave a wrong node "patched" in place instead of replaced).
|
|
340
337
|
|
|
341
|
-
**The conditional-inject class (querySelector-guard-before-innerHTML
|
|
342
|
-
`
|
|
343
|
-
granularity** — `if (!this.querySelector('input-ui')) this.innerHTML = …`
|
|
338
|
+
**The conditional-inject class (querySelector-guard-before-innerHTML, `search-ui`, `pagination-ui`) is this same principle at container
|
|
339
|
+
granularity**, `if (!this.querySelector('input-ui')) this.innerHTML = …`
|
|
344
340
|
IS an adopt-or-diff check, just a boolean presence check instead of a value
|
|
345
341
|
comparison. It's SSR-safe exactly when the querySelector target is
|
|
346
342
|
STRUCTURALLY SPECIFIC (an exact tag/slot, never "has any children at all").
|
|
@@ -351,4 +347,4 @@ idempotent-write guard, not yet applied to `search-ui`/`pagination-ui`).
|
|
|
351
347
|
## Verify targets for a new guard
|
|
352
348
|
|
|
353
349
|
See [`test-without-linkedom.md`](test-without-linkedom.md) for how to prove a new
|
|
354
|
-
guard actually works
|
|
350
|
+
guard actually works, deleting the real API in a test, not mocking it.
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
# SSR support
|
|
1
|
+
# SSR support, status ledger
|
|
2
2
|
|
|
3
3
|
**Filing a new downstream SSR-seam report?** Use the `ssr` label plus
|
|
4
4
|
[`.github/ISSUE_TEMPLATE/ssr-seam-report.md`](../../../../../../.github/ISSUE_TEMPLATE/ssr-seam-report.md)
|
|
5
|
-
(gh#1384)
|
|
5
|
+
(gh#1384), it captures the resolved `@adia-ai/web-components` version, the
|
|
6
6
|
downstream kit version, the component + fixture, an SSR-vs-CSR diff or
|
|
7
7
|
minimal repro, and a cosmetic/behavioral/blocked-upstream classification, so
|
|
8
8
|
a report lands triageable instead of free-form (the archaeology the #284/#288
|
|
9
9
|
rows below needed). `docs/ops/spec/spec-ssr-kit.md` is referenced by some
|
|
10
|
-
downstream reports but does not exist in this repo
|
|
10
|
+
downstream reports but does not exist in this repo, it lives in adiav2's own
|
|
11
11
|
repo (`@adiav2/ssr-kit`'s spec); do not look for it here.
|
|
12
12
|
|
|
13
|
-
**This file drifts fast
|
|
13
|
+
**This file drifts fast, re-verify against the live issue tracker
|
|
14
14
|
(`gh issue view <n>`) before trusting it for anything beyond a quick orientation.**
|
|
15
15
|
Every row was true as of 2026-07-17; a closed issue below may have re-opened, a PR
|
|
16
16
|
number may have been superseded by a follow-up fix, etc. This ledger is a map, not
|
|
17
|
-
the territory
|
|
17
|
+
the territory: the territory is `gh issue list` / `gh issue view`.
|
|
18
18
|
|
|
19
19
|
| Issue | Failure shape | Status | Shipped in | Notes |
|
|
20
20
|
|---|---|---|---|---|
|
|
21
|
-
| [#285](https://github.com/adiahealth/gen-ui-kit/issues/285) | §1
|
|
22
|
-
| [#1430](https://github.com/adiahealth/gen-ui-kit/issues/1430) | §1
|
|
23
|
-
| [#1436](https://github.com/adiahealth/gen-ui-kit/issues/1436) | §1
|
|
24
|
-
| [#1435](https://github.com/adiahealth/gen-ui-kit/issues/1435) | reported as §2
|
|
25
|
-
| [#286](https://github.com/adiahealth/gen-ui-kit/issues/286) | §3
|
|
26
|
-
| [#284](https://github.com/adiahealth/gen-ui-kit/issues/284) | §2
|
|
27
|
-
| [#288](https://github.com/adiahealth/gen-ui-kit/issues/288) | §4
|
|
28
|
-
| [#287](https://github.com/adiahealth/gen-ui-kit/issues/287) | Unrelated to the three SSR shapes above
|
|
29
|
-
| [#1678](https://github.com/adiahealth/gen-ui-kit/issues/1678) | §5 (new)
|
|
21
|
+
| [#285](https://github.com/adiahealth/gen-ui-kit/issues/285) | §1, browser-only API called unconditionally | **CLOSED** | PR #292 (merged 2026-07-17) | `UIElement` constructor + `adoptStyles()` + 22 component/trait/module files guarded; 10 more already correct from an earlier pass |
|
|
22
|
+
| [#1430](https://github.com/adiahealth/gen-ui-kit/issues/1430) | §1, browser-only API called unconditionally (module scope: `window.matchMedia` behind a `typeof window` guard) | **FIXED 2026-08-17** | `core/responsive.js` feature-detects `matchMedia` + `innerWidth`; `swiper.class.js` `play()` guarded; `scripts/dev/ssr-linkedom-smoke.mjs` + `test/ssr-linkedom-smoke.test.js` gate the whole catalog under linkedom | 6 components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`) were un-importable server-side; 118→124 of 124. Side findings on the issue: gh#285's "shim no longer necessary" over-reached (two of the consumer's shim sections patch `custom-elements-ssr` itself, not this library, see `consumer-workarounds.md`); `audit-template-child-conflict.mjs` isn't shipped in the package. |
|
|
23
|
+
| [#1436](https://github.com/adiahealth/gen-ui-kit/issues/1436) | §1, bare `instanceof Node/Element/HTMLElement` (ReferenceError under linkedom, only `HTMLElement` is global) | **FIXED 2026-08-17** | `core/dom.js` `isNode()`/`isElement()`; 15 sites swept; plus rAF/MutationObserver/getBoundingClientRect guards in feed, noodles, preview, toolbar, nav-group, stepper, nav found by the same gate's render sweep | Same PR as #1430, one root-cause class. |
|
|
24
|
+
| [#1435](https://github.com/adiahealth/gen-ui-kit/issues/1435) | reported as §2, check/radio/switch "lose" a `<span slot="label">` child | **NOT an SSR seam** (2026-08-17) | yaml/a2ui `slots` descriptions clarified for the three (separate follow-up PR, a docs-contract change, not a shape-1 guard) | The fixture is not a supported form in CSR either: `label` on all three is ATTRIBUTE-driven (`label="…"` → `aria-label` + `::after content: attr(label)` / template-owned span); a late CSR upgrade replaces the foreign child the same way. The yaml `slots:` entries name template-OWNED anatomy parts (`box`/`dot`/`track`/`thumb`/`label`), which a downstream reimplementation of the audit read as consumer insertion points, the audit's own definition (`slots.default` + non-null template) is what "consumer children" means here. |
|
|
25
|
+
| [#286](https://github.com/adiahealth/gen-ui-kit/issues/286) | §3, connect-time measurement treated as confirmed | **CLOSED** (for `admin-sidebar` specifically) | PR #290 (merged 2026-07-17) | The GENERAL pattern (any other component reading a rect/size synchronously at connect) is NOT swept, only this one instance is fixed |
|
|
26
|
+
| [#284](https://github.com/adiahealth/gen-ui-kit/issues/284) | §2, destructive `stamp()` on connect | **CLOSED 2026-07-18, narrowed AND separately fixed** | `scripts/dev/audit-template-child-conflict.mjs` (PR #295) + `packages/web-components/core/element.js` connectedCallback resync (PR #309) | Two distinct findings. (1) The ORIGINAL diagnosis (destructive `stamp()`) was narrowed 2026-07-17: every component the issue names has `static template = () => null`, so `stamp()` never runs for them, zero shipped components exposed; a static audit gate catches a future regression instead of a lifecycle rewrite. (2) A SEPARATE, real, live bug was found in the same investigation area and fixed 2026-07-18: happy-dom/linkedom don't replay `attributeChangedCallback` for attributes already present at custom-element upgrade (spec §4.13.5 step 6), any `reflect: true` property seeded only from pre-parsed/SSR HTML stayed at its class default after upgrade (e.g. `<nav-item-ui text="Profile">` rendered with an empty label). `connectedCallback` now re-syncs every declared property from its live attribute before `connected()` runs. See `failure-shapes.md` §2 for the full narrative. |
|
|
27
|
+
| [#288](https://github.com/adiahealth/gen-ui-kit/issues/288) | §4, property-only components can't seed from SSR HTML | **CLOSED 2026-07-18** | `table.class.js` `data="[…]"` attribute hydration | Never actually blocked on #284 (table-ui/chart-ui/select-ui all use `static template = () => null`, so the stamp() question never applied). Scope was narrower than filed: `select-ui` already parsed declarative `<option>` children, `chart-ui` already hydrated `.data` from a `data="[…]"` attribute, only `table-ui`'s `.data` had no declarative form. Fixed in the same attribute-hydration shape as chart-ui, not the JSON-script-child form originally proposed. |
|
|
28
|
+
| [#287](https://github.com/adiahealth/gen-ui-kit/issues/287) | Unrelated to the three SSR shapes above, a Phosphor-icon `import.meta.glob` path failure under workspace hoisting, plus the dep-optimizer silently emptying the icon registry | **CLOSED** | PR #294 (merged 2026-07-17) | Root cause confirmed empirically (a from-scratch pnpm workspace repro): the glob's leading `/` joined to Vite's configured root, which only reaches a transitive dependency's assets when hoisted there. Fixed by switching to an entry-file-relative glob (`../node_modules/...`). A second, independent cause (Vite's dev-server `optimizeDeps` pre-bundling) has no package-side fix, documented as a consumer `optimizeDeps.exclude` workaround. A bundler/workspace-resolution bug, not an SSR-lifecycle bug, still doesn't route through this pack's failure taxonomy. |
|
|
29
|
+
| [#1678](https://github.com/adiahealth/gen-ui-kit/issues/1678) | §5 (new), a custom `render()`/`connected()` unconditionally rebuilds a subtree that already matches; §5.1 (new), `isEqualNode()` proves structural safety only, never runtime-state safety (a renderer-owned listener can be silently dropped on adoption, CodeRabbit finding, closed in the same PR); §6 (new), the conditional-inject class, decision recorded | **PR #1756 opened 2026-08-20 (branch `1678-table-ssr-adopt-or-diff`), NOT yet merged, human review required before landing (core table-ui render path)** | `table.class.js` `adoptOrDiffChildren()` + `RENDERER_OWNED` + `setAttrIfChanged()`/`removeAttrIfPresent()`; `cell-types.js`'s `attachesListeners: true` on `actions` | §5/§5.1 implemented for table-ui only, pending merge, no framework-wide static audit exists for this shape yet. §6's decision: querySelector-guard-before-innerHTML (`search-ui`, `pagination-ui`) is SSR-safe when the guard target is structurally specific; [#1755](https://github.com/adiahealth/gen-ui-kit/issues/1755) (filed alongside this PR, already merged) extends the idempotent-write guard to those two components' own post-adopt attribute writes, scoped OUT of this PR. |
|
|
30
30
|
|
|
31
31
|
## What "done" looks like for the open items
|
|
32
32
|
|
|
33
|
-
- **#284's original scope is closed as a non-issue against 0.8.4
|
|
33
|
+
- **#284's original scope is closed as a non-issue against 0.8.4**, see the ledger
|
|
34
34
|
row above. Re-open only if a FUTURE component's yaml/class.js trips
|
|
35
35
|
`audit-template-child-conflict.mjs`, or if `adiav2`'s production workaround
|
|
36
36
|
(`consumer-workarounds.md`) turns out to be catching something this repo's
|
|
37
37
|
synthetic repro didn't (confirm via the issue thread before assuming so).
|
|
38
|
-
- **#288 is done
|
|
38
|
+
- **#288 is done**, see the ledger row above. `table-ui`'s `data="[…]"` attribute
|
|
39
39
|
is the shipped form; `select-ui` and `chart-ui` already had their own declarative
|
|
40
40
|
forms before this issue was even filed.
|
|
41
41
|
|
|
@@ -43,5 +43,5 @@ the territory — the territory is `gh issue list` / `gh issue view`.
|
|
|
43
43
|
|
|
44
44
|
When any row's status changes, update the table AND re-check whether
|
|
45
45
|
[`consumer-workarounds.md`](consumer-workarounds.md)'s claims about what's "now
|
|
46
|
-
deletable" are still accurate
|
|
46
|
+
deletable" are still accurate, a workaround section that says "should no longer be
|
|
47
47
|
necessary" for a shape that's since re-opened is a false manifest, not a stale detail.
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
# Testing an SSR gap
|
|
1
|
+
# Testing an SSR gap, the linkedom gate first, then the deletion pattern
|
|
2
2
|
|
|
3
3
|
**UPDATE 2026-08-17 (gh#1430/#1436): this repo now HAS a `linkedom` root
|
|
4
4
|
devDependency and a shipped shim gate.** `scripts/dev/ssr-linkedom-smoke.mjs`
|
|
5
5
|
installs exactly the six globals `custom-elements-ssr/server-shim.js` installs
|
|
6
|
-
(document, window, customElements, HTMLElement, Event, CustomEvent
|
|
6
|
+
(document, window, customElements, HTMLElement, Event, CustomEvent, and nothing
|
|
7
7
|
else: no Node/Element/Text/DocumentFragment, no matchMedia, no
|
|
8
8
|
requestAnimationFrame, no getBoundingClientRect), imports every
|
|
9
9
|
`components/*/*.js` entry, and constructs + connects every registered tag the way
|
|
10
10
|
`CustomElementRender` does; `packages/web-components/test/ssr-linkedom-smoke.test.js`
|
|
11
11
|
runs it as a Node child process and fails on any import or render throw. **For a
|
|
12
|
-
shape-1 question ("does this crash under SSR"), run that first
|
|
13
|
-
scripts/dev/ssr-linkedom-smoke.mjs
|
|
12
|
+
shape-1 question ("does this crash under SSR"), run that first**, `node
|
|
13
|
+
scripts/dev/ssr-linkedom-smoke.mjs`: it is the consumer's environment, not an
|
|
14
14
|
approximation. Add a row to its `FIXTURES` table when a guard lands on a branch only
|
|
15
15
|
some attribute value reaches (`swiper-ui[autoplay]` is the model). The rest of this
|
|
16
16
|
file is the UNIT-level pattern for a focused regression test in the happy-dom
|
|
17
17
|
suite; it stays valid, but it is no longer the only proof available.
|
|
18
18
|
|
|
19
19
|
The main test suite still runs on `happy-dom` (`vitest.config.js`), which
|
|
20
|
-
implements most of the APIs linkedom lacks
|
|
20
|
+
implements most of the APIs linkedom lacks, so a naive test using the default
|
|
21
21
|
environment will not reproduce an SSR-shaped bug. Two consequences:
|
|
22
22
|
|
|
23
23
|
1. **You cannot trust "the tests pass" as proof an SSR fix works** unless the test
|
|
24
24
|
itself removes the API under test. A guard around `attachInternals` that's never
|
|
25
25
|
exercised without `attachInternals` proves nothing.
|
|
26
|
-
2. **The house pattern is deletion, not mocking
|
|
26
|
+
2. **The house pattern is deletion, not mocking**, `delete
|
|
27
27
|
HTMLElement.prototype.attachInternals` (or `delete
|
|
28
28
|
Document.prototype.adoptedStyleSheets`, or `delete globalThis.ResizeObserver`)
|
|
29
29
|
inside a `try { ... } finally { restore }` block, in the SAME test environment the
|
|
30
30
|
suite already runs in. This is a closer approximation of "the API is genuinely
|
|
31
31
|
absent" than a mock that might itself paper over the exact code path that would
|
|
32
|
-
throw
|
|
32
|
+
throw, deleting the real thing exercises the REAL `typeof x === 'undefined'` /
|
|
33
33
|
`typeof x === 'function'` check the guard actually uses.
|
|
34
34
|
|
|
35
|
-
## The pattern (descriptor-based restore
|
|
35
|
+
## The pattern (descriptor-based restore, copy this shape for a new test)
|
|
36
36
|
|
|
37
37
|
The shipped test (`packages/web-components/core/element.test.js`, describe block
|
|
38
|
-
`'UIElement
|
|
38
|
+
`'UIElement, SSR browser-API absence (gh#285)'`) restores via a plain `=` assignment
|
|
39
39
|
(`HTMLElement.prototype.attachInternals = original`). That works there ONLY because
|
|
40
40
|
`original` is always a real function in this repo's test environment (happy-dom always
|
|
41
|
-
provides `attachInternals`)
|
|
41
|
+
provides `attachInternals`), it never actually exercises the absent-original case.
|
|
42
42
|
**Don't copy that shortcut.** If `original` is ever genuinely `undefined` (a NEW test
|
|
43
43
|
targeting an API this environment doesn't have either), a plain assignment leaves the
|
|
44
|
-
property present with value `undefined` instead of restoring true absence
|
|
44
|
+
property present with value `undefined` instead of restoring true absence, later
|
|
45
45
|
tests checking `'attachInternals' in HTMLElement.prototype` would wrongly see `true`,
|
|
46
46
|
making pass/fail order-dependent (flagged by review on this pack's own PR #293).
|
|
47
|
-
Use the descriptor-based restore
|
|
48
|
-
below
|
|
47
|
+
Use the descriptor-based restore, the exact shape `adoptedStyleSheets` already uses
|
|
48
|
+
below, for every new instance of this pattern:
|
|
49
49
|
|
|
50
50
|
```js
|
|
51
51
|
it('constructs without throwing when attachInternals does not exist', () => {
|
|
@@ -61,8 +61,7 @@ it('constructs without throwing when attachInternals does not exist', () => {
|
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
For a global (not a prototype method), the same descriptor-based restore applies
|
|
65
|
-
globals are own-properties of `globalThis`, so `Object.getOwnPropertyDescriptor`
|
|
64
|
+
For a global (not a prototype method), the same descriptor-based restore applies, globals are own-properties of `globalThis`, so `Object.getOwnPropertyDescriptor`
|
|
66
65
|
works identically:
|
|
67
66
|
```js
|
|
68
67
|
const desc = Object.getOwnPropertyDescriptor(globalThis, 'ResizeObserver');
|
|
@@ -91,31 +90,31 @@ try {
|
|
|
91
90
|
Deletion proves "the guarded code path doesn't throw when the API is absent." It does
|
|
92
91
|
**not** prove:
|
|
93
92
|
- that linkedom's ACTUAL behavior matches "absent" exactly (linkedom may implement a
|
|
94
|
-
PARTIAL or subtly-wrong version of an API rather than nothing at all
|
|
93
|
+
PARTIAL or subtly-wrong version of an API rather than nothing at all, always check
|
|
95
94
|
the consumer's bug report for the exact error, don't assume "missing" when the
|
|
96
95
|
report says something more specific);
|
|
97
96
|
- anything about shape 2's ORIGINAL destructive-`stamp()` diagnosis (narrowed
|
|
98
|
-
2026-07-17) or shape 5 (declarative data binding, gh#288)
|
|
97
|
+
2026-07-17) or shape 5 (declarative data binding, gh#288), neither is an
|
|
99
98
|
API-absence bug, so this deletion-based method doesn't apply to them. Shape
|
|
100
99
|
2's narrowing was verified with exactly the shape of test this section
|
|
101
|
-
describes
|
|
100
|
+
describes, a container element given real children (or text) BEFORE
|
|
102
101
|
`document.appendChild()` connects it (simulating server-parsed markup that
|
|
103
|
-
already has content when `connectedCallback` first runs)
|
|
102
|
+
already has content when `connectedCallback` first runs), confirming
|
|
104
103
|
`nav-ui`/`check-ui` preserve pre-existing content while a synthetic
|
|
105
104
|
non-null-template component loses it (see
|
|
106
105
|
[`failure-shapes.md`](failure-shapes.md) §2). That reproduction was a
|
|
107
|
-
throwaway, not a shipped permanent test
|
|
106
|
+
throwaway, not a shipped permanent test, if shape 2 needs re-verifying later
|
|
108
107
|
(a new component trips `audit-template-child-conflict.mjs`, or the consumer
|
|
109
108
|
reports something new), rebuild it the same way rather than assuming the old
|
|
110
109
|
narrowing still holds.
|
|
111
110
|
|
|
112
111
|
**Shape 2b (the attribute-upgrade-replay gap, gh#284's real fix, 2026-07-18)
|
|
113
|
-
IS testable this way, and now HAS a shipped permanent test
|
|
112
|
+
IS testable this way, and now HAS a shipped permanent test**, it's not an
|
|
114
113
|
API-absence bug either, but the exact same "define the tag AFTER the markup
|
|
115
114
|
already exists" sequence reproduces it directly, no deletion trick needed
|
|
116
115
|
(the gap is happy-dom's own upgrade behavior, not a missing API to delete).
|
|
117
116
|
See `packages/web-components/core/element.test.js`,
|
|
118
|
-
`describe('UIElement
|
|
117
|
+
`describe('UIElement: SSR attribute-upgrade replay (gh#284)')`, for the
|
|
119
118
|
canonical `mountLateUpgrade()` helper and four tests: confirms the
|
|
120
119
|
environment gap on a bare custom element, confirms a reflected string prop
|
|
121
120
|
and a reflected boolean prop both resolve correctly post-fix, and confirms
|
|
@@ -126,19 +125,19 @@ Deletion proves "the guarded code path doesn't throw when the API is absent." It
|
|
|
126
125
|
## Live-browser confirmation is still required, separately
|
|
127
126
|
|
|
128
127
|
The delete/try/finally tests prove the GUARD doesn't crash. They do not prove real
|
|
129
|
-
browsers are unaffected
|
|
128
|
+
browsers are unaffected, that's a SEPARATE check, done live (Playwright or the
|
|
130
129
|
Chrome extension), confirming the real API still gets used correctly when it's
|
|
131
130
|
actually present. Both gh#285 and gh#286's fixes were verified this second way before
|
|
132
131
|
shipping (a real `ElementInternals` instance still used and functional; a real
|
|
133
|
-
`ResizeObserver` tick still corrects state)
|
|
132
|
+
`ResizeObserver` tick still corrects state), do both, never one instead of the other.
|
|
134
133
|
|
|
135
|
-
## The deeper simulation
|
|
134
|
+
## The deeper simulation, DONE 2026-08-17
|
|
136
135
|
|
|
137
136
|
The real-`linkedom` simulation this section used to propose is now the shipped
|
|
138
137
|
gate described at the top of this file (`scripts/dev/ssr-linkedom-smoke.mjs`,
|
|
139
|
-
`linkedom` pinned as a root devDependency, used only there
|
|
138
|
+
`linkedom` pinned as a root devDependency, used only there, the main suite stays
|
|
140
139
|
on happy-dom). It sweeps import + connect for the whole catalog; a shape-2/4
|
|
141
140
|
question (content preserved across a late upgrade, declarative data seeding) is
|
|
142
|
-
still answered by a targeted test
|
|
141
|
+
still answered by a targeted test, extend the script's `FIXTURES` table with the
|
|
143
142
|
attrs/children in question, or write the assertion in
|
|
144
143
|
`test/ssr-upgrade-adoption-contract.test.js`'s late-upgrade shape.
|