@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +2 -2
- package/index.js +482 -99
- 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,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` / `
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2031
|
-
//
|
|
2032
|
-
|
|
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-
|
|
2156
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
4954
|
-
//
|
|
4955
|
-
//
|
|
4956
|
-
// value
|
|
4957
|
-
//
|
|
4958
|
-
//
|
|
4959
|
-
//
|
|
4960
|
-
|
|
4961
|
-
|
|
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
|
|
4992
|
-
|
|
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
|
|
4996
|
-
// string concat
|
|
4997
|
-
|
|
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
|
-
|
|
5008
|
-
|
|
5009
|
-
p.
|
|
5010
|
-
|
|
5011
|
-
|
|
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
|
|
5016
|
-
|
|
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
|
-
|
|
5033
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
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(
|