@solidjs/babel-plugin 2.0.0-rc.13 → 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.
@@ -7,4 +7,4 @@ src/shared/transform.ts -> src/dom/element.ts -> src/universal/element.ts -> src
7
7
  src/shared/transform.ts -> src/ssr/element.ts -> src/shared/transform.ts
8
8
  src/shared/transform.ts -> src/shared/component.ts -> src/shared/transform.ts
9
9
  src/shared/transform.ts -> src/shared/fragment.ts -> src/shared/transform.ts
10
- created index.js in 3.4s
10
+ created index.js in 3.2s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## 2.0.0-rc.13
4
21
 
5
22
  ## 2.0.0-rc.12
package/README.md CHANGED
@@ -253,7 +253,7 @@ Inline style attributes in templates when the value is a string or `Record<strin
253
253
  - Type: `boolean`
254
254
  - Default: `false`
255
255
 
256
- SSR-only: keep attribute-slot positions on intrinsic elements bindable — `ref` / `on*` compile to a guarded `ssrClaim` hole instead of dropping, and a dynamic `class` / `style` compiles to a whole-attribute `ssrElementAttribute` hole instead of a value inside template quotes.
256
+ SSR-only: keep attribute-slot positions on intrinsic elements bindable — `ref` / `onXxx` handlers compile to a guarded `ssrClaim` hole instead of dropping, and a dynamic `class` / `style` compiles to a whole-attribute `ssrElementAttribute` hole instead of a value inside template quotes.
257
257
 
258
258
  ### hoistProps
259
259
 
@@ -329,7 +329,7 @@ const Parent = () => {
329
329
 
330
330
  ### on(eventName)
331
331
 
332
- CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties.
332
+ CamelCase attributes such as `onClick` are event handlers. The compiler delegates events that bubble or compose, otherwise it uses Level 1 `on_____` properties. Only `on` followed by an uppercase letter is an event: lowercase names such as `onclick` are plain attributes, compiled (and server-rendered) like any other attribute.
333
333
 
334
334
  Pass an array to bind a value: the second item is the first argument to the handler, the event is second.
335
335
 
package/index.js CHANGED
@@ -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
- return children.filter(
433
- ({ node: child }) =>
434
- !(t__namespace.isJSXExpressionContainer(child) && t__namespace.isJSXEmptyExpression(child.expression)) && (
435
- !t__namespace.isJSXText(child) || !/^[\r\n]\s*$/.test(child.extra?.raw ?? ""))
436
- );
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
  }
@@ -2783,7 +2814,7 @@ results)
2783
2814
  }
2784
2815
  } else if (key === "children") {
2785
2816
  children = value;
2786
- } else if (key.startsWith("on")) {
2817
+ } else if (isEventName(key)) {
2787
2818
  const ev = toEventName(key);
2788
2819
  if (
2789
2820
  config.delegateEvents && (
@@ -4071,6 +4102,7 @@ function takeHoistedProps(program) {
4071
4102
 
4072
4103
 
4073
4104
 
4105
+
4074
4106
  function keyName(node) {
4075
4107
  if (node.computed) return undefined;
4076
4108
  if (t__namespace.isIdentifier(node.key)) return node.key.name;
@@ -4089,6 +4121,20 @@ function insideRealFunction(path, stop) {
4089
4121
  return false;
4090
4122
  }
4091
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
+
4092
4138
  /** Is `binding` initialized by the time the site constructs its props? The
4093
4139
  * getter read it lazily; the constructor reads it now. A parameter or a
4094
4140
  * hoisted function always is. Otherwise the declaration must precede the
@@ -4174,6 +4220,10 @@ function hoistSite(p, state) {
4174
4220
  if (q.node.meta.name === "new" && !insideRealFunction(q, m)) fallback = true;
4175
4221
  return;
4176
4222
  }
4223
+ if (isTypePosition(q)) {
4224
+ q.skip();
4225
+ return;
4226
+ }
4177
4227
  if (!q.isIdentifier()) return;
4178
4228
  const name = q.node.name;
4179
4229
  let write = false;
@@ -4206,7 +4256,7 @@ function hoistSite(p, state) {
4206
4256
  }
4207
4257
  let capture = captures.get(binding);
4208
4258
  if (!capture) {
4209
- capture = { binding, name, index: captures.size };
4259
+ capture = { binding, name, reference: q.node, index: captures.size };
4210
4260
  captures.set(binding, capture);
4211
4261
  }
4212
4262
  uses.add(binding);
@@ -4228,7 +4278,9 @@ function hoistSite(p, state) {
4228
4278
  for (const capture of captures.values()) {
4229
4279
  const param = scope.generateUidIdentifier("p");
4230
4280
  params.push(param);
4231
- args.push(t__namespace.identifier(capture.name));
4281
+ const argument = t__namespace.identifier(capture.name);
4282
+ argument.loc = capture.reference.loc;
4283
+ args.push(argument);
4232
4284
  body.push(
4233
4285
  t__namespace.expressionStatement(
4234
4286
  t__namespace.assignmentExpression(
@@ -4910,13 +4962,11 @@ info)
4910
4962
  return;
4911
4963
  }
4912
4964
  if (key.startsWith("prop:")) return;
4913
- if (key.startsWith("on")) {
4965
+ if (isEventName(key)) {
4914
4966
  // `onXxx` lowercases to the event name — the client runtime's own
4915
4967
  // derivation (`onClick` -> `click`); the position is bound under it.
4916
- if (info.serverComponents) {
4917
- const pos = key.slice(2).toLowerCase();
4918
- if (pos) claims.push([pos, value.expression]);
4919
- }
4968
+ if (info.serverComponents)
4969
+ claims.push([toEventName(key), value.expression]);
4920
4970
  return;
4921
4971
  }
4922
4972
  if (ChildProperties.has(key)) {
@@ -4950,15 +5000,11 @@ info)
4950
5000
  checkMember: true,
4951
5001
  checkTags: true
4952
5002
  });
4953
- // Server components (principles §9.2.3): a dynamic `class`/`style`
4954
- // is the one attribute shape the plain SSR output serializes INSIDE
4955
- // template quotes (`class="${ssrClassName(x)}"`), where an attribute-slot
4956
- // value read at that position — the whole value, or a name's
4957
- // condition in object form — would be stringified instead of
4958
- // bound. Under the option the whole attribute is a runtime hole,
4959
- // `ssrElementAttribute("class", x)`, whose helper emits the same
4960
- // bytes for a plain value and the position marker for a stand-in.
4961
- // Object literals stay objects (no inlining) for the same reason.
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.
4962
5008
  if (info.serverComponents && (key === "class" || key === "style")) {
4963
5009
  const attr = t.callExpression(registerImportMethod(path, "ssrElementAttribute"), [
4964
5010
  t.stringLiteral(key),
@@ -4974,6 +5020,36 @@ info)
4974
5020
  );
4975
5021
  return;
4976
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
+ }
4977
5053
  let doEscape = true;
4978
5054
  let isBoolean =
4979
5055
  t.isBooleanLiteral(value) ||
@@ -4988,52 +5064,48 @@ info)
4988
5064
  if (value.expression.properties.length === 0) {
4989
5065
  return;
4990
5066
  }
4991
- const props = value.expression.properties.flatMap((p, i) => {
4992
- if (t.isSpreadElement(p) || t.isObjectMethod(p)) return [];
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;
4993
5079
  if (p.computed) {
4994
5080
  // Computed keys are user-controlled at runtime; wrap with
4995
- // `_$escape(..., true)` so ssrStyleProperty can stay a pure
4996
- // string concat helper (literal-key path is already safe).
4997
- const escape = registerImportMethod(path, "escape");
4998
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
4999
- t.binaryExpression(
5081
+ // `_$escape(..., true)` so the style helpers can stay pure
5082
+ // string concat (the literal-key path is already safe).
5083
+ name = t.binaryExpression(
5000
5084
  "+",
5001
- t.callExpression(escape, [
5085
+ t.callExpression(registerImportMethod(path, "escape"), [
5002
5086
  p.key,
5003
5087
  t.booleanLiteral(true)]
5004
5088
  ),
5005
5089
  t.stringLiteral(":")
5006
- ),
5007
- escapeExpression(
5008
- path,
5009
- p.value,
5010
- true,
5011
- true
5012
- )]
5090
+ );
5091
+ } else {
5092
+ name = t.stringLiteral(
5093
+ (t.isIdentifier(p.key) ?
5094
+ p.key.name :
5095
+ p.key.value) + ":"
5013
5096
  );
5014
5097
  }
5015
- return t.callExpression(registerImportMethod(path, "ssrStyleProperty"), [
5016
- t.stringLiteral(
5017
- (i ? ";" : "") + (
5018
- t.isIdentifier(p.key) ?
5019
- p.key.name :
5020
- p.key.value) +
5021
- ":"
5022
- ),
5098
+ return [
5099
+ name,
5023
5100
  escapeExpression(
5024
5101
  path,
5025
5102
  p.value,
5026
5103
  true,
5027
5104
  true
5028
- )]
5029
- );
5030
- });
5105
+ )];
5031
5106
 
5032
- let res = props[0];
5033
- for (let i = 1; i < props.length; i++) {
5034
- res = t.binaryExpression("+", res, props[i]);
5035
- }
5036
- value.expression = res;
5107
+ });
5108
+ value.expression = t.callExpression(helper, args);
5037
5109
  } else {
5038
5110
  value.expression = t.callExpression(registerImportMethod(path, "ssrStyle"), [
5039
5111
  value.expression]
@@ -5403,12 +5475,11 @@ path,
5403
5475
  node.name.name;
5404
5476
 
5405
5477
  if (hasChildren && key === "children") return;
5406
- if (key === "ref" || key.startsWith("on")) {
5478
+ if (key === "ref" || isEventName(key)) {
5407
5479
  if (serverComponents && t.isJSXExpressionContainer(value)) {
5408
5480
  const expression = value.expression;
5409
- const pos = key === "ref" ? "ref" : key.slice(2).toLowerCase();
5481
+ const pos = key === "ref" ? "ref" : toEventName(key);
5410
5482
  if (
5411
- pos &&
5412
5483
  !(
5413
5484
  t.isJSXEmptyExpression(expression) ||
5414
5485
  t.isStringLiteral(expression) ||
@@ -6044,14 +6115,14 @@ path)
6044
6115
  t__namespace.isFunction(childResult[0]) ?
6045
6116
  childResult[0].body :
6046
6117
  childResult[0];
6047
- runningObject.push(
6048
- t__namespace.objectMethod(
6049
- "get",
6050
- t__namespace.identifier("children"),
6051
- [],
6052
- t__namespace.isExpression(body) ? t__namespace.blockStatement([t__namespace.returnStatement(body)]) : body
6053
- )
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
6054
6123
  );
6124
+ if (childResult[2]?.length) getter.leadingComments = childResult[2];
6125
+ runningObject.push(getter);
6055
6126
  } else runningObject.push(t__namespace.objectProperty(t__namespace.identifier("children"), childResult[0]));
6056
6127
  }
6057
6128
  if (runningObject.length || !props.length) props.push(propsLiteral(runningObject));
@@ -6112,9 +6183,11 @@ config)
6112
6183
  if (!filteredChildren.length) return;
6113
6184
  let dynamic = false;
6114
6185
  let pathNodes = [];
6186
+ let coverageIgnoreComments;
6115
6187
 
6116
6188
  let transformedChildren = filteredChildren.reduce(
6117
6189
  (memo, path) => {
6190
+ coverageIgnoreComments ||= getCoverageIgnoreComments(path);
6118
6191
  if (t__namespace.isJSXText(path.node)) {
6119
6192
  const v = htmlEntities.decode(trimWhitespace(path.node.extra?.raw ?? ""));
6120
6193
  if (v.length) {
@@ -6171,7 +6244,7 @@ config)
6171
6244
  transformedChildren = t__namespace.arrowFunctionExpression([], t__namespace.arrayExpression(transformedChildren));
6172
6245
  dynamic = true;
6173
6246
  }
6174
- return [transformedChildren, dynamic];
6247
+ return [transformedChildren, dynamic, coverageIgnoreComments];
6175
6248
  }
6176
6249
 
6177
6250
  function transformFragmentChildren(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solidjs/babel-plugin",
3
3
  "description": "Babel compiler plugin for Solid templates",
4
- "version": "2.0.0-rc.13",
4
+ "version": "2.0.0-rc.14",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {