@solidjs/babel-plugin 2.0.0-rc.14 → 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.2s
10
+ created index.js in 2.1s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
3
19
  ## 2.0.0-rc.14
4
20
 
5
21
  ### Patch Changes
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",
@@ -1841,6 +1856,7 @@ const t$1 = t__namespace;
1841
1856
 
1842
1857
 
1843
1858
 
1859
+
1844
1860
 
1845
1861
 
1846
1862
  function isNamedAttribute(
@@ -1977,6 +1993,7 @@ info = {})
1977
1993
  declarations: [],
1978
1994
  exprs: [],
1979
1995
  dynamics: [],
1996
+ claims: [],
1980
1997
  postExprs: [],
1981
1998
  isImportNode,
1982
1999
  isWrapped,
@@ -2054,13 +2071,25 @@ info = {})
2054
2071
  if (!info.skipId) {
2055
2072
  results.id = path.scope.generateUidIdentifier("el$");
2056
2073
  }
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)) {
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
+
2064
2093
  results.exprs.push(
2065
2094
  t$1.expressionStatement(
2066
2095
  t$1.callExpression(
@@ -2070,7 +2099,6 @@ info = {})
2070
2099
  )
2071
2100
  );
2072
2101
  }
2073
- transformAttributes$1(path, results);
2074
2102
  if (config.contextToCustomElements && (tagName === "slot" || hasCustomElement)) {
2075
2103
  contextToCustomElement(path, results);
2076
2104
  }
@@ -2112,7 +2140,7 @@ path,
2112
2140
  elem,
2113
2141
  name,
2114
2142
  value,
2115
- { dynamic, prevId, tagName, styleProperty, classProperty } = {})
2143
+ { dynamic, prevId, tagName, styleProperty, classProperty, checkableInput } = {})
2116
2144
  {
2117
2145
  // pull out namespace
2118
2146
  const config = getConfig(path);
@@ -2183,8 +2211,21 @@ value,
2183
2211
  // Locked/stateful DOM properties (value/checked/...) must go through the
2184
2212
  // runtime in hydratable builds so the hydration claim pass can adopt
2185
2213
  // 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")) {
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
+ {
2188
2229
  return t$1.callExpression(registerImportMethod(path, "setProperty"), [
2189
2230
  elem,
2190
2231
  t$1.stringLiteral(name),
@@ -2197,24 +2238,14 @@ value,
2197
2238
  t$1.memberExpression(elem, t$1.identifier(name)),
2198
2239
  value
2199
2240
  );
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
2241
  if (
2212
2242
  (name === "value" || name === "defaultValue") && (
2213
2243
  tagName === "input" || tagName === "textarea") &&
2214
2244
  !t$1.isStringLiteral(value) &&
2215
2245
  !t$1.isNumericLiteral(value))
2216
2246
  {
2217
- // 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)
2218
2249
  return t$1.assignmentExpression(
2219
2250
  "=",
2220
2251
  t$1.memberExpression(elem, t$1.identifier(name)),
@@ -2255,6 +2286,35 @@ handler)
2255
2286
  return t$1.isFunction(handler);
2256
2287
  }
2257
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
+
2258
2318
  function transformAttributes$1(
2259
2319
  path,
2260
2320
  results)
@@ -2267,7 +2327,8 @@ results)
2267
2327
  const tagName = getTagName(path.node),
2268
2328
  hasChildren = path.node.children.length > 0,
2269
2329
  hasSpread = attributes.some((attribute) => t$1.isJSXSpreadAttribute(attribute.node)),
2270
- config = getConfig(path);
2330
+ config = getConfig(path),
2331
+ checkableInput = tagName === "input" && inputMayBeCheckable(path.node);
2271
2332
  const refs = [];
2272
2333
 
2273
2334
  // preprocess spreads
@@ -2945,6 +3006,7 @@ results)
2945
3006
  key,
2946
3007
  value: value.expression,
2947
3008
  tagName,
3009
+ checkableInput,
2948
3010
  styleProperty: isStyleProperty,
2949
3011
  classProperty: isClassProperty
2950
3012
  });
@@ -2953,6 +3015,7 @@ results)
2953
3015
  t$1.expressionStatement(
2954
3016
  setAttr$1(attribute, elem, key, value.expression, {
2955
3017
  tagName,
3018
+ checkableInput,
2956
3019
  styleProperty: isStyleProperty,
2957
3020
  classProperty: isClassProperty
2958
3021
  })
@@ -3001,7 +3064,10 @@ results)
3001
3064
  {
3002
3065
  results.exprs.push(
3003
3066
  t$1.expressionStatement(
3004
- setAttr$1(attribute, elem, key, staticValue, { tagName })
3067
+ setAttr$1(attribute, elem, key, staticValue, {
3068
+ tagName,
3069
+ checkableInput
3070
+ })
3005
3071
  )
3006
3072
  );
3007
3073
  } else {
@@ -3168,6 +3234,7 @@ config)
3168
3234
  results.declarations.push(...child.declarations);
3169
3235
  results.exprs.push(...child.exprs);
3170
3236
  results.dynamics.push(...child.dynamics);
3237
+ if (child.claims) results.claims.push(...child.claims);
3171
3238
  childPostExprs.push(...(child.postExprs || []));
3172
3239
  results.hasHydratableEvent =
3173
3240
  results.hasHydratableEvent || child.hasHydratableEvent;
@@ -3512,6 +3579,23 @@ wrap)
3512
3579
  {
3513
3580
  const config = getConfig(path);
3514
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
+ }
3515
3599
  registerTemplate(path, result);
3516
3600
  const decl = result.decl;
3517
3601
  if (
@@ -3520,7 +3604,7 @@ wrap)
3520
3604
  {
3521
3605
  return decl.declarations[0].init;
3522
3606
  } else {
3523
- const dynamicsStmt = wrapDynamics$1(path, result.dynamics);
3607
+ const dynamicsStmt = wrapDynamics$1(path, result.dynamics, result.claims, ownerId);
3524
3608
  const stmts = [
3525
3609
  decl,
3526
3610
  ...result.exprs,
@@ -3662,13 +3746,31 @@ function wrapReadShallow(path, key, value) {
3662
3746
  );
3663
3747
  }
3664
3748
 
3665
- function wrapDynamics$1(path, dynamics) {
3749
+ function wrapDynamics$1(
3750
+ path,
3751
+ dynamics,
3752
+ claims = [],
3753
+ ownerId)
3754
+ {
3666
3755
  if (!dynamics.length) return;
3667
3756
  const config = getConfig(path);
3668
3757
 
3669
3758
  // dynamics are only queued when effectWrapper is configured (element.ts
3670
3759
  // guards every push), so the name is always a string here
3671
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
+ );
3672
3774
  // `sourceNames.bindings`: the effect is named by what it writes — each
3673
3775
  // binding's `<tag>.<attribute>` as written (undoing the `prop:` the locked
3674
3776
  // DOM property pre-pass added to `value`, `checked`, …), the merged effect
@@ -3713,12 +3815,14 @@ function wrapDynamics$1(path, dynamics) {
3713
3815
  t__namespace.expressionStatement(
3714
3816
  setAttr$1(path, dynamics[0].elem, dynamics[0].key, newValue, {
3715
3817
  tagName: dynamics[0].tagName,
3818
+ checkableInput: dynamics[0].checkableInput,
3716
3819
  dynamic: true,
3717
3820
  prevId: prevValue,
3718
3821
  styleProperty: dynamics[0].styleProperty,
3719
3822
  classProperty: dynamics[0].classProperty
3720
3823
  })
3721
- )]
3824
+ ),
3825
+ ...claimStatements]
3722
3826
  )
3723
3827
  ),
3724
3828
  ...label]
@@ -3732,7 +3836,9 @@ function wrapDynamics$1(path, dynamics) {
3732
3836
  const statements = [];
3733
3837
  const properties = [];
3734
3838
 
3735
- 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;
3736
3842
  const propIdent = t__namespace.identifier(getNumberedId(index));
3737
3843
  const propMember = t__namespace.memberExpression(prevId, propIdent);
3738
3844
  const optionalPropMember = t__namespace.optionalMemberExpression(prevId, propIdent, false, true);
@@ -3750,7 +3856,8 @@ function wrapDynamics$1(path, dynamics) {
3750
3856
  setAttr$1(path, elem, key, propIdent, {
3751
3857
  tagName,
3752
3858
  dynamic: true,
3753
- prevId: optionalPropMember
3859
+ prevId: optionalPropMember,
3860
+ checkableInput
3754
3861
  })
3755
3862
  )
3756
3863
  );
@@ -3770,7 +3877,8 @@ function wrapDynamics$1(path, dynamics) {
3770
3877
  tagName,
3771
3878
  dynamic: true,
3772
3879
  styleProperty,
3773
- classProperty
3880
+ classProperty,
3881
+ checkableInput
3774
3882
  })
3775
3883
  )
3776
3884
  )
@@ -3783,7 +3891,7 @@ function wrapDynamics$1(path, dynamics) {
3783
3891
  t__namespace.arrowFunctionExpression([], t__namespace.objectExpression(values)),
3784
3892
  t__namespace.arrowFunctionExpression(
3785
3893
  [t__namespace.objectPattern(properties.map((id) => t__namespace.objectProperty(id, id, false, true))), prevId],
3786
- t__namespace.blockStatement(statements)
3894
+ t__namespace.blockStatement([...statements, ...claimStatements])
3787
3895
  ),
3788
3896
  ...label]
3789
3897
  )
@@ -3969,6 +4077,45 @@ function appendSkips(path, skips) {
3969
4077
  );
3970
4078
  }
3971
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
+
3972
4119
  /**
3973
4120
  * Hoisted props shapes (SSR, #3511).
3974
4121
  *
@@ -4905,6 +5052,93 @@ function normalizeAttributes(path) {
4905
5052
  return attributes;
4906
5053
  }
4907
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
+
4908
5142
  function transformAttributes(
4909
5143
  path,
4910
5144
  results,
@@ -4915,6 +5149,16 @@ info)
4915
5149
  const hasChildren = path.node.children.length > 0,
4916
5150
  attributes = normalizeAttributes(path);
4917
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;
4918
5162
  // Server-components handler positions: ref/on* expressions on
4919
5163
  // server-rendered intrinsics collect here and emit as one guarded
4920
5164
  // whole-attribute hole after the loop, where `ssrClaim` turns attribute-slot
@@ -5000,6 +5244,22 @@ info)
5000
5244
  checkMember: true,
5001
5245
  checkTags: true
5002
5246
  });
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
+ }
5003
5263
  // Server components (principles §9.2.3) need a whole-attribute
5004
5264
  // serializer that can emit a position marker for a binding-slot
5005
5265
  // value. The ordinary class/style serializers cannot bind slots.
@@ -5141,7 +5401,7 @@ info)
5141
5401
  );
5142
5402
  const expression = value.expression;
5143
5403
 
5144
- if (!(doEscape || isBoolean) || t.isLiteral(expression)) {
5404
+ if (!(doEscape || isBoolean) || t.isLiteral(expression) || linkTemplate) {
5145
5405
  if (isBoolean) {
5146
5406
  t.isBooleanLiteral(expression) &&
5147
5407
  expression.value === true &&
@@ -5170,6 +5430,16 @@ info)
5170
5430
  const isBoolean = !!booleanLiteral;
5171
5431
  if (booleanLiteral && !booleanLiteral.value) return;
5172
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
+ );
5173
5443
  if (!staticValue) return;
5174
5444
  let text = isBoolean ?
5175
5445
  "" :
@@ -5188,6 +5458,40 @@ info)
5188
5458
  );
5189
5459
  }
5190
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
+ }
5191
5495
  if (claims.length) {
5192
5496
  const map = claimMap(claims);
5193
5497
  // `_$sharedConfig.context && _$sharedConfig.context.claims
@@ -5498,7 +5802,11 @@ path,
5498
5802
  return;
5499
5803
  }
5500
5804
  if (key.startsWith("prop:")) return;
5501
- 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))) {
5502
5810
  const part = tailAttribute(path, tagName, key, node);
5503
5811
  if (part !== undefined) {
5504
5812
  if (typeof part === "string" && typeof tail[tail.length - 1] === "string")
@@ -15167,8 +15475,10 @@ var postprocess = (path, state) => {
15167
15475
  if (hoisted) path.node.body.unshift(...hoisted);
15168
15476
  }
15169
15477
  // Spread-element skip predicates sit between the templates and the hoisted
15170
- // 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.
15171
15480
  if (data.ssrSkips?.length) appendSkips(path, data.ssrSkips);
15481
+ if (data.ssrLinkAttrs?.length) appendLinkAttrs(path, data.ssrLinkAttrs);
15172
15482
 
15173
15483
  if (data.events) {
15174
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.14",
4
+ "version": "2.0.0-rc.15",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {