@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 +1 -1
- package/src/babel/index.ts +4 -0
- package/src/babel/jsx/element.ts +4 -2
- package/src/babel/jsx/ssr.ts +17 -0
- package/test/ssr-raw-text.test.ts +68 -0
package/package.json
CHANGED
package/src/babel/index.ts
CHANGED
|
@@ -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 '';
|
package/src/babel/jsx/element.ts
CHANGED
|
@@ -642,8 +642,10 @@ export function processElement(
|
|
|
642
642
|
addScopedClass(varName, hash, statements);
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
// children
|
|
646
|
-
|
|
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,
|
package/src/babel/jsx/ssr.ts
CHANGED
|
@@ -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
|
+
// &&), 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}<b>${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
|
+
});
|