@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +16 -0
- package/index.js +345 -35
- package/package.json +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -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
|
-
[32mcreated [1mindex.js[22m in [
|
|
10
|
+
[32mcreated [1mindex.js[22m in [1m2.1s[22m[39m
|
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
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
|
|
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-
|
|
2187
|
-
|
|
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
|
|
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, {
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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(
|