babel-plugin-jsx-dom-expressions 0.40.7 → 0.40.11

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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/index.js +47 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -184,6 +184,28 @@ When `true` (the default), the template output drops the whitespace between a qu
184
184
 
185
185
  Set this to `false` to emit a single space between attributes (`<svg width="12" height="13">`) so the generated HTML is strictly valid. This may also be desirable together with `omitQuotes: false` / `omitLastClosingTag: false` when targeting stricter parsers.
186
186
 
187
+ ### omitServerOnlyTemplates
188
+
189
+ - Type: `boolean`
190
+ - Default: `true`
191
+
192
+ Only applies when `hydratable` is `true`. When `true` (the default), an element marked `$ServerOnly` has no template registered, so its markup is never shipped to the client and the generated code calls `getNextElement()` with no argument. That element can then only be obtained by hydrating against server-rendered DOM.
193
+
194
+ Because `getNextElement` falls back to `template()` whenever it is not hydrating, such an element throws `TypeError: template is not a function` if it is ever re-created on the client. Hot module replacement does exactly that: it disposes the old root and re-runs the component outside of hydration, so editing any file containing `$ServerOnly` breaks the page in development.
195
+
196
+ Set this to `false` to keep registering the template. The hydration path is unchanged, since `getNextElement` prefers the hydration registry and only calls `template()` when there is nothing to hydrate. The only cost is the template markup being present in the output, which makes this suitable for development builds:
197
+
198
+ ```js
199
+ {
200
+ plugins: [
201
+ ["jsx-dom-expressions", {
202
+ hydratable: true,
203
+ omitServerOnlyTemplates: process.env.NODE_ENV === "production"
204
+ }]
205
+ ]
206
+ }
207
+ ```
208
+
187
209
  ### requireImportSource
188
210
 
189
211
  - Type: `string | false`
package/index.js CHANGED
@@ -409,6 +409,25 @@ function isComponent(tagName) {
409
409
 
410
410
  }
411
411
 
412
+ // <script> and <style> are only raw text in HTML. Inside SVG the parser decodes
413
+ // entities and treats `<` as markup, so their children still need escaping.
414
+ function isRawTextElement(path) {
415
+ const tagName = getTagName(path.node);
416
+ if (tagName !== "script" && tagName !== "style") return false;
417
+ let parent = path.parentPath,
418
+ rootTag;
419
+ while (t__namespace.isJSXElement(parent.node)) {
420
+ const parentTag = getTagName(parent.node);
421
+ if (isComponent(parentTag)) break;
422
+ if (parentTag === "foreignObject") return true;
423
+ if (parentTag === "svg") return false;
424
+ rootTag = parentTag;
425
+ parent = parent.parentPath;
426
+ }
427
+ // a top-level SVG element is parsed inside an <svg> wrapper
428
+ return !rootTag || !SVGElements.has(rootTag);
429
+ }
430
+
412
431
  function hasStaticMarker(object, path) {
413
432
  if (!object) return false;
414
433
  if (
@@ -1856,7 +1875,7 @@ function transformAttributes$2(path, results) {
1856
1875
  }
1857
1876
  } else {
1858
1877
  if (config.hydratable && key === "$ServerOnly") {
1859
- results.skipTemplate = true;
1878
+ if (config.omitServerOnlyTemplates) results.skipTemplate = true;
1860
1879
  return;
1861
1880
  }
1862
1881
  if (t__namespace.isJSXExpressionContainer(value)) value = value.expression;
@@ -1901,6 +1920,7 @@ function findLastElement(children, hydratable) {
1901
1920
  function transformChildren$2(path, results, config) {
1902
1921
  let tempPath = results.id && results.id.name,
1903
1922
  tagName = getTagName(path.node),
1923
+ doNotEscape = isRawTextElement(path),
1904
1924
  nextPlaceholder,
1905
1925
  childPostExprs = [],
1906
1926
  i = 0;
@@ -1913,6 +1933,7 @@ function transformChildren$2(path, results, config) {
1913
1933
 
1914
1934
  }
1915
1935
  const transformed = transformNode(child, {
1936
+ doNotEscape,
1916
1937
  toBeClosed: results.toBeClosed,
1917
1938
  lastElement: index === lastElement,
1918
1939
  skipId: !results.id || !detectExpressions(filteredChildren, index, config)
@@ -2542,7 +2563,7 @@ function transformElement$2(path, info) {
2542
2563
 
2543
2564
  const config = getConfig(path);
2544
2565
  const tagName = getTagName(path.node);
2545
- if (tagName === "script" || tagName === "style") path.doNotEscape = true;
2566
+ if (isRawTextElement(path)) path.doNotEscape = true;
2546
2567
 
2547
2568
  // contains spread attributes
2548
2569
  if (path.node.openingElement.attributes.some((a) => t__namespace.isJSXSpreadAttribute(a)))
@@ -2647,6 +2668,10 @@ function escapeExpression(path, expression, attr, escapeLiterals) {
2647
2668
  } else expression.body = escapeExpression(path, expression.body, attr, escapeLiterals);
2648
2669
  return expression;
2649
2670
  } else if (t__namespace.isTemplateLiteral(expression)) {
2671
+ // Interpolations are escaped recursively. The static quasis are not —
2672
+ // `url("${x}")` would otherwise leave a raw `"` inside style="..." /
2673
+ // attr="...", which HTML parses as attribute injection.
2674
+ if (attr) escapeTemplateQuasis(expression, true);
2650
2675
  expression.expressions = expression.expressions.map((e) =>
2651
2676
  escapeExpression(path, e, attr, escapeLiterals));
2652
2677
 
@@ -2689,6 +2714,18 @@ function escapeExpression(path, expression, attr, escapeLiterals) {
2689
2714
 
2690
2715
  }
2691
2716
 
2717
+ function escapeTemplateQuasis(expression, attr) {
2718
+ for (const quasi of expression.quasis) {
2719
+ const src = quasi.value.cooked != null ? quasi.value.cooked : quasi.value.raw;
2720
+ const escaped = escapeHTML(src, attr);
2721
+ if (typeof escaped !== "string" || escaped === src) continue;
2722
+ quasi.value.cooked = escaped;
2723
+ // Codegen prints `raw`. Re-escape template delimiters so `&quot;` etc.
2724
+ // stay literal text.
2725
+ quasi.value.raw = escaped.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
2726
+ }
2727
+ }
2728
+
2692
2729
  function transformToObject(attrName, attributes, selectedAttributes) {
2693
2730
  const properties = [];
2694
2731
  const existingAttribute = attributes.find((a) => a.node.name.name === attrName);
@@ -2825,8 +2862,11 @@ function transformAttributes$1(path, results, info) {
2825
2862
  let doEscape = true;
2826
2863
 
2827
2864
  if (key.startsWith("attr:")) key = key.replace("attr:", "");
2828
- if (BooleanAttributes.has(key) || key.startsWith("bool:")) {
2829
- key = key.replace("bool:", "");
2865
+ // preservesPitch is a boolean property, but not an HTML boolean attribute.
2866
+ const isBooleanAttribute =
2867
+ key !== "preservesPitch" && BooleanAttributes.has(toAttribute(key, isSVG));
2868
+ if (isBooleanAttribute || key.startsWith("bool:")) {
2869
+ key = toAttribute(key.replace("bool:", ""), isSVG);
2830
2870
  results.template.push("");
2831
2871
  const fn = t__namespace.callExpression(registerImportMethod(attribute, "ssrAttribute"), [
2832
2872
  t__namespace.stringLiteral(key),
@@ -3785,8 +3825,7 @@ function transformComponent(path) {
3785
3825
  }))
3786
3826
  {
3787
3827
  if (
3788
- config.wrapConditionals &&
3789
- config.generate !== "ssr" && (
3828
+ config.wrapConditionals && (
3790
3829
  t__namespace.isLogicalExpression(value.expression) ||
3791
3830
  t__namespace.isConditionalExpression(value.expression)))
3792
3831
  {
@@ -4053,7 +4092,7 @@ function transformNode(path, info = {}) {
4053
4092
  info.doNotEscape ?
4054
4093
  staticValue.toString() :
4055
4094
  escapeHTML(staticValue.toString()) :
4056
- trimWhitespace(node.extra.raw);
4095
+ trimWhitespace(info.doNotEscape ? node.value : node.extra.raw);
4057
4096
  if (!text.length) return null;
4058
4097
  const results = {
4059
4098
  template: text,
@@ -4310,6 +4349,7 @@ var config = {
4310
4349
  omitQuotes: true,
4311
4350
  omitAttributeSpacing: true,
4312
4351
  contextToCustomElements: false,
4352
+ omitServerOnlyTemplates: true,
4313
4353
  staticMarker: "@once",
4314
4354
  effectWrapper: "effect",
4315
4355
  memoWrapper: "memo",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "babel-plugin-jsx-dom-expressions",
3
3
  "description": "A JSX to DOM plugin that wraps expressions for fine grained change detection",
4
- "version": "0.40.7",
4
+ "version": "0.40.11",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -33,5 +33,5 @@
33
33
  "@rollup/plugin-babel": "6.0.3",
34
34
  "@types/babel__core": "^7.20.5"
35
35
  },
36
- "gitHead": "3c01eb2ac61b193d4164319befe3f1b2634549cb"
36
+ "gitHead": "3eba17b37754abaae2346c0237a4a65de802a316"
37
37
  }