@solidjs/babel-plugin 2.0.0-rc.11 → 2.0.0-rc.14
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 +27 -0
- package/README.md +2 -2
- package/index.js +274 -83
- 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 [1m3.2s[22m[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @solidjs/babel-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-rc.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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).
|
|
8
|
+
- 3c1f809: Preserve `/* istanbul ignore … */` and `/* c8 ignore … */` block comments written as JSX children on the generated component `children` getter. Line comments are not carried.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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`.
|
|
11
|
+
- 4f67697: **Breaking:** only `on` followed by an uppercase letter (`onClick`, `onPointerDown`) is an event handler. Lowercase `on*` names (`onclick`, `onmouseover`) are plain attributes everywhere:
|
|
12
|
+
- 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)`).
|
|
13
|
+
- `@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.
|
|
14
|
+
- `@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.
|
|
15
|
+
- `@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 => …}`).
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
- 7513131: Preserve the source location of captured references in SSR props constructors so source maps point to the authored expression, including dynamic TSRX tags.
|
|
19
|
+
|
|
20
|
+
## 2.0.0-rc.13
|
|
21
|
+
|
|
22
|
+
## 2.0.0-rc.12
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- ad1ecc5: `serverComponents` (SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamic `class` or `style` now compiles to a whole-attribute `_$ssrElementAttribute` hole instead of a value inside the template's quotes, so a server component's `class={{ selected: filters.all }}` can mark the class name a client attribute slot owns; `ref`/`on*` positions compile, as before, to one guarded `_$ssrClaim` hole per element, which now emits `_s:on:*`/`_s:ref` markers for slot reads (the `_bnd` behavior-claim marker is gone). A spread element compiles its named `ref`/`on*` (`<button {...rest} onClick={row.go} ref={row.el} />`, wherever they sit relative to the spreads) to the same claim map — `{ click: expr, ref: [a, b] }`, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (`<b {...a} onClick={go} {...b}>` → `{ 1: { click: go } }`) and handed to `_$ssrElement` as a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client's `spread`/`merge()` do (the last source that has the key owns it, `undefined` included). A `ref` beside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercased `on*` remainder (`onClick` → `click`); `on:`/`oncapture:` are not 2.0 syntaxes and are no longer special-cased. Both compilers share the `attributeSlots` server-components fixture; plain SSR output is unchanged.
|
|
27
|
+
- 077c787: A `<textarea>` with a dynamic `value`/`defaultValue` no longer desyncs hydration ids for the siblings after it (#3691). The server folds the value into the textarea's text content, and that fold was taking the `_$scope` hydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like the `innerHTML`/`textContent` redirects (#3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.
|
|
28
|
+
- c71486d: DOM output: an element's `ref` now runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration (#3685).
|
|
29
|
+
|
|
3
30
|
## 2.0.0-rc.11
|
|
4
31
|
|
|
5
32
|
## 2.0.0-rc.10
|
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:
|
|
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
|
@@ -427,13 +427,38 @@ path)
|
|
|
427
427
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
const coverageIgnoreCommentData = "solid.coverageIgnoreComments";
|
|
431
|
+
|
|
432
|
+
function getCoverageIgnoreComments(path) {
|
|
433
|
+
return path.getData(coverageIgnoreCommentData);
|
|
434
|
+
}
|
|
435
|
+
|
|
430
436
|
// remove unnecessary JSX Text nodes
|
|
431
437
|
function filterChildren(children) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
438
|
+
const filtered = [];
|
|
439
|
+
let pendingCoverageIgnoreComments = [];
|
|
440
|
+
|
|
441
|
+
for (const path of children) {
|
|
442
|
+
const child = path.node;
|
|
443
|
+
if (t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) {
|
|
444
|
+
pendingCoverageIgnoreComments.push(
|
|
445
|
+
...(child.expression.innerComments?.filter(
|
|
446
|
+
(comment) =>
|
|
447
|
+
comment.type === "CommentBlock" && /^\s*(istanbul|c8)\s+ignore\b/.test(comment.value)
|
|
448
|
+
) ?? [])
|
|
449
|
+
);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (t__namespace.isJSXText(child) && /^[\r\n]\s*$/.test(child.extra?.raw ?? "")) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (pendingCoverageIgnoreComments.length) {
|
|
456
|
+
path.setData(coverageIgnoreCommentData, pendingCoverageIgnoreComments);
|
|
457
|
+
pendingCoverageIgnoreComments = [];
|
|
458
|
+
}
|
|
459
|
+
filtered.push(path);
|
|
460
|
+
}
|
|
461
|
+
return filtered;
|
|
437
462
|
}
|
|
438
463
|
|
|
439
464
|
function checkLength(children) {
|
|
@@ -461,6 +486,12 @@ function trimWhitespace(text) {
|
|
|
461
486
|
return text.replace(/\s+/g, " ");
|
|
462
487
|
}
|
|
463
488
|
|
|
489
|
+
// Only `on` + an uppercase letter is an event handler. Lowercase `on*`
|
|
490
|
+
// names (`onclick`) are plain attributes.
|
|
491
|
+
function isEventName(name) {
|
|
492
|
+
return /^on[A-Z]/.test(name);
|
|
493
|
+
}
|
|
494
|
+
|
|
464
495
|
function toEventName(name) {
|
|
465
496
|
return name.slice(2).toLowerCase();
|
|
466
497
|
}
|
|
@@ -1047,6 +1078,13 @@ isSSR)
|
|
|
1047
1078
|
child.extra = { raw: text, rawValue: text };
|
|
1048
1079
|
} else {
|
|
1049
1080
|
child = t__namespace.jsxExpressionContainer(value);
|
|
1081
|
+
// The folded value is the textarea's text content on the server, but
|
|
1082
|
+
// the client writes it as a plain `value` property effect that never
|
|
1083
|
+
// allocates a hydration id. Flag it like the innerHTML/textContent
|
|
1084
|
+
// redirects (#3015) so the SSR child pipeline skips the `_$scope` id
|
|
1085
|
+
// reservation — otherwise the reservation shifts every keyed sibling
|
|
1086
|
+
// after the textarea by one id (#3691).
|
|
1087
|
+
child._childProperty = true;
|
|
1050
1088
|
}
|
|
1051
1089
|
path.node.children = [child];
|
|
1052
1090
|
attr.remove();
|
|
@@ -2230,6 +2268,7 @@ results)
|
|
|
2230
2268
|
hasChildren = path.node.children.length > 0,
|
|
2231
2269
|
hasSpread = attributes.some((attribute) => t$1.isJSXSpreadAttribute(attribute.node)),
|
|
2232
2270
|
config = getConfig(path);
|
|
2271
|
+
const refs = [];
|
|
2233
2272
|
|
|
2234
2273
|
// preprocess spreads
|
|
2235
2274
|
if (hasSpread) {
|
|
@@ -2709,7 +2748,7 @@ results)
|
|
|
2709
2748
|
binding.kind === "const" || binding.kind === "module");
|
|
2710
2749
|
if (!isConstant && t$1.isLVal(value.expression)) {
|
|
2711
2750
|
const refIdentifier = path.scope.generateUidIdentifier("_ref$");
|
|
2712
|
-
|
|
2751
|
+
refs.push(
|
|
2713
2752
|
t$1.variableDeclaration("var", [t$1.variableDeclarator(refIdentifier, value.expression)]),
|
|
2714
2753
|
t$1.expressionStatement(
|
|
2715
2754
|
t$1.conditionalExpression(
|
|
@@ -2738,7 +2777,7 @@ results)
|
|
|
2738
2777
|
t$1.isFunction(value.expression) ||
|
|
2739
2778
|
t$1.isArrayExpression(value.expression))
|
|
2740
2779
|
{
|
|
2741
|
-
|
|
2780
|
+
refs.push(
|
|
2742
2781
|
t$1.expressionStatement(
|
|
2743
2782
|
t$1.callExpression(
|
|
2744
2783
|
registerImportMethod(path, "ref", getRendererConfig(path, "dom").moduleName),
|
|
@@ -2748,7 +2787,7 @@ results)
|
|
|
2748
2787
|
);
|
|
2749
2788
|
} else {
|
|
2750
2789
|
const refIdentifier = path.scope.generateUidIdentifier("_ref$");
|
|
2751
|
-
|
|
2790
|
+
refs.push(
|
|
2752
2791
|
t$1.variableDeclaration("var", [t$1.variableDeclarator(refIdentifier, value.expression)]),
|
|
2753
2792
|
t$1.expressionStatement(
|
|
2754
2793
|
t$1.logicalExpression(
|
|
@@ -2775,7 +2814,7 @@ results)
|
|
|
2775
2814
|
}
|
|
2776
2815
|
} else if (key === "children") {
|
|
2777
2816
|
children = value;
|
|
2778
|
-
} else if (key
|
|
2817
|
+
} else if (isEventName(key)) {
|
|
2779
2818
|
const ev = toEventName(key);
|
|
2780
2819
|
if (
|
|
2781
2820
|
config.delegateEvents && (
|
|
@@ -2978,6 +3017,9 @@ results)
|
|
|
2978
3017
|
path.node.children.push(children);
|
|
2979
3018
|
}
|
|
2980
3019
|
if (spreadExpr) results.exprs.push(...(Array.isArray(spreadExpr) ? spreadExpr : [spreadExpr]));
|
|
3020
|
+
// Refs run after the element's attributes and spread are applied, so a ref's
|
|
3021
|
+
// own writes survive client creation the way they survive hydration.
|
|
3022
|
+
results.exprs.push(...refs);
|
|
2981
3023
|
|
|
2982
3024
|
results.hasHydratableEvent = results.hasHydratableEvent || hasHydratableEvent;
|
|
2983
3025
|
}
|
|
@@ -4060,6 +4102,7 @@ function takeHoistedProps(program) {
|
|
|
4060
4102
|
|
|
4061
4103
|
|
|
4062
4104
|
|
|
4105
|
+
|
|
4063
4106
|
function keyName(node) {
|
|
4064
4107
|
if (node.computed) return undefined;
|
|
4065
4108
|
if (t__namespace.isIdentifier(node.key)) return node.key.name;
|
|
@@ -4078,6 +4121,20 @@ function insideRealFunction(path, stop) {
|
|
|
4078
4121
|
return false;
|
|
4079
4122
|
}
|
|
4080
4123
|
|
|
4124
|
+
/** Type positions are erased before the code runs: a name there (the `x` of
|
|
4125
|
+
* `typeof x`, or a type that shares a value's name) is not a value the
|
|
4126
|
+
* constructor can be handed. `TSType` covers `implements`/`extends` heritage. */
|
|
4127
|
+
function isTypePosition(path) {
|
|
4128
|
+
return (
|
|
4129
|
+
path.isTSType() ||
|
|
4130
|
+
path.isTSTypeAnnotation() ||
|
|
4131
|
+
path.isTSTypeParameterDeclaration() ||
|
|
4132
|
+
path.isTSTypeParameterInstantiation() ||
|
|
4133
|
+
path.isTSInterfaceDeclaration() ||
|
|
4134
|
+
path.isTSTypeAliasDeclaration());
|
|
4135
|
+
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4081
4138
|
/** Is `binding` initialized by the time the site constructs its props? The
|
|
4082
4139
|
* getter read it lazily; the constructor reads it now. A parameter or a
|
|
4083
4140
|
* hoisted function always is. Otherwise the declaration must precede the
|
|
@@ -4163,6 +4220,10 @@ function hoistSite(p, state) {
|
|
|
4163
4220
|
if (q.node.meta.name === "new" && !insideRealFunction(q, m)) fallback = true;
|
|
4164
4221
|
return;
|
|
4165
4222
|
}
|
|
4223
|
+
if (isTypePosition(q)) {
|
|
4224
|
+
q.skip();
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4166
4227
|
if (!q.isIdentifier()) return;
|
|
4167
4228
|
const name = q.node.name;
|
|
4168
4229
|
let write = false;
|
|
@@ -4195,7 +4256,7 @@ function hoistSite(p, state) {
|
|
|
4195
4256
|
}
|
|
4196
4257
|
let capture = captures.get(binding);
|
|
4197
4258
|
if (!capture) {
|
|
4198
|
-
capture = { binding, name, index: captures.size };
|
|
4259
|
+
capture = { binding, name, reference: q.node, index: captures.size };
|
|
4199
4260
|
captures.set(binding, capture);
|
|
4200
4261
|
}
|
|
4201
4262
|
uses.add(binding);
|
|
@@ -4217,7 +4278,9 @@ function hoistSite(p, state) {
|
|
|
4217
4278
|
for (const capture of captures.values()) {
|
|
4218
4279
|
const param = scope.generateUidIdentifier("p");
|
|
4219
4280
|
params.push(param);
|
|
4220
|
-
|
|
4281
|
+
const argument = t__namespace.identifier(capture.name);
|
|
4282
|
+
argument.loc = capture.reference.loc;
|
|
4283
|
+
args.push(argument);
|
|
4221
4284
|
body.push(
|
|
4222
4285
|
t__namespace.expressionStatement(
|
|
4223
4286
|
t__namespace.assignmentExpression(
|
|
@@ -4852,10 +4915,12 @@ info)
|
|
|
4852
4915
|
const hasChildren = path.node.children.length > 0,
|
|
4853
4916
|
attributes = normalizeAttributes(path);
|
|
4854
4917
|
let children;
|
|
4855
|
-
// Server-components
|
|
4856
|
-
// intrinsics collect here and emit as one guarded
|
|
4857
|
-
//
|
|
4858
|
-
//
|
|
4918
|
+
// Server-components handler positions: ref/on* expressions on
|
|
4919
|
+
// server-rendered intrinsics collect here and emit as one guarded
|
|
4920
|
+
// whole-attribute hole after the loop, where `ssrClaim` turns attribute-slot
|
|
4921
|
+
// reads into `_s:on:*` / `_s:ref` markers (and drops server-local
|
|
4922
|
+
// functions). Evaluation is gated on the render context's claims flag so
|
|
4923
|
+
// plain SSR never runs the expressions.
|
|
4859
4924
|
const claims = [];
|
|
4860
4925
|
|
|
4861
4926
|
attributes.forEach((attribute) => {
|
|
@@ -4897,14 +4962,11 @@ info)
|
|
|
4897
4962
|
return;
|
|
4898
4963
|
}
|
|
4899
4964
|
if (key.startsWith("prop:")) return;
|
|
4900
|
-
if (key
|
|
4901
|
-
//
|
|
4902
|
-
//
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
const pos = key.startsWith("on:") ? key.slice(3) : key.slice(2).toLowerCase();
|
|
4906
|
-
if (pos) claims.push([pos, value.expression]);
|
|
4907
|
-
}
|
|
4965
|
+
if (isEventName(key)) {
|
|
4966
|
+
// `onXxx` lowercases to the event name — the client runtime's own
|
|
4967
|
+
// derivation (`onClick` -> `click`); the position is bound under it.
|
|
4968
|
+
if (info.serverComponents)
|
|
4969
|
+
claims.push([toEventName(key), value.expression]);
|
|
4908
4970
|
return;
|
|
4909
4971
|
}
|
|
4910
4972
|
if (ChildProperties.has(key)) {
|
|
@@ -4938,6 +5000,56 @@ info)
|
|
|
4938
5000
|
checkMember: true,
|
|
4939
5001
|
checkTags: true
|
|
4940
5002
|
});
|
|
5003
|
+
// Server components (principles §9.2.3) need a whole-attribute
|
|
5004
|
+
// serializer that can emit a position marker for a binding-slot
|
|
5005
|
+
// value. The ordinary class/style serializers cannot bind slots.
|
|
5006
|
+
// Object literals stay objects (no inlining) so a slot used as a
|
|
5007
|
+
// property's value can also emit its marker.
|
|
5008
|
+
if (info.serverComponents && (key === "class" || key === "style")) {
|
|
5009
|
+
const attr = t.callExpression(registerImportMethod(path, "ssrElementAttribute"), [
|
|
5010
|
+
t.stringLiteral(key),
|
|
5011
|
+
value.expression]
|
|
5012
|
+
);
|
|
5013
|
+
results.template.push("");
|
|
5014
|
+
results.templateValues.push(
|
|
5015
|
+
isDynamicValue ?
|
|
5016
|
+
hoistExpression(path, results, t.arrowFunctionExpression([], attr), {
|
|
5017
|
+
group: true
|
|
5018
|
+
}) :
|
|
5019
|
+
attr
|
|
5020
|
+
);
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
if ((key === "class" || key === "style") && !t.isObjectExpression(value.expression)) {
|
|
5024
|
+
// The whole attribute must be absent for nullish values. Keep the
|
|
5025
|
+
// ordinary serializers: ssrElementAttribute would also bind slots
|
|
5026
|
+
// without serverComponents. Capture the value once before testing it.
|
|
5027
|
+
const id = path.scope.generateUidIdentifier("v$");
|
|
5028
|
+
const attr = t.callExpression(
|
|
5029
|
+
t.arrowFunctionExpression(
|
|
5030
|
+
[id],
|
|
5031
|
+
t.conditionalExpression(
|
|
5032
|
+
t.binaryExpression("==", t.cloneNode(id), t.nullLiteral()),
|
|
5033
|
+
t.stringLiteral(""),
|
|
5034
|
+
t.callExpression(registerImportMethod(path, "ssrAttribute"), [
|
|
5035
|
+
t.stringLiteral(key),
|
|
5036
|
+
t.callExpression(
|
|
5037
|
+
registerImportMethod(path, key === "class" ? "ssrClassName" : "ssrStyle"),
|
|
5038
|
+
[t.cloneNode(id)]
|
|
5039
|
+
)]
|
|
5040
|
+
)
|
|
5041
|
+
)
|
|
5042
|
+
),
|
|
5043
|
+
[value.expression]
|
|
5044
|
+
);
|
|
5045
|
+
results.template.push("");
|
|
5046
|
+
results.templateValues.push(
|
|
5047
|
+
isDynamicValue ?
|
|
5048
|
+
hoistExpression(path, results, t.arrowFunctionExpression([], attr), { group: true }) :
|
|
5049
|
+
attr
|
|
5050
|
+
);
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
4941
5053
|
let doEscape = true;
|
|
4942
5054
|
let isBoolean =
|
|
4943
5055
|
t.isBooleanLiteral(value) ||
|
|
@@ -4952,52 +5064,48 @@ info)
|
|
|
4952
5064
|
if (value.expression.properties.length === 0) {
|
|
4953
5065
|
return;
|
|
4954
5066
|
}
|
|
4955
|
-
const
|
|
4956
|
-
|
|
5067
|
+
const entries = value.expression.properties.filter(
|
|
5068
|
+
(p) => t.isObjectProperty(p)
|
|
5069
|
+
);
|
|
5070
|
+
// One entry is `ssrStyleProperty(name, value)`; several go to
|
|
5071
|
+
// `ssrStyleProperties`, which writes the `;` only between the
|
|
5072
|
+
// entries it writes, so a nullish one leaves no separator behind.
|
|
5073
|
+
const helper = registerImportMethod(
|
|
5074
|
+
path,
|
|
5075
|
+
entries.length === 1 ? "ssrStyleProperty" : "ssrStyleProperties"
|
|
5076
|
+
);
|
|
5077
|
+
const args = entries.flatMap((p) => {
|
|
5078
|
+
let name;
|
|
4957
5079
|
if (p.computed) {
|
|
4958
5080
|
// Computed keys are user-controlled at runtime; wrap with
|
|
4959
|
-
// `_$escape(..., true)` so
|
|
4960
|
-
// string concat
|
|
4961
|
-
|
|
4962
|
-
return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
|
|
4963
|
-
t.binaryExpression(
|
|
5081
|
+
// `_$escape(..., true)` so the style helpers can stay pure
|
|
5082
|
+
// string concat (the literal-key path is already safe).
|
|
5083
|
+
name = t.binaryExpression(
|
|
4964
5084
|
"+",
|
|
4965
|
-
t.callExpression(escape, [
|
|
5085
|
+
t.callExpression(registerImportMethod(path, "escape"), [
|
|
4966
5086
|
p.key,
|
|
4967
5087
|
t.booleanLiteral(true)]
|
|
4968
5088
|
),
|
|
4969
5089
|
t.stringLiteral(":")
|
|
4970
|
-
)
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
p.
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
)]
|
|
5090
|
+
);
|
|
5091
|
+
} else {
|
|
5092
|
+
name = t.stringLiteral(
|
|
5093
|
+
(t.isIdentifier(p.key) ?
|
|
5094
|
+
p.key.name :
|
|
5095
|
+
p.key.value) + ":"
|
|
4977
5096
|
);
|
|
4978
5097
|
}
|
|
4979
|
-
return
|
|
4980
|
-
|
|
4981
|
-
(i ? ";" : "") + (
|
|
4982
|
-
t.isIdentifier(p.key) ?
|
|
4983
|
-
p.key.name :
|
|
4984
|
-
p.key.value) +
|
|
4985
|
-
":"
|
|
4986
|
-
),
|
|
5098
|
+
return [
|
|
5099
|
+
name,
|
|
4987
5100
|
escapeExpression(
|
|
4988
5101
|
path,
|
|
4989
5102
|
p.value,
|
|
4990
5103
|
true,
|
|
4991
5104
|
true
|
|
4992
|
-
)]
|
|
4993
|
-
);
|
|
4994
|
-
});
|
|
5105
|
+
)];
|
|
4995
5106
|
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
res = t.binaryExpression("+", res, props[i]);
|
|
4999
|
-
}
|
|
5000
|
-
value.expression = res;
|
|
5107
|
+
});
|
|
5108
|
+
value.expression = t.callExpression(helper, args);
|
|
5001
5109
|
} else {
|
|
5002
5110
|
value.expression = t.callExpression(registerImportMethod(path, "ssrStyle"), [
|
|
5003
5111
|
value.expression]
|
|
@@ -5081,23 +5189,7 @@ info)
|
|
|
5081
5189
|
}
|
|
5082
5190
|
});
|
|
5083
5191
|
if (claims.length) {
|
|
5084
|
-
|
|
5085
|
-
// exempt from that pass (client semantics fire every ref), so multiple
|
|
5086
|
-
// refs merge into an array value.
|
|
5087
|
-
const byPos = new Map();
|
|
5088
|
-
for (const [pos, expr] of claims) {
|
|
5089
|
-
let list = byPos.get(pos);
|
|
5090
|
-
if (!list) byPos.set(pos, list = []);
|
|
5091
|
-
list.push(expr);
|
|
5092
|
-
}
|
|
5093
|
-
const map = t.objectExpression(
|
|
5094
|
-
[...byPos].map(([pos, exprs]) =>
|
|
5095
|
-
t.objectProperty(
|
|
5096
|
-
t.stringLiteral(pos),
|
|
5097
|
-
exprs.length === 1 ? exprs[0] : t.arrayExpression(exprs)
|
|
5098
|
-
)
|
|
5099
|
-
)
|
|
5100
|
-
);
|
|
5192
|
+
const map = claimMap(claims);
|
|
5101
5193
|
// `_$sharedConfig.context && _$sharedConfig.context.claims
|
|
5102
5194
|
// ? _$ssrClaim({...}) : ""`
|
|
5103
5195
|
// — the claims flag is only set inside a server component's render
|
|
@@ -5243,7 +5335,7 @@ results,
|
|
|
5243
5335
|
|
|
5244
5336
|
function createElement(
|
|
5245
5337
|
path,
|
|
5246
|
-
{ topLevel, hydratable })
|
|
5338
|
+
{ topLevel, hydratable, serverComponents })
|
|
5247
5339
|
{
|
|
5248
5340
|
const tagName = getTagName(path.node),
|
|
5249
5341
|
config = getConfig(path),
|
|
@@ -5296,6 +5388,21 @@ path,
|
|
|
5296
5388
|
const node = attribute.node;
|
|
5297
5389
|
return !(t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "ref");
|
|
5298
5390
|
});
|
|
5391
|
+
// Server components (principles §9.2.3): the named `ref`/`on*` attributes
|
|
5392
|
+
// of a spread element are handler positions like a template element's,
|
|
5393
|
+
// and compile to the same claim map — `{ click: expr, ref: [a, b] }`,
|
|
5394
|
+
// duplicate refs merged, a duplicate handler last-wins as the template
|
|
5395
|
+
// path strips it — keyed by the index of the source each attribute sits
|
|
5396
|
+
// before (`<b {...a} onClick={go} {...b}>` → `{ 1: { click: go } }`, the
|
|
5397
|
+
// spreads being sources 0 and 2), and handed to `ssrElement` as a thunk
|
|
5398
|
+
// it reads only inside a server component's render (the gate the template
|
|
5399
|
+
// path's `ssrClaim` guard reads), so plain SSR never evaluates the
|
|
5400
|
+
// expressions. The runtime settles each handler position in source order,
|
|
5401
|
+
// as the client's `spread(el, [a, { onClick: go }, b])` does — a spread at
|
|
5402
|
+
// that index or later that HAS the key owns it — and merges every ref.
|
|
5403
|
+
// Plain SSR output is unchanged (dropped, as a server element has no
|
|
5404
|
+
// handlers to run).
|
|
5405
|
+
const claims = [];
|
|
5299
5406
|
|
|
5300
5407
|
let props;
|
|
5301
5408
|
// Attributes written AFTER the last spread are markup, not a source: no
|
|
@@ -5316,7 +5423,13 @@ path,
|
|
|
5316
5423
|
// keys.
|
|
5317
5424
|
const tail = [];
|
|
5318
5425
|
const skipKeys = [];
|
|
5319
|
-
if (
|
|
5426
|
+
if (
|
|
5427
|
+
propAttributes.length === 1 &&
|
|
5428
|
+
t.isJSXSpreadAttribute(propAttributes[0].node) &&
|
|
5429
|
+
// A `ref` beside the lone spread is a claim under `serverComponents`;
|
|
5430
|
+
// the loop below places it.
|
|
5431
|
+
!(serverComponents && attributes.length > 1))
|
|
5432
|
+
{
|
|
5320
5433
|
props = [propAttributes[0].node.argument];
|
|
5321
5434
|
} else {
|
|
5322
5435
|
props = [];
|
|
@@ -5362,8 +5475,29 @@ path,
|
|
|
5362
5475
|
node.name.name;
|
|
5363
5476
|
|
|
5364
5477
|
if (hasChildren && key === "children") return;
|
|
5365
|
-
if (key === "ref")
|
|
5366
|
-
|
|
5478
|
+
if (key === "ref" || isEventName(key)) {
|
|
5479
|
+
if (serverComponents && t.isJSXExpressionContainer(value)) {
|
|
5480
|
+
const expression = value.expression;
|
|
5481
|
+
const pos = key === "ref" ? "ref" : toEventName(key);
|
|
5482
|
+
if (
|
|
5483
|
+
!(
|
|
5484
|
+
t.isJSXEmptyExpression(expression) ||
|
|
5485
|
+
t.isStringLiteral(expression) ||
|
|
5486
|
+
t.isNumericLiteral(expression) ||
|
|
5487
|
+
t.isBooleanLiteral(expression)))
|
|
5488
|
+
|
|
5489
|
+
{
|
|
5490
|
+
// The index of the source this attribute sits before: the
|
|
5491
|
+
// sources pushed so far, plus the running literal if it will
|
|
5492
|
+
// be pushed ahead of the next spread. The literal never
|
|
5493
|
+
// carries a handler key, so a spread's index is either below
|
|
5494
|
+
// this or at/after it — never ambiguous.
|
|
5495
|
+
claims.push([props.length + (runningObject.length ? 1 : 0), pos, expression]);
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
if (key.startsWith("prop:")) return;
|
|
5367
5501
|
if (i > lastSpread) {
|
|
5368
5502
|
const part = tailAttribute(path, tagName, key, node);
|
|
5369
5503
|
if (part !== undefined) {
|
|
@@ -5440,10 +5574,65 @@ path,
|
|
|
5440
5574
|
}
|
|
5441
5575
|
args.push(registerSkip(path, skipKeys), markup);
|
|
5442
5576
|
}
|
|
5577
|
+
if (claims.length) {
|
|
5578
|
+
if (!skipKeys.length) args.push(t.identifier("undefined"), t.identifier("undefined"));
|
|
5579
|
+
args.push(t.arrowFunctionExpression([], claimSegments(claims)));
|
|
5580
|
+
}
|
|
5443
5581
|
const exprs = [t.callExpression(registerImportMethod(path, "ssrElement"), args)];
|
|
5444
5582
|
return { exprs, template: "", declarations: [], dynamics: [], spreadElement: true };
|
|
5445
5583
|
}
|
|
5446
5584
|
|
|
5585
|
+
/**
|
|
5586
|
+
* A spread element's claim map by source index (`createElement`): `{ 1: {
|
|
5587
|
+
* click: go }, 3: { ref: el } }`. A handler position keeps its LAST named
|
|
5588
|
+
* attribute only — the template path's duplicate strip, applied here to the
|
|
5589
|
+
* claims (a later attribute wins whatever sits between, so the earlier one
|
|
5590
|
+
* is never read on either side); refs merge within a segment as `claimMap`
|
|
5591
|
+
* merges them, and across segments at the runtime.
|
|
5592
|
+
*/
|
|
5593
|
+
function claimSegments(
|
|
5594
|
+
claims)
|
|
5595
|
+
{
|
|
5596
|
+
const lastHandler = new Map();
|
|
5597
|
+
claims.forEach(([, pos], i) => {
|
|
5598
|
+
if (pos !== "ref") lastHandler.set(pos, i);
|
|
5599
|
+
});
|
|
5600
|
+
const segments = new Map();
|
|
5601
|
+
claims.forEach(([index, pos, expr], i) => {
|
|
5602
|
+
if (pos !== "ref" && lastHandler.get(pos) !== i) return;
|
|
5603
|
+
let list = segments.get(index);
|
|
5604
|
+
if (!list) segments.set(index, list = []);
|
|
5605
|
+
list.push([pos, expr]);
|
|
5606
|
+
});
|
|
5607
|
+
return t.objectExpression(
|
|
5608
|
+
[...segments].map(([index, list]) => t.objectProperty(t.numericLiteral(index), claimMap(list)))
|
|
5609
|
+
);
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
/**
|
|
5613
|
+
* The compiled claim map of an element's handler positions, `{ click: expr,
|
|
5614
|
+
* ref: [a, b] }`: duplicate event keys were last-wins-stripped by the
|
|
5615
|
+
* template path's duplicate strip (or `claimSegments`); `ref` is exempt from
|
|
5616
|
+
* that pass (client semantics fire every ref), so multiple refs merge into
|
|
5617
|
+
* an array value.
|
|
5618
|
+
*/
|
|
5619
|
+
function claimMap(claims) {
|
|
5620
|
+
const byPos = new Map();
|
|
5621
|
+
for (const [pos, expr] of claims) {
|
|
5622
|
+
let list = byPos.get(pos);
|
|
5623
|
+
if (!list) byPos.set(pos, list = []);
|
|
5624
|
+
list.push(expr);
|
|
5625
|
+
}
|
|
5626
|
+
return t.objectExpression(
|
|
5627
|
+
[...byPos].map(([pos, exprs]) =>
|
|
5628
|
+
t.objectProperty(
|
|
5629
|
+
t.stringLiteral(pos),
|
|
5630
|
+
exprs.length === 1 ? exprs[0] : t.arrayExpression(exprs)
|
|
5631
|
+
)
|
|
5632
|
+
)
|
|
5633
|
+
);
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5447
5636
|
/**
|
|
5448
5637
|
* What an attribute after an element's last spread contributes to
|
|
5449
5638
|
* `ssrElement`'s attribute string. A static is written as the template path
|
|
@@ -5926,14 +6115,14 @@ path)
|
|
|
5926
6115
|
t__namespace.isFunction(childResult[0]) ?
|
|
5927
6116
|
childResult[0].body :
|
|
5928
6117
|
childResult[0];
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
|
|
5935
|
-
)
|
|
6118
|
+
const getter = t__namespace.objectMethod(
|
|
6119
|
+
"get",
|
|
6120
|
+
t__namespace.identifier("children"),
|
|
6121
|
+
[],
|
|
6122
|
+
t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
|
|
5936
6123
|
);
|
|
6124
|
+
if (childResult[2]?.length) getter.leadingComments = childResult[2];
|
|
6125
|
+
runningObject.push(getter);
|
|
5937
6126
|
} else runningObject.push(t__namespace.objectProperty(t__namespace.identifier("children"), childResult[0]));
|
|
5938
6127
|
}
|
|
5939
6128
|
if (runningObject.length || !props.length) props.push(propsLiteral(runningObject));
|
|
@@ -5994,9 +6183,11 @@ config)
|
|
|
5994
6183
|
if (!filteredChildren.length) return;
|
|
5995
6184
|
let dynamic = false;
|
|
5996
6185
|
let pathNodes = [];
|
|
6186
|
+
let coverageIgnoreComments;
|
|
5997
6187
|
|
|
5998
6188
|
let transformedChildren = filteredChildren.reduce(
|
|
5999
6189
|
(memo, path) => {
|
|
6190
|
+
coverageIgnoreComments ||= getCoverageIgnoreComments(path);
|
|
6000
6191
|
if (t__namespace.isJSXText(path.node)) {
|
|
6001
6192
|
const v = htmlEntities.decode(trimWhitespace(path.node.extra?.raw ?? ""));
|
|
6002
6193
|
if (v.length) {
|
|
@@ -6053,7 +6244,7 @@ config)
|
|
|
6053
6244
|
transformedChildren = t__namespace.arrowFunctionExpression([], t__namespace.arrayExpression(transformedChildren));
|
|
6054
6245
|
dynamic = true;
|
|
6055
6246
|
}
|
|
6056
|
-
return [transformedChildren, dynamic];
|
|
6247
|
+
return [transformedChildren, dynamic, coverageIgnoreComments];
|
|
6057
6248
|
}
|
|
6058
6249
|
|
|
6059
6250
|
function transformFragmentChildren(
|