@dom-expressions/babel-plugin-jsx 0.50.0-next.15 → 0.50.0-next.17

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/index.js +247 -66
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # babel-plugin-jsx-dom-expressions
2
2
 
3
+ ## 0.50.0-next.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 203c9d5: Fix two option-handling bugs surfaced by the compiler parity sweep:
8
+ - `memoWrapper: false` no longer crashes when a conditional or logical expression is transformed (`transformCondition` registered an import under the falsy wrapper name). Conditions now compile memo-less: hoisted conditions keep a plain `var _c$ = () => !!cond` thunk and inline conditions collapse to an immediately-invoked thunk.
9
+ - `inlineStyles: false` no longer silently drops a literal `style` on a child element whose position otherwise allocates no element reference (e.g. `<svg><rect style="fill:red"/><g/></svg>` lost the style entirely). `detectExpressions` now accounts for the style-to-IIFE rewrite, so the element gets a reference and the style compiles to the expected effect.
10
+
11
+ The `effectWrapper`/`memoWrapper` config types now admit `false` alongside the import-name string.
12
+
13
+ - bb7b2fd: Fix omitLastClosingTag corrupting templates when per-slot insertion markers follow the last static element. An element trailed by two or more dynamic slots now keeps its closing tag, so the trailing `<!>` placeholders parse as its siblings instead of being swallowed as children of the still-open element (which crashed the template walk with "Cannot read properties of null (reading 'nextSibling')").
14
+ - 4686501: Fix hydration id drift for spread elements with dynamic children. The ssr generate's spread-element path (`ssrElement`) never applied the hole id `scope()` wrap that `transformChildren` applies on the template path — while the dom generate scope-wraps the matching insert accessor regardless of spread. For a shape like `<a {...props}>{children()}</a>`, the client reserved one hydration id for the hole and the server did not, so every hydration key allocated after the hole drifted and the following siblings were left unclaimed (duplicated DOM, "unclaimed server-rendered node" warnings). `createElement` now wraps dynamic, id-allocating children holes in `scope()` exactly like the template path.
15
+ - 241ff76: Fix a spread element with dynamic props being left unclaimed on hydration. `mergeProps` with a function source creates a memo, which consumes a hydration child id. The ssr generate evaluated `mergeProps(...)` in `ssrElement`'s argument position — before the element's own hydration key was allocated — while the client claims the element (`getNextElement`) before applying the spread. The element's id shifted by one on the server and the client re-created it instead of claiming (later siblings re-synced, hiding the drift; a `<title>` rendered this way duplicated on every hydration). The ssr generate now defers the merge behind a thunk when hydratable and `ssrElement` allocates the hydration key before resolving function props, matching the client's allocation order.
16
+
17
+ ## 0.50.0-next.16
18
+
19
+ ### Patch Changes
20
+
21
+ - 04849df: Preserve JS value semantics for wrapped `&&` conditions (#532). The dom generate's condition wrap used to emit `memo(() => !!left)() && right`, collapsing every falsy left value to `false` — visibly wrong for component props (`undefined` became `false`, breaking `== null` checks) and a hydration mismatch against the untransformed server output (`{0 && <div/>}` rendered "0" on the server, nothing on the client). `left && right` is exactly `left ? right : left`, so the wrap now emits `memo(() => !!left)() ? right : left`: branching still keys off the memoized truthiness (truthy-value churn never re-creates the right side) while the alternate returns the raw left, matching the server for free. Statically boolean lefts (comparisons, `!x`) keep the plain `memo(() => left)() && right` form — the memo's value is the expression's value, so it's already exact with no second evaluation. Ported identically to the Rust jsx-compiler.
22
+ - 248f784: fix(compiler): key the hole `scope()` wrap off the transform's `dynamic` flag instead of the transformed expression shape. The dom generate simplifies `{sig()}` to the bare getter `sig`, which the old `isDeferredChildSlotExpression` predicate didn't count as deferred while the matching ssr arrow was — so the server scope-wrapped the hole and the client didn't, shifting every sibling hydration id after it. Bare getters are re-wrapped as `() => sig()` on the dom side so `scope()` doesn't tag the user's function.
23
+ - c2a542b: Fix hydration key mismatches when async holes defer past eager siblings
24
+ (solidjs/solid#2801 bug 2). Dynamic element children that can allocate
25
+ hydration ids (conditionals, component-children access, call expressions)
26
+ are now compiled with their own id scope on both generates: the dom and ssr
27
+ generates wrap the hole expression in a new `scope()` runtime helper using a
28
+ shared predicate, so marking cannot desync.
29
+
30
+ On the client, `scope(fn)` tags the accessor and `insert()` makes the outer
31
+ render effect non-transparent (its own id scope) for tagged accessors; the
32
+ inner unwrapping effect stays transparent so content ids keep a fixed depth.
33
+ On the server, `scope` (framework-provided via rxcore as `ssrScope`) reserves
34
+ one id slot at registration and evaluates the hole — including async retries
35
+ — under that reserved id with a zeroed child counter, so retry timing can no
36
+ longer shift sibling ids. The ssr generate's `orderedInsert` sibling
37
+ thunk-wrapping is removed; it is superseded by hole scopes.
38
+
39
+ Hole content ids gain one nesting level (e.g. `_hk=10` instead of `_hk=1`)
40
+ identically on both sides. rxcore implementations must provide an `ssrScope`
41
+ export and honor a `scope: true` effect option (mapped to a non-transparent
42
+ render effect).
43
+
44
+ - bb7470e: Give every dynamic child slot its own insertion marker when a parent hosts more than one (solidjs/solid#2830). Adjacent expression slots used to share a marker (`null` at the tail, a shared following sibling, or one reused `<!>` between text), which collapsed them into a single `$$SLOT` ownership region: a node migrating between adjacent slots was destroyed by the slot it left, arrays exchanging members could throw `NotFoundError`, and a slot emptied via `[]` refilled at the wrong position. Slots in multi-slot parents now ride the immediately following static sibling or get a dedicated `<!>` placeholder — the same per-slot geometry hydratable output has always produced, which is why these shapes already worked after hydration. Zero runtime changes; single-slot parents compile byte-identically to before.
45
+ - 668264f: Universal JSX now passes compile-time static host props to `createElement(tag, staticProps)` so custom renderers can configure nodes before children are inserted. Dynamic props and elements with spreads continue to use the existing `setProp` / `spread` paths.
46
+
3
47
  ## 0.50.0-next.15
4
48
 
5
49
  ### Patch Changes
package/index.js CHANGED
@@ -444,6 +444,34 @@ function toEventName(name) {
444
444
  return name.slice(2).toLowerCase();
445
445
  }
446
446
 
447
+ // === Hole-scope predicates (shared by dom + ssr generates) ===
448
+ //
449
+ // A dynamic element child ("hole") whose evaluation can allocate hydration
450
+ // ids (create hydratable elements, run components, register memos) gets its
451
+ // own owner scope so server and client allocate identical ids regardless of
452
+ // WHEN the hole evaluates (eager, deferred by async, or re-run). Both
453
+ // generates must agree exactly on which holes qualify, so the predicates
454
+ // live here.
455
+
456
+ function canReturnHydratableChild(node) {
457
+ if (t__namespace.isTSNonNullExpression(node) || t__namespace.isTSAsExpression(node) || t__namespace.isTSSatisfiesExpression(node))
458
+ return canReturnHydratableChild(node.expression);
459
+ if (t__namespace.isJSXElement(node) || t__namespace.isJSXFragment(node) || t__namespace.isCallExpression(node)) return true;
460
+ if (t__namespace.isMemberExpression(node) || t__namespace.isOptionalMemberExpression(node)) {
461
+ return !node.computed && t__namespace.isIdentifier(node.property, { name: "children" });
462
+ }
463
+ if (t__namespace.isConditionalExpression(node)) {
464
+ return canReturnHydratableChild(node.consequent) || canReturnHydratableChild(node.alternate);
465
+ }
466
+ return t__namespace.isLogicalExpression(node) && canReturnHydratableChild(node.right);
467
+ }
468
+
469
+ function canChildSlotAllocateIds(node) {
470
+ if (node.isJSXElement() || node.isJSXFragment()) return true;
471
+ if (node.isJSXSpreadChild()) return true;
472
+ return node.isJSXExpressionContainer() && canReturnHydratableChild(node.node.expression);
473
+ }
474
+
447
475
  function wrappedByText(list, startIndex) {
448
476
  let index = startIndex,
449
477
  wrapped;
@@ -467,6 +495,30 @@ function wrappedByText(list, startIndex) {
467
495
  return false;
468
496
  }
469
497
 
498
+ const BOOLEAN_BINARY_OPS = new Set([
499
+ "==",
500
+ "!=",
501
+ "===",
502
+ "!==",
503
+ "<",
504
+ ">",
505
+ "<=",
506
+ ">=",
507
+ "instanceof",
508
+ "in"]
509
+ );
510
+
511
+ // Statically guaranteed to evaluate to a boolean: the memoized value IS the
512
+ // expression's value (`false` is the only falsy boolean), so no `!!` coercion
513
+ // is needed and the `&&` wrap can keep the logical form instead of the
514
+ // value-preserving ternary (no second evaluation of the left).
515
+ function isBooleanExpression(node) {
516
+ return (
517
+ t__namespace.isBinaryExpression(node) && BOOLEAN_BINARY_OPS.has(node.operator) ||
518
+ t__namespace.isUnaryExpression(node) && node.operator === "!");
519
+
520
+ }
521
+
470
522
 
471
523
 
472
524
 
@@ -481,8 +533,12 @@ path,
481
533
  inline)
482
534
  {
483
535
  const config = getConfig(path);
484
- const expr = path.node;
485
- const memo = registerImportMethod(path, config.memoWrapper, undefined);
536
+ let expr = path.node;
537
+ // memoWrapper: false compiles memo-less inline memos collapse to plain
538
+ // thunks and the hoisted declaration keeps its bare arrow.
539
+ const memo = config.memoWrapper ?
540
+ registerImportMethod(path, config.memoWrapper, undefined) :
541
+ undefined;
486
542
  let dTest,
487
543
  cond,
488
544
  id;
@@ -497,10 +553,12 @@ inline)
497
553
  dTest = isDynamic(path.get("test"), { checkMember: true });
498
554
  if (dTest) {
499
555
  cond = expr.test;
500
- if (!t__namespace.isBinaryExpression(cond))
556
+ if (!isBooleanExpression(cond))
501
557
  cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
502
558
  id = inline ?
559
+ memo ?
503
560
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
561
+ t__namespace.arrowFunctionExpression([], cond) :
504
562
  path.scope.generateUidIdentifier("_c$");
505
563
  expr.test = t__namespace.callExpression(id, []);
506
564
  if (t__namespace.isConditionalExpression(expr.consequent) || t__namespace.isLogicalExpression(expr.consequent)) {
@@ -523,12 +581,32 @@ inline)
523
581
  }));
524
582
  if (dTest) {
525
583
  cond = nextPath.node.left;
526
- if (!t__namespace.isBinaryExpression(cond))
527
- cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
584
+ // `left && right` is exactly `left ? right : left`. Branch on the
585
+ // memoized truthiness (so truthy-value churn never re-creates the right
586
+ // side) but return the raw left in the alternate so the expression keeps
587
+ // JS value semantics — `0`/`""`/`undefined` flow through instead of
588
+ // collapsing to `false`, matching the untransformed ssr output (#532).
589
+ // Statically boolean lefts skip the ternary: the memo's value is the
590
+ // expression's value, so the logical form is already exact and the left
591
+ // never evaluates twice.
592
+ const boolLeft = isBooleanExpression(cond);
593
+ if (!boolLeft) cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
528
594
  id = inline ?
595
+ memo ?
529
596
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
597
+ t__namespace.arrowFunctionExpression([], cond) :
530
598
  path.scope.generateUidIdentifier("_c$");
531
- nextPath.node.left = t__namespace.callExpression(id, []);
599
+ if (boolLeft) {
600
+ nextPath.node.left = t__namespace.callExpression(id, []);
601
+ } else {
602
+ const alternate = t__namespace.cloneNode(nextPath.node.left, true);
603
+ nextPath.replaceWith(
604
+ t__namespace.conditionalExpression(t__namespace.callExpression(id, []), nextPath.node.right, alternate)
605
+ );
606
+ // replaceWith swaps the node out of the tree; when the `&&` was the
607
+ // top-level expression the local reference is stale.
608
+ expr = path.node;
609
+ }
532
610
  }
533
611
  }
534
612
  if (dTest && !inline && cond && id) {
@@ -536,7 +614,7 @@ inline)
536
614
  t__namespace.variableDeclaration("var", [
537
615
  t__namespace.variableDeclarator(
538
616
  id,
539
- config.memoWrapper ?
617
+ memo ?
540
618
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
541
619
  t__namespace.arrowFunctionExpression([], cond)
542
620
  )]
@@ -2136,9 +2214,35 @@ results)
2136
2214
  results.hasHydratableEvent = results.hasHydratableEvent || hasHydratableEvent;
2137
2215
  }
2138
2216
 
2217
+ // Children that compile to `insert()` calls and contribute no markup of their
2218
+ // own: dynamic expression containers, components, and spread children. Mirrors
2219
+ // the `!child.id && child.exprs.length` count in transformChildren.
2220
+ function countDynamicSlots(children) {
2221
+ let count = 0;
2222
+ for (const child of children) {
2223
+ const node = child.node;
2224
+ if (
2225
+ t$1.isJSXText(node) ||
2226
+ t$1.isJSXExpressionContainer(node) &&
2227
+ getStaticExpression(child) !== false ||
2228
+ t$1.isJSXElement(node) && !isComponent(getTagName(node)))
2229
+
2230
+ continue;
2231
+ count++;
2232
+ }
2233
+ return count;
2234
+ }
2235
+
2139
2236
  function findLastElement(children, hydratable) {
2140
2237
  let lastElement = -1,
2141
2238
  tagName;
2239
+ // Counterpart of transformChildren's per-slot markers: with two or more
2240
+ // dynamic slots under this parent (CSR only), a trailing dynamic child
2241
+ // appends a dedicated `<!>` placeholder to the template, so an earlier
2242
+ // element may not omit its closing tag — the still-open element would
2243
+ // swallow the placeholder as a child while the generated
2244
+ // firstChild/nextSibling walk expects it as a sibling.
2245
+ const perSlotMarkers = !hydratable && countDynamicSlots(children) > 1;
2142
2246
  for (let i = children.length - 1; i >= 0; i--) {
2143
2247
  const node = children[i].node;
2144
2248
  if (
@@ -2152,6 +2256,9 @@ function findLastElement(children, hydratable) {
2152
2256
  lastElement = i;
2153
2257
  break;
2154
2258
  }
2259
+ // This trailing dynamic slot will emit a per-slot placeholder after any
2260
+ // preceding element's markup: nothing here may omit its closing tag.
2261
+ if (perSlotMarkers) break;
2155
2262
  }
2156
2263
  return lastElement;
2157
2264
  }
@@ -2163,7 +2270,6 @@ config)
2163
2270
  {
2164
2271
  let tempPath = results.id && results.id.name || "",
2165
2272
  tagName = getTagName(path.node),
2166
- nextPlaceholder,
2167
2273
  childPostExprs = [],
2168
2274
  i = 0;
2169
2275
  const filteredChildren = filterChildren(path.get("children")),
@@ -2181,6 +2287,8 @@ config)
2181
2287
  skipId: !results.id || !detectExpressions(filteredChildren, index, config)
2182
2288
  });
2183
2289
  if (!transformed) return memo;
2290
+ transformed.allocatesIds =
2291
+ config.hydratable && canChildSlotAllocateIds(child);
2184
2292
  const i = memo.length;
2185
2293
  if (transformed.text && i && memo[i - 1].text) {
2186
2294
  memo[i - 1].template =
@@ -2193,6 +2301,13 @@ config)
2193
2301
  []
2194
2302
  );
2195
2303
 
2304
+ // Dynamic slots under this parent (children compiled to `insert()` calls).
2305
+ // With two or more, slots may not share an insertion marker: the marker is
2306
+ // also the runtime's ownership tag ($$SLOT), and adoption only re-tags when
2307
+ // the marker is truthy — shared or null markers let one slot's cleanup
2308
+ // destroy a node that migrated to its neighbor (solidjs/solid#2830).
2309
+ const dynamicSlots = childNodes.reduce((n, c) => c && !c.id && c.exprs.length ? n + 1 : n, 0);
2310
+
2196
2311
  childNodes.forEach((child, index) => {
2197
2312
  if (!child) return;
2198
2313
  if (child.tagName && child.renderer !== "dom") {
@@ -2245,23 +2360,47 @@ config)
2245
2360
  results.isImportNode = results.isImportNode || child.isImportNode;
2246
2361
  results.isWrapped = results.isWrapped || child.isWrapped;
2247
2362
  tempPath = child.id.name;
2248
- nextPlaceholder = null;
2249
2363
  i++;
2250
2364
  } else if (child.exprs.length) {
2251
2365
  let insert = registerImportMethod(path, "insert", getRendererConfig(path, "dom").moduleName);
2252
2366
  const multi = checkLength(filteredChildren),
2253
- markers = config.hydratable && multi;
2367
+ markers = config.hydratable && multi,
2368
+ // CSR counterpart of the hydratable per-slot markers: when this parent
2369
+ // hosts multiple dynamic slots, each gets its own truthy marker — the
2370
+ // immediately following sibling when it has a reference, otherwise a
2371
+ // dedicated `<!>` placeholder.
2372
+ perSlot = !markers && dynamicSlots > 1;
2373
+ // Mirror of the ssr generate's `scope()` wrap: deferred holes that can
2374
+ // allocate hydration ids get their own owner scope (insert makes the
2375
+ // outer render effect non-transparent for tagged accessors). Keyed off
2376
+ // `dynamic` so both generates decide identically for the same source.
2377
+ if (child.allocatesIds && child.dynamic) {
2378
+ let expr = child.exprs[0];
2379
+ // The shared transform simplifies `{sig()}` to the bare getter `sig`;
2380
+ // rewrap so tagging the scope doesn't mutate the user's function.
2381
+ if (!t$1.isFunction(expr) && !(t$1.isCallExpression(expr) && t$1.isFunction(expr.callee))) {
2382
+ expr = t$1.arrowFunctionExpression([], t$1.callExpression(expr, []));
2383
+ }
2384
+ child.exprs[0] = t$1.callExpression(
2385
+ registerImportMethod(path, "scope", getRendererConfig(path, "dom").moduleName),
2386
+ [expr]
2387
+ );
2388
+ }
2254
2389
  // boxed by textNodes
2255
- if (markers || wrappedByText(childNodes, index)) {
2390
+ if (markers || perSlot || wrappedByText(childNodes, index)) {
2256
2391
  let exprId;
2257
2392
  let contentId;
2258
2393
  if (markers) tempPath = createPlaceholder(path, results, tempPath, i++, "$")[0].name;
2259
- if (nextPlaceholder) {
2260
- exprId = nextPlaceholder;
2261
- } else {
2394
+ // Ride the immediately following sibling's reference when it exists —
2395
+ // unless the slot is boxed by text, where a placeholder is structurally
2396
+ // required to keep the surrounding template text nodes from merging.
2397
+ if (perSlot && !wrappedByText(childNodes, index)) {
2398
+ exprId = childNodes[index + 1] && childNodes[index + 1].id || undefined;
2399
+ }
2400
+ if (!exprId) {
2262
2401
  [exprId, contentId] = createPlaceholder(path, results, tempPath, i++, markers ? "/" : "");
2402
+ tempPath = exprId.name;
2263
2403
  }
2264
- if (!markers) nextPlaceholder = exprId;
2265
2404
  const args = contentId ?
2266
2405
  [
2267
2406
  results.id,
@@ -2275,7 +2414,6 @@ config)
2275
2414
  exprId];
2276
2415
 
2277
2416
  results.exprs.push(t$1.expressionStatement(t$1.callExpression(insert, args)));
2278
- tempPath = exprId.name;
2279
2417
  } else if (multi) {
2280
2418
  results.exprs.push(
2281
2419
  t$1.expressionStatement(
@@ -2293,7 +2431,7 @@ config)
2293
2431
  )
2294
2432
  );
2295
2433
  }
2296
- } else nextPlaceholder = null;
2434
+ }
2297
2435
  });
2298
2436
  results.postExprs.unshift(...childPostExprs);
2299
2437
  }
@@ -2382,6 +2520,15 @@ config)
2382
2520
  t$1.isJSXSpreadAttribute(attr) ||
2383
2521
  t$1.isJSXIdentifier(attr.name) &&
2384
2522
  ["textContent", "innerHTML", "innerText"].includes(attr.name.name) ||
2523
+ // inlineStyles: false rewrites every style value into an IIFE
2524
+ // expression, so even literal styles compile to dynamic
2525
+ // bindings that need an element reference.
2526
+ !config.inlineStyles &&
2527
+ t$1.isJSXIdentifier(attr.name) &&
2528
+ attr.name.name === "style" && (
2529
+ t$1.isStringLiteral(attr.value) ||
2530
+ t$1.isJSXExpressionContainer(attr.value) &&
2531
+ !t$1.isJSXEmptyExpression(attr.value.expression)) ||
2385
2532
  t$1.isJSXNamespacedName(attr.name) && attr.name.namespace.name === "prop" ||
2386
2533
  t$1.isJSXExpressionContainer(attr.value) &&
2387
2534
  !(
@@ -2653,6 +2800,8 @@ function wrapDynamics$1(path, dynamics) {
2653
2800
  if (!dynamics.length) return;
2654
2801
  const config = getConfig(path);
2655
2802
 
2803
+ // dynamics are only queued when effectWrapper is configured (element.ts
2804
+ // guards every push), so the name is always a string here
2656
2805
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
2657
2806
 
2658
2807
  if (dynamics.length === 1) {
@@ -2782,7 +2931,6 @@ const t = t__namespace;
2782
2931
 
2783
2932
 
2784
2933
 
2785
-
2786
2934
 
2787
2935
 
2788
2936
  function appendToTemplate(template, value) {
@@ -2961,8 +3109,7 @@ info = {})
2961
3109
  hoistExpression(
2962
3110
  path,
2963
3111
  results,
2964
- t.callExpression(registerImportMethod(path, "ssrHydrationKey"), []),
2965
- { }
3112
+ t.callExpression(registerImportMethod(path, "ssrHydrationKey"), [])
2966
3113
  )
2967
3114
  );
2968
3115
  }
@@ -3505,29 +3652,6 @@ quasis)
3505
3652
  });
3506
3653
  }
3507
3654
 
3508
- function canReturnHydratableChild(node) {
3509
- if (t.isTSNonNullExpression(node) || t.isTSAsExpression(node) || t.isTSSatisfiesExpression(node))
3510
- return canReturnHydratableChild(node.expression);
3511
- if (t.isJSXElement(node) || t.isJSXFragment(node) || t.isCallExpression(node)) return true;
3512
- if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
3513
- return !node.computed && t.isIdentifier(node.property, { name: "children" });
3514
- }
3515
- if (t.isConditionalExpression(node)) {
3516
- return canReturnHydratableChild(node.consequent) || canReturnHydratableChild(node.alternate);
3517
- }
3518
- return t.isLogicalExpression(node) && canReturnHydratableChild(node.right);
3519
- }
3520
-
3521
- function canChildSlotAllocateIds(node) {
3522
- if (node.isJSXElement() || node.isJSXFragment()) return true;
3523
- if (node.isJSXSpreadChild()) return true;
3524
- return node.isJSXExpressionContainer() && canReturnHydratableChild(node.node.expression);
3525
- }
3526
-
3527
- function isDeferredChildSlotExpression(node) {
3528
- return t.isFunction(node) || t.isCallExpression(node) && t.isFunction(node.callee);
3529
- }
3530
-
3531
3655
  function transformChildren$1(
3532
3656
  path,
3533
3657
  results,
@@ -3538,14 +3662,13 @@ results,
3538
3662
  const filteredChildren = filterChildren(path.get("children"));
3539
3663
  const multi = checkLength(filteredChildren),
3540
3664
  markers = hydratable && multi;
3541
- let orderedInsert = false;
3542
3665
  filteredChildren.forEach((node) => {
3543
3666
  if (node.isJSXFragment()) {
3544
3667
  throw new Error(
3545
3668
  `Fragments can only be used top level in JSX. Not used under a <${tagName}>.`
3546
3669
  );
3547
3670
  }
3548
- const allocatesIds = canChildSlotAllocateIds(node);
3671
+ const allocatesIds = hydratable && canChildSlotAllocateIds(node);
3549
3672
  const child = transformNode(node, { doNotEscape, parentResults: results });
3550
3673
  if (!child) return;
3551
3674
  appendToTemplate(results.template, child.template);
@@ -3570,14 +3693,14 @@ results,
3570
3693
  _groupableTextContent ?
3571
3694
  { group: true } :
3572
3695
  undefined;
3573
- const expr =
3574
- hydratable &&
3575
- orderedInsert &&
3576
- allocatesIds &&
3577
- !isDeferredChildSlotExpression(child.exprs[0]) ?
3578
- t.arrowFunctionExpression([], child.exprs[0]) :
3579
- child.exprs[0];
3580
- if (hydratable && allocatesIds && isDeferredChildSlotExpression(expr)) orderedInsert = true;
3696
+ // Deferred holes that can allocate hydration ids evaluate under their
3697
+ // own owner scope so retry timing can't skew sibling ids (mirrors the
3698
+ // dom generate's `scope()` wrap around the matching insert accessor).
3699
+ // Keyed off `dynamic` so both generates decide identically.
3700
+ let expr = child.exprs[0];
3701
+ if (allocatesIds && child.dynamic) {
3702
+ expr = t.callExpression(registerImportMethod(path, "scope"), [expr]);
3703
+ }
3581
3704
 
3582
3705
  // boxed by textNodes
3583
3706
  if (markers && !child.spreadElement) {
@@ -3615,12 +3738,25 @@ path,
3615
3738
  `Fragments can only be used top level in JSX. Not used under a <${tagName}>.`
3616
3739
  );
3617
3740
  }
3741
+ const allocatesIds = hydratable && canChildSlotAllocateIds(path);
3618
3742
  const child = transformNode(path);
3619
3743
  if (!child) return memo;
3620
3744
  if (markers && child.exprs.length && !child.spreadElement)
3621
3745
  memo.push(t.stringLiteral("<!--$-->"));
3622
3746
  if (child.exprs.length && !doNotEscape && !child.spreadElement)
3623
3747
  child.exprs[0] = escapeExpression(path, child.exprs[0]);
3748
+ // Deferred holes that can allocate hydration ids evaluate under their
3749
+ // own owner scope, exactly like `transformChildren` does for the
3750
+ // template path. Spread elements render through `ssrElement` instead
3751
+ // of a template, but their children holes still need the wrap — the
3752
+ // dom generate scope()s the matching insert accessor regardless of
3753
+ // spread, so skipping it here desyncs every hydration id that follows
3754
+ // the hole.
3755
+ if (child.exprs.length && allocatesIds && child.dynamic) {
3756
+ child.exprs[0] = t.callExpression(registerImportMethod(path, "scope"), [
3757
+ child.exprs[0]]
3758
+ );
3759
+ }
3624
3760
  memo.push(
3625
3761
  getCreateTemplate(config, path, child)(path, child, false)
3626
3762
  );
@@ -3689,7 +3825,19 @@ path,
3689
3825
  if (runningObject.length || !props.length) props.push(t.objectExpression(runningObject));
3690
3826
 
3691
3827
  if (props.length > 1 || dynamicSpread) {
3692
- props = [t.callExpression(registerImportMethod(path, "mergeProps"), props)];
3828
+ let merged = t.callExpression(
3829
+ registerImportMethod(path, "mergeProps"),
3830
+ props
3831
+ );
3832
+ // Defer the merge behind a thunk when hydratable: `mergeProps` with a
3833
+ // function source creates a memo, which consumes a hydration child id.
3834
+ // Evaluated in argument position it would run before `ssrElement`
3835
+ // allocates the element's own id, while the client claims the element
3836
+ // (getNextElement) before applying the spread — shifting the element's
3837
+ // id by one and leaving it unclaimed. `ssrElement` resolves function
3838
+ // props after allocating the hydration key, matching the client order.
3839
+ if (hydratable) merged = t.arrowFunctionExpression([], merged);
3840
+ props = [merged];
3693
3841
  }
3694
3842
  }
3695
3843
 
@@ -3751,8 +3899,9 @@ wrap)
3751
3899
  const inner = result.wontEscape ?
3752
3900
  result.exprs[0] :
3753
3901
  wrapFragmentChildWithEscape(path, result.exprs[0]);
3754
- return t__namespace.callExpression(registerImportMethod(path, getConfig(path).memoWrapper, undefined), [
3755
- inner]
3902
+ return t__namespace.callExpression(
3903
+ registerImportMethod(path, getConfig(path).memoWrapper, undefined),
3904
+ [inner]
3756
3905
  );
3757
3906
  }
3758
3907
  return result.exprs[0];
@@ -3905,6 +4054,9 @@ info = {})
3905
4054
  renderer: "universal"
3906
4055
  };
3907
4056
 
4057
+ const initProps = transformAttributes(path, results);
4058
+ const createElementArgs = [t__namespace.stringLiteral(tagName)];
4059
+ if (initProps.length) createElementArgs.push(t__namespace.objectExpression(initProps));
3908
4060
  results.declarations.push(
3909
4061
  t__namespace.variableDeclarator(
3910
4062
  results.id,
@@ -3914,12 +4066,10 @@ info = {})
3914
4066
  "createElement",
3915
4067
  getRendererConfig(path, "universal").moduleName
3916
4068
  ),
3917
- [t__namespace.stringLiteral(tagName)]
4069
+ createElementArgs
3918
4070
  )
3919
4071
  )
3920
4072
  );
3921
-
3922
- transformAttributes(path, results);
3923
4073
  transformChildren(path, results);
3924
4074
 
3925
4075
  return results;
@@ -3931,12 +4081,14 @@ results)
3931
4081
  {
3932
4082
  let children, spreadExpr;
3933
4083
  let attributes = path.get("openingElement").get("attributes");
4084
+ const initProps = [];
3934
4085
  const elem = results.id,
3935
4086
  hasChildren = path.node.children.length > 0,
3936
- config = getConfig(path);
4087
+ config = getConfig(path),
4088
+ hasSpread = attributes.some((attribute) => t__namespace.isJSXSpreadAttribute(attribute.node));
3937
4089
 
3938
4090
  // preprocess spreads
3939
- if (attributes.some((attribute) => t__namespace.isJSXSpreadAttribute(attribute.node))) {
4091
+ if (hasSpread) {
3940
4092
  [attributes, spreadExpr] = processSpreads(path, attributes, {
3941
4093
  elem,
3942
4094
  hasChildren,
@@ -4065,20 +4217,47 @@ results)
4065
4217
  {
4066
4218
  results.dynamics.push({ elem, key, value: value.expression });
4067
4219
  } else {
4068
- results.exprs.push(
4069
- t__namespace.expressionStatement(setAttr(attribute, elem, key, value.expression))
4220
+ addStaticAttr(
4221
+ attribute,
4222
+ results,
4223
+ initProps,
4224
+ elem,
4225
+ key,
4226
+ value.expression,
4227
+ hasSpread
4070
4228
  );
4071
4229
  }
4072
4230
  } else {
4073
- results.exprs.push(
4074
- t__namespace.expressionStatement(setAttr(attribute, elem, key, value))
4075
- );
4231
+ addStaticAttr(attribute, results, initProps, elem, key, value, hasSpread);
4076
4232
  }
4077
4233
  });
4078
4234
  if (spreadExpr) results.exprs.push(spreadExpr);
4079
4235
  if (!hasChildren && children) {
4080
4236
  path.node.children.push(children);
4081
4237
  }
4238
+ return initProps;
4239
+ }
4240
+
4241
+ function addStaticAttr(
4242
+ path,
4243
+ results,
4244
+ initProps,
4245
+ elem,
4246
+ key,
4247
+ value,
4248
+ hasSpread)
4249
+ {
4250
+ if (!value) value = t__namespace.booleanLiteral(true);
4251
+ if (hasSpread) {
4252
+ results.exprs.push(t__namespace.expressionStatement(setAttr(path, elem, key, value)));
4253
+ } else {
4254
+ initProps.push(
4255
+ t__namespace.objectProperty(
4256
+ t__namespace.isValidIdentifier(key) ? t__namespace.identifier(key) : t__namespace.stringLiteral(key),
4257
+ value
4258
+ )
4259
+ );
4260
+ }
4082
4261
  }
4083
4262
 
4084
4263
  function setAttr(
@@ -4349,6 +4528,8 @@ function wrapDynamics(path, dynamics) {
4349
4528
  if (!dynamics.length) return;
4350
4529
  const config = getConfig(path);
4351
4530
 
4531
+ // dynamics are only queued when effectWrapper is configured (element.ts
4532
+ // guards every push), so the name is always a string here
4352
4533
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
4353
4534
 
4354
4535
  if (dynamics.length === 1) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dom-expressions/babel-plugin-jsx",
3
3
  "description": "A JSX to DOM plugin that wraps expressions for fine grained change detection",
4
- "version": "0.50.0-next.15",
4
+ "version": "0.50.0-next.17",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "@rollup/plugin-babel": "7.0.0",
29
29
  "@types/babel__core": "^7.20.5",
30
30
  "@types/babel__traverse": "^7.28.0",
31
- "@dom-expressions/runtime": "0.50.0-next.15"
31
+ "@dom-expressions/runtime": "0.50.0-next.17"
32
32
  },
33
33
  "gitHead": "eb463c653325e24824422cff6bfed0d35113ef33",
34
34
  "scripts": {