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

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 +30 -0
  2. package/index.js +166 -61
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # babel-plugin-jsx-dom-expressions
2
2
 
3
+ ## 0.50.0-next.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+ - 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.
9
+ - c2a542b: Fix hydration key mismatches when async holes defer past eager siblings
10
+ (solidjs/solid#2801 bug 2). Dynamic element children that can allocate
11
+ hydration ids (conditionals, component-children access, call expressions)
12
+ are now compiled with their own id scope on both generates: the dom and ssr
13
+ generates wrap the hole expression in a new `scope()` runtime helper using a
14
+ shared predicate, so marking cannot desync.
15
+
16
+ On the client, `scope(fn)` tags the accessor and `insert()` makes the outer
17
+ render effect non-transparent (its own id scope) for tagged accessors; the
18
+ inner unwrapping effect stays transparent so content ids keep a fixed depth.
19
+ On the server, `scope` (framework-provided via rxcore as `ssrScope`) reserves
20
+ one id slot at registration and evaluates the hole — including async retries
21
+ — under that reserved id with a zeroed child counter, so retry timing can no
22
+ longer shift sibling ids. The ssr generate's `orderedInsert` sibling
23
+ thunk-wrapping is removed; it is superseded by hole scopes.
24
+
25
+ Hole content ids gain one nesting level (e.g. `_hk=10` instead of `_hk=1`)
26
+ identically on both sides. rxcore implementations must provide an `ssrScope`
27
+ export and honor a `scope: true` effect option (mapped to a non-transparent
28
+ render effect).
29
+
30
+ - 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.
31
+ - 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.
32
+
3
33
  ## 0.50.0-next.15
4
34
 
5
35
  ### 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,7 +533,7 @@ path,
481
533
  inline)
482
534
  {
483
535
  const config = getConfig(path);
484
- const expr = path.node;
536
+ let expr = path.node;
485
537
  const memo = registerImportMethod(path, config.memoWrapper, undefined);
486
538
  let dTest,
487
539
  cond,
@@ -497,7 +549,7 @@ inline)
497
549
  dTest = isDynamic(path.get("test"), { checkMember: true });
498
550
  if (dTest) {
499
551
  cond = expr.test;
500
- if (!t__namespace.isBinaryExpression(cond))
552
+ if (!isBooleanExpression(cond))
501
553
  cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
502
554
  id = inline ?
503
555
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
@@ -523,12 +575,30 @@ inline)
523
575
  }));
524
576
  if (dTest) {
525
577
  cond = nextPath.node.left;
526
- if (!t__namespace.isBinaryExpression(cond))
527
- cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
578
+ // `left && right` is exactly `left ? right : left`. Branch on the
579
+ // memoized truthiness (so truthy-value churn never re-creates the right
580
+ // side) but return the raw left in the alternate so the expression keeps
581
+ // JS value semantics — `0`/`""`/`undefined` flow through instead of
582
+ // collapsing to `false`, matching the untransformed ssr output (#532).
583
+ // Statically boolean lefts skip the ternary: the memo's value is the
584
+ // expression's value, so the logical form is already exact and the left
585
+ // never evaluates twice.
586
+ const boolLeft = isBooleanExpression(cond);
587
+ if (!boolLeft) cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
528
588
  id = inline ?
529
589
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
530
590
  path.scope.generateUidIdentifier("_c$");
531
- nextPath.node.left = t__namespace.callExpression(id, []);
591
+ if (boolLeft) {
592
+ nextPath.node.left = t__namespace.callExpression(id, []);
593
+ } else {
594
+ const alternate = t__namespace.cloneNode(nextPath.node.left, true);
595
+ nextPath.replaceWith(
596
+ t__namespace.conditionalExpression(t__namespace.callExpression(id, []), nextPath.node.right, alternate)
597
+ );
598
+ // replaceWith swaps the node out of the tree; when the `&&` was the
599
+ // top-level expression the local reference is stale.
600
+ expr = path.node;
601
+ }
532
602
  }
533
603
  }
534
604
  if (dTest && !inline && cond && id) {
@@ -2163,7 +2233,6 @@ config)
2163
2233
  {
2164
2234
  let tempPath = results.id && results.id.name || "",
2165
2235
  tagName = getTagName(path.node),
2166
- nextPlaceholder,
2167
2236
  childPostExprs = [],
2168
2237
  i = 0;
2169
2238
  const filteredChildren = filterChildren(path.get("children")),
@@ -2181,6 +2250,8 @@ config)
2181
2250
  skipId: !results.id || !detectExpressions(filteredChildren, index, config)
2182
2251
  });
2183
2252
  if (!transformed) return memo;
2253
+ transformed.allocatesIds =
2254
+ config.hydratable && canChildSlotAllocateIds(child);
2184
2255
  const i = memo.length;
2185
2256
  if (transformed.text && i && memo[i - 1].text) {
2186
2257
  memo[i - 1].template =
@@ -2193,6 +2264,13 @@ config)
2193
2264
  []
2194
2265
  );
2195
2266
 
2267
+ // Dynamic slots under this parent (children compiled to `insert()` calls).
2268
+ // With two or more, slots may not share an insertion marker: the marker is
2269
+ // also the runtime's ownership tag ($$SLOT), and adoption only re-tags when
2270
+ // the marker is truthy — shared or null markers let one slot's cleanup
2271
+ // destroy a node that migrated to its neighbor (solidjs/solid#2830).
2272
+ const dynamicSlots = childNodes.reduce((n, c) => c && !c.id && c.exprs.length ? n + 1 : n, 0);
2273
+
2196
2274
  childNodes.forEach((child, index) => {
2197
2275
  if (!child) return;
2198
2276
  if (child.tagName && child.renderer !== "dom") {
@@ -2245,23 +2323,47 @@ config)
2245
2323
  results.isImportNode = results.isImportNode || child.isImportNode;
2246
2324
  results.isWrapped = results.isWrapped || child.isWrapped;
2247
2325
  tempPath = child.id.name;
2248
- nextPlaceholder = null;
2249
2326
  i++;
2250
2327
  } else if (child.exprs.length) {
2251
2328
  let insert = registerImportMethod(path, "insert", getRendererConfig(path, "dom").moduleName);
2252
2329
  const multi = checkLength(filteredChildren),
2253
- markers = config.hydratable && multi;
2330
+ markers = config.hydratable && multi,
2331
+ // CSR counterpart of the hydratable per-slot markers: when this parent
2332
+ // hosts multiple dynamic slots, each gets its own truthy marker — the
2333
+ // immediately following sibling when it has a reference, otherwise a
2334
+ // dedicated `<!>` placeholder.
2335
+ perSlot = !markers && dynamicSlots > 1;
2336
+ // Mirror of the ssr generate's `scope()` wrap: deferred holes that can
2337
+ // allocate hydration ids get their own owner scope (insert makes the
2338
+ // outer render effect non-transparent for tagged accessors). Keyed off
2339
+ // `dynamic` so both generates decide identically for the same source.
2340
+ if (child.allocatesIds && child.dynamic) {
2341
+ let expr = child.exprs[0];
2342
+ // The shared transform simplifies `{sig()}` to the bare getter `sig`;
2343
+ // rewrap so tagging the scope doesn't mutate the user's function.
2344
+ if (!t$1.isFunction(expr) && !(t$1.isCallExpression(expr) && t$1.isFunction(expr.callee))) {
2345
+ expr = t$1.arrowFunctionExpression([], t$1.callExpression(expr, []));
2346
+ }
2347
+ child.exprs[0] = t$1.callExpression(
2348
+ registerImportMethod(path, "scope", getRendererConfig(path, "dom").moduleName),
2349
+ [expr]
2350
+ );
2351
+ }
2254
2352
  // boxed by textNodes
2255
- if (markers || wrappedByText(childNodes, index)) {
2353
+ if (markers || perSlot || wrappedByText(childNodes, index)) {
2256
2354
  let exprId;
2257
2355
  let contentId;
2258
2356
  if (markers) tempPath = createPlaceholder(path, results, tempPath, i++, "$")[0].name;
2259
- if (nextPlaceholder) {
2260
- exprId = nextPlaceholder;
2261
- } else {
2357
+ // Ride the immediately following sibling's reference when it exists —
2358
+ // unless the slot is boxed by text, where a placeholder is structurally
2359
+ // required to keep the surrounding template text nodes from merging.
2360
+ if (perSlot && !wrappedByText(childNodes, index)) {
2361
+ exprId = childNodes[index + 1] && childNodes[index + 1].id || undefined;
2362
+ }
2363
+ if (!exprId) {
2262
2364
  [exprId, contentId] = createPlaceholder(path, results, tempPath, i++, markers ? "/" : "");
2365
+ tempPath = exprId.name;
2263
2366
  }
2264
- if (!markers) nextPlaceholder = exprId;
2265
2367
  const args = contentId ?
2266
2368
  [
2267
2369
  results.id,
@@ -2275,7 +2377,6 @@ config)
2275
2377
  exprId];
2276
2378
 
2277
2379
  results.exprs.push(t$1.expressionStatement(t$1.callExpression(insert, args)));
2278
- tempPath = exprId.name;
2279
2380
  } else if (multi) {
2280
2381
  results.exprs.push(
2281
2382
  t$1.expressionStatement(
@@ -2293,7 +2394,7 @@ config)
2293
2394
  )
2294
2395
  );
2295
2396
  }
2296
- } else nextPlaceholder = null;
2397
+ }
2297
2398
  });
2298
2399
  results.postExprs.unshift(...childPostExprs);
2299
2400
  }
@@ -2782,7 +2883,6 @@ const t = t__namespace;
2782
2883
 
2783
2884
 
2784
2885
 
2785
-
2786
2886
 
2787
2887
 
2788
2888
  function appendToTemplate(template, value) {
@@ -2961,8 +3061,7 @@ info = {})
2961
3061
  hoistExpression(
2962
3062
  path,
2963
3063
  results,
2964
- t.callExpression(registerImportMethod(path, "ssrHydrationKey"), []),
2965
- { }
3064
+ t.callExpression(registerImportMethod(path, "ssrHydrationKey"), [])
2966
3065
  )
2967
3066
  );
2968
3067
  }
@@ -3505,29 +3604,6 @@ quasis)
3505
3604
  });
3506
3605
  }
3507
3606
 
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
3607
  function transformChildren$1(
3532
3608
  path,
3533
3609
  results,
@@ -3538,14 +3614,13 @@ results,
3538
3614
  const filteredChildren = filterChildren(path.get("children"));
3539
3615
  const multi = checkLength(filteredChildren),
3540
3616
  markers = hydratable && multi;
3541
- let orderedInsert = false;
3542
3617
  filteredChildren.forEach((node) => {
3543
3618
  if (node.isJSXFragment()) {
3544
3619
  throw new Error(
3545
3620
  `Fragments can only be used top level in JSX. Not used under a <${tagName}>.`
3546
3621
  );
3547
3622
  }
3548
- const allocatesIds = canChildSlotAllocateIds(node);
3623
+ const allocatesIds = hydratable && canChildSlotAllocateIds(node);
3549
3624
  const child = transformNode(node, { doNotEscape, parentResults: results });
3550
3625
  if (!child) return;
3551
3626
  appendToTemplate(results.template, child.template);
@@ -3570,14 +3645,14 @@ results,
3570
3645
  _groupableTextContent ?
3571
3646
  { group: true } :
3572
3647
  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;
3648
+ // Deferred holes that can allocate hydration ids evaluate under their
3649
+ // own owner scope so retry timing can't skew sibling ids (mirrors the
3650
+ // dom generate's `scope()` wrap around the matching insert accessor).
3651
+ // Keyed off `dynamic` so both generates decide identically.
3652
+ let expr = child.exprs[0];
3653
+ if (allocatesIds && child.dynamic) {
3654
+ expr = t.callExpression(registerImportMethod(path, "scope"), [expr]);
3655
+ }
3581
3656
 
3582
3657
  // boxed by textNodes
3583
3658
  if (markers && !child.spreadElement) {
@@ -3905,6 +3980,9 @@ info = {})
3905
3980
  renderer: "universal"
3906
3981
  };
3907
3982
 
3983
+ const initProps = transformAttributes(path, results);
3984
+ const createElementArgs = [t__namespace.stringLiteral(tagName)];
3985
+ if (initProps.length) createElementArgs.push(t__namespace.objectExpression(initProps));
3908
3986
  results.declarations.push(
3909
3987
  t__namespace.variableDeclarator(
3910
3988
  results.id,
@@ -3914,12 +3992,10 @@ info = {})
3914
3992
  "createElement",
3915
3993
  getRendererConfig(path, "universal").moduleName
3916
3994
  ),
3917
- [t__namespace.stringLiteral(tagName)]
3995
+ createElementArgs
3918
3996
  )
3919
3997
  )
3920
3998
  );
3921
-
3922
- transformAttributes(path, results);
3923
3999
  transformChildren(path, results);
3924
4000
 
3925
4001
  return results;
@@ -3931,12 +4007,14 @@ results)
3931
4007
  {
3932
4008
  let children, spreadExpr;
3933
4009
  let attributes = path.get("openingElement").get("attributes");
4010
+ const initProps = [];
3934
4011
  const elem = results.id,
3935
4012
  hasChildren = path.node.children.length > 0,
3936
- config = getConfig(path);
4013
+ config = getConfig(path),
4014
+ hasSpread = attributes.some((attribute) => t__namespace.isJSXSpreadAttribute(attribute.node));
3937
4015
 
3938
4016
  // preprocess spreads
3939
- if (attributes.some((attribute) => t__namespace.isJSXSpreadAttribute(attribute.node))) {
4017
+ if (hasSpread) {
3940
4018
  [attributes, spreadExpr] = processSpreads(path, attributes, {
3941
4019
  elem,
3942
4020
  hasChildren,
@@ -4065,20 +4143,47 @@ results)
4065
4143
  {
4066
4144
  results.dynamics.push({ elem, key, value: value.expression });
4067
4145
  } else {
4068
- results.exprs.push(
4069
- t__namespace.expressionStatement(setAttr(attribute, elem, key, value.expression))
4146
+ addStaticAttr(
4147
+ attribute,
4148
+ results,
4149
+ initProps,
4150
+ elem,
4151
+ key,
4152
+ value.expression,
4153
+ hasSpread
4070
4154
  );
4071
4155
  }
4072
4156
  } else {
4073
- results.exprs.push(
4074
- t__namespace.expressionStatement(setAttr(attribute, elem, key, value))
4075
- );
4157
+ addStaticAttr(attribute, results, initProps, elem, key, value, hasSpread);
4076
4158
  }
4077
4159
  });
4078
4160
  if (spreadExpr) results.exprs.push(spreadExpr);
4079
4161
  if (!hasChildren && children) {
4080
4162
  path.node.children.push(children);
4081
4163
  }
4164
+ return initProps;
4165
+ }
4166
+
4167
+ function addStaticAttr(
4168
+ path,
4169
+ results,
4170
+ initProps,
4171
+ elem,
4172
+ key,
4173
+ value,
4174
+ hasSpread)
4175
+ {
4176
+ if (!value) value = t__namespace.booleanLiteral(true);
4177
+ if (hasSpread) {
4178
+ results.exprs.push(t__namespace.expressionStatement(setAttr(path, elem, key, value)));
4179
+ } else {
4180
+ initProps.push(
4181
+ t__namespace.objectProperty(
4182
+ t__namespace.isValidIdentifier(key) ? t__namespace.identifier(key) : t__namespace.stringLiteral(key),
4183
+ value
4184
+ )
4185
+ );
4186
+ }
4082
4187
  }
4083
4188
 
4084
4189
  function setAttr(
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.16",
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.16"
32
32
  },
33
33
  "gitHead": "eb463c653325e24824422cff6bfed0d35113ef33",
34
34
  "scripts": {