@solidjs/babel-plugin 2.0.0-rc.15 → 2.0.0-rc.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.
@@ -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 2.3s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @solidjs/babel-plugin
2
2
 
3
+ ## 2.0.0-rc.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 3e72783: The removed Solid 1.x attribute forms are now compile errors on native elements in the `dom` and `ssr` generates (#4187). `on:`, `oncapture:`, `attr:`, `bool:`, `use:` and `classList` used to compile silently into literal attributes (`<div use:tip="() => {}">`, `classlist="[object Object]"`). Both compilers now reject them with the same message, which names the 2.0 replacement: a camelCase handler or a `ref` callback for `on:`, a plain attribute for `attr:`, a boolean value for `bool:`, a `ref` directive factory for `use:`, and an object or array `class` for `classList`. Components, the universal generate, `prop:` and the XML namespaces (`xlink:`, `xml:`, `xmlns:`) are unaffected. The check runs only at compile time and adds no runtime code.
8
+ - 6cca8b5: Dev builds now warn with `UNCOMMITTED_INPUT_OVERWRITTEN` (#4186) when a stateful DOM property binding (`value`/`checked` on an input, `value` on a textarea, `value`/`selected` on an option, `muted` on media) is about to re-write the value it wrote last time over an edit the user never committed to the bound signal — e.g. `value={props.name} onChange={commit}` with no draft signal, when another dynamic attribute in the same template changes. Runtime behavior is unchanged: the write still happens, once per element and property the warning names the fix (write the signal from `onInput`, use `defaultValue`/`defaultChecked` for an uncontrolled element, or write a rejected edit back with `e.currentTarget.value = next`).
9
+
10
+ Production output is unchanged byte for byte. In dev DOM output both compilers write stateful properties through a new `setStatefulProperty(el, name, value)` helper instead of the inline `el.name = value`; hydratable output keeps `setProperty`, and `spread`/`assign` check in the runtime. The check lives only in `@solidjs/web`'s dev build; the production entry carries a plain `node[name] = value` stub that no production-compiled module imports, so it tree-shakes away.
11
+
12
+ The "Form inputs" section of the DOM RFC, the cheatsheet and the solid-2.0 skill now say that a change to any other dynamic attribute in the same template re-runs a stateful binding, link the new code, and drop `equals: false` as a way to reject input (it is what the warning reports); write the accepted value back with `e.currentTarget.value = next` instead.
13
+
14
+ - 2ba1c4a: A valueless `prop:` attribute (`<el prop:active />`) now compiles as `prop:active={true}`, matching bare plain attributes, instead of being dropped silently (#4180).
15
+
16
+ ## 2.0.0-rc.16
17
+
18
+ ### Patch Changes
19
+
20
+ - 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.
21
+ - 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.
22
+ - 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.
23
+
3
24
  ## 2.0.0-rc.15
4
25
 
5
26
  ### 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",
@@ -2233,11 +2321,6 @@ value,
2233
2321
  );
2234
2322
  }
2235
2323
 
2236
- const assignment = t$1.assignmentExpression(
2237
- "=",
2238
- t$1.memberExpression(elem, t$1.identifier(name)),
2239
- value
2240
- );
2241
2324
  if (
2242
2325
  (name === "value" || name === "defaultValue") && (
2243
2326
  tagName === "input" || tagName === "textarea") &&
@@ -2246,13 +2329,20 @@ value,
2246
2329
  {
2247
2330
  // prevents undefined on text input/textarea.value/defaultValue, fallback
2248
2331
  // to empty string (#2957)
2249
- return t$1.assignmentExpression(
2250
- "=",
2251
- t$1.memberExpression(elem, t$1.identifier(name)),
2252
- t$1.logicalExpression("??", value, t$1.stringLiteral(""))
2332
+ value = t$1.logicalExpression("??", value, t$1.stringLiteral(""));
2333
+ }
2334
+ // Dev DOM output writes stateful properties (value/checked/selected/
2335
+ // muted) through `setStatefulProperty` — the same `elem[name] = value`,
2336
+ // preceded in the dev runtime by the UNCOMMITTED_INPUT_OVERWRITTEN check
2337
+ // (#4186). Production output keeps the inline assignment, byte for byte.
2338
+ if (config.dev && config.generate === "dom" && isStatefulDOMProperty(tagName, name)) {
2339
+ return t$1.callExpression(registerImportMethod(path, "setStatefulProperty"), [
2340
+ elem,
2341
+ t$1.stringLiteral(name),
2342
+ value]
2253
2343
  );
2254
2344
  }
2255
- return assignment;
2345
+ return t$1.assignmentExpression("=", t$1.memberExpression(elem, t$1.identifier(name)), value);
2256
2346
  }
2257
2347
 
2258
2348
  const ns = name.indexOf(":") > -1 && Namespaces[name.split(":")[0]];
@@ -2498,6 +2588,8 @@ results)
2498
2588
  t$1.jsxExpressionContainer(p.value)
2499
2589
  );
2500
2590
  newAttr._styleProperty = true;
2591
+ // The split-out property is located at the `style` attribute it came from.
2592
+ newAttr.loc = styleObjectAttribute.node.loc;
2501
2593
  path.
2502
2594
  get("openingElement").
2503
2595
  node.attributes.splice(Number(styleObjectAttribute.key) + ++i, 0, newAttr);
@@ -2548,13 +2640,16 @@ results)
2548
2640
  ))
2549
2641
  {
2550
2642
  classArrayAttribute.node.value = t$1.stringLiteral(staticClasses.join(" "));
2643
+ const dynamicClassAttribute = t$1.jsxAttribute(
2644
+ t$1.jsxIdentifier("class"),
2645
+ t$1.jsxExpressionContainer(dynamicClassElement)
2646
+ );
2647
+ // `sourceNames.locations`: the split-out object locates at the array
2648
+ // attribute it came from.
2649
+ dynamicClassAttribute.loc = classArrayAttribute.node.loc;
2551
2650
  path.
2552
2651
  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
- );
2652
+ node.attributes.splice(Number(classArrayAttribute.key) + 1, 0, dynamicClassAttribute);
2558
2653
  }
2559
2654
  }
2560
2655
 
@@ -2600,6 +2695,8 @@ results)
2600
2695
  t$1.jsxExpressionContainer(p.value)
2601
2696
  );
2602
2697
  newAttr._classProperty = true;
2698
+ // The split-out property is located at the `class` attribute it came from.
2699
+ newAttr.loc = classListAttribute.node.loc;
2603
2700
  path.
2604
2701
  get("openingElement").
2605
2702
  node.attributes.splice(Number(classListAttribute.key) + ++i, 0, newAttr);
@@ -2782,7 +2879,9 @@ results)
2782
2879
  reservedNameSpace &&
2783
2880
  !t$1.isJSXExpressionContainer(value))
2784
2881
  {
2785
- node.value = value = t$1.jsxExpressionContainer(value || t$1.jsxEmptyExpression());
2882
+ // A valueless `prop:x` means `prop:x={true}`, like a bare plain
2883
+ // attribute (#4180).
2884
+ node.value = value = t$1.jsxExpressionContainer(value || t$1.booleanLiteral(true));
2786
2885
  }
2787
2886
  if (
2788
2887
  t$1.isJSXExpressionContainer(value) && (
@@ -3008,7 +3107,11 @@ results)
3008
3107
  tagName,
3009
3108
  checkableInput,
3010
3109
  styleProperty: isStyleProperty,
3011
- classProperty: isClassProperty
3110
+ classProperty: isClassProperty,
3111
+ // `sourceNames.locations`: the attribute's start (for a property
3112
+ // split out of a `style={{…}}` / `class={{…}}` object, the
3113
+ // object attribute's start — see the pre-passes above).
3114
+ loc: sourceLocation(attribute, attribute.node)
3012
3115
  });
3013
3116
  } else {
3014
3117
  results.exprs.push(
@@ -3169,6 +3272,10 @@ config)
3169
3272
  if (!transformed) return memo;
3170
3273
  transformed.allocatesIds = allocatesIds;
3171
3274
  transformed.functionHole = functionHole;
3275
+ // `sourceNames.locations`: where the hole is written, for its insert
3276
+ // (a spread child `{...items()}` is a hole too).
3277
+ if (child.isJSXExpressionContainer() || child.isJSXSpreadChild())
3278
+ transformed.loc = sourceLocation(child, child.node);
3172
3279
  const i = memo.length;
3173
3280
  if (transformed.text && i && memo[i - 1].text) {
3174
3281
  memo[i - 1].template =
@@ -3252,14 +3359,11 @@ config)
3252
3359
  // is inserted directly and creates no effect to name.
3253
3360
  const accessorHole =
3254
3361
  child.dynamic || child.functionHole;
3362
+ // `sourceNames.locations`: `loc` after `name`, the hole's `{` start.
3255
3363
  const emitInsert = (args) => {
3256
3364
  if (accessorHole && namesBindings(getConfig(path))) {
3257
3365
  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
- );
3366
+ args.push(bindingOptions(`${tagName}.children`, child.loc));
3263
3367
  }
3264
3368
  results.exprs.push(t$1.expressionStatement(t$1.callExpression(insert, args)));
3265
3369
  };
@@ -3559,7 +3663,14 @@ attributes,
3559
3663
  // `sourceNames.bindings`: the tag as written rides as spread's trailing
3560
3664
  // argument (past the runtime-only `skip` slot); the runtime labels its
3561
3665
  // attribute effect `<tag>.spread` and its children insert `<tag>.children`.
3562
- if (namesBindings(config)) args.push(t$1.identifier("undefined"), t$1.stringLiteral(tagName));
3666
+ // `sourceNames.locations`: the first spread attribute's start (`{...`)
3667
+ // follows the name as one more string argument.
3668
+ if (namesBindings(config)) {
3669
+ args.push(t$1.identifier("undefined"), t$1.stringLiteral(tagName));
3670
+ const spread = attributes.find((a) => t$1.isJSXSpreadAttribute(a.node));
3671
+ const loc = spread && sourceLocation(spread, spread.node);
3672
+ if (loc !== undefined) args.push(t$1.stringLiteral(loc));
3673
+ }
3563
3674
 
3564
3675
  return [
3565
3676
  filteredAttributes,
@@ -3775,20 +3886,18 @@ ownerId)
3775
3886
  // binding's `<tag>.<attribute>` as written (undoing the `prop:` the locked
3776
3887
  // DOM property pre-pass added to `value`, `checked`, …), the merged effect
3777
3888
  // listing all of its bindings — as a trailing `{ name }` options argument.
3889
+ // `sourceNames.locations` adds `loc` after it: the first binding's
3890
+ // attribute start (a merged effect points at the first attribute it lists).
3778
3891
  const label = namesBindings(config) ?
3779
3892
  [
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
- )]
3893
+ bindingOptions(
3894
+ dynamics.
3895
+ map(
3896
+ (d) =>
3897
+ `${d.tagName}.${isLockedDOMProperty(d.tagName, d.key) ? d.key.replace(/^prop:/, "") : d.key}`
3898
+ ).
3899
+ join(", "),
3900
+ dynamics.find((d) => d.loc !== undefined)?.loc
3792
3901
  )] :
3793
3902
 
3794
3903
  [];
@@ -5159,13 +5268,6 @@ info)
5159
5268
  // holes evaluated. The hole never takes a hydration id.
5160
5269
  const linkEntries =
5161
5270
  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
5271
 
5170
5272
  attributes.forEach((attribute) => {
5171
5273
  if (!t.isJSXAttribute(attribute.node)) return;
@@ -5196,23 +5298,13 @@ info)
5196
5298
  {
5197
5299
  if (t.isJSXEmptyExpression(value.expression)) return;
5198
5300
  if (key === "ref") {
5199
- if (info.serverComponents) {
5200
- claims.push(["ref", value.expression]);
5201
- return;
5202
- }
5203
5301
  results.declarations.push(
5204
5302
  t.variableDeclarator(path.scope.generateUidIdentifier("_ref$"), value.expression)
5205
5303
  );
5206
5304
  return;
5207
5305
  }
5208
5306
  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
- }
5307
+ if (isEventName(key)) return;
5216
5308
  if (ChildProperties.has(key)) {
5217
5309
  if (key === "children" && VoidElements.has(tagName)) return;
5218
5310
  if (info.hydratable && key === "textContent" && value && value.expression) {
@@ -5260,30 +5352,9 @@ info)
5260
5352
  value.expression = t.assignmentExpression("=", temp, expression);
5261
5353
  linkEntries.push([key, t.cloneNode(temp), !!isDynamicValue]);
5262
5354
  }
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
5355
  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.
5356
+ // The whole attribute must be absent for nullish values. Capture the
5357
+ // value once before testing it.
5287
5358
  const id = path.scope.generateUidIdentifier("v$");
5288
5359
  const attr = t.callExpression(
5289
5360
  t.arrowFunctionExpression(
@@ -5492,28 +5563,6 @@ info)
5492
5563
  results.template.push("");
5493
5564
  results.templateValues.push(hole);
5494
5565
  }
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
5566
  if (!hasChildren && children) {
5518
5567
  path.node.children.push(children);
5519
5568
  }
@@ -5639,7 +5688,7 @@ results,
5639
5688
 
5640
5689
  function createElement(
5641
5690
  path,
5642
- { topLevel, hydratable, serverComponents })
5691
+ { topLevel, hydratable })
5643
5692
  {
5644
5693
  const tagName = getTagName(path.node),
5645
5694
  config = getConfig(path),
@@ -5692,22 +5741,6 @@ path,
5692
5741
  const node = attribute.node;
5693
5742
  return !(t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "ref");
5694
5743
  });
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
5744
  let props;
5712
5745
  // Attributes written AFTER the last spread are markup, not a source: no
5713
5746
  // later source can override them, so they go to `ssrElement` as its
@@ -5727,13 +5760,26 @@ path,
5727
5760
  // keys.
5728
5761
  const tail = [];
5729
5762
  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
- {
5763
+ // Plain SSR evaluates a named `ref` expression without calling the ref, as
5764
+ // the template path does (`_ref$ = expr`), so a ref factory's owned setup
5765
+ // (`ref={tooltip()}`) runs on both sides and the hydration ids after it
5766
+ // line up (#4081). The client evaluates it after applying the spread and
5767
+ // before the children; `ssrElement` calls the `attrs` thunk at that point.
5768
+ // Only an expression that calls something can run setup: `ref={el}` and
5769
+ // `ref={props.ref}` stay out of the thunk.
5770
+ const refs = [];
5771
+ for (const attribute of attributes) {
5772
+ const node = attribute.node;
5773
+ if (
5774
+ t.isJSXAttribute(node) &&
5775
+ t.isJSXIdentifier(node.name, { name: "ref" }) &&
5776
+ t.isJSXExpressionContainer(node.value) &&
5777
+ !t.isJSXEmptyExpression(node.value.expression) &&
5778
+ callsOutsideFunctions(node.value.expression))
5779
+
5780
+ refs.push(node.value.expression);
5781
+ }
5782
+ if (propAttributes.length === 1 && t.isJSXSpreadAttribute(propAttributes[0].node)) {
5737
5783
  props = [propAttributes[0].node.argument];
5738
5784
  } else {
5739
5785
  props = [];
@@ -5779,28 +5825,7 @@ path,
5779
5825
  node.name.name;
5780
5826
 
5781
5827
  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
- }
5828
+ if (key === "ref" || isEventName(key)) return;
5804
5829
  if (key.startsWith("prop:")) return;
5805
5830
  // An anchor's link-relevant attributes stay a source, never baked
5806
5831
  // markup: `ssrElement` collects them from the walk for the render's
@@ -5869,76 +5894,52 @@ path,
5869
5894
  // set) and attribute string — a literal when every part is static, else a
5870
5895
  // thunk concatenating the parts in source order. A `false` static bakes to
5871
5896
  // no markup but still skips its key.
5872
- if (skipKeys.length) {
5897
+ if (skipKeys.length || refs.length) {
5873
5898
  let markup;
5874
- if (tail.every((part) => typeof part === "string")) markup = t.stringLiteral(tail.join(""));else
5875
- {
5899
+ if (tail.every((part) => typeof part === "string")) {
5900
+ markup = t.stringLiteral(tail.join(""));
5901
+ // The refs run after the tail's (static) markup and return it.
5902
+ if (refs.length)
5903
+ markup = t.arrowFunctionExpression([], t.sequenceExpression([...refs, markup]));
5904
+ } else {
5876
5905
  let concat;
5877
5906
  for (const part of tail) {
5878
5907
  const expr = typeof part === "string" ? t.stringLiteral(part) : part;
5879
5908
  concat = concat ? t.binaryExpression("+", concat, expr) : expr;
5880
5909
  }
5910
+ // The tail's getters run first, as the client applies them with the
5911
+ // spread before it evaluates the ref: `markup + (ref, "")`.
5912
+ if (refs.length)
5913
+ concat = t.binaryExpression(
5914
+ "+",
5915
+ concat,
5916
+ t.sequenceExpression([...refs, t.stringLiteral("")])
5917
+ );
5881
5918
  markup = t.arrowFunctionExpression([], concat);
5882
5919
  }
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)));
5920
+ args.push(skipKeys.length ? registerSkip(path, skipKeys) : t.identifier("undefined"), markup);
5888
5921
  }
5889
5922
  const exprs = [t.callExpression(registerImportMethod(path, "ssrElement"), args)];
5890
5923
  return { exprs, template: "", declarations: [], dynamics: [], spreadElement: true };
5891
5924
  }
5892
5925
 
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
- }
5926
+ /** Does evaluating `node` call anything? A function's body does not run. */
5927
+ function callsOutsideFunctions(node) {
5928
+ if (
5929
+ t.isCallExpression(node) ||
5930
+ t.isOptionalCallExpression(node) ||
5931
+ t.isNewExpression(node) ||
5932
+ t.isTaggedTemplateExpression(node))
5919
5933
 
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
- );
5934
+ return true;
5935
+ if (t.isFunction(node)) return false;
5936
+ for (const key of t.VISITOR_KEYS[node.type] ?? []) {
5937
+ const child = node[key];
5938
+ if (Array.isArray(child)) {
5939
+ for (const c of child) if (c && callsOutsideFunctions(c)) return true;
5940
+ } else if (child && typeof child.type === "string" && callsOutsideFunctions(child)) return true;
5941
+ }
5942
+ return false;
5942
5943
  }
5943
5944
 
5944
5945
  /**
@@ -6451,6 +6452,10 @@ path)
6451
6452
  sourceNames(config).components && (config.generate === "dom" || config.generate === "ssr");
6452
6453
  if (labelled) {
6453
6454
  componentArgs.push(t__namespace.stringLiteral(tagName));
6455
+ // `sourceNames.locations`: the opening element's start (`<Home`) as a
6456
+ // fourth argument, `file:line:col`, so a diagnostic can point at the JSX.
6457
+ const loc = sourceLocation(path, path.node.openingElement);
6458
+ if (loc !== undefined) componentArgs.push(t__namespace.stringLiteral(loc));
6454
6459
  }
6455
6460
  // SSR's prod `createComponent` is literally `Comp(props || {})`. Since the
6456
6461
  // compiler always emits a real `props[0]` object expression above (see the
@@ -6585,6 +6590,63 @@ config)
6585
6590
  results.exprs.push(childNodes.length === 1 ? childNodes[0] : t__namespace.arrayExpression(childNodes));
6586
6591
  }
6587
6592
 
6593
+ /**
6594
+ * Solid 1.x attribute forms that 2.0 removed (#4187). Left alone they
6595
+ * compile into literal attributes (`<div use:tip="() => {}">`,
6596
+ * `classlist="[object Object]"`) with no diagnostic, so on native elements
6597
+ * of the web generates (`dom` / `ssr`) they are a compile error that names
6598
+ * the 2.0 replacement. Components are untouched (their props are
6599
+ * user-defined names with no compiler semantics), as is the universal
6600
+ * generate (a custom renderer's `setProp` owns its own naming), and every
6601
+ * other namespace — `prop:`, `xlink:`, `xml:`, `xmlns:`, `svg:`, … — still
6602
+ * compiles as before. The message text is mirrored in the native compiler
6603
+ * (`packages/compiler/src/shared/removed.rs`); keep the two in sync.
6604
+ */
6605
+ function removedAttributeMessage(tagName, key) {
6606
+ if (key === "classList") {
6607
+ return (
6608
+ `\`classList\` on <${tagName}> was removed in Solid 2.0. ` +
6609
+ "Use `class` with an object or array instead: `class={{ active: isActive() }}` " +
6610
+ 'or `class={["base", { active: isActive() }]}`.');
6611
+
6612
+ }
6613
+ const colon = key.indexOf(":");
6614
+ if (colon < 0) return;
6615
+ const namespace = key.slice(0, colon);
6616
+ const name = key.slice(colon + 1);
6617
+ const removed = `\`${key}\` on <${tagName}> uses the \`${namespace}:\` namespace, which was removed in Solid 2.0.`;
6618
+ switch (namespace) {
6619
+ case "on":
6620
+ case "oncapture":
6621
+ return (
6622
+ `${removed} Use a camelCase handler such as \`onClick={handler}\` for Solid events, ` +
6623
+ "or a `ref` callback that calls `addEventListener` for listener options or custom event names.");
6624
+
6625
+ case "attr":
6626
+ return `${removed} Attributes are the default in 2.0: write \`${name}={value}\` (or \`prop:${name}\` to set a DOM property).`;
6627
+ case "bool":
6628
+ return `${removed} Boolean values add or remove the attribute in 2.0: write \`${name}={condition}\`.`;
6629
+ case "use":
6630
+ return (
6631
+ `${removed} Use a \`ref\` directive factory instead: \`ref={${name}(options)}\`, ` +
6632
+ `or \`ref={${name}}\` without options; arrays compose (\`ref={[a, b]}\`).`);
6633
+
6634
+ }
6635
+ }
6636
+
6637
+ /** Throws a code-frame error at the first removed 1.x attribute form. */
6638
+ function assertNoRemovedAttributes(path, tagName) {
6639
+ for (const attribute of path.get("openingElement").get("attributes")) {
6640
+ if (!attribute.isJSXAttribute()) continue;
6641
+ const name = attribute.node.name;
6642
+ const key = t__namespace.isJSXNamespacedName(name) ?
6643
+ `${name.namespace.name}:${name.name.name}` :
6644
+ name.name;
6645
+ const message = removedAttributeMessage(tagName, key);
6646
+ if (message) throw attribute.buildCodeFrameError(message);
6647
+ }
6648
+ }
6649
+
6588
6650
  function isTransformConditionStatements(
6589
6651
  expr)
6590
6652
  {
@@ -6865,11 +6927,13 @@ info = {})
6865
6927
  );
6866
6928
 
6867
6929
  if (tagRenderer?.name === "dom" || getConfig(path).generate === "dom") {
6930
+ assertNoRemovedAttributes(path, tagName);
6868
6931
  renameElementKey(path, false);
6869
6932
  return transformElement$2(path, info);
6870
6933
  }
6871
6934
 
6872
6935
  if (getConfig(path).generate === "ssr") {
6936
+ assertNoRemovedAttributes(path, tagName);
6873
6937
  renameElementKey(path, true);
6874
6938
  return transformElement$1(path, info);
6875
6939
  }
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.17",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {