octane 0.1.3 → 0.1.4
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/dist/compiler/compile.js +2437 -379
- package/dist/compiler/slot-hooks.js +28 -5
- package/dist/compiler/vite.js +84 -7
- package/dist/constants.d.ts +28 -13
- package/dist/constants.js +25 -81
- package/dist/css.js +35 -0
- package/dist/dom-tables.d.ts +13 -0
- package/dist/dom-tables.js +312 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +88 -0
- package/dist/runtime.d.ts +264 -17
- package/dist/runtime.js +2180 -294
- package/dist/runtime.server.d.ts +140 -7
- package/dist/runtime.server.js +681 -64
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +43 -1
- package/dist/server/rpc.js +9 -0
- package/dist/static/index.d.ts +14 -0
- package/dist/static/index.js +4 -0
- package/package.json +3 -3
package/dist/compiler/compile.js
CHANGED
|
@@ -37,22 +37,20 @@ import {
|
|
|
37
37
|
import { print as esrapPrint } from 'esrap';
|
|
38
38
|
import esrapTsx from 'esrap/languages/tsx';
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'wbr',
|
|
55
|
-
]);
|
|
40
|
+
// DOM truth tables shared with the client/server runtimes (via constants.ts) —
|
|
41
|
+
// static bakes and dynamic writes MUST agree on which attributes render, under
|
|
42
|
+
// what name, and in what form, or client/SSR/hydration drift apart. See the
|
|
43
|
+
// dom-tables.js header for per-table semantics.
|
|
44
|
+
import {
|
|
45
|
+
VOID_ELEMENTS,
|
|
46
|
+
BOOLEAN_ATTR_PROPS,
|
|
47
|
+
MUST_USE_PROPERTY_PROPS,
|
|
48
|
+
SVG_ONLY_TAGS,
|
|
49
|
+
ATTRIBUTE_ALIASES,
|
|
50
|
+
isEnumeratedBooleanAttr,
|
|
51
|
+
cssStyleValue,
|
|
52
|
+
hyphenateStyleName,
|
|
53
|
+
} from '../dom-tables.js';
|
|
56
54
|
|
|
57
55
|
// React parity: a void element must neither have children nor use
|
|
58
56
|
// `dangerouslySetInnerHTML` — React throws (ReactDOMComponent-test.js:1794/:1807).
|
|
@@ -100,6 +98,129 @@ function rejectVoidElementContent(tag, node, ctx) {
|
|
|
100
98
|
);
|
|
101
99
|
}
|
|
102
100
|
|
|
101
|
+
// Controlled-form binding kinds: `value`/`checked`/`defaultValue`/
|
|
102
|
+
// `defaultChecked` on <input>/<textarea>/<select> route to the runtime
|
|
103
|
+
// property helpers (setValue & co — React-parity controlled semantics on
|
|
104
|
+
// native events) instead of setAttribute. STATIC literals included: baking
|
|
105
|
+
// `value="a"` into the template would freeze the attribute instead of driving
|
|
106
|
+
// the property. Everything else — <option> (its `value` attribute is what the
|
|
107
|
+
// select projection reads), custom elements, non-form tags — keeps plain
|
|
108
|
+
// attribute semantics. Mirrors the runtime's setAttribute routing branch.
|
|
109
|
+
function controlledKindFor(tag, attrName) {
|
|
110
|
+
if (tag === 'input') {
|
|
111
|
+
if (attrName === 'value') return 'value';
|
|
112
|
+
if (attrName === 'checked') return 'checked';
|
|
113
|
+
if (attrName === 'defaultValue') return 'defaultValue';
|
|
114
|
+
if (attrName === 'defaultChecked') return 'defaultChecked';
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (tag === 'textarea') {
|
|
118
|
+
if (attrName === 'value') return 'value';
|
|
119
|
+
if (attrName === 'defaultValue') return 'defaultValue';
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (tag === 'select') {
|
|
123
|
+
if (attrName === 'value') return 'selectValue';
|
|
124
|
+
if (attrName === 'defaultValue') return 'defaultValue';
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The `_$`-aliased runtime helper for each controlled binding kind
|
|
131
|
+
// (+ autoFocus, which shares the routing but is mount-only).
|
|
132
|
+
const CONTROLLED_KIND_HELPERS = {
|
|
133
|
+
value: 'setValue',
|
|
134
|
+
checked: 'setChecked',
|
|
135
|
+
selectValue: 'setSelectValue',
|
|
136
|
+
defaultValue: 'setDefaultValue',
|
|
137
|
+
defaultChecked: 'setDefaultChecked',
|
|
138
|
+
autoFocus: 'setAutoFocus',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Serialize one STATIC literal attribute value into template/SSR HTML —
|
|
142
|
+
// shared by emitElementHtml and ssrEmitElement so both bakes stay identical,
|
|
143
|
+
// mirroring the runtimes' dynamic coercion: aria-*/enumerated/data-* booleans
|
|
144
|
+
// stringify, boolean-attr props render the canonical `attr=""` presence form
|
|
145
|
+
// (falsy drops; overloaded download/capture keep string payloads), booleans
|
|
146
|
+
// on non-boolean attrs DROP (React: `title={true}` never renders), everything
|
|
147
|
+
// else escapes as before. Custom elements keep raw semantics.
|
|
148
|
+
function bakeStaticAttr(attrName, lv, tag) {
|
|
149
|
+
if (lv == null) return '';
|
|
150
|
+
const isCustom = tag !== undefined && tag.includes('-');
|
|
151
|
+
const lower = attrName.toLowerCase();
|
|
152
|
+
if (typeof lv === 'boolean') {
|
|
153
|
+
if (attrName.startsWith('aria-') || attrName.startsWith('data-')) {
|
|
154
|
+
return ` ${attrName}="${lv}"`;
|
|
155
|
+
}
|
|
156
|
+
// Enumerated booleans stringify — "false" is a real state, absent means
|
|
157
|
+
// inherit (the same gate the runtimes apply to dynamic values).
|
|
158
|
+
if (isEnumeratedBooleanAttr(lower)) {
|
|
159
|
+
return ` ${attrName}="${lv}"`;
|
|
160
|
+
}
|
|
161
|
+
if (isCustom) return lv ? ` ${attrName}` : '';
|
|
162
|
+
// Boolean attrs + the overloaded booleans (download/capture) + the
|
|
163
|
+
// mustUseProperty statics all take presence semantics for BOOLEAN
|
|
164
|
+
// literals; non-boolean overloaded values pass through verbatim below.
|
|
165
|
+
if (
|
|
166
|
+
BOOLEAN_ATTR_PROPS.has(lower) ||
|
|
167
|
+
MUST_USE_PROPERTY_PROPS.has(lower) ||
|
|
168
|
+
lower === 'download' ||
|
|
169
|
+
lower === 'capture'
|
|
170
|
+
) {
|
|
171
|
+
return lv ? ` ${lower}=""` : '';
|
|
172
|
+
}
|
|
173
|
+
return ''; // boolean on a non-boolean attribute never renders (React)
|
|
174
|
+
}
|
|
175
|
+
if (!isCustom && (BOOLEAN_ATTR_PROPS.has(lower) || MUST_USE_PROPERTY_PROPS.has(lower))) {
|
|
176
|
+
return lv ? ` ${lower}=""` : '';
|
|
177
|
+
}
|
|
178
|
+
if (typeof lv === 'string') return ` ${attrName}="${escapeAttr(lv)}"`;
|
|
179
|
+
if (typeof lv === 'number') return ` ${attrName}="${lv}"`;
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// React contract: a `<textarea>` with a `value`/`defaultValue` prop OWNS its
|
|
184
|
+
// content — children would fight the prop (React throws for defaultValue +
|
|
185
|
+
// children and warns for value + children). Compile-time error on both emit
|
|
186
|
+
// paths, like rejectVoidElementContent; a plain `<textarea>text</textarea>`
|
|
187
|
+
// keeps its native content (that IS defaultValue semantics).
|
|
188
|
+
function rejectTextareaValueChildren(tag, node, ctx) {
|
|
189
|
+
if (tag !== 'textarea') return;
|
|
190
|
+
const attrs = node.attributes || node.openingElement?.attributes || [];
|
|
191
|
+
let hasValueProp = false;
|
|
192
|
+
for (const a of attrs) {
|
|
193
|
+
if (a.type !== 'Attribute' && a.type !== 'JSXAttribute') continue;
|
|
194
|
+
const n = jsxAttrRawName(a);
|
|
195
|
+
if (n === 'value' || n === 'defaultValue') {
|
|
196
|
+
hasValueProp = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!hasValueProp) return;
|
|
201
|
+
// Renderable-children check — same lightweight scan as rejectVoidElementContent.
|
|
202
|
+
let offending = false;
|
|
203
|
+
for (const c of node.children || []) {
|
|
204
|
+
if (!c) continue;
|
|
205
|
+
if (c.type === 'JSXText') {
|
|
206
|
+
if (/^\s*$/.test(c.value)) continue;
|
|
207
|
+
} else if (c.type === 'JSXExpressionContainer') {
|
|
208
|
+
if (!c.expression || c.expression.type === 'JSXEmptyExpression') continue;
|
|
209
|
+
} else if (c.type === 'JSXStyleElement') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
offending = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (!offending) return;
|
|
216
|
+
const l = node.loc && node.loc.start;
|
|
217
|
+
const at = l ? ` (${ctx.mapSourceName ? ctx.mapSourceName + ':' : ''}${l.line}:${l.column})` : '';
|
|
218
|
+
throw new Error(
|
|
219
|
+
'`<textarea>` must not have children when it uses `value` or `defaultValue` — ' +
|
|
220
|
+
`the prop owns the content. Move the text into the prop.${at}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
103
224
|
// Compiler-generated code references runtime helpers under a collision-proof
|
|
104
225
|
// `_$` alias — `import { setText as _$setText } from 'octane'` + `_$setText(…)`
|
|
105
226
|
// — because generated statements are interleaved with USER statements inside
|
|
@@ -142,6 +263,87 @@ function addUserImportSpecifiers(ctx, node) {
|
|
|
142
263
|
}
|
|
143
264
|
}
|
|
144
265
|
|
|
266
|
+
// M3 marker elision (docs/comment-marker-elision-plan.md): a component body
|
|
267
|
+
// whose ENTIRE output is one component call spans its own block's range by
|
|
268
|
+
// construction, so the call site can INHERIT the parent block's markers on the
|
|
269
|
+
// client and the server can skip the child's frame pair — collapsing sole-child
|
|
270
|
+
// wrapper chains (incl. `<ctx.Provider>` router/binding stacks) to the
|
|
271
|
+
// outermost pair. The predicate must be computed from the same AST by BOTH
|
|
272
|
+
// compile modes (client stamp ↔ server pair-skip ↔ hydration adopt-nothing
|
|
273
|
+
// agree by construction). Exclusions:
|
|
274
|
+
// - `key=` (key-driven identity forces the slot to own its range),
|
|
275
|
+
// - the octane boundary builtins (Suspense / ErrorBoundary / Activity —
|
|
276
|
+
// their pairs are load-bearing for streaming). Direct imported names are
|
|
277
|
+
// excluded here (collectOctaneBoundaryNames); member/dynamic/aliased tags
|
|
278
|
+
// that RESOLVE to a boundary builtin are declined at RUNTIME by identity —
|
|
279
|
+
// componentSlot and ssrComponent both check the resolved comp against the
|
|
280
|
+
// builtins, so the two sides always agree.
|
|
281
|
+
// `bodyNodes` is the normalized, HeadHoist-filtered root list of a `@{ … }`
|
|
282
|
+
// (JSXCodeBlock) body — callers gate on the body form.
|
|
283
|
+
function inheritSoleCompRoot(bodyNodes, ctx) {
|
|
284
|
+
if (bodyNodes.length !== 1) return false;
|
|
285
|
+
const n = bodyNodes[0];
|
|
286
|
+
if (n.type !== 'Element' || !isComponentTag(n)) return false;
|
|
287
|
+
const id = n.openingElement?.name || n.id;
|
|
288
|
+
if (!id) return false;
|
|
289
|
+
if (
|
|
290
|
+
(id.type === 'Identifier' || id.type === 'JSXIdentifier') &&
|
|
291
|
+
typeof id.name === 'string' &&
|
|
292
|
+
ctx._octaneBoundaryNames &&
|
|
293
|
+
ctx._octaneBoundaryNames.has(id.name)
|
|
294
|
+
) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const attrs = n.attributes || n.openingElement?.attributes || [];
|
|
298
|
+
for (const a of attrs) {
|
|
299
|
+
if (a.type !== 'Attribute' && a.type !== 'JSXAttribute') continue;
|
|
300
|
+
const name = a.name && (a.name.name || a.name);
|
|
301
|
+
if (name === 'key') return false;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Collect the LOCAL names bound to the octane boundary builtins (see
|
|
307
|
+
// inheritSoleCompRoot) from a module's import declarations. Aliased imports
|
|
308
|
+
// (`import { Suspense as S }`) are matched by their local binding.
|
|
309
|
+
function collectOctaneBoundaryNames(astBody) {
|
|
310
|
+
const names = new Set();
|
|
311
|
+
for (const node of astBody) {
|
|
312
|
+
if (node.type !== 'ImportDeclaration' || node.source.value !== 'octane') continue;
|
|
313
|
+
for (const sp of node.specifiers || []) {
|
|
314
|
+
const imported = sp.imported?.name;
|
|
315
|
+
if (
|
|
316
|
+
(imported === 'Suspense' ||
|
|
317
|
+
imported === 'ErrorBoundary' ||
|
|
318
|
+
imported === 'Activity' ||
|
|
319
|
+
imported === 'ViewTransition' ||
|
|
320
|
+
imported === 'unstable_ViewTransition') &&
|
|
321
|
+
sp.local?.name
|
|
322
|
+
) {
|
|
323
|
+
names.add(sp.local.name);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return names;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Does this module import ViewTransition from 'octane' (any local alias)?
|
|
331
|
+
// Drives the client prelude's `_$vtSeen()` module-load hint: the runtime's
|
|
332
|
+
// view-transition machinery is gated on a sticky VT_SEEN flag, and without
|
|
333
|
+
// the hint the very FIRST transition flush that mounts a boundary would only
|
|
334
|
+
// learn "this app uses ViewTransition" mid-drain — after the chance to
|
|
335
|
+
// snapshot the old state has passed (docs/view-transitions-plan.md).
|
|
336
|
+
function moduleImportsViewTransition(astBody) {
|
|
337
|
+
for (const node of astBody) {
|
|
338
|
+
if (node.type !== 'ImportDeclaration' || node.source.value !== 'octane') continue;
|
|
339
|
+
for (const sp of node.specifiers || []) {
|
|
340
|
+
const imported = sp.imported?.name;
|
|
341
|
+
if (imported === 'ViewTransition' || imported === 'unstable_ViewTransition') return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
145
347
|
export const HOOK_NAMES = new Set([
|
|
146
348
|
'useState',
|
|
147
349
|
'useReducer',
|
|
@@ -166,16 +368,26 @@ export const HOOK_NAMES = new Set([
|
|
|
166
368
|
// Namespace inheritance — mirrors HTML5 foreign-content rules. The element
|
|
167
369
|
// itself and its children may have *different* namespaces: <foreignObject>
|
|
168
370
|
// inside SVG is still an SVG element, but its children switch back to HTML.
|
|
371
|
+
// SVG_ONLY_TAGS (imported from ../dom-tables.js — the runtime's de-opt
|
|
372
|
+
// reconciler uses the same table) drives the inference: a tag that exists ONLY
|
|
373
|
+
// in the SVG namespace implies SVG in a namespace-ambiguous position — a
|
|
374
|
+
// component's ROOT template, a fragment root — without a lexical `<svg>`
|
|
375
|
+
// ancestor. A component whose root is `<g>`/`<path>` must not compile to an
|
|
376
|
+
// HTML template: the HTMLUnknownElement it would produce inside an `<svg>`
|
|
377
|
+
// paints nothing.
|
|
378
|
+
|
|
169
379
|
function nsForSelf(tag, parentNs) {
|
|
170
380
|
if (tag === 'svg') return 'svg';
|
|
171
381
|
if (tag === 'math') return 'mathml';
|
|
172
|
-
|
|
382
|
+
if (parentNs === 'html' && SVG_ONLY_TAGS.has(tag)) return 'svg';
|
|
383
|
+
return parentNs; // includes <foreignObject> under an svg parent — itself SVG-ns
|
|
173
384
|
}
|
|
174
385
|
|
|
175
386
|
function nsForChildren(tag, parentNs) {
|
|
176
387
|
if (tag === 'foreignObject') return 'html';
|
|
177
388
|
if (tag === 'svg') return 'svg';
|
|
178
389
|
if (tag === 'math') return 'mathml';
|
|
390
|
+
if (parentNs === 'html' && SVG_ONLY_TAGS.has(tag)) return 'svg';
|
|
179
391
|
return parentNs;
|
|
180
392
|
}
|
|
181
393
|
|
|
@@ -190,13 +402,14 @@ function elementTagName(node) {
|
|
|
190
402
|
|
|
191
403
|
function isNonHtmlRootTag(node) {
|
|
192
404
|
const t = elementTagName(node);
|
|
193
|
-
return t === 'svg' || t === 'math';
|
|
405
|
+
return t === 'svg' || t === 'math' || (t !== null && SVG_ONLY_TAGS.has(t));
|
|
194
406
|
}
|
|
195
407
|
|
|
196
408
|
function nsForRootTag(node, parentNs) {
|
|
197
409
|
const t = elementTagName(node);
|
|
198
410
|
if (t === 'svg') return 'svg';
|
|
199
411
|
if (t === 'math') return 'mathml';
|
|
412
|
+
if (t !== null && SVG_ONLY_TAGS.has(t)) return 'svg';
|
|
200
413
|
return parentNs;
|
|
201
414
|
}
|
|
202
415
|
|
|
@@ -218,11 +431,23 @@ function jsxAttrRawName(attr) {
|
|
|
218
431
|
return n.name || n;
|
|
219
432
|
}
|
|
220
433
|
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
434
|
+
// ATTRIBUTE_ALIASES (imported from ../dom-tables.js): React 19's alias table,
|
|
435
|
+
// camelCase JSX prop → the attribute the browser actually parses
|
|
436
|
+
// (`strokeWidth` → `stroke-width`, `htmlFor` → `for`). The compiler bakes
|
|
437
|
+
// these into static template/SSR markup; the client/server runtimes apply the
|
|
438
|
+
// same table to dynamic bindings, spreads, and de-opt props.
|
|
439
|
+
|
|
440
|
+
// `className` plus the ATTRIBUTE_ALIASES table above: emit the native
|
|
441
|
+
// attribute names so the browser actually applies them (and dynamic bindings
|
|
442
|
+
// know which setter to pick). Custom elements keep every name VERBATIM (raw
|
|
443
|
+
// props, no alias tables) — the same gate the runtimes' setAttribute/ssrAttr
|
|
444
|
+
// apply to dynamic values.
|
|
445
|
+
function normalizeJsxAttrName(raw, tag) {
|
|
446
|
+
// `className` → `class` applies EVERYWHERE, custom elements included (React
|
|
447
|
+
// special-cases it in setPropOnCustomElement); only the alias table is raw.
|
|
448
|
+
if (raw === 'className') return 'class';
|
|
449
|
+
if (tag !== undefined && tag.includes('-')) return raw;
|
|
450
|
+
return ATTRIBUTE_ALIASES.get(raw) || raw;
|
|
226
451
|
}
|
|
227
452
|
|
|
228
453
|
// React-shape `onXxx` event-handler attribute: `on` + an uppercase letter, so
|
|
@@ -257,105 +482,25 @@ function objectExprIsStaticLiteral(obj) {
|
|
|
257
482
|
return true;
|
|
258
483
|
}
|
|
259
484
|
|
|
260
|
-
//
|
|
261
|
-
// `
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (c >= 65 && c <= 90) {
|
|
270
|
-
out += '-' + String.fromCharCode(c + 32);
|
|
271
|
-
changed = true;
|
|
272
|
-
} else out += name[i];
|
|
273
|
-
}
|
|
274
|
-
if (!changed) return name;
|
|
275
|
-
if (out.charCodeAt(0) === 109 && out.charCodeAt(1) === 115 && out.charCodeAt(2) === 45)
|
|
276
|
-
out = '-' + out;
|
|
277
|
-
return out;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// React's unitless-property set — keep in sync with `constants.ts`
|
|
281
|
-
// (UNITLESS_STYLE_PROPS). Canonical form: lowercased, dashes stripped.
|
|
282
|
-
const UNITLESS_STYLE_PROPS = new Set();
|
|
283
|
-
for (const base of [
|
|
284
|
-
'animationIterationCount',
|
|
285
|
-
'aspectRatio',
|
|
286
|
-
'borderImageOutset',
|
|
287
|
-
'borderImageSlice',
|
|
288
|
-
'borderImageWidth',
|
|
289
|
-
'boxFlex',
|
|
290
|
-
'boxFlexGroup',
|
|
291
|
-
'boxOrdinalGroup',
|
|
292
|
-
'columnCount',
|
|
293
|
-
'columns',
|
|
294
|
-
'flex',
|
|
295
|
-
'flexGrow',
|
|
296
|
-
'flexPositive',
|
|
297
|
-
'flexShrink',
|
|
298
|
-
'flexNegative',
|
|
299
|
-
'flexOrder',
|
|
300
|
-
'gridArea',
|
|
301
|
-
'gridRow',
|
|
302
|
-
'gridRowEnd',
|
|
303
|
-
'gridRowSpan',
|
|
304
|
-
'gridRowStart',
|
|
305
|
-
'gridColumn',
|
|
306
|
-
'gridColumnEnd',
|
|
307
|
-
'gridColumnSpan',
|
|
308
|
-
'gridColumnStart',
|
|
309
|
-
'fontWeight',
|
|
310
|
-
'lineClamp',
|
|
311
|
-
'lineHeight',
|
|
312
|
-
'opacity',
|
|
313
|
-
'order',
|
|
314
|
-
'orphans',
|
|
315
|
-
'tabSize',
|
|
316
|
-
'widows',
|
|
317
|
-
'zIndex',
|
|
318
|
-
'zoom',
|
|
319
|
-
'fillOpacity',
|
|
320
|
-
'floodOpacity',
|
|
321
|
-
'stopOpacity',
|
|
322
|
-
'strokeDasharray',
|
|
323
|
-
'strokeDashoffset',
|
|
324
|
-
'strokeMiterlimit',
|
|
325
|
-
'strokeOpacity',
|
|
326
|
-
'strokeWidth',
|
|
327
|
-
]) {
|
|
328
|
-
const c = base.toLowerCase();
|
|
329
|
-
UNITLESS_STYLE_PROPS.add(c);
|
|
330
|
-
UNITLESS_STYLE_PROPS.add('webkit' + c);
|
|
331
|
-
UNITLESS_STYLE_PROPS.add('ms' + c);
|
|
332
|
-
UNITLESS_STYLE_PROPS.add('moz' + c);
|
|
333
|
-
UNITLESS_STYLE_PROPS.add('o' + c);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// React parity: a bare number gets `px` unless it's 0, a custom prop, or unitless.
|
|
337
|
-
function cssStyleValueStatic(name, value) {
|
|
338
|
-
if (
|
|
339
|
-
typeof value === 'number' &&
|
|
340
|
-
value !== 0 &&
|
|
341
|
-
name.charCodeAt(0) !== 45 &&
|
|
342
|
-
!UNITLESS_STYLE_PROPS.has(name.replaceAll('-', '').toLowerCase())
|
|
343
|
-
) {
|
|
344
|
-
return value + 'px';
|
|
345
|
-
}
|
|
346
|
-
return '' + value;
|
|
347
|
-
}
|
|
348
|
-
|
|
485
|
+
// Serialize a fully-literal style object into a `style="…"` string at compile
|
|
486
|
+
// time. `hyphenateStyleName` + `cssStyleValue` come from ../dom-tables.js —
|
|
487
|
+
// the SAME key normalization and px/unitless/trim coercion the runtimes apply
|
|
488
|
+
// to dynamic style objects — so a STATIC baked object style produces the same
|
|
489
|
+
// CSS a dynamic one would. The string is CSSOM-serialization-shaped
|
|
490
|
+
// (`prop: value; prop2: value2;` — declarations TERMINATED, not separated):
|
|
491
|
+
// that's what the same element's style attribute reads back as once anything
|
|
492
|
+
// touches el.style, and what React's CSSOM writes serialize to — so baked and
|
|
493
|
+
// dynamic styles are byte-identical in innerHTML.
|
|
349
494
|
function staticObjectToCssString(obj) {
|
|
350
495
|
const parts = [];
|
|
351
496
|
for (const p of obj.properties || []) {
|
|
352
497
|
const name = p.key.type === 'Identifier' ? p.key.name : p.key.value;
|
|
353
498
|
const value = p.value.value;
|
|
354
499
|
if (value == null || value === false || value === '') continue;
|
|
355
|
-
const cssValue = value === true ? '' :
|
|
356
|
-
parts.push(`${hyphenateStyleName(name)}: ${cssValue}
|
|
500
|
+
const cssValue = value === true ? '' : cssStyleValue(name, value);
|
|
501
|
+
parts.push(`${hyphenateStyleName(name)}: ${cssValue};`);
|
|
357
502
|
}
|
|
358
|
-
return parts.join('
|
|
503
|
+
return parts.join(' ');
|
|
359
504
|
}
|
|
360
505
|
|
|
361
506
|
// ===========================================================================
|
|
@@ -594,6 +739,48 @@ function collectFreeIdentifiers(root, initiallyBound) {
|
|
|
594
739
|
return;
|
|
595
740
|
}
|
|
596
741
|
|
|
742
|
+
// JSX tag position: a COMPONENT tag is a real identifier reference — a
|
|
743
|
+
// per-body local (`const C = props.comp`) used as `<C/>` must show up as
|
|
744
|
+
// free, or a hoisted helper (Phase 2) would silently drop it from its
|
|
745
|
+
// env tuple. Host tag names ('div') and attribute NAMES are static —
|
|
746
|
+
// only component-shaped identifier tags (isCompatTag rule), member-tag
|
|
747
|
+
// ROOT objects, and dynamic `<{expr}/>` expressions count.
|
|
748
|
+
if (t === 'Element' || t === 'JSXElement') {
|
|
749
|
+
const tag = n.openingElement?.name || n.id;
|
|
750
|
+
if (tag) {
|
|
751
|
+
if (
|
|
752
|
+
(tag.type === 'Identifier' || tag.type === 'JSXIdentifier') &&
|
|
753
|
+
typeof tag.name === 'string'
|
|
754
|
+
) {
|
|
755
|
+
if (!/^[a-z]/.test(tag.name) && !tag.name.includes('-') && !scope.has(tag.name)) {
|
|
756
|
+
free.add(tag.name);
|
|
757
|
+
}
|
|
758
|
+
} else if (tag.type === 'MemberExpression' || tag.type === 'JSXMemberExpression') {
|
|
759
|
+
let o = tag;
|
|
760
|
+
while (o && (o.type === 'MemberExpression' || o.type === 'JSXMemberExpression')) {
|
|
761
|
+
o = o.object;
|
|
762
|
+
}
|
|
763
|
+
if (
|
|
764
|
+
o &&
|
|
765
|
+
(o.type === 'Identifier' || o.type === 'JSXIdentifier') &&
|
|
766
|
+
typeof o.name === 'string' &&
|
|
767
|
+
!scope.has(o.name)
|
|
768
|
+
) {
|
|
769
|
+
free.add(o.name);
|
|
770
|
+
}
|
|
771
|
+
} else if (tag.type === 'JSXExpressionContainer') {
|
|
772
|
+
walk(tag.expression, scope);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const attrs = n.attributes || n.openingElement?.attributes || [];
|
|
776
|
+
for (const a of attrs) {
|
|
777
|
+
if (a.type === 'Attribute' || a.type === 'JSXAttribute') walk(a.value, scope);
|
|
778
|
+
else walk(a.argument ?? a, scope); // spread attribute
|
|
779
|
+
}
|
|
780
|
+
walk(n.children, scope);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
597
784
|
// Member access — `obj.prop`: prop is a static name, not a binding ref.
|
|
598
785
|
if (t === 'MemberExpression' && !n.computed) {
|
|
599
786
|
walk(n.object, scope);
|
|
@@ -737,6 +924,54 @@ function containsComponentCallOrControlFlow(stmts) {
|
|
|
737
924
|
return found;
|
|
738
925
|
}
|
|
739
926
|
|
|
927
|
+
/**
|
|
928
|
+
* True when the keyed-item body executes any call DURING render:
|
|
929
|
+
* CallExpression / NewExpression / TaggedTemplateExpression in render-value
|
|
930
|
+
* position (holes, attribute values, locals). Such a call can read mutable
|
|
931
|
+
* state that neither the item reference nor the deps tuple changes with —
|
|
932
|
+
* e.g. `header.column.getIsSorted()` on a memoized table-core header — so the
|
|
933
|
+
* PURE/DEP-PURE survivor short-circuit (see the body analysis in makeForCall)
|
|
934
|
+
* would freeze its output where React re-runs the body unconditionally.
|
|
935
|
+
*
|
|
936
|
+
* Calls nested inside FUNCTION VALUES (event-handler arrows, function
|
|
937
|
+
* expressions) are deferred to invoke time and close over the same ref-stable
|
|
938
|
+
* item/deps a skipped survivor would have, so they can't go stale at render
|
|
939
|
+
* time — the walk does not descend into function bodies or parameters.
|
|
940
|
+
*/
|
|
941
|
+
function containsRenderCall(stmts) {
|
|
942
|
+
let found = false;
|
|
943
|
+
const seen = new WeakSet();
|
|
944
|
+
function walk(n) {
|
|
945
|
+
if (found || !n) return;
|
|
946
|
+
if (Array.isArray(n)) {
|
|
947
|
+
for (const x of n) walk(x);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (typeof n !== 'object') return;
|
|
951
|
+
const t = n.type;
|
|
952
|
+
if (!t) return;
|
|
953
|
+
if (seen.has(n)) return;
|
|
954
|
+
seen.add(n);
|
|
955
|
+
if (
|
|
956
|
+
t === 'ArrowFunctionExpression' ||
|
|
957
|
+
t === 'FunctionExpression' ||
|
|
958
|
+
t === 'FunctionDeclaration'
|
|
959
|
+
) {
|
|
960
|
+
return; // deferred — runs at event/invoke time, not during render
|
|
961
|
+
}
|
|
962
|
+
if (t === 'CallExpression' || t === 'NewExpression' || t === 'TaggedTemplateExpression') {
|
|
963
|
+
found = true;
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
for (const key in n) {
|
|
967
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
968
|
+
walk(n[key]);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (const s of stmts) walk(s);
|
|
972
|
+
return found;
|
|
973
|
+
}
|
|
974
|
+
|
|
740
975
|
/**
|
|
741
976
|
* `() => fn(a, b, …)` — a zero-param arrow whose body is a single
|
|
742
977
|
* function call. Returns `{ callee, args }` if so, else null. Used to compile
|
|
@@ -1201,11 +1436,20 @@ export function compile(source, filename, options) {
|
|
|
1201
1436
|
// name), used by hydration-mismatch warnings and reusable by a future Chrome-DevTools
|
|
1202
1437
|
// element→source layer. Strictly dev-gated so PROD output is byte-identical (zero cost).
|
|
1203
1438
|
const devEnabled = !!(options && options.dev);
|
|
1439
|
+
// parallelUse: the parallel-`use()` transform pipeline (auto-memoized
|
|
1440
|
+
// creations → hoisted parallel starts → batched unwrap → __warm fetch
|
|
1441
|
+
// plans; docs/suspense-parallel-use-plan.md). ON by default — pass
|
|
1442
|
+
// `parallelUse: false` to opt out (React-timing waterfall semantics).
|
|
1443
|
+
// Output is byte-identical with the flag off, and the pipeline is a no-op
|
|
1444
|
+
// for use()-free modules either way.
|
|
1445
|
+
const parallelUseEnabled = !(options && options.parallelUse === false);
|
|
1204
1446
|
|
|
1205
1447
|
const ctx = {
|
|
1206
1448
|
filename,
|
|
1207
1449
|
mode,
|
|
1208
1450
|
dev: devEnabled,
|
|
1451
|
+
parallelUse: parallelUseEnabled,
|
|
1452
|
+
hmr: hmrEnabled, // gates Symbol.for vs Symbol() hook slots (allocHookSymbol)
|
|
1209
1453
|
runtimeNeeded: new Set(), // helpers referenced by GENERATED code — imported as `name as _$name`
|
|
1210
1454
|
userRuntimeNames: new Set(), // specifiers USER code references — imported verbatim
|
|
1211
1455
|
hoistedTemplates: [], // { name, html }
|
|
@@ -1216,6 +1460,8 @@ export function compile(source, filename, options) {
|
|
|
1216
1460
|
currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
|
|
1217
1461
|
knownStringLocals: null, // Set<string> of provably-string locals (text-hole inference)
|
|
1218
1462
|
nextHookSymId: 0,
|
|
1463
|
+
nextPuId: 0, // parallel-use `__pu$N` hoisted-creation temps
|
|
1464
|
+
_pendingWarm: null, // `X.__warm = …` source, set by compileFunctionBody, drained by compileComponent
|
|
1219
1465
|
nextFragId: 0,
|
|
1220
1466
|
nextTemplateId: 0,
|
|
1221
1467
|
nextHelperId: 0,
|
|
@@ -1231,6 +1477,23 @@ export function compile(source, filename, options) {
|
|
|
1231
1477
|
// the top-level (autoCallback) pass and drained per component below.
|
|
1232
1478
|
_setupMaps: null,
|
|
1233
1479
|
};
|
|
1480
|
+
// Imported local bindings (any source). Used by the M1 cross-module
|
|
1481
|
+
// singleRoot sentinel: only an IMPORTED identifier is a stable component
|
|
1482
|
+
// identity for the lifetime of a slot — a local `const Comp = cond ? A : B`
|
|
1483
|
+
// re-resolves per render, and a markerless slot regime must never be chosen
|
|
1484
|
+
// off an identity that can change (see makeCompCall).
|
|
1485
|
+
ctx.importedNames = new Set();
|
|
1486
|
+
for (const node of ast.body) {
|
|
1487
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
1488
|
+
for (const sp of node.specifiers || []) {
|
|
1489
|
+
if (sp.local && sp.local.name) ctx.importedNames.add(sp.local.name);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// M3 inherit-range exclusion set (see inheritSoleCompRoot).
|
|
1493
|
+
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
1494
|
+
// Client prelude `_$vtSeen()` module-load hint (view-transitions plan).
|
|
1495
|
+
ctx._usesViewTransition = moduleImportsViewTransition(ast.body);
|
|
1496
|
+
|
|
1234
1497
|
// List of exported components needing HMR wrapping. Each entry: { name,
|
|
1235
1498
|
// exportKind: 'default' | 'named' }. We emit the `Comp = hmr(Comp)` lines
|
|
1236
1499
|
// and the `import.meta.hot.accept` block after walking the body, so the
|
|
@@ -1333,6 +1596,19 @@ export function compile(source, filename, options) {
|
|
|
1333
1596
|
})(root);
|
|
1334
1597
|
if (reject) eligible = false;
|
|
1335
1598
|
}
|
|
1599
|
+
// M3 inherit-range: a body whose sole output is one component call BORROWS
|
|
1600
|
+
// its own block's marker range at that site (see inheritSoleCompRoot), so
|
|
1601
|
+
// callers must give it a real Block with a coherent range — a
|
|
1602
|
+
// componentSlotLite invocation (LiteBlockImpl: no startMarker, endMarker =
|
|
1603
|
+
// the call-site anchor) has nothing to borrow, and under HYDRATION the
|
|
1604
|
+
// declined borrow would probe for a child pair the server never emitted.
|
|
1605
|
+
// Reject lite for such callees.
|
|
1606
|
+
if (eligible && compNode.body && compNode.body.type === 'JSXCodeBlock') {
|
|
1607
|
+
const bodyNodes = normalizeChildren(
|
|
1608
|
+
compNode.body.render ? [compNode.body.render] : [],
|
|
1609
|
+
).filter((n) => n.type !== 'HeadHoist');
|
|
1610
|
+
if (inheritSoleCompRoot(bodyNodes, ctx)) eligible = false;
|
|
1611
|
+
}
|
|
1336
1612
|
info.eligible = eligible;
|
|
1337
1613
|
// Single-ELEMENT-root output: the component's body renders exactly one plain
|
|
1338
1614
|
// DOM element (not a component tag, fragment, or control-flow). Such a
|
|
@@ -1423,18 +1699,27 @@ export function compile(source, filename, options) {
|
|
|
1423
1699
|
if (hmrEnabled) hmrComponents.push({ name: c.id.name, exportKind: 'named' });
|
|
1424
1700
|
} else if (isReturnJsxFunction(node)) {
|
|
1425
1701
|
// `function Foo() { …hooks…; return <jsx>; }` — a plain return-JSX function.
|
|
1702
|
+
const base = bodyLine;
|
|
1426
1703
|
ctx._setupMaps = null;
|
|
1427
1704
|
const chunk = compileReturnJsxFunction(node, ctx, {}) + '\n\n';
|
|
1705
|
+
pushDeclAnchor(node, base);
|
|
1706
|
+
drainSetupMaps(base);
|
|
1428
1707
|
body += chunk;
|
|
1429
1708
|
bodyLine += countNewlines(chunk);
|
|
1430
1709
|
} else if (node.type === 'ExportNamedDeclaration' && isReturnJsxFunction(node.declaration)) {
|
|
1710
|
+
const base = bodyLine;
|
|
1431
1711
|
ctx._setupMaps = null;
|
|
1432
1712
|
const chunk = compileReturnJsxFunction(node.declaration, ctx, { export: true }) + '\n\n';
|
|
1713
|
+
pushDeclAnchor(node, base);
|
|
1714
|
+
drainSetupMaps(base);
|
|
1433
1715
|
body += chunk;
|
|
1434
1716
|
bodyLine += countNewlines(chunk);
|
|
1435
1717
|
} else if (node.type === 'ExportDefaultDeclaration' && isReturnJsxFunction(node.declaration)) {
|
|
1718
|
+
const base = bodyLine;
|
|
1436
1719
|
ctx._setupMaps = null;
|
|
1437
1720
|
const chunk = compileReturnJsxFunction(node.declaration, ctx, { default: true }) + '\n\n';
|
|
1721
|
+
pushDeclAnchor(node, base);
|
|
1722
|
+
drainSetupMaps(base);
|
|
1438
1723
|
body += chunk;
|
|
1439
1724
|
bodyLine += countNewlines(chunk);
|
|
1440
1725
|
} else if (node.type === 'ImportDeclaration' && node.source.value === 'octane') {
|
|
@@ -1536,12 +1821,37 @@ export function compile(source, filename, options) {
|
|
|
1536
1821
|
'}\n';
|
|
1537
1822
|
}
|
|
1538
1823
|
|
|
1824
|
+
// Cross-module singleRoot stamps (docs/comment-marker-elision-plan.md M1):
|
|
1825
|
+
// a component whose body provably renders ONE plain element carries the
|
|
1826
|
+
// marker on its BINDING, so a `componentSlot(…, 2)` call site in ANOTHER
|
|
1827
|
+
// module can take the markerless singleRoot path at mount. Emitted at the
|
|
1828
|
+
// module tail so it lands on the final binding (incl. the hmr() wrapper —
|
|
1829
|
+
// the stamp goes on what importers see). Also feeds the runtime's existing
|
|
1830
|
+
// value-position `$$singleRoot` descriptor check.
|
|
1831
|
+
let stampBlock = '';
|
|
1832
|
+
if (ctx.componentInfo) {
|
|
1833
|
+
const stamps = [];
|
|
1834
|
+
for (const [name, info] of ctx.componentInfo) {
|
|
1835
|
+
if (info.singleRoot === true) stamps.push(`${name}.$$singleRoot = true;`);
|
|
1836
|
+
}
|
|
1837
|
+
if (stamps.length > 0) stampBlock = stamps.join('\n') + '\n';
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Module-load ViewTransition hint (see moduleImportsViewTransition) —
|
|
1841
|
+
// registered before the import list is built so `__vtSeen` gets aliased in.
|
|
1842
|
+
let vtHintBlock = '';
|
|
1843
|
+
if (ctx._usesViewTransition) {
|
|
1844
|
+
ctx.runtimeNeeded.add('__vtSeen');
|
|
1845
|
+
vtHintBlock = rtAlias('__vtSeen') + '();\n';
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1539
1848
|
// Built after HMR wiring so the import list includes `hmr`/`HMR` when needed.
|
|
1540
1849
|
const finalRuntimeImport = buildRuntimeImport(ctx, 'octane');
|
|
1541
1850
|
|
|
1542
1851
|
// Everything before `body` in the output — shifts every body segment's
|
|
1543
1852
|
// generated line down by the prelude's line count.
|
|
1544
|
-
const prelude =
|
|
1853
|
+
const prelude =
|
|
1854
|
+
finalRuntimeImport + vtHintBlock + delegateCall + styleBlock + templatesBlock + helpersBlock;
|
|
1545
1855
|
const preludeLines = countNewlines(prelude);
|
|
1546
1856
|
const segments = bodySegments.map((s) => ({
|
|
1547
1857
|
genLine: s.genLine + preludeLines,
|
|
@@ -1551,7 +1861,7 @@ export function compile(source, filename, options) {
|
|
|
1551
1861
|
}));
|
|
1552
1862
|
|
|
1553
1863
|
return {
|
|
1554
|
-
code: prelude + body + hmrBlock,
|
|
1864
|
+
code: prelude + body + stampBlock + hmrBlock,
|
|
1555
1865
|
map: buildSourceMap(source, ctx.mapSourceName, segments),
|
|
1556
1866
|
};
|
|
1557
1867
|
}
|
|
@@ -1590,6 +1900,16 @@ function compileServer(source, filename, options) {
|
|
|
1590
1900
|
const ctx = {
|
|
1591
1901
|
filename,
|
|
1592
1902
|
mode: 'server',
|
|
1903
|
+
hmr: false, // SSR never hot-swaps in place — hook slots are plain Symbol()s
|
|
1904
|
+
// SSR MIRROR of the parallel-`use()` pipeline (docs/suspense-parallel-use-
|
|
1905
|
+
// plan.md Phase 5): the same memoize (Pass A) + hoist/batch (Pass B)
|
|
1906
|
+
// transforms run on server bodies, emitting `_$puMemo`/`_$puBatch` — the
|
|
1907
|
+
// server-runtime twins with cross-pass creation identity — so independent
|
|
1908
|
+
// fetches REGISTER before the first suspend and a body stratum costs ONE
|
|
1909
|
+
// network round instead of one per use(). Same opt-out flag as the client.
|
|
1910
|
+
parallelUse: !(options && options.parallelUse === false),
|
|
1911
|
+
nextPuId: 0, // parallel-use `__pu$N` hoisted-creation temps
|
|
1912
|
+
_pendingWarm: null, // `X.__warm = …` source, set by ssrCompileBody, drained by compileServerComponent
|
|
1593
1913
|
runtimeNeeded: new Set(), // helpers referenced by GENERATED code — imported as `name as _$name`
|
|
1594
1914
|
userRuntimeNames: new Set(), // specifiers USER code references — imported verbatim
|
|
1595
1915
|
hoistedHelpers: [],
|
|
@@ -1604,6 +1924,9 @@ function compileServer(source, filename, options) {
|
|
|
1604
1924
|
mapSourceName: (filename || 'module.tsrx').split(/[\\/]/).pop(),
|
|
1605
1925
|
_setupMaps: null,
|
|
1606
1926
|
};
|
|
1927
|
+
// M3 inherit-range exclusion set — must match the client compile's
|
|
1928
|
+
// (see inheritSoleCompRoot; both modes read the same import declarations).
|
|
1929
|
+
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
1607
1930
|
|
|
1608
1931
|
let body = '';
|
|
1609
1932
|
for (const node of ast.body) {
|
|
@@ -1689,9 +2012,16 @@ function compileServerComponent(node, ctx) {
|
|
|
1689
2012
|
ctx.knownStringLocals = prevKnownStr;
|
|
1690
2013
|
}
|
|
1691
2014
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2015
|
+
// SSR parallel-use mirror: attach the compiled fetch plan so a PARENT's warm
|
|
2016
|
+
// walk (`_$warmChild(Comp, props)` from its first suspending batch) can start
|
|
2017
|
+
// this component's independent creations before its body ever runs.
|
|
2018
|
+
const warmSrc = ctx._pendingWarm;
|
|
2019
|
+
ctx._pendingWarm = null;
|
|
2020
|
+
const warmTail = warmSrc ? `\n${name}.__warm = ${warmSrc};` : '';
|
|
2021
|
+
|
|
2022
|
+
if (isDefault) return `const ${name} = ${fn};${warmTail}\nexport default ${name};`;
|
|
2023
|
+
if (isExported) return `export const ${name} = ${fn};${warmTail}`;
|
|
2024
|
+
return `const ${name} = ${fn};${warmTail}`;
|
|
1695
2025
|
}
|
|
1696
2026
|
|
|
1697
2027
|
function ssrCompileBody(node, ctx, name, cssHash, cssEntries, parentNs = 'html') {
|
|
@@ -1716,7 +2046,24 @@ function ssrCompileBody(node, ctx, name, cssHash, cssEntries, parentNs = 'html')
|
|
|
1716
2046
|
// return-JSX form: the returned host element (+ its directive children) is
|
|
1717
2047
|
// the render output — route it to jsxNodes so it flows through ssrEmitNode
|
|
1718
2048
|
// (byte-identical SSR to the `@{}` form), not printed as `return <jsx>`.
|
|
1719
|
-
|
|
2049
|
+
// EXCEPT a returned FRAGMENT: the client VALUE-lowers `return <>…</>` to an
|
|
2050
|
+
// array of createElement descriptors (rewriteJsxValues), mounted by the
|
|
2051
|
+
// return-slot childSlot — whose hydration adopts ONE `<!--[-->…<!--]-->`
|
|
2052
|
+
// range for the slot plus one range PER ITEM (including text items). The
|
|
2053
|
+
// template walk would instead concatenate children with markerless text
|
|
2054
|
+
// separators and no slot range, desyncing the hydration cursor. Route the
|
|
2055
|
+
// whole fragment through the same value hole (`ssrChild(loweredArray)`) so
|
|
2056
|
+
// the runtime's ssrChild array branch emits the exact per-item shape the
|
|
2057
|
+
// client adopts.
|
|
2058
|
+
if (child.argument.type === 'JSXFragment' || child.argument.type === 'Fragment') {
|
|
2059
|
+
jsxNodes.push({
|
|
2060
|
+
type: 'TSRXExpression',
|
|
2061
|
+
expression: child.argument,
|
|
2062
|
+
loc: child.argument.loc,
|
|
2063
|
+
});
|
|
2064
|
+
} else {
|
|
2065
|
+
jsxNodes.push(child.argument);
|
|
2066
|
+
}
|
|
1720
2067
|
} else if (isJsxNode(child)) {
|
|
1721
2068
|
if (child.type === 'Element' && elementTagName(child) === 'style') continue;
|
|
1722
2069
|
jsxNodes.push(child);
|
|
@@ -1725,7 +2072,34 @@ function ssrCompileBody(node, ctx, name, cssHash, cssEntries, parentNs = 'html')
|
|
|
1725
2072
|
}
|
|
1726
2073
|
|
|
1727
2074
|
const inlinedSubs = [];
|
|
1728
|
-
|
|
2075
|
+
// SSR parallel-use mirror: memoize + hoist/batch BEFORE rewriteHookCalls —
|
|
2076
|
+
// the rewritten `use(__pu$N)` unwraps then get their server site keys from
|
|
2077
|
+
// rewriteHookCalls exactly like hand-written use() calls, and the
|
|
2078
|
+
// `_$puMemo`/`_$puBatch` helper calls it emits are compiler-aliased names it
|
|
2079
|
+
// leaves alone. Mirrors the client pipeline shape exactly: TOP bodies run
|
|
2080
|
+
// Pass A on setup statements + the WalkJsx transform over the render tree
|
|
2081
|
+
// (directive-arm statements memoize HERE; the transformed nodes flow into
|
|
2082
|
+
// ssrEmitNodes below, so ssrCompileSub receives arms PRE-memoized and runs
|
|
2083
|
+
// Pass B only) + the warm artifacts (`Comp.__warm` fetch plan + the first
|
|
2084
|
+
// batch's warm thunk — see runtime.server.ts warmMemo/warmChild); synthetic
|
|
2085
|
+
// subs (statement arrays) run Pass B only. Loops/functions are excluded by
|
|
2086
|
+
// the passes themselves (same rules as the client).
|
|
2087
|
+
let workingStatements = statements;
|
|
2088
|
+
if (ctx.parallelUse) {
|
|
2089
|
+
let warmThunk = null;
|
|
2090
|
+
const isTopBody = !Array.isArray(node.body);
|
|
2091
|
+
if (isTopBody) {
|
|
2092
|
+
const creations = [];
|
|
2093
|
+
const warmChildren = [];
|
|
2094
|
+
workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
|
|
2095
|
+
jsxNodes = parallelUseWalkJsx(jsxNodes, ctx, name, creations, warmChildren, [], new Set());
|
|
2096
|
+
const warm = buildWarmArtifacts(node, ctx, name, creations, warmChildren);
|
|
2097
|
+
warmThunk = warm.thunk;
|
|
2098
|
+
ctx._pendingWarm = warm.warmSrc;
|
|
2099
|
+
}
|
|
2100
|
+
workingStatements = rewriteParallelUse(workingStatements, ctx, name, warmThunk);
|
|
2101
|
+
}
|
|
2102
|
+
const rewritten = workingStatements
|
|
1729
2103
|
.map((s) => rewriteHookCalls(s, ctx, name))
|
|
1730
2104
|
.map((s) => rewriteJsxValues(s, ctx));
|
|
1731
2105
|
const setupCode = rewritten.map((s) => ' ' + printNode(s).replace(/\n/g, '\n ')).join('\n');
|
|
@@ -1743,9 +2117,33 @@ function ssrCompileBody(node, ctx, name, cssHash, cssEntries, parentNs = 'html')
|
|
|
1743
2117
|
// block). The server must match per form — flag value position so ssrEmitComponent
|
|
1744
2118
|
// emits descriptor children for `.tsx`, keeping the server/client block counts
|
|
1745
2119
|
// identical (a mismatch desyncs the hydration cursor). Restored after this body.
|
|
2120
|
+
//
|
|
2121
|
+
// SYNTHETIC subs (ssrCompileSub passes the statement ARRAY: @if/@for/@switch/@try
|
|
2122
|
+
// branches and `__schildren` component children) are always TEMPLATE position —
|
|
2123
|
+
// the client compiles those branches through the template walk (componentSlot +
|
|
2124
|
+
// markChildrenBlock render-fns) even when the enclosing body is a `return <jsx>`
|
|
2125
|
+
// value body. Resetting to value here made ssrEmitComponent take the descriptor
|
|
2126
|
+
// path inside every sub, which both desynced the block count from the client AND
|
|
2127
|
+
// silently DROPPED directive-block children of nested components (lowerJsxChild
|
|
2128
|
+
// cannot lower an @if to a descriptor) — a `<C>@if (…) { … }</C>` nested one sub
|
|
2129
|
+
// deep rendered `<C>` childless.
|
|
1746
2130
|
const prevValuePos = ctx._tsxValuePos;
|
|
1747
|
-
ctx._tsxValuePos =
|
|
2131
|
+
ctx._tsxValuePos = Array.isArray(node.body)
|
|
2132
|
+
? false
|
|
2133
|
+
: !(node.body && node.body.type === 'JSXCodeBlock');
|
|
2134
|
+
// M3 inherit-range (mirror of the client's planJsx stamp — the SAME
|
|
2135
|
+
// inheritSoleCompRoot predicate over the same normalized roots, so the
|
|
2136
|
+
// client slot borrows exactly where the server skips the frame pair):
|
|
2137
|
+
// a `@{}` body whose sole output is one component call emits that call
|
|
2138
|
+
// with `inherit=true` → ssrComponent skips the `<!--[-->…<!--]-->` frame
|
|
2139
|
+
// wrap (the parent's own pair already bounds it). Synthetic sub-bodies
|
|
2140
|
+
// (statement arrays) never qualify. Consumed by ssrEmitComponent at the
|
|
2141
|
+
// root emit, before it recurses into props/children.
|
|
2142
|
+
const prevInheritRoot = ctx._ssrInheritRoot;
|
|
2143
|
+
ctx._ssrInheritRoot =
|
|
2144
|
+
!!(node.body && node.body.type === 'JSXCodeBlock') && inheritSoleCompRoot(bodyNodes, ctx);
|
|
1748
2145
|
const htmlExpr = ssrEmitNodes(bodyNodes, ctx, name, inlinedSubs, parentNs, cssHash);
|
|
2146
|
+
ctx._ssrInheritRoot = prevInheritRoot;
|
|
1749
2147
|
ctx._tsxValuePos = prevValuePos;
|
|
1750
2148
|
|
|
1751
2149
|
let cssLines = '';
|
|
@@ -1899,14 +2297,20 @@ function ssrEmitNode(node, ctx, name, inlinedSubs, parentNs, cssHash, nlGuard =
|
|
|
1899
2297
|
function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
1900
2298
|
const tag = elementTagName(node);
|
|
1901
2299
|
rejectVoidElementContent(tag, node, ctx);
|
|
2300
|
+
rejectTextareaValueChildren(tag, node, ctx);
|
|
1902
2301
|
const attrs = node.attributes || node.openingElement?.attributes || [];
|
|
1903
|
-
|
|
1904
|
-
|
|
2302
|
+
// NB: the ns helpers take the TAG STRING (passing the node silently returns
|
|
2303
|
+
// the inherited ns — svg subtrees would never enter the svg namespace).
|
|
2304
|
+
const selfNs = nsForSelf(tag, parentNs);
|
|
2305
|
+
const childNs = nsForChildren(tag, selfNs);
|
|
1905
2306
|
|
|
1906
2307
|
// `parts` are JS expressions concatenated with `+`. `lit` accumulates the
|
|
1907
2308
|
// current static run so adjacent literals fold into one quoted chunk.
|
|
2309
|
+
// `<option>` builds its ATTRS-ONLY expression here — ssrOption assembles
|
|
2310
|
+
// the whole tag at runtime so an enclosing controlled `<select>` scope can
|
|
2311
|
+
// mark it ` selected` (see the option branch at the bottom).
|
|
1908
2312
|
const parts = [];
|
|
1909
|
-
let lit = '<' + tag;
|
|
2313
|
+
let lit = tag === 'option' ? '' : '<' + tag;
|
|
1910
2314
|
const flush = () => {
|
|
1911
2315
|
if (lit) {
|
|
1912
2316
|
parts.push(JSON.stringify(lit));
|
|
@@ -1914,6 +2318,17 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
1914
2318
|
}
|
|
1915
2319
|
};
|
|
1916
2320
|
|
|
2321
|
+
// Controlled-form serialization state (mirrors the client helpers — see the
|
|
2322
|
+
// controlled section of runtime.server.ts): <input> maps `defaultValue`/
|
|
2323
|
+
// `defaultChecked` onto the native attrs and routes dynamic value/checked
|
|
2324
|
+
// through dedicated serializers; <textarea> routes value/defaultValue into
|
|
2325
|
+
// the CONTENT position; <select> feeds them to the option-projection scope
|
|
2326
|
+
// (never an attribute); <option> captures its `value` for the scope compare.
|
|
2327
|
+
let ctlValue = null; // textarea/select captured `value` expr
|
|
2328
|
+
let ctlDefault = null; // textarea/select captured `defaultValue` expr
|
|
2329
|
+
let selMultiple = 'false'; // select `multiple` expr (constant or temp)
|
|
2330
|
+
let optValue = null; // option `value` expr (constant or temp); null = no value attr
|
|
2331
|
+
|
|
1917
2332
|
const firstSpreadIdx = attrs.findIndex(
|
|
1918
2333
|
(a) => a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute',
|
|
1919
2334
|
);
|
|
@@ -1960,13 +2375,13 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
1960
2375
|
// Events and refs have no server semantics — dropped.
|
|
1961
2376
|
if (rawAttrName === 'ref') continue;
|
|
1962
2377
|
if (isEventAttrName(rawAttrName)) continue;
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2378
|
+
// `autoFocus` never serializes (React DOM server parity — the client
|
|
2379
|
+
// focuses at its mount commit; custom elements keep raw props).
|
|
2380
|
+
if (rawAttrName === 'autoFocus' && !tag.includes('-')) continue;
|
|
2381
|
+
// Custom elements keep names VERBATIM (React parity — they get raw props,
|
|
2382
|
+
// no alias tables; `className`→`class` still applies); ssrAttr applies
|
|
2383
|
+
// the same gate for dynamic values.
|
|
2384
|
+
const attrName = normalizeJsxAttrName(rawAttrName, tag);
|
|
1970
2385
|
const val = attr.value;
|
|
1971
2386
|
const isAfterSpread = firstSpreadIdx !== -1 && attrI > firstSpreadIdx;
|
|
1972
2387
|
|
|
@@ -1979,6 +2394,112 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
1979
2394
|
continue;
|
|
1980
2395
|
}
|
|
1981
2396
|
|
|
2397
|
+
// ── Controlled form props (see the state block above the loop) ──
|
|
2398
|
+
if (
|
|
2399
|
+
(tag === 'input' || tag === 'textarea' || tag === 'select') &&
|
|
2400
|
+
(attrName === 'value' ||
|
|
2401
|
+
attrName === 'defaultValue' ||
|
|
2402
|
+
(tag === 'input' && (attrName === 'checked' || attrName === 'defaultChecked')))
|
|
2403
|
+
) {
|
|
2404
|
+
const ctlInner =
|
|
2405
|
+
val == null ? null : val.type === 'JSXExpressionContainer' ? val.expression : val;
|
|
2406
|
+
if (tag === 'input') {
|
|
2407
|
+
// defaultValue/defaultChecked serialize as the NATIVE attrs;
|
|
2408
|
+
// dynamic value/checked go through serializers that mirror the
|
|
2409
|
+
// client helpers byte-for-byte (value={false} → value="false",
|
|
2410
|
+
// checked truthy → bare presence).
|
|
2411
|
+
const isCheck = attrName === 'checked' || attrName === 'defaultChecked';
|
|
2412
|
+
if (ctlInner === null && !isAfterSpread) {
|
|
2413
|
+
// Bare boolean prop (`<input checked/>`) → static presence
|
|
2414
|
+
// (`value` bare mirrors the client's String(true)).
|
|
2415
|
+
lit += isCheck ? ' checked' : ' value="true"';
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
if (ctlInner !== null && ctlInner.type === 'Literal' && !isAfterSpread) {
|
|
2419
|
+
const lv = ctlInner.value;
|
|
2420
|
+
if (isCheck) {
|
|
2421
|
+
if (lv != null && lv !== false) lit += ' checked';
|
|
2422
|
+
} else if (lv != null) {
|
|
2423
|
+
lit += ` value="${escapeAttr(String(lv))}"`;
|
|
2424
|
+
}
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
flush();
|
|
2428
|
+
const ctlExpr =
|
|
2429
|
+
ctlInner === null ? 'true' : printExprWithTsrx(ctlInner, ctx, name, inlinedSubs);
|
|
2430
|
+
if (isCheck) {
|
|
2431
|
+
ctx.runtimeNeeded.add('ssrCheckedAttr');
|
|
2432
|
+
parts.push(`_$ssrCheckedAttr(${ctlExpr})`);
|
|
2433
|
+
} else {
|
|
2434
|
+
ctx.runtimeNeeded.add('ssrValueAttr');
|
|
2435
|
+
parts.push(`_$ssrValueAttr(${ctlExpr})`);
|
|
2436
|
+
}
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
// textarea / select: value/defaultValue never serialize as attributes —
|
|
2440
|
+
// captured for the content position (textarea) / projection scope (select).
|
|
2441
|
+
const ctlExpr =
|
|
2442
|
+
ctlInner === null ? 'true' : printExprWithTsrx(ctlInner, ctx, name, inlinedSubs);
|
|
2443
|
+
if (attrName === 'value') ctlValue = ctlExpr;
|
|
2444
|
+
else ctlDefault = ctlExpr;
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
if (tag === 'select' && attrName === 'multiple') {
|
|
2448
|
+
// Serialize the attribute normally AND capture the value for the
|
|
2449
|
+
// option-projection scope — a dynamic value binds to a temp so the
|
|
2450
|
+
// expression evaluates once.
|
|
2451
|
+
if (val == null) {
|
|
2452
|
+
selMultiple = 'true';
|
|
2453
|
+
lit += ' multiple';
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
const mInner = val.type === 'JSXExpressionContainer' ? val.expression : val;
|
|
2457
|
+
if (mInner.type === 'Literal' && !isAfterSpread) {
|
|
2458
|
+
selMultiple = mInner.value ? 'true' : 'false';
|
|
2459
|
+
if (mInner.value === true) lit += ' multiple';
|
|
2460
|
+
else if (typeof mInner.value === 'string') lit += ` multiple="${escapeAttr(mInner.value)}"`;
|
|
2461
|
+
else if (typeof mInner.value === 'number') lit += ` multiple="${mInner.value}"`;
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
const tmp = `__sp${spreadTemps.length}`;
|
|
2465
|
+
spreadTemps.push({
|
|
2466
|
+
tempName: tmp,
|
|
2467
|
+
argExpr: printExprWithTsrx(mInner, ctx, name, inlinedSubs),
|
|
2468
|
+
});
|
|
2469
|
+
selMultiple = tmp;
|
|
2470
|
+
flush();
|
|
2471
|
+
ctx.runtimeNeeded.add('ssrAttr');
|
|
2472
|
+
parts.push(`_$ssrAttr('multiple', ${tmp}, ${JSON.stringify(tag)})`);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
if (tag === 'option' && attrName === 'value') {
|
|
2476
|
+
// The option's value feeds BOTH the attribute and the select-scope
|
|
2477
|
+
// compare — a dynamic value binds to a temp for single evaluation.
|
|
2478
|
+
if (val == null) {
|
|
2479
|
+
optValue = '""';
|
|
2480
|
+
lit += ' value';
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const oInner = val.type === 'JSXExpressionContainer' ? val.expression : val;
|
|
2484
|
+
if (oInner.type === 'Literal' && !isAfterSpread) {
|
|
2485
|
+
optValue = JSON.stringify(String(oInner.value));
|
|
2486
|
+
if (typeof oInner.value === 'string') lit += ` value="${escapeAttr(oInner.value)}"`;
|
|
2487
|
+
else if (typeof oInner.value === 'number') lit += ` value="${oInner.value}"`;
|
|
2488
|
+
else if (oInner.value === true) lit += ' value';
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const tmp = `__sp${spreadTemps.length}`;
|
|
2492
|
+
spreadTemps.push({
|
|
2493
|
+
tempName: tmp,
|
|
2494
|
+
argExpr: printExprWithTsrx(oInner, ctx, name, inlinedSubs),
|
|
2495
|
+
});
|
|
2496
|
+
optValue = tmp;
|
|
2497
|
+
flush();
|
|
2498
|
+
ctx.runtimeNeeded.add('ssrAttr');
|
|
2499
|
+
parts.push(`_$ssrAttr('value', ${tmp}, ${JSON.stringify(tag)})`);
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
1982
2503
|
// Boolean attribute (no value) → present.
|
|
1983
2504
|
if (val == null) {
|
|
1984
2505
|
lit += ' ' + attrName;
|
|
@@ -2005,15 +2526,10 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
2005
2526
|
}
|
|
2006
2527
|
|
|
2007
2528
|
// Static literal (and not after a spread) → inline into the tag.
|
|
2529
|
+
// bakeStaticAttr applies the shared React-parity value tables (client
|
|
2530
|
+
// bake stays byte-identical — hydration parity).
|
|
2008
2531
|
if (!isAfterSpread && inner.type === 'Literal') {
|
|
2009
|
-
|
|
2010
|
-
else if (typeof inner.value === 'number') lit += ` ${attrName}="${inner.value}"`;
|
|
2011
|
-
else if (typeof inner.value === 'boolean' && attrName.startsWith('aria-')) {
|
|
2012
|
-
// `aria-*` is ENUMERATED (React parity, mirroring ssrAttr): a boolean
|
|
2013
|
-
// literal serialises as "true"/"false" — `aria-hidden={false}` must
|
|
2014
|
-
// render, not drop out via the generic boolean handling.
|
|
2015
|
-
lit += ` ${attrName}="${inner.value}"`;
|
|
2016
|
-
} else if (inner.value === true) lit += ` ${attrName}`;
|
|
2532
|
+
lit += bakeStaticAttr(attrName, inner.value, tag);
|
|
2017
2533
|
continue;
|
|
2018
2534
|
}
|
|
2019
2535
|
|
|
@@ -2053,8 +2569,8 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
2053
2569
|
return finalize();
|
|
2054
2570
|
}
|
|
2055
2571
|
|
|
2056
|
-
lit += '>';
|
|
2057
|
-
const normChildren = normalizeChildren(node.children || []);
|
|
2572
|
+
if (tag !== 'option') lit += '>'; // option: ssrOption assembles the tag (attrs-only here)
|
|
2573
|
+
const normChildren = normalizeChildren(node.children || [], childNs === 'svg');
|
|
2058
2574
|
// Only-child renderable `{expr}` → markerless `ssrChildText` (mirrors the client's
|
|
2059
2575
|
// markerless `childTextHole` mount: a primitive is the host's bare text, an object
|
|
2060
2576
|
// still gets a `<!--[-->…<!--]-->` block). Must match the client's only-child
|
|
@@ -2086,6 +2602,40 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
2086
2602
|
nlGuardFirst,
|
|
2087
2603
|
);
|
|
2088
2604
|
}
|
|
2605
|
+
// Controlled `<textarea value/defaultValue>`: the prop IS the content
|
|
2606
|
+
// (children were rejected at compile time) — value wins over defaultValue,
|
|
2607
|
+
// a nullish value falls through to the default (the client cascade).
|
|
2608
|
+
if (tag === 'textarea' && (ctlValue !== null || ctlDefault !== null)) {
|
|
2609
|
+
ctx.runtimeNeeded.add('ssrTextareaValue');
|
|
2610
|
+
const src =
|
|
2611
|
+
ctlValue !== null && ctlDefault !== null
|
|
2612
|
+
? `(${ctlValue}) ?? (${ctlDefault})`
|
|
2613
|
+
: (ctlValue ?? ctlDefault);
|
|
2614
|
+
childrenExpr = `_$ssrTextareaValue(${src})`;
|
|
2615
|
+
}
|
|
2616
|
+
// Controlled `<select value/defaultValue>`: push the option-projection
|
|
2617
|
+
// scope around the children serialization — every compiled/de-opt
|
|
2618
|
+
// `<option>` inside (across component boundaries and @for bodies; SSR is a
|
|
2619
|
+
// synchronous nested call tree) consults it via ssrOption.
|
|
2620
|
+
if (tag === 'select' && (ctlValue !== null || ctlDefault !== null)) {
|
|
2621
|
+
ctx.runtimeNeeded.add('ssrSelectScope');
|
|
2622
|
+
childrenExpr = `_$ssrSelectScope(${ctlValue ?? 'void 0'}, ${ctlDefault ?? 'void 0'}, ${selMultiple}, () => (${childrenExpr}))`;
|
|
2623
|
+
}
|
|
2624
|
+
// `<option>`: assemble via ssrOption so an active select scope can mark it
|
|
2625
|
+
// ` selected` (returns a plain `<option …>` when no scope is active).
|
|
2626
|
+
if (tag === 'option') {
|
|
2627
|
+
let contentExpr = childrenExpr;
|
|
2628
|
+
if (htmlSources.length > 0) {
|
|
2629
|
+
ctx.runtimeNeeded.add('ssrInnerHtml');
|
|
2630
|
+
contentExpr = `(_$ssrInnerHtml([${htmlSources.join(', ')}]) ?? (${childrenExpr}))`;
|
|
2631
|
+
}
|
|
2632
|
+
flush();
|
|
2633
|
+
const attrsExpr = parts.length > 0 ? parts.join(' + ') : "''";
|
|
2634
|
+
parts.length = 0;
|
|
2635
|
+
ctx.runtimeNeeded.add('ssrOption');
|
|
2636
|
+
parts.push(`_$ssrOption(${optValue ?? 'void 0'}, ${attrsExpr}, ${contentExpr})`);
|
|
2637
|
+
return finalize();
|
|
2638
|
+
}
|
|
2089
2639
|
if (htmlSources.length > 0) {
|
|
2090
2640
|
// Raw HTML (explicit and/or spread-supplied) wins over children when present
|
|
2091
2641
|
// at runtime (last source wins); otherwise the children render.
|
|
@@ -2102,6 +2652,12 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash) {
|
|
|
2102
2652
|
}
|
|
2103
2653
|
|
|
2104
2654
|
function ssrEmitComponent(node, ctx, name, inlinedSubs, cssHash) {
|
|
2655
|
+
// M3 inherit-range: consume the body-root flag ONCE, before this component's
|
|
2656
|
+
// props/children compile below (they recurse into ssrEmitNodes/ssrCompileSub
|
|
2657
|
+
// and must not inherit it). Set by ssrCompileBody only for the sole
|
|
2658
|
+
// comp-call root of a `@{}` body — which is exactly this emit.
|
|
2659
|
+
const inherit = ctx._ssrInheritRoot === true;
|
|
2660
|
+
ctx._ssrInheritRoot = false;
|
|
2105
2661
|
const compExpr = tagExpr(node);
|
|
2106
2662
|
const attrs = node.attributes || node.openingElement?.attributes || [];
|
|
2107
2663
|
const propParts = [];
|
|
@@ -2167,11 +2723,18 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, cssHash) {
|
|
|
2167
2723
|
// that. Mirrors the client `@{}` convention (componentSlot + a render fn).
|
|
2168
2724
|
const sub = ssrCompileSub(node.children, ctx, '__schildren', [], cssHash, 'html');
|
|
2169
2725
|
inlinedSubs.push(sub.fn + ';');
|
|
2170
|
-
|
|
2726
|
+
// Tag the server children-block like the client does (see the client
|
|
2727
|
+
// emission in lowerComponentCall): a consumer's `typeof children ===
|
|
2728
|
+
// 'function' && !isChildrenBlock(children)` render-prop check must agree
|
|
2729
|
+
// on BOTH runtimes. Untagged, a binding (e.g. the router Link) INVOKES
|
|
2730
|
+
// the block as a render prop server-side, gets its HTML string back, and
|
|
2731
|
+
// the enclosing hole escapes that markup into visible text.
|
|
2732
|
+
ctx.runtimeNeeded.add('markChildrenBlock');
|
|
2733
|
+
propParts.push(`"children": _$markChildrenBlock(${sub.fnName})`);
|
|
2171
2734
|
}
|
|
2172
2735
|
}
|
|
2173
2736
|
ctx.runtimeNeeded.add('ssrComponent');
|
|
2174
|
-
return `_$ssrComponent(__s, ${compExpr}, { ${propParts.join(', ')} })`;
|
|
2737
|
+
return `_$ssrComponent(__s, ${compExpr}, { ${propParts.join(', ')} }${inherit ? ', true' : ''})`;
|
|
2175
2738
|
}
|
|
2176
2739
|
|
|
2177
2740
|
// ---------------------------------------------------------------------------
|
|
@@ -2566,12 +3129,20 @@ function compileComponent(node, ctx, options) {
|
|
|
2566
3129
|
ctx.knownStringLocals = prevKnownStr;
|
|
2567
3130
|
}
|
|
2568
3131
|
|
|
3132
|
+
// parallelUse warm plan: attached to the INNER function object (not the
|
|
3133
|
+
// module const) so the component's own body — where the function-
|
|
3134
|
+
// expression name shadows the const — resolves `_$warmChild(Self, …)` to
|
|
3135
|
+
// an object that carries the plan. hmr() forwards `__warm` from the
|
|
3136
|
+
// wrapped fn onto its wrapper for cross-module references.
|
|
3137
|
+
const warmedFn = ctx._pendingWarm ? `Object.assign(${fn}, { __warm: ${ctx._pendingWarm} })` : fn;
|
|
3138
|
+
ctx._pendingWarm = null;
|
|
3139
|
+
|
|
2569
3140
|
// HMR-wrap exported components inline so the binding stays a `const` (no
|
|
2570
3141
|
// reassignment dance needed). The wrapper preserves the user-facing
|
|
2571
3142
|
// function-name identity by NAMING the inner FunctionExpression — `hmr`
|
|
2572
3143
|
// returns a wrapper that delegates to whatever fn is currently committed,
|
|
2573
3144
|
// and `module.Foo[HMR].update(...)` swaps it on each accept.
|
|
2574
|
-
const valueExpr = hmrWrap && isExported ? `_$hmr(${
|
|
3145
|
+
const valueExpr = hmrWrap && isExported ? `_$hmr(${warmedFn})` : warmedFn;
|
|
2575
3146
|
if (isDefault) {
|
|
2576
3147
|
return `const ${name} = ${valueExpr};\nexport default ${name};`;
|
|
2577
3148
|
}
|
|
@@ -2641,6 +3212,29 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
2641
3212
|
);
|
|
2642
3213
|
}
|
|
2643
3214
|
|
|
3215
|
+
// parallelUse: the parallel-`use()` pipeline (docs/suspense-parallel-use-plan.md)
|
|
3216
|
+
// slots in HERE — after autoCallback (so memoized creations aren't re-wrapped),
|
|
3217
|
+
// before rewriteHookCalls (so the _$useMemo/_$useBatch calls it emits are
|
|
3218
|
+
// compiler-aliased, not user identifiers). Top-level component bodies run the
|
|
3219
|
+
// full pipeline: Pass A memoizes creations across the body AND the directive
|
|
3220
|
+
// arms of the render tree (arms hoist into sub-bodies later, already
|
|
3221
|
+
// transformed), the warm plan is derived from that same analysis, and Pass B
|
|
3222
|
+
// hoists+batches. Sub-bodies (hoisted @try/@if arms via hoistBodyHelper's
|
|
3223
|
+
// legacy path) arrive pre-memoized and run Pass B only.
|
|
3224
|
+
if (ctx.parallelUse) {
|
|
3225
|
+
let warmThunk = null;
|
|
3226
|
+
if (options && options.autoCallback) {
|
|
3227
|
+
const creations = [];
|
|
3228
|
+
const warmChildren = [];
|
|
3229
|
+
workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
|
|
3230
|
+
jsxNodes = parallelUseWalkJsx(jsxNodes, ctx, name, creations, warmChildren, [], new Set());
|
|
3231
|
+
const warm = buildWarmArtifacts(node, ctx, name, creations, warmChildren);
|
|
3232
|
+
warmThunk = warm.thunk;
|
|
3233
|
+
ctx._pendingWarm = warm.warmSrc;
|
|
3234
|
+
}
|
|
3235
|
+
workingStatements = rewriteParallelUse(workingStatements, ctx, name, warmThunk);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
2644
3238
|
// Rewrite hook calls and `<tsrx>` blocks in statements before printing them.
|
|
2645
3239
|
// A `<tsrx>` block at expression position (e.g. `const f = <tsrx>...</tsrx>`)
|
|
2646
3240
|
// is hoisted as a render function in inlinedSubs and replaced with an
|
|
@@ -2684,7 +3278,14 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
2684
3278
|
const prevFDC = ctx._foldedDirectiveCalls;
|
|
2685
3279
|
if (node.body && node.body.foldedDirectives)
|
|
2686
3280
|
ctx._foldedDirectiveCalls = node.body.foldedDirectives;
|
|
3281
|
+
// M3 inherit-range: only a real `@{ … }` (JSXCodeBlock) component body spans
|
|
3282
|
+
// its block's whole range — synthetic sub-bodies (@if/@for/@try arms,
|
|
3283
|
+
// children render-fns) pass statement arrays and stay unflagged. planJsx
|
|
3284
|
+
// consumes the flag once (nested planJsx calls see it cleared).
|
|
3285
|
+
const prevInheritBody = ctx._inheritBody;
|
|
3286
|
+
ctx._inheritBody = !!(node.body && node.body.type === 'JSXCodeBlock');
|
|
2687
3287
|
const plan = planJsx(jsxNodes, ctx, name, inlinedSubs, parentNs, cssHash);
|
|
3288
|
+
ctx._inheritBody = prevInheritBody;
|
|
2688
3289
|
ctx._foldedDirectiveCalls = prevFDC;
|
|
2689
3290
|
|
|
2690
3291
|
const lines = [];
|
|
@@ -2722,26 +3323,846 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
2722
3323
|
lines.push(plan.mount);
|
|
2723
3324
|
lines.push(` }`);
|
|
2724
3325
|
}
|
|
2725
|
-
if (plan.everyRender) lines.push(plan.everyRender);
|
|
3326
|
+
if (plan.everyRender) lines.push(plan.everyRender);
|
|
3327
|
+
}
|
|
3328
|
+
if (plan.after) lines.push(plan.after);
|
|
3329
|
+
// Hoisted `<title>`/`<meta>`/`<link>` → headBlock into document.head
|
|
3330
|
+
// (out-of-band; re-applied each render for reactivity, removed on unmount).
|
|
3331
|
+
if (plan.head) lines.push(plan.head);
|
|
3332
|
+
|
|
3333
|
+
// PROPS-FIRST convention: `(…userProps, __s, __extra)`. The scope is the 2nd arg
|
|
3334
|
+
// (a placeholder leads when there are no user params), so a plain function
|
|
3335
|
+
// `App(props)` binds `props`, while compiled bodies still read `__s` by name.
|
|
3336
|
+
const sig = params ? `${params}, __s, __extra` : `__props, __s, __extra`;
|
|
3337
|
+
return `function ${name}(${sig}) {\n const __block = __s.block;\n${lines.join('\n')}\n}`;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// ===========================================================================
|
|
3341
|
+
// Parallel use() — docs/suspense-parallel-use-plan.md
|
|
3342
|
+
// ===========================================================================
|
|
3343
|
+
|
|
3344
|
+
// The parallel-`use()` pipeline (gated on ctx.parallelUse). Three cooperating
|
|
3345
|
+
// transforms reconstruct Solid/Ripple's "fetch starts at creation, suspension
|
|
3346
|
+
// happens at read" property for React-shaped `use()` code:
|
|
3347
|
+
//
|
|
3348
|
+
// Pass A (top-level component bodies, incl. directive-arm bodies BEFORE
|
|
3349
|
+
// they're hoisted): wrap each non-trivial `use(<expr>)` argument in a
|
|
3350
|
+
// slot-keyed `_$useMemo(() => <expr>, [deps], _h$N)` creation. Deps are
|
|
3351
|
+
// one-level member paths of the expression's free variables (props.id,
|
|
3352
|
+
// fetchFn), so refetch happens exactly when inputs change and a replay
|
|
3353
|
+
// can never mint a fresh promise. Loops and nested functions are NOT
|
|
3354
|
+
// entered: loop iterations share one slot symbol, so a memoized creation
|
|
3355
|
+
// there would fresh-promise every render and re-suspend forever.
|
|
3356
|
+
//
|
|
3357
|
+
// Pass B (every function body): find maximal runs of `use()` declaration
|
|
3358
|
+
// statements (const/let interleaves only, taint-tracked), hoist each
|
|
3359
|
+
// run's creations into `__pu$N` temps above the first unwrap, and emit
|
|
3360
|
+
// `_$useBatch([...temps], warmThunk?)` so the boundary suspends ONCE on
|
|
3361
|
+
// the whole stratum instead of once per promise. Unwrap order (and with
|
|
3362
|
+
// it hydration-seed order) is preserved.
|
|
3363
|
+
//
|
|
3364
|
+
// Warm plan (top-level bodies): from the SAME analysis, child component
|
|
3365
|
+
// slots whose reachability guards and props are provably independent of
|
|
3366
|
+
// every non-param local become `_$warmChild` calls in the first batch's
|
|
3367
|
+
// warm thunk, and the component gets a compiled `Comp.__warm` fetch plan
|
|
3368
|
+
// (its own warm-safe creations + guarded child warm calls) so warming
|
|
3369
|
+
// recurses down the tree — the whole descendant fetch tree starts in the
|
|
3370
|
+
// first attempt.
|
|
3371
|
+
//
|
|
3372
|
+
// With the flag off, output is byte-identical (pinned by
|
|
3373
|
+
// tests/compile-parallel-use.test.ts).
|
|
3374
|
+
|
|
3375
|
+
// Names bound by a binding pattern (params, declarator ids). Mirrors the
|
|
3376
|
+
// pattern handling of collectFreeIdentifiers' collectBindings.
|
|
3377
|
+
function collectPatternNames(pat, into) {
|
|
3378
|
+
if (!pat) return into;
|
|
3379
|
+
switch (pat.type) {
|
|
3380
|
+
case 'Identifier':
|
|
3381
|
+
into.add(pat.name);
|
|
3382
|
+
break;
|
|
3383
|
+
case 'ObjectPattern':
|
|
3384
|
+
for (const p of pat.properties || []) {
|
|
3385
|
+
if (p.type === 'RestElement') collectPatternNames(p.argument, into);
|
|
3386
|
+
else collectPatternNames(p.value, into);
|
|
3387
|
+
}
|
|
3388
|
+
break;
|
|
3389
|
+
case 'ArrayPattern':
|
|
3390
|
+
for (const el of pat.elements || []) collectPatternNames(el, into);
|
|
3391
|
+
break;
|
|
3392
|
+
case 'AssignmentPattern':
|
|
3393
|
+
collectPatternNames(pat.left, into);
|
|
3394
|
+
break;
|
|
3395
|
+
case 'RestElement':
|
|
3396
|
+
collectPatternNames(pat.argument, into);
|
|
3397
|
+
break;
|
|
3398
|
+
}
|
|
3399
|
+
return into;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// Strip TS value-preserving wrappers (`x as T`, `x!`, `<T>x`, `x satisfies T`).
|
|
3403
|
+
function unwrapTsExpr(n) {
|
|
3404
|
+
while (
|
|
3405
|
+
n &&
|
|
3406
|
+
(n.type === 'TSAsExpression' ||
|
|
3407
|
+
n.type === 'TSNonNullExpression' ||
|
|
3408
|
+
n.type === 'TSTypeAssertion' ||
|
|
3409
|
+
n.type === 'TSSatisfiesExpression' ||
|
|
3410
|
+
n.type === 'ParenthesizedExpression')
|
|
3411
|
+
) {
|
|
3412
|
+
n = n.expression;
|
|
3413
|
+
}
|
|
3414
|
+
return n;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// Dep extraction for a memoized creation: one-level member paths for free
|
|
3418
|
+
// identifiers used as `obj.prop` (→ `props.id`, so a fresh props OBJECT with
|
|
3419
|
+
// unchanged fields doesn't refetch), bare identifiers otherwise. Scope-aware:
|
|
3420
|
+
// identifiers bound inside nested functions don't become deps.
|
|
3421
|
+
function collectDepPaths(expr) {
|
|
3422
|
+
const deps = [];
|
|
3423
|
+
const seen = new Set();
|
|
3424
|
+
const push = (node, key) => {
|
|
3425
|
+
if (seen.has(key)) return;
|
|
3426
|
+
seen.add(key);
|
|
3427
|
+
deps.push(node);
|
|
3428
|
+
};
|
|
3429
|
+
walk(expr, new Set());
|
|
3430
|
+
return deps;
|
|
3431
|
+
|
|
3432
|
+
function walk(n, bound) {
|
|
3433
|
+
if (!n || typeof n !== 'object') return;
|
|
3434
|
+
if (Array.isArray(n)) {
|
|
3435
|
+
for (const x of n) walk(x, bound);
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
switch (n.type) {
|
|
3439
|
+
case 'Identifier':
|
|
3440
|
+
if (!bound.has(n.name)) push({ type: 'Identifier', name: n.name }, n.name);
|
|
3441
|
+
return;
|
|
3442
|
+
case 'MemberExpression':
|
|
3443
|
+
if (!n.computed && n.object.type === 'Identifier' && n.property.type === 'Identifier') {
|
|
3444
|
+
if (!bound.has(n.object.name)) {
|
|
3445
|
+
const key = n.object.name + '.' + n.property.name;
|
|
3446
|
+
push(
|
|
3447
|
+
{
|
|
3448
|
+
type: 'MemberExpression',
|
|
3449
|
+
object: { type: 'Identifier', name: n.object.name },
|
|
3450
|
+
property: { type: 'Identifier', name: n.property.name },
|
|
3451
|
+
computed: false,
|
|
3452
|
+
optional: false,
|
|
3453
|
+
},
|
|
3454
|
+
key,
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
walk(n.object, bound);
|
|
3460
|
+
if (n.computed) walk(n.property, bound);
|
|
3461
|
+
return;
|
|
3462
|
+
case 'FunctionExpression':
|
|
3463
|
+
case 'ArrowFunctionExpression': {
|
|
3464
|
+
const inner = new Set(bound);
|
|
3465
|
+
for (const p of n.params || []) collectPatternNames(p, inner);
|
|
3466
|
+
walk(n.body, inner);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
case 'Property':
|
|
3470
|
+
if (n.computed) walk(n.key, bound);
|
|
3471
|
+
walk(n.value, bound);
|
|
3472
|
+
return;
|
|
3473
|
+
case 'VariableDeclarator':
|
|
3474
|
+
walk(n.init, bound);
|
|
3475
|
+
return;
|
|
3476
|
+
default:
|
|
3477
|
+
for (const k in n) {
|
|
3478
|
+
if (k === 'loc' || k === 'start' || k === 'end' || k === 'metadata') continue;
|
|
3479
|
+
walk(n[k], bound);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// A `use()` argument that needs no memoization: already-stable references
|
|
3486
|
+
// (identifiers, static member chains) and literals.
|
|
3487
|
+
function isTrivialUseArg(n) {
|
|
3488
|
+
n = unwrapTsExpr(n);
|
|
3489
|
+
if (!n) return true;
|
|
3490
|
+
if (n.type === 'Identifier' || n.type === 'Literal') return true;
|
|
3491
|
+
if (n.type === 'MemberExpression' && !n.computed) return isTrivialUseArg(n.object);
|
|
3492
|
+
return false;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
const LOOP_TYPES = new Set([
|
|
3496
|
+
'ForStatement',
|
|
3497
|
+
'ForOfStatement',
|
|
3498
|
+
'ForInStatement',
|
|
3499
|
+
'WhileStatement',
|
|
3500
|
+
'DoWhileStatement',
|
|
3501
|
+
]);
|
|
3502
|
+
const FN_TYPES = new Set(['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression']);
|
|
3503
|
+
|
|
3504
|
+
// Is this statement a `const/let x = use(<arg>)` declaration (or a bare
|
|
3505
|
+
// `use(<arg>)` expression statement)? Returns the use CallExpression or null.
|
|
3506
|
+
function useCallOfStatement(stmt) {
|
|
3507
|
+
let call = null;
|
|
3508
|
+
if (
|
|
3509
|
+
stmt.type === 'VariableDeclaration' &&
|
|
3510
|
+
(stmt.kind === 'const' || stmt.kind === 'let') &&
|
|
3511
|
+
stmt.declarations &&
|
|
3512
|
+
stmt.declarations.length === 1
|
|
3513
|
+
) {
|
|
3514
|
+
call = unwrapTsExpr(stmt.declarations[0].init);
|
|
3515
|
+
} else if (stmt.type === 'ExpressionStatement') {
|
|
3516
|
+
call = unwrapTsExpr(stmt.expression);
|
|
3517
|
+
}
|
|
3518
|
+
if (
|
|
3519
|
+
call &&
|
|
3520
|
+
call.type === 'CallExpression' &&
|
|
3521
|
+
call.callee.type === 'Identifier' &&
|
|
3522
|
+
call.callee.name === 'use' &&
|
|
3523
|
+
call.arguments.length >= 1
|
|
3524
|
+
) {
|
|
3525
|
+
return call;
|
|
3526
|
+
}
|
|
3527
|
+
return null;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// ── Pass A: memoize use() argument creations ───────────────────────────────
|
|
3531
|
+
//
|
|
3532
|
+
// Walks a statement array (recursing into if/blocks, NOT into loops or nested
|
|
3533
|
+
// functions), rewriting each non-trivial `use(<expr>)` argument to
|
|
3534
|
+
// `_$useMemo(() => <expr>, [deps], _h$N)` in place. Records every creation
|
|
3535
|
+
// with its guard chain for the warm plan. Returns a new array.
|
|
3536
|
+
function parallelUseMemoizePass(stmts, ctx, componentName, creations, guards, locals) {
|
|
3537
|
+
return stmts.map((stmt) => rewriteStmt(stmt));
|
|
3538
|
+
|
|
3539
|
+
function rewriteStmt(stmt) {
|
|
3540
|
+
if (!stmt || typeof stmt !== 'object') return stmt;
|
|
3541
|
+
if (LOOP_TYPES.has(stmt.type) || FN_TYPES.has(stmt.type)) return stmt;
|
|
3542
|
+
const call = useCallOfStatement(stmt);
|
|
3543
|
+
if (call) {
|
|
3544
|
+
const rewritten = rewriteUseCall(call);
|
|
3545
|
+
if (rewritten === call) return stmt;
|
|
3546
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
3547
|
+
return {
|
|
3548
|
+
...stmt,
|
|
3549
|
+
declarations: [{ ...stmt.declarations[0], init: rewritten }],
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
return { ...stmt, expression: rewritten };
|
|
3553
|
+
}
|
|
3554
|
+
if (stmt.type === 'IfStatement') {
|
|
3555
|
+
return {
|
|
3556
|
+
...stmt,
|
|
3557
|
+
consequent: rewriteStmt(stmt.consequent),
|
|
3558
|
+
alternate: stmt.alternate ? rewriteStmt(stmt.alternate) : stmt.alternate,
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
if (stmt.type === 'BlockStatement') {
|
|
3562
|
+
return {
|
|
3563
|
+
...stmt,
|
|
3564
|
+
body: parallelUseMemoizePass(stmt.body, ctx, componentName, creations, guards, locals),
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
return stmt;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
function rewriteUseCall(call) {
|
|
3571
|
+
const arg = unwrapTsExpr(call.arguments[0]);
|
|
3572
|
+
if (isTrivialUseArg(arg)) return call;
|
|
3573
|
+
const symVar = allocHookSymbol(ctx, `${componentName}.use.memo#${ctx.nextHookSymId}`);
|
|
3574
|
+
const deps = collectDepPaths(arg);
|
|
3575
|
+
// Server mirror: `puMemo` — keyed CROSS-PASS creation cache (a fresh
|
|
3576
|
+
// SSRScope per pass makes client useMemo semantics useless there).
|
|
3577
|
+
const memoHelper = ctx.mode === 'server' ? 'puMemo' : 'useMemo';
|
|
3578
|
+
ctx.runtimeNeeded.add(memoHelper);
|
|
3579
|
+
creations.push({ symVar, expr: arg, deps, guards: [...guards], locals });
|
|
3580
|
+
const memoCall = {
|
|
3581
|
+
type: 'CallExpression',
|
|
3582
|
+
callee: { type: 'Identifier', name: `_$${memoHelper}` },
|
|
3583
|
+
arguments: [
|
|
3584
|
+
{ type: 'ArrowFunctionExpression', params: [], expression: true, async: false, body: arg },
|
|
3585
|
+
{ type: 'ArrayExpression', elements: deps },
|
|
3586
|
+
{ type: 'Identifier', name: symVar },
|
|
3587
|
+
],
|
|
3588
|
+
optional: false,
|
|
3589
|
+
};
|
|
3590
|
+
return { ...call, arguments: [memoCall, ...call.arguments.slice(1)] };
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// Pass A over the RENDER tree: memoize statements inside directive-arm bodies
|
|
3595
|
+
// (they hoist into sub-bodies later, already transformed) and collect
|
|
3596
|
+
// child-component warm candidates with their guard chains. Recursion stops at
|
|
3597
|
+
// @for/@switch arms (v1) and does not enter component children. `locals`
|
|
3598
|
+
// accumulates arm-scoped bindings (e.g. `const a = use(…)` inside a @try
|
|
3599
|
+
// body) so warm-safety can see them — they are NOT in
|
|
3600
|
+
// ctx.currentComponentLocals, which only covers the top body.
|
|
3601
|
+
function parallelUseWalkJsx(nodes, ctx, componentName, creations, warmChildren, guards, locals) {
|
|
3602
|
+
return nodes.map((n) => walkNode(n));
|
|
3603
|
+
|
|
3604
|
+
function walkNode(node) {
|
|
3605
|
+
if (!node || typeof node !== 'object') return node;
|
|
3606
|
+
switch (node.type) {
|
|
3607
|
+
case 'JSXElement': {
|
|
3608
|
+
const nameNode = node.openingElement && node.openingElement.name;
|
|
3609
|
+
const isComponent =
|
|
3610
|
+
nameNode && nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name);
|
|
3611
|
+
if (isComponent) {
|
|
3612
|
+
collectWarmChild(node, nameNode.name);
|
|
3613
|
+
return node; // component children render inside the child — don't descend
|
|
3614
|
+
}
|
|
3615
|
+
return {
|
|
3616
|
+
...node,
|
|
3617
|
+
children: parallelUseWalkJsx(
|
|
3618
|
+
node.children || [],
|
|
3619
|
+
ctx,
|
|
3620
|
+
componentName,
|
|
3621
|
+
creations,
|
|
3622
|
+
warmChildren,
|
|
3623
|
+
guards,
|
|
3624
|
+
locals,
|
|
3625
|
+
),
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
case 'JSXFragment':
|
|
3629
|
+
return {
|
|
3630
|
+
...node,
|
|
3631
|
+
children: parallelUseWalkJsx(
|
|
3632
|
+
node.children || [],
|
|
3633
|
+
ctx,
|
|
3634
|
+
componentName,
|
|
3635
|
+
creations,
|
|
3636
|
+
warmChildren,
|
|
3637
|
+
guards,
|
|
3638
|
+
locals,
|
|
3639
|
+
),
|
|
3640
|
+
};
|
|
3641
|
+
case 'JSXIfExpression': {
|
|
3642
|
+
const not = (e) => ({ type: 'UnaryExpression', operator: '!', prefix: true, argument: e });
|
|
3643
|
+
const consequent = walkArm(node.consequent, [...guards, node.test]);
|
|
3644
|
+
const alternate = node.alternate
|
|
3645
|
+
? walkArm(node.alternate, [...guards, not(node.test)])
|
|
3646
|
+
: node.alternate;
|
|
3647
|
+
return { ...node, consequent, alternate };
|
|
3648
|
+
}
|
|
3649
|
+
case 'JSXTryExpression': {
|
|
3650
|
+
// The try arm renders whenever the component renders — no extra
|
|
3651
|
+
// guard. @pending/@catch arms are exceptional paths: no warming.
|
|
3652
|
+
const out = { ...node };
|
|
3653
|
+
if (node.block) out.block = walkArm(node.block, guards);
|
|
3654
|
+
return out;
|
|
3655
|
+
}
|
|
3656
|
+
default:
|
|
3657
|
+
// @for / @switch arms: v1 — no memoization, no warming (loop slot
|
|
3658
|
+
// sharing; switch guards deferred). Everything else is inert.
|
|
3659
|
+
return node;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
function walkArm(arm, armGuards) {
|
|
3664
|
+
if (!arm || arm.type !== 'BlockStatement') return arm;
|
|
3665
|
+
// Arm-scoped bindings become visible to warm-safety for everything in
|
|
3666
|
+
// this arm (conservatively hoisted: order within the arm is ignored).
|
|
3667
|
+
const armLocals = new Set(locals);
|
|
3668
|
+
for (const entry of arm.body) {
|
|
3669
|
+
if (entry.type === 'VariableDeclaration') {
|
|
3670
|
+
for (const d of entry.declarations || []) collectPatternNames(d.id, armLocals);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
// Arm bodies interleave statements and JSX nodes; route each entry to
|
|
3674
|
+
// the right walker.
|
|
3675
|
+
const body = arm.body.map((entry) => {
|
|
3676
|
+
if (
|
|
3677
|
+
entry.type === 'JSXElement' ||
|
|
3678
|
+
entry.type === 'JSXFragment' ||
|
|
3679
|
+
entry.type === 'JSXIfExpression' ||
|
|
3680
|
+
entry.type === 'JSXTryExpression'
|
|
3681
|
+
) {
|
|
3682
|
+
return parallelUseWalkJsx(
|
|
3683
|
+
[entry],
|
|
3684
|
+
ctx,
|
|
3685
|
+
componentName,
|
|
3686
|
+
creations,
|
|
3687
|
+
warmChildren,
|
|
3688
|
+
armGuards,
|
|
3689
|
+
armLocals,
|
|
3690
|
+
)[0];
|
|
3691
|
+
}
|
|
3692
|
+
return parallelUseMemoizePass(
|
|
3693
|
+
[entry],
|
|
3694
|
+
ctx,
|
|
3695
|
+
componentName,
|
|
3696
|
+
creations,
|
|
3697
|
+
armGuards,
|
|
3698
|
+
armLocals,
|
|
3699
|
+
)[0];
|
|
3700
|
+
});
|
|
3701
|
+
return { ...arm, body };
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
function collectWarmChild(node, compName) {
|
|
3705
|
+
const attrs = node.openingElement.attributes || [];
|
|
3706
|
+
if ((node.children || []).length > 0) return; // children render inside the child — skip
|
|
3707
|
+
const props = [];
|
|
3708
|
+
for (const a of attrs) {
|
|
3709
|
+
if (a.type !== 'JSXAttribute' || a.name.type !== 'JSXIdentifier') return; // spread etc.
|
|
3710
|
+
const key = a.name.name;
|
|
3711
|
+
if (key === 'ref' || key === 'key') return; // instance-wired props — skip this slot
|
|
3712
|
+
let value;
|
|
3713
|
+
if (a.value == null) value = { type: 'Literal', value: true, raw: 'true' };
|
|
3714
|
+
else if (a.value.type === 'JSXExpressionContainer') value = a.value.expression;
|
|
3715
|
+
else value = a.value; // Literal
|
|
3716
|
+
props.push({ key, value });
|
|
3717
|
+
}
|
|
3718
|
+
warmChildren.push({ compName, props, guards: [...guards], locals });
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// Does this expression subtree contain JSX (or a TSRX directive)? Such
|
|
3723
|
+
// expressions cannot be re-printed into a warm plan — they only exist in
|
|
3724
|
+
// lowered form inside the real render path.
|
|
3725
|
+
function containsJsxNode(n) {
|
|
3726
|
+
if (!n || typeof n !== 'object') return false;
|
|
3727
|
+
if (Array.isArray(n)) return n.some(containsJsxNode);
|
|
3728
|
+
if (typeof n.type === 'string' && n.type.startsWith('JSX')) return true;
|
|
3729
|
+
for (const k in n) {
|
|
3730
|
+
if (k === 'loc' || k === 'start' || k === 'end' || k === 'metadata') continue;
|
|
3731
|
+
if (containsJsxNode(n[k])) return true;
|
|
3732
|
+
}
|
|
3733
|
+
return false;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
// Warm-safety: every free identifier of the expression is a component param
|
|
3737
|
+
// or module-scope (NOT a non-param local — component-body OR directive-arm
|
|
3738
|
+
// scoped; those may not exist or may be suspended-data-derived at warm time),
|
|
3739
|
+
// and the expression is plain JS (no JSX — descriptors only exist lowered).
|
|
3740
|
+
function isWarmSafeExpr(expr, paramNames, componentLocals, armLocals) {
|
|
3741
|
+
if (containsJsxNode(expr)) return false;
|
|
3742
|
+
const free = collectFreeIdentifiers(expr, []);
|
|
3743
|
+
for (const id of free) {
|
|
3744
|
+
if (paramNames.has(id)) continue;
|
|
3745
|
+
if (componentLocals && componentLocals.has(id)) return false;
|
|
3746
|
+
if (armLocals && armLocals.has(id)) return false;
|
|
3747
|
+
}
|
|
3748
|
+
return true;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// ── Pass B: run detection + hoist + batch ───────────────────────────────────
|
|
3752
|
+
function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
|
|
3753
|
+
let firstBatch = true;
|
|
3754
|
+
return transformList(statements);
|
|
3755
|
+
|
|
3756
|
+
function transformList(stmts) {
|
|
3757
|
+
const out = [];
|
|
3758
|
+
let run = null; // { members: [{stmt, call?, creation?}], names: Set }
|
|
3759
|
+
const flush = () => {
|
|
3760
|
+
if (!run) return;
|
|
3761
|
+
emitRun(run, out);
|
|
3762
|
+
run = null;
|
|
3763
|
+
};
|
|
3764
|
+
for (const stmt of stmts) {
|
|
3765
|
+
const call = stmt && typeof stmt === 'object' ? useCallOfStatement(stmt) : null;
|
|
3766
|
+
if (call) {
|
|
3767
|
+
const creation = unwrapTsExpr(call.arguments[0]);
|
|
3768
|
+
const free = collectFreeIdentifiers(creation, []);
|
|
3769
|
+
let conflict = false;
|
|
3770
|
+
if (run) {
|
|
3771
|
+
for (const id of free) {
|
|
3772
|
+
if (run.names.has(id)) {
|
|
3773
|
+
conflict = true;
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
if (conflict) flush();
|
|
3779
|
+
if (!run) run = { members: [], names: new Set() };
|
|
3780
|
+
run.members.push({ stmt, call, creation });
|
|
3781
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
3782
|
+
collectPatternNames(stmt.declarations[0].id, run.names);
|
|
3783
|
+
}
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
if (
|
|
3787
|
+
run &&
|
|
3788
|
+
stmt &&
|
|
3789
|
+
stmt.type === 'VariableDeclaration' &&
|
|
3790
|
+
(stmt.kind === 'const' || stmt.kind === 'let')
|
|
3791
|
+
) {
|
|
3792
|
+
// Interleaved declaration: allowed in a run, but its bindings join
|
|
3793
|
+
// the no-hoist-past set (a later creation referencing them cannot
|
|
3794
|
+
// hoist above this statement).
|
|
3795
|
+
run.members.push({ stmt });
|
|
3796
|
+
for (const d of stmt.declarations || []) collectPatternNames(d.id, run.names);
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
flush();
|
|
3800
|
+
// Recurse into conditional blocks so a guarded use() run batches
|
|
3801
|
+
// within its own block (loops/functions stay untouched).
|
|
3802
|
+
if (stmt && stmt.type === 'IfStatement') {
|
|
3803
|
+
out.push({
|
|
3804
|
+
...stmt,
|
|
3805
|
+
consequent: transformStmtBlock(stmt.consequent),
|
|
3806
|
+
alternate: stmt.alternate ? transformStmtBlock(stmt.alternate) : stmt.alternate,
|
|
3807
|
+
});
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
if (stmt && stmt.type === 'BlockStatement') {
|
|
3811
|
+
out.push({ ...stmt, body: transformList(stmt.body) });
|
|
3812
|
+
continue;
|
|
3813
|
+
}
|
|
3814
|
+
out.push(stmt);
|
|
3815
|
+
}
|
|
3816
|
+
flush();
|
|
3817
|
+
return out;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
function transformStmtBlock(s) {
|
|
3821
|
+
if (!s) return s;
|
|
3822
|
+
if (s.type === 'BlockStatement') return { ...s, body: transformList(s.body) };
|
|
3823
|
+
return s;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
function emitRun(run, out) {
|
|
3827
|
+
const uses = run.members.filter((m) => m.call);
|
|
3828
|
+
if (uses.length === 0) {
|
|
3829
|
+
for (const m of run.members) out.push(m.stmt);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
// Hoist each creation into a temp, batch, then re-emit the original
|
|
3833
|
+
// statements with creations swapped for the temps. Unwrap order (and
|
|
3834
|
+
// hydration-seed order with it) is untouched.
|
|
3835
|
+
const temps = [];
|
|
3836
|
+
const tempOf = new Map();
|
|
3837
|
+
for (const m of uses) {
|
|
3838
|
+
const name = `__pu$${ctx.nextPuId++}`;
|
|
3839
|
+
temps.push({ name, init: m.creation });
|
|
3840
|
+
tempOf.set(m.call, name);
|
|
3841
|
+
}
|
|
3842
|
+
for (const t of temps) {
|
|
3843
|
+
out.push({
|
|
3844
|
+
type: 'VariableDeclaration',
|
|
3845
|
+
kind: 'const',
|
|
3846
|
+
declarations: [
|
|
3847
|
+
{
|
|
3848
|
+
type: 'VariableDeclarator',
|
|
3849
|
+
id: { type: 'Identifier', name: t.name },
|
|
3850
|
+
init: t.init,
|
|
3851
|
+
},
|
|
3852
|
+
],
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
// Server mirror: `puBatch` registers every unresolved thenable of the run
|
|
3856
|
+
// with the render loop and suspends ONCE (identity-resolved on the next
|
|
3857
|
+
// pass — see runtime.server.ts).
|
|
3858
|
+
const batchHelper = ctx.mode === 'server' ? 'puBatch' : 'useBatch';
|
|
3859
|
+
ctx.runtimeNeeded.add(batchHelper);
|
|
3860
|
+
const batchArgs = [
|
|
3861
|
+
{
|
|
3862
|
+
type: 'ArrayExpression',
|
|
3863
|
+
elements: temps.map((t) => ({ type: 'Identifier', name: t.name })),
|
|
3864
|
+
},
|
|
3865
|
+
];
|
|
3866
|
+
if (firstBatch && warmThunk) batchArgs.push(warmThunk);
|
|
3867
|
+
firstBatch = false;
|
|
3868
|
+
out.push({
|
|
3869
|
+
type: 'ExpressionStatement',
|
|
3870
|
+
expression: {
|
|
3871
|
+
type: 'CallExpression',
|
|
3872
|
+
callee: { type: 'Identifier', name: `_$${batchHelper}` },
|
|
3873
|
+
arguments: batchArgs,
|
|
3874
|
+
optional: false,
|
|
3875
|
+
},
|
|
3876
|
+
});
|
|
3877
|
+
for (const m of run.members) {
|
|
3878
|
+
if (!m.call) {
|
|
3879
|
+
out.push(m.stmt);
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
const tempId = { type: 'Identifier', name: tempOf.get(m.call) };
|
|
3883
|
+
const newCall = { ...m.call, arguments: [tempId, ...m.call.arguments.slice(1)] };
|
|
3884
|
+
if (m.stmt.type === 'VariableDeclaration') {
|
|
3885
|
+
out.push({
|
|
3886
|
+
...m.stmt,
|
|
3887
|
+
declarations: [{ ...m.stmt.declarations[0], init: newCall }],
|
|
3888
|
+
});
|
|
3889
|
+
} else {
|
|
3890
|
+
out.push({ ...m.stmt, expression: newCall });
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
2726
3893
|
}
|
|
2727
|
-
|
|
2728
|
-
// Hoisted `<title>`/`<meta>`/`<link>` → headBlock into document.head
|
|
2729
|
-
// (out-of-band; re-applied each render for reactivity, removed on unmount).
|
|
2730
|
-
if (plan.head) lines.push(plan.head);
|
|
3894
|
+
}
|
|
2731
3895
|
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
const
|
|
2736
|
-
|
|
3896
|
+
// Build the warm thunk AST (child warm calls for the first in-body batch) +
|
|
3897
|
+
// the `Comp.__warm` source (creations + child calls) for a top-level body.
|
|
3898
|
+
function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
|
|
3899
|
+
const paramNames = new Set();
|
|
3900
|
+
for (const p of node.params || []) collectPatternNames(p, paramNames);
|
|
3901
|
+
const locals = ctx.currentComponentLocals;
|
|
3902
|
+
|
|
3903
|
+
const guardOk = (guards, armLocals) =>
|
|
3904
|
+
guards.every((g) => isWarmSafeExpr(g, paramNames, locals, armLocals));
|
|
3905
|
+
const andChain = (guards) =>
|
|
3906
|
+
guards.length === 0
|
|
3907
|
+
? null
|
|
3908
|
+
: guards.reduce(
|
|
3909
|
+
(acc, g) =>
|
|
3910
|
+
acc ? { type: 'LogicalExpression', operator: '&&', left: acc, right: g } : g,
|
|
3911
|
+
null,
|
|
3912
|
+
);
|
|
3913
|
+
|
|
3914
|
+
const warmMemos = creations.filter(
|
|
3915
|
+
(c) => guardOk(c.guards, c.locals) && isWarmSafeExpr(c.expr, paramNames, locals, c.locals),
|
|
3916
|
+
);
|
|
3917
|
+
const warmKids = warmChildren.filter(
|
|
3918
|
+
(w) =>
|
|
3919
|
+
guardOk(w.guards, w.locals) &&
|
|
3920
|
+
!paramNames.has(w.compName) &&
|
|
3921
|
+
!(locals && locals.has(w.compName)) &&
|
|
3922
|
+
!(w.locals && w.locals.has(w.compName)) &&
|
|
3923
|
+
w.props.every((p) => isWarmSafeExpr(p.value, paramNames, locals, w.locals)),
|
|
3924
|
+
);
|
|
3925
|
+
if (warmKids.length === 0 && warmMemos.length === 0) return { thunk: null, warmSrc: null };
|
|
3926
|
+
|
|
3927
|
+
const stmtFor = (guards, callExpr) => {
|
|
3928
|
+
const g = andChain(guards);
|
|
3929
|
+
const call = { type: 'ExpressionStatement', expression: callExpr };
|
|
3930
|
+
return g ? { type: 'IfStatement', test: g, consequent: call, alternate: null } : call;
|
|
3931
|
+
};
|
|
3932
|
+
const memoCall = (c) => ({
|
|
3933
|
+
type: 'CallExpression',
|
|
3934
|
+
callee: { type: 'Identifier', name: '_$warmMemo' },
|
|
3935
|
+
arguments: [
|
|
3936
|
+
{ type: 'ArrowFunctionExpression', params: [], expression: true, async: false, body: c.expr },
|
|
3937
|
+
{ type: 'ArrayExpression', elements: c.deps },
|
|
3938
|
+
{ type: 'Identifier', name: c.symVar },
|
|
3939
|
+
],
|
|
3940
|
+
optional: false,
|
|
3941
|
+
});
|
|
3942
|
+
const childCall = (w) => ({
|
|
3943
|
+
type: 'CallExpression',
|
|
3944
|
+
callee: { type: 'Identifier', name: '_$warmChild' },
|
|
3945
|
+
arguments: [
|
|
3946
|
+
{ type: 'Identifier', name: w.compName },
|
|
3947
|
+
{
|
|
3948
|
+
type: 'ObjectExpression',
|
|
3949
|
+
properties: w.props.map((p) => ({
|
|
3950
|
+
type: 'Property',
|
|
3951
|
+
key: { type: 'Identifier', name: p.key },
|
|
3952
|
+
value: p.value,
|
|
3953
|
+
kind: 'init',
|
|
3954
|
+
method: false,
|
|
3955
|
+
shorthand: false,
|
|
3956
|
+
computed: false,
|
|
3957
|
+
})),
|
|
3958
|
+
},
|
|
3959
|
+
],
|
|
3960
|
+
optional: false,
|
|
3961
|
+
});
|
|
3962
|
+
|
|
3963
|
+
if (warmMemos.length > 0) ctx.runtimeNeeded.add('warmMemo');
|
|
3964
|
+
if (warmKids.length > 0) ctx.runtimeNeeded.add('warmChild');
|
|
3965
|
+
|
|
3966
|
+
// In-body warm thunk: children only — the body's own creations already ran
|
|
3967
|
+
// as real memos by the time the batch throws.
|
|
3968
|
+
const thunk =
|
|
3969
|
+
warmKids.length === 0
|
|
3970
|
+
? null
|
|
3971
|
+
: {
|
|
3972
|
+
type: 'ArrowFunctionExpression',
|
|
3973
|
+
params: [],
|
|
3974
|
+
expression: false,
|
|
3975
|
+
async: false,
|
|
3976
|
+
body: {
|
|
3977
|
+
type: 'BlockStatement',
|
|
3978
|
+
body: warmKids.map((w) => stmtFor(w.guards, childCall(w))),
|
|
3979
|
+
},
|
|
3980
|
+
};
|
|
3981
|
+
|
|
3982
|
+
// The hoisted fetch plan: creations + child calls, params destructured
|
|
3983
|
+
// from the incoming props object. Single-param components only (the norm).
|
|
3984
|
+
// Returned as a bare arrow — compileComponent attaches it to the INNER
|
|
3985
|
+
// function object via Object.assign, so the component's own body (where
|
|
3986
|
+
// the function-expression name shadows the module const) sees it too;
|
|
3987
|
+
// hmr() forwards it from the wrapped fn onto the HMR wrapper.
|
|
3988
|
+
let warmSrc = null;
|
|
3989
|
+
if ((node.params || []).length <= 1 && (warmMemos.length > 0 || warmKids.length > 0)) {
|
|
3990
|
+
const bodyStmts = [
|
|
3991
|
+
...warmMemos.map((c) => stmtFor(c.guards, memoCall(c))),
|
|
3992
|
+
...warmKids.map((w) => stmtFor(w.guards, childCall(w))),
|
|
3993
|
+
];
|
|
3994
|
+
const destructure =
|
|
3995
|
+
node.params.length === 1 ? `\tconst ${printNode(node.params[0])} = __wp;\n` : '';
|
|
3996
|
+
const body = bodyStmts.map((s) => '\t' + printNode(s).replace(/\n/g, '\n\t')).join('\n');
|
|
3997
|
+
warmSrc = `(__wp) => {\n${destructure}${body}\n}`;
|
|
3998
|
+
}
|
|
3999
|
+
return { thunk, warmSrc };
|
|
2737
4000
|
}
|
|
2738
4001
|
|
|
2739
4002
|
// ===========================================================================
|
|
2740
4003
|
// Hook-call rewriting
|
|
2741
4004
|
// ===========================================================================
|
|
2742
4005
|
|
|
4006
|
+
// Plain JS loop statements (display word for the diagnostic). A TEMPLATE `@for`
|
|
4007
|
+
// also parses to a ForOfStatement — told apart by its JSX body, the same
|
|
4008
|
+
// classification isJsxNode uses — and is the SUPPORTED way to loop hooks: each
|
|
4009
|
+
// item renders in its own block scope (mountItem → renderBlock swaps
|
|
4010
|
+
// CURRENT_SCOPE per item), so per-item hooks get per-item state.
|
|
4011
|
+
const JS_LOOP_WORD = {
|
|
4012
|
+
ForStatement: 'for',
|
|
4013
|
+
ForInStatement: 'for…in',
|
|
4014
|
+
ForOfStatement: 'for…of',
|
|
4015
|
+
WhileStatement: 'while',
|
|
4016
|
+
DoWhileStatement: 'do…while',
|
|
4017
|
+
};
|
|
4018
|
+
|
|
4019
|
+
// A SLOT-KEYED hook call: a builtin base hook, or a custom hook by the
|
|
4020
|
+
// `use[A-Z]` convention (identifier or method form) — the same match
|
|
4021
|
+
// rewriteHookCalls slots below. `useContext` (keyed by context identity) and
|
|
4022
|
+
// bare `use` (keyed by per-render call order — `block.__thenableIdx` on the
|
|
4023
|
+
// client, the frame occurrence counter in runtime.server.ts) are NOT
|
|
4024
|
+
// slot-keyed, so they genuinely work in loops and are exempt.
|
|
4025
|
+
function slotKeyedHookName(n) {
|
|
4026
|
+
if (n.type !== 'CallExpression') return null;
|
|
4027
|
+
if (n.callee.type === 'Identifier') {
|
|
4028
|
+
const name = n.callee.name;
|
|
4029
|
+
if (HOOK_NAMES.has(name)) return name;
|
|
4030
|
+
if (/^use[A-Z]/.test(name) && name !== 'useContext') return name;
|
|
4031
|
+
return null;
|
|
4032
|
+
}
|
|
4033
|
+
if (
|
|
4034
|
+
!n.optional &&
|
|
4035
|
+
n.callee.type === 'MemberExpression' &&
|
|
4036
|
+
!n.callee.computed &&
|
|
4037
|
+
n.callee.property.type === 'Identifier' &&
|
|
4038
|
+
/^use[A-Z]/.test(n.callee.property.name) &&
|
|
4039
|
+
n.callee.property.name !== 'useContext'
|
|
4040
|
+
) {
|
|
4041
|
+
return n.callee.property.name;
|
|
4042
|
+
}
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// REJECT a slot-keyed hook lexically inside a plain JS loop. Hooks are keyed by
|
|
4047
|
+
// a per-call-site symbol, so every iteration of the loop would hit the ONE slot
|
|
4048
|
+
// assigned to that call site: useState/useReducer would share a single state
|
|
4049
|
+
// cell across all iterations, useMemo would recompute every iteration (each
|
|
4050
|
+
// iteration's deps overwrite the previous entry, only the last survives), and
|
|
4051
|
+
// slot-keyed effects would collide the same way — all silently. The scan skips
|
|
4052
|
+
// ONLY nested DEFERRED function boundaries (a function declared in the loop may
|
|
4053
|
+
// be a local component or a deferred callback — each component instance gets
|
|
4054
|
+
// its own scope). A function that provably EXECUTES during the iteration is
|
|
4055
|
+
// walked into: an IIFE callee (incl. `.call`/`.apply` on a function
|
|
4056
|
+
// expression) and an inline callback to a known synchronous array-iteration
|
|
4057
|
+
// method (`.map`, `.forEach`, …) — its hooks run once per iteration and
|
|
4058
|
+
// collide exactly like inline ones. Directive constructs inside the loop
|
|
4059
|
+
// subtree are NOT exempt either: their bodies do compile to helper render fns,
|
|
4060
|
+
// but the construct's own per-call-site slot (ifBlock/forBlock state on
|
|
4061
|
+
// `scope.slots`) repeats each iteration exactly like a hook slot does, so
|
|
4062
|
+
// hooks reached through them collide all the same. Every LEGIT template
|
|
4063
|
+
// directive is protected before the scan starts — the guard in
|
|
4064
|
+
// rewriteHookCalls fires only on plain-JS loop statements, and a template
|
|
4065
|
+
// `@for` (ForOfStatement with a JSX body) is excluded there, so hooks in
|
|
4066
|
+
// template positions (including `.map()`-to-`@for` children) never reach this
|
|
4067
|
+
// walker.
|
|
4068
|
+
const SYNC_ITERATION_METHODS = new Set([
|
|
4069
|
+
'map',
|
|
4070
|
+
'forEach',
|
|
4071
|
+
'filter',
|
|
4072
|
+
'flatMap',
|
|
4073
|
+
'reduce',
|
|
4074
|
+
'reduceRight',
|
|
4075
|
+
'some',
|
|
4076
|
+
'every',
|
|
4077
|
+
'find',
|
|
4078
|
+
'findIndex',
|
|
4079
|
+
'findLast',
|
|
4080
|
+
'findLastIndex',
|
|
4081
|
+
'sort',
|
|
4082
|
+
'toSorted',
|
|
4083
|
+
]);
|
|
4084
|
+
function isFunctionNode(n) {
|
|
4085
|
+
return (
|
|
4086
|
+
n &&
|
|
4087
|
+
(n.type === 'FunctionDeclaration' ||
|
|
4088
|
+
n.type === 'FunctionExpression' ||
|
|
4089
|
+
n.type === 'ArrowFunctionExpression')
|
|
4090
|
+
);
|
|
4091
|
+
}
|
|
4092
|
+
function rejectHookInJsLoop(loop, ctx, componentName) {
|
|
4093
|
+
const seen = new WeakSet();
|
|
4094
|
+
// Function nodes that run during the loop iteration itself — marked when
|
|
4095
|
+
// their enclosing CallExpression is visited (parents visit before children),
|
|
4096
|
+
// so the boundary check below can let the walk continue into their bodies.
|
|
4097
|
+
const syncInvoked = new WeakSet();
|
|
4098
|
+
function walk(n) {
|
|
4099
|
+
if (!n) return;
|
|
4100
|
+
if (Array.isArray(n)) {
|
|
4101
|
+
for (const x of n) walk(x);
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
if (typeof n !== 'object') return;
|
|
4105
|
+
const t = n.type;
|
|
4106
|
+
if (!t || seen.has(n)) return;
|
|
4107
|
+
seen.add(n);
|
|
4108
|
+
if (t === 'CallExpression') {
|
|
4109
|
+
// IIFE: `(() => …)()` — the callee body executes in place.
|
|
4110
|
+
if (isFunctionNode(n.callee)) syncInvoked.add(n.callee);
|
|
4111
|
+
if (n.callee.type === 'MemberExpression' && !n.callee.computed) {
|
|
4112
|
+
const prop = n.callee.property;
|
|
4113
|
+
// `(function () { … }).call(this)` / `.apply(...)` — IIFE variants.
|
|
4114
|
+
if (
|
|
4115
|
+
prop.type === 'Identifier' &&
|
|
4116
|
+
(prop.name === 'call' || prop.name === 'apply') &&
|
|
4117
|
+
isFunctionNode(n.callee.object)
|
|
4118
|
+
) {
|
|
4119
|
+
syncInvoked.add(n.callee.object);
|
|
4120
|
+
}
|
|
4121
|
+
// `xs.map(cb)` and friends — the callback runs synchronously, once
|
|
4122
|
+
// per element, during this iteration. (Template-position `.map()`
|
|
4123
|
+
// children never sit inside a plain JS loop, so the legit
|
|
4124
|
+
// map-to-`@for` pattern can't reach here.)
|
|
4125
|
+
if (prop.type === 'Identifier' && SYNC_ITERATION_METHODS.has(prop.name)) {
|
|
4126
|
+
for (const a of n.arguments) if (isFunctionNode(a)) syncInvoked.add(a);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
if (isFunctionNode(n) && !syncInvoked.has(n)) return;
|
|
4131
|
+
const hook = slotKeyedHookName(n);
|
|
4132
|
+
if (hook !== null) {
|
|
4133
|
+
const l = (n.loc && n.loc.start) || (loop.loc && loop.loc.start);
|
|
4134
|
+
const at = l
|
|
4135
|
+
? ` (${ctx.mapSourceName ? ctx.mapSourceName + ':' : ''}${l.line}:${l.column})`
|
|
4136
|
+
: '';
|
|
4137
|
+
throw new Error(
|
|
4138
|
+
`\`${hook}\` is called inside a \`${JS_LOOP_WORD[loop.type]}\` loop in ${componentName}. ` +
|
|
4139
|
+
`Hooks are keyed by call site, so every iteration would share this call site's ONE ` +
|
|
4140
|
+
`hook slot — state, memo, and effect entries would silently collide across ` +
|
|
4141
|
+
`iterations. Loop in the template with the keyed \`@for\` directive instead (each ` +
|
|
4142
|
+
`item renders in its own scope, so per-item hooks get per-item state), or extract ` +
|
|
4143
|
+
`the loop body into a child component. \`use()\` and \`useContext\` are exempt ` +
|
|
4144
|
+
`(call-order / context-identity keyed, not slot-keyed).${at}`,
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
for (const key in n) {
|
|
4148
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4149
|
+
walk(n[key]);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
walk(loop);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
2743
4155
|
function rewriteHookCalls(node, ctx, componentName) {
|
|
2744
4156
|
return mapAst(node, (n) => {
|
|
4157
|
+
// A plain JS loop must not contain slot-keyed hook calls — reject before
|
|
4158
|
+
// slotting. (A template `@for` is also a ForOfStatement; its JSX body tells
|
|
4159
|
+
// it apart, and it is the supported way to loop hooks.)
|
|
4160
|
+
if (
|
|
4161
|
+
JS_LOOP_WORD[n.type] !== undefined &&
|
|
4162
|
+
!(n.type === 'ForOfStatement' && bodyContainsJsx(n.body))
|
|
4163
|
+
) {
|
|
4164
|
+
rejectHookInJsLoop(n, ctx, componentName);
|
|
4165
|
+
}
|
|
2745
4166
|
if (n.type === 'CallExpression' && n.callee.type === 'Identifier') {
|
|
2746
4167
|
const name = n.callee.name;
|
|
2747
4168
|
// Three kinds of call get a trailing per-call-site slot symbol:
|
|
@@ -2756,7 +4177,8 @@ function rewriteHookCalls(node, ctx, componentName) {
|
|
|
2756
4177
|
// (or the same hook twice) stay independent.
|
|
2757
4178
|
//
|
|
2758
4179
|
// NB: a `use*` NAME is reserved for hooks (React's convention) — a non-hook
|
|
2759
|
-
// function named like one gets a harmless extra trailing argument
|
|
4180
|
+
// function named like one gets a harmless extra trailing argument (though
|
|
4181
|
+
// inside a plain JS loop the convention is enforced: rejectHookInJsLoop).
|
|
2760
4182
|
const isBuiltin = HOOK_NAMES.has(name);
|
|
2761
4183
|
const isCustom = /^use[A-Z]/.test(name) && name !== 'useContext';
|
|
2762
4184
|
const isServerUse = name === 'use' && ctx.mode === 'server';
|
|
@@ -2811,6 +4233,49 @@ function rewriteHookCalls(node, ctx, componentName) {
|
|
|
2811
4233
|
};
|
|
2812
4234
|
}
|
|
2813
4235
|
}
|
|
4236
|
+
// METHOD-style custom hooks — `route.useLoaderData()`, `api.useSearch()`
|
|
4237
|
+
// (React-ecosystem object-carried hooks: TanStack Route/RouteApi accessors
|
|
4238
|
+
// and the like). Same `use[A-Z]` convention, applied to the PROPERTY name.
|
|
4239
|
+
// The callee is a member access, so the plain withSlot form would sever
|
|
4240
|
+
// `this`; instead the WHOLE call is wrapped in a thunk —
|
|
4241
|
+
// `_$withSlot(sym, () => obj.useX(...args, sym))` — which pushes the
|
|
4242
|
+
// call-site path symbol for the hook's inner base hooks while the trailing
|
|
4243
|
+
// `sym` still reaches slot-forwarding bindings (splitSlot convention).
|
|
4244
|
+
if (
|
|
4245
|
+
n.type === 'CallExpression' &&
|
|
4246
|
+
!n.optional &&
|
|
4247
|
+
n.callee.type === 'MemberExpression' &&
|
|
4248
|
+
!n.callee.computed &&
|
|
4249
|
+
n.callee.property.type === 'Identifier' &&
|
|
4250
|
+
/^use[A-Z]/.test(n.callee.property.name) &&
|
|
4251
|
+
n.callee.property.name !== 'useContext'
|
|
4252
|
+
) {
|
|
4253
|
+
const debug = `${componentName}.${n.callee.property.name}#${ctx.nextHookSymId}`;
|
|
4254
|
+
const symVar = allocHookSymbol(ctx, debug);
|
|
4255
|
+
ctx.runtimeNeeded.add('withSlot');
|
|
4256
|
+
const object = rewriteHookCalls(n.callee.object, ctx, componentName);
|
|
4257
|
+
const args = n.arguments.map((a) => rewriteHookCalls(a, ctx, componentName));
|
|
4258
|
+
return {
|
|
4259
|
+
type: 'CallExpression',
|
|
4260
|
+
callee: { type: 'Identifier', name: '_$withSlot' },
|
|
4261
|
+
arguments: [
|
|
4262
|
+
{ type: 'Identifier', name: symVar },
|
|
4263
|
+
{
|
|
4264
|
+
type: 'ArrowFunctionExpression',
|
|
4265
|
+
params: [],
|
|
4266
|
+
expression: true,
|
|
4267
|
+
async: false,
|
|
4268
|
+
body: {
|
|
4269
|
+
type: 'CallExpression',
|
|
4270
|
+
callee: { ...n.callee, object },
|
|
4271
|
+
arguments: [...args, { type: 'Identifier', name: symVar }],
|
|
4272
|
+
optional: false,
|
|
4273
|
+
},
|
|
4274
|
+
},
|
|
4275
|
+
],
|
|
4276
|
+
optional: false,
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
2814
4279
|
return null;
|
|
2815
4280
|
});
|
|
2816
4281
|
}
|
|
@@ -2845,14 +4310,44 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
2845
4310
|
generator: false,
|
|
2846
4311
|
body: { type: 'BlockStatement', body: newStatements },
|
|
2847
4312
|
};
|
|
2848
|
-
|
|
4313
|
+
// Print with esrap's real per-token map (same output bytes as printNode) so
|
|
4314
|
+
// this function contributes segments to the module map — compile() drains
|
|
4315
|
+
// them via ctx._setupMaps, exactly like a component's setup statements.
|
|
4316
|
+
// Chained consumers (e.g. @octanejs/mdx's two-stage .mdx map) need segments
|
|
4317
|
+
// on these lines; a map-less print would compose to an empty chain.
|
|
4318
|
+
const printed = printNodeWithMap(fn, ctx);
|
|
4319
|
+
let code = printed.code;
|
|
4320
|
+
const mappings = printed.mappings;
|
|
2849
4321
|
if (compInlinedSubs.length) {
|
|
2850
4322
|
// Helpers are hoisted function declarations → position-independent; splice them
|
|
2851
4323
|
// in right after the function's opening `{` so they're in the component scope.
|
|
2852
4324
|
const i = code.indexOf('{');
|
|
2853
4325
|
const subs = compInlinedSubs.map((s) => ' ' + s.replace(/\n/g, '\n ')).join('\n');
|
|
4326
|
+
// The splice inserts `'\n' + subs` after the `{`: every printed line below
|
|
4327
|
+
// the splice line shifts down by the inserted line count. Keep the map in
|
|
4328
|
+
// sync by inserting that many empty mapping lines at the same point.
|
|
4329
|
+
// Decoded rows are dense up to the LAST line with segments, so when the
|
|
4330
|
+
// array ends at (or before) the splice line, every shifted line has no
|
|
4331
|
+
// segments and there is nothing to realign — skip the padding rather than
|
|
4332
|
+
// append useless empty rows.
|
|
4333
|
+
const spliceLine = countNewlines(code.slice(0, i + 1));
|
|
4334
|
+
const inserted = 1 + countNewlines(subs);
|
|
4335
|
+
if (mappings.length > spliceLine + 1) {
|
|
4336
|
+
mappings.splice(spliceLine + 1, 0, ...Array.from({ length: inserted }, () => []));
|
|
4337
|
+
}
|
|
2854
4338
|
code = code.slice(0, i + 1) + '\n' + subs + code.slice(i + 1);
|
|
2855
4339
|
}
|
|
4340
|
+
// Thread the mappings back to compile() through the same side-channel the
|
|
4341
|
+
// component path uses (drained by drainSetupMaps at the emit site). The
|
|
4342
|
+
// `export ` prefix shifts only line 0's columns; `export default` appends a
|
|
4343
|
+
// trailing (unmapped) line and shifts nothing.
|
|
4344
|
+
ctx._setupMaps =
|
|
4345
|
+
options && options.export
|
|
4346
|
+
? [
|
|
4347
|
+
{ fnRelLine: 0, colShift: 'export '.length, mappings: mappings.slice(0, 1) },
|
|
4348
|
+
{ fnRelLine: 1, colShift: 0, mappings: mappings.slice(1) },
|
|
4349
|
+
]
|
|
4350
|
+
: [{ fnRelLine: 0, colShift: 0, mappings }];
|
|
2856
4351
|
if (options && options.default) return `${code}\nexport default ${name};`;
|
|
2857
4352
|
if (options && options.export) return `export ${code}`;
|
|
2858
4353
|
return code;
|
|
@@ -3039,6 +4534,19 @@ function extractFragment(node, ctx, holeProps) {
|
|
|
3039
4534
|
ic.condExpr = `props.${condHole}`;
|
|
3040
4535
|
ic.thenHelper = `props.${thenHole}`;
|
|
3041
4536
|
ic.elseHelper = elseHoleName ? `props.${elseHoleName}` : null;
|
|
4537
|
+
// Phase 2: the hoisted helpers' env values are COMPONENT-scope
|
|
4538
|
+
// identifiers — thread the whole tuple as one array hole so the
|
|
4539
|
+
// renderer-side call passes current values (same as deps for @for).
|
|
4540
|
+
if (ic.envNames && ic.envNames.length) {
|
|
4541
|
+
const envHole = `h${holeProps.length}`;
|
|
4542
|
+
holeProps.push(
|
|
4543
|
+
objectProp(envHole, {
|
|
4544
|
+
type: 'ArrayExpression',
|
|
4545
|
+
elements: ic.envNames.map((n) => ({ type: 'Identifier', name: n })),
|
|
4546
|
+
}),
|
|
4547
|
+
);
|
|
4548
|
+
ic.envExpr = `props.${envHole}`;
|
|
4549
|
+
}
|
|
3042
4550
|
fc.directiveCalls.ifCalls.push(ic);
|
|
3043
4551
|
newChildren.push({
|
|
3044
4552
|
type: 'FoldedDirective',
|
|
@@ -3079,9 +4587,10 @@ function extractFragment(node, ctx, holeProps) {
|
|
|
3079
4587
|
holeProps.push(objectProp(emptyHole, { type: 'Identifier', name: rec.emptyHelper }));
|
|
3080
4588
|
rec.emptyHelper = `props.${emptyHole}`;
|
|
3081
4589
|
}
|
|
3082
|
-
if (rec.
|
|
4590
|
+
if (rec.depNames.length) {
|
|
3083
4591
|
// Thread each dep value as its own hole so the reconciler's deps-equality
|
|
3084
|
-
// check
|
|
4592
|
+
// check (and the Phase 2 env stamp — deps doubles as the helpers' env
|
|
4593
|
+
// tuple) sees the component-scope values, not undefined renderer locals.
|
|
3085
4594
|
rec.depNames = rec.depNames.map((dn) => {
|
|
3086
4595
|
const depHole = `h${holeProps.length}`;
|
|
3087
4596
|
holeProps.push(objectProp(depHole, { type: 'Identifier', name: dn }));
|
|
@@ -3129,6 +4638,17 @@ function extractFragment(node, ctx, holeProps) {
|
|
|
3129
4638
|
holeProps.push(objectProp(defHole, { type: 'Identifier', name: rec.defaultHelper }));
|
|
3130
4639
|
rec.defaultHelper = `props.${defHole}`;
|
|
3131
4640
|
}
|
|
4641
|
+
// Phase 2: env tuple hole (see the @if fold above).
|
|
4642
|
+
if (rec.envNames && rec.envNames.length) {
|
|
4643
|
+
const envHole = `h${holeProps.length}`;
|
|
4644
|
+
holeProps.push(
|
|
4645
|
+
objectProp(envHole, {
|
|
4646
|
+
type: 'ArrayExpression',
|
|
4647
|
+
elements: rec.envNames.map((n) => ({ type: 'Identifier', name: n })),
|
|
4648
|
+
}),
|
|
4649
|
+
);
|
|
4650
|
+
rec.envExpr = `props.${envHole}`;
|
|
4651
|
+
}
|
|
3132
4652
|
fc.directiveCalls.switchCalls.push(rec);
|
|
3133
4653
|
newChildren.push({
|
|
3134
4654
|
type: 'FoldedDirective',
|
|
@@ -3165,6 +4685,17 @@ function extractFragment(node, ctx, holeProps) {
|
|
|
3165
4685
|
holeProps.push(objectProp(pendHole, { type: 'Identifier', name: rec.pendingHelper }));
|
|
3166
4686
|
rec.pendingHelper = `props.${pendHole}`;
|
|
3167
4687
|
}
|
|
4688
|
+
// Phase 2: env tuple hole (see the @if fold above).
|
|
4689
|
+
if (rec.envNames && rec.envNames.length) {
|
|
4690
|
+
const envHole = `h${holeProps.length}`;
|
|
4691
|
+
holeProps.push(
|
|
4692
|
+
objectProp(envHole, {
|
|
4693
|
+
type: 'ArrayExpression',
|
|
4694
|
+
elements: rec.envNames.map((n) => ({ type: 'Identifier', name: n })),
|
|
4695
|
+
}),
|
|
4696
|
+
);
|
|
4697
|
+
rec.envExpr = `props.${envHole}`;
|
|
4698
|
+
}
|
|
3168
4699
|
fc.directiveCalls.tryCalls.push(rec);
|
|
3169
4700
|
newChildren.push({
|
|
3170
4701
|
type: 'FoldedDirective',
|
|
@@ -3331,7 +4862,18 @@ function lowerJsxChild(child, ctx) {
|
|
|
3331
4862
|
const e = lowerJsxChild(c, ctx);
|
|
3332
4863
|
if (e !== null) els.push(e);
|
|
3333
4864
|
}
|
|
3334
|
-
|
|
4865
|
+
// A fragment's children are FIXED siblings (React's "static children" —
|
|
4866
|
+
// `jsxs` — which React never key-warns): tag the array so the de-opt list
|
|
4867
|
+
// keys it by index silently, reserving the missing-key warning for
|
|
4868
|
+
// runtime-built arrays (`.map()` results). Emitted in BOTH modes — the
|
|
4869
|
+
// server export is the identity (`ssrChild` just renders the array).
|
|
4870
|
+
ctx.runtimeNeeded.add('positionalChildren');
|
|
4871
|
+
return {
|
|
4872
|
+
type: 'CallExpression',
|
|
4873
|
+
callee: { type: 'Identifier', name: rtAlias('positionalChildren') },
|
|
4874
|
+
arguments: [{ type: 'ArrayExpression', elements: els }],
|
|
4875
|
+
optional: false,
|
|
4876
|
+
};
|
|
3335
4877
|
}
|
|
3336
4878
|
return null; // Comment / unknown — drop.
|
|
3337
4879
|
}
|
|
@@ -3417,17 +4959,42 @@ function jsxElementToCreateElement(node, ctx) {
|
|
|
3417
4959
|
};
|
|
3418
4960
|
}
|
|
3419
4961
|
|
|
4962
|
+
// Short, unique, path-free slot description for non-HMR output: a djb2 hash of
|
|
4963
|
+
// the module filename + the per-module hook index. The DESCRIPTION is
|
|
4964
|
+
// load-bearing beyond debugging: resolveSlot/currentPathSlot (runtime) compose
|
|
4965
|
+
// a base hook's effective slot inside custom hooks by CONCATENATING slot
|
|
4966
|
+
// descriptions into a Symbol.for key — a bare `Symbol()` (description
|
|
4967
|
+
// undefined) collapses every composed path to "undefined|undefined" and
|
|
4968
|
+
// collides custom-hook state across call sites (broke the router's useStore →
|
|
4969
|
+
// website hydration). So every emitted slot must carry a unique description;
|
|
4970
|
+
// this one is ~10 chars and keeps the absolute module path out of bundles.
|
|
4971
|
+
export function hookSlotHash(filename) {
|
|
4972
|
+
const src = filename || '<anon>';
|
|
4973
|
+
let h = 5381;
|
|
4974
|
+
for (let i = 0; i < src.length; i++) h = (Math.imul(h, 33) + src.charCodeAt(i)) | 0;
|
|
4975
|
+
return (h >>> 0).toString(36);
|
|
4976
|
+
}
|
|
4977
|
+
|
|
3420
4978
|
function allocHookSymbol(ctx, debugName) {
|
|
3421
4979
|
const id = ctx.nextHookSymId++;
|
|
3422
4980
|
const name = `_h$${id}`;
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
4981
|
+
if (ctx.hmr) {
|
|
4982
|
+
// HMR (dev serve): Symbol.for(stableKey) so re-imports produce the SAME
|
|
4983
|
+
// Symbol identity, which keeps the existing hooks Map keys valid across
|
|
4984
|
+
// body swaps. The stable key embeds the source filename so symbols don't
|
|
4985
|
+
// collide across modules. `debugName` includes the component name + hook
|
|
4986
|
+
// name + call-site index — stable provided the user doesn't reorder hooks
|
|
4987
|
+
// between renders (which would violate React's rules anyway).
|
|
4988
|
+
const stableKey = `octane:${ctx.filename || '<anon>'}:${debugName}`;
|
|
4989
|
+
ctx.hoistedHelpers.push(`const ${name} = Symbol.for(${JSON.stringify(stableKey)});`);
|
|
4990
|
+
} else {
|
|
4991
|
+
// No HMR (prod builds, SSR, tests): nothing re-imports the module
|
|
4992
|
+
// expecting registry identity, so a plain Symbol suffices — but it MUST
|
|
4993
|
+
// carry a unique description (see hookSlotHash above). ~10 chars vs the
|
|
4994
|
+
// ~100-char registry key, and no file path in shipped bundles.
|
|
4995
|
+
if (ctx._hookHash === undefined) ctx._hookHash = hookSlotHash(ctx.filename);
|
|
4996
|
+
ctx.hoistedHelpers.push(`const ${name} = Symbol(${JSON.stringify(`${ctx._hookHash}#${id}`)});`);
|
|
4997
|
+
}
|
|
3431
4998
|
return name;
|
|
3432
4999
|
}
|
|
3433
5000
|
|
|
@@ -3593,7 +5160,10 @@ function emitHeadServer(headNodes, ctx) {
|
|
|
3593
5160
|
* makeSwitchCall consume
|
|
3594
5161
|
* - Anything else → passed through
|
|
3595
5162
|
*/
|
|
3596
|
-
|
|
5163
|
+
// `inSvg`: the children being normalized sit inside an SVG-namespace subtree.
|
|
5164
|
+
// SVG has its own `<title>` (the accessibility tooltip element) — it must stay
|
|
5165
|
+
// where it is, NOT hoist to document.head (React 19 makes the same exception).
|
|
5166
|
+
function normalizeChildren(nodes, inSvg = false) {
|
|
3597
5167
|
const out = [];
|
|
3598
5168
|
if (!nodes) return out;
|
|
3599
5169
|
for (const n of nodes) {
|
|
@@ -3662,10 +5232,10 @@ function normalizeChildren(nodes) {
|
|
|
3662
5232
|
const refInner =
|
|
3663
5233
|
refVal && refVal.type === 'JSXExpressionContainer' ? refVal.expression : refVal;
|
|
3664
5234
|
out.push({ type: 'FragmentStart', refExpr: refInner });
|
|
3665
|
-
out.push(...normalizeChildren(n.children || []));
|
|
5235
|
+
out.push(...normalizeChildren(n.children || [], inSvg));
|
|
3666
5236
|
out.push({ type: 'FragmentEnd' });
|
|
3667
5237
|
} else {
|
|
3668
|
-
out.push(...normalizeChildren(n.children || []));
|
|
5238
|
+
out.push(...normalizeChildren(n.children || [], inSvg));
|
|
3669
5239
|
}
|
|
3670
5240
|
continue;
|
|
3671
5241
|
}
|
|
@@ -3700,8 +5270,9 @@ function normalizeChildren(nodes) {
|
|
|
3700
5270
|
// `<title>`/`<meta>`/`<link>` → hoist to the document-head channel (NOT
|
|
3701
5271
|
// body DOM). Kept in `out` as a synthetic node so planJsx / ssrCompileBody
|
|
3702
5272
|
// can partition it out and emit it via headBlock (client) / ssrHeadEl
|
|
3703
|
-
// (server). Lifted from wherever they appear in the output
|
|
3704
|
-
|
|
5273
|
+
// (server). Lifted from wherever they appear in the output — EXCEPT an
|
|
5274
|
+
// SVG `<title>`, which is the SVG tooltip element and stays in place.
|
|
5275
|
+
if (isHoistableHeadElementNode(n) && !(inSvg && jsxTagName(n) === 'title')) {
|
|
3705
5276
|
out.push({ type: 'HeadHoist', element: n });
|
|
3706
5277
|
continue;
|
|
3707
5278
|
}
|
|
@@ -3715,7 +5286,7 @@ function normalizeChildren(nodes) {
|
|
|
3715
5286
|
loc: n.loc, // preserve element position for dev hydration LOC (component slots)
|
|
3716
5287
|
});
|
|
3717
5288
|
} else if (n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'JSXFragment') {
|
|
3718
|
-
out.push(...normalizeChildren(n.children || []));
|
|
5289
|
+
out.push(...normalizeChildren(n.children || [], inSvg));
|
|
3719
5290
|
} else if (n.type === 'JSXStyleElement') {
|
|
3720
5291
|
// Drop a `<style>` block at child position — its CSS gets registered
|
|
3721
5292
|
// via the @tsrx/core scoping pipeline (applyCssScoping / applyStyleMap);
|
|
@@ -3787,7 +5358,7 @@ function normalizeChildren(nodes) {
|
|
|
3787
5358
|
if (body.length === 0 && render === null) continue;
|
|
3788
5359
|
if (body.length === 0 && render !== null) {
|
|
3789
5360
|
// Recurse — render is a single JSX node, treat as a sibling child.
|
|
3790
|
-
out.push(...normalizeChildren([render]));
|
|
5361
|
+
out.push(...normalizeChildren([render], inSvg));
|
|
3791
5362
|
} else {
|
|
3792
5363
|
throw new Error(
|
|
3793
5364
|
'`@{ … }` with setup statements is not supported at JSX child position. ' +
|
|
@@ -4010,7 +5581,21 @@ const TS_TYPE_PROPS = [
|
|
|
4010
5581
|
function stripTsOnlyWrappers(node) {
|
|
4011
5582
|
if (node === null || typeof node !== 'object') return node;
|
|
4012
5583
|
if (Array.isArray(node)) {
|
|
4013
|
-
for (let i =
|
|
5584
|
+
for (let i = node.length - 1; i >= 0; i--) {
|
|
5585
|
+
// Type-only STATEMENTS nested below module scope (a `type X = …` or
|
|
5586
|
+
// `interface I {}` inside a function body) never hit the top-level
|
|
5587
|
+
// `ast.body` filter, and stripping their annotations below would
|
|
5588
|
+
// leave esrap an alias with a nulled typeAnnotation (crash) — drop
|
|
5589
|
+
// the whole statement here instead. The matched node types are
|
|
5590
|
+
// statement-only, so pruning from any AST array is safe (sparse
|
|
5591
|
+
// ArrayExpression holes are `null` and isTypeOnlyStatement keeps
|
|
5592
|
+
// them). Checked BEFORE the per-node strip so `declare` is intact.
|
|
5593
|
+
if (isTypeOnlyStatement(node[i])) {
|
|
5594
|
+
node.splice(i, 1);
|
|
5595
|
+
} else {
|
|
5596
|
+
node[i] = stripTsOnlyWrappers(node[i]);
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
4014
5599
|
return node;
|
|
4015
5600
|
}
|
|
4016
5601
|
if (
|
|
@@ -4114,6 +5699,13 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4114
5699
|
// through the wrapper path; HTML-contributing nodes are at `_root.childNodes[i]`.
|
|
4115
5700
|
const single =
|
|
4116
5701
|
jsxNodes.length === 1 && jsxNodes[0].type === 'Element' && !isComponentTag(jsxNodes[0]);
|
|
5702
|
+
// M3 inherit-range: consume the caller's body-form flag ONCE — nested planJsx
|
|
5703
|
+
// runs (directive arm bodies compiled during this walk) must not see it. The
|
|
5704
|
+
// sole-comp-root predicate is shared with the server compile
|
|
5705
|
+
// (inheritSoleCompRoot), so the client stamp and the server pair-skip agree
|
|
5706
|
+
// by construction; the stamped cc emits componentSlot(..., inherit=true).
|
|
5707
|
+
const inheritRoot = ctx._inheritBody === true && inheritSoleCompRoot(jsxNodes, ctx);
|
|
5708
|
+
ctx._inheritBody = false;
|
|
4117
5709
|
// Top-level control-flow directives (@if/@for/@switch/@try/<Activity>). In a
|
|
4118
5710
|
// body that ALSO has static template roots, each construct emits a `<!>`
|
|
4119
5711
|
// anchor at its child index (mirroring the in-element mixed-children path)
|
|
@@ -4179,6 +5771,12 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4179
5771
|
}
|
|
4180
5772
|
}
|
|
4181
5773
|
partsHtml.push(part);
|
|
5774
|
+
// M3 inherit-range: stamp the sole comp-call root's cc. Its own entry is
|
|
5775
|
+
// the LAST one its emitNodeHtml pushed (nested children/prop ccs are
|
|
5776
|
+
// pushed first, before makeCompCall returns to the root push).
|
|
5777
|
+
if (inheritRoot && compCalls.length > 0) {
|
|
5778
|
+
compCalls[compCalls.length - 1].inheritRange = true;
|
|
5779
|
+
}
|
|
4182
5780
|
// Advance the child index only when the node actually contributed template
|
|
4183
5781
|
// HTML (an element / text / `<!>` anchor). Component calls and un-anchored
|
|
4184
5782
|
// constructs contribute none — advancing for them would shift every later
|
|
@@ -4193,15 +5791,19 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4193
5791
|
const noTemplate = html === '';
|
|
4194
5792
|
|
|
4195
5793
|
const mountLines = [];
|
|
4196
|
-
//
|
|
4197
|
-
//
|
|
4198
|
-
//
|
|
4199
|
-
//
|
|
4200
|
-
//
|
|
4201
|
-
//
|
|
4202
|
-
//
|
|
4203
|
-
//
|
|
4204
|
-
|
|
5794
|
+
// The binding bag is allocated in ONE shot at the very END of the mount path
|
|
5795
|
+
// by a shared runtime arity factory (`_$bagN(__s, root, v0, v1, …)` — see
|
|
5796
|
+
// makeBag): the mount statements fill pre-declared LOCALS (`_m0, _m1, …`,
|
|
5797
|
+
// declared by the placeholder below once the field count is known), and the
|
|
5798
|
+
// factory builds the object literal from the real values (final hidden class
|
|
5799
|
+
// + real field representations at allocation), inserts the root, and commits
|
|
5800
|
+
// `__s.slots[0]` — commit LAST, so a throw mid-mount (a `use()` suspending, a
|
|
5801
|
+
// child render throwing) leaves the bag `undefined` and the next attempt
|
|
5802
|
+
// re-enters the mount branch instead of updating a half-populated bag.
|
|
5803
|
+
// Control-flow-only bodies carry no bag, so all of it is skipped.
|
|
5804
|
+
const bag = makeBag();
|
|
5805
|
+
const BAG_LOCALS_PLACEHOLDER = ` /*__bagLocals__*/`;
|
|
5806
|
+
if (!noTemplate) mountLines.push(BAG_LOCALS_PLACEHOLDER);
|
|
4205
5807
|
|
|
4206
5808
|
let elementVars;
|
|
4207
5809
|
let ensureVar;
|
|
@@ -4216,12 +5818,26 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4216
5818
|
// the inner root.
|
|
4217
5819
|
// - SVG/MathML multi-root: pass ns + frag=1; runtime wraps and returns
|
|
4218
5820
|
// the wrap itself (caller drains its children — no <octane-frag>).
|
|
5821
|
+
// Multi-root fragments at an HTML parent imply SVG only when EVERY element
|
|
5822
|
+
// root does (an all-SVG fragment — e.g. portal children `<rect/><g/>` — must
|
|
5823
|
+
// parse in foreign content; a MIXED fragment can't share one wrapper, so it
|
|
5824
|
+
// keeps HTML and the SVG-only roots would mis-parse — reject? No: mixed
|
|
5825
|
+
// fragments at ambiguous positions are user error the browser also mangles).
|
|
5826
|
+
const elementRoots = single ? null : jsxNodes.filter((n) => elementTagName(n) !== null);
|
|
5827
|
+
const fragImpliesSvg =
|
|
5828
|
+
!single && elementRoots.length > 0 && elementRoots.every(isNonHtmlRootTag);
|
|
4219
5829
|
const isHtmlNs =
|
|
4220
5830
|
parentNs === 'html' &&
|
|
4221
5831
|
(single
|
|
4222
|
-
? !isNonHtmlRootTag(jsxNodes[0]) //
|
|
4223
|
-
:
|
|
4224
|
-
const tplNs = isHtmlNs
|
|
5832
|
+
? !isNonHtmlRootTag(jsxNodes[0]) // svg/math/SVG-only root means non-HTML ns
|
|
5833
|
+
: !fragImpliesSvg);
|
|
5834
|
+
const tplNs = isHtmlNs
|
|
5835
|
+
? 'html'
|
|
5836
|
+
: single
|
|
5837
|
+
? nsForRootTag(jsxNodes[0], parentNs)
|
|
5838
|
+
: parentNs === 'html' && fragImpliesSvg
|
|
5839
|
+
? 'svg'
|
|
5840
|
+
: parentNs;
|
|
4225
5841
|
const flag = nsFlag(tplNs);
|
|
4226
5842
|
const fragArg = !single && flag !== 0 ? 1 : 0;
|
|
4227
5843
|
const tplHtml = single || flag !== 0 ? html : `<octane-frag>${html}</octane-frag>`;
|
|
@@ -4380,6 +5996,9 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4380
5996
|
if (b.kind === 'text') ctx.runtimeNeeded.add('htextSwap');
|
|
4381
5997
|
if (b.kind === 'textOnlyChild') ctx.runtimeNeeded.add('htext');
|
|
4382
5998
|
if (b.kind === 'attr') ctx.runtimeNeeded.add('setAttribute');
|
|
5999
|
+
if (CONTROLLED_KIND_HELPERS[b.kind] !== undefined) {
|
|
6000
|
+
ctx.runtimeNeeded.add(CONTROLLED_KIND_HELPERS[b.kind]);
|
|
6001
|
+
}
|
|
4383
6002
|
if (b.kind === 'class') {
|
|
4384
6003
|
if (b.ns && b.ns !== 'html') {
|
|
4385
6004
|
// SVG/MathML `className` is read-only — use setClassAttr, which sets the
|
|
@@ -4389,6 +6008,12 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4389
6008
|
}
|
|
4390
6009
|
if (b.kind === 'style') ctx.runtimeNeeded.add('setStyle');
|
|
4391
6010
|
if (b.kind === 'formAction') ctx.runtimeNeeded.add('setFormAction');
|
|
6011
|
+
if (b.kind === 'event-bundle') {
|
|
6012
|
+
// 3b: mount builds the descriptor via evtN, update mutates via evtNu.
|
|
6013
|
+
const arity = b.argExprs.length <= 2 ? String(b.argExprs.length) : 'N';
|
|
6014
|
+
ctx.runtimeNeeded.add(`evt${arity}`);
|
|
6015
|
+
ctx.runtimeNeeded.add(`evt${arity}u`);
|
|
6016
|
+
}
|
|
4392
6017
|
if (b.kind === 'spread') {
|
|
4393
6018
|
ctx.runtimeNeeded.add('setSpread');
|
|
4394
6019
|
ctx.runtimeNeeded.add('queueRefDetach'); // unmount-detach of a spread-supplied ref
|
|
@@ -4410,8 +6035,8 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4410
6035
|
b.endElVar = ensureVar(b.endPath);
|
|
4411
6036
|
}
|
|
4412
6037
|
// `htextSwap` (sibling text hole) detaches its `<!>`; defer it past all walks (see above).
|
|
4413
|
-
if (b.kind === 'text') deferredTextMounts.push(emitBindingMount(b, elVar));
|
|
4414
|
-
else mountLines.push(emitBindingMount(b, elVar));
|
|
6038
|
+
if (b.kind === 'text') deferredTextMounts.push(emitBindingMount(b, elVar, bag));
|
|
6039
|
+
else mountLines.push(emitBindingMount(b, elVar, bag));
|
|
4415
6040
|
}
|
|
4416
6041
|
// Shared construct mount-loop (@for/@if/component/@try/@switch/portal):
|
|
4417
6042
|
// resolve each record's host element, stash it on the bag under the
|
|
@@ -4425,12 +6050,12 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4425
6050
|
for (const c of list) {
|
|
4426
6051
|
const elVar = ensureVar(c.hostPath || []);
|
|
4427
6052
|
c.elVar = elVar;
|
|
4428
|
-
if (!noTemplate) mountLines.push(`
|
|
6053
|
+
if (!noTemplate) mountLines.push(` ${bag.local(`_${hostKey}$${c.id}`)} = ${elVar};`);
|
|
4429
6054
|
if (!noTemplate && stampLoc) stampHostLoc(elVar, c.hostPath, c.loc);
|
|
4430
6055
|
if (anchorKey !== null && c.anchorPath) {
|
|
4431
6056
|
const anchorVar = ensureVar(c.anchorPath);
|
|
4432
6057
|
c.anchorVar = anchorVar;
|
|
4433
|
-
mountLines.push(`
|
|
6058
|
+
mountLines.push(` ${bag.local(`_${anchorKey}$${c.id}`)} = ${anchorVar};`);
|
|
4434
6059
|
}
|
|
4435
6060
|
if (each) each(c);
|
|
4436
6061
|
}
|
|
@@ -4442,11 +6067,11 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4442
6067
|
each: (cc) => {
|
|
4443
6068
|
// A renderable `{expr}` child in a TEMPLATE body (has a bag) uses the inline
|
|
4444
6069
|
// text-hole fast path — cache its text node (`_chv`) + last value (`_chp`) on
|
|
4445
|
-
// the bag so updates do a direct `setText` like a `.tsrx` text binding.
|
|
4446
|
-
//
|
|
6070
|
+
// the bag so updates do a direct `setText` like a `.tsrx` text binding.
|
|
6071
|
+
// Const-seeded straight into the bag factory args — no mount statement.
|
|
4447
6072
|
if (cc.isChild && !noTemplate) {
|
|
4448
|
-
|
|
4449
|
-
|
|
6073
|
+
bag.constField(`_chv$${cc.id}`, 'null');
|
|
6074
|
+
bag.constField(`_chp$${cc.id}`, 'undefined');
|
|
4450
6075
|
}
|
|
4451
6076
|
},
|
|
4452
6077
|
});
|
|
@@ -4462,21 +6087,66 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4462
6087
|
for (const line of deferredTextMounts) mountLines.push(line);
|
|
4463
6088
|
|
|
4464
6089
|
if (!noTemplate) {
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
}
|
|
6090
|
+
// Allocate + insert + commit in ONE shared-factory call, LAST — see the
|
|
6091
|
+
// matching comment at the bag-locals placeholder above. `_$bagN(__s, root,
|
|
6092
|
+
// v0, …)` builds `{a: v0, b: v1, …}` (final hidden class + real values at
|
|
6093
|
+
// allocation), inserts `root` before the block's end marker (skipped for
|
|
6094
|
+
// the multi-root null-root form — drainFrag placed the content), commits
|
|
6095
|
+
// `__s.slots[0]`, and returns the bag for the after-lines below.
|
|
6096
|
+
if (!single) {
|
|
4468
6097
|
// Multi-root: drain the <octane-frag> wrapper's children into the live
|
|
4469
6098
|
// parent via the runtime helper — a hydration-aware no-op when clone()
|
|
4470
6099
|
// adopted the server content in place (virtual wrapper).
|
|
4471
6100
|
ctx.runtimeNeeded.add('drainFrag');
|
|
4472
6101
|
mountLines.push(` _$drainFrag(_root, __block.parentNode, __block.endMarker);`);
|
|
4473
6102
|
}
|
|
6103
|
+
const rootArg = single ? '_root' : 'null';
|
|
6104
|
+
const args = bag.fields.map((f) => f.constExpr ?? f.local);
|
|
6105
|
+
if (bag.fields.length <= BAG_FACTORY_MAX) {
|
|
6106
|
+
ctx.runtimeNeeded.add(`bag${bag.fields.length}`);
|
|
6107
|
+
mountLines.push(
|
|
6108
|
+
` _b = _$bag${bag.fields.length}(__s, ${rootArg}${args.length ? ', ' + args.join(', ') : ''});`,
|
|
6109
|
+
);
|
|
6110
|
+
} else {
|
|
6111
|
+
// Spill path — beyond the shared-factory arities: one inline literal
|
|
6112
|
+
// (still real values, single allocation) through the generic commit.
|
|
6113
|
+
ctx.runtimeNeeded.add('bagOf');
|
|
6114
|
+
mountLines.push(
|
|
6115
|
+
` _b = _$bagOf(__s, ${rootArg}, { ${bag.fields.map((f) => `${f.name}: ${f.constExpr ?? f.local}`).join(', ')} });`,
|
|
6116
|
+
);
|
|
6117
|
+
}
|
|
6118
|
+
// REF MANIFEST (compiled-output plan, ref-manifest phase): bodies with
|
|
6119
|
+
// ref-carrying bindings stamp a module-scope constant on the scope so the
|
|
6120
|
+
// runtime's suspense-hide walk (detachSubtreeRefs) finds the bag fields
|
|
6121
|
+
// WITHOUT a key scan — flat [kind, field, elField] triads ('r' element
|
|
6122
|
+
// ref / 's' spread / 'f' fragment ref, whose third slot is unused).
|
|
6123
|
+
{
|
|
6124
|
+
const rm = [];
|
|
6125
|
+
for (const b of elementBindings) {
|
|
6126
|
+
if (b.kind === 'ref') {
|
|
6127
|
+
rm.push('r', bag.letter(`_ref$${b.id}`), bag.letter(`_el$${b.id}`));
|
|
6128
|
+
} else if (b.kind === 'spread') {
|
|
6129
|
+
rm.push('s', bag.letter(`_sp$${b.id}`), bag.letter(`_el$${b.id}`));
|
|
6130
|
+
} else if (b.kind === 'fragmentRef') {
|
|
6131
|
+
rm.push('f', bag.letter(`_fi$${b.id}`), '');
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
if (rm.length > 0) {
|
|
6135
|
+
const rmName = `_rm$${ctx.nextHelperId++}`;
|
|
6136
|
+
ctx.hoistedHelpers.push(
|
|
6137
|
+
`const ${rmName} = [${rm.map((x) => JSON.stringify(x)).join(', ')}];`,
|
|
6138
|
+
);
|
|
6139
|
+
mountLines.push(` __s.refFields = ${rmName};`);
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
// Declare the mount locals the factory args read — patched into the
|
|
6143
|
+
// placeholder now that the field set is complete.
|
|
6144
|
+
const locals = bag.fields.filter((f) => f.local !== null).map((f) => f.local);
|
|
6145
|
+
const init = mountLines.indexOf(BAG_LOCALS_PLACEHOLDER);
|
|
6146
|
+
if (init === -1) throw new Error('octane compiler: bag locals placeholder missing');
|
|
6147
|
+
if (locals.length > 0) mountLines[init] = ` let ${locals.join(', ')};`;
|
|
6148
|
+
else mountLines.splice(init, 1);
|
|
4474
6149
|
}
|
|
4475
|
-
// Commit the binding bag to the scope LAST — see the matching comment at
|
|
4476
|
-
// the `_b = {}` initialization above. Reaching here means every binding
|
|
4477
|
-
// and the DOM range have been successfully constructed, so future
|
|
4478
|
-
// renders can safely take the update branch keyed on `__s.slots[0]`.
|
|
4479
|
-
if (!noTemplate) mountLines.push(` __s.slots[0] = _b;`);
|
|
4480
6150
|
|
|
4481
6151
|
// Update. Deferred property-writes run their diff EVERY render (it does the
|
|
4482
6152
|
// mount-time write too); everything else diffs only on re-render (the `else`
|
|
@@ -4485,7 +6155,7 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4485
6155
|
const updateLines = [];
|
|
4486
6156
|
const everyRenderLines = [];
|
|
4487
6157
|
for (const b of elementBindings) {
|
|
4488
|
-
const code = emitBindingUpdate(b);
|
|
6158
|
+
const code = emitBindingUpdate(b, bag);
|
|
4489
6159
|
if (!code) continue;
|
|
4490
6160
|
if (b.deferred) everyRenderLines.push(code);
|
|
4491
6161
|
else updateLines.push(code);
|
|
@@ -4543,18 +6213,29 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4543
6213
|
// and content never lands after later siblings).
|
|
4544
6214
|
// - The host is a real in-template element: no anchor — append into it
|
|
4545
6215
|
// (insertBefore(_, null) === appendChild).
|
|
4546
|
-
// Returns the anchor EXPRESSION, or null for the append case.
|
|
6216
|
+
// Returns the anchor EXPRESSION, or null for the append case. The after-lines
|
|
6217
|
+
// run right after the mount/update branches where `_b` holds the committed
|
|
6218
|
+
// bag, so bag reads go through `_b.<letter>` (shorter than `__s.slots[0].…`).
|
|
4547
6219
|
const anchorExprFor = (c, anchorKey) =>
|
|
4548
6220
|
c.anchorVar
|
|
4549
|
-
? `
|
|
6221
|
+
? `_b.${bag.letter(`_${anchorKey}$${c.id}`)}`
|
|
4550
6222
|
: !isElHost(c.elVar)
|
|
4551
6223
|
? '__block.endMarker'
|
|
4552
6224
|
: null;
|
|
6225
|
+
// Host expression for a construct's slot call — the bag-stashed host element,
|
|
6226
|
+
// or the block's own parentNode for bagless (control-flow-only) bodies.
|
|
6227
|
+
const hostExprFor = (key) => (noTemplate ? '__block.parentNode' : `_b.${bag.letter(key)}`);
|
|
6228
|
+
// Phase 2: a construct's env argument — the captured-locals tuple its
|
|
6229
|
+
// hoisted helpers destructure from `__extra`. Folded records carry a
|
|
6230
|
+
// pre-built `props.hN` expression (the fold threads the values through the
|
|
6231
|
+
// fragment renderer's props); inline records emit the identifier array.
|
|
6232
|
+
const envExprFor = (c) =>
|
|
6233
|
+
c.envExpr ?? (c.envNames && c.envNames.length ? `[${c.envNames.join(', ')}]` : null);
|
|
4553
6234
|
for (const fc of forCalls) {
|
|
4554
6235
|
ctx.runtimeNeeded.add('forBlock');
|
|
4555
6236
|
const slotIndex = fc.slotIndex;
|
|
4556
6237
|
// Control-flow-only bodies have no bag: the host is __block.parentNode directly.
|
|
4557
|
-
const hostExpr =
|
|
6238
|
+
const hostExpr = hostExprFor(`_for$${fc.id}`);
|
|
4558
6239
|
// flags: bit 0 = pure (auto-memo), bit 1 = singleRoot (skip per-item markers),
|
|
4559
6240
|
// bit 2 = depEligible (runtime compares deps array, upgrades to pure
|
|
4560
6241
|
// for survivors when deps unchanged this render),
|
|
@@ -4574,8 +6255,12 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4574
6255
|
const anchorExpr = anchorExprFor(fc, 'forAnchor');
|
|
4575
6256
|
const hasAnchor = anchorExpr !== null;
|
|
4576
6257
|
const hasEmpty = fc.emptyHelper && fc.emptyHelper !== 'null';
|
|
4577
|
-
|
|
4578
|
-
|
|
6258
|
+
// deps doubles as the Phase 2 env tuple — emitted whenever the item/empty
|
|
6259
|
+
// helpers captured anything, not only for the dep-pure promotion. A deps
|
|
6260
|
+
// arg forces the flags placeholder too (positional alignment).
|
|
6261
|
+
const hasDeps = fc.depNames.length > 0;
|
|
6262
|
+
const flagsPart = flags || hasDeps || hasEmpty || hasAnchor ? ', ' + (flags || 0) : '';
|
|
6263
|
+
const depsPart = hasDeps
|
|
4579
6264
|
? `, [${fc.depNames.join(', ')}]`
|
|
4580
6265
|
: hasEmpty || hasAnchor
|
|
4581
6266
|
? ', undefined'
|
|
@@ -4589,15 +6274,18 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4589
6274
|
}
|
|
4590
6275
|
for (const ic of ifCalls) {
|
|
4591
6276
|
const slotIndex = ic.slotIndex;
|
|
4592
|
-
const hostExpr =
|
|
6277
|
+
const hostExpr = hostExprFor(`_ifHost$${ic.id}`);
|
|
4593
6278
|
// Anchor selection — see anchorExprFor.
|
|
4594
6279
|
const ifAnchor = anchorExprFor(ic, 'ifAnchor');
|
|
4595
6280
|
const anchorArg = ifAnchor ? `, ${ifAnchor}` : '';
|
|
6281
|
+
const ifEnv = envExprFor(ic);
|
|
6282
|
+
// env is positional AFTER anchor — backfill an `undefined` anchor slot.
|
|
6283
|
+
const ifEnvArg = ifEnv ? (anchorArg ? `, ${ifEnv}` : `, undefined, ${ifEnv}`) : '';
|
|
4596
6284
|
if (ic.activity) {
|
|
4597
6285
|
ctx.runtimeNeeded.add('activityBlock');
|
|
4598
6286
|
pushAfter(
|
|
4599
6287
|
ic.id,
|
|
4600
|
-
` _$activityBlock(__s, ${slotIndex}, ${hostExpr}, (${ic.modeExpr}), ${ic.thenHelper}${anchorArg});`,
|
|
6288
|
+
` _$activityBlock(__s, ${slotIndex}, ${hostExpr}, (${ic.modeExpr}), ${ic.thenHelper}${anchorArg}${ifEnvArg});`,
|
|
4601
6289
|
);
|
|
4602
6290
|
continue;
|
|
4603
6291
|
}
|
|
@@ -4605,12 +6293,12 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4605
6293
|
const elseArg = ic.elseHelper || 'null';
|
|
4606
6294
|
pushAfter(
|
|
4607
6295
|
ic.id,
|
|
4608
|
-
` _$ifBlock(__s, ${slotIndex}, ${hostExpr}, (${ic.condExpr}), ${ic.thenHelper}, ${elseArg}${anchorArg});`,
|
|
6296
|
+
` _$ifBlock(__s, ${slotIndex}, ${hostExpr}, (${ic.condExpr}), ${ic.thenHelper}, ${elseArg}${anchorArg}${ifEnvArg});`,
|
|
4609
6297
|
);
|
|
4610
6298
|
}
|
|
4611
6299
|
for (const cc of compCalls) {
|
|
4612
6300
|
const slotIndex = cc.slotIndex;
|
|
4613
|
-
const hostExpr =
|
|
6301
|
+
const hostExpr = hostExprFor(`_compHost$${cc.id}`);
|
|
4614
6302
|
// Renderable `{expr}` hole — dispatch the value at runtime (component /
|
|
4615
6303
|
// element → block; primitive → text; nullish/boolean/'' → nothing). Shares
|
|
4616
6304
|
// the host/`<!>`-anchor resolution + hole-aware hydration walk with real
|
|
@@ -4623,9 +6311,11 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4623
6311
|
if (cc.onlyChildText && !noTemplate) {
|
|
4624
6312
|
ctx.runtimeNeeded.add('setText');
|
|
4625
6313
|
ctx.runtimeNeeded.add('childTextHole');
|
|
6314
|
+
const chp = `_b.${bag.letter(`_chp$${cc.id}`)}`;
|
|
6315
|
+
const chv = `_b.${bag.letter(`_chv$${cc.id}`)}`;
|
|
4626
6316
|
pushAfter(
|
|
4627
6317
|
cc.id,
|
|
4628
|
-
` { const _v = (${cc.valueExpr}); if (
|
|
6318
|
+
` { const _v = (${cc.valueExpr}); if (${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null && typeof _v !== 'object' && typeof _v !== 'function') _$setText(_t, _v); else ${chv} = _$childTextHole(__s, ${slotIndex}, ${hostExpr}, _v, _t); } }`,
|
|
4629
6319
|
);
|
|
4630
6320
|
continue;
|
|
4631
6321
|
}
|
|
@@ -4654,9 +6344,29 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4654
6344
|
// When the slot has its OWN `<!>` placeholder, tell textHole/childSlot to
|
|
4655
6345
|
// reuse it as the end marker (no second comment minted) — `ownEnd`.
|
|
4656
6346
|
const ownEndArg = cc.anchorVar ? ', true' : '';
|
|
6347
|
+
const chp = `_b.${bag.letter(`_chp$${cc.id}`)}`;
|
|
6348
|
+
const chv = `_b.${bag.letter(`_chv$${cc.id}`)}`;
|
|
6349
|
+
pushAfter(
|
|
6350
|
+
cc.id,
|
|
6351
|
+
` { const _v = (${cc.valueExpr}); if (${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null && typeof _v !== 'object' && typeof _v !== 'function') _$setText(_t, _v); else ${chv} = _$textHole(__s, ${slotIndex}, ${hostExpr}, _v, ${anchorExpr}${ownEndArg}); } }`,
|
|
6352
|
+
);
|
|
6353
|
+
continue;
|
|
6354
|
+
}
|
|
6355
|
+
// M3 inherit-range: the sole comp-call root of a `@{}` body — the slot
|
|
6356
|
+
// BORROWS the enclosing block's marker range (10th positional arg), so it
|
|
6357
|
+
// mints nothing and the server skips the child's frame pair at the same
|
|
6358
|
+
// site (ssrEmitComponent reads the same predicate). Supersedes lite (the
|
|
6359
|
+
// borrow needs a real Block behind the slot) and singleRoot (the borrow
|
|
6360
|
+
// already elides every marker, and hydration must NOT expect a server
|
|
6361
|
+
// pair). The anchor stays: it is the runtime's fallback insert position
|
|
6362
|
+
// when the borrow is declined (incoherent parent regime) and the probe
|
|
6363
|
+
// anchor for transition swaps.
|
|
6364
|
+
if (cc.inheritRange) {
|
|
6365
|
+
ctx.runtimeNeeded.add('componentSlot');
|
|
6366
|
+
const inheritAnchor = anchorExprFor(cc, 'compAnchor') ?? 'undefined';
|
|
4657
6367
|
pushAfter(
|
|
4658
6368
|
cc.id,
|
|
4659
|
-
`
|
|
6369
|
+
` _$componentSlot(__s, ${slotIndex}, ${hostExpr}, ${cc.compExpr}, ${cc.propsExpr}, ${inheritAnchor}, undefined, undefined, true);`,
|
|
4660
6370
|
);
|
|
4661
6371
|
continue;
|
|
4662
6372
|
}
|
|
@@ -4694,11 +6404,13 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4694
6404
|
keyArg = `, (${cc.keyExpr})`;
|
|
4695
6405
|
}
|
|
4696
6406
|
// singleRoot is the 8th positional arg (after anchor, key). It's gated on
|
|
4697
|
-
// no-key, so backfill anchor + key placeholders to land it in the right
|
|
6407
|
+
// no-key, so backfill anchor + key placeholders to land it in the right
|
|
6408
|
+
// slot. `true` = proven same-module single-element root; `2` = the
|
|
6409
|
+
// cross-module sentinel (runtime checks the callee's $$singleRoot stamp).
|
|
4698
6410
|
let singleRootArg = '';
|
|
4699
|
-
if (cc.singleRoot) {
|
|
6411
|
+
if (cc.singleRoot || cc.maybeSingleRoot) {
|
|
4700
6412
|
if (anchorArg === '') anchorArg = ', undefined';
|
|
4701
|
-
singleRootArg = ', undefined, true';
|
|
6413
|
+
singleRootArg = cc.singleRoot ? ', undefined, true' : ', undefined, 2';
|
|
4702
6414
|
}
|
|
4703
6415
|
pushAfter(
|
|
4704
6416
|
cc.id,
|
|
@@ -4707,39 +6419,44 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4707
6419
|
}
|
|
4708
6420
|
for (const pc of ctx._portalCalls) {
|
|
4709
6421
|
const slotIndex = pc.slotIndex;
|
|
4710
|
-
const hostExpr =
|
|
6422
|
+
const hostExpr = hostExprFor(`_portalHost$${pc.id}`);
|
|
4711
6423
|
ctx.runtimeNeeded.add('portal');
|
|
6424
|
+
const portalEnv = envExprFor(pc);
|
|
4712
6425
|
pushAfter(
|
|
4713
6426
|
pc.id,
|
|
4714
|
-
` _$portal(__s, ${slotIndex}, ${pc.targetExpr}, ${pc.bodyExpr}, ${pc.propsExpr}, ${hostExpr});`,
|
|
6427
|
+
` _$portal(__s, ${slotIndex}, ${pc.targetExpr}, ${pc.bodyExpr}, ${pc.propsExpr}, ${hostExpr}${portalEnv ? `, ${portalEnv}` : ''});`,
|
|
4715
6428
|
);
|
|
4716
6429
|
}
|
|
4717
6430
|
// Restore the outer plan's portal-call list — pairs with the save above.
|
|
4718
6431
|
ctx._portalCalls = _prevPortalCalls;
|
|
4719
6432
|
for (const tc of tryCalls) {
|
|
4720
6433
|
const slotIndex = tc.slotIndex;
|
|
4721
|
-
const hostExpr =
|
|
6434
|
+
const hostExpr = hostExprFor(`_tryHost$${tc.id}`);
|
|
4722
6435
|
ctx.runtimeNeeded.add('tryBlock');
|
|
4723
6436
|
// Anchor selection — see anchorExprFor (mirrors ifBlock, including the
|
|
4724
6437
|
// __block.endMarker fallback for a body that is ONLY a @try).
|
|
4725
6438
|
const tryAnchor = anchorExprFor(tc, 'tryAnchor');
|
|
4726
6439
|
const tryAnchorArg = tryAnchor ? `, ${tryAnchor}` : '';
|
|
6440
|
+
const tryEnv = envExprFor(tc);
|
|
6441
|
+
const tryEnvArg = tryEnv ? (tryAnchorArg ? `, ${tryEnv}` : `, undefined, ${tryEnv}`) : '';
|
|
4727
6442
|
pushAfter(
|
|
4728
6443
|
tc.id,
|
|
4729
|
-
` _$tryBlock(__s, ${slotIndex}, ${hostExpr}, ${tc.tryHelper}, ${tc.catchHelper}, ${tc.pendingHelper}${tryAnchorArg});`,
|
|
6444
|
+
` _$tryBlock(__s, ${slotIndex}, ${hostExpr}, ${tc.tryHelper}, ${tc.catchHelper}, ${tc.pendingHelper}${tryAnchorArg}${tryEnvArg});`,
|
|
4730
6445
|
);
|
|
4731
6446
|
}
|
|
4732
6447
|
for (const sc of ctx._switchCalls) {
|
|
4733
6448
|
const slotIndex = sc.slotIndex;
|
|
4734
|
-
const hostExpr =
|
|
6449
|
+
const hostExpr = hostExprFor(`_switchHost$${sc.id}`);
|
|
4735
6450
|
ctx.runtimeNeeded.add('switchBlock');
|
|
4736
6451
|
// Anchor selection — see anchorExprFor (mirrors ifBlock, including the
|
|
4737
6452
|
// __block.endMarker fallback for a body that is ONLY a @switch).
|
|
4738
6453
|
const switchAnchor = anchorExprFor(sc, 'switchAnchor');
|
|
4739
6454
|
const anchorArg = switchAnchor ? `, ${switchAnchor}` : '';
|
|
6455
|
+
const swEnv = envExprFor(sc);
|
|
6456
|
+
const swEnvArg = swEnv ? (anchorArg ? `, ${swEnv}` : `, undefined, ${swEnv}`) : '';
|
|
4740
6457
|
pushAfter(
|
|
4741
6458
|
sc.id,
|
|
4742
|
-
` _$switchBlock(__s, ${slotIndex}, ${hostExpr}, (${sc.discExpr}), ${sc.casesArrayExpr}, ${sc.defaultHelper}${anchorArg});`,
|
|
6459
|
+
` _$switchBlock(__s, ${slotIndex}, ${hostExpr}, (${sc.discExpr}), ${sc.casesArrayExpr}, ${sc.defaultHelper}${anchorArg}${swEnvArg});`,
|
|
4743
6460
|
);
|
|
4744
6461
|
}
|
|
4745
6462
|
// Restore the outer plan's switch-call list — pairs with the save above.
|
|
@@ -4752,19 +6469,23 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
4752
6469
|
// Restore the outer plan's per-element LOC map — pairs with the save at planJsx top.
|
|
4753
6470
|
ctx._elemLocs = _prevElemLocs;
|
|
4754
6471
|
|
|
6472
|
+
const updateJoined = updateLines.join('\n');
|
|
6473
|
+
const everyRenderJoined = everyRenderLines.join('\n');
|
|
6474
|
+
const afterJoined = afterCalls
|
|
6475
|
+
.map((c, i) => ({ ...c, i }))
|
|
6476
|
+
.sort((a, b) => a.id - b.id || a.i - b.i)
|
|
6477
|
+
.map((c) => c.line)
|
|
6478
|
+
.join('\n');
|
|
6479
|
+
|
|
4755
6480
|
return {
|
|
4756
6481
|
// Does this body carry a binding bag (`__s.slots[0]`)? Control-flow-only
|
|
4757
6482
|
// bodies don't → no `let _b … if (undefined) … else {}` wrapper in the
|
|
4758
6483
|
// assembled body (the slot calls use __block.parentNode directly).
|
|
4759
6484
|
hasBag: !noTemplate,
|
|
4760
6485
|
mount: mountLines.join('\n'),
|
|
4761
|
-
update:
|
|
4762
|
-
everyRender:
|
|
4763
|
-
after:
|
|
4764
|
-
.map((c, i) => ({ ...c, i }))
|
|
4765
|
-
.sort((a, b) => a.id - b.id || a.i - b.i)
|
|
4766
|
-
.map((c) => c.line)
|
|
4767
|
-
.join('\n'),
|
|
6486
|
+
update: updateJoined,
|
|
6487
|
+
everyRender: everyRenderJoined,
|
|
6488
|
+
after: afterJoined,
|
|
4768
6489
|
head: headEmit,
|
|
4769
6490
|
// DEV ONLY (`''` in prod → no body emission, byte-identical output): a structured
|
|
4770
6491
|
// `{ slotIndex: [line, column] }` literal for hydration-mismatch warnings + a future
|
|
@@ -4791,20 +6512,81 @@ function buildLocsLiteral(constructs) {
|
|
|
4791
6512
|
// event-bundle "stable bundle" hoisting is a separate, guarded optimization).
|
|
4792
6513
|
const DEFERRABLE_MOUNT_KINDS = new Set(['attr', 'class', 'style', 'formAction', 'htmlOnlyChild']);
|
|
4793
6514
|
|
|
6515
|
+
// Per-body binding-bag registry. Fields are keyed by the historical long name
|
|
6516
|
+
// (`_el$3`, `_prev$3`, …) but EMIT as single characters (`a`, `b`, …) — bag
|
|
6517
|
+
// field names are object properties, so unlike locals a minifier can never
|
|
6518
|
+
// shorten them; 1-char names are the shipped-bytes win. Each field is either
|
|
6519
|
+
// LOCAL-backed (the mount path assigns a pre-declared `_mN` local; the runtime
|
|
6520
|
+
// bag factory receives it positionally) or CONST-seeded (`null`/`undefined`
|
|
6521
|
+
// seeds pass straight to the factory — no local, no mount statement).
|
|
6522
|
+
// Registration order = mount-write order = the factory's positional args =
|
|
6523
|
+
// the letter sequence, so `_$bagN(s, root, v0, v1, …)` builds `{a: v0, b: v1,
|
|
6524
|
+
// …}` with every field carrying its REAL mount value. `letter()` throws for a
|
|
6525
|
+
// field that was never mount-registered — an update/slot-call line referencing
|
|
6526
|
+
// an unmounted field is a compiler bug, surfaced at compile time.
|
|
6527
|
+
const BAG_LC = 'abcdefghijklmnopqrstuvwxyz';
|
|
6528
|
+
const BAG_UC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
6529
|
+
// Highest shared-factory arity (bag0..bag16 in the runtime); bigger bags fall
|
|
6530
|
+
// back to `bagOf(__s, root, { … })` with an inline literal of real values.
|
|
6531
|
+
const BAG_FACTORY_MAX = 16;
|
|
6532
|
+
function bagLetter(i) {
|
|
6533
|
+
if (i < 26) return BAG_LC[i];
|
|
6534
|
+
if (i < 52) return BAG_UC[i - 26];
|
|
6535
|
+
return `f${i}`;
|
|
6536
|
+
}
|
|
6537
|
+
function makeBag() {
|
|
6538
|
+
const fields = [];
|
|
6539
|
+
const byKey = new Map();
|
|
6540
|
+
const reg = (key, constExpr) => {
|
|
6541
|
+
let r = byKey.get(key);
|
|
6542
|
+
if (r === undefined) {
|
|
6543
|
+
const i = fields.length;
|
|
6544
|
+
r = {
|
|
6545
|
+
key,
|
|
6546
|
+
// Every field gets a letter — incl. ref-carrying kinds since the
|
|
6547
|
+
// ref-manifest phase: the runtime's suspense-hide walk discovers
|
|
6548
|
+
// them through the compiled `__s.refFields` manifest, not by key
|
|
6549
|
+
// prefix, so nothing needs a long name anymore.
|
|
6550
|
+
name: bagLetter(i),
|
|
6551
|
+
local: constExpr === undefined ? `_m${i}` : null,
|
|
6552
|
+
constExpr: constExpr === undefined ? null : constExpr,
|
|
6553
|
+
};
|
|
6554
|
+
fields.push(r);
|
|
6555
|
+
byKey.set(key, r);
|
|
6556
|
+
}
|
|
6557
|
+
return r;
|
|
6558
|
+
};
|
|
6559
|
+
return {
|
|
6560
|
+
/** Mount-write target for `key` — the pre-declared local. */
|
|
6561
|
+
local: (key) => reg(key, undefined).local,
|
|
6562
|
+
/** Seed `key` with a constant expression (no local, no mount write). */
|
|
6563
|
+
constField: (key, expr) => {
|
|
6564
|
+
reg(key, expr);
|
|
6565
|
+
},
|
|
6566
|
+
/** Emitted bag property name for `key` (update/slot-call reads+writes). */
|
|
6567
|
+
letter: (key) => {
|
|
6568
|
+
const r = byKey.get(key);
|
|
6569
|
+
if (r === undefined) throw new Error(`octane compiler: bag field ${key} never mounted`);
|
|
6570
|
+
return r.name;
|
|
6571
|
+
},
|
|
6572
|
+
fields,
|
|
6573
|
+
};
|
|
6574
|
+
}
|
|
6575
|
+
|
|
4794
6576
|
// Mount for a DEFERRED property-write binding: store the element ref + seed the
|
|
4795
6577
|
// diff field to `undefined`. The every-render diff then performs the actual
|
|
4796
6578
|
// write — including on the first render, since the `undefined` seed makes its
|
|
4797
6579
|
// `_prev !== _v` guard fire, and `setAttribute(el, name, undefined)` /
|
|
4798
6580
|
// `setClassName(el, undefined)` no-op on a freshly-cloned element (so the output
|
|
4799
6581
|
// is byte-identical to the old unconditional mount write).
|
|
4800
|
-
function emitDeferredMount(b, elVar) {
|
|
6582
|
+
function emitDeferredMount(b, elVar, bag) {
|
|
4801
6583
|
// `style` diffs on `_sty`; attr / class / formAction / htmlOnlyChild on `_prev`.
|
|
4802
|
-
|
|
4803
|
-
return ` {
|
|
6584
|
+
bag.constField(b.kind === 'style' ? `_sty$${b.id}` : `_prev$${b.id}`, 'undefined');
|
|
6585
|
+
return ` ${bag.local(`_el$${b.id}`)} = ${elVar};`;
|
|
4804
6586
|
}
|
|
4805
6587
|
|
|
4806
|
-
function emitBindingMount(b, elVar) {
|
|
4807
|
-
if (b.deferred) return emitDeferredMount(b, elVar);
|
|
6588
|
+
function emitBindingMount(b, elVar, bag) {
|
|
6589
|
+
if (b.deferred) return emitDeferredMount(b, elVar, bag);
|
|
4808
6590
|
// `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
|
|
4809
6591
|
// keep the server value + skip the warning on a hydration mismatch for this element.
|
|
4810
6592
|
if (b.kind === 'suppress') return ` ${elVar}.__oct_suppress = true;`;
|
|
@@ -4818,8 +6600,8 @@ function emitBindingMount(b, elVar) {
|
|
|
4818
6600
|
// hydration mismatch re-render).
|
|
4819
6601
|
return ` {
|
|
4820
6602
|
const _v = ${E};
|
|
4821
|
-
|
|
4822
|
-
|
|
6603
|
+
${bag.local(`_txt$${b.id}`)} = _$htext(${elVar}, _v);
|
|
6604
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
4823
6605
|
}`;
|
|
4824
6606
|
}
|
|
4825
6607
|
case 'htmlOnlyChild': {
|
|
@@ -4827,8 +6609,8 @@ function emitBindingMount(b, elVar) {
|
|
|
4827
6609
|
return ` {
|
|
4828
6610
|
const _v = ${E};
|
|
4829
6611
|
${elVar}.innerHTML = (_v == null ? '' : ${coerce});
|
|
4830
|
-
|
|
4831
|
-
|
|
6612
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6613
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
4832
6614
|
}`;
|
|
4833
6615
|
}
|
|
4834
6616
|
case 'text': {
|
|
@@ -4841,18 +6623,38 @@ function emitBindingMount(b, elVar) {
|
|
|
4841
6623
|
// htextSwap coerces the value itself, so the mount is a bare call.
|
|
4842
6624
|
return ` {
|
|
4843
6625
|
const _v = ${E};
|
|
4844
|
-
|
|
4845
|
-
|
|
6626
|
+
${bag.local(`_txt$${b.id}`)} = _$htextSwap(${elVar}, _v);
|
|
6627
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
4846
6628
|
}`;
|
|
4847
6629
|
}
|
|
4848
6630
|
case 'attr': {
|
|
4849
6631
|
return ` {
|
|
4850
6632
|
const _v = ${E};
|
|
4851
6633
|
_$setAttribute(${elVar}, ${JSON.stringify(b.name)}, _v);
|
|
4852
|
-
|
|
4853
|
-
|
|
6634
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6635
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
6636
|
+
}`;
|
|
6637
|
+
}
|
|
6638
|
+
case 'value':
|
|
6639
|
+
case 'checked':
|
|
6640
|
+
case 'selectValue':
|
|
6641
|
+
case 'defaultValue':
|
|
6642
|
+
case 'defaultChecked': {
|
|
6643
|
+
// Controlled form props: property helper, NO `_prev$` cache — the
|
|
6644
|
+
// helper diffs against the DOM (which the user mutates), and the
|
|
6645
|
+
// update must re-run every render to reassert drift (React's
|
|
6646
|
+
// controlled contract). Not in DEFERRABLE_MOUNT_KINDS: the mount
|
|
6647
|
+
// runs inside the hydration window and arms the element.
|
|
6648
|
+
return ` {
|
|
6649
|
+
_$${CONTROLLED_KIND_HELPERS[b.kind]}(${elVar}, ${E});
|
|
6650
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
4854
6651
|
}`;
|
|
4855
6652
|
}
|
|
6653
|
+
case 'autoFocus': {
|
|
6654
|
+
// Mount-only (React ignores later autoFocus changes); the focus
|
|
6655
|
+
// itself fires at commit, after the tree is connected.
|
|
6656
|
+
return ` _$setAutoFocus(${elVar}, ${E});`;
|
|
6657
|
+
}
|
|
4856
6658
|
case 'class': {
|
|
4857
6659
|
// On SVG/MathML hosts the `className` property is read-only — fall back
|
|
4858
6660
|
// to setAttribute. Compile-time choice, zero runtime branching.
|
|
@@ -4861,16 +6663,16 @@ function emitBindingMount(b, elVar) {
|
|
|
4861
6663
|
return ` {
|
|
4862
6664
|
const _v = ${E};
|
|
4863
6665
|
${setter};
|
|
4864
|
-
|
|
4865
|
-
|
|
6666
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6667
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
4866
6668
|
}`;
|
|
4867
6669
|
}
|
|
4868
6670
|
case 'style': {
|
|
4869
6671
|
return ` {
|
|
4870
6672
|
const _v = ${E};
|
|
4871
6673
|
_$setStyle(${elVar}, _v, undefined);
|
|
4872
|
-
|
|
4873
|
-
|
|
6674
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6675
|
+
${bag.local(`_sty$${b.id}`)} = _v;
|
|
4874
6676
|
}`;
|
|
4875
6677
|
}
|
|
4876
6678
|
case 'spread': {
|
|
@@ -4881,16 +6683,19 @@ function emitBindingMount(b, elVar) {
|
|
|
4881
6683
|
// (queueRefDetach: unmount cleanups run mid-render, and a state-setter
|
|
4882
6684
|
// ref firing null synchronously can render before a replacement
|
|
4883
6685
|
// element's deferred attach — commit-phase detach batches the two).
|
|
6686
|
+
// The cleanup closure reads the bag through the captured `_b` — the bag
|
|
6687
|
+
// exists by the time any cleanup runs (committed at mount end), and the
|
|
6688
|
+
// `_sp$` field is re-written by updates, so the read must be live.
|
|
4884
6689
|
return ` {
|
|
4885
6690
|
const _v = ${E};
|
|
4886
6691
|
_$setSpread(${elVar}, _v, undefined, __s);
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
__s.cleanups.push(() => { const _sp = _b._sp$${b.id}; if (_sp != null && _sp.ref != null) _$queueRefDetach(_sp.ref, _b._el$${b.id}); });
|
|
6692
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6693
|
+
${bag.local(`_sp$${b.id}`)} = _v;
|
|
6694
|
+
__s.cleanups.push(() => { const _sp = _b.${bag.letter(`_sp$${b.id}`)}; if (_sp != null && _sp.ref != null) _$queueRefDetach(_sp.ref, _b.${bag.letter(`_el$${b.id}`)}); });
|
|
4890
6695
|
}`;
|
|
4891
6696
|
}
|
|
4892
6697
|
case 'event': {
|
|
4893
|
-
return `
|
|
6698
|
+
return ` ${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
4894
6699
|
${elVar}[${JSON.stringify(b.slotKey)}] = (${b.expr});`;
|
|
4895
6700
|
}
|
|
4896
6701
|
case 'formAction': {
|
|
@@ -4900,21 +6705,22 @@ function emitBindingMount(b, elVar) {
|
|
|
4900
6705
|
return ` {
|
|
4901
6706
|
const _v = ${E};
|
|
4902
6707
|
_$setFormAction(${elVar}, ${JSON.stringify(b.name)}, _v, undefined);
|
|
4903
|
-
|
|
4904
|
-
|
|
6708
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6709
|
+
${bag.local(`_prev$${b.id}`)} = _v;
|
|
4905
6710
|
}`;
|
|
4906
6711
|
}
|
|
4907
6712
|
case 'event-bundle': {
|
|
4908
|
-
//
|
|
4909
|
-
//
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
6713
|
+
// 3b (docs/compiled-output-optimization-plan.md): ONE shared-helper call
|
|
6714
|
+
// builds the `{ fn, args }` descriptor, assigns the element's event slot,
|
|
6715
|
+
// and returns the descriptor — the ONLY bag field this binding needs (the
|
|
6716
|
+
// update path mutates the descriptor in place; dispatch reads `el[key]`
|
|
6717
|
+
// per event, so the mutation is observed without re-assignment).
|
|
6718
|
+
const n = b.argExprs.length;
|
|
6719
|
+
const argsPart = b.argExprs.map((e) => `, (${e})`).join('');
|
|
6720
|
+
if (n <= 2) {
|
|
6721
|
+
return ` ${bag.local(`_ev$${b.id}`)} = _$evt${n}(${elVar}, ${JSON.stringify(b.slotKey)}, (${b.fnExpr})${argsPart});`;
|
|
6722
|
+
}
|
|
6723
|
+
return ` ${bag.local(`_ev$${b.id}`)} = _$evtN(${elVar}, ${JSON.stringify(b.slotKey)}, (${b.fnExpr}), [${b.argExprs.map((e) => `(${e})`).join(', ')}]);`;
|
|
4918
6724
|
}
|
|
4919
6725
|
case 'ref': {
|
|
4920
6726
|
// attachRef handles all three supported shapes: callback (function),
|
|
@@ -4931,12 +6737,14 @@ function emitBindingMount(b, elVar) {
|
|
|
4931
6737
|
// bound element rides along as the cleanup target, so a callback ref
|
|
4932
6738
|
// shared across elements (ref={registerItem} on every @for row)
|
|
4933
6739
|
// releases ITS row's React-19 cleanup, not another row's.
|
|
6740
|
+
// Both deferred closures read through the captured `_b` (committed by the
|
|
6741
|
+
// time attach/cleanup run); `_ref$` must be a LIVE read — updates re-point it.
|
|
4934
6742
|
return ` {
|
|
4935
6743
|
const _r = (${b.expr});
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
_$queueRefAttach(__s, () => _$attachRef(_r, _b._el$${b.id}));
|
|
4939
|
-
__s.cleanups.push(() => _$queueRefDetach(_b._ref$${b.id}, _b._el$${b.id}));
|
|
6744
|
+
${bag.local(`_ref$${b.id}`)} = _r;
|
|
6745
|
+
${bag.local(`_el$${b.id}`)} = ${elVar};
|
|
6746
|
+
_$queueRefAttach(__s, () => _$attachRef(_r, _b.${bag.letter(`_el$${b.id}`)}));
|
|
6747
|
+
__s.cleanups.push(() => _$queueRefDetach(_b.${bag.letter(`_ref$${b.id}`)}, _b.${bag.letter(`_el$${b.id}`)}));
|
|
4940
6748
|
}`;
|
|
4941
6749
|
}
|
|
4942
6750
|
case 'fragmentRef': {
|
|
@@ -4948,39 +6756,53 @@ function emitBindingMount(b, elVar) {
|
|
|
4948
6756
|
// the ref + destroys the instance on unmount.
|
|
4949
6757
|
return ` {
|
|
4950
6758
|
const _r = (${b.expr});
|
|
4951
|
-
|
|
6759
|
+
${bag.local(`_fi$${b.id}`)} = _$mountFragmentRef(__s, ${elVar}, ${b.endElVar}, _r);
|
|
4952
6760
|
}`;
|
|
4953
6761
|
}
|
|
4954
6762
|
}
|
|
4955
6763
|
return '';
|
|
4956
6764
|
}
|
|
4957
6765
|
|
|
4958
|
-
function emitBindingUpdate(b) {
|
|
6766
|
+
function emitBindingUpdate(b, bag) {
|
|
4959
6767
|
const E = `(${b.expr})`;
|
|
6768
|
+
// 1-char bag field names (see makeBag) — resolved from the same registry the
|
|
6769
|
+
// mount pass registered them in; an unmounted field throws at compile time.
|
|
6770
|
+
const F = (prefix) => `_b.${bag.letter(`${prefix}$${b.id}`)}`;
|
|
4960
6771
|
switch (b.kind) {
|
|
4961
6772
|
case 'textOnlyChild':
|
|
4962
6773
|
case 'text': {
|
|
4963
|
-
return ` { const _v = ${E}; if (
|
|
6774
|
+
return ` { const _v = ${E}; if (${F('_prev')} !== _v) { _$setText(${F('_txt')}, _v); ${F('_prev')} = _v; } }`;
|
|
4964
6775
|
}
|
|
4965
6776
|
case 'htmlOnlyChild': {
|
|
4966
6777
|
const coerce = b.knownString ? '_v' : 'String(_v)';
|
|
4967
|
-
return ` { const _v = ${E}; if (
|
|
6778
|
+
return ` { const _v = ${E}; if (${F('_prev')} !== _v) { ${F('_el')}.innerHTML = (_v == null ? '' : ${coerce}); ${F('_prev')} = _v; } }`;
|
|
4968
6779
|
}
|
|
4969
6780
|
case 'attr': {
|
|
4970
|
-
return ` { const _v = ${E}; if (
|
|
6781
|
+
return ` { const _v = ${E}; if (${F('_prev')} !== _v) { _$setAttribute(${F('_el')}, ${JSON.stringify(b.name)}, _v); ${F('_prev')} = _v; } }`;
|
|
6782
|
+
}
|
|
6783
|
+
case 'value':
|
|
6784
|
+
case 'checked':
|
|
6785
|
+
case 'selectValue':
|
|
6786
|
+
case 'defaultValue':
|
|
6787
|
+
case 'defaultChecked': {
|
|
6788
|
+
// Deliberately UNGUARDED (no `_prev$` compare): a controlled prop
|
|
6789
|
+
// reasserts on every commit — the helper's DOM-diff makes an
|
|
6790
|
+
// unchanged value free, and a prev-guard would skip exactly the
|
|
6791
|
+
// "unrelated re-render while the DOM drifted" reassert case.
|
|
6792
|
+
return ` _$${CONTROLLED_KIND_HELPERS[b.kind]}(${F('_el')}, ${E});`;
|
|
4971
6793
|
}
|
|
4972
6794
|
case 'class': {
|
|
4973
6795
|
const setter =
|
|
4974
6796
|
b.ns && b.ns !== 'html'
|
|
4975
|
-
? `_$setClassAttr(
|
|
4976
|
-
: `_$setClassName(
|
|
4977
|
-
return ` { const _v = ${E}; if (
|
|
6797
|
+
? `_$setClassAttr(${F('_el')}, _v)`
|
|
6798
|
+
: `_$setClassName(${F('_el')}, _v)`;
|
|
6799
|
+
return ` { const _v = ${E}; if (${F('_prev')} !== _v) { ${setter}; ${F('_prev')} = _v; } }`;
|
|
4978
6800
|
}
|
|
4979
6801
|
case 'style': {
|
|
4980
6802
|
// Object styles need per-prop diffing — call setStyle even when the
|
|
4981
6803
|
// reference is unchanged it'd just no-op via the internal diff. We DO
|
|
4982
6804
|
// skip identity matches to avoid the call overhead.
|
|
4983
|
-
return ` { const _v = ${E}; if (
|
|
6805
|
+
return ` { const _v = ${E}; if (${F('_sty')} !== _v) { _$setStyle(${F('_el')}, _v, ${F('_sty')}); ${F('_sty')} = _v; } }`;
|
|
4984
6806
|
}
|
|
4985
6807
|
case 'spread': {
|
|
4986
6808
|
// setSpread does its own per-key diffing internally and handles cleanup
|
|
@@ -4989,33 +6811,25 @@ function emitBindingUpdate(b) {
|
|
|
4989
6811
|
// `__s` rides along on updates too so a spread-supplied ref's attach is
|
|
4990
6812
|
// deferred to commit (after all queued detaches) — same phasing as the
|
|
4991
6813
|
// direct `ref` binding above.
|
|
4992
|
-
return ` { const _v = ${E}; if (
|
|
6814
|
+
return ` { const _v = ${E}; if (${F('_sp')} !== _v) { _$setSpread(${F('_el')}, _v, ${F('_sp')}, __s); ${F('_sp')} = _v; } }`;
|
|
4993
6815
|
}
|
|
4994
6816
|
case 'event': {
|
|
4995
|
-
return `
|
|
6817
|
+
return ` ${F('_el')}[${JSON.stringify(b.slotKey)}] = (${b.expr});`;
|
|
4996
6818
|
}
|
|
4997
6819
|
case 'formAction': {
|
|
4998
|
-
return ` { const _v = ${E}; if (
|
|
6820
|
+
return ` { const _v = ${E}; if (${F('_prev')} !== _v) { _$setFormAction(${F('_el')}, ${JSON.stringify(b.name)}, _v, ${F('_prev')}); ${F('_prev')} = _v; } }`;
|
|
4999
6821
|
}
|
|
5000
6822
|
case 'event-bundle': {
|
|
5001
|
-
//
|
|
5002
|
-
//
|
|
5003
|
-
//
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
const
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
.join(' || ');
|
|
5012
|
-
const writes = [`_b._fn$${b.id} = ${fnVar};`]
|
|
5013
|
-
.concat(b.argExprs.map((_e, i) => `_b._a$${b.id}$${i} = ${argVars[i]};`))
|
|
5014
|
-
.concat([
|
|
5015
|
-
`_b._el$${b.id}[${JSON.stringify(b.slotKey)}] = { fn: ${fnVar}, args: [${argVars.join(', ')}] };`,
|
|
5016
|
-
])
|
|
5017
|
-
.join(' ');
|
|
5018
|
-
return ` { ${reads} if (${cmps}) { ${writes} } }`;
|
|
6823
|
+
// 3b: mutate the mount-built descriptor in place — branch-free (two
|
|
6824
|
+
// plain field writes cost less than the old compare + rebuild +
|
|
6825
|
+
// re-assign, and keyed-list survivors were already skipped one level
|
|
6826
|
+
// up by the pure/deps short-circuit).
|
|
6827
|
+
const n = b.argExprs.length;
|
|
6828
|
+
const argsPart = b.argExprs.map((e) => `, (${e})`).join('');
|
|
6829
|
+
if (n <= 2) {
|
|
6830
|
+
return ` _$evt${n}u(${F('_ev')}, (${b.fnExpr})${argsPart});`;
|
|
6831
|
+
}
|
|
6832
|
+
return ` _$evtNu(${F('_ev')}, (${b.fnExpr}), [${b.argExprs.map((e) => `(${e})`).join(', ')}]);`;
|
|
5019
6833
|
}
|
|
5020
6834
|
case 'ref': {
|
|
5021
6835
|
// Ref expression identity may change across renders. React 19: detach
|
|
@@ -5033,11 +6847,11 @@ function emitBindingUpdate(b) {
|
|
|
5033
6847
|
// callback ref.
|
|
5034
6848
|
return ` {
|
|
5035
6849
|
const _r = (${b.expr});
|
|
5036
|
-
if (_r !==
|
|
5037
|
-
const _old =
|
|
5038
|
-
if (_old != null) _$queueRefDetach(_old,
|
|
5039
|
-
if (_r != null) _$queueRefAttach(__s, () => _$attachRef(_r,
|
|
5040
|
-
|
|
6850
|
+
if (_r !== ${F('_ref')}) {
|
|
6851
|
+
const _old = ${F('_ref')};
|
|
6852
|
+
if (_old != null) _$queueRefDetach(_old, ${F('_el')});
|
|
6853
|
+
if (_r != null) _$queueRefAttach(__s, () => _$attachRef(_r, ${F('_el')}));
|
|
6854
|
+
${F('_ref')} = _r;
|
|
5041
6855
|
}
|
|
5042
6856
|
}`;
|
|
5043
6857
|
}
|
|
@@ -5050,7 +6864,7 @@ function emitBindingUpdate(b) {
|
|
|
5050
6864
|
// detaches the new ref.
|
|
5051
6865
|
return ` {
|
|
5052
6866
|
const _r = (${b.expr});
|
|
5053
|
-
const _fi =
|
|
6867
|
+
const _fi = ${F('_fi')};
|
|
5054
6868
|
if (_fi && _r !== _fi._currentRef) {
|
|
5055
6869
|
if (_fi._currentRef != null) _$queueRefDetach(_fi._currentRef, _fi);
|
|
5056
6870
|
if (_r != null) _$queueRefAttach(__s, () => _$attachRef(_r, _fi));
|
|
@@ -5242,6 +7056,7 @@ function emitElementHtml(
|
|
|
5242
7056
|
const tag = node.id?.name || node.openingElement?.name?.name;
|
|
5243
7057
|
if (!tag) throw new Error('Element without tag');
|
|
5244
7058
|
rejectVoidElementContent(tag, node, ctx);
|
|
7059
|
+
rejectTextareaValueChildren(tag, node, ctx);
|
|
5245
7060
|
|
|
5246
7061
|
// The host element's own namespace (e.g. `<svg>` is in SVG ns even if its
|
|
5247
7062
|
// parent context is HTML); its descendants' inherited ns may differ
|
|
@@ -5295,7 +7110,7 @@ function emitElementHtml(
|
|
|
5295
7110
|
if (!isFalse) bindings.push({ id: bindings.length, kind: 'suppress', path });
|
|
5296
7111
|
continue;
|
|
5297
7112
|
}
|
|
5298
|
-
const attrName = normalizeJsxAttrName(rawAttrName);
|
|
7113
|
+
const attrName = normalizeJsxAttrName(rawAttrName, tag);
|
|
5299
7114
|
|
|
5300
7115
|
const val = attr.value;
|
|
5301
7116
|
// If this attr comes AFTER a spread, we MUST emit as a binding (later wins).
|
|
@@ -5357,6 +7172,42 @@ function emitElementHtml(
|
|
|
5357
7172
|
continue;
|
|
5358
7173
|
}
|
|
5359
7174
|
|
|
7175
|
+
// Controlled form props ALWAYS compile to property bindings — static
|
|
7176
|
+
// literals and bare booleans (`<input checked/>`) included; nothing
|
|
7177
|
+
// bakes into the template HTML (see controlledKindFor).
|
|
7178
|
+
const ctlKind = controlledKindFor(tag, attrName);
|
|
7179
|
+
if (ctlKind !== null) {
|
|
7180
|
+
let ctlExpr;
|
|
7181
|
+
if (val == null) {
|
|
7182
|
+
ctlExpr = 'true';
|
|
7183
|
+
} else {
|
|
7184
|
+
const ctlInner = val.type === 'JSXExpressionContainer' ? val.expression : val;
|
|
7185
|
+
ctlExpr = printExprWithTsrx(ctlInner, ctx, componentName, inlinedSubs);
|
|
7186
|
+
}
|
|
7187
|
+
bindings.push({ id: bindings.length, kind: ctlKind, expr: ctlExpr, path, ns: hostNs });
|
|
7188
|
+
continue;
|
|
7189
|
+
}
|
|
7190
|
+
// `autoFocus` never bakes/writes an attribute — React parity: the
|
|
7191
|
+
// runtime focuses the element in its mount commit (setAutoFocus).
|
|
7192
|
+
if (attrName === 'autoFocus' && !tag.includes('-')) {
|
|
7193
|
+
bindings.push({
|
|
7194
|
+
id: bindings.length,
|
|
7195
|
+
kind: 'autoFocus',
|
|
7196
|
+
expr:
|
|
7197
|
+
val == null
|
|
7198
|
+
? 'true'
|
|
7199
|
+
: printExprWithTsrx(
|
|
7200
|
+
val.type === 'JSXExpressionContainer' ? val.expression : val,
|
|
7201
|
+
ctx,
|
|
7202
|
+
componentName,
|
|
7203
|
+
inlinedSubs,
|
|
7204
|
+
),
|
|
7205
|
+
path,
|
|
7206
|
+
ns: hostNs,
|
|
7207
|
+
});
|
|
7208
|
+
continue;
|
|
7209
|
+
}
|
|
7210
|
+
|
|
5360
7211
|
if (val == null) {
|
|
5361
7212
|
if (isAfterSpread) {
|
|
5362
7213
|
// Boolean attr after spread → emit as `true` binding.
|
|
@@ -5398,19 +7249,11 @@ function emitElementHtml(
|
|
|
5398
7249
|
|
|
5399
7250
|
// Static literal value? Inline into HTML — UNLESS we're after a spread,
|
|
5400
7251
|
// in which case we MUST emit as a binding so source order is preserved.
|
|
7252
|
+
// bakeStaticAttr applies the shared React-parity value tables (aria-*/
|
|
7253
|
+
// enumerated/data-* booleans stringify, boolean attrs canonicalize to
|
|
7254
|
+
// `attr=""`/absent, booleans on non-boolean attrs drop).
|
|
5401
7255
|
if (inner.type === 'Literal' && !isAfterSpread) {
|
|
5402
|
-
|
|
5403
|
-
attrHtml += ` ${attrName}="${escapeAttr(inner.value)}"`;
|
|
5404
|
-
} else if (typeof inner.value === 'number') {
|
|
5405
|
-
attrHtml += ` ${attrName}="${inner.value}"`;
|
|
5406
|
-
} else if (typeof inner.value === 'boolean' && attrName.startsWith('aria-')) {
|
|
5407
|
-
// `aria-*` is ENUMERATED (React parity, mirroring the runtime
|
|
5408
|
-
// setAttribute / ssrAttr): a boolean literal bakes as the string
|
|
5409
|
-
// "true"/"false" — `aria-hidden={false}` must render, not drop.
|
|
5410
|
-
attrHtml += ` ${attrName}="${inner.value}"`;
|
|
5411
|
-
} else if (inner.value === true) {
|
|
5412
|
-
attrHtml += ` ${attrName}`;
|
|
5413
|
-
}
|
|
7256
|
+
attrHtml += bakeStaticAttr(attrName, inner.value, tag);
|
|
5414
7257
|
continue;
|
|
5415
7258
|
}
|
|
5416
7259
|
|
|
@@ -5500,7 +7343,7 @@ function emitElementHtml(
|
|
|
5500
7343
|
|
|
5501
7344
|
let html = `<${tag}${attrHtml}>`;
|
|
5502
7345
|
|
|
5503
|
-
const children = normalizeChildren(node.children || []);
|
|
7346
|
+
const children = normalizeChildren(node.children || [], childNs === 'svg');
|
|
5504
7347
|
// Special case: a single Text child (only-child text fast path).
|
|
5505
7348
|
if (children.length === 1 && children[0].type === 'Text') {
|
|
5506
7349
|
const txtChild = children[0];
|
|
@@ -5865,9 +7708,21 @@ function makePortalCall(callNode, ctx, componentName, inlinedSubs, parentNs, css
|
|
|
5865
7708
|
// block, so it must be hoisted here — otherwise the raw JSX would be
|
|
5866
7709
|
// printed verbatim into the emitted portal() call (invalid output).
|
|
5867
7710
|
let bodyExpr;
|
|
7711
|
+
let envNames = null;
|
|
5868
7712
|
const bt = bodyArg ? bodyArg.type : null;
|
|
5869
7713
|
if (bt === 'Element' || bt === 'Fragment' || bt === 'JSXElement' || bt === 'JSXFragment') {
|
|
5870
|
-
|
|
7714
|
+
// Phase 2: hoisted portal body + env tuple (see hoistBodyHelper).
|
|
7715
|
+
envNames = unionEnv(ctx, [{ stmts: [bodyArg], params: [] }]);
|
|
7716
|
+
bodyExpr = hoistBodyHelper(
|
|
7717
|
+
ctx,
|
|
7718
|
+
inlinedSubs,
|
|
7719
|
+
'__portal',
|
|
7720
|
+
[bodyArg],
|
|
7721
|
+
[],
|
|
7722
|
+
parentNs,
|
|
7723
|
+
cssHash,
|
|
7724
|
+
envNames,
|
|
7725
|
+
);
|
|
5871
7726
|
} else {
|
|
5872
7727
|
bodyExpr = printExprWithTsrx(bodyArg, ctx, componentName, inlinedSubs);
|
|
5873
7728
|
}
|
|
@@ -5876,28 +7731,118 @@ function makePortalCall(callNode, ctx, componentName, inlinedSubs, parentNs, css
|
|
|
5876
7731
|
return {
|
|
5877
7732
|
id: ctx.nextHelperId++,
|
|
5878
7733
|
loc: devLoc(ctx, callNode),
|
|
7734
|
+
envNames,
|
|
5879
7735
|
bodyExpr,
|
|
5880
7736
|
targetExpr,
|
|
5881
7737
|
propsExpr,
|
|
5882
7738
|
};
|
|
5883
7739
|
}
|
|
5884
7740
|
|
|
7741
|
+
// Phase 2 (docs/compiled-output-optimization-plan.md): captured parent locals
|
|
7742
|
+
// for a construct body helper about to be HOISTED to module scope — the free
|
|
7743
|
+
// identifiers of the body (its own params/locals excluded by the scope-aware
|
|
7744
|
+
// walker) intersected with the locals visible at the call site: the enclosing
|
|
7745
|
+
// component's locals, extended per enclosing hoisted helper (see
|
|
7746
|
+
// hoistBodyHelper). `__pu$N` parallel-use temps are compiler-generated
|
|
7747
|
+
// component-body locals minted AFTER collectComponentLocals ran, so they are
|
|
7748
|
+
// matched by name shape. Returns null when there is no component context —
|
|
7749
|
+
// the caller then keeps the legacy inline (closure) placement.
|
|
7750
|
+
function helperCaptures(ctx, stmts, params) {
|
|
7751
|
+
if (!ctx.currentComponentLocals) return null;
|
|
7752
|
+
const scope = new Set();
|
|
7753
|
+
for (const p of params || []) collectBindings(p, scope);
|
|
7754
|
+
const free = collectFreeIdentifiers({ type: 'BlockStatement', body: stmts }, scope);
|
|
7755
|
+
const env = [];
|
|
7756
|
+
for (const n of free) {
|
|
7757
|
+
if (ctx.currentComponentLocals.has(n) || /^__pu\$\d+$/.test(n)) env.push(n);
|
|
7758
|
+
}
|
|
7759
|
+
env.sort();
|
|
7760
|
+
return env;
|
|
7761
|
+
}
|
|
7762
|
+
|
|
7763
|
+
// A construct's helpers (then+else, all switch cases, try+pending+catch,
|
|
7764
|
+
// item+empty) share ONE env tuple — `block.extra` is per construct block and
|
|
7765
|
+
// every helper destructures the same layout — so the emitted array is the
|
|
7766
|
+
// sorted UNION of each body's captures. Null propagates (no component
|
|
7767
|
+
// context → all of the construct's helpers stay inline).
|
|
7768
|
+
function unionEnv(ctx, bodies) {
|
|
7769
|
+
let all = null;
|
|
7770
|
+
for (const b of bodies) {
|
|
7771
|
+
if (!b) continue;
|
|
7772
|
+
const c = helperCaptures(ctx, b.stmts, b.params);
|
|
7773
|
+
if (c === null) return null;
|
|
7774
|
+
if (all === null) all = new Set();
|
|
7775
|
+
for (const n of c) all.add(n);
|
|
7776
|
+
}
|
|
7777
|
+
return all === null ? null : [...all].sort();
|
|
7778
|
+
}
|
|
7779
|
+
|
|
5885
7780
|
// Hoist a construct sub-body (an @if/@else branch, an `<Activity>`/@try/
|
|
5886
|
-
// @pending/@catch/@switch-case body,
|
|
5887
|
-
//
|
|
5888
|
-
//
|
|
5889
|
-
//
|
|
5890
|
-
//
|
|
5891
|
-
//
|
|
5892
|
-
|
|
7781
|
+
// @pending/@catch/@switch-case body, a @for item/@empty body, a portal body)
|
|
7782
|
+
// as a helper. Phase 2: with `envNames` (the construct's shared env union,
|
|
7783
|
+
// possibly empty) the helper is hoisted to MODULE scope — zero per-render
|
|
7784
|
+
// closure allocations — and captured parent locals arrive through the
|
|
7785
|
+
// `__extra` ABI slot (renderBlock forwards `block.extra` as the third body
|
|
7786
|
+
// arg; the compiled call site passes the current values every parent render):
|
|
7787
|
+
//
|
|
7788
|
+
// function __then$0(__props, __s, __extra) { const [label] = __extra; … }
|
|
7789
|
+
//
|
|
7790
|
+
// `envNames: null` keeps the legacy placement — the helper is emitted INSIDE
|
|
7791
|
+
// the component function so its closures capture the parent's locals
|
|
7792
|
+
// lexically (component children `__children$N` still use this: they are
|
|
7793
|
+
// invoked through props, not through a construct block, so there is no
|
|
7794
|
+
// block.extra channel to ride).
|
|
7795
|
+
function hoistBodyHelper(ctx, inlinedSubs, prefix, stmts, params, parentNs, cssHash, envNames) {
|
|
5893
7796
|
const helperName = `${prefix}$${ctx.nextHelperId++}`;
|
|
7797
|
+
let bodyStmts = stmts;
|
|
7798
|
+
if (envNames && envNames.length > 0) {
|
|
7799
|
+
// Destructure the construct's shared env tuple. The layout is the UNION
|
|
7800
|
+
// across the construct's helpers, so every helper destructures the same
|
|
7801
|
+
// slots (names a body doesn't use are harmless consts).
|
|
7802
|
+
bodyStmts = [
|
|
7803
|
+
{
|
|
7804
|
+
type: 'VariableDeclaration',
|
|
7805
|
+
kind: 'const',
|
|
7806
|
+
declarations: [
|
|
7807
|
+
{
|
|
7808
|
+
type: 'VariableDeclarator',
|
|
7809
|
+
id: {
|
|
7810
|
+
type: 'ArrayPattern',
|
|
7811
|
+
elements: envNames.map((n) => ({ type: 'Identifier', name: n })),
|
|
7812
|
+
},
|
|
7813
|
+
init: { type: 'Identifier', name: '__extra' },
|
|
7814
|
+
},
|
|
7815
|
+
],
|
|
7816
|
+
},
|
|
7817
|
+
...stmts,
|
|
7818
|
+
];
|
|
7819
|
+
}
|
|
5894
7820
|
const fake = {
|
|
5895
7821
|
type: 'Component',
|
|
5896
7822
|
id: { type: 'Identifier', name: helperName },
|
|
5897
7823
|
params: params || [],
|
|
5898
|
-
body:
|
|
7824
|
+
body: bodyStmts,
|
|
5899
7825
|
};
|
|
5900
|
-
|
|
7826
|
+
if (envNames == null) {
|
|
7827
|
+
inlinedSubs.push(compileFunctionBody(fake, ctx, helperName, parentNs, cssHash) + ';');
|
|
7828
|
+
return helperName;
|
|
7829
|
+
}
|
|
7830
|
+
// Module-scope placement. Nested constructs compiled INSIDE this body
|
|
7831
|
+
// compute THEIR captures against the names visible at their call sites —
|
|
7832
|
+
// which live in THIS compiled body: the component's locals extended with
|
|
7833
|
+
// this helper's params, env destructure, and body-level locals. (A nested
|
|
7834
|
+
// helper's env values are emitted as plain identifiers at its call site.)
|
|
7835
|
+
const prevLocals = ctx.currentComponentLocals;
|
|
7836
|
+
const extended = new Set(prevLocals);
|
|
7837
|
+
for (const n of collectComponentLocals(fake)) extended.add(n);
|
|
7838
|
+
ctx.currentComponentLocals = extended;
|
|
7839
|
+
let code;
|
|
7840
|
+
try {
|
|
7841
|
+
code = compileFunctionBody(fake, ctx, helperName, parentNs, cssHash);
|
|
7842
|
+
} finally {
|
|
7843
|
+
ctx.currentComponentLocals = prevLocals;
|
|
7844
|
+
}
|
|
7845
|
+
ctx.hoistedHelpers.push(code + ';');
|
|
5901
7846
|
return helperName;
|
|
5902
7847
|
}
|
|
5903
7848
|
|
|
@@ -5911,6 +7856,16 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
|
5911
7856
|
|
|
5912
7857
|
const thenStmts =
|
|
5913
7858
|
node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
|
|
7859
|
+
const elseStmts = node.alternate
|
|
7860
|
+
? node.alternate.type === 'BlockStatement'
|
|
7861
|
+
? node.alternate.body
|
|
7862
|
+
: [node.alternate]
|
|
7863
|
+
: null;
|
|
7864
|
+
// Phase 2: one shared env tuple for both branches (see unionEnv).
|
|
7865
|
+
const envNames = unionEnv(ctx, [
|
|
7866
|
+
{ stmts: thenStmts, params: [] },
|
|
7867
|
+
elseStmts && { stmts: elseStmts, params: [] },
|
|
7868
|
+
]);
|
|
5914
7869
|
const thenHelperName = hoistBodyHelper(
|
|
5915
7870
|
ctx,
|
|
5916
7871
|
inlinedSubs,
|
|
@@ -5919,13 +7874,21 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
|
5919
7874
|
[],
|
|
5920
7875
|
parentNs,
|
|
5921
7876
|
cssHash,
|
|
7877
|
+
envNames,
|
|
5922
7878
|
);
|
|
5923
7879
|
|
|
5924
7880
|
let elseHelperName = null;
|
|
5925
|
-
if (
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
7881
|
+
if (elseStmts) {
|
|
7882
|
+
elseHelperName = hoistBodyHelper(
|
|
7883
|
+
ctx,
|
|
7884
|
+
inlinedSubs,
|
|
7885
|
+
'__else',
|
|
7886
|
+
elseStmts,
|
|
7887
|
+
[],
|
|
7888
|
+
parentNs,
|
|
7889
|
+
cssHash,
|
|
7890
|
+
envNames,
|
|
7891
|
+
);
|
|
5929
7892
|
}
|
|
5930
7893
|
|
|
5931
7894
|
return {
|
|
@@ -5933,6 +7896,7 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
|
5933
7896
|
loc: devLoc(ctx, node),
|
|
5934
7897
|
condExpr,
|
|
5935
7898
|
condTest: node.test, // raw test AST — the fold threads it as a `props.hN` hole
|
|
7899
|
+
envNames,
|
|
5936
7900
|
thenHelper: thenHelperName,
|
|
5937
7901
|
elseHelper: elseHelperName,
|
|
5938
7902
|
hostPath: null,
|
|
@@ -5946,6 +7910,8 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
|
5946
7910
|
// every parent render (like ifBlock's cond); a missing mode defaults to visible.
|
|
5947
7911
|
function makeActivityCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
5948
7912
|
const modeExpr = node.mode ? printExpr(node.mode) : "'visible'";
|
|
7913
|
+
// Phase 2: hoisted body + env tuple (see hoistBodyHelper).
|
|
7914
|
+
const envNames = unionEnv(ctx, [{ stmts: node.children, params: [] }]);
|
|
5949
7915
|
const bodyHelperName = hoistBodyHelper(
|
|
5950
7916
|
ctx,
|
|
5951
7917
|
inlinedSubs,
|
|
@@ -5954,12 +7920,14 @@ function makeActivityCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = n
|
|
|
5954
7920
|
[],
|
|
5955
7921
|
parentNs,
|
|
5956
7922
|
cssHash,
|
|
7923
|
+
envNames,
|
|
5957
7924
|
);
|
|
5958
7925
|
return {
|
|
5959
7926
|
id: ctx.nextHelperId++,
|
|
5960
7927
|
loc: devLoc(ctx, node),
|
|
5961
7928
|
activity: true,
|
|
5962
7929
|
modeExpr,
|
|
7930
|
+
envNames,
|
|
5963
7931
|
thenHelper: bodyHelperName,
|
|
5964
7932
|
elseHelper: null,
|
|
5965
7933
|
hostPath: null,
|
|
@@ -6000,7 +7968,12 @@ function isComponentTag(node) {
|
|
|
6000
7968
|
// codegen path as `<Foo>` / `<ctx.Provider>`.
|
|
6001
7969
|
if (name.type === 'JSXExpressionContainer' && name.isDynamic === true) return true;
|
|
6002
7970
|
if (name.type === 'Identifier' || name.type === 'JSXIdentifier') {
|
|
6003
|
-
|
|
7971
|
+
if (typeof name.name !== 'string') return false;
|
|
7972
|
+
// JSX semantics (Babel/TS `isCompatTag`): an identifier tag is a HOST
|
|
7973
|
+
// string tag only when it starts with a lowercase ASCII letter (or isn't
|
|
7974
|
+
// a plain identifier, e.g. `<my-element>`); everything else — `<Foo>`,
|
|
7975
|
+
// `<_Inner>`, `<$Inner>` — is a component REFERENCE.
|
|
7976
|
+
return !/^[a-z]/.test(name.name) && !name.name.includes('-');
|
|
6004
7977
|
}
|
|
6005
7978
|
return false;
|
|
6006
7979
|
}
|
|
@@ -6158,6 +8131,9 @@ function makeCompCall(
|
|
|
6158
8131
|
// Compile children as a render function: (scope) => { renders JSX into scope }.
|
|
6159
8132
|
// The function is inlined inside the parent component body so its closures
|
|
6160
8133
|
// capture the parent's locals (props, state, etc.).
|
|
8134
|
+
// Phase 2 NOTE: children render-fns stay INLINE (envNames=null) — they are
|
|
8135
|
+
// invoked through props (childrenAsBody / render-prop checks), not through
|
|
8136
|
+
// a construct block, so there is no block.extra channel for captures.
|
|
6161
8137
|
const childrenHelperName = hoistBodyHelper(
|
|
6162
8138
|
ctx,
|
|
6163
8139
|
inlinedSubs,
|
|
@@ -6166,8 +8142,14 @@ function makeCompCall(
|
|
|
6166
8142
|
[],
|
|
6167
8143
|
parentNs,
|
|
6168
8144
|
cssHash,
|
|
8145
|
+
null,
|
|
6169
8146
|
);
|
|
6170
|
-
|
|
8147
|
+
// Tag the children-block render fn so a consumer can tell it from a render-prop
|
|
8148
|
+
// function child (`<C>{(x) => …}</C>`, passed RAW above) — both are `typeof === 'function'`,
|
|
8149
|
+
// so React-ecosystem `typeof children === 'function'` checks need `isChildrenBlock` to
|
|
8150
|
+
// exclude compiled element/text children. See runtime `markChildrenBlock`/`isChildrenBlock`.
|
|
8151
|
+
ctx.runtimeNeeded.add('markChildrenBlock');
|
|
8152
|
+
propParts.push(`"children": _$markChildrenBlock(${childrenHelperName})`);
|
|
6171
8153
|
}
|
|
6172
8154
|
|
|
6173
8155
|
const propsExpr = `{ ${propParts.join(', ')} }`;
|
|
@@ -6185,18 +8167,29 @@ function makeCompCall(
|
|
|
6185
8167
|
// client mount — no `comp`/`/comp` markers. (Lite components are already
|
|
6186
8168
|
// markerless, so this only matters for the full path.)
|
|
6187
8169
|
let singleRoot = false;
|
|
8170
|
+
// maybeSingleRoot: the call site qualifies syntactically but the callee is
|
|
8171
|
+
// CROSS-MODULE (not in componentInfo) — emit the `2` sentinel so the runtime
|
|
8172
|
+
// elides iff the callee carries the definition-site `$$singleRoot` stamp
|
|
8173
|
+
// (docs/comment-marker-elision-plan.md M1).
|
|
8174
|
+
let maybeSingleRoot = false;
|
|
6188
8175
|
if (ctx.componentInfo && keyExpr == null) {
|
|
6189
8176
|
const tagName = node.openingElement?.name || node.id || node.name;
|
|
6190
8177
|
const isBareIdent =
|
|
6191
8178
|
tagName && (tagName.type === 'Identifier' || tagName.type === 'JSXIdentifier');
|
|
6192
8179
|
if (isBareIdent) {
|
|
8180
|
+
const hasSpread = propParts.some((p) => p.startsWith('...'));
|
|
8181
|
+
const hasChildrenProp = propParts.some((p) => p.startsWith('"children":'));
|
|
8182
|
+
const callSiteOk = !hasSpread && !hasChildrenProp;
|
|
6193
8183
|
const calleeInfo = ctx.componentInfo.get(compExpr);
|
|
6194
8184
|
if (calleeInfo) {
|
|
6195
|
-
const hasSpread = propParts.some((p) => p.startsWith('...'));
|
|
6196
|
-
const hasChildrenProp = propParts.some((p) => p.startsWith('"children":'));
|
|
6197
|
-
const callSiteOk = !hasSpread && !hasChildrenProp;
|
|
6198
8185
|
if (calleeInfo.eligible) liteEligible = callSiteOk;
|
|
6199
8186
|
else if (calleeInfo.singleRoot) singleRoot = callSiteOk;
|
|
8187
|
+
} else if (ctx.importedNames !== undefined && ctx.importedNames.has(compExpr)) {
|
|
8188
|
+
// IMPORTED bindings only: immutable identity for the slot's whole
|
|
8189
|
+
// life. A local variable callee (`const Comp = cond ? A : B`) can
|
|
8190
|
+
// change identity per render — the markerless regime must not be
|
|
8191
|
+
// pinned to whichever component happened to mount first.
|
|
8192
|
+
maybeSingleRoot = callSiteOk;
|
|
6200
8193
|
}
|
|
6201
8194
|
}
|
|
6202
8195
|
}
|
|
@@ -6209,6 +8202,7 @@ function makeCompCall(
|
|
|
6209
8202
|
keyExpr,
|
|
6210
8203
|
liteEligible,
|
|
6211
8204
|
singleRoot,
|
|
8205
|
+
maybeSingleRoot,
|
|
6212
8206
|
loc: devLoc(ctx, node),
|
|
6213
8207
|
};
|
|
6214
8208
|
}
|
|
@@ -6220,31 +8214,16 @@ function makeCompCall(
|
|
|
6220
8214
|
function makeTryCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
6221
8215
|
// node.block = try BlockStatement, node.handler = CatchClause (param, resetParam, body),
|
|
6222
8216
|
// node.pending = optional BlockStatement (TSRX `pending { ... }`)
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
// Optional `pending { ... }` arm — compiled like any sub-body.
|
|
6234
|
-
let pendingHelperName = 'null';
|
|
6235
|
-
if (node.pending && node.pending.body && node.pending.body.length > 0) {
|
|
6236
|
-
pendingHelperName = hoistBodyHelper(
|
|
6237
|
-
ctx,
|
|
6238
|
-
inlinedSubs,
|
|
6239
|
-
'__pending',
|
|
6240
|
-
node.pending.body,
|
|
6241
|
-
[],
|
|
6242
|
-
parentNs,
|
|
6243
|
-
cssHash,
|
|
6244
|
-
);
|
|
6245
|
-
}
|
|
6246
|
-
|
|
6247
|
-
let catchHelperName = 'null';
|
|
8217
|
+
//
|
|
8218
|
+
// Phase 2: one shared env tuple for try/pending/catch (see unionEnv). The
|
|
8219
|
+
// catch body's `err`/`reset` destructure (from its own `__props` param) is
|
|
8220
|
+
// built FIRST and included in its analyzed statements, so those bind as
|
|
8221
|
+
// locals, not captures.
|
|
8222
|
+
const tryStmts = node.block.body;
|
|
8223
|
+
const pendingStmts =
|
|
8224
|
+
node.pending && node.pending.body && node.pending.body.length > 0 ? node.pending.body : null;
|
|
8225
|
+
|
|
8226
|
+
let catchBodyStmts = null;
|
|
6248
8227
|
if (node.handler) {
|
|
6249
8228
|
const handler = node.handler;
|
|
6250
8229
|
const errName = handler.param?.name || '_err';
|
|
@@ -6287,19 +8266,58 @@ function makeTryCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
6287
8266
|
},
|
|
6288
8267
|
],
|
|
6289
8268
|
};
|
|
8269
|
+
catchBodyStmts = [destructure, ...catchStmts];
|
|
8270
|
+
}
|
|
8271
|
+
|
|
8272
|
+
const catchParams = [{ type: 'Identifier', name: '__props' }];
|
|
8273
|
+
const envNames = unionEnv(ctx, [
|
|
8274
|
+
{ stmts: tryStmts, params: [] },
|
|
8275
|
+
pendingStmts && { stmts: pendingStmts, params: [] },
|
|
8276
|
+
catchBodyStmts && { stmts: catchBodyStmts, params: catchParams },
|
|
8277
|
+
]);
|
|
8278
|
+
|
|
8279
|
+
const tryHelperName = hoistBodyHelper(
|
|
8280
|
+
ctx,
|
|
8281
|
+
inlinedSubs,
|
|
8282
|
+
'__try',
|
|
8283
|
+
tryStmts,
|
|
8284
|
+
[],
|
|
8285
|
+
parentNs,
|
|
8286
|
+
cssHash,
|
|
8287
|
+
envNames,
|
|
8288
|
+
);
|
|
8289
|
+
|
|
8290
|
+
let pendingHelperName = 'null';
|
|
8291
|
+
if (pendingStmts) {
|
|
8292
|
+
pendingHelperName = hoistBodyHelper(
|
|
8293
|
+
ctx,
|
|
8294
|
+
inlinedSubs,
|
|
8295
|
+
'__pending',
|
|
8296
|
+
pendingStmts,
|
|
8297
|
+
[],
|
|
8298
|
+
parentNs,
|
|
8299
|
+
cssHash,
|
|
8300
|
+
envNames,
|
|
8301
|
+
);
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
let catchHelperName = 'null';
|
|
8305
|
+
if (catchBodyStmts) {
|
|
6290
8306
|
catchHelperName = hoistBodyHelper(
|
|
6291
8307
|
ctx,
|
|
6292
8308
|
inlinedSubs,
|
|
6293
8309
|
'__catch',
|
|
6294
|
-
|
|
6295
|
-
|
|
8310
|
+
catchBodyStmts,
|
|
8311
|
+
catchParams,
|
|
6296
8312
|
parentNs,
|
|
6297
8313
|
cssHash,
|
|
8314
|
+
envNames,
|
|
6298
8315
|
);
|
|
6299
8316
|
}
|
|
6300
8317
|
return {
|
|
6301
8318
|
id: ctx.nextHelperId++,
|
|
6302
8319
|
loc: devLoc(ctx, node),
|
|
8320
|
+
envNames,
|
|
6303
8321
|
tryHelper: tryHelperName,
|
|
6304
8322
|
catchHelper: catchHelperName,
|
|
6305
8323
|
pendingHelper: pendingHelperName,
|
|
@@ -6321,6 +8339,11 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
|
|
|
6321
8339
|
const discExpr = printExpr(node.discriminant);
|
|
6322
8340
|
const caseRecords = [];
|
|
6323
8341
|
let defaultHelper = 'null';
|
|
8342
|
+
// Phase 2: one shared env tuple across every case + default (see unionEnv).
|
|
8343
|
+
const envNames = unionEnv(
|
|
8344
|
+
ctx,
|
|
8345
|
+
(node.cases || []).map((c) => ({ stmts: c.consequent || [], params: [] })),
|
|
8346
|
+
);
|
|
6324
8347
|
for (const c of node.cases || []) {
|
|
6325
8348
|
const stmts = c.consequent || [];
|
|
6326
8349
|
const isDefault = c.test == null;
|
|
@@ -6332,6 +8355,7 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
|
|
|
6332
8355
|
[],
|
|
6333
8356
|
parentNs,
|
|
6334
8357
|
cssHash,
|
|
8358
|
+
envNames,
|
|
6335
8359
|
);
|
|
6336
8360
|
if (isDefault) {
|
|
6337
8361
|
defaultHelper = helperName;
|
|
@@ -6346,6 +8370,7 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
|
|
|
6346
8370
|
loc: devLoc(ctx, node),
|
|
6347
8371
|
discExpr,
|
|
6348
8372
|
discNode: node.discriminant, // AST — the fold threads it as a `props.hN` hole
|
|
8373
|
+
envNames,
|
|
6349
8374
|
casesArrayExpr,
|
|
6350
8375
|
caseRecords, // { testNode, helper } per case — the fold builds the cases hole
|
|
6351
8376
|
defaultHelper,
|
|
@@ -6378,11 +8403,11 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
6378
8403
|
// items.length === 0 the runtime mounts the empty branch in place of the
|
|
6379
8404
|
// (empty) item list; transitioning items → 0 unmounts the chain and mounts
|
|
6380
8405
|
// the empty body, and 0 → items does the reverse.
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
8406
|
+
const emptyStmts = node.empty
|
|
8407
|
+
? node.empty.type === 'BlockStatement'
|
|
8408
|
+
? node.empty.body
|
|
8409
|
+
: [node.empty]
|
|
8410
|
+
: null;
|
|
6386
8411
|
const leftDeclId = node.left.declarations[0].id;
|
|
6387
8412
|
const isDestructured = leftDeclId.type !== 'Identifier';
|
|
6388
8413
|
// `itemName` is the identifier used in the body signature + keyFn. For a
|
|
@@ -6523,19 +8548,51 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
6523
8548
|
}
|
|
6524
8549
|
}
|
|
6525
8550
|
const hasNestedComp = containsComponentCallOrControlFlow(subStmts);
|
|
6526
|
-
|
|
6527
|
-
|
|
8551
|
+
// A render-time CALL disqualifies the survivor short-circuit entirely: the
|
|
8552
|
+
// call can read state neither the item ref nor the deps tuple witnesses
|
|
8553
|
+
// (`header.column.getIsSorted()` flips while `header` stays the memoized
|
|
8554
|
+
// object), so a skipped body would render stale output — React re-runs
|
|
8555
|
+
// bodies unconditionally. Property reads stay eligible (the measured
|
|
8556
|
+
// js-framework-benchmark/dbmon wins are read-only bodies).
|
|
8557
|
+
const hasRenderCall = containsRenderCall(subStmts);
|
|
8558
|
+
pure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
|
|
8559
|
+
depEligible = !pure && hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
|
|
6528
8560
|
depNames.sort();
|
|
6529
8561
|
}
|
|
6530
8562
|
|
|
8563
|
+
// Phase 2: ONE env tuple shared by the item + @empty helpers — it IS the
|
|
8564
|
+
// forBlock `deps` array (deps was already the captured-locals snapshot for
|
|
8565
|
+
// dep-pure promotion; the runtime additionally stamps it as `block.extra`
|
|
8566
|
+
// on every item/empty block). The union may widen deps with @empty-only
|
|
8567
|
+
// captures — a conservative, correct deps for promotion purposes.
|
|
8568
|
+
const itemAllStmts = [...indexInjection, ...destructureInjection, ...subStmts];
|
|
8569
|
+
const itemParams = [{ type: 'Identifier', name: itemName }];
|
|
8570
|
+
const envNames = unionEnv(ctx, [
|
|
8571
|
+
{ stmts: itemAllStmts, params: itemParams },
|
|
8572
|
+
emptyStmts && { stmts: emptyStmts, params: [] },
|
|
8573
|
+
]);
|
|
8574
|
+
let emptyHelperName = 'null';
|
|
8575
|
+
if (emptyStmts) {
|
|
8576
|
+
emptyHelperName = hoistBodyHelper(
|
|
8577
|
+
ctx,
|
|
8578
|
+
inlinedSubs,
|
|
8579
|
+
'__empty',
|
|
8580
|
+
emptyStmts,
|
|
8581
|
+
[],
|
|
8582
|
+
parentNs,
|
|
8583
|
+
cssHash,
|
|
8584
|
+
envNames,
|
|
8585
|
+
);
|
|
8586
|
+
}
|
|
6531
8587
|
const itemHelperName = hoistBodyHelper(
|
|
6532
8588
|
ctx,
|
|
6533
8589
|
inlinedSubs,
|
|
6534
8590
|
'__item',
|
|
6535
|
-
|
|
6536
|
-
|
|
8591
|
+
itemAllStmts,
|
|
8592
|
+
itemParams,
|
|
6537
8593
|
parentNs,
|
|
6538
8594
|
cssHash,
|
|
8595
|
+
envNames,
|
|
6539
8596
|
);
|
|
6540
8597
|
|
|
6541
8598
|
// Single-root detection: when the body emits exactly one Element root and
|
|
@@ -6565,11 +8622,12 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
6565
8622
|
bodyHelper: itemHelperName,
|
|
6566
8623
|
pure,
|
|
6567
8624
|
singleRoot,
|
|
6568
|
-
//
|
|
6569
|
-
//
|
|
6570
|
-
//
|
|
8625
|
+
// The env union doubles as the deps array: emitted whenever the helpers
|
|
8626
|
+
// capture anything (Phase 2 — the runtime stamps it as block.extra), and
|
|
8627
|
+
// ALSO compared for the dep-pure survivor short-circuit when depEligible.
|
|
8628
|
+
// depNames must be exactly the tuple layout the helpers destructure.
|
|
6571
8629
|
depEligible,
|
|
6572
|
-
depNames,
|
|
8630
|
+
depNames: envNames || depNames,
|
|
6573
8631
|
// True only when the header binds NO `index <name>` — the body then can't
|
|
6574
8632
|
// observe an item's position, so a pure reorder (same item ref, position
|
|
6575
8633
|
// changed) need not re-render the survivor. Conservative: an index binding
|