@dom-expressions/babel-plugin-jsx 0.50.0-next.16 → 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 +14 -0
  2. package/index.js +81 -5
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 0.50.0-next.16
4
18
 
5
19
  ### Patch Changes
package/index.js CHANGED
@@ -534,7 +534,11 @@ inline)
534
534
  {
535
535
  const config = getConfig(path);
536
536
  let expr = path.node;
537
- const memo = registerImportMethod(path, config.memoWrapper, undefined);
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;
538
542
  let dTest,
539
543
  cond,
540
544
  id;
@@ -552,7 +556,9 @@ inline)
552
556
  if (!isBooleanExpression(cond))
553
557
  cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
554
558
  id = inline ?
559
+ memo ?
555
560
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
561
+ t__namespace.arrowFunctionExpression([], cond) :
556
562
  path.scope.generateUidIdentifier("_c$");
557
563
  expr.test = t__namespace.callExpression(id, []);
558
564
  if (t__namespace.isConditionalExpression(expr.consequent) || t__namespace.isLogicalExpression(expr.consequent)) {
@@ -586,7 +592,9 @@ inline)
586
592
  const boolLeft = isBooleanExpression(cond);
587
593
  if (!boolLeft) cond = t__namespace.unaryExpression("!", t__namespace.unaryExpression("!", cond, true), true);
588
594
  id = inline ?
595
+ memo ?
589
596
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
597
+ t__namespace.arrowFunctionExpression([], cond) :
590
598
  path.scope.generateUidIdentifier("_c$");
591
599
  if (boolLeft) {
592
600
  nextPath.node.left = t__namespace.callExpression(id, []);
@@ -606,7 +614,7 @@ inline)
606
614
  t__namespace.variableDeclaration("var", [
607
615
  t__namespace.variableDeclarator(
608
616
  id,
609
- config.memoWrapper ?
617
+ memo ?
610
618
  t__namespace.callExpression(memo, [t__namespace.arrowFunctionExpression([], cond)]) :
611
619
  t__namespace.arrowFunctionExpression([], cond)
612
620
  )]
@@ -2206,9 +2214,35 @@ results)
2206
2214
  results.hasHydratableEvent = results.hasHydratableEvent || hasHydratableEvent;
2207
2215
  }
2208
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
+
2209
2236
  function findLastElement(children, hydratable) {
2210
2237
  let lastElement = -1,
2211
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;
2212
2246
  for (let i = children.length - 1; i >= 0; i--) {
2213
2247
  const node = children[i].node;
2214
2248
  if (
@@ -2222,6 +2256,9 @@ function findLastElement(children, hydratable) {
2222
2256
  lastElement = i;
2223
2257
  break;
2224
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;
2225
2262
  }
2226
2263
  return lastElement;
2227
2264
  }
@@ -2483,6 +2520,15 @@ config)
2483
2520
  t$1.isJSXSpreadAttribute(attr) ||
2484
2521
  t$1.isJSXIdentifier(attr.name) &&
2485
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)) ||
2486
2532
  t$1.isJSXNamespacedName(attr.name) && attr.name.namespace.name === "prop" ||
2487
2533
  t$1.isJSXExpressionContainer(attr.value) &&
2488
2534
  !(
@@ -2754,6 +2800,8 @@ function wrapDynamics$1(path, dynamics) {
2754
2800
  if (!dynamics.length) return;
2755
2801
  const config = getConfig(path);
2756
2802
 
2803
+ // dynamics are only queued when effectWrapper is configured (element.ts
2804
+ // guards every push), so the name is always a string here
2757
2805
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
2758
2806
 
2759
2807
  if (dynamics.length === 1) {
@@ -3690,12 +3738,25 @@ path,
3690
3738
  `Fragments can only be used top level in JSX. Not used under a <${tagName}>.`
3691
3739
  );
3692
3740
  }
3741
+ const allocatesIds = hydratable && canChildSlotAllocateIds(path);
3693
3742
  const child = transformNode(path);
3694
3743
  if (!child) return memo;
3695
3744
  if (markers && child.exprs.length && !child.spreadElement)
3696
3745
  memo.push(t.stringLiteral("<!--$-->"));
3697
3746
  if (child.exprs.length && !doNotEscape && !child.spreadElement)
3698
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
+ }
3699
3760
  memo.push(
3700
3761
  getCreateTemplate(config, path, child)(path, child, false)
3701
3762
  );
@@ -3764,7 +3825,19 @@ path,
3764
3825
  if (runningObject.length || !props.length) props.push(t.objectExpression(runningObject));
3765
3826
 
3766
3827
  if (props.length > 1 || dynamicSpread) {
3767
- 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];
3768
3841
  }
3769
3842
  }
3770
3843
 
@@ -3826,8 +3899,9 @@ wrap)
3826
3899
  const inner = result.wontEscape ?
3827
3900
  result.exprs[0] :
3828
3901
  wrapFragmentChildWithEscape(path, result.exprs[0]);
3829
- return t__namespace.callExpression(registerImportMethod(path, getConfig(path).memoWrapper, undefined), [
3830
- inner]
3902
+ return t__namespace.callExpression(
3903
+ registerImportMethod(path, getConfig(path).memoWrapper, undefined),
3904
+ [inner]
3831
3905
  );
3832
3906
  }
3833
3907
  return result.exprs[0];
@@ -4454,6 +4528,8 @@ function wrapDynamics(path, dynamics) {
4454
4528
  if (!dynamics.length) return;
4455
4529
  const config = getConfig(path);
4456
4530
 
4531
+ // dynamics are only queued when effectWrapper is configured (element.ts
4532
+ // guards every push), so the name is always a string here
4457
4533
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
4458
4534
 
4459
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.16",
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.16"
31
+ "@dom-expressions/runtime": "0.50.0-next.17"
32
32
  },
33
33
  "gitHead": "eb463c653325e24824422cff6bfed0d35113ef33",
34
34
  "scripts": {