@solidjs/babel-plugin 2.0.0-rc.15 → 2.0.0-rc.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.
@@ -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 2.1s
10
+ created index.js in 3.3s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @solidjs/babel-plugin
2
2
 
3
+ ## 2.0.0-rc.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 4a99c84: `sourceNames` gains a `locations` kind (and the plugin a `root` option): component and binding labels carry a `file:line:col` for diagnostics. `createComponent(Home, props, "Home", "src/App.tsx:12:5")` points at the `<Home` opening element (DOM and SSR output); a binding's options gain `loc` after `name` — the attribute's start for an attribute effect (a merged effect points at the first attribute it lists), the hole's `{` for an insert — and a spread passes it after the tag (`spread(el, props, false, undefined, "div", "src/App.tsx:20:12")`). The path is relative to `root` (default: the working directory) with `/` separators; line and column are 1-based, so a dev server links the location straight to the source. Like the other kinds, `locations` follows `dev` when unset, `true` turns every kind on, and the object form picks; a location is only emitted beside its label, and never when Babel has no `filename` for the module.
8
+ - 3f36f77: Server components: binding slots are removed. Client components in slots replace them: place a client component through a `Slot` (`<props.x />`) where the server's markup needs client behavior, or a form `action` for a mutation. Removed with them: the `BindingSlot`, `SlotOutput` and `SlotError` types, the lazy `@solidjs/web/frames/bind` entry and its `"bind"` tier, the `_s:*` position markers and the compiled `ssrClaim` hole, the effect of the `serverComponents` compiler option (`@solidjs/compiler` and `@solidjs/babel-plugin` still accept it, so existing build configs keep working, and ignore it), and the `BINDING_SLOT_POSITION` diagnostic code (`@solidjs/signals`' `DiagnosticCode`, `solid-js`'s `diagnostics.json`). Ordinary slots, their `slot:<id>` ranges and records, and `$key` are unchanged.
9
+ - d0ba31c: SSR evaluates a `ref` expression beside a spread, as it already does without one, so a ref factory's owned setup (`ref={tooltip()}`) runs on the server too and the hydration ids after it line up. The ref itself is still never called on the server (#4081). Under `serverComponents` the same holds outside a server component's render, where a ref expression was never evaluated on either path; inside one the claim evaluates it, once.
10
+
3
11
  ## 2.0.0-rc.15
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
@@ -129,16 +129,24 @@ Development output. With `hydratable`, emits the hydration walk validation helpe
129
129
 
130
130
  ### sourceNames
131
131
 
132
- - Type: `boolean | { components?: boolean; bindings?: boolean }`
132
+ - Type: `boolean | { components?: boolean; bindings?: boolean; locations?: boolean }`
133
133
  - Default: follows `dev` — every kind on when `dev: true`, off otherwise
134
134
 
135
135
  Names as written in source, carried into the output so the dev and observe runtimes can label the reactive graph — in diagnostic `ownerPath`s, attribution chains, and the Performance panel tracks — even after a minifier renames everything. Unset, the option follows `dev`; `true`/`false` sets every kind (`sourceNames: false` opts a dev build out); the object form picks, and each kind it leaves unspecified follows `dev`. The production runtimes ignore the names, and production output (`dev: false`) is byte-identical whether the option is set or not. `@solidjs/compiler`'s `transform()` takes the same option with the same shape and defaults.
136
136
 
137
137
  - `components`: emit the tag as written as a third `createComponent` argument — `<Home />` compiles to `createComponent(Home, props, "Home")`, `<Ui.Button />` to `"Ui.Button"` — so each component's owner reads `<Home>` even when a `lazy()`/HMR wrapper hides the function. Applies to DOM and SSR output; for SSR the compiler keeps the `createComponent` call it otherwise inlines to `Comp(props)`, so the server runtime labels the owner the same way (prod SSR output, without the option, is unchanged). Universal and dynamic output are unaffected.
138
138
  - `bindings`: every compiled binding effect is named by what it writes, as a trailing options argument the dev and observe runtimes read and production ignores. An attribute effect gets `<tag>.<attribute>` as written — `<span textContent={label()} />` compiles to `effect(() => label(), v => …, { name: "span.textContent" })`, `class:active` to `div.class:active`, a `style={{ color: c() }}` property to `div.style:color` — and a template's merged effect lists all of its bindings (`"button.class, span.textContent"`). A hole's insert is named for the parent it fills: `<div>{count()}</div>` compiles to `insert(el, count, undefined, undefined, { name: "div.children" })`; a static child (a component call, a literal) creates no effect and gets no name. A spread passes the tag as its trailing argument (`spread(el, props, false, undefined, "div")`), and the runtime labels its attribute effect `div.spread` and its children insert `div.children`. DOM output only.
139
+ - `locations`: where the JSX is written, beside each label the other two kinds emit, as `file:line:col` — the module path relative to [`root`](#root) with `/` separators, line and column 1-based (the stack-trace convention, so a dev server links `http://localhost:5173/src/App.tsx:18:3` to the right place). A component call gets it as a fourth argument: `<Home />` compiles to `createComponent(Home, props, "Home", "src/App.tsx:12:5")`, the location of `<Home`. A binding gets `loc` after `name` in its options: an attribute effect points at the attribute (`{ name: "div.class", loc: "src/App.tsx:14:7" }`; a merged effect at the first attribute it lists; a property split out of a `style={{…}}` / `class={{…}}` object at that object attribute), a hole's insert at its `{`, and a spread passes it after the tag (`spread(el, props, false, undefined, "div", "src/App.tsx:20:12")`), the first spread attribute's start. A location is only emitted where its label is, and only when Babel knows the module's `filename` (a file outside `root` gets a `../` path). The dev and observe runtimes print it in diagnostic reports; production ignores it.
139
140
 
140
141
  These are the JSX-level kinds — what a JSX compiler can see. Naming the primitives themselves (`createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive) is not a JSX-transform feature and this plugin does not do it: it is `@solidjs/compiler`'s standalone `transformSourceNames` pass, which the build tool runs on every module — `.ts`, `.js`, and JSX files alike — independently of which JSX compiler handles the file. `@solidjs/vite-plugin` (its `solid: { sourceNames }` option, solid-vite-plugin #371) wires both: the JSX-level kinds through this option and primitive names through the standalone pass, for its dev and `observe` postures.
141
142
 
143
+ ### root
144
+
145
+ - Type: `string`
146
+ - Default: the working directory (Babel's `cwd`)
147
+
148
+ Project root that `sourceNames.locations` makes the module path relative to — the build tool passes its project root so the location is a dev-server path. A relative `root` is resolved against the working directory.
149
+
142
150
  ### delegateEvents
143
151
 
144
152
  - Type: `boolean`
@@ -248,13 +256,6 @@ const template = <div>Hello</div>;
248
256
 
249
257
  Inline style attributes in templates when the value is a string or `Record<string, string>`. Disable for strong CSP configurations; styles are then set at runtime.
250
258
 
251
- ### serverComponents
252
-
253
- - Type: `boolean`
254
- - Default: `false`
255
-
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
-
258
259
  ### hoistProps
259
260
 
260
261
  - Type: `boolean`
package/index.js CHANGED
@@ -1654,6 +1654,23 @@ attributes,
1654
1654
 
1655
1655
 
1656
1656
 
1657
+
1658
+
1659
+
1660
+
1661
+
1662
+
1663
+
1664
+
1665
+
1666
+
1667
+
1668
+
1669
+
1670
+
1671
+
1672
+
1673
+
1657
1674
 
1658
1675
 
1659
1676
 
@@ -1706,7 +1723,6 @@ const config = {
1706
1723
  memoWrapper: "memo",
1707
1724
  validate: true,
1708
1725
  inlineStyles: true,
1709
- serverComponents: false,
1710
1726
  hoistProps: true
1711
1727
  };
1712
1728
 
@@ -1717,8 +1733,12 @@ const config = {
1717
1733
  */
1718
1734
  function sourceNames(config) {
1719
1735
  const value = config.sourceNames ?? config.dev;
1720
- if (typeof value === "boolean") return { components: value, bindings: value };
1721
- return { components: value.components ?? config.dev, bindings: value.bindings ?? config.dev };
1736
+ if (typeof value === "boolean") return { components: value, bindings: value, locations: value };
1737
+ return {
1738
+ components: value.components ?? config.dev,
1739
+ bindings: value.bindings ?? config.dev,
1740
+ locations: value.locations ?? config.dev
1741
+ };
1722
1742
  }
1723
1743
 
1724
1744
  /**
@@ -1731,6 +1751,74 @@ function namesBindings(config) {
1731
1751
  return config.generate === "dom" && sourceNames(config).bindings;
1732
1752
  }
1733
1753
 
1754
+ /**
1755
+ * The `sourceNames.locations` string for `node`: `<file>:<line>:<column>`,
1756
+ * the file relative to `config.root` (default: Babel's `cwd`, the working
1757
+ * directory) with posix separators, line and column both 1-based — the
1758
+ * stack-trace / editor convention, so a browser links
1759
+ * `http://localhost:5173/src/App.tsx:18:3` to the right place. Babel's
1760
+ * `loc.start.column` is 0-based, so it is bumped by one here (the refresh
1761
+ * pass's `location` keeps Babel's 0-based column; that is a different
1762
+ * consumer). A file outside `root` yields a path starting with `..`, emitted
1763
+ * as-is. `undefined` when the kind is off, when Babel was given no `filename`
1764
+ * for the module, or when `node` is synthetic and carries no `loc`.
1765
+ *
1766
+ * `@solidjs/compiler` computes the same string with `relative_id` so both
1767
+ * compilers print identical output.
1768
+ */
1769
+ function sourceLocation(path, node) {
1770
+ const config = getConfig(path);
1771
+ if (!sourceNames(config).locations) return;
1772
+ const start = node.loc?.start;
1773
+ if (!start) return;
1774
+ const opts = path.hub.file?.opts;
1775
+ const filename = opts?.filename;
1776
+ if (!filename) return;
1777
+ const cwd = opts?.cwd ?? "";
1778
+ const root = config.root ? resolve(cwd, config.root) : resolve(cwd, cwd);
1779
+ return `${relative(root, resolve(cwd, filename))}:${start.line}:${start.column + 1}`;
1780
+ }
1781
+
1782
+ /**
1783
+ * A binding's `{ name, loc? }` options literal — `name` first, then `loc`
1784
+ * only when there is one, so both JSX compilers print the same text.
1785
+ */
1786
+ function bindingOptions(name, loc) {
1787
+ const properties = [t__namespace.objectProperty(t__namespace.identifier("name"), t__namespace.stringLiteral(name))];
1788
+ if (loc !== undefined)
1789
+ properties.push(t__namespace.objectProperty(t__namespace.identifier("loc"), t__namespace.stringLiteral(loc)));
1790
+ return t__namespace.objectExpression(properties);
1791
+ }
1792
+
1793
+ // Path arithmetic without node's `path` (this plugin bundles for any host):
1794
+ // the same steps as `@solidjs/compiler`'s `relative_id` — resolve both paths
1795
+ // against `cwd`, fold `.`/`..`, drop the common prefix.
1796
+
1797
+ /** `p` resolved against `base` (posix separators, `.`/`..` folded), as segments. */
1798
+ function resolve(base, p) {
1799
+ const posix = p.replace(/\\/g, "/");
1800
+ const absolute = /^(\/|[A-Za-z]:\/)/.test(posix);
1801
+ const joined = absolute ? posix : `${base.replace(/\\/g, "/")}/${posix}`;
1802
+ const segments = [];
1803
+ for (const segment of joined.split("/")) {
1804
+ if (segment === "." || segment === "" && segments.length) continue;
1805
+ if (segment === "..") {
1806
+ // Never past the root marker (`""` for `/…`, `C:` for a drive).
1807
+ if (segments.length > 1) segments.pop();
1808
+ continue;
1809
+ }
1810
+ segments.push(segment);
1811
+ }
1812
+ return segments;
1813
+ }
1814
+
1815
+ /** `file` relative to `root`, both resolved: `..` per root segment left, then the file's rest. */
1816
+ function relative(root, file) {
1817
+ let i = 0;
1818
+ while (i < root.length && i < file.length && root[i] === file[i]) i++;
1819
+ return [...root.slice(i).map(() => ".."), ...file.slice(i)].join("/");
1820
+ }
1821
+
1734
1822
  const InlineElements = [
1735
1823
  "a",
1736
1824
  "abbr",
@@ -2498,6 +2586,8 @@ results)
2498
2586
  t$1.jsxExpressionContainer(p.value)
2499
2587
  );
2500
2588
  newAttr._styleProperty = true;
2589
+ // The split-out property is located at the `style` attribute it came from.
2590
+ newAttr.loc = styleObjectAttribute.node.loc;
2501
2591
  path.
2502
2592
  get("openingElement").
2503
2593
  node.attributes.splice(Number(styleObjectAttribute.key) + ++i, 0, newAttr);
@@ -2548,13 +2638,16 @@ results)
2548
2638
  ))
2549
2639
  {
2550
2640
  classArrayAttribute.node.value = t$1.stringLiteral(staticClasses.join(" "));
2641
+ const dynamicClassAttribute = t$1.jsxAttribute(
2642
+ t$1.jsxIdentifier("class"),
2643
+ t$1.jsxExpressionContainer(dynamicClassElement)
2644
+ );
2645
+ // `sourceNames.locations`: the split-out object locates at the array
2646
+ // attribute it came from.
2647
+ dynamicClassAttribute.loc = classArrayAttribute.node.loc;
2551
2648
  path.
2552
2649
  get("openingElement").
2553
- node.attributes.splice(
2554
- Number(classArrayAttribute.key) + 1,
2555
- 0,
2556
- t$1.jsxAttribute(t$1.jsxIdentifier("class"), t$1.jsxExpressionContainer(dynamicClassElement))
2557
- );
2650
+ node.attributes.splice(Number(classArrayAttribute.key) + 1, 0, dynamicClassAttribute);
2558
2651
  }
2559
2652
  }
2560
2653
 
@@ -2600,6 +2693,8 @@ results)
2600
2693
  t$1.jsxExpressionContainer(p.value)
2601
2694
  );
2602
2695
  newAttr._classProperty = true;
2696
+ // The split-out property is located at the `class` attribute it came from.
2697
+ newAttr.loc = classListAttribute.node.loc;
2603
2698
  path.
2604
2699
  get("openingElement").
2605
2700
  node.attributes.splice(Number(classListAttribute.key) + ++i, 0, newAttr);
@@ -3008,7 +3103,11 @@ results)
3008
3103
  tagName,
3009
3104
  checkableInput,
3010
3105
  styleProperty: isStyleProperty,
3011
- classProperty: isClassProperty
3106
+ classProperty: isClassProperty,
3107
+ // `sourceNames.locations`: the attribute's start (for a property
3108
+ // split out of a `style={{…}}` / `class={{…}}` object, the
3109
+ // object attribute's start — see the pre-passes above).
3110
+ loc: sourceLocation(attribute, attribute.node)
3012
3111
  });
3013
3112
  } else {
3014
3113
  results.exprs.push(
@@ -3169,6 +3268,10 @@ config)
3169
3268
  if (!transformed) return memo;
3170
3269
  transformed.allocatesIds = allocatesIds;
3171
3270
  transformed.functionHole = functionHole;
3271
+ // `sourceNames.locations`: where the hole is written, for its insert
3272
+ // (a spread child `{...items()}` is a hole too).
3273
+ if (child.isJSXExpressionContainer() || child.isJSXSpreadChild())
3274
+ transformed.loc = sourceLocation(child, child.node);
3172
3275
  const i = memo.length;
3173
3276
  if (transformed.text && i && memo[i - 1].text) {
3174
3277
  memo[i - 1].template =
@@ -3252,14 +3355,11 @@ config)
3252
3355
  // is inserted directly and creates no effect to name.
3253
3356
  const accessorHole =
3254
3357
  child.dynamic || child.functionHole;
3358
+ // `sourceNames.locations`: `loc` after `name`, the hole's `{` start.
3255
3359
  const emitInsert = (args) => {
3256
3360
  if (accessorHole && namesBindings(getConfig(path))) {
3257
3361
  while (args.length < 4) args.push(t$1.identifier("undefined"));
3258
- args.push(
3259
- t$1.objectExpression([
3260
- t$1.objectProperty(t$1.identifier("name"), t$1.stringLiteral(`${tagName}.children`))]
3261
- )
3262
- );
3362
+ args.push(bindingOptions(`${tagName}.children`, child.loc));
3263
3363
  }
3264
3364
  results.exprs.push(t$1.expressionStatement(t$1.callExpression(insert, args)));
3265
3365
  };
@@ -3559,7 +3659,14 @@ attributes,
3559
3659
  // `sourceNames.bindings`: the tag as written rides as spread's trailing
3560
3660
  // argument (past the runtime-only `skip` slot); the runtime labels its
3561
3661
  // attribute effect `<tag>.spread` and its children insert `<tag>.children`.
3562
- if (namesBindings(config)) args.push(t$1.identifier("undefined"), t$1.stringLiteral(tagName));
3662
+ // `sourceNames.locations`: the first spread attribute's start (`{...`)
3663
+ // follows the name as one more string argument.
3664
+ if (namesBindings(config)) {
3665
+ args.push(t$1.identifier("undefined"), t$1.stringLiteral(tagName));
3666
+ const spread = attributes.find((a) => t$1.isJSXSpreadAttribute(a.node));
3667
+ const loc = spread && sourceLocation(spread, spread.node);
3668
+ if (loc !== undefined) args.push(t$1.stringLiteral(loc));
3669
+ }
3563
3670
 
3564
3671
  return [
3565
3672
  filteredAttributes,
@@ -3775,20 +3882,18 @@ ownerId)
3775
3882
  // binding's `<tag>.<attribute>` as written (undoing the `prop:` the locked
3776
3883
  // DOM property pre-pass added to `value`, `checked`, …), the merged effect
3777
3884
  // listing all of its bindings — as a trailing `{ name }` options argument.
3885
+ // `sourceNames.locations` adds `loc` after it: the first binding's
3886
+ // attribute start (a merged effect points at the first attribute it lists).
3778
3887
  const label = namesBindings(config) ?
3779
3888
  [
3780
- t__namespace.objectExpression([
3781
- t__namespace.objectProperty(
3782
- t__namespace.identifier("name"),
3783
- t__namespace.stringLiteral(
3784
- dynamics.
3785
- map(
3786
- (d) =>
3787
- `${d.tagName}.${isLockedDOMProperty(d.tagName, d.key) ? d.key.replace(/^prop:/, "") : d.key}`
3788
- ).
3789
- join(", ")
3790
- )
3791
- )]
3889
+ bindingOptions(
3890
+ dynamics.
3891
+ map(
3892
+ (d) =>
3893
+ `${d.tagName}.${isLockedDOMProperty(d.tagName, d.key) ? d.key.replace(/^prop:/, "") : d.key}`
3894
+ ).
3895
+ join(", "),
3896
+ dynamics.find((d) => d.loc !== undefined)?.loc
3792
3897
  )] :
3793
3898
 
3794
3899
  [];
@@ -5159,13 +5264,6 @@ info)
5159
5264
  // holes evaluated. The hole never takes a hydration id.
5160
5265
  const linkEntries =
5161
5266
  tagName === "a" && isLinkClaimCandidate(attributes) ? [] : null;
5162
- // Server-components handler positions: ref/on* expressions on
5163
- // server-rendered intrinsics collect here and emit as one guarded
5164
- // whole-attribute hole after the loop, where `ssrClaim` turns attribute-slot
5165
- // reads into `_s:on:*` / `_s:ref` markers (and drops server-local
5166
- // functions). Evaluation is gated on the render context's claims flag so
5167
- // plain SSR never runs the expressions.
5168
- const claims = [];
5169
5267
 
5170
5268
  attributes.forEach((attribute) => {
5171
5269
  if (!t.isJSXAttribute(attribute.node)) return;
@@ -5196,23 +5294,13 @@ info)
5196
5294
  {
5197
5295
  if (t.isJSXEmptyExpression(value.expression)) return;
5198
5296
  if (key === "ref") {
5199
- if (info.serverComponents) {
5200
- claims.push(["ref", value.expression]);
5201
- return;
5202
- }
5203
5297
  results.declarations.push(
5204
5298
  t.variableDeclarator(path.scope.generateUidIdentifier("_ref$"), value.expression)
5205
5299
  );
5206
5300
  return;
5207
5301
  }
5208
5302
  if (key.startsWith("prop:")) return;
5209
- if (isEventName(key)) {
5210
- // `onXxx` lowercases to the event name — the client runtime's own
5211
- // derivation (`onClick` -> `click`); the position is bound under it.
5212
- if (info.serverComponents)
5213
- claims.push([toEventName(key), value.expression]);
5214
- return;
5215
- }
5303
+ if (isEventName(key)) return;
5216
5304
  if (ChildProperties.has(key)) {
5217
5305
  if (key === "children" && VoidElements.has(tagName)) return;
5218
5306
  if (info.hydratable && key === "textContent" && value && value.expression) {
@@ -5260,30 +5348,9 @@ info)
5260
5348
  value.expression = t.assignmentExpression("=", temp, expression);
5261
5349
  linkEntries.push([key, t.cloneNode(temp), !!isDynamicValue]);
5262
5350
  }
5263
- // Server components (principles §9.2.3) need a whole-attribute
5264
- // serializer that can emit a position marker for a binding-slot
5265
- // value. The ordinary class/style serializers cannot bind slots.
5266
- // Object literals stay objects (no inlining) so a slot used as a
5267
- // property's value can also emit its marker.
5268
- if (info.serverComponents && (key === "class" || key === "style")) {
5269
- const attr = t.callExpression(registerImportMethod(path, "ssrElementAttribute"), [
5270
- t.stringLiteral(key),
5271
- value.expression]
5272
- );
5273
- results.template.push("");
5274
- results.templateValues.push(
5275
- isDynamicValue ?
5276
- hoistExpression(path, results, t.arrowFunctionExpression([], attr), {
5277
- group: true
5278
- }) :
5279
- attr
5280
- );
5281
- return;
5282
- }
5283
5351
  if ((key === "class" || key === "style") && !t.isObjectExpression(value.expression)) {
5284
- // The whole attribute must be absent for nullish values. Keep the
5285
- // ordinary serializers: ssrElementAttribute would also bind slots
5286
- // without serverComponents. Capture the value once before testing it.
5352
+ // The whole attribute must be absent for nullish values. Capture the
5353
+ // value once before testing it.
5287
5354
  const id = path.scope.generateUidIdentifier("v$");
5288
5355
  const attr = t.callExpression(
5289
5356
  t.arrowFunctionExpression(
@@ -5492,28 +5559,6 @@ info)
5492
5559
  results.template.push("");
5493
5560
  results.templateValues.push(hole);
5494
5561
  }
5495
- if (claims.length) {
5496
- const map = claimMap(claims);
5497
- // `_$sharedConfig.context && _$sharedConfig.context.claims
5498
- // ? _$ssrClaim({...}) : ""`
5499
- // — the claims flag is only set inside a server component's render
5500
- // scope (and cleared inside suppressed fill windows), so ordinary SSR
5501
- // pays the property reads and never evaluates the expressions.
5502
- const sharedConfigId = registerImportMethod(path, "sharedConfig");
5503
- const contextRead = () =>
5504
- t.memberExpression(t.cloneNode(sharedConfigId), t.identifier("context"));
5505
- const guarded = t.conditionalExpression(
5506
- t.logicalExpression(
5507
- "&&",
5508
- contextRead(),
5509
- t.memberExpression(contextRead(), t.identifier("claims"))
5510
- ),
5511
- t.callExpression(registerImportMethod(path, "ssrClaim"), [map]),
5512
- t.stringLiteral("")
5513
- );
5514
- results.template.push("");
5515
- results.templateValues.push(hoistExpression(path, results, guarded));
5516
- }
5517
5562
  if (!hasChildren && children) {
5518
5563
  path.node.children.push(children);
5519
5564
  }
@@ -5639,7 +5684,7 @@ results,
5639
5684
 
5640
5685
  function createElement(
5641
5686
  path,
5642
- { topLevel, hydratable, serverComponents })
5687
+ { topLevel, hydratable })
5643
5688
  {
5644
5689
  const tagName = getTagName(path.node),
5645
5690
  config = getConfig(path),
@@ -5692,22 +5737,6 @@ path,
5692
5737
  const node = attribute.node;
5693
5738
  return !(t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "ref");
5694
5739
  });
5695
- // Server components (principles §9.2.3): the named `ref`/`on*` attributes
5696
- // of a spread element are handler positions like a template element's,
5697
- // and compile to the same claim map — `{ click: expr, ref: [a, b] }`,
5698
- // duplicate refs merged, a duplicate handler last-wins as the template
5699
- // path strips it — keyed by the index of the source each attribute sits
5700
- // before (`<b {...a} onClick={go} {...b}>` → `{ 1: { click: go } }`, the
5701
- // spreads being sources 0 and 2), and handed to `ssrElement` as a thunk
5702
- // it reads only inside a server component's render (the gate the template
5703
- // path's `ssrClaim` guard reads), so plain SSR never evaluates the
5704
- // expressions. The runtime settles each handler position in source order,
5705
- // as the client's `spread(el, [a, { onClick: go }, b])` does — a spread at
5706
- // that index or later that HAS the key owns it — and merges every ref.
5707
- // Plain SSR output is unchanged (dropped, as a server element has no
5708
- // handlers to run).
5709
- const claims = [];
5710
-
5711
5740
  let props;
5712
5741
  // Attributes written AFTER the last spread are markup, not a source: no
5713
5742
  // later source can override them, so they go to `ssrElement` as its
@@ -5727,13 +5756,26 @@ path,
5727
5756
  // keys.
5728
5757
  const tail = [];
5729
5758
  const skipKeys = [];
5730
- if (
5731
- propAttributes.length === 1 &&
5732
- t.isJSXSpreadAttribute(propAttributes[0].node) &&
5733
- // A `ref` beside the lone spread is a claim under `serverComponents`;
5734
- // the loop below places it.
5735
- !(serverComponents && attributes.length > 1))
5736
- {
5759
+ // Plain SSR evaluates a named `ref` expression without calling the ref, as
5760
+ // the template path does (`_ref$ = expr`), so a ref factory's owned setup
5761
+ // (`ref={tooltip()}`) runs on both sides and the hydration ids after it
5762
+ // line up (#4081). The client evaluates it after applying the spread and
5763
+ // before the children; `ssrElement` calls the `attrs` thunk at that point.
5764
+ // Only an expression that calls something can run setup: `ref={el}` and
5765
+ // `ref={props.ref}` stay out of the thunk.
5766
+ const refs = [];
5767
+ for (const attribute of attributes) {
5768
+ const node = attribute.node;
5769
+ if (
5770
+ t.isJSXAttribute(node) &&
5771
+ t.isJSXIdentifier(node.name, { name: "ref" }) &&
5772
+ t.isJSXExpressionContainer(node.value) &&
5773
+ !t.isJSXEmptyExpression(node.value.expression) &&
5774
+ callsOutsideFunctions(node.value.expression))
5775
+
5776
+ refs.push(node.value.expression);
5777
+ }
5778
+ if (propAttributes.length === 1 && t.isJSXSpreadAttribute(propAttributes[0].node)) {
5737
5779
  props = [propAttributes[0].node.argument];
5738
5780
  } else {
5739
5781
  props = [];
@@ -5779,28 +5821,7 @@ path,
5779
5821
  node.name.name;
5780
5822
 
5781
5823
  if (hasChildren && key === "children") return;
5782
- if (key === "ref" || isEventName(key)) {
5783
- if (serverComponents && t.isJSXExpressionContainer(value)) {
5784
- const expression = value.expression;
5785
- const pos = key === "ref" ? "ref" : toEventName(key);
5786
- if (
5787
- !(
5788
- t.isJSXEmptyExpression(expression) ||
5789
- t.isStringLiteral(expression) ||
5790
- t.isNumericLiteral(expression) ||
5791
- t.isBooleanLiteral(expression)))
5792
-
5793
- {
5794
- // The index of the source this attribute sits before: the
5795
- // sources pushed so far, plus the running literal if it will
5796
- // be pushed ahead of the next spread. The literal never
5797
- // carries a handler key, so a spread's index is either below
5798
- // this or at/after it — never ambiguous.
5799
- claims.push([props.length + (runningObject.length ? 1 : 0), pos, expression]);
5800
- }
5801
- }
5802
- return;
5803
- }
5824
+ if (key === "ref" || isEventName(key)) return;
5804
5825
  if (key.startsWith("prop:")) return;
5805
5826
  // An anchor's link-relevant attributes stay a source, never baked
5806
5827
  // markup: `ssrElement` collects them from the walk for the render's
@@ -5869,76 +5890,52 @@ path,
5869
5890
  // set) and attribute string — a literal when every part is static, else a
5870
5891
  // thunk concatenating the parts in source order. A `false` static bakes to
5871
5892
  // no markup but still skips its key.
5872
- if (skipKeys.length) {
5893
+ if (skipKeys.length || refs.length) {
5873
5894
  let markup;
5874
- if (tail.every((part) => typeof part === "string")) markup = t.stringLiteral(tail.join(""));else
5875
- {
5895
+ if (tail.every((part) => typeof part === "string")) {
5896
+ markup = t.stringLiteral(tail.join(""));
5897
+ // The refs run after the tail's (static) markup and return it.
5898
+ if (refs.length)
5899
+ markup = t.arrowFunctionExpression([], t.sequenceExpression([...refs, markup]));
5900
+ } else {
5876
5901
  let concat;
5877
5902
  for (const part of tail) {
5878
5903
  const expr = typeof part === "string" ? t.stringLiteral(part) : part;
5879
5904
  concat = concat ? t.binaryExpression("+", concat, expr) : expr;
5880
5905
  }
5906
+ // The tail's getters run first, as the client applies them with the
5907
+ // spread before it evaluates the ref: `markup + (ref, "")`.
5908
+ if (refs.length)
5909
+ concat = t.binaryExpression(
5910
+ "+",
5911
+ concat,
5912
+ t.sequenceExpression([...refs, t.stringLiteral("")])
5913
+ );
5881
5914
  markup = t.arrowFunctionExpression([], concat);
5882
5915
  }
5883
- args.push(registerSkip(path, skipKeys), markup);
5884
- }
5885
- if (claims.length) {
5886
- if (!skipKeys.length) args.push(t.identifier("undefined"), t.identifier("undefined"));
5887
- args.push(t.arrowFunctionExpression([], claimSegments(claims)));
5916
+ args.push(skipKeys.length ? registerSkip(path, skipKeys) : t.identifier("undefined"), markup);
5888
5917
  }
5889
5918
  const exprs = [t.callExpression(registerImportMethod(path, "ssrElement"), args)];
5890
5919
  return { exprs, template: "", declarations: [], dynamics: [], spreadElement: true };
5891
5920
  }
5892
5921
 
5893
- /**
5894
- * A spread element's claim map by source index (`createElement`): `{ 1: {
5895
- * click: go }, 3: { ref: el } }`. A handler position keeps its LAST named
5896
- * attribute only — the template path's duplicate strip, applied here to the
5897
- * claims (a later attribute wins whatever sits between, so the earlier one
5898
- * is never read on either side); refs merge within a segment as `claimMap`
5899
- * merges them, and across segments at the runtime.
5900
- */
5901
- function claimSegments(
5902
- claims)
5903
- {
5904
- const lastHandler = new Map();
5905
- claims.forEach(([, pos], i) => {
5906
- if (pos !== "ref") lastHandler.set(pos, i);
5907
- });
5908
- const segments = new Map();
5909
- claims.forEach(([index, pos, expr], i) => {
5910
- if (pos !== "ref" && lastHandler.get(pos) !== i) return;
5911
- let list = segments.get(index);
5912
- if (!list) segments.set(index, list = []);
5913
- list.push([pos, expr]);
5914
- });
5915
- return t.objectExpression(
5916
- [...segments].map(([index, list]) => t.objectProperty(t.numericLiteral(index), claimMap(list)))
5917
- );
5918
- }
5922
+ /** Does evaluating `node` call anything? A function's body does not run. */
5923
+ function callsOutsideFunctions(node) {
5924
+ if (
5925
+ t.isCallExpression(node) ||
5926
+ t.isOptionalCallExpression(node) ||
5927
+ t.isNewExpression(node) ||
5928
+ t.isTaggedTemplateExpression(node))
5919
5929
 
5920
- /**
5921
- * The compiled claim map of an element's handler positions, `{ click: expr,
5922
- * ref: [a, b] }`: duplicate event keys were last-wins-stripped by the
5923
- * template path's duplicate strip (or `claimSegments`); `ref` is exempt from
5924
- * that pass (client semantics fire every ref), so multiple refs merge into
5925
- * an array value.
5926
- */
5927
- function claimMap(claims) {
5928
- const byPos = new Map();
5929
- for (const [pos, expr] of claims) {
5930
- let list = byPos.get(pos);
5931
- if (!list) byPos.set(pos, list = []);
5932
- list.push(expr);
5933
- }
5934
- return t.objectExpression(
5935
- [...byPos].map(([pos, exprs]) =>
5936
- t.objectProperty(
5937
- t.stringLiteral(pos),
5938
- exprs.length === 1 ? exprs[0] : t.arrayExpression(exprs)
5939
- )
5940
- )
5941
- );
5930
+ return true;
5931
+ if (t.isFunction(node)) return false;
5932
+ for (const key of t.VISITOR_KEYS[node.type] ?? []) {
5933
+ const child = node[key];
5934
+ if (Array.isArray(child)) {
5935
+ for (const c of child) if (c && callsOutsideFunctions(c)) return true;
5936
+ } else if (child && typeof child.type === "string" && callsOutsideFunctions(child)) return true;
5937
+ }
5938
+ return false;
5942
5939
  }
5943
5940
 
5944
5941
  /**
@@ -6451,6 +6448,10 @@ path)
6451
6448
  sourceNames(config).components && (config.generate === "dom" || config.generate === "ssr");
6452
6449
  if (labelled) {
6453
6450
  componentArgs.push(t__namespace.stringLiteral(tagName));
6451
+ // `sourceNames.locations`: the opening element's start (`<Home`) as a
6452
+ // fourth argument, `file:line:col`, so a diagnostic can point at the JSX.
6453
+ const loc = sourceLocation(path, path.node.openingElement);
6454
+ if (loc !== undefined) componentArgs.push(t__namespace.stringLiteral(loc));
6454
6455
  }
6455
6456
  // SSR's prod `createComponent` is literally `Comp(props || {})`. Since the
6456
6457
  // compiler always emits a real `props[0]` object expression above (see the
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.15",
4
+ "version": "2.0.0-rc.16",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {