@sigil-dev/compiler 0.9.7 → 0.9.10

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@sigil-dev/compiler",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.9.7",
5
+ "version": "0.9.10",
6
6
  "private": false,
7
7
  "description": "Compiler for the Sigil framework",
8
8
  "peerDependencies": {
@@ -21,6 +21,10 @@ const SSR_HELPERS = template.statements.ast(`
21
21
  if (Array.isArray(v)) return new __H(v.map(x => x?.[__SAFE] ? x.v : __e(x)).join(''));
22
22
  return new __H(String(v ?? ''));
23
23
  };
24
+ const __r = (v, tag) => {
25
+ if (v === false || v === null || v === undefined) return '';
26
+ return String(v).replace(new RegExp('</(' + tag + ')', 'gi'), '<\\\\/$1');
27
+ };
24
28
  const __b = (n, v) => {
25
29
  if (v === true) return ' ' + n;
26
30
  if (v === false || v === null || v === undefined) return '';
@@ -642,8 +642,10 @@ export function processElement(
642
642
  addScopedClass(varName, hash, statements);
643
643
  }
644
644
 
645
- // children
646
- for (const child of node.children) {
645
+ // children. <script> text is already in the server HTML, raw and unmarked
646
+ // (see ssr.ts), so hydration has nothing to claim there: the marker search
647
+ // would miss and append a second copy of the code.
648
+ for (const child of hydrate && tag === "script" ? [] : node.children) {
647
649
  if (t.isJSXElement(child)) {
648
650
  const childVar = processElement(
649
651
  child,
@@ -253,8 +253,25 @@ export function processElementSSR(
253
253
  return buildTemplate(parts);
254
254
  }
255
255
 
256
+ // <script> holds raw text: HTML escaping would corrupt the code (&& →
257
+ // &amp;&amp;), and <!--g--> markers are just text inside it, not comments,
258
+ // so there is nothing for hydration to find. __r only guards against the
259
+ // code closing its own element. (<style> never gets here: scoped style
260
+ // blocks are stripped from the source before compiling.)
261
+ const rawText = tagName === "script";
262
+
256
263
  // children
257
264
  for (const child of node.children) {
265
+ if (rawText && t.isJSXExpressionContainer(child)) {
266
+ if (t.isJSXEmptyExpression(child.expression)) continue;
267
+ parts.push(
268
+ t.callExpression(t.identifier("__r"), [
269
+ child.expression as t.Expression,
270
+ t.stringLiteral(tagName),
271
+ ]),
272
+ );
273
+ continue;
274
+ }
258
275
  if (t.isJSXText(child)) {
259
276
  const text = child.value;
260
277
  if (!text.trim()) continue;
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { transformSync } from "@babel/core";
6
+ import sigilPlugin from "../src/babel/index.ts";
7
+ import { transformSSR } from "./helpers/transform.ts";
8
+
9
+ // Real SSR output: compile, write to disk, import, render.
10
+ const dir = mkdtempSync(join(tmpdir(), "sigil-ssr-raw-"));
11
+ let n = 0;
12
+ async function render(src: string, ...args: unknown[]): Promise<string> {
13
+ const file = join(dir, `m${n++}.js`);
14
+ writeFileSync(file, transformSSR(src));
15
+ const mod = await import(file);
16
+ return String(mod.default(...args));
17
+ }
18
+
19
+ const CODE = `if (a > 1 && b < 2) x = "q";`;
20
+ // "<!-" + "-": a literal comment opener is a legacy HTML comment in JS source
21
+ const G_OPEN = "<!-" + "-g-->";
22
+ const G_CLOSE = "<!-" + "-/g-->";
23
+
24
+ describe("SSR script text", () => {
25
+ test("{code} inside a script is emitted as is", async () => {
26
+ expect(
27
+ await render("export default (c) => <script>{c}</script>;", CODE),
28
+ ).toBe(`<script>${CODE}</script>`);
29
+ });
30
+
31
+ test("the code can't close its own element", async () => {
32
+ expect(
33
+ await render(
34
+ "export default (c) => <script>{c}</script>;",
35
+ 'x = "</SCRIPT>"',
36
+ ),
37
+ ).toBe('<script>x = "<\\/SCRIPT>"</script>');
38
+ });
39
+
40
+ test("null, undefined and false render nothing", async () => {
41
+ for (const v of [null, undefined, false]) {
42
+ expect(
43
+ await render("export default (c) => <script>{c}</script>;", v),
44
+ ).toBe("<script></script>");
45
+ }
46
+ });
47
+
48
+ test("other elements still escape and mark their text", async () => {
49
+ expect(await render("export default (c) => <p>{c}</p>;", "<b>")).toBe(
50
+ `<p>${G_OPEN}&lt;b&gt;${G_CLOSE}</p>`,
51
+ );
52
+ });
53
+
54
+ test("hydration leaves script text alone", () => {
55
+ const code = transformSync(
56
+ "const el = <div><script>{code}</script><p>{text}</p></div>;",
57
+ {
58
+ plugins: [
59
+ "@babel/plugin-syntax-jsx",
60
+ [sigilPlugin, { mode: "hydrate" }],
61
+ ],
62
+ filename: "test.tsx",
63
+ },
64
+ )!.code!;
65
+ expect(code.match(/hydrateSlot\(/g)?.length).toBe(1); // only the <p> slot
66
+ expect(code).not.toContain("() => code");
67
+ });
68
+ });