@solidjs/babel-plugin 2.0.0-rc.13 → 2.0.0-rc.15

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.4s
10
+ created index.js in 2.1s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @solidjs/babel-plugin
2
2
 
3
+ ## 2.0.0-rc.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 617d195: Emit element claims after the element's initial attributes are applied (#3923), at parity across both compilers.
8
+
9
+ 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.
10
+
11
+ - 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).
12
+ - 43570f8: Reapply a select's bound value when its options arrive after the value was written.
13
+ - 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).
14
+
15
+ 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.
16
+
17
+ 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.
18
+
19
+ ## 2.0.0-rc.14
20
+
21
+ ### Patch Changes
22
+
23
+ - 59fb244: SSR `hoistProps` skips TypeScript type positions when collecting captures, so on authored TSX (types parsed, stripped later) a `typeof x` query or a type named like a value no longer captures that value, or forces the literal fallback when it is reassigned (#3828).
24
+ - 3c1f809: Preserve `/* istanbul ignore … */` and `/* c8 ignore … */` block comments written as JSX children on the generated component `children` getter. Line comments are not carried.
25
+ - 98b916d: Fix compiled SSR output that renders `style=""` or `class=""` for `null` and `undefined`. Babel and the native compiler now omit these attributes on elements without a spread.
26
+ - 98d35b9: Fix computed keys in compiled SSR `style` objects (builds without `serverComponents`). A computed key's name got no `;`, so `style={{ color: "red", [k]: v }}` rendered `color:redtop:1px` and the browser dropped both declarations. Hydration keeps the server's inline style, so they stayed dropped. A nullish first value also no longer leaves a leading `;`. Both compilers now pass a multi-entry object to a new `ssrStyleProperties(name, value, …)` helper, which writes the separator only between the entries it writes; a single entry keeps `ssrStyleProperty`.
27
+ - 4f67697: **Breaking:** only `on` followed by an uppercase letter (`onClick`, `onPointerDown`) is an event handler. Lowercase `on*` names (`onclick`, `onmouseover`) are plain attributes everywhere:
28
+ - Both compilers compile `onclick={expr}` like any other attribute (`setAttribute`, reactive when `expr` is dynamic) instead of binding a delegated or native event, and SSR renders it as an escaped attribute instead of dropping it. A leftover 1.x `on:click={fn}` is likewise a plain namespaced attribute (it previously compiled to `addEventListener(":click", fn)`).
29
+ - `@solidjs/web` `spread`/`assign` set lowercase `on*` keys as attributes, the server spread walk renders them, and `useHead` applies lowercase `on*` attributes (camelCase handler names stay skipped). `ssrAttribute` escapes a function value instead of interpolating its source raw.
30
+ - `@solidjs/html` and `@solidjs/h` elements wrap a function passed to a lowercase `on*` in a getter like any other attribute; only `onXxx` and `ref` are exempt.
31
+ - `@solidjs/html` components follow `@solidjs/h`'s rule: every zero-argument function prop, `onXxx` handlers and `ref` included, is a getter, so a component handler must declare its event argument (`onClick=${e => …}`).
32
+ - New dev-only check `LOWERCASE_EVENT_ATTRIBUTE` (added to the `DiagnosticCode` union): warns once per attribute name when a function is set on a lowercase `on*` (or `on:`) attribute, naming the camelCase handler to use.
33
+
34
+ - 7513131: Preserve the source location of captured references in SSR props constructors so source maps point to the authored expression, including dynamic TSRX tags.
35
+
3
36
  ## 2.0.0-rc.13
4
37
 
5
38
  ## 2.0.0-rc.12
package/README.md CHANGED
@@ -253,7 +253,7 @@ Inline style attributes in templates when the value is a string or `Record<strin
253
253
  - Type: `boolean`
254
254
  - Default: `false`
255
255
 
256
- SSR-only: keep attribute-slot positions on intrinsic elements bindable — `ref` / `on*` 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.
256
+ SSR-only: keep attribute-slot positions on intrinsic elements bindable — `ref` / `onXxx` handlers compile to a guarded `ssrClaim` hole instead of dropping, and a dynamic `class` / `style` compiles to a whole-attribute `ssrElementAttribute` hole instead of a value inside template quotes.
257
257
 
258
258
  ### hoistProps
259
259
 
@@ -329,7 +329,7 @@ const Parent = () => {
329
329
 
330
330
  ### on(eventName)
331
331
 
332
- CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties.
332
+ CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties. Only `on` followed by an uppercase letter is an event: lowercase names such as `onclick` are plain attributes, compiled (and server-rendered) like any other attribute.
333
333
 
334
334
  Pass an array to bind a value: the second item is the first argument to the handler, the event is second.
335
335
 
package/index.js CHANGED
@@ -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",
@@ -427,13 +442,38 @@ path)
427
442
 
428
443
  }
429
444
 
445
+ const coverageIgnoreCommentData = "solid.coverageIgnoreComments";
446
+
447
+ function getCoverageIgnoreComments(path) {
448
+ return path.getData(coverageIgnoreCommentData);
449
+ }
450
+
430
451
  // remove unnecessary JSX Text nodes
431
452
  function filterChildren(children) {
432
- return children.filter(
433
- ({ node: child }) =>
434
- !(t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) && (
435
- !t__namespace.isJSXText(child) || !/^[\r\n]\s*$/.test(child.extra?.raw ?? ""))
436
- );
453
+ const filtered = [];
454
+ let pendingCoverageIgnoreComments = [];
455
+
456
+ for (const path of children) {
457
+ const child = path.node;
458
+ if (t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) {
459
+ pendingCoverageIgnoreComments.push(
460
+ ...(child.expression.innerComments?.filter(
461
+ (comment) =>
462
+ comment.type === "CommentBlock" && /^\s*(istanbul|c8)\s+ignore\b/.test(comment.value)
463
+ ) ?? [])
464
+ );
465
+ continue;
466
+ }
467
+ if (t__namespace.isJSXText(child) && /^[\r\n]\s*$/.test(child.extra?.raw ?? "")) {
468
+ continue;
469
+ }
470
+ if (pendingCoverageIgnoreComments.length) {
471
+ path.setData(coverageIgnoreCommentData, pendingCoverageIgnoreComments);
472
+ pendingCoverageIgnoreComments = [];
473
+ }
474
+ filtered.push(path);
475
+ }
476
+ return filtered;
437
477
  }
438
478
 
439
479
  function checkLength(children) {
@@ -461,6 +501,12 @@ function trimWhitespace(text) {
461
501
  return text.replace(/\s+/g, " ");
462
502
  }
463
503
 
504
+ // Only `on` + an uppercase letter is an event handler. Lowercase `on*`
505
+ // names (`onclick`) are plain attributes.
506
+ function isEventName(name) {
507
+ return /^on[A-Z]/.test(name);
508
+ }
509
+
464
510
  function toEventName(name) {
465
511
  return name.slice(2).toLowerCase();
466
512
  }
@@ -1810,6 +1856,7 @@ const t$1 = t__namespace;
1810
1856
 
1811
1857
 
1812
1858
 
1859
+
1813
1860
 
1814
1861
 
1815
1862
  function isNamedAttribute(
@@ -1946,6 +1993,7 @@ info = {})
1946
1993
  declarations: [],
1947
1994
  exprs: [],
1948
1995
  dynamics: [],
1996
+ claims: [],
1949
1997
  postExprs: [],
1950
1998
  isImportNode,
1951
1999
  isWrapped,
@@ -2023,13 +2071,25 @@ info = {})
2023
2071
  if (!info.skipId) {
2024
2072
  results.id = path.scope.generateUidIdentifier("el$");
2025
2073
  }
2026
- // Claim contract: a[href] / form[action] elements are claimed at creation
2027
- // so registered consumers (e.g. a router's link-state layer) see them.
2028
- // detectExpressions forces the id chain, so claim targets always have one.
2029
- // Emitted ahead of the attribute expressions — writes to the claimed
2030
- // attribute recheck through the runtime's setAttribute, so order stays
2031
- // correct either way, but "claim at creation" reads first.
2032
- if (results.id && isClaimTarget(path.node)) {
2074
+ // Claim contract (#3923): a[href] / form[action] elements are claimed ONCE,
2075
+ // after their initial attributes are applied, so registered consumers
2076
+ // (e.g. a router's link-state layer) see the element they will manage.
2077
+ // Decided before the attributes are lowered — processSpreads folds a
2078
+ // spread's attributes away — and emitted after: a fully static element
2079
+ // (attributes in the template, writes at creation) is claimed at creation,
2080
+ // after those writes and its refs; an element with dynamic bindings at the
2081
+ // tail of the template root's binding effect, whose first run applies
2082
+ // them (wrapDynamics); a spread element by the spread runtime after the
2083
+ // spread's first application — no compiled claim. detectExpressions forces
2084
+ // the id chain, so claim targets always have one.
2085
+ const claimTarget =
2086
+ !!results.id &&
2087
+ isClaimTarget(path.node) &&
2088
+ !path.node.openingElement.attributes.some((attr) => t$1.isJSXSpreadAttribute(attr));
2089
+ transformAttributes$1(path, results);
2090
+ if (claimTarget) {
2091
+ if (results.dynamics.length) results.claims.push(results.id);else
2092
+
2033
2093
  results.exprs.push(
2034
2094
  t$1.expressionStatement(
2035
2095
  t$1.callExpression(
@@ -2039,7 +2099,6 @@ info = {})
2039
2099
  )
2040
2100
  );
2041
2101
  }
2042
- transformAttributes$1(path, results);
2043
2102
  if (config.contextToCustomElements && (tagName === "slot" || hasCustomElement)) {
2044
2103
  contextToCustomElement(path, results);
2045
2104
  }
@@ -2081,7 +2140,7 @@ path,
2081
2140
  elem,
2082
2141
  name,
2083
2142
  value,
2084
- { dynamic, prevId, tagName, styleProperty, classProperty } = {})
2143
+ { dynamic, prevId, tagName, styleProperty, classProperty, checkableInput } = {})
2085
2144
  {
2086
2145
  // pull out namespace
2087
2146
  const config = getConfig(path);
@@ -2152,8 +2211,21 @@ value,
2152
2211
  // Locked/stateful DOM properties (value/checked/...) must go through the
2153
2212
  // runtime in hydratable builds so the hydration claim pass can adopt
2154
2213
  // pre-hydration user state instead of overwriting it (#3182). setProperty
2155
- // carries the select-microtask and input/textarea nullish special cases.
2156
- if (config.hydratable && (isLocked || namespace !== "prop")) {
2214
+ // carries the select-value retry and input/textarea nullish special cases.
2215
+ // `<select value>` always goes through it: a microtask only sees options
2216
+ // inserted in the same turn, so options that arrive later never get
2217
+ // selected (#3928). So does a non-literal value/defaultValue on an input
2218
+ // that may be a checkbox or radio: a nullish value must remove the
2219
+ // attribute so the checked box submits its native "on" (#4009, #4017),
2220
+ // and setProperty already tests the live `type` for that.
2221
+ if (
2222
+ config.hydratable && (isLocked || namespace !== "prop") ||
2223
+ name === "value" && tagName === "select" ||
2224
+ checkableInput && (
2225
+ name === "value" || name === "defaultValue") &&
2226
+ !t$1.isStringLiteral(value) &&
2227
+ !t$1.isNumericLiteral(value))
2228
+ {
2157
2229
  return t$1.callExpression(registerImportMethod(path, "setProperty"), [
2158
2230
  elem,
2159
2231
  t$1.stringLiteral(name),
@@ -2166,24 +2238,14 @@ value,
2166
2238
  t$1.memberExpression(elem, t$1.identifier(name)),
2167
2239
  value
2168
2240
  );
2169
- // handle select/options... TODO: consider other ways in the future
2170
- // TODO: there may be a race condition here
2171
- if (name === "value" && tagName === "select") {
2172
- return t$1.logicalExpression(
2173
- "||",
2174
- t$1.callExpression(t$1.identifier("queueMicrotask"), [
2175
- t$1.arrowFunctionExpression([], assignment)]
2176
- ),
2177
- assignment
2178
- );
2179
- }
2180
2241
  if (
2181
2242
  (name === "value" || name === "defaultValue") && (
2182
2243
  tagName === "input" || tagName === "textarea") &&
2183
2244
  !t$1.isStringLiteral(value) &&
2184
2245
  !t$1.isNumericLiteral(value))
2185
2246
  {
2186
- // prevents undefined on input/textarea.value/defaultValue, fallback to empty string
2247
+ // prevents undefined on text input/textarea.value/defaultValue, fallback
2248
+ // to empty string (#2957)
2187
2249
  return t$1.assignmentExpression(
2188
2250
  "=",
2189
2251
  t$1.memberExpression(elem, t$1.identifier(name)),
@@ -2224,6 +2286,35 @@ handler)
2224
2286
  return t$1.isFunction(handler);
2225
2287
  }
2226
2288
 
2289
+ /**
2290
+ * Whether an `<input>` may be a checkbox or radio, whose nullish
2291
+ * value/defaultValue removes the `value` attribute (restoring the native "on"
2292
+ * submission value) instead of clearing it to "" like a text field (#4017).
2293
+ * Only a plain `type` written as a string literal is resolved here; a missing
2294
+ * `type` is a text input. Anything else (an expression, `prop:type`,
2295
+ * `attr:type`, a spread) is unknown and treated as possibly checkable, which
2296
+ * stays correct because setProperty tests the live `type`. Mirrored by the
2297
+ * native compiler's `input_may_be_checkable` (dom/attrs.rs).
2298
+ */
2299
+ function inputMayBeCheckable(node) {
2300
+ let checkable = false;
2301
+ for (const attribute of node.openingElement.attributes) {
2302
+ if (t$1.isJSXSpreadAttribute(attribute)) return true;
2303
+ const name = attribute.name;
2304
+ if (t$1.isJSXNamespacedName(name)) {
2305
+ if (name.name.name === "type") return true;
2306
+ continue;
2307
+ }
2308
+ if (name.name !== "type") continue;
2309
+ let value = attribute.value;
2310
+ if (t$1.isJSXExpressionContainer(value)) value = value.expression;
2311
+ if (value == null) checkable = false;else
2312
+ if (t$1.isStringLiteral(value)) checkable = /^(checkbox|radio)$/i.test(value.value);else
2313
+ return true;
2314
+ }
2315
+ return checkable;
2316
+ }
2317
+
2227
2318
  function transformAttributes$1(
2228
2319
  path,
2229
2320
  results)
@@ -2236,7 +2327,8 @@ results)
2236
2327
  const tagName = getTagName(path.node),
2237
2328
  hasChildren = path.node.children.length > 0,
2238
2329
  hasSpread = attributes.some((attribute) => t$1.isJSXSpreadAttribute(attribute.node)),
2239
- config = getConfig(path);
2330
+ config = getConfig(path),
2331
+ checkableInput = tagName === "input" && inputMayBeCheckable(path.node);
2240
2332
  const refs = [];
2241
2333
 
2242
2334
  // preprocess spreads
@@ -2783,7 +2875,7 @@ results)
2783
2875
  }
2784
2876
  } else if (key === "children") {
2785
2877
  children = value;
2786
- } else if (key.startsWith("on")) {
2878
+ } else if (isEventName(key)) {
2787
2879
  const ev = toEventName(key);
2788
2880
  if (
2789
2881
  config.delegateEvents && (
@@ -2914,6 +3006,7 @@ results)
2914
3006
  key,
2915
3007
  value: value.expression,
2916
3008
  tagName,
3009
+ checkableInput,
2917
3010
  styleProperty: isStyleProperty,
2918
3011
  classProperty: isClassProperty
2919
3012
  });
@@ -2922,6 +3015,7 @@ results)
2922
3015
  t$1.expressionStatement(
2923
3016
  setAttr$1(attribute, elem, key, value.expression, {
2924
3017
  tagName,
3018
+ checkableInput,
2925
3019
  styleProperty: isStyleProperty,
2926
3020
  classProperty: isClassProperty
2927
3021
  })
@@ -2970,7 +3064,10 @@ results)
2970
3064
  {
2971
3065
  results.exprs.push(
2972
3066
  t$1.expressionStatement(
2973
- setAttr$1(attribute, elem, key, staticValue, { tagName })
3067
+ setAttr$1(attribute, elem, key, staticValue, {
3068
+ tagName,
3069
+ checkableInput
3070
+ })
2974
3071
  )
2975
3072
  );
2976
3073
  } else {
@@ -3137,6 +3234,7 @@ config)
3137
3234
  results.declarations.push(...child.declarations);
3138
3235
  results.exprs.push(...child.exprs);
3139
3236
  results.dynamics.push(...child.dynamics);
3237
+ if (child.claims) results.claims.push(...child.claims);
3140
3238
  childPostExprs.push(...(child.postExprs || []));
3141
3239
  results.hasHydratableEvent =
3142
3240
  results.hasHydratableEvent || child.hasHydratableEvent;
@@ -3481,6 +3579,23 @@ wrap)
3481
3579
  {
3482
3580
  const config = getConfig(path);
3483
3581
  if (result.id) {
3582
+ // Claim targets with dynamic bindings are claimed at the tail of the
3583
+ // binding effect's first run, under the owner the template is created
3584
+ // with: the effect's callback runs ownerless when a held mount lands, so
3585
+ // the owner is captured here, at creation, and handed to the claim.
3586
+ let ownerId;
3587
+ if (result.claims?.length) {
3588
+ ownerId = path.scope.generateUidIdentifier("o$");
3589
+ result.declarations.push(
3590
+ t__namespace.variableDeclarator(
3591
+ ownerId,
3592
+ t__namespace.callExpression(
3593
+ registerImportMethod(path, "getOwner", getRendererConfig(path, "dom").moduleName),
3594
+ []
3595
+ )
3596
+ )
3597
+ );
3598
+ }
3484
3599
  registerTemplate(path, result);
3485
3600
  const decl = result.decl;
3486
3601
  if (
@@ -3489,7 +3604,7 @@ wrap)
3489
3604
  {
3490
3605
  return decl.declarations[0].init;
3491
3606
  } else {
3492
- const dynamicsStmt = wrapDynamics$1(path, result.dynamics);
3607
+ const dynamicsStmt = wrapDynamics$1(path, result.dynamics, result.claims, ownerId);
3493
3608
  const stmts = [
3494
3609
  decl,
3495
3610
  ...result.exprs,
@@ -3631,13 +3746,31 @@ function wrapReadShallow(path, key, value) {
3631
3746
  );
3632
3747
  }
3633
3748
 
3634
- function wrapDynamics$1(path, dynamics) {
3749
+ function wrapDynamics$1(
3750
+ path,
3751
+ dynamics,
3752
+ claims = [],
3753
+ ownerId)
3754
+ {
3635
3755
  if (!dynamics.length) return;
3636
3756
  const config = getConfig(path);
3637
3757
 
3638
3758
  // dynamics are only queued when effectWrapper is configured (element.ts
3639
3759
  // guards every push), so the name is always a string here
3640
3760
  const effectWrapperId = registerImportMethod(path, config.effectWrapper, undefined);
3761
+ // The claim contract's mount claims for this root's claim targets with
3762
+ // dynamic bindings (element.ts): the tail of the effect body, so the first
3763
+ // run claims after it applied the initial attributes; `claimElement` is
3764
+ // once-per-element, so reruns cost one check. `ownerId` is the owner
3765
+ // captured at creation (createTemplate) the handlers run under.
3766
+ const claimStatements = claims.map((id) =>
3767
+ t__namespace.expressionStatement(
3768
+ t__namespace.callExpression(
3769
+ registerImportMethod(path, "claimElement", getRendererConfig(path, "dom").moduleName),
3770
+ [id, ownerId]
3771
+ )
3772
+ )
3773
+ );
3641
3774
  // `sourceNames.bindings`: the effect is named by what it writes — each
3642
3775
  // binding's `<tag>.<attribute>` as written (undoing the `prop:` the locked
3643
3776
  // DOM property pre-pass added to `value`, `checked`, …), the merged effect
@@ -3682,12 +3815,14 @@ function wrapDynamics$1(path, dynamics) {
3682
3815
  t__namespace.expressionStatement(
3683
3816
  setAttr$1(path, dynamics[0].elem, dynamics[0].key, newValue, {
3684
3817
  tagName: dynamics[0].tagName,
3818
+ checkableInput: dynamics[0].checkableInput,
3685
3819
  dynamic: true,
3686
3820
  prevId: prevValue,
3687
3821
  styleProperty: dynamics[0].styleProperty,
3688
3822
  classProperty: dynamics[0].classProperty
3689
3823
  })
3690
- )]
3824
+ ),
3825
+ ...claimStatements]
3691
3826
  )
3692
3827
  ),
3693
3828
  ...label]
@@ -3701,7 +3836,9 @@ function wrapDynamics$1(path, dynamics) {
3701
3836
  const statements = [];
3702
3837
  const properties = [];
3703
3838
 
3704
- dynamics.forEach(({ elem, key, value, tagName, styleProperty, classProperty }, index) => {
3839
+ dynamics.forEach((binding, index) => {
3840
+ const { elem, key, tagName, styleProperty, classProperty, checkableInput } = binding;
3841
+ let value = binding.value;
3705
3842
  const propIdent = t__namespace.identifier(getNumberedId(index));
3706
3843
  const propMember = t__namespace.memberExpression(prevId, propIdent);
3707
3844
  const optionalPropMember = t__namespace.optionalMemberExpression(prevId, propIdent, false, true);
@@ -3719,7 +3856,8 @@ function wrapDynamics$1(path, dynamics) {
3719
3856
  setAttr$1(path, elem, key, propIdent, {
3720
3857
  tagName,
3721
3858
  dynamic: true,
3722
- prevId: optionalPropMember
3859
+ prevId: optionalPropMember,
3860
+ checkableInput
3723
3861
  })
3724
3862
  )
3725
3863
  );
@@ -3739,7 +3877,8 @@ function wrapDynamics$1(path, dynamics) {
3739
3877
  tagName,
3740
3878
  dynamic: true,
3741
3879
  styleProperty,
3742
- classProperty
3880
+ classProperty,
3881
+ checkableInput
3743
3882
  })
3744
3883
  )
3745
3884
  )
@@ -3752,7 +3891,7 @@ function wrapDynamics$1(path, dynamics) {
3752
3891
  t__namespace.arrowFunctionExpression([], t__namespace.objectExpression(values)),
3753
3892
  t__namespace.arrowFunctionExpression(
3754
3893
  [t__namespace.objectPattern(properties.map((id) => t__namespace.objectProperty(id, id, false, true))), prevId],
3755
- t__namespace.blockStatement(statements)
3894
+ t__namespace.blockStatement([...statements, ...claimStatements])
3756
3895
  ),
3757
3896
  ...label]
3758
3897
  )
@@ -3938,6 +4077,45 @@ function appendSkips(path, skips) {
3938
4077
  );
3939
4078
  }
3940
4079
 
4080
+ /**
4081
+ * The link-attributes object of a static anchor's `ssrLinkClaim` hole
4082
+ * (ssr/element.ts `transformAttributes`): `{ href: "/about" }`, the anchor's
4083
+ * link-relevant attributes as the literals they were written as. Hoisted to
4084
+ * the module like a template — one object per distinct attribute set,
4085
+ * shared by every anchor that writes the same one — so the hole allocates
4086
+ * nothing per render.
4087
+ */
4088
+ function registerLinkAttrs(path, entries) {
4089
+ const data = path.scope.getProgramParent().data;
4090
+ const records = data.ssrLinkAttrs || (data.ssrLinkAttrs = []);
4091
+ const key = entries.map(([name, value]) => `${name}=${value}`).join("\0");
4092
+ const found = records.find((r) => r.key === key);
4093
+ if (found) return found.id;
4094
+ const id = path.scope.generateUidIdentifier("lk$");
4095
+ records.push({
4096
+ key,
4097
+ id,
4098
+ object: t__namespace.objectExpression(
4099
+ entries.map(([name, value]) => t__namespace.objectProperty(linkAttrsKey(name), t__namespace.stringLiteral(value)))
4100
+ )
4101
+ });
4102
+ return id;
4103
+ }
4104
+
4105
+ /** The key of a link-attributes object property: `href`, or `"aria-current"`. */
4106
+ function linkAttrsKey(name) {
4107
+ return t__namespace.isValidIdentifier(name) ? t__namespace.identifier(name) : t__namespace.stringLiteral(name);
4108
+ }
4109
+
4110
+ function appendLinkAttrs(path, records) {
4111
+ path.node.body.unshift(
4112
+ t__namespace.variableDeclaration(
4113
+ "var",
4114
+ records.map((r) => t__namespace.variableDeclarator(r.id, r.object))
4115
+ )
4116
+ );
4117
+ }
4118
+
3941
4119
  /**
3942
4120
  * Hoisted props shapes (SSR, #3511).
3943
4121
  *
@@ -4071,6 +4249,7 @@ function takeHoistedProps(program) {
4071
4249
 
4072
4250
 
4073
4251
 
4252
+
4074
4253
  function keyName(node) {
4075
4254
  if (node.computed) return undefined;
4076
4255
  if (t__namespace.isIdentifier(node.key)) return node.key.name;
@@ -4089,6 +4268,20 @@ function insideRealFunction(path, stop) {
4089
4268
  return false;
4090
4269
  }
4091
4270
 
4271
+ /** Type positions are erased before the code runs: a name there (the `x` of
4272
+ * `typeof x`, or a type that shares a value's name) is not a value the
4273
+ * constructor can be handed. `TSType` covers `implements`/`extends` heritage. */
4274
+ function isTypePosition(path) {
4275
+ return (
4276
+ path.isTSType() ||
4277
+ path.isTSTypeAnnotation() ||
4278
+ path.isTSTypeParameterDeclaration() ||
4279
+ path.isTSTypeParameterInstantiation() ||
4280
+ path.isTSInterfaceDeclaration() ||
4281
+ path.isTSTypeAliasDeclaration());
4282
+
4283
+ }
4284
+
4092
4285
  /** Is `binding` initialized by the time the site constructs its props? The
4093
4286
  * getter read it lazily; the constructor reads it now. A parameter or a
4094
4287
  * hoisted function always is. Otherwise the declaration must precede the
@@ -4174,6 +4367,10 @@ function hoistSite(p, state) {
4174
4367
  if (q.node.meta.name === "new" && !insideRealFunction(q, m)) fallback = true;
4175
4368
  return;
4176
4369
  }
4370
+ if (isTypePosition(q)) {
4371
+ q.skip();
4372
+ return;
4373
+ }
4177
4374
  if (!q.isIdentifier()) return;
4178
4375
  const name = q.node.name;
4179
4376
  let write = false;
@@ -4206,7 +4403,7 @@ function hoistSite(p, state) {
4206
4403
  }
4207
4404
  let capture = captures.get(binding);
4208
4405
  if (!capture) {
4209
- capture = { binding, name, index: captures.size };
4406
+ capture = { binding, name, reference: q.node, index: captures.size };
4210
4407
  captures.set(binding, capture);
4211
4408
  }
4212
4409
  uses.add(binding);
@@ -4228,7 +4425,9 @@ function hoistSite(p, state) {
4228
4425
  for (const capture of captures.values()) {
4229
4426
  const param = scope.generateUidIdentifier("p");
4230
4427
  params.push(param);
4231
- args.push(t__namespace.identifier(capture.name));
4428
+ const argument = t__namespace.identifier(capture.name);
4429
+ argument.loc = capture.reference.loc;
4430
+ args.push(argument);
4232
4431
  body.push(
4233
4432
  t__namespace.expressionStatement(
4234
4433
  t__namespace.assignmentExpression(
@@ -4853,6 +5052,93 @@ function normalizeAttributes(path) {
4853
5052
  return attributes;
4854
5053
  }
4855
5054
 
5055
+ /**
5056
+ * The compile-time value of a JSX attribute, as the SSR static branch sees
5057
+ * it: `null` for a bare attribute (`<a link>`), the literal for a string /
5058
+ * number / boolean (quoted or in a container), `undefined` when the value is
5059
+ * an expression the compiler cannot see through (dynamic, or an empty
5060
+ * container the attribute drops out for).
5061
+ */
5062
+ function staticAttributeValue(
5063
+ value)
5064
+ {
5065
+ if (value == null) return null;
5066
+ if (t.isStringLiteral(value)) return value.value;
5067
+ if (!t.isJSXExpressionContainer(value)) return undefined;
5068
+ const expression = value.expression;
5069
+ if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) return expression.value;
5070
+ if (t.isBooleanLiteral(expression)) return expression.value;
5071
+ return undefined;
5072
+ }
5073
+
5074
+ // A URL scheme at the start of an href. `http:`/`https:` (and a
5075
+ // protocol-relative `//host`) may still be this origin — only the handler,
5076
+ // which knows the request, can say — so only another scheme rules an
5077
+ // anchor out at compile time.
5078
+ const HREF_SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
5079
+
5080
+ /**
5081
+ * Whether an anchor (the template path's, no spread) gets the `ssrLinkClaim`
5082
+ * hole (solidjs/solid#3878): it has an `href`, and nothing the compiler can
5083
+ * see rules it out as the current page — the anchors a link handler would
5084
+ * answer `""` for however the request looks. A static non-empty `target`,
5085
+ * a `download`, a `rel` naming `external`, an author-written `aria-current`
5086
+ * (hers to keep), an empty href or one with a non-HTTP scheme (`mailto:`,
5087
+ * `tel:`, `javascript:` …) get no hole and no per-render call; a dynamic
5088
+ * value in any of these positions is the handler's to judge at runtime.
5089
+ * Mirrors the DOM transform's `isClaimTarget` (`a[href]`), narrowed.
5090
+ */
5091
+ function isLinkClaimCandidate(attributes) {
5092
+ let href = false;
5093
+ for (const attribute of attributes) {
5094
+ const node = attribute.node;
5095
+ if (!t.isJSXAttribute(node) || !t.isJSXIdentifier(node.name)) continue;
5096
+ const key = node.name.name;
5097
+ if (!LinkAttributes.has(key)) continue;
5098
+ if (t.isJSXExpressionContainer(node.value) && t.isJSXEmptyExpression(node.value.expression))
5099
+ continue;
5100
+ const value = staticAttributeValue(node.value);
5101
+ if (key === "href") {
5102
+ if (value === false) continue;
5103
+ href = true;
5104
+ if (value === undefined) continue;
5105
+ // bare `href`, `href=""`, `href={true}`: an empty href is no link
5106
+ if (value === null || value === true || value === "") return false;
5107
+ const scheme = typeof value === "string" ? HREF_SCHEME.exec(value) : null;
5108
+ if (scheme && scheme[1].toLowerCase() !== "http" && scheme[1].toLowerCase() !== "https")
5109
+ return false;
5110
+ continue;
5111
+ }
5112
+ // A dynamic value is the handler's to judge; `={false}` omits the attribute.
5113
+ if (value === undefined || value === false) continue;
5114
+ switch (key) {
5115
+ case "target":
5116
+ // A bare `target`, `target=""` or `target={true}` is the empty
5117
+ // target — still a link.
5118
+ if (value !== null && value !== true && value !== "") return false;
5119
+ break;
5120
+ case "rel":
5121
+ if (typeof value === "string" && value.split(/\s+/).includes("external")) return false;
5122
+ break;
5123
+ case "download":
5124
+ case "aria-current":
5125
+ // Present in any form: a download, or the author's own current marker.
5126
+ return false;
5127
+ // `link`: the explicit-links opt-in marker — the handler reads it.
5128
+ }
5129
+ }
5130
+ return href;
5131
+ }
5132
+
5133
+ /**
5134
+ * The attribute entries of a candidate anchor's `ssrLinkClaim` hole,
5135
+ * collected by `transformAttributes` as it writes each attribute: a static
5136
+ * value as the string it serializes to, a dynamic value as the function-local
5137
+ * temp its attribute hole assigns (`_lv$N`), flagged when that hole is a
5138
+ * thunk (`isDynamic`) so the link hole is one too and reads the temp after it.
5139
+ */
5140
+
5141
+
4856
5142
  function transformAttributes(
4857
5143
  path,
4858
5144
  results,
@@ -4863,6 +5149,16 @@ info)
4863
5149
  const hasChildren = path.node.children.length > 0,
4864
5150
  attributes = normalizeAttributes(path);
4865
5151
  let children;
5152
+ // Link claims (solidjs/solid#3878): a candidate anchor gets one hole after
5153
+ // its attributes — `ssrLinkClaim(attrs)` — where a render's link handler
5154
+ // writes the anchor's link state (`aria-current="page"`, `data-active`)
5155
+ // into the server HTML, and `""` otherwise. `attrs` is the anchor's
5156
+ // link-relevant attributes: a static anchor's are a hoisted object, so the
5157
+ // hole is one eager call with no allocation per render; a dynamic one's
5158
+ // join the element's attribute group and reuse the values its attribute
5159
+ // holes evaluated. The hole never takes a hydration id.
5160
+ const linkEntries =
5161
+ tagName === "a" && isLinkClaimCandidate(attributes) ? [] : null;
4866
5162
  // Server-components handler positions: ref/on* expressions on
4867
5163
  // server-rendered intrinsics collect here and emit as one guarded
4868
5164
  // whole-attribute hole after the loop, where `ssrClaim` turns attribute-slot
@@ -4910,13 +5206,11 @@ info)
4910
5206
  return;
4911
5207
  }
4912
5208
  if (key.startsWith("prop:")) return;
4913
- if (key.startsWith("on")) {
5209
+ if (isEventName(key)) {
4914
5210
  // `onXxx` lowercases to the event name — the client runtime's own
4915
5211
  // derivation (`onClick` -> `click`); the position is bound under it.
4916
- if (info.serverComponents) {
4917
- const pos = key.slice(2).toLowerCase();
4918
- if (pos) claims.push([pos, value.expression]);
4919
- }
5212
+ if (info.serverComponents)
5213
+ claims.push([toEventName(key), value.expression]);
4920
5214
  return;
4921
5215
  }
4922
5216
  if (ChildProperties.has(key)) {
@@ -4950,15 +5244,27 @@ info)
4950
5244
  checkMember: true,
4951
5245
  checkTags: true
4952
5246
  });
4953
- // Server components (principles §9.2.3): a dynamic `class`/`style`
4954
- // is the one attribute shape the plain SSR output serializes INSIDE
4955
- // template quotes (`class="${ssrClassName(x)}"`), where an attribute-slot
4956
- // value read at that position — the whole value, or a name's
4957
- // condition in object form — would be stringified instead of
4958
- // bound. Under the option the whole attribute is a runtime hole,
4959
- // `ssrElementAttribute("class", x)`, whose helper emits the same
4960
- // bytes for a plain value and the position marker for a stand-in.
4961
- // Object literals stay objects (no inlining) for the same reason.
5247
+ // A candidate anchor's dynamic link attribute: the hole evaluates the
5248
+ // raw value ONCE into a function-local temp (`_$escape(_lv$ = expr,
5249
+ // true)`) that the link hole reads after it — the handler sees the
5250
+ // value as written, not attribute-escaped. A template literal keeps
5251
+ // its inline-quoted slot: the whole value is escaped at runtime
5252
+ // instead of its parts, the same bytes (escaping is per character),
5253
+ // and a template literal is never nullish.
5254
+ let linkTemplate = false;
5255
+ if (linkEntries !== null && LinkAttributes.has(key)) {
5256
+ const expression = value.expression;
5257
+ linkTemplate = t.isTemplateLiteral(expression);
5258
+ const temp = path.scope.generateUidIdentifier("lv$");
5259
+ path.scope.push({ id: temp, kind: "var" });
5260
+ value.expression = t.assignmentExpression("=", temp, expression);
5261
+ linkEntries.push([key, t.cloneNode(temp), !!isDynamicValue]);
5262
+ }
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.
4962
5268
  if (info.serverComponents && (key === "class" || key === "style")) {
4963
5269
  const attr = t.callExpression(registerImportMethod(path, "ssrElementAttribute"), [
4964
5270
  t.stringLiteral(key),
@@ -4974,6 +5280,36 @@ info)
4974
5280
  );
4975
5281
  return;
4976
5282
  }
5283
+ 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.
5287
+ const id = path.scope.generateUidIdentifier("v$");
5288
+ const attr = t.callExpression(
5289
+ t.arrowFunctionExpression(
5290
+ [id],
5291
+ t.conditionalExpression(
5292
+ t.binaryExpression("==", t.cloneNode(id), t.nullLiteral()),
5293
+ t.stringLiteral(""),
5294
+ t.callExpression(registerImportMethod(path, "ssrAttribute"), [
5295
+ t.stringLiteral(key),
5296
+ t.callExpression(
5297
+ registerImportMethod(path, key === "class" ? "ssrClassName" : "ssrStyle"),
5298
+ [t.cloneNode(id)]
5299
+ )]
5300
+ )
5301
+ )
5302
+ ),
5303
+ [value.expression]
5304
+ );
5305
+ results.template.push("");
5306
+ results.templateValues.push(
5307
+ isDynamicValue ?
5308
+ hoistExpression(path, results, t.arrowFunctionExpression([], attr), { group: true }) :
5309
+ attr
5310
+ );
5311
+ return;
5312
+ }
4977
5313
  let doEscape = true;
4978
5314
  let isBoolean =
4979
5315
  t.isBooleanLiteral(value) ||
@@ -4988,52 +5324,48 @@ info)
4988
5324
  if (value.expression.properties.length === 0) {
4989
5325
  return;
4990
5326
  }
4991
- const props = value.expression.properties.flatMap((p, i) => {
4992
- if (t.isSpreadElement(p) || t.isObjectMethod(p)) return [];
5327
+ const entries = value.expression.properties.filter(
5328
+ (p) => t.isObjectProperty(p)
5329
+ );
5330
+ // One entry is `ssrStyleProperty(name, value)`; several go to
5331
+ // `ssrStyleProperties`, which writes the `;` only between the
5332
+ // entries it writes, so a nullish one leaves no separator behind.
5333
+ const helper = registerImportMethod(
5334
+ path,
5335
+ entries.length === 1 ? "ssrStyleProperty" : "ssrStyleProperties"
5336
+ );
5337
+ const args = entries.flatMap((p) => {
5338
+ let name;
4993
5339
  if (p.computed) {
4994
5340
  // Computed keys are user-controlled at runtime; wrap with
4995
- // `_$escape(..., true)` so ssrStyleProperty can stay a pure
4996
- // string concat helper (literal-key path is already safe).
4997
- const escape = registerImportMethod(path, "escape");
4998
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
4999
- t.binaryExpression(
5341
+ // `_$escape(..., true)` so the style helpers can stay pure
5342
+ // string concat (the literal-key path is already safe).
5343
+ name = t.binaryExpression(
5000
5344
  "+",
5001
- t.callExpression(escape, [
5345
+ t.callExpression(registerImportMethod(path, "escape"), [
5002
5346
  p.key,
5003
5347
  t.booleanLiteral(true)]
5004
5348
  ),
5005
5349
  t.stringLiteral(":")
5006
- ),
5007
- escapeExpression(
5008
- path,
5009
- p.value,
5010
- true,
5011
- true
5012
- )]
5350
+ );
5351
+ } else {
5352
+ name = t.stringLiteral(
5353
+ (t.isIdentifier(p.key) ?
5354
+ p.key.name :
5355
+ p.key.value) + ":"
5013
5356
  );
5014
5357
  }
5015
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
5016
- t.stringLiteral(
5017
- (i ? ";" : "") + (
5018
- t.isIdentifier(p.key) ?
5019
- p.key.name :
5020
- p.key.value) +
5021
- ":"
5022
- ),
5358
+ return [
5359
+ name,
5023
5360
  escapeExpression(
5024
5361
  path,
5025
5362
  p.value,
5026
5363
  true,
5027
5364
  true
5028
- )]
5029
- );
5030
- });
5365
+ )];
5031
5366
 
5032
- let res = props[0];
5033
- for (let i = 1; i < props.length; i++) {
5034
- res = t.binaryExpression("+", res, props[i]);
5035
- }
5036
- value.expression = res;
5367
+ });
5368
+ value.expression = t.callExpression(helper, args);
5037
5369
  } else {
5038
5370
  value.expression = t.callExpression(registerImportMethod(path, "ssrStyle"), [
5039
5371
  value.expression]
@@ -5069,7 +5401,7 @@ info)
5069
5401
  );
5070
5402
  const expression = value.expression;
5071
5403
 
5072
- if (!(doEscape || isBoolean) || t.isLiteral(expression)) {
5404
+ if (!(doEscape || isBoolean) || t.isLiteral(expression) || linkTemplate) {
5073
5405
  if (isBoolean) {
5074
5406
  t.isBooleanLiteral(expression) &&
5075
5407
  expression.value === true &&
@@ -5098,6 +5430,16 @@ info)
5098
5430
  const isBoolean = !!booleanLiteral;
5099
5431
  if (booleanLiteral && !booleanLiteral.value) return;
5100
5432
  appendToTemplate(results.template, ` ${key}`);
5433
+ // A candidate anchor's static link attribute travels as the string the
5434
+ // attribute carries: a bare attribute's is `""`.
5435
+ if (linkEntries !== null && LinkAttributes.has(key))
5436
+ linkEntries.push([
5437
+ key,
5438
+ !staticValue || isBoolean ?
5439
+ "" :
5440
+ String(staticValue.value),
5441
+ false]
5442
+ );
5101
5443
  if (!staticValue) return;
5102
5444
  let text = isBoolean ?
5103
5445
  "" :
@@ -5116,6 +5458,40 @@ info)
5116
5458
  );
5117
5459
  }
5118
5460
  });
5461
+ if (linkEntries !== null) {
5462
+ const ssrLinkClaim = registerImportMethod(path, "ssrLinkClaim");
5463
+ let hole;
5464
+ if (linkEntries.every(([, value]) => typeof value === "string")) {
5465
+ // Every link attribute static: `_$ssrLinkClaim(_lk$N)` over the hoisted
5466
+ // object, evaluated in argument position like a hydration key.
5467
+ hole = t.callExpression(ssrLinkClaim, [
5468
+ registerLinkAttrs(
5469
+ path,
5470
+ linkEntries.map(([key, value]) => [key, value])
5471
+ )]
5472
+ );
5473
+ } else {
5474
+ // `_$ssrLinkClaim({ href: _lv$, rel: "noopener" })`, built per render.
5475
+ // A thunk when any captured attribute hole is one — it must run after
5476
+ // them — and groupable, so it joins the element's attribute group.
5477
+ hole = t.callExpression(ssrLinkClaim, [
5478
+ t.objectExpression(
5479
+ linkEntries.map(([key, value]) =>
5480
+ t.objectProperty(
5481
+ linkAttrsKey(key),
5482
+ typeof value === "string" ? t.stringLiteral(value) : value
5483
+ )
5484
+ )
5485
+ )]
5486
+ );
5487
+ if (linkEntries.some(([,, dynamic]) => dynamic))
5488
+ hole = hoistExpression(path, results, t.arrowFunctionExpression([], hole), {
5489
+ group: true
5490
+ });
5491
+ }
5492
+ results.template.push("");
5493
+ results.templateValues.push(hole);
5494
+ }
5119
5495
  if (claims.length) {
5120
5496
  const map = claimMap(claims);
5121
5497
  // `_$sharedConfig.context && _$sharedConfig.context.claims
@@ -5403,12 +5779,11 @@ path,
5403
5779
  node.name.name;
5404
5780
 
5405
5781
  if (hasChildren && key === "children") return;
5406
- if (key === "ref" || key.startsWith("on")) {
5782
+ if (key === "ref" || isEventName(key)) {
5407
5783
  if (serverComponents && t.isJSXExpressionContainer(value)) {
5408
5784
  const expression = value.expression;
5409
- const pos = key === "ref" ? "ref" : key.slice(2).toLowerCase();
5785
+ const pos = key === "ref" ? "ref" : toEventName(key);
5410
5786
  if (
5411
- pos &&
5412
5787
  !(
5413
5788
  t.isJSXEmptyExpression(expression) ||
5414
5789
  t.isStringLiteral(expression) ||
@@ -5427,7 +5802,11 @@ path,
5427
5802
  return;
5428
5803
  }
5429
5804
  if (key.startsWith("prop:")) return;
5430
- if (i > lastSpread) {
5805
+ // An anchor's link-relevant attributes stay a source, never baked
5806
+ // markup: `ssrElement` collects them from the walk for the render's
5807
+ // link handler (solidjs/solid#3878), and a baked tail is invisible to
5808
+ // it.
5809
+ if (i > lastSpread && !(tagName === "a" && LinkAttributes.has(key))) {
5431
5810
  const part = tailAttribute(path, tagName, key, node);
5432
5811
  if (part !== undefined) {
5433
5812
  if (typeof part === "string" && typeof tail[tail.length - 1] === "string")
@@ -6044,14 +6423,14 @@ path)
6044
6423
  t__namespace.isFunction(childResult[0]) ?
6045
6424
  childResult[0].body :
6046
6425
  childResult[0];
6047
- runningObject.push(
6048
- t__namespace.objectMethod(
6049
- "get",
6050
- t__namespace.identifier("children"),
6051
- [],
6052
- t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
6053
- )
6426
+ const getter = t__namespace.objectMethod(
6427
+ "get",
6428
+ t__namespace.identifier("children"),
6429
+ [],
6430
+ t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
6054
6431
  );
6432
+ if (childResult[2]?.length) getter.leadingComments = childResult[2];
6433
+ runningObject.push(getter);
6055
6434
  } else runningObject.push(t__namespace.objectProperty(t__namespace.identifier("children"), childResult[0]));
6056
6435
  }
6057
6436
  if (runningObject.length || !props.length) props.push(propsLiteral(runningObject));
@@ -6112,9 +6491,11 @@ config)
6112
6491
  if (!filteredChildren.length) return;
6113
6492
  let dynamic = false;
6114
6493
  let pathNodes = [];
6494
+ let coverageIgnoreComments;
6115
6495
 
6116
6496
  let transformedChildren = filteredChildren.reduce(
6117
6497
  (memo, path) => {
6498
+ coverageIgnoreComments ||= getCoverageIgnoreComments(path);
6118
6499
  if (t__namespace.isJSXText(path.node)) {
6119
6500
  const v = htmlEntities.decode(trimWhitespace(path.node.extra?.raw ?? ""));
6120
6501
  if (v.length) {
@@ -6171,7 +6552,7 @@ config)
6171
6552
  transformedChildren = t__namespace.arrowFunctionExpression([], t__namespace.arrayExpression(transformedChildren));
6172
6553
  dynamic = true;
6173
6554
  }
6174
- return [transformedChildren, dynamic];
6555
+ return [transformedChildren, dynamic, coverageIgnoreComments];
6175
6556
  }
6176
6557
 
6177
6558
  function transformFragmentChildren(
@@ -15094,8 +15475,10 @@ var postprocess = (path, state) => {
15094
15475
  if (hoisted) path.node.body.unshift(...hoisted);
15095
15476
  }
15096
15477
  // Spread-element skip predicates sit between the templates and the hoisted
15097
- // props shapes (unshifted after them, before the templates unshift).
15478
+ // props shapes (unshifted after them, before the templates unshift); the
15479
+ // static anchors' link-attributes objects right after the templates.
15098
15480
  if (data.ssrSkips?.length) appendSkips(path, data.ssrSkips);
15481
+ if (data.ssrLinkAttrs?.length) appendLinkAttrs(path, data.ssrLinkAttrs);
15099
15482
 
15100
15483
  if (data.events) {
15101
15484
  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.13",
4
+ "version": "2.0.0-rc.15",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {