@solidjs/babel-plugin 2.0.0-rc.14 → 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 3.2s
10
+ created index.js in 3.3s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
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
+
11
+ ## 2.0.0-rc.15
12
+
13
+ ### Patch Changes
14
+
15
+ - 617d195: Emit element claims after the element's initial attributes are applied (#3923), at parity across both compilers.
16
+
17
+ A fully static `a[href]` / `form[action]` is claimed at the end of its creation statements (after static writes and refs). An element with dynamic bindings captures its owner at creation (`var _o$ = _$getOwner()`) and is claimed as the last statement of its binding effect's callback — `_$claimElement(_el$, _o$)` — in both the single-binding and the multi-binding shapes, so the claim observes the applied values on the first run whether that run is synchronous or lands from a held flush. An element carrying a spread emits no compiled claim: the `spread` runtime claims it after the first application.
18
+
19
+ - ca15970: Non-hydratable client builds now remove the `value` attribute when a checkbox or radio input binds a nullish `value` or `defaultValue`, matching the runtime fix from #4009, so a checked box submits `"on"` instead of `""`. Both compilers route these bindings through `setProperty` when the input's static `type` is `checkbox` or `radio`, or when its `type` cannot be known at compile time (an expression, `prop:type`, `attr:type`). Text inputs, inputs without a `type`, and textareas still clear to `""` inline (#2957).
20
+ - 43570f8: Reapply a select's bound value when its options arrive after the value was written.
21
+ - 22f942d: SSR: a router can mark links in server HTML (#3878). Element claims never fire during SSR, so plain anchors arrived without `aria-current="page"` / `data-active` — late on hydrated pages, never on pages that do not hydrate. Both compilers now give every candidate `<a href>` in SSR output one hole after its attributes, `ssrLinkClaim(attrs)`, where a render's **link handler** writes the anchor's link state and `""` otherwise. A static anchor's hole is one eager call over a hoisted attributes object (no allocation per render); a dynamic anchor's joins the element's attribute group and reuses the raw value its `href` hole evaluated. Anchors the compiler can rule out — a static non-empty `target`, a `download`, a `rel` naming `external`, an author-written `aria-current`, an empty href or a non-HTTP scheme — get no hole at all. The hole creates no owner and takes no hydration id. A spread anchor's link attributes are collected by `ssrElement` from its winning sources (an anchor's trailing link attributes stay a source instead of baked tail markup).
22
+
23
+ New server API (`@experimental`): `setLinkClaim(handler)` sets the handler of the render the caller belongs to — found through the owner chain like `getHydrationWriter()`, so concurrent renders never share one — and returns `false` outside a render; `undefined` clears it. The handler, `(attrs) => string`, receives the anchor's link-relevant attributes (`href`, `target`, `rel`, `download`, `link`, `aria-current`, raw values as written) and returns its attribute markup. An author-written `aria-current` always wins. Server components inline in the document render under the page's handler; a server component's own frame stream has none unless its render sets one.
24
+
25
+ Trust marker: the first `setLinkClaim` of a render writes the `links` hydration record once (`_$HY.r.links = 1`, with the shell's hydration script on the document face, a data record on a frame stream). `hasServerLinkState()` (client; `false` on the server) reads it non-destructively, so a link consumer can tell "this page's anchors carry the server's link state" from "no handler ran" and trust the HTML at hydration instead of resolving every anchor's URL at claim. Without a handler the HTML and the hydration ids are byte-for-byte what they were.
26
+
3
27
  ## 2.0.0-rc.14
4
28
 
5
29
  ### 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
@@ -46,6 +46,21 @@ const ChildProperties = /*#__PURE__*/new Set([
46
46
  "children"]
47
47
  );
48
48
 
49
+ // The attributes a link handler decides an anchor's link state on
50
+ // (solidjs/solid#3878): the SSR compilers carry the ones present on a
51
+ // candidate `<a href>` into its `ssrLinkClaim` hole, and `ssrElement`
52
+ // collects the same set from a spread anchor's winning sources. `link` is
53
+ // the explicit-links opt-in marker; `aria-current` is the author's, and
54
+ // wins.
55
+ const LinkAttributes = /*#__PURE__*/new Set([
56
+ "href",
57
+ "target",
58
+ "rel",
59
+ "download",
60
+ "link",
61
+ "aria-current"]
62
+ );
63
+
49
64
  // list of Element events that will be delegated
50
65
  const DelegatedEvents = /*#__PURE__*/new Set([
51
66
  "beforeinput",
@@ -1639,6 +1654,23 @@ attributes,
1639
1654
 
1640
1655
 
1641
1656
 
1657
+
1658
+
1659
+
1660
+
1661
+
1662
+
1663
+
1664
+
1665
+
1666
+
1667
+
1668
+
1669
+
1670
+
1671
+
1672
+
1673
+
1642
1674
 
1643
1675
 
1644
1676
 
@@ -1691,7 +1723,6 @@ const config = {
1691
1723
  memoWrapper: "memo",
1692
1724
  validate: true,
1693
1725
  inlineStyles: true,
1694
- serverComponents: false,
1695
1726
  hoistProps: true
1696
1727
  };
1697
1728
 
@@ -1702,8 +1733,12 @@ const config = {
1702
1733
  */
1703
1734
  function sourceNames(config) {
1704
1735
  const value = config.sourceNames ?? config.dev;
1705
- if (typeof value === "boolean") return { components: value, bindings: value };
1706
- 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
+ };
1707
1742
  }
1708
1743
 
1709
1744
  /**
@@ -1716,6 +1751,74 @@ function namesBindings(config) {
1716
1751
  return config.generate === "dom" && sourceNames(config).bindings;
1717
1752
  }
1718
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
+
1719
1822
  const InlineElements = [
1720
1823
  "a",
1721
1824
  "abbr",
@@ -1841,6 +1944,7 @@ const t$1 = t__namespace;
1841
1944
 
1842
1945
 
1843
1946
 
1947
+
1844
1948
 
1845
1949
 
1846
1950
  function isNamedAttribute(
@@ -1977,6 +2081,7 @@ info = {})
1977
2081
  declarations: [],
1978
2082
  exprs: [],
1979
2083
  dynamics: [],
2084
+ claims: [],
1980
2085
  postExprs: [],
1981
2086
  isImportNode,
1982
2087
  isWrapped,
@@ -2054,13 +2159,25 @@ info = {})
2054
2159
  if (!info.skipId) {
2055
2160
  results.id = path.scope.generateUidIdentifier("el$");
2056
2161
  }
2057
- // Claim contract: a[href] / form[action] elements are claimed at creation
2058
- // so registered consumers (e.g. a router's link-state layer) see them.
2059
- // detectExpressions forces the id chain, so claim targets always have one.
2060
- // Emitted ahead of the attribute expressions — writes to the claimed
2061
- // attribute recheck through the runtime's setAttribute, so order stays
2062
- // correct either way, but "claim at creation" reads first.
2063
- if (results.id && isClaimTarget(path.node)) {
2162
+ // Claim contract (#3923): a[href] / form[action] elements are claimed ONCE,
2163
+ // after their initial attributes are applied, so registered consumers
2164
+ // (e.g. a router's link-state layer) see the element they will manage.
2165
+ // Decided before the attributes are lowered — processSpreads folds a
2166
+ // spread's attributes away — and emitted after: a fully static element
2167
+ // (attributes in the template, writes at creation) is claimed at creation,
2168
+ // after those writes and its refs; an element with dynamic bindings at the
2169
+ // tail of the template root's binding effect, whose first run applies
2170
+ // them (wrapDynamics); a spread element by the spread runtime after the
2171
+ // spread's first application — no compiled claim. detectExpressions forces
2172
+ // the id chain, so claim targets always have one.
2173
+ const claimTarget =
2174
+ !!results.id &&
2175
+ isClaimTarget(path.node) &&
2176
+ !path.node.openingElement.attributes.some((attr) => t$1.isJSXSpreadAttribute(attr));
2177
+ transformAttributes$1(path, results);
2178
+ if (claimTarget) {
2179
+ if (results.dynamics.length) results.claims.push(results.id);else
2180
+
2064
2181
  results.exprs.push(
2065
2182
  t$1.expressionStatement(
2066
2183
  t$1.callExpression(
@@ -2070,7 +2187,6 @@ info = {})
2070
2187
  )
2071
2188
  );
2072
2189
  }
2073
- transformAttributes$1(path, results);
2074
2190
  if (config.contextToCustomElements && (tagName === "slot" || hasCustomElement)) {
2075
2191
  contextToCustomElement(path, results);
2076
2192
  }
@@ -2112,7 +2228,7 @@ path,
2112
2228
  elem,
2113
2229
  name,
2114
2230
  value,
2115
- { dynamic, prevId, tagName, styleProperty, classProperty } = {})
2231
+ { dynamic, prevId, tagName, styleProperty, classProperty, checkableInput } = {})
2116
2232
  {
2117
2233
  // pull out namespace
2118
2234
  const config = getConfig(path);
@@ -2183,8 +2299,21 @@ value,
2183
2299
  // Locked/stateful DOM properties (value/checked/...) must go through the
2184
2300
  // runtime in hydratable builds so the hydration claim pass can adopt
2185
2301
  // pre-hydration user state instead of overwriting it (#3182). setProperty
2186
- // carries the select-microtask and input/textarea nullish special cases.
2187
- if (config.hydratable && (isLocked || namespace !== "prop")) {
2302
+ // carries the select-value retry and input/textarea nullish special cases.
2303
+ // `<select value>` always goes through it: a microtask only sees options
2304
+ // inserted in the same turn, so options that arrive later never get
2305
+ // selected (#3928). So does a non-literal value/defaultValue on an input
2306
+ // that may be a checkbox or radio: a nullish value must remove the
2307
+ // attribute so the checked box submits its native "on" (#4009, #4017),
2308
+ // and setProperty already tests the live `type` for that.
2309
+ if (
2310
+ config.hydratable && (isLocked || namespace !== "prop") ||
2311
+ name === "value" && tagName === "select" ||
2312
+ checkableInput && (
2313
+ name === "value" || name === "defaultValue") &&
2314
+ !t$1.isStringLiteral(value) &&
2315
+ !t$1.isNumericLiteral(value))
2316
+ {
2188
2317
  return t$1.callExpression(registerImportMethod(path, "setProperty"), [
2189
2318
  elem,
2190
2319
  t$1.stringLiteral(name),
@@ -2197,24 +2326,14 @@ value,
2197
2326
  t$1.memberExpression(elem, t$1.identifier(name)),
2198
2327
  value
2199
2328
  );
2200
- // handle select/options... TODO: consider other ways in the future
2201
- // TODO: there may be a race condition here
2202
- if (name === "value" && tagName === "select") {
2203
- return t$1.logicalExpression(
2204
- "||",
2205
- t$1.callExpression(t$1.identifier("queueMicrotask"), [
2206
- t$1.arrowFunctionExpression([], assignment)]
2207
- ),
2208
- assignment
2209
- );
2210
- }
2211
2329
  if (
2212
2330
  (name === "value" || name === "defaultValue") && (
2213
2331
  tagName === "input" || tagName === "textarea") &&
2214
2332
  !t$1.isStringLiteral(value) &&
2215
2333
  !t$1.isNumericLiteral(value))
2216
2334
  {
2217
- // prevents undefined on input/textarea.value/defaultValue, fallback to empty string
2335
+ // prevents undefined on text input/textarea.value/defaultValue, fallback
2336
+ // to empty string (#2957)
2218
2337
  return t$1.assignmentExpression(
2219
2338
  "=",
2220
2339
  t$1.memberExpression(elem, t$1.identifier(name)),
@@ -2255,6 +2374,35 @@ handler)
2255
2374
  return t$1.isFunction(handler);
2256
2375
  }
2257
2376
 
2377
+ /**
2378
+ * Whether an `<input>` may be a checkbox or radio, whose nullish
2379
+ * value/defaultValue removes the `value` attribute (restoring the native "on"
2380
+ * submission value) instead of clearing it to "" like a text field (#4017).
2381
+ * Only a plain `type` written as a string literal is resolved here; a missing
2382
+ * `type` is a text input. Anything else (an expression, `prop:type`,
2383
+ * `attr:type`, a spread) is unknown and treated as possibly checkable, which
2384
+ * stays correct because setProperty tests the live `type`. Mirrored by the
2385
+ * native compiler's `input_may_be_checkable` (dom/attrs.rs).
2386
+ */
2387
+ function inputMayBeCheckable(node) {
2388
+ let checkable = false;
2389
+ for (const attribute of node.openingElement.attributes) {
2390
+ if (t$1.isJSXSpreadAttribute(attribute)) return true;
2391
+ const name = attribute.name;
2392
+ if (t$1.isJSXNamespacedName(name)) {
2393
+ if (name.name.name === "type") return true;
2394
+ continue;
2395
+ }
2396
+ if (name.name !== "type") continue;
2397
+ let value = attribute.value;
2398
+ if (t$1.isJSXExpressionContainer(value)) value = value.expression;
2399
+ if (value == null) checkable = false;else
2400
+ if (t$1.isStringLiteral(value)) checkable = /^(checkbox|radio)$/i.test(value.value);else
2401
+ return true;
2402
+ }
2403
+ return checkable;
2404
+ }
2405
+
2258
2406
  function transformAttributes$1(
2259
2407
  path,
2260
2408
  results)
@@ -2267,7 +2415,8 @@ results)
2267
2415
  const tagName = getTagName(path.node),
2268
2416
  hasChildren = path.node.children.length > 0,
2269
2417
  hasSpread = attributes.some((attribute) => t$1.isJSXSpreadAttribute(attribute.node)),
2270
- config = getConfig(path);
2418
+ config = getConfig(path),
2419
+ checkableInput = tagName === "input" && inputMayBeCheckable(path.node);
2271
2420
  const refs = [];
2272
2421
 
2273
2422
  // preprocess spreads
@@ -2437,6 +2586,8 @@ results)
2437
2586
  t$1.jsxExpressionContainer(p.value)
2438
2587
  );
2439
2588
  newAttr._styleProperty = true;
2589
+ // The split-out property is located at the `style` attribute it came from.
2590
+ newAttr.loc = styleObjectAttribute.node.loc;
2440
2591
  path.
2441
2592
  get("openingElement").
2442
2593
  node.attributes.splice(Number(styleObjectAttribute.key) + ++i, 0, newAttr);
@@ -2487,13 +2638,16 @@ results)
2487
2638
  ))
2488
2639
  {
2489
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;
2490
2648
  path.
2491
2649
  get("openingElement").
2492
- node.attributes.splice(
2493
- Number(classArrayAttribute.key) + 1,
2494
- 0,
2495
- t$1.jsxAttribute(t$1.jsxIdentifier("class"), t$1.jsxExpressionContainer(dynamicClassElement))
2496
- );
2650
+ node.attributes.splice(Number(classArrayAttribute.key) + 1, 0, dynamicClassAttribute);
2497
2651
  }
2498
2652
  }
2499
2653
 
@@ -2539,6 +2693,8 @@ results)
2539
2693
  t$1.jsxExpressionContainer(p.value)
2540
2694
  );
2541
2695
  newAttr._classProperty = true;
2696
+ // The split-out property is located at the `class` attribute it came from.
2697
+ newAttr.loc = classListAttribute.node.loc;
2542
2698
  path.
2543
2699
  get("openingElement").
2544
2700
  node.attributes.splice(Number(classListAttribute.key) + ++i, 0, newAttr);
@@ -2945,14 +3101,20 @@ results)
2945
3101
  key,
2946
3102
  value: value.expression,
2947
3103
  tagName,
3104
+ checkableInput,
2948
3105
  styleProperty: isStyleProperty,
2949
- 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)
2950
3111
  });
2951
3112
  } else {
2952
3113
  results.exprs.push(
2953
3114
  t$1.expressionStatement(
2954
3115
  setAttr$1(attribute, elem, key, value.expression, {
2955
3116
  tagName,
3117
+ checkableInput,
2956
3118
  styleProperty: isStyleProperty,
2957
3119
  classProperty: isClassProperty
2958
3120
  })
@@ -3001,7 +3163,10 @@ results)
3001
3163
  {
3002
3164
  results.exprs.push(
3003
3165
  t$1.expressionStatement(
3004
- setAttr$1(attribute, elem, key, staticValue, { tagName })
3166
+ setAttr$1(attribute, elem, key, staticValue, {
3167
+ tagName,
3168
+ checkableInput
3169
+ })
3005
3170
  )
3006
3171
  );
3007
3172
  } else {
@@ -3103,6 +3268,10 @@ config)
3103
3268
  if (!transformed) return memo;
3104
3269
  transformed.allocatesIds = allocatesIds;
3105
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);
3106
3275
  const i = memo.length;
3107
3276
  if (transformed.text && i && memo[i - 1].text) {
3108
3277
  memo[i - 1].template =
@@ -3168,6 +3337,7 @@ config)
3168
3337
  results.declarations.push(...child.declarations);
3169
3338
  results.exprs.push(...child.exprs);
3170
3339
  results.dynamics.push(...child.dynamics);
3340
+ if (child.claims) results.claims.push(...child.claims);
3171
3341
  childPostExprs.push(...(child.postExprs || []));
3172
3342
  results.hasHydratableEvent =
3173
3343
  results.hasHydratableEvent || child.hasHydratableEvent;
@@ -3185,14 +3355,11 @@ config)
3185
3355
  // is inserted directly and creates no effect to name.
3186
3356
  const accessorHole =
3187
3357
  child.dynamic || child.functionHole;
3358
+ // `sourceNames.locations`: `loc` after `name`, the hole's `{` start.
3188
3359
  const emitInsert = (args) => {
3189
3360
  if (accessorHole && namesBindings(getConfig(path))) {
3190
3361
  while (args.length < 4) args.push(t$1.identifier("undefined"));
3191
- args.push(
3192
- t$1.objectExpression([
3193
- t$1.objectProperty(t$1.identifier("name"), t$1.stringLiteral(`${tagName}.children`))]
3194
- )
3195
- );
3362
+ args.push(bindingOptions(`${tagName}.children`, child.loc));
3196
3363
  }
3197
3364
  results.exprs.push(t$1.expressionStatement(t$1.callExpression(insert, args)));
3198
3365
  };
@@ -3492,7 +3659,14 @@ attributes,
3492
3659
  // `sourceNames.bindings`: the tag as written rides as spread's trailing
3493
3660
  // argument (past the runtime-only `skip` slot); the runtime labels its
3494
3661
  // attribute effect `<tag>.spread` and its children insert `<tag>.children`.
3495
- 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
+ }
3496
3670
 
3497
3671
  return [
3498
3672
  filteredAttributes,
@@ -3512,6 +3686,23 @@ wrap)
3512
3686
  {
3513
3687
  const config = getConfig(path);
3514
3688
  if (result.id) {
3689
+ // Claim targets with dynamic bindings are claimed at the tail of the
3690
+ // binding effect's first run, under the owner the template is created
3691
+ // with: the effect's callback runs ownerless when a held mount lands, so
3692
+ // the owner is captured here, at creation, and handed to the claim.
3693
+ let ownerId;
3694
+ if (result.claims?.length) {
3695
+ ownerId = path.scope.generateUidIdentifier("o$");
3696
+ result.declarations.push(
3697
+ t__namespace.variableDeclarator(
3698
+ ownerId,
3699
+ t__namespace.callExpression(
3700
+ registerImportMethod(path, "getOwner", getRendererConfig(path, "dom").moduleName),
3701
+ []
3702
+ )
3703
+ )
3704
+ );
3705
+ }
3515
3706
  registerTemplate(path, result);
3516
3707
  const decl = result.decl;
3517
3708
  if (
@@ -3520,7 +3711,7 @@ wrap)
3520
3711
  {
3521
3712
  return decl.declarations[0].init;
3522
3713
  } else {
3523
- const dynamicsStmt = wrapDynamics$1(path, result.dynamics);
3714
+ const dynamicsStmt = wrapDynamics$1(path, result.dynamics, result.claims, ownerId);
3524
3715
  const stmts = [
3525
3716
  decl,
3526
3717
  ...result.exprs,
@@ -3662,31 +3853,47 @@ function wrapReadShallow(path, key, value) {
3662
3853
  );
3663
3854
  }
3664
3855
 
3665
- function wrapDynamics$1(path, dynamics) {
3856
+ function wrapDynamics$1(
3857
+ path,
3858
+ dynamics,
3859
+ claims = [],
3860
+ ownerId)
3861
+ {
3666
3862
  if (!dynamics.length) return;
3667
3863
  const config = getConfig(path);
3668
3864
 
3669
3865
  // dynamics are only queued when effectWrapper is configured (element.ts
3670
3866
  // guards every push), so the name is always a string here
3671
3867
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
3868
+ // The claim contract's mount claims for this root's claim targets with
3869
+ // dynamic bindings (element.ts): the tail of the effect body, so the first
3870
+ // run claims after it applied the initial attributes; `claimElement` is
3871
+ // once-per-element, so reruns cost one check. `ownerId` is the owner
3872
+ // captured at creation (createTemplate) the handlers run under.
3873
+ const claimStatements = claims.map((id) =>
3874
+ t__namespace.expressionStatement(
3875
+ t__namespace.callExpression(
3876
+ registerImportMethod(path, "claimElement", getRendererConfig(path, "dom").moduleName),
3877
+ [id, ownerId]
3878
+ )
3879
+ )
3880
+ );
3672
3881
  // `sourceNames.bindings`: the effect is named by what it writes — each
3673
3882
  // binding's `<tag>.<attribute>` as written (undoing the `prop:` the locked
3674
3883
  // DOM property pre-pass added to `value`, `checked`, …), the merged effect
3675
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).
3676
3887
  const label = namesBindings(config) ?
3677
3888
  [
3678
- t__namespace.objectExpression([
3679
- t__namespace.objectProperty(
3680
- t__namespace.identifier("name"),
3681
- t__namespace.stringLiteral(
3682
- dynamics.
3683
- map(
3684
- (d) =>
3685
- `${d.tagName}.${isLockedDOMProperty(d.tagName, d.key) ? d.key.replace(/^prop:/, "") : d.key}`
3686
- ).
3687
- join(", ")
3688
- )
3689
- )]
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
3690
3897
  )] :
3691
3898
 
3692
3899
  [];
@@ -3713,12 +3920,14 @@ function wrapDynamics$1(path, dynamics) {
3713
3920
  t__namespace.expressionStatement(
3714
3921
  setAttr$1(path, dynamics[0].elem, dynamics[0].key, newValue, {
3715
3922
  tagName: dynamics[0].tagName,
3923
+ checkableInput: dynamics[0].checkableInput,
3716
3924
  dynamic: true,
3717
3925
  prevId: prevValue,
3718
3926
  styleProperty: dynamics[0].styleProperty,
3719
3927
  classProperty: dynamics[0].classProperty
3720
3928
  })
3721
- )]
3929
+ ),
3930
+ ...claimStatements]
3722
3931
  )
3723
3932
  ),
3724
3933
  ...label]
@@ -3732,7 +3941,9 @@ function wrapDynamics$1(path, dynamics) {
3732
3941
  const statements = [];
3733
3942
  const properties = [];
3734
3943
 
3735
- dynamics.forEach(({ elem, key, value, tagName, styleProperty, classProperty }, index) => {
3944
+ dynamics.forEach((binding, index) => {
3945
+ const { elem, key, tagName, styleProperty, classProperty, checkableInput } = binding;
3946
+ let value = binding.value;
3736
3947
  const propIdent = t__namespace.identifier(getNumberedId(index));
3737
3948
  const propMember = t__namespace.memberExpression(prevId, propIdent);
3738
3949
  const optionalPropMember = t__namespace.optionalMemberExpression(prevId, propIdent, false, true);
@@ -3750,7 +3961,8 @@ function wrapDynamics$1(path, dynamics) {
3750
3961
  setAttr$1(path, elem, key, propIdent, {
3751
3962
  tagName,
3752
3963
  dynamic: true,
3753
- prevId: optionalPropMember
3964
+ prevId: optionalPropMember,
3965
+ checkableInput
3754
3966
  })
3755
3967
  )
3756
3968
  );
@@ -3770,7 +3982,8 @@ function wrapDynamics$1(path, dynamics) {
3770
3982
  tagName,
3771
3983
  dynamic: true,
3772
3984
  styleProperty,
3773
- classProperty
3985
+ classProperty,
3986
+ checkableInput
3774
3987
  })
3775
3988
  )
3776
3989
  )
@@ -3783,7 +3996,7 @@ function wrapDynamics$1(path, dynamics) {
3783
3996
  t__namespace.arrowFunctionExpression([], t__namespace.objectExpression(values)),
3784
3997
  t__namespace.arrowFunctionExpression(
3785
3998
  [t__namespace.objectPattern(properties.map((id) => t__namespace.objectProperty(id, id, false, true))), prevId],
3786
- t__namespace.blockStatement(statements)
3999
+ t__namespace.blockStatement([...statements, ...claimStatements])
3787
4000
  ),
3788
4001
  ...label]
3789
4002
  )
@@ -3969,6 +4182,45 @@ function appendSkips(path, skips) {
3969
4182
  );
3970
4183
  }
3971
4184
 
4185
+ /**
4186
+ * The link-attributes object of a static anchor's `ssrLinkClaim` hole
4187
+ * (ssr/element.ts `transformAttributes`): `{ href: "/about" }`, the anchor's
4188
+ * link-relevant attributes as the literals they were written as. Hoisted to
4189
+ * the module like a template — one object per distinct attribute set,
4190
+ * shared by every anchor that writes the same one — so the hole allocates
4191
+ * nothing per render.
4192
+ */
4193
+ function registerLinkAttrs(path, entries) {
4194
+ const data = path.scope.getProgramParent().data;
4195
+ const records = data.ssrLinkAttrs || (data.ssrLinkAttrs = []);
4196
+ const key = entries.map(([name, value]) => `${name}=${value}`).join("\0");
4197
+ const found = records.find((r) => r.key === key);
4198
+ if (found) return found.id;
4199
+ const id = path.scope.generateUidIdentifier("lk$");
4200
+ records.push({
4201
+ key,
4202
+ id,
4203
+ object: t__namespace.objectExpression(
4204
+ entries.map(([name, value]) => t__namespace.objectProperty(linkAttrsKey(name), t__namespace.stringLiteral(value)))
4205
+ )
4206
+ });
4207
+ return id;
4208
+ }
4209
+
4210
+ /** The key of a link-attributes object property: `href`, or `"aria-current"`. */
4211
+ function linkAttrsKey(name) {
4212
+ return t__namespace.isValidIdentifier(name) ? t__namespace.identifier(name) : t__namespace.stringLiteral(name);
4213
+ }
4214
+
4215
+ function appendLinkAttrs(path, records) {
4216
+ path.node.body.unshift(
4217
+ t__namespace.variableDeclaration(
4218
+ "var",
4219
+ records.map((r) => t__namespace.variableDeclarator(r.id, r.object))
4220
+ )
4221
+ );
4222
+ }
4223
+
3972
4224
  /**
3973
4225
  * Hoisted props shapes (SSR, #3511).
3974
4226
  *
@@ -4905,6 +5157,93 @@ function normalizeAttributes(path) {
4905
5157
  return attributes;
4906
5158
  }
4907
5159
 
5160
+ /**
5161
+ * The compile-time value of a JSX attribute, as the SSR static branch sees
5162
+ * it: `null` for a bare attribute (`<a link>`), the literal for a string /
5163
+ * number / boolean (quoted or in a container), `undefined` when the value is
5164
+ * an expression the compiler cannot see through (dynamic, or an empty
5165
+ * container the attribute drops out for).
5166
+ */
5167
+ function staticAttributeValue(
5168
+ value)
5169
+ {
5170
+ if (value == null) return null;
5171
+ if (t.isStringLiteral(value)) return value.value;
5172
+ if (!t.isJSXExpressionContainer(value)) return undefined;
5173
+ const expression = value.expression;
5174
+ if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) return expression.value;
5175
+ if (t.isBooleanLiteral(expression)) return expression.value;
5176
+ return undefined;
5177
+ }
5178
+
5179
+ // A URL scheme at the start of an href. `http:`/`https:` (and a
5180
+ // protocol-relative `//host`) may still be this origin — only the handler,
5181
+ // which knows the request, can say — so only another scheme rules an
5182
+ // anchor out at compile time.
5183
+ const HREF_SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
5184
+
5185
+ /**
5186
+ * Whether an anchor (the template path's, no spread) gets the `ssrLinkClaim`
5187
+ * hole (solidjs/solid#3878): it has an `href`, and nothing the compiler can
5188
+ * see rules it out as the current page — the anchors a link handler would
5189
+ * answer `""` for however the request looks. A static non-empty `target`,
5190
+ * a `download`, a `rel` naming `external`, an author-written `aria-current`
5191
+ * (hers to keep), an empty href or one with a non-HTTP scheme (`mailto:`,
5192
+ * `tel:`, `javascript:` …) get no hole and no per-render call; a dynamic
5193
+ * value in any of these positions is the handler's to judge at runtime.
5194
+ * Mirrors the DOM transform's `isClaimTarget` (`a[href]`), narrowed.
5195
+ */
5196
+ function isLinkClaimCandidate(attributes) {
5197
+ let href = false;
5198
+ for (const attribute of attributes) {
5199
+ const node = attribute.node;
5200
+ if (!t.isJSXAttribute(node) || !t.isJSXIdentifier(node.name)) continue;
5201
+ const key = node.name.name;
5202
+ if (!LinkAttributes.has(key)) continue;
5203
+ if (t.isJSXExpressionContainer(node.value) && t.isJSXEmptyExpression(node.value.expression))
5204
+ continue;
5205
+ const value = staticAttributeValue(node.value);
5206
+ if (key === "href") {
5207
+ if (value === false) continue;
5208
+ href = true;
5209
+ if (value === undefined) continue;
5210
+ // bare `href`, `href=""`, `href={true}`: an empty href is no link
5211
+ if (value === null || value === true || value === "") return false;
5212
+ const scheme = typeof value === "string" ? HREF_SCHEME.exec(value) : null;
5213
+ if (scheme && scheme[1].toLowerCase() !== "http" && scheme[1].toLowerCase() !== "https")
5214
+ return false;
5215
+ continue;
5216
+ }
5217
+ // A dynamic value is the handler's to judge; `={false}` omits the attribute.
5218
+ if (value === undefined || value === false) continue;
5219
+ switch (key) {
5220
+ case "target":
5221
+ // A bare `target`, `target=""` or `target={true}` is the empty
5222
+ // target — still a link.
5223
+ if (value !== null && value !== true && value !== "") return false;
5224
+ break;
5225
+ case "rel":
5226
+ if (typeof value === "string" && value.split(/\s+/).includes("external")) return false;
5227
+ break;
5228
+ case "download":
5229
+ case "aria-current":
5230
+ // Present in any form: a download, or the author's own current marker.
5231
+ return false;
5232
+ // `link`: the explicit-links opt-in marker — the handler reads it.
5233
+ }
5234
+ }
5235
+ return href;
5236
+ }
5237
+
5238
+ /**
5239
+ * The attribute entries of a candidate anchor's `ssrLinkClaim` hole,
5240
+ * collected by `transformAttributes` as it writes each attribute: a static
5241
+ * value as the string it serializes to, a dynamic value as the function-local
5242
+ * temp its attribute hole assigns (`_lv$N`), flagged when that hole is a
5243
+ * thunk (`isDynamic`) so the link hole is one too and reads the temp after it.
5244
+ */
5245
+
5246
+
4908
5247
  function transformAttributes(
4909
5248
  path,
4910
5249
  results,
@@ -4915,13 +5254,16 @@ info)
4915
5254
  const hasChildren = path.node.children.length > 0,
4916
5255
  attributes = normalizeAttributes(path);
4917
5256
  let children;
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.
4924
- const claims = [];
5257
+ // Link claims (solidjs/solid#3878): a candidate anchor gets one hole after
5258
+ // its attributes — `ssrLinkClaim(attrs)` — where a render's link handler
5259
+ // writes the anchor's link state (`aria-current="page"`, `data-active`)
5260
+ // into the server HTML, and `""` otherwise. `attrs` is the anchor's
5261
+ // link-relevant attributes: a static anchor's are a hoisted object, so the
5262
+ // hole is one eager call with no allocation per render; a dynamic one's
5263
+ // join the element's attribute group and reuse the values its attribute
5264
+ // holes evaluated. The hole never takes a hydration id.
5265
+ const linkEntries =
5266
+ tagName === "a" && isLinkClaimCandidate(attributes) ? [] : null;
4925
5267
 
4926
5268
  attributes.forEach((attribute) => {
4927
5269
  if (!t.isJSXAttribute(attribute.node)) return;
@@ -4952,23 +5294,13 @@ info)
4952
5294
  {
4953
5295
  if (t.isJSXEmptyExpression(value.expression)) return;
4954
5296
  if (key === "ref") {
4955
- if (info.serverComponents) {
4956
- claims.push(["ref", value.expression]);
4957
- return;
4958
- }
4959
5297
  results.declarations.push(
4960
5298
  t.variableDeclarator(path.scope.generateUidIdentifier("_ref$"), value.expression)
4961
5299
  );
4962
5300
  return;
4963
5301
  }
4964
5302
  if (key.startsWith("prop:")) return;
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]);
4970
- return;
4971
- }
5303
+ if (isEventName(key)) return;
4972
5304
  if (ChildProperties.has(key)) {
4973
5305
  if (key === "children" && VoidElements.has(tagName)) return;
4974
5306
  if (info.hydratable && key === "textContent" && value && value.expression) {
@@ -5000,30 +5332,25 @@ info)
5000
5332
  checkMember: true,
5001
5333
  checkTags: true
5002
5334
  });
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;
5335
+ // A candidate anchor's dynamic link attribute: the hole evaluates the
5336
+ // raw value ONCE into a function-local temp (`_$escape(_lv$ = expr,
5337
+ // true)`) that the link hole reads after it — the handler sees the
5338
+ // value as written, not attribute-escaped. A template literal keeps
5339
+ // its inline-quoted slot: the whole value is escaped at runtime
5340
+ // instead of its parts, the same bytes (escaping is per character),
5341
+ // and a template literal is never nullish.
5342
+ let linkTemplate = false;
5343
+ if (linkEntries !== null && LinkAttributes.has(key)) {
5344
+ const expression = value.expression;
5345
+ linkTemplate = t.isTemplateLiteral(expression);
5346
+ const temp = path.scope.generateUidIdentifier("lv$");
5347
+ path.scope.push({ id: temp, kind: "var" });
5348
+ value.expression = t.assignmentExpression("=", temp, expression);
5349
+ linkEntries.push([key, t.cloneNode(temp), !!isDynamicValue]);
5022
5350
  }
5023
5351
  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.
5352
+ // The whole attribute must be absent for nullish values. Capture the
5353
+ // value once before testing it.
5027
5354
  const id = path.scope.generateUidIdentifier("v$");
5028
5355
  const attr = t.callExpression(
5029
5356
  t.arrowFunctionExpression(
@@ -5141,7 +5468,7 @@ info)
5141
5468
  );
5142
5469
  const expression = value.expression;
5143
5470
 
5144
- if (!(doEscape || isBoolean) || t.isLiteral(expression)) {
5471
+ if (!(doEscape || isBoolean) || t.isLiteral(expression) || linkTemplate) {
5145
5472
  if (isBoolean) {
5146
5473
  t.isBooleanLiteral(expression) &&
5147
5474
  expression.value === true &&
@@ -5170,6 +5497,16 @@ info)
5170
5497
  const isBoolean = !!booleanLiteral;
5171
5498
  if (booleanLiteral && !booleanLiteral.value) return;
5172
5499
  appendToTemplate(results.template, ` ${key}`);
5500
+ // A candidate anchor's static link attribute travels as the string the
5501
+ // attribute carries: a bare attribute's is `""`.
5502
+ if (linkEntries !== null && LinkAttributes.has(key))
5503
+ linkEntries.push([
5504
+ key,
5505
+ !staticValue || isBoolean ?
5506
+ "" :
5507
+ String(staticValue.value),
5508
+ false]
5509
+ );
5173
5510
  if (!staticValue) return;
5174
5511
  let text = isBoolean ?
5175
5512
  "" :
@@ -5188,27 +5525,39 @@ info)
5188
5525
  );
5189
5526
  }
5190
5527
  });
5191
- if (claims.length) {
5192
- const map = claimMap(claims);
5193
- // `_$sharedConfig.context && _$sharedConfig.context.claims
5194
- // ? _$ssrClaim({...}) : ""`
5195
- // — the claims flag is only set inside a server component's render
5196
- // scope (and cleared inside suppressed fill windows), so ordinary SSR
5197
- // pays the property reads and never evaluates the expressions.
5198
- const sharedConfigId = registerImportMethod(path, "sharedConfig");
5199
- const contextRead = () =>
5200
- t.memberExpression(t.cloneNode(sharedConfigId), t.identifier("context"));
5201
- const guarded = t.conditionalExpression(
5202
- t.logicalExpression(
5203
- "&&",
5204
- contextRead(),
5205
- t.memberExpression(contextRead(), t.identifier("claims"))
5206
- ),
5207
- t.callExpression(registerImportMethod(path, "ssrClaim"), [map]),
5208
- t.stringLiteral("")
5209
- );
5528
+ if (linkEntries !== null) {
5529
+ const ssrLinkClaim = registerImportMethod(path, "ssrLinkClaim");
5530
+ let hole;
5531
+ if (linkEntries.every(([, value]) => typeof value === "string")) {
5532
+ // Every link attribute static: `_$ssrLinkClaim(_lk$N)` over the hoisted
5533
+ // object, evaluated in argument position like a hydration key.
5534
+ hole = t.callExpression(ssrLinkClaim, [
5535
+ registerLinkAttrs(
5536
+ path,
5537
+ linkEntries.map(([key, value]) => [key, value])
5538
+ )]
5539
+ );
5540
+ } else {
5541
+ // `_$ssrLinkClaim({ href: _lv$, rel: "noopener" })`, built per render.
5542
+ // A thunk when any captured attribute hole is one — it must run after
5543
+ // them — and groupable, so it joins the element's attribute group.
5544
+ hole = t.callExpression(ssrLinkClaim, [
5545
+ t.objectExpression(
5546
+ linkEntries.map(([key, value]) =>
5547
+ t.objectProperty(
5548
+ linkAttrsKey(key),
5549
+ typeof value === "string" ? t.stringLiteral(value) : value
5550
+ )
5551
+ )
5552
+ )]
5553
+ );
5554
+ if (linkEntries.some(([,, dynamic]) => dynamic))
5555
+ hole = hoistExpression(path, results, t.arrowFunctionExpression([], hole), {
5556
+ group: true
5557
+ });
5558
+ }
5210
5559
  results.template.push("");
5211
- results.templateValues.push(hoistExpression(path, results, guarded));
5560
+ results.templateValues.push(hole);
5212
5561
  }
5213
5562
  if (!hasChildren && children) {
5214
5563
  path.node.children.push(children);
@@ -5335,7 +5684,7 @@ results,
5335
5684
 
5336
5685
  function createElement(
5337
5686
  path,
5338
- { topLevel, hydratable, serverComponents })
5687
+ { topLevel, hydratable })
5339
5688
  {
5340
5689
  const tagName = getTagName(path.node),
5341
5690
  config = getConfig(path),
@@ -5388,22 +5737,6 @@ path,
5388
5737
  const node = attribute.node;
5389
5738
  return !(t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "ref");
5390
5739
  });
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 = [];
5406
-
5407
5740
  let props;
5408
5741
  // Attributes written AFTER the last spread are markup, not a source: no
5409
5742
  // later source can override them, so they go to `ssrElement` as its
@@ -5423,13 +5756,26 @@ path,
5423
5756
  // keys.
5424
5757
  const tail = [];
5425
5758
  const skipKeys = [];
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
- {
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)) {
5433
5779
  props = [propAttributes[0].node.argument];
5434
5780
  } else {
5435
5781
  props = [];
@@ -5475,30 +5821,13 @@ path,
5475
5821
  node.name.name;
5476
5822
 
5477
5823
  if (hasChildren && key === "children") 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
- }
5824
+ if (key === "ref" || isEventName(key)) return;
5500
5825
  if (key.startsWith("prop:")) return;
5501
- if (i > lastSpread) {
5826
+ // An anchor's link-relevant attributes stay a source, never baked
5827
+ // markup: `ssrElement` collects them from the walk for the render's
5828
+ // link handler (solidjs/solid#3878), and a baked tail is invisible to
5829
+ // it.
5830
+ if (i > lastSpread && !(tagName === "a" && LinkAttributes.has(key))) {
5502
5831
  const part = tailAttribute(path, tagName, key, node);
5503
5832
  if (part !== undefined) {
5504
5833
  if (typeof part === "string" && typeof tail[tail.length - 1] === "string")
@@ -5561,76 +5890,52 @@ path,
5561
5890
  // set) and attribute string — a literal when every part is static, else a
5562
5891
  // thunk concatenating the parts in source order. A `false` static bakes to
5563
5892
  // no markup but still skips its key.
5564
- if (skipKeys.length) {
5893
+ if (skipKeys.length || refs.length) {
5565
5894
  let markup;
5566
- if (tail.every((part) => typeof part === "string")) markup = t.stringLiteral(tail.join(""));else
5567
- {
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 {
5568
5901
  let concat;
5569
5902
  for (const part of tail) {
5570
5903
  const expr = typeof part === "string" ? t.stringLiteral(part) : part;
5571
5904
  concat = concat ? t.binaryExpression("+", concat, expr) : expr;
5572
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
+ );
5573
5914
  markup = t.arrowFunctionExpression([], concat);
5574
5915
  }
5575
- args.push(registerSkip(path, skipKeys), markup);
5576
- }
5577
- if (claims.length) {
5578
- if (!skipKeys.length) args.push(t.identifier("undefined"), t.identifier("undefined"));
5579
- args.push(t.arrowFunctionExpression([], claimSegments(claims)));
5916
+ args.push(skipKeys.length ? registerSkip(path, skipKeys) : t.identifier("undefined"), markup);
5580
5917
  }
5581
5918
  const exprs = [t.callExpression(registerImportMethod(path, "ssrElement"), args)];
5582
5919
  return { exprs, template: "", declarations: [], dynamics: [], spreadElement: true };
5583
5920
  }
5584
5921
 
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
- }
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))
5611
5929
 
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
- );
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;
5634
5939
  }
5635
5940
 
5636
5941
  /**
@@ -6143,6 +6448,10 @@ path)
6143
6448
  sourceNames(config).components && (config.generate === "dom" || config.generate === "ssr");
6144
6449
  if (labelled) {
6145
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));
6146
6455
  }
6147
6456
  // SSR's prod `createComponent` is literally `Comp(props || {})`. Since the
6148
6457
  // compiler always emits a real `props[0]` object expression above (see the
@@ -15167,8 +15476,10 @@ var postprocess = (path, state) => {
15167
15476
  if (hoisted) path.node.body.unshift(...hoisted);
15168
15477
  }
15169
15478
  // Spread-element skip predicates sit between the templates and the hoisted
15170
- // props shapes (unshifted after them, before the templates unshift).
15479
+ // props shapes (unshifted after them, before the templates unshift); the
15480
+ // static anchors' link-attributes objects right after the templates.
15171
15481
  if (data.ssrSkips?.length) appendSkips(path, data.ssrSkips);
15482
+ if (data.ssrLinkAttrs?.length) appendLinkAttrs(path, data.ssrLinkAttrs);
15172
15483
 
15173
15484
  if (data.events) {
15174
15485
  path.node.body.push(
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.14",
4
+ "version": "2.0.0-rc.16",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {