@solidjs/babel-plugin 2.0.0-rc.11 → 2.0.0-rc.14

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.
@@ -7,4 +7,4 @@ src/shared/transform.ts -> src/dom/element.ts -> src/universal/element.ts -> src
7
7
  src/shared/transform.ts -> src/ssr/element.ts -> src/shared/transform.ts
8
8
  src/shared/transform.ts -> src/shared/component.ts -> src/shared/transform.ts
9
9
  src/shared/transform.ts -> src/shared/fragment.ts -> src/shared/transform.ts
10
- created index.js in 3s
10
+ created index.js in 3.2s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @solidjs/babel-plugin
2
2
 
3
+ ## 2.0.0-rc.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 59fb244: SSR `hoistProps` skips TypeScript type positions when collecting captures, so on authored TSX (types parsed, stripped later) a `typeof x` query or a type named like a value no longer captures that value, or forces the literal fallback when it is reassigned (#3828).
8
+ - 3c1f809: Preserve `/* istanbul ignore … */` and `/* c8 ignore … */` block comments written as JSX children on the generated component `children` getter. Line comments are not carried.
9
+ - 98b916d: Fix compiled SSR output that renders `style=""` or `class=""` for `null` and `undefined`. Babel and the native compiler now omit these attributes on elements without a spread.
10
+ - 98d35b9: Fix computed keys in compiled SSR `style` objects (builds without `serverComponents`). A computed key's name got no `;`, so `style={{ color: "red", [k]: v }}` rendered `color:redtop:1px` and the browser dropped both declarations. Hydration keeps the server's inline style, so they stayed dropped. A nullish first value also no longer leaves a leading `;`. Both compilers now pass a multi-entry object to a new `ssrStyleProperties(name, value, …)` helper, which writes the separator only between the entries it writes; a single entry keeps `ssrStyleProperty`.
11
+ - 4f67697: **Breaking:** only `on` followed by an uppercase letter (`onClick`, `onPointerDown`) is an event handler. Lowercase `on*` names (`onclick`, `onmouseover`) are plain attributes everywhere:
12
+ - Both compilers compile `onclick={expr}` like any other attribute (`setAttribute`, reactive when `expr` is dynamic) instead of binding a delegated or native event, and SSR renders it as an escaped attribute instead of dropping it. A leftover 1.x `on:click={fn}` is likewise a plain namespaced attribute (it previously compiled to `addEventListener(":click", fn)`).
13
+ - `@solidjs/web` `spread`/`assign` set lowercase `on*` keys as attributes, the server spread walk renders them, and `useHead` applies lowercase `on*` attributes (camelCase handler names stay skipped). `ssrAttribute` escapes a function value instead of interpolating its source raw.
14
+ - `@solidjs/html` and `@solidjs/h` elements wrap a function passed to a lowercase `on*` in a getter like any other attribute; only `onXxx` and `ref` are exempt.
15
+ - `@solidjs/html` components follow `@solidjs/h`'s rule: every zero-argument function prop, `onXxx` handlers and `ref` included, is a getter, so a component handler must declare its event argument (`onClick=${e => …}`).
16
+ - New dev-only check `LOWERCASE_EVENT_ATTRIBUTE` (added to the `DiagnosticCode` union): warns once per attribute name when a function is set on a lowercase `on*` (or `on:`) attribute, naming the camelCase handler to use.
17
+
18
+ - 7513131: Preserve the source location of captured references in SSR props constructors so source maps point to the authored expression, including dynamic TSRX tags.
19
+
20
+ ## 2.0.0-rc.13
21
+
22
+ ## 2.0.0-rc.12
23
+
24
+ ### Patch Changes
25
+
26
+ - ad1ecc5: `serverComponents` (SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamic `class` or `style` now compiles to a whole-attribute `_$ssrElementAttribute` hole instead of a value inside the template's quotes, so a server component's `class={{ selected: filters.all }}` can mark the class name a client attribute slot owns; `ref`/`on*` positions compile, as before, to one guarded `_$ssrClaim` hole per element, which now emits `_s:on:*`/`_s:ref` markers for slot reads (the `_bnd` behavior-claim marker is gone). A spread element compiles its named `ref`/`on*` (`<button {...rest} onClick={row.go} ref={row.el} />`, wherever they sit relative to the spreads) to the same claim map — `{ click: expr, ref: [a, b] }`, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (`<b {...a} onClick={go} {...b}>` → `{ 1: { click: go } }`) and handed to `_$ssrElement` as a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client's `spread`/`merge()` do (the last source that has the key owns it, `undefined` included). A `ref` beside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercased `on*` remainder (`onClick` → `click`); `on:`/`oncapture:` are not 2.0 syntaxes and are no longer special-cased. Both compilers share the `attributeSlots` server-components fixture; plain SSR output is unchanged.
27
+ - 077c787: A `<textarea>` with a dynamic `value`/`defaultValue` no longer desyncs hydration ids for the siblings after it (#3691). The server folds the value into the textarea's text content, and that fold was taking the `_$scope` hydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like the `innerHTML`/`textContent` redirects (#3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.
28
+ - c71486d: DOM output: an element's `ref` now runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration (#3685).
29
+
3
30
  ## 2.0.0-rc.11
4
31
 
5
32
  ## 2.0.0-rc.10
package/README.md CHANGED
@@ -253,7 +253,7 @@ Inline style attributes in templates when the value is a string or `Record<strin
253
253
  - Type: `boolean`
254
254
  - Default: `false`
255
255
 
256
- SSR-only: emit behavior-claim (`_bnd`) markers for `ref` / `on*` on intrinsic elements.
256
+ SSR-only: keep attribute-slot positions on intrinsic elements bindable — `ref` / `onXxx` handlers compile to a guarded `ssrClaim` hole instead of dropping, and a dynamic `class` / `style` compiles to a whole-attribute `ssrElementAttribute` hole instead of a value inside template quotes.
257
257
 
258
258
  ### hoistProps
259
259
 
@@ -329,7 +329,7 @@ const Parent = () => {
329
329
 
330
330
  ### on(eventName)
331
331
 
332
- CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties.
332
+ CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties. Only `on` followed by an uppercase letter is an event: lowercase names such as `onclick` are plain attributes, compiled (and server-rendered) like any other attribute.
333
333
 
334
334
  Pass an array to bind a value: the second item is the first argument to the handler, the event is second.
335
335
 
package/index.js CHANGED
@@ -427,13 +427,38 @@ path)
427
427
 
428
428
  }
429
429
 
430
+ const coverageIgnoreCommentData = "solid.coverageIgnoreComments";
431
+
432
+ function getCoverageIgnoreComments(path) {
433
+ return path.getData(coverageIgnoreCommentData);
434
+ }
435
+
430
436
  // remove unnecessary JSX Text nodes
431
437
  function filterChildren(children) {
432
- return children.filter(
433
- ({ node: child }) =>
434
- !(t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) && (
435
- !t__namespace.isJSXText(child) || !/^[\r\n]\s*$/.test(child.extra?.raw ?? ""))
436
- );
438
+ const filtered = [];
439
+ let pendingCoverageIgnoreComments = [];
440
+
441
+ for (const path of children) {
442
+ const child = path.node;
443
+ if (t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) {
444
+ pendingCoverageIgnoreComments.push(
445
+ ...(child.expression.innerComments?.filter(
446
+ (comment) =>
447
+ comment.type === "CommentBlock" && /^\s*(istanbul|c8)\s+ignore\b/.test(comment.value)
448
+ ) ?? [])
449
+ );
450
+ continue;
451
+ }
452
+ if (t__namespace.isJSXText(child) && /^[\r\n]\s*$/.test(child.extra?.raw ?? "")) {
453
+ continue;
454
+ }
455
+ if (pendingCoverageIgnoreComments.length) {
456
+ path.setData(coverageIgnoreCommentData, pendingCoverageIgnoreComments);
457
+ pendingCoverageIgnoreComments = [];
458
+ }
459
+ filtered.push(path);
460
+ }
461
+ return filtered;
437
462
  }
438
463
 
439
464
  function checkLength(children) {
@@ -461,6 +486,12 @@ function trimWhitespace(text) {
461
486
  return text.replace(/\s+/g, " ");
462
487
  }
463
488
 
489
+ // Only `on` + an uppercase letter is an event handler. Lowercase `on*`
490
+ // names (`onclick`) are plain attributes.
491
+ function isEventName(name) {
492
+ return /^on[A-Z]/.test(name);
493
+ }
494
+
464
495
  function toEventName(name) {
465
496
  return name.slice(2).toLowerCase();
466
497
  }
@@ -1047,6 +1078,13 @@ isSSR)
1047
1078
  child.extra = { raw: text, rawValue: text };
1048
1079
  } else {
1049
1080
  child = t__namespace.jsxExpressionContainer(value);
1081
+ // The folded value is the textarea's text content on the server, but
1082
+ // the client writes it as a plain `value` property effect that never
1083
+ // allocates a hydration id. Flag it like the innerHTML/textContent
1084
+ // redirects (#3015) so the SSR child pipeline skips the `_$scope` id
1085
+ // reservation — otherwise the reservation shifts every keyed sibling
1086
+ // after the textarea by one id (#3691).
1087
+ child._childProperty = true;
1050
1088
  }
1051
1089
  path.node.children = [child];
1052
1090
  attr.remove();
@@ -2230,6 +2268,7 @@ results)
2230
2268
  hasChildren = path.node.children.length > 0,
2231
2269
  hasSpread = attributes.some((attribute) => t$1.isJSXSpreadAttribute(attribute.node)),
2232
2270
  config = getConfig(path);
2271
+ const refs = [];
2233
2272
 
2234
2273
  // preprocess spreads
2235
2274
  if (hasSpread) {
@@ -2709,7 +2748,7 @@ results)
2709
2748
  binding.kind === "const" || binding.kind === "module");
2710
2749
  if (!isConstant && t$1.isLVal(value.expression)) {
2711
2750
  const refIdentifier = path.scope.generateUidIdentifier("_ref$");
2712
- results.exprs.unshift(
2751
+ refs.push(
2713
2752
  t$1.variableDeclaration("var", [t$1.variableDeclarator(refIdentifier, value.expression)]),
2714
2753
  t$1.expressionStatement(
2715
2754
  t$1.conditionalExpression(
@@ -2738,7 +2777,7 @@ results)
2738
2777
  t$1.isFunction(value.expression) ||
2739
2778
  t$1.isArrayExpression(value.expression))
2740
2779
  {
2741
- results.exprs.unshift(
2780
+ refs.push(
2742
2781
  t$1.expressionStatement(
2743
2782
  t$1.callExpression(
2744
2783
  registerImportMethod(path, "ref", getRendererConfig(path, "dom").moduleName),
@@ -2748,7 +2787,7 @@ results)
2748
2787
  );
2749
2788
  } else {
2750
2789
  const refIdentifier = path.scope.generateUidIdentifier("_ref$");
2751
- results.exprs.unshift(
2790
+ refs.push(
2752
2791
  t$1.variableDeclaration("var", [t$1.variableDeclarator(refIdentifier, value.expression)]),
2753
2792
  t$1.expressionStatement(
2754
2793
  t$1.logicalExpression(
@@ -2775,7 +2814,7 @@ results)
2775
2814
  }
2776
2815
  } else if (key === "children") {
2777
2816
  children = value;
2778
- } else if (key.startsWith("on")) {
2817
+ } else if (isEventName(key)) {
2779
2818
  const ev = toEventName(key);
2780
2819
  if (
2781
2820
  config.delegateEvents && (
@@ -2978,6 +3017,9 @@ results)
2978
3017
  path.node.children.push(children);
2979
3018
  }
2980
3019
  if (spreadExpr) results.exprs.push(...(Array.isArray(spreadExpr) ? spreadExpr : [spreadExpr]));
3020
+ // Refs run after the element's attributes and spread are applied, so a ref's
3021
+ // own writes survive client creation the way they survive hydration.
3022
+ results.exprs.push(...refs);
2981
3023
 
2982
3024
  results.hasHydratableEvent = results.hasHydratableEvent || hasHydratableEvent;
2983
3025
  }
@@ -4060,6 +4102,7 @@ function takeHoistedProps(program) {
4060
4102
 
4061
4103
 
4062
4104
 
4105
+
4063
4106
  function keyName(node) {
4064
4107
  if (node.computed) return undefined;
4065
4108
  if (t__namespace.isIdentifier(node.key)) return node.key.name;
@@ -4078,6 +4121,20 @@ function insideRealFunction(path, stop) {
4078
4121
  return false;
4079
4122
  }
4080
4123
 
4124
+ /** Type positions are erased before the code runs: a name there (the `x` of
4125
+ * `typeof x`, or a type that shares a value's name) is not a value the
4126
+ * constructor can be handed. `TSType` covers `implements`/`extends` heritage. */
4127
+ function isTypePosition(path) {
4128
+ return (
4129
+ path.isTSType() ||
4130
+ path.isTSTypeAnnotation() ||
4131
+ path.isTSTypeParameterDeclaration() ||
4132
+ path.isTSTypeParameterInstantiation() ||
4133
+ path.isTSInterfaceDeclaration() ||
4134
+ path.isTSTypeAliasDeclaration());
4135
+
4136
+ }
4137
+
4081
4138
  /** Is `binding` initialized by the time the site constructs its props? The
4082
4139
  * getter read it lazily; the constructor reads it now. A parameter or a
4083
4140
  * hoisted function always is. Otherwise the declaration must precede the
@@ -4163,6 +4220,10 @@ function hoistSite(p, state) {
4163
4220
  if (q.node.meta.name === "new" && !insideRealFunction(q, m)) fallback = true;
4164
4221
  return;
4165
4222
  }
4223
+ if (isTypePosition(q)) {
4224
+ q.skip();
4225
+ return;
4226
+ }
4166
4227
  if (!q.isIdentifier()) return;
4167
4228
  const name = q.node.name;
4168
4229
  let write = false;
@@ -4195,7 +4256,7 @@ function hoistSite(p, state) {
4195
4256
  }
4196
4257
  let capture = captures.get(binding);
4197
4258
  if (!capture) {
4198
- capture = { binding, name, index: captures.size };
4259
+ capture = { binding, name, reference: q.node, index: captures.size };
4199
4260
  captures.set(binding, capture);
4200
4261
  }
4201
4262
  uses.add(binding);
@@ -4217,7 +4278,9 @@ function hoistSite(p, state) {
4217
4278
  for (const capture of captures.values()) {
4218
4279
  const param = scope.generateUidIdentifier("p");
4219
4280
  params.push(param);
4220
- args.push(t__namespace.identifier(capture.name));
4281
+ const argument = t__namespace.identifier(capture.name);
4282
+ argument.loc = capture.reference.loc;
4283
+ args.push(argument);
4221
4284
  body.push(
4222
4285
  t__namespace.expressionStatement(
4223
4286
  t__namespace.assignmentExpression(
@@ -4852,10 +4915,12 @@ info)
4852
4915
  const hasChildren = path.node.children.length > 0,
4853
4916
  attributes = normalizeAttributes(path);
4854
4917
  let children;
4855
- // Server-components claims: ref/on* positions on server-rendered
4856
- // intrinsics collect here and emit as one guarded whole-attribute hole
4857
- // (` _bnd="..."` or "") after the loop. Evaluation is gated on the render
4858
- // context's claims flag so plain SSR never runs the expressions.
4918
+ // Server-components handler positions: ref/on* expressions on
4919
+ // server-rendered intrinsics collect here and emit as one guarded
4920
+ // whole-attribute hole after the loop, where `ssrClaim` turns attribute-slot
4921
+ // reads into `_s:on:*` / `_s:ref` markers (and drops server-local
4922
+ // functions). Evaluation is gated on the render context's claims flag so
4923
+ // plain SSR never runs the expressions.
4859
4924
  const claims = [];
4860
4925
 
4861
4926
  attributes.forEach((attribute) => {
@@ -4897,14 +4962,11 @@ info)
4897
4962
  return;
4898
4963
  }
4899
4964
  if (key.startsWith("prop:")) return;
4900
- if (key.startsWith("on")) {
4901
- // Capture-phase variants can't ride delegation; v1 drops them as
4902
- // before. `on:x` keeps the raw name, `onXxx` lowercases — the same
4903
- // event-name derivation as the client runtime.
4904
- if (info.serverComponents && !key.startsWith("oncapture:")) {
4905
- const pos = key.startsWith("on:") ? key.slice(3) : key.slice(2).toLowerCase();
4906
- if (pos) claims.push([pos, value.expression]);
4907
- }
4965
+ if (isEventName(key)) {
4966
+ // `onXxx` lowercases to the event name — the client runtime's own
4967
+ // derivation (`onClick` -> `click`); the position is bound under it.
4968
+ if (info.serverComponents)
4969
+ claims.push([toEventName(key), value.expression]);
4908
4970
  return;
4909
4971
  }
4910
4972
  if (ChildProperties.has(key)) {
@@ -4938,6 +5000,56 @@ info)
4938
5000
  checkMember: true,
4939
5001
  checkTags: true
4940
5002
  });
5003
+ // Server components (principles §9.2.3) need a whole-attribute
5004
+ // serializer that can emit a position marker for a binding-slot
5005
+ // value. The ordinary class/style serializers cannot bind slots.
5006
+ // Object literals stay objects (no inlining) so a slot used as a
5007
+ // property's value can also emit its marker.
5008
+ if (info.serverComponents && (key === "class" || key === "style")) {
5009
+ const attr = t.callExpression(registerImportMethod(path, "ssrElementAttribute"), [
5010
+ t.stringLiteral(key),
5011
+ value.expression]
5012
+ );
5013
+ results.template.push("");
5014
+ results.templateValues.push(
5015
+ isDynamicValue ?
5016
+ hoistExpression(path, results, t.arrowFunctionExpression([], attr), {
5017
+ group: true
5018
+ }) :
5019
+ attr
5020
+ );
5021
+ return;
5022
+ }
5023
+ if ((key === "class" || key === "style") && !t.isObjectExpression(value.expression)) {
5024
+ // The whole attribute must be absent for nullish values. Keep the
5025
+ // ordinary serializers: ssrElementAttribute would also bind slots
5026
+ // without serverComponents. Capture the value once before testing it.
5027
+ const id = path.scope.generateUidIdentifier("v$");
5028
+ const attr = t.callExpression(
5029
+ t.arrowFunctionExpression(
5030
+ [id],
5031
+ t.conditionalExpression(
5032
+ t.binaryExpression("==", t.cloneNode(id), t.nullLiteral()),
5033
+ t.stringLiteral(""),
5034
+ t.callExpression(registerImportMethod(path, "ssrAttribute"), [
5035
+ t.stringLiteral(key),
5036
+ t.callExpression(
5037
+ registerImportMethod(path, key === "class" ? "ssrClassName" : "ssrStyle"),
5038
+ [t.cloneNode(id)]
5039
+ )]
5040
+ )
5041
+ )
5042
+ ),
5043
+ [value.expression]
5044
+ );
5045
+ results.template.push("");
5046
+ results.templateValues.push(
5047
+ isDynamicValue ?
5048
+ hoistExpression(path, results, t.arrowFunctionExpression([], attr), { group: true }) :
5049
+ attr
5050
+ );
5051
+ return;
5052
+ }
4941
5053
  let doEscape = true;
4942
5054
  let isBoolean =
4943
5055
  t.isBooleanLiteral(value) ||
@@ -4952,52 +5064,48 @@ info)
4952
5064
  if (value.expression.properties.length === 0) {
4953
5065
  return;
4954
5066
  }
4955
- const props = value.expression.properties.flatMap((p, i) => {
4956
- if (t.isSpreadElement(p) || t.isObjectMethod(p)) return [];
5067
+ const entries = value.expression.properties.filter(
5068
+ (p) => t.isObjectProperty(p)
5069
+ );
5070
+ // One entry is `ssrStyleProperty(name, value)`; several go to
5071
+ // `ssrStyleProperties`, which writes the `;` only between the
5072
+ // entries it writes, so a nullish one leaves no separator behind.
5073
+ const helper = registerImportMethod(
5074
+ path,
5075
+ entries.length === 1 ? "ssrStyleProperty" : "ssrStyleProperties"
5076
+ );
5077
+ const args = entries.flatMap((p) => {
5078
+ let name;
4957
5079
  if (p.computed) {
4958
5080
  // Computed keys are user-controlled at runtime; wrap with
4959
- // `_$escape(..., true)` so ssrStyleProperty can stay a pure
4960
- // string concat helper (literal-key path is already safe).
4961
- const escape = registerImportMethod(path, "escape");
4962
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
4963
- t.binaryExpression(
5081
+ // `_$escape(..., true)` so the style helpers can stay pure
5082
+ // string concat (the literal-key path is already safe).
5083
+ name = t.binaryExpression(
4964
5084
  "+",
4965
- t.callExpression(escape, [
5085
+ t.callExpression(registerImportMethod(path, "escape"), [
4966
5086
  p.key,
4967
5087
  t.booleanLiteral(true)]
4968
5088
  ),
4969
5089
  t.stringLiteral(":")
4970
- ),
4971
- escapeExpression(
4972
- path,
4973
- p.value,
4974
- true,
4975
- true
4976
- )]
5090
+ );
5091
+ } else {
5092
+ name = t.stringLiteral(
5093
+ (t.isIdentifier(p.key) ?
5094
+ p.key.name :
5095
+ p.key.value) + ":"
4977
5096
  );
4978
5097
  }
4979
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
4980
- t.stringLiteral(
4981
- (i ? ";" : "") + (
4982
- t.isIdentifier(p.key) ?
4983
- p.key.name :
4984
- p.key.value) +
4985
- ":"
4986
- ),
5098
+ return [
5099
+ name,
4987
5100
  escapeExpression(
4988
5101
  path,
4989
5102
  p.value,
4990
5103
  true,
4991
5104
  true
4992
- )]
4993
- );
4994
- });
5105
+ )];
4995
5106
 
4996
- let res = props[0];
4997
- for (let i = 1; i < props.length; i++) {
4998
- res = t.binaryExpression("+", res, props[i]);
4999
- }
5000
- value.expression = res;
5107
+ });
5108
+ value.expression = t.callExpression(helper, args);
5001
5109
  } else {
5002
5110
  value.expression = t.callExpression(registerImportMethod(path, "ssrStyle"), [
5003
5111
  value.expression]
@@ -5081,23 +5189,7 @@ info)
5081
5189
  }
5082
5190
  });
5083
5191
  if (claims.length) {
5084
- // Duplicate event keys were already last-wins-stripped above; `ref` is
5085
- // exempt from that pass (client semantics fire every ref), so multiple
5086
- // refs merge into an array value.
5087
- const byPos = new Map();
5088
- for (const [pos, expr] of claims) {
5089
- let list = byPos.get(pos);
5090
- if (!list) byPos.set(pos, list = []);
5091
- list.push(expr);
5092
- }
5093
- const map = t.objectExpression(
5094
- [...byPos].map(([pos, exprs]) =>
5095
- t.objectProperty(
5096
- t.stringLiteral(pos),
5097
- exprs.length === 1 ? exprs[0] : t.arrayExpression(exprs)
5098
- )
5099
- )
5100
- );
5192
+ const map = claimMap(claims);
5101
5193
  // `_$sharedConfig.context && _$sharedConfig.context.claims
5102
5194
  // ? _$ssrClaim({...}) : ""`
5103
5195
  // — the claims flag is only set inside a server component's render
@@ -5243,7 +5335,7 @@ results,
5243
5335
 
5244
5336
  function createElement(
5245
5337
  path,
5246
- { topLevel, hydratable })
5338
+ { topLevel, hydratable, serverComponents })
5247
5339
  {
5248
5340
  const tagName = getTagName(path.node),
5249
5341
  config = getConfig(path),
@@ -5296,6 +5388,21 @@ path,
5296
5388
  const node = attribute.node;
5297
5389
  return !(t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "ref");
5298
5390
  });
5391
+ // Server components (principles §9.2.3): the named `ref`/`on*` attributes
5392
+ // of a spread element are handler positions like a template element's,
5393
+ // and compile to the same claim map — `{ click: expr, ref: [a, b] }`,
5394
+ // duplicate refs merged, a duplicate handler last-wins as the template
5395
+ // path strips it — keyed by the index of the source each attribute sits
5396
+ // before (`<b {...a} onClick={go} {...b}>` → `{ 1: { click: go } }`, the
5397
+ // spreads being sources 0 and 2), and handed to `ssrElement` as a thunk
5398
+ // it reads only inside a server component's render (the gate the template
5399
+ // path's `ssrClaim` guard reads), so plain SSR never evaluates the
5400
+ // expressions. The runtime settles each handler position in source order,
5401
+ // as the client's `spread(el, [a, { onClick: go }, b])` does — a spread at
5402
+ // that index or later that HAS the key owns it — and merges every ref.
5403
+ // Plain SSR output is unchanged (dropped, as a server element has no
5404
+ // handlers to run).
5405
+ const claims = [];
5299
5406
 
5300
5407
  let props;
5301
5408
  // Attributes written AFTER the last spread are markup, not a source: no
@@ -5316,7 +5423,13 @@ path,
5316
5423
  // keys.
5317
5424
  const tail = [];
5318
5425
  const skipKeys = [];
5319
- if (propAttributes.length === 1 && t.isJSXSpreadAttribute(propAttributes[0].node)) {
5426
+ if (
5427
+ propAttributes.length === 1 &&
5428
+ t.isJSXSpreadAttribute(propAttributes[0].node) &&
5429
+ // A `ref` beside the lone spread is a claim under `serverComponents`;
5430
+ // the loop below places it.
5431
+ !(serverComponents && attributes.length > 1))
5432
+ {
5320
5433
  props = [propAttributes[0].node.argument];
5321
5434
  } else {
5322
5435
  props = [];
@@ -5362,8 +5475,29 @@ path,
5362
5475
  node.name.name;
5363
5476
 
5364
5477
  if (hasChildren && key === "children") return;
5365
- if (key === "ref") return;
5366
- if (key.startsWith("prop:") || key.startsWith("on")) return;
5478
+ if (key === "ref" || isEventName(key)) {
5479
+ if (serverComponents && t.isJSXExpressionContainer(value)) {
5480
+ const expression = value.expression;
5481
+ const pos = key === "ref" ? "ref" : toEventName(key);
5482
+ if (
5483
+ !(
5484
+ t.isJSXEmptyExpression(expression) ||
5485
+ t.isStringLiteral(expression) ||
5486
+ t.isNumericLiteral(expression) ||
5487
+ t.isBooleanLiteral(expression)))
5488
+
5489
+ {
5490
+ // The index of the source this attribute sits before: the
5491
+ // sources pushed so far, plus the running literal if it will
5492
+ // be pushed ahead of the next spread. The literal never
5493
+ // carries a handler key, so a spread's index is either below
5494
+ // this or at/after it — never ambiguous.
5495
+ claims.push([props.length + (runningObject.length ? 1 : 0), pos, expression]);
5496
+ }
5497
+ }
5498
+ return;
5499
+ }
5500
+ if (key.startsWith("prop:")) return;
5367
5501
  if (i > lastSpread) {
5368
5502
  const part = tailAttribute(path, tagName, key, node);
5369
5503
  if (part !== undefined) {
@@ -5440,10 +5574,65 @@ path,
5440
5574
  }
5441
5575
  args.push(registerSkip(path, skipKeys), markup);
5442
5576
  }
5577
+ if (claims.length) {
5578
+ if (!skipKeys.length) args.push(t.identifier("undefined"), t.identifier("undefined"));
5579
+ args.push(t.arrowFunctionExpression([], claimSegments(claims)));
5580
+ }
5443
5581
  const exprs = [t.callExpression(registerImportMethod(path, "ssrElement"), args)];
5444
5582
  return { exprs, template: "", declarations: [], dynamics: [], spreadElement: true };
5445
5583
  }
5446
5584
 
5585
+ /**
5586
+ * A spread element's claim map by source index (`createElement`): `{ 1: {
5587
+ * click: go }, 3: { ref: el } }`. A handler position keeps its LAST named
5588
+ * attribute only — the template path's duplicate strip, applied here to the
5589
+ * claims (a later attribute wins whatever sits between, so the earlier one
5590
+ * is never read on either side); refs merge within a segment as `claimMap`
5591
+ * merges them, and across segments at the runtime.
5592
+ */
5593
+ function claimSegments(
5594
+ claims)
5595
+ {
5596
+ const lastHandler = new Map();
5597
+ claims.forEach(([, pos], i) => {
5598
+ if (pos !== "ref") lastHandler.set(pos, i);
5599
+ });
5600
+ const segments = new Map();
5601
+ claims.forEach(([index, pos, expr], i) => {
5602
+ if (pos !== "ref" && lastHandler.get(pos) !== i) return;
5603
+ let list = segments.get(index);
5604
+ if (!list) segments.set(index, list = []);
5605
+ list.push([pos, expr]);
5606
+ });
5607
+ return t.objectExpression(
5608
+ [...segments].map(([index, list]) => t.objectProperty(t.numericLiteral(index), claimMap(list)))
5609
+ );
5610
+ }
5611
+
5612
+ /**
5613
+ * The compiled claim map of an element's handler positions, `{ click: expr,
5614
+ * ref: [a, b] }`: duplicate event keys were last-wins-stripped by the
5615
+ * template path's duplicate strip (or `claimSegments`); `ref` is exempt from
5616
+ * that pass (client semantics fire every ref), so multiple refs merge into
5617
+ * an array value.
5618
+ */
5619
+ function claimMap(claims) {
5620
+ const byPos = new Map();
5621
+ for (const [pos, expr] of claims) {
5622
+ let list = byPos.get(pos);
5623
+ if (!list) byPos.set(pos, list = []);
5624
+ list.push(expr);
5625
+ }
5626
+ return t.objectExpression(
5627
+ [...byPos].map(([pos, exprs]) =>
5628
+ t.objectProperty(
5629
+ t.stringLiteral(pos),
5630
+ exprs.length === 1 ? exprs[0] : t.arrayExpression(exprs)
5631
+ )
5632
+ )
5633
+ );
5634
+ }
5635
+
5447
5636
  /**
5448
5637
  * What an attribute after an element's last spread contributes to
5449
5638
  * `ssrElement`'s attribute string. A static is written as the template path
@@ -5926,14 +6115,14 @@ path)
5926
6115
  t__namespace.isFunction(childResult[0]) ?
5927
6116
  childResult[0].body :
5928
6117
  childResult[0];
5929
- runningObject.push(
5930
- t__namespace.objectMethod(
5931
- "get",
5932
- t__namespace.identifier("children"),
5933
- [],
5934
- t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
5935
- )
6118
+ const getter = t__namespace.objectMethod(
6119
+ "get",
6120
+ t__namespace.identifier("children"),
6121
+ [],
6122
+ t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
5936
6123
  );
6124
+ if (childResult[2]?.length) getter.leadingComments = childResult[2];
6125
+ runningObject.push(getter);
5937
6126
  } else runningObject.push(t__namespace.objectProperty(t__namespace.identifier("children"), childResult[0]));
5938
6127
  }
5939
6128
  if (runningObject.length || !props.length) props.push(propsLiteral(runningObject));
@@ -5994,9 +6183,11 @@ config)
5994
6183
  if (!filteredChildren.length) return;
5995
6184
  let dynamic = false;
5996
6185
  let pathNodes = [];
6186
+ let coverageIgnoreComments;
5997
6187
 
5998
6188
  let transformedChildren = filteredChildren.reduce(
5999
6189
  (memo, path) => {
6190
+ coverageIgnoreComments ||= getCoverageIgnoreComments(path);
6000
6191
  if (t__namespace.isJSXText(path.node)) {
6001
6192
  const v = htmlEntities.decode(trimWhitespace(path.node.extra?.raw ?? ""));
6002
6193
  if (v.length) {
@@ -6053,7 +6244,7 @@ config)
6053
6244
  transformedChildren = t__namespace.arrowFunctionExpression([], t__namespace.arrayExpression(transformedChildren));
6054
6245
  dynamic = true;
6055
6246
  }
6056
- return [transformedChildren, dynamic];
6247
+ return [transformedChildren, dynamic, coverageIgnoreComments];
6057
6248
  }
6058
6249
 
6059
6250
  function transformFragmentChildren(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solidjs/babel-plugin",
3
3
  "description": "Babel compiler plugin for Solid templates",
4
- "version": "2.0.0-rc.11",
4
+ "version": "2.0.0-rc.14",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {