@vesk/compiler 0.2.25 → 0.2.27
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/client-codegen.d.ts +15 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +343 -73
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +22 -1
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +200 -22
- package/dist/ir.d.ts +24 -1
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +28 -0
- package/dist/module-imports.d.ts +22 -1
- package/dist/module-imports.d.ts.map +1 -1
- package/dist/module-imports.js +177 -2
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +57 -24
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +20 -15
- package/package.json +2 -2
package/dist/client-codegen.js
CHANGED
|
@@ -2,13 +2,14 @@ import { walk } from 'zimmerframe';
|
|
|
2
2
|
import { print } from 'esrap';
|
|
3
3
|
import ts from 'esrap/languages/ts';
|
|
4
4
|
import tsx from 'esrap/languages/tsx';
|
|
5
|
-
import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, } from '@vesk/compiler/src/ir';
|
|
5
|
+
import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, PropSlot, PropSlotRender, } from '@vesk/compiler/src/ir';
|
|
6
6
|
import { parse } from '@vesk/compiler/src/parser';
|
|
7
7
|
import { generateIR } from '@vesk/compiler/src/ir-generator';
|
|
8
8
|
import { transformTopLevelForActions } from '@vesk/compiler/src/actions';
|
|
9
9
|
import { extractRuntimeNames, extractTopLevelNames } from '@vesk/compiler/src/server-utils';
|
|
10
10
|
import { importBindingPairs, localValueImportNames } from '@vesk/compiler/src/module-imports';
|
|
11
11
|
import { stripTrackGeneric } from '@vesk/compiler/src/scan';
|
|
12
|
+
import { importModuleTarget } from '@vesk/compiler/src/tokens';
|
|
12
13
|
import { inlineMdImportsFrom } from '@vesk/compiler/src/md-inline';
|
|
13
14
|
import { stripTsTypes, hasTsSyntax } from '@vesk/compiler/src/strip-ts';
|
|
14
15
|
function memberExpr(object, property) {
|
|
@@ -247,12 +248,36 @@ function indent(code, level = 1) {
|
|
|
247
248
|
const pad = '\t'.repeat(level);
|
|
248
249
|
return code.split('\n').map((l) => (l ? pad + l : '')).join('\n');
|
|
249
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Allocates a disjoint `$n` name block per source file. esbuild's bundler
|
|
253
|
+
* renames colliding top-level symbols when it links a chunk, and on 0.25.x
|
|
254
|
+
* that rename can leave references inside nested closures pointing at the
|
|
255
|
+
* pre-rename name — surfacing as `$nNN is not defined` at hydration. Seeding
|
|
256
|
+
* every file from a hash of its path keeps the names globally unique so the
|
|
257
|
+
* renamer has nothing to rename. Callers without a `sourcePath` keep the
|
|
258
|
+
* historical `$n0…` numbering.
|
|
259
|
+
*/
|
|
260
|
+
export function nameAllocFor(sourcePath, start = 0) {
|
|
261
|
+
if (!sourcePath)
|
|
262
|
+
return { c: start };
|
|
263
|
+
let h = 0x811c9dc5;
|
|
264
|
+
for (let i = 0; i < sourcePath.length; i++) {
|
|
265
|
+
h ^= sourcePath.charCodeAt(i);
|
|
266
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
267
|
+
}
|
|
268
|
+
return { c: h * 1_000_000 + start };
|
|
269
|
+
}
|
|
250
270
|
class Ctx {
|
|
251
271
|
lines = [];
|
|
252
272
|
effects = [];
|
|
253
|
-
|
|
273
|
+
alloc;
|
|
254
274
|
importedNames = new Set();
|
|
255
275
|
linkNames = new Set();
|
|
276
|
+
// Runtime component imports that claim their own SSR root via `nextElement`
|
|
277
|
+
// (Link/NavLink/Md/Form/Field/LoadingIndicator) and return a DocumentFragment
|
|
278
|
+
// when claimed. They need no call-site adoption guard — the guard targets
|
|
279
|
+
// genuinely-plain components that never touch the walker.
|
|
280
|
+
selfClaimNames = new Set();
|
|
256
281
|
delegatedEvents = new Set();
|
|
257
282
|
directEvents = new Set();
|
|
258
283
|
hydrate = false;
|
|
@@ -264,12 +289,15 @@ class Ctx {
|
|
|
264
289
|
walker = '__hydrate';
|
|
265
290
|
asyncComps = new Set();
|
|
266
291
|
isAsyncScope = false;
|
|
292
|
+
constructor(alloc) {
|
|
293
|
+
this.alloc = alloc ?? { c: 0 };
|
|
294
|
+
}
|
|
267
295
|
push(...args) {
|
|
268
296
|
for (const a of args)
|
|
269
297
|
if (a)
|
|
270
298
|
this.lines.push(a);
|
|
271
299
|
}
|
|
272
|
-
n() { return `$n${this.c++}`; }
|
|
300
|
+
n() { return `$n${this.alloc.c++}`; }
|
|
273
301
|
getCode() { return this.lines.join('\n'); }
|
|
274
302
|
flushEffects() {
|
|
275
303
|
if (this.effects.length === 0)
|
|
@@ -286,8 +314,17 @@ class Ctx {
|
|
|
286
314
|
lines.push(`if (!document.${guard}) {`);
|
|
287
315
|
lines.push(`\tdocument.${guard} = true;`);
|
|
288
316
|
lines.push(`\tdocument.addEventListener(${JSON.stringify(type)}, (e) => {`);
|
|
289
|
-
|
|
290
|
-
|
|
317
|
+
// Walk up from the target to the nearest element that actually has a
|
|
318
|
+
// handler for this event. Using `closest('[data-vsk-ev]')` alone stops at
|
|
319
|
+
// the nearest event-capable element even when its handler is undefined
|
|
320
|
+
// (`<Button/>` forwards an optional onClick that was never passed), which
|
|
321
|
+
// shadows a real handler on an ancestor (e.g. a Popover trigger wrapper).
|
|
322
|
+
lines.push(`\t\tvar el = e.target;`);
|
|
323
|
+
lines.push(`\t\tif (el && el.nodeType !== 1) el = el.parentElement;`);
|
|
324
|
+
lines.push(`\t\twhile (el && el.nodeType === 1) {`);
|
|
325
|
+
lines.push(`\t\t\tif (el.${prop}) { el.${prop}(e); break; }`);
|
|
326
|
+
lines.push(`\t\t\tel = el.parentElement;`);
|
|
327
|
+
lines.push(`\t\t}`);
|
|
291
328
|
lines.push(`\t});`);
|
|
292
329
|
lines.push(`}`);
|
|
293
330
|
}
|
|
@@ -319,7 +356,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
|
|
|
319
356
|
if (node instanceof OpaqueDynamicRegion)
|
|
320
357
|
return emitOpaque(ctx, node, tracked, effectsVar, parentVar);
|
|
321
358
|
if (node instanceof MapRegion)
|
|
322
|
-
return emitMap(ctx, node, tracked, parentVar);
|
|
359
|
+
return emitMap(ctx, node, tracked, effectsVar, parentVar);
|
|
323
360
|
if (node instanceof ServerBlock)
|
|
324
361
|
return null;
|
|
325
362
|
if (node instanceof ClientBlock) {
|
|
@@ -347,7 +384,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
|
|
|
347
384
|
ctx.push(`if (props.children !== undefined && props.children !== null) {`);
|
|
348
385
|
ctx.push(` if (typeof props.children === 'function') {`);
|
|
349
386
|
ctx.push(` const __child = props.children(${ctx.walker});`);
|
|
350
|
-
ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => $root);`);
|
|
387
|
+
ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => $mount || $root);`);
|
|
351
388
|
ctx.push(` } else {`);
|
|
352
389
|
ctx.push(` ${parentVar}.appendChild(props.children);`);
|
|
353
390
|
ctx.push(` }`);
|
|
@@ -366,6 +403,32 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
|
|
|
366
403
|
return emitForLoop(ctx, node, tracked, parentVar);
|
|
367
404
|
if (node instanceof SwitchBlock)
|
|
368
405
|
return emitSwitchBlock(ctx, node, tracked, parentVar);
|
|
406
|
+
if (node instanceof PropSlot)
|
|
407
|
+
return null;
|
|
408
|
+
if (node instanceof PropSlotRender) {
|
|
409
|
+
const slotProp = `props.${node.propName}`;
|
|
410
|
+
if (!parentVar)
|
|
411
|
+
return null;
|
|
412
|
+
if (ctx.hydrate) {
|
|
413
|
+
ctx.push(`if (${slotProp} !== undefined && ${slotProp} !== null) {`);
|
|
414
|
+
ctx.push(` if (typeof ${slotProp} === 'function') {`);
|
|
415
|
+
ctx.push(` const __child = ${slotProp}(${ctx.walker});`);
|
|
416
|
+
ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => ${parentVar});`);
|
|
417
|
+
ctx.push(` } else if (${slotProp}.nodeType !== undefined) {`);
|
|
418
|
+
ctx.push(` ${parentVar}.appendChild(${slotProp});`);
|
|
419
|
+
ctx.push(` } else {`);
|
|
420
|
+
ctx.push(` ${parentVar}.appendChild(document.createTextNode(String(${slotProp})));`);
|
|
421
|
+
ctx.push(` }`);
|
|
422
|
+
ctx.push(`}`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
ctx.push(`if (${slotProp} !== undefined && ${slotProp} !== null) {`);
|
|
426
|
+
ctx.push(` if (${slotProp}.nodeType !== undefined) ${parentVar}.appendChild(${slotProp});`);
|
|
427
|
+
ctx.push(` else ${parentVar}.appendChild(document.createTextNode(String(${slotProp})));`);
|
|
428
|
+
ctx.push(`}`);
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
369
432
|
return null;
|
|
370
433
|
}
|
|
371
434
|
const PROPERTY_ATTRS = {
|
|
@@ -402,9 +465,15 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
402
465
|
children.push(child);
|
|
403
466
|
}
|
|
404
467
|
}
|
|
468
|
+
const dynAttrNames = new Set();
|
|
469
|
+
for (const d of dynAttrs)
|
|
470
|
+
if (d.target)
|
|
471
|
+
dynAttrNames.add(d.target);
|
|
405
472
|
for (const attr of node.attributes) {
|
|
406
473
|
if (attr.name.startsWith('on') && attr.name.length > 2)
|
|
407
474
|
continue;
|
|
475
|
+
if (dynAttrNames.has(attr.name))
|
|
476
|
+
continue;
|
|
408
477
|
if (attr.value === '') {
|
|
409
478
|
ctx.push(`${el}.setAttribute(${JSON.stringify(attr.name)}, '');`);
|
|
410
479
|
}
|
|
@@ -412,16 +481,48 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
412
481
|
ctx.push(`${el}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(escapeHtml(attr.value))});`);
|
|
413
482
|
}
|
|
414
483
|
}
|
|
484
|
+
// Hydrate-mode A5 snapshot: exactly one text-ish child that is a reactive
|
|
485
|
+
// dynamic binding gets initialized from the claimed parent's SSR text.
|
|
486
|
+
// Computed only in hydrate mode — normal mode pays zero for this.
|
|
487
|
+
let singleReactiveText = false;
|
|
488
|
+
if (ctx.hydrate) {
|
|
489
|
+
const textKids = children.filter((c) => c instanceof TextNode || (c instanceof DynamicBinding && c.kind === 'text'));
|
|
490
|
+
singleReactiveText = textKids.length === 1 && textKids[0] instanceof DynamicBinding
|
|
491
|
+
&& isReactiveExpression(textKids[0].expression, tracked);
|
|
492
|
+
}
|
|
493
|
+
let residueBefore = 0;
|
|
415
494
|
for (const child of children) {
|
|
416
|
-
|
|
495
|
+
let childVar;
|
|
496
|
+
if (singleReactiveText && child instanceof DynamicBinding && child.kind === 'text') {
|
|
497
|
+
childVar = emitDynamicBinding(ctx, child, tracked, effectsVar, `${el}.__vsk_ssrText`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
childVar = emitNode(ctx, child, tracked, effectsVar, el);
|
|
501
|
+
}
|
|
417
502
|
if (childVar) {
|
|
418
503
|
if (ctx.hydrate) {
|
|
419
|
-
|
|
504
|
+
// Fresh text nodes (static or dynamic) cannot be appended at the end of
|
|
505
|
+
// a claimed element: its SSR element children (component-boundary
|
|
506
|
+
// wrappers, skipped static markup) are still in the DOM at their SSR
|
|
507
|
+
// slots, so appending would move the text AFTER them, reordering
|
|
508
|
+
// mixed text + component children (the hero "compiler-first framework ·
|
|
509
|
+
// <VersionBadge />" label). Insert the fresh node at the SSR position
|
|
510
|
+
// of the text instead: the k-th source text child precedes the residue
|
|
511
|
+
// elements rendered before it, so it slots in before
|
|
512
|
+
// `el.__vsk_ssrEls[k]` (the residue list captured by the runtime at
|
|
513
|
+
// claim time).
|
|
514
|
+
if (child instanceof TextNode || child instanceof DynamicBinding) {
|
|
515
|
+
ctx.push(`if (${childVar}.parentNode !== ${el}) { const __ssr = ${el}.__vsk_ssrEls || []; if (${residueBefore} < __ssr.length) ${el}.insertBefore(${childVar}, __ssr[${residueBefore}]); else ${el}.appendChild(${childVar}); }`);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
ctx.push(`if (${childVar}.parentNode !== ${el}) { if (!${el} || ${childVar}.parentNode == null || !${el}.contains(${childVar})) ${el}.appendChild(${childVar}); }`);
|
|
519
|
+
}
|
|
420
520
|
}
|
|
421
521
|
else {
|
|
422
522
|
ctx.push(`${el}.appendChild(${childVar});`);
|
|
423
523
|
}
|
|
424
524
|
}
|
|
525
|
+
residueBefore += ssrResidueEstimate(child);
|
|
425
526
|
}
|
|
426
527
|
for (const attr of dynAttrs) {
|
|
427
528
|
const target = attr.target || '';
|
|
@@ -444,12 +545,22 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
444
545
|
}
|
|
445
546
|
ctx.push(`${el}.setAttribute('data-vsk-ev', '');`);
|
|
446
547
|
}
|
|
548
|
+
else if (target === 'style') {
|
|
549
|
+
const expr = transformTracked(attr.expression, tracked);
|
|
550
|
+
const eff = `effect(() => { applyStyle(${el}, ${expr}); })`;
|
|
551
|
+
if (effectsVar) {
|
|
552
|
+
ctx.push(`${effectsVar}.push(${eff});`);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
ctx.effects.push(`${eff};`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
447
558
|
else {
|
|
448
559
|
const expr = transformTracked(attr.expression, tracked);
|
|
449
560
|
const useProp = PROPERTY_ATTRS[node.tag]?.has(target);
|
|
450
561
|
const eff = useProp
|
|
451
562
|
? `effect(() => { ${el}.${target} = ${expr}; })`
|
|
452
|
-
: `effect(() => { ${el}.setAttribute(${JSON.stringify(target)}, String(
|
|
563
|
+
: `effect(() => { const __v = ${expr}; if (__v != null && __v !== false) ${el}.setAttribute(${JSON.stringify(target)}, __v === true ? 'true' : String(__v)); })`;
|
|
453
564
|
if (effectsVar) {
|
|
454
565
|
ctx.push(`${effectsVar}.push(${eff});`);
|
|
455
566
|
}
|
|
@@ -604,7 +715,7 @@ function isReactiveExpression(node, tracked) {
|
|
|
604
715
|
});
|
|
605
716
|
return reactive;
|
|
606
717
|
}
|
|
607
|
-
function emitDynamicBinding(ctx, node, tracked, effectsVar) {
|
|
718
|
+
function emitDynamicBinding(ctx, node, tracked, effectsVar, ssrTextExpr = null) {
|
|
608
719
|
if (node.kind === 'attribute')
|
|
609
720
|
return null;
|
|
610
721
|
const expr = transformTracked(node.expression, tracked);
|
|
@@ -613,7 +724,13 @@ function emitDynamicBinding(ctx, node, tracked, effectsVar) {
|
|
|
613
724
|
ctx.push(`const ${v} = document.createTextNode(String(${expr}));`);
|
|
614
725
|
return v;
|
|
615
726
|
}
|
|
616
|
-
|
|
727
|
+
// Hydrate-mode sole-dynamic-text snapshot (Hydrate-Todo A5): initialize from
|
|
728
|
+
// the claimed parent's stashed SSR text so a missed effect degrades to stale
|
|
729
|
+
// text instead of an empty element. The effect below still overwrites on its
|
|
730
|
+
// first run, so live values are unaffected. Fresh (unclaimed) parents stash
|
|
731
|
+
// nothing and the `?? ''` keeps the old empty initial.
|
|
732
|
+
const init = ssrTextExpr ? `String(${ssrTextExpr} ?? '')` : `''`;
|
|
733
|
+
ctx.push(`const ${v} = document.createTextNode(${init});`);
|
|
617
734
|
const eff = `effect(() => { ${v}.data = String(${expr}); })`;
|
|
618
735
|
if (effectsVar) {
|
|
619
736
|
ctx.push(`${effectsVar}.push(${eff});`);
|
|
@@ -623,6 +740,29 @@ function emitDynamicBinding(ctx, node, tracked, effectsVar) {
|
|
|
623
740
|
}
|
|
624
741
|
return v;
|
|
625
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Emits a fresh DocumentFragment built from `nodes` (used for both the
|
|
745
|
+
* `children` of a component call and every named content slot). Children
|
|
746
|
+
* effects are collected into `effectsVar` (or `ctx.effects`) exactly as the
|
|
747
|
+
* component-call path does today, so slot bodies inherit identical effects,
|
|
748
|
+
* claim, and async semantics as regular children.
|
|
749
|
+
*/
|
|
750
|
+
function emitFragment(ctx, nodes, tracked, effectsVar) {
|
|
751
|
+
const frag = ctx.n();
|
|
752
|
+
const savedEffects = ctx.effects;
|
|
753
|
+
ctx.effects = [];
|
|
754
|
+
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
755
|
+
for (const child of nodes) {
|
|
756
|
+
const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
|
|
757
|
+
if (childVar)
|
|
758
|
+
ctx.push(`$f.appendChild(${childVar});`);
|
|
759
|
+
}
|
|
760
|
+
for (const eff of ctx.effects)
|
|
761
|
+
ctx.push(effectsVar ? `${effectsVar}.push(${effectBlockToHandlerExpr(eff)});` : eff);
|
|
762
|
+
ctx.effects = savedEffects;
|
|
763
|
+
ctx.push(`return $f; })();`);
|
|
764
|
+
return frag;
|
|
765
|
+
}
|
|
626
766
|
function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components') {
|
|
627
767
|
const propsEntries = node.props.map((p) => {
|
|
628
768
|
if (typeof p.value === 'string')
|
|
@@ -638,37 +778,60 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
638
778
|
const expr = transformTracked(sp, tracked);
|
|
639
779
|
propsEntries.push(`...${expr}`);
|
|
640
780
|
}
|
|
641
|
-
const propsObj = `{ ${propsEntries.join(', ')} }`;
|
|
642
781
|
const v = ctx.n();
|
|
643
|
-
|
|
782
|
+
// Await children whenever the calling scope is async. Async-ness is
|
|
783
|
+
// parent-driven (mirroring the server side) so that imported async
|
|
784
|
+
// components from other files — whose names are not in the local
|
|
785
|
+
// asyncComps set — still resolve to nodes before append/insert.
|
|
786
|
+
const awaitKw = ctx.isAsyncScope ? 'await ' : '';
|
|
644
787
|
// Member-expression tags (`<it.icon>`) carry the raw component-valued
|
|
645
788
|
// expression — invoke it directly; it is never a registry name.
|
|
646
789
|
const calleeExpr = node.calleeExpr ? `(${node.calleeExpr})` : null;
|
|
790
|
+
// Named content slots (`trigger={<Button/>}`) thread through the same channel
|
|
791
|
+
// as `children`: a fresh DocumentFragment per slot hoisted from the IR.
|
|
792
|
+
const slotChildren = node.children.filter((c) => c instanceof PropSlot);
|
|
793
|
+
const regChildren = node.children.filter((c) => !(c instanceof PropSlot));
|
|
794
|
+
const callArgs = () => `{ ${propsEntries.join(', ')} }`;
|
|
647
795
|
if (ctx.hydrate) {
|
|
648
796
|
const access = calleeExpr
|
|
649
797
|
?? (ctx.importedNames.has(node.componentName)
|
|
650
798
|
? node.componentName
|
|
651
799
|
: `__components[${JSON.stringify(node.componentName)}]`);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
800
|
+
// SSR for component calls is marker-ONLY (`<!--vsk-->` + the component's
|
|
801
|
+
// own root; no display:contents wrapper). Compiled `.vsk` components claim
|
|
802
|
+
// their own root on the SHARED walker. Call targets that are not known
|
|
803
|
+
// compiled components (imported values, member-expression tags) cannot
|
|
804
|
+
// claim, so after the call we adopt their SSR root ourselves and replace it
|
|
805
|
+
// with the fresh node they returned — never a duplicate, never a box.
|
|
806
|
+
const plainTarget = !!(calleeExpr || ctx.importedNames.has(node.componentName)) && !ctx.selfClaimNames.has(node.componentName);
|
|
807
|
+
const walkerArg = ctx.walker;
|
|
808
|
+
const maybeReplace = (v) => {
|
|
809
|
+
if (!plainTarget)
|
|
810
|
+
return;
|
|
811
|
+
// nodeType 11 = DocumentFragment: self-claiming imports (Link/Md/Form)
|
|
812
|
+
// return a fragment when they adopted their SSR root, so there is nothing
|
|
813
|
+
// to replace — claiming here would steal the NEXT sibling's root.
|
|
814
|
+
ctx.push(`if (${v} && ${v}.parentNode == null && ${v}.nodeType !== 11) { const __sr = ${walkerArg}.claimOnly(); if (__sr && __sr.parentNode) __sr.parentNode.replaceChild(${v}, __sr); }`);
|
|
815
|
+
};
|
|
816
|
+
const maybeRetire = () => {
|
|
817
|
+
if (ctx.selfClaimNames.has(node.componentName)) {
|
|
818
|
+
ctx.push(`${walkerArg}.retireDetached();`);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
for (const slot of slotChildren) {
|
|
822
|
+
const frag = emitFragment(ctx, slot.body, tracked, effectsVar);
|
|
823
|
+
propsEntries.push(`${JSON.stringify(slot.propName)}: ${frag}`);
|
|
824
|
+
}
|
|
825
|
+
if (regChildren.length > 0) {
|
|
658
826
|
const frag = ctx.n();
|
|
659
|
-
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
660
|
-
const savedWalker = ctx.walker;
|
|
661
|
-
const savedEffects = ctx.effects;
|
|
662
|
-
// Link/NavLink hydrate branches WIPE their SSR anchor (a.replaceChildren())
|
|
663
|
-
// and remount the children fragment. Suppressed fully-static children would
|
|
664
|
-
// therefore be lost, so rebuild the whole subtree fresh instead of claiming.
|
|
665
827
|
const wipeMount = ctx.linkNames.has(node.componentName);
|
|
666
828
|
const savedHydrate = ctx.hydrate;
|
|
667
829
|
if (wipeMount)
|
|
668
830
|
ctx.hydrate = false;
|
|
669
|
-
ctx.
|
|
831
|
+
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
832
|
+
const savedEffects = ctx.effects;
|
|
670
833
|
ctx.effects = [];
|
|
671
|
-
for (const child of
|
|
834
|
+
for (const child of regChildren) {
|
|
672
835
|
const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
|
|
673
836
|
if (childVar)
|
|
674
837
|
ctx.push(`$f.appendChild(${childVar});`);
|
|
@@ -676,23 +839,26 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
676
839
|
for (const eff of ctx.effects)
|
|
677
840
|
ctx.push(effectsVar ? `${effectsVar}.push(${effectBlockToHandlerExpr(eff)});` : eff);
|
|
678
841
|
ctx.effects = savedEffects;
|
|
679
|
-
ctx.walker = savedWalker;
|
|
680
842
|
ctx.hydrate = savedHydrate;
|
|
681
843
|
ctx.push(`return $f; })();`);
|
|
682
844
|
propsEntries.push(`children: ${frag}`);
|
|
683
|
-
ctx.push(`const ${v} = ${awaitKw}${access}({ ${propsEntries.join(', ')} }, __registry, ${walkerVar});`);
|
|
684
|
-
return v;
|
|
685
845
|
}
|
|
686
|
-
ctx.push(`const ${v} = ${awaitKw}${access}(${
|
|
846
|
+
ctx.push(`const ${v} = ${awaitKw}${access}(${callArgs()}, __registry, ${walkerArg});`);
|
|
847
|
+
maybeReplace(v);
|
|
848
|
+
maybeRetire();
|
|
687
849
|
return v;
|
|
688
850
|
}
|
|
689
851
|
else {
|
|
690
|
-
|
|
852
|
+
for (const slot of slotChildren) {
|
|
853
|
+
const frag = emitFragment(ctx, slot.body, tracked, effectsVar);
|
|
854
|
+
propsEntries.push(`${JSON.stringify(slot.propName)}: ${frag}`);
|
|
855
|
+
}
|
|
856
|
+
if (regChildren.length > 0) {
|
|
691
857
|
const frag = ctx.n();
|
|
692
|
-
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
693
858
|
const savedEffects = ctx.effects;
|
|
694
859
|
ctx.effects = [];
|
|
695
|
-
|
|
860
|
+
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
861
|
+
for (const child of regChildren) {
|
|
696
862
|
const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
|
|
697
863
|
if (childVar)
|
|
698
864
|
ctx.push(`$f.appendChild(${childVar});`);
|
|
@@ -704,13 +870,13 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
704
870
|
propsEntries.push(`children: ${frag}`);
|
|
705
871
|
}
|
|
706
872
|
if (calleeExpr) {
|
|
707
|
-
ctx.push(`const ${v} = ${calleeExpr}(
|
|
873
|
+
ctx.push(`const ${v} = ${calleeExpr}(${callArgs()});`);
|
|
708
874
|
}
|
|
709
875
|
else if (ctx.importedNames.has(node.componentName)) {
|
|
710
|
-
ctx.push(`const ${v} = ${awaitKw}${node.componentName}(
|
|
876
|
+
ctx.push(`const ${v} = ${awaitKw}${node.componentName}(${callArgs()});`);
|
|
711
877
|
}
|
|
712
878
|
else {
|
|
713
|
-
ctx.push(`const ${v} = ${awaitKw}${compPrefix}[${JSON.stringify(node.componentName)}](
|
|
879
|
+
ctx.push(`const ${v} = ${awaitKw}${compPrefix}[${JSON.stringify(node.componentName)}](${callArgs()});`);
|
|
714
880
|
}
|
|
715
881
|
}
|
|
716
882
|
return v;
|
|
@@ -1329,7 +1495,7 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
|
|
|
1329
1495
|
}`);
|
|
1330
1496
|
return null;
|
|
1331
1497
|
}
|
|
1332
|
-
function emitMap(ctx, node, tracked, parentVar) {
|
|
1498
|
+
function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
1333
1499
|
const arrExpr = transformTracked(node.expression, tracked);
|
|
1334
1500
|
const itemVar = node.itemVariable;
|
|
1335
1501
|
const anchor = ctx.n();
|
|
@@ -1349,6 +1515,26 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1349
1515
|
// emitting a top-level effect there would reference the loop/statement local
|
|
1350
1516
|
// (`col`, `pages`) which is out of scope — `ReferenceError` at hydrate.
|
|
1351
1517
|
const reactiveSource = isReactiveExpression(node.expression, tracked);
|
|
1518
|
+
// The re-render effect below closes over the anchors/reconciler declared
|
|
1519
|
+
// inline above. When this map is nested inside a branch/item builder those
|
|
1520
|
+
// bindings live in that nested closure — so the effect must be created in
|
|
1521
|
+
// the same scope (registered on the enclosing effects array), not flushed
|
|
1522
|
+
// to the component top level where the names are out of scope
|
|
1523
|
+
// (`$nNN is not defined` at hydration, plus an esbuild shadowing rename).
|
|
1524
|
+
const pushEffect = (lets, effCall) => {
|
|
1525
|
+
if (effTarget) {
|
|
1526
|
+
ctx.push(`${effTarget}.push((() => {
|
|
1527
|
+
${lets}
|
|
1528
|
+
return ${effCall};
|
|
1529
|
+
})());`);
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
ctx.effects.push(`{
|
|
1533
|
+
${lets}
|
|
1534
|
+
${effCall};
|
|
1535
|
+
}`);
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1352
1538
|
ctx.push(`const ${anchor} = document.createComment('map');`);
|
|
1353
1539
|
if (!hyd && !hydKeyed)
|
|
1354
1540
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
@@ -1436,6 +1622,13 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1436
1622
|
ctx.push(`const ${hasItems} = () => { const __l = ${arrExpr}; return __l != null && __l.length > 0; };`);
|
|
1437
1623
|
if (node.keyExpr) {
|
|
1438
1624
|
const keyExpr = transformTracked(node.keyExpr, tracked);
|
|
1625
|
+
// A `key` may reference the loop's `index` variable (`for (… ; key i; index i)`).
|
|
1626
|
+
// The key function is evaluated outside the item renderer (where the index is
|
|
1627
|
+
// in scope), so it must receive the index as its own parameter — otherwise the
|
|
1628
|
+
// emitted `item => i` key closes over an undefined `i` (ReferenceError).
|
|
1629
|
+
// Parenthesize the parameters: without them `item, i => …` parses as a comma
|
|
1630
|
+
// sequence expression rather than a two-parameter arrow in argument position.
|
|
1631
|
+
const keyFnSrc = `(${itemVar}${node.indexVariable ? `, ${node.indexVariable}` : ''}) => ${keyExpr}`;
|
|
1439
1632
|
const reconciler = ctx.n();
|
|
1440
1633
|
const initList = ctx.n();
|
|
1441
1634
|
ctx.push(`let ${reconciler} = () => {};`);
|
|
@@ -1444,10 +1637,10 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1444
1637
|
// inside `parent` (pre-attach, `anchor.parentNode` is null) and peeks all
|
|
1445
1638
|
// keys before claim-by-key consumes the walker, so server-rendered items
|
|
1446
1639
|
// are adopted and wired in place instead of being rebuilt.
|
|
1447
|
-
ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${
|
|
1640
|
+
ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e, __root) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'}, null, __root), __hydrate, ${parent}); };`);
|
|
1448
1641
|
}
|
|
1449
1642
|
else {
|
|
1450
|
-
ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${
|
|
1643
|
+
ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
|
|
1451
1644
|
}
|
|
1452
1645
|
if (emptyRenderName) {
|
|
1453
1646
|
const isEmptyVar = ctx.n();
|
|
@@ -1455,9 +1648,7 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1455
1648
|
ctx.push(`if (!${isEmptyVar}) { ${initList}(); } else { ${aw}${emptyRenderName}(); }`);
|
|
1456
1649
|
if (!reactiveSource)
|
|
1457
1650
|
return null;
|
|
1458
|
-
|
|
1459
|
-
let __first = true;
|
|
1460
|
-
effect(${effOpen}
|
|
1651
|
+
pushEffect(`let __first = true;`, `effect(${effOpen}
|
|
1461
1652
|
const __new = ${hasItems}();
|
|
1462
1653
|
if (__first) { __first = false; __new; return; }
|
|
1463
1654
|
if (__new !== ${isEmptyVar}) {
|
|
@@ -1469,21 +1660,17 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1469
1660
|
${effectsVar}.length = 0;
|
|
1470
1661
|
__cleanup(${anchor}, ${endAnchor});
|
|
1471
1662
|
if (__new) { ${initList}(); } else { ${aw}${emptyRenderName}(); }
|
|
1472
|
-
});
|
|
1473
|
-
}`);
|
|
1663
|
+
})`);
|
|
1474
1664
|
}
|
|
1475
1665
|
else {
|
|
1476
1666
|
ctx.push(`${initList}();`);
|
|
1477
1667
|
if (!reactiveSource)
|
|
1478
1668
|
return null;
|
|
1479
|
-
|
|
1480
|
-
let __first = true;
|
|
1481
|
-
effect(() => {
|
|
1669
|
+
pushEffect(`let __first = true;`, `effect(() => {
|
|
1482
1670
|
const __nv = ${arrExpr};
|
|
1483
1671
|
if (__first) { __first = false; __nv; ${reconciler}(${arrExpr}); return; }
|
|
1484
1672
|
${reconciler}(${arrExpr});
|
|
1485
|
-
});
|
|
1486
|
-
}`);
|
|
1673
|
+
})`);
|
|
1487
1674
|
}
|
|
1488
1675
|
}
|
|
1489
1676
|
else {
|
|
@@ -1516,9 +1703,7 @@ function emitMap(ctx, node, tracked, parentVar) {
|
|
|
1516
1703
|
ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
|
|
1517
1704
|
if (!reactiveSource)
|
|
1518
1705
|
return null;
|
|
1519
|
-
|
|
1520
|
-
let __first = true;
|
|
1521
|
-
effect(${effOpen}
|
|
1706
|
+
pushEffect(`let __first = true;`, `effect(${effOpen}
|
|
1522
1707
|
const __new = ${hasItems}();
|
|
1523
1708
|
if (__first) { __first = false; __new; return; }
|
|
1524
1709
|
if (__new !== ${isEmptyVar}) {
|
|
@@ -1537,8 +1722,7 @@ ${renderAllItems(' ', null)}
|
|
|
1537
1722
|
if (__new) {
|
|
1538
1723
|
${renderAllItems(' ', null)}
|
|
1539
1724
|
} else { ${aw}${emptyRenderName}(${collectVar ? collectVar : ''}); }
|
|
1540
|
-
});
|
|
1541
|
-
}`);
|
|
1725
|
+
})`);
|
|
1542
1726
|
}
|
|
1543
1727
|
else {
|
|
1544
1728
|
const collectVar = hyd ? ctx.n() : null;
|
|
@@ -1549,17 +1733,14 @@ ${renderAllItems(' ', null)}
|
|
|
1549
1733
|
ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
|
|
1550
1734
|
if (!reactiveSource)
|
|
1551
1735
|
return null;
|
|
1552
|
-
|
|
1553
|
-
let __first = true;
|
|
1554
|
-
effect(${effOpen}
|
|
1736
|
+
pushEffect(`let __first = true;`, `effect(${effOpen}
|
|
1555
1737
|
const __nv = ${arrExpr};
|
|
1556
1738
|
if (__first) { __first = false; __nv; return; }
|
|
1557
1739
|
for (const e of ${effectsVar}) destroy_block(e);
|
|
1558
1740
|
${effectsVar}.length = 0;
|
|
1559
1741
|
__cleanup(${anchor}, ${endAnchor});
|
|
1560
1742
|
${renderAllItems(' ', null)}
|
|
1561
|
-
});
|
|
1562
|
-
}`);
|
|
1743
|
+
})`);
|
|
1563
1744
|
}
|
|
1564
1745
|
}
|
|
1565
1746
|
return null;
|
|
@@ -1567,11 +1748,12 @@ ${renderAllItems(' ', null)}
|
|
|
1567
1748
|
function computeAsyncComponents(comps) {
|
|
1568
1749
|
return new Set(comps.filter((c) => c.isAsync).map((c) => c.name));
|
|
1569
1750
|
}
|
|
1570
|
-
function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set()) {
|
|
1751
|
+
function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set(), selfClaimNames = new Set(), alloc) {
|
|
1571
1752
|
const tracked = collectTrackedNames(comp.body);
|
|
1572
|
-
const ctx = new Ctx();
|
|
1753
|
+
const ctx = new Ctx(alloc);
|
|
1573
1754
|
ctx.importedNames = importedNames;
|
|
1574
1755
|
ctx.linkNames = linkNames;
|
|
1756
|
+
ctx.selfClaimNames = selfClaimNames;
|
|
1575
1757
|
ctx.hydrate = hydrate;
|
|
1576
1758
|
ctx.asyncComps = asyncComps;
|
|
1577
1759
|
ctx.isAsyncScope = comp.isAsync || asyncComps.has(comp.name);
|
|
@@ -1587,7 +1769,16 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
|
|
|
1587
1769
|
ctx.push(indent(`}`));
|
|
1588
1770
|
}
|
|
1589
1771
|
if (ctx.hydrate) {
|
|
1772
|
+
// Two distinct roles must not be conflated:
|
|
1773
|
+
// - `$root` is the shared page container. It is only ever a last-resort
|
|
1774
|
+
// mount target for detached dynamic regions (`__place`) that could not
|
|
1775
|
+
// be claimed in place; it must never be handed back to a caller.
|
|
1776
|
+
// - `$mount` is THIS component's own first top-level node. It is what the
|
|
1777
|
+
// hydrator returns so the parent can position/guard the component. A
|
|
1778
|
+
// component's own node is never an ancestor of the caller, so the
|
|
1779
|
+
// parent's `contains()` guards and `__place` end-anchor bounds are valid.
|
|
1590
1780
|
ctx.push(indent(`const $root = __hydrate.root;`));
|
|
1781
|
+
ctx.push(indent(`let $mount = null;`));
|
|
1591
1782
|
}
|
|
1592
1783
|
else {
|
|
1593
1784
|
ctx.push(indent(`const $root = document.createDocumentFragment();`));
|
|
@@ -1596,11 +1787,23 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
|
|
|
1596
1787
|
const paramInit = buildParamInit(comp.paramNames);
|
|
1597
1788
|
if (paramInit)
|
|
1598
1789
|
ctx.push(indent(paramInit));
|
|
1790
|
+
// Hoist TrackDecls to top to avoid TDZ when Head/effects reference them before declaration
|
|
1599
1791
|
for (const node of comp.body) {
|
|
1792
|
+
if (node instanceof TrackDecl) {
|
|
1793
|
+
emitNode(ctx, node, tracked, null);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
for (const node of comp.body) {
|
|
1797
|
+
if (node instanceof TrackDecl)
|
|
1798
|
+
continue;
|
|
1600
1799
|
const v = emitNode(ctx, node, tracked, null);
|
|
1601
1800
|
if (v) {
|
|
1602
1801
|
if (ctx.hydrate) {
|
|
1603
|
-
|
|
1802
|
+
// The first top-level node becomes `$mount`. Late top-level nodes are
|
|
1803
|
+
// already in the DOM when claimed; only a divergent fresh node is
|
|
1804
|
+
// nested onto the mount (matching the pre-marker wrapper behaviour).
|
|
1805
|
+
ctx.push(indent(`if ($mount === null) $mount = ${v};`));
|
|
1806
|
+
ctx.push(indent(`if (${v} !== $mount && ${v}.parentNode == null) $mount.appendChild(${v});`));
|
|
1604
1807
|
}
|
|
1605
1808
|
else {
|
|
1606
1809
|
ctx.push(indent(`$root.appendChild(${v});`));
|
|
@@ -1613,7 +1816,15 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
|
|
|
1613
1816
|
const delCode = ctx.emitDelegates();
|
|
1614
1817
|
if (delCode)
|
|
1615
1818
|
ctx.push(indent(delCode.trim()));
|
|
1616
|
-
ctx.
|
|
1819
|
+
if (ctx.hydrate) {
|
|
1820
|
+
// A hydrator with no claimable nodes must still hand its caller a value the
|
|
1821
|
+
// parent guards can safely inspect (an empty fragment places nothing).
|
|
1822
|
+
ctx.push(indent(`if ($mount === null) $mount = document.createDocumentFragment();`));
|
|
1823
|
+
ctx.push(indent(`return __pendingChild || $mount;`));
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
ctx.push(indent(`return __pendingChild || $root;`));
|
|
1827
|
+
}
|
|
1617
1828
|
ctx.push(indent(`} finally {`));
|
|
1618
1829
|
ctx.push(indent(`\tsetActiveComponent(__prev);`));
|
|
1619
1830
|
ctx.push(indent(`}`));
|
|
@@ -1627,7 +1838,7 @@ function buildParamInit(paramNames) {
|
|
|
1627
1838
|
return '';
|
|
1628
1839
|
return `const { ${paramNames.join(', ')} } = props;`;
|
|
1629
1840
|
}
|
|
1630
|
-
function buildComponentMap(irRoot, hydrate = false) {
|
|
1841
|
+
function buildComponentMap(irRoot, hydrate = false, alloc) {
|
|
1631
1842
|
const mapLines = [];
|
|
1632
1843
|
mapLines.push(`const __components = {};`);
|
|
1633
1844
|
const asyncComps = computeAsyncComponents(irRoot.components);
|
|
@@ -1635,7 +1846,19 @@ function buildComponentMap(irRoot, hydrate = false) {
|
|
|
1635
1846
|
// hydration, so their children fragments must be rebuilt in full. Track the
|
|
1636
1847
|
// file-local binding names regardless of aliasing (`const Nav = NavLink`).
|
|
1637
1848
|
const linkNames = new Set();
|
|
1849
|
+
// Runtime components that self-claim during hydration (adopt their own SSR
|
|
1850
|
+
// root via nextElement and return a fragment when claimed). Excluded from the
|
|
1851
|
+
// call-site adoption guard; the walker cursor passes them but never a
|
|
1852
|
+
// claimOnly() on their behalf.
|
|
1853
|
+
const selfClaimNames = new Set();
|
|
1854
|
+
const SELF_CLAIM_RUNTIME = new Set(['Link', 'NavLink', 'Md', 'Form', 'Field', 'LoadingIndicator']);
|
|
1638
1855
|
for (const imp of irRoot.imports) {
|
|
1856
|
+
if (importModuleTarget(imp) !== '@vesk/runtime')
|
|
1857
|
+
continue;
|
|
1858
|
+
for (const pair of importBindingPairs(imp)) {
|
|
1859
|
+
if (SELF_CLAIM_RUNTIME.has(pair.imported))
|
|
1860
|
+
selfClaimNames.add(pair.local);
|
|
1861
|
+
}
|
|
1639
1862
|
for (const pair of importBindingPairs(imp)) {
|
|
1640
1863
|
if (pair.imported === 'Link' || pair.imported === 'NavLink')
|
|
1641
1864
|
linkNames.add(pair.local);
|
|
@@ -1650,11 +1873,13 @@ function buildComponentMap(irRoot, hydrate = false) {
|
|
|
1650
1873
|
const directNames = new Set([...extractRuntimeNames(irRoot.imports), ...localValueImportNames(irRoot.imports), ...topValueNames]);
|
|
1651
1874
|
for (const comp of irRoot.components) {
|
|
1652
1875
|
if (hydrate && isStaticComponent(comp)) {
|
|
1653
|
-
|
|
1876
|
+
// Static components keep their SSR content untouched: claim the root
|
|
1877
|
+
// WITHOUT stripping its direct text (nothing will re-create it).
|
|
1878
|
+
const stub = `(props, __registry, __hydrate) => { return __hydrate.claimOnly(); }`;
|
|
1654
1879
|
mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${stub};`);
|
|
1655
1880
|
continue;
|
|
1656
1881
|
}
|
|
1657
|
-
const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames);
|
|
1882
|
+
const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames, selfClaimNames, alloc);
|
|
1658
1883
|
mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${code};`);
|
|
1659
1884
|
}
|
|
1660
1885
|
mapLines.push(`function __cleanup(start, end) {`);
|
|
@@ -1683,6 +1908,40 @@ function buildComponentMap(irRoot, hydrate = false) {
|
|
|
1683
1908
|
mapLines.push(`}`);
|
|
1684
1909
|
return mapLines.join('\n\n');
|
|
1685
1910
|
}
|
|
1911
|
+
function ssrResidueEstimate(node) {
|
|
1912
|
+
if (Array.isArray(node)) {
|
|
1913
|
+
let total = 0;
|
|
1914
|
+
for (const n of node)
|
|
1915
|
+
total += ssrResidueEstimate(n);
|
|
1916
|
+
return total;
|
|
1917
|
+
}
|
|
1918
|
+
if (node instanceof StaticNode)
|
|
1919
|
+
return 1;
|
|
1920
|
+
if (node instanceof ComponentCall)
|
|
1921
|
+
return 1;
|
|
1922
|
+
if (node instanceof OpaqueDynamicRegion) {
|
|
1923
|
+
return Math.max(ssrResidueEstimate(node.consequentNodes), ssrResidueEstimate(node.alternateNodes));
|
|
1924
|
+
}
|
|
1925
|
+
if (node instanceof MapRegion) {
|
|
1926
|
+
return Math.max(ssrResidueEstimate(node.bodyTemplate), ssrResidueEstimate(node.alternateNodes));
|
|
1927
|
+
}
|
|
1928
|
+
if (node instanceof ForLoop)
|
|
1929
|
+
return ssrResidueEstimate(node.bodyTemplate);
|
|
1930
|
+
if (node instanceof WhileLoop)
|
|
1931
|
+
return ssrResidueEstimate(node.bodyTemplate);
|
|
1932
|
+
if (node instanceof SwitchBlock) {
|
|
1933
|
+
let max = 0;
|
|
1934
|
+
for (const c of node.cases)
|
|
1935
|
+
max = Math.max(max, ssrResidueEstimate(c.body));
|
|
1936
|
+
return max;
|
|
1937
|
+
}
|
|
1938
|
+
if (node instanceof TryCatch) {
|
|
1939
|
+
return Math.max(ssrResidueEstimate(node.bodyTemplate), ssrResidueEstimate(node.catchBody));
|
|
1940
|
+
}
|
|
1941
|
+
// TextNode, DynamicBinding, TrackDecl, RuntimeStatement, SlotNode,
|
|
1942
|
+
// ServerBlock, ClientBlock, HeadBlock emit no SSR element residue.
|
|
1943
|
+
return 0;
|
|
1944
|
+
}
|
|
1686
1945
|
function isStaticIR(body) {
|
|
1687
1946
|
for (const node of body) {
|
|
1688
1947
|
if (node instanceof StaticNode) {
|
|
@@ -1778,6 +2037,10 @@ function findBindingInIR(nodes, names) {
|
|
|
1778
2037
|
if (findBindingInIR(node.children, names))
|
|
1779
2038
|
return true;
|
|
1780
2039
|
}
|
|
2040
|
+
else if (node instanceof PropSlot) {
|
|
2041
|
+
if (findBindingInIR(node.body, names))
|
|
2042
|
+
return true;
|
|
2043
|
+
}
|
|
1781
2044
|
else if (node instanceof RuntimeStatement) {
|
|
1782
2045
|
if (astHasBinding(node.ast, names))
|
|
1783
2046
|
return true;
|
|
@@ -1806,6 +2069,10 @@ function findFormNameInIR(nodes, name) {
|
|
|
1806
2069
|
if (findFormNameInIR(node.children, name))
|
|
1807
2070
|
return true;
|
|
1808
2071
|
}
|
|
2072
|
+
if (node instanceof PropSlot) {
|
|
2073
|
+
if (findFormNameInIR(node.body, name))
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
1809
2076
|
if (node instanceof DynamicBinding && node.expression && node.expression.raw) {
|
|
1810
2077
|
if (node.expression.raw.includes(fnPattern))
|
|
1811
2078
|
return true;
|
|
@@ -1884,7 +2151,7 @@ function emitClientFromIR(ir, options) {
|
|
|
1884
2151
|
if (!options.forceClient && !needsClient) {
|
|
1885
2152
|
return '';
|
|
1886
2153
|
}
|
|
1887
|
-
const componentMapCode = buildComponentMap(ir, options.hydrate);
|
|
2154
|
+
const componentMapCode = buildComponentMap(ir, options.hydrate, options.nameAllocator);
|
|
1888
2155
|
const importLines = ir.imports.length > 0 ? ir.imports.join('\n') + '\n' : '';
|
|
1889
2156
|
const topCode = (options.includeTopLevel === false ? [] : transformTopLevelForActions(ir.topLevelCode, 'client')).join('\n') + '\n';
|
|
1890
2157
|
const exportLines = [];
|
|
@@ -1899,7 +2166,7 @@ function emitClientFromIR(ir, options) {
|
|
|
1899
2166
|
}
|
|
1900
2167
|
}
|
|
1901
2168
|
const exportCode = exportLines.join('\n');
|
|
1902
|
-
const runtimeNames = ['track', 'get', 'set', 'destroy_block', 'getActiveComponent', 'setActiveComponent', 'reactiveProps'];
|
|
2169
|
+
const runtimeNames = ['track', 'get', 'set', 'destroy_block', 'getActiveComponent', 'setActiveComponent', 'reactiveProps', 'applyStyle'];
|
|
1903
2170
|
if (ir.components.some(c => !isStaticIR(c.body)))
|
|
1904
2171
|
runtimeNames.push('effect');
|
|
1905
2172
|
for (const name of usedRuntimeBindings(ir))
|
|
@@ -1950,7 +2217,7 @@ export function compileClient(source, _componentName, options = {}) {
|
|
|
1950
2217
|
}
|
|
1951
2218
|
const ast = parse(source, options.sourcePath ? { filename: options.sourcePath } : {});
|
|
1952
2219
|
const ir = generateIR(ast, source, options.sourcePath);
|
|
1953
|
-
return emitClientFromIR(ir, options);
|
|
2220
|
+
return emitClientFromIR(ir, { ...options, nameAllocator: options.nameAllocator ?? nameAllocFor(options.sourcePath) });
|
|
1954
2221
|
}
|
|
1955
2222
|
/**
|
|
1956
2223
|
* Compiles a component source in BOTH client modes (plain + hydrate)
|
|
@@ -1979,9 +2246,12 @@ export function compileClientBoth(source, _componentName, sourcePath) {
|
|
|
1979
2246
|
if (exportedComp)
|
|
1980
2247
|
name = exportedComp.name;
|
|
1981
2248
|
}
|
|
2249
|
+
// Comp and hyd land in the same scoped file block, so they share one
|
|
2250
|
+
// allocator: hyd continues after the names comp already consumed.
|
|
2251
|
+
const alloc = nameAllocFor(sourcePath);
|
|
1982
2252
|
return {
|
|
1983
|
-
comp: emitClientFromIR(ir, { forceClient: true }),
|
|
1984
|
-
hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false }),
|
|
2253
|
+
comp: emitClientFromIR(ir, { forceClient: true, nameAllocator: alloc }),
|
|
2254
|
+
hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false, nameAllocator: alloc }),
|
|
1985
2255
|
name,
|
|
1986
2256
|
};
|
|
1987
2257
|
}
|