@sigil-dev/compiler 0.9.7 → 0.9.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.
- package/package.json +3 -1
- package/src/babel/handlers/dom/state.ts +4 -5
- package/src/babel/index.ts +77 -24
- package/src/babel/jsx/children.ts +10 -2
- package/src/babel/jsx/element.ts +209 -112
- package/src/babel/jsx/fragment.ts +15 -3
- package/src/babel/jsx/keyed-list.ts +10 -2
- package/src/babel/jsx/ssr.ts +138 -238
- package/src/babel/jsx/utils.ts +47 -1
- package/src/babel/util/bind.ts +4 -0
- package/src/babel/util/css.ts +170 -113
- package/src/babel/util/props.ts +256 -0
- package/src/bun-plugin.ts +28 -17
- package/src/vite/index.ts +4 -1
- package/test/bun-plugin.test.ts +45 -0
- package/test/hydration.test.ts +3 -2
- package/test/jsx.test.ts +26 -4
- package/test/render-parity.test.ts +639 -0
- package/test/scoped-css.test.ts +39 -2
- package/test/ssr-raw-text.test.ts +68 -0
- package/test/ssr.test.ts +1 -1
- package/test/style-object.test.ts +29 -6
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.
|
|
5
|
+
"version": "0.9.11",
|
|
6
6
|
"private": false,
|
|
7
7
|
"description": "Compiler for the Sigil framework",
|
|
8
8
|
"peerDependencies": {
|
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
"@babel/types": "8.0.0-rc.6"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@babel/core": "8.0.0-rc.6",
|
|
26
27
|
"@babel/generator": "^7.29.7",
|
|
27
28
|
"@babel/parser": "^7.29.7",
|
|
29
|
+
"@babel/plugin-syntax-jsx": "8.0.0-rc.6",
|
|
28
30
|
"@babel/plugin-transform-typescript": "8.0.0-rc.6",
|
|
29
31
|
"@babel/preset-typescript": "8.0.0-rc.6"
|
|
30
32
|
}
|
|
@@ -9,7 +9,9 @@ export function handleState(
|
|
|
9
9
|
signals: Set<string>,
|
|
10
10
|
dev?: boolean,
|
|
11
11
|
fileKey?: string, // e.g. "src/routes/counter.tsx"
|
|
12
|
+
raw = false, // $state.raw: same rewrites, no deep proxy
|
|
12
13
|
): void {
|
|
14
|
+
const factory = raw ? MAGIC.createRawSignal : MAGIC.createSignal;
|
|
13
15
|
const { id } = declarator.node;
|
|
14
16
|
if (!t.isIdentifier(id)) throw new Error("Destructured $state not supported");
|
|
15
17
|
const name = id.name;
|
|
@@ -35,7 +37,7 @@ export function handleState(
|
|
|
35
37
|
const key = `${fileKey}#${name}`;
|
|
36
38
|
// createSignal(__hmrAdopt__("file#count", original))
|
|
37
39
|
init.replaceWith(
|
|
38
|
-
t.callExpression(t.identifier(
|
|
40
|
+
t.callExpression(t.identifier(factory), [
|
|
39
41
|
t.callExpression(t.identifier("__hmrAdopt__"), [
|
|
40
42
|
t.stringLiteral(key),
|
|
41
43
|
...(originalArgs as t.Expression[]),
|
|
@@ -53,10 +55,7 @@ export function handleState(
|
|
|
53
55
|
);
|
|
54
56
|
} else {
|
|
55
57
|
init.replaceWith(
|
|
56
|
-
t.callExpression(
|
|
57
|
-
t.identifier(MAGIC.createSignal),
|
|
58
|
-
originalArgs as t.Expression[],
|
|
59
|
-
),
|
|
58
|
+
t.callExpression(t.identifier(factory), originalArgs as t.Expression[]),
|
|
60
59
|
);
|
|
61
60
|
}
|
|
62
61
|
|
package/src/babel/index.ts
CHANGED
|
@@ -9,6 +9,13 @@ import { processElementSSR, processFragmentSSR } from "./jsx/ssr.ts";
|
|
|
9
9
|
import { warnDeadReactivity } from "./util/dead-code.ts";
|
|
10
10
|
import { getMacro } from "./util/helpers.ts";
|
|
11
11
|
import { MAGIC } from "./util/magic.ts";
|
|
12
|
+
import {
|
|
13
|
+
collectImportRoots,
|
|
14
|
+
importRoots,
|
|
15
|
+
liftDestructuredProps,
|
|
16
|
+
propObjects,
|
|
17
|
+
rewriteImportReads,
|
|
18
|
+
} from "./util/props.ts";
|
|
12
19
|
|
|
13
20
|
const SSR_HELPERS = template.statements.ast(`
|
|
14
21
|
const __SAFE = Symbol.for('sigil.safe');
|
|
@@ -21,25 +28,57 @@ const SSR_HELPERS = template.statements.ast(`
|
|
|
21
28
|
if (Array.isArray(v)) return new __H(v.map(x => x?.[__SAFE] ? x.v : __e(x)).join(''));
|
|
22
29
|
return new __H(String(v ?? ''));
|
|
23
30
|
};
|
|
24
|
-
const
|
|
25
|
-
if (v === true) return ' ' + n;
|
|
31
|
+
const __r = (v, tag) => {
|
|
26
32
|
if (v === false || v === null || v === undefined) return '';
|
|
27
|
-
return
|
|
33
|
+
return String(v).replace(new RegExp('</(' + tag + ')', 'gi'), '<\\\\/$1');
|
|
28
34
|
};
|
|
29
35
|
const __e = (v) => {
|
|
30
36
|
if (v?.[__SAFE]) return v.v;
|
|
31
37
|
if (Array.isArray(v)) return v.map(x => x?.[__SAFE] ? x.v : __e(x)).join('');
|
|
32
38
|
if (v === false || v === null || v === undefined) return '';
|
|
33
|
-
if (typeof v === 'object' && v.constructor === Object) {
|
|
34
|
-
const UNITLESS = new Set(['animationIterationCount','borderImageOutset','borderImageSlice','borderImageWidth','boxFlex','boxFlexGroup','boxSnapAfterPatch','boxSupress','columnCount','columnFlex','columnFlexGrow','columns','flexGrow','flexPositive','flexShrink','flexNegative','fontWeight','gridArea','gridRow','gridRowEnd','gridRowSpan','gridRowStart','gridColumn','gridColumnEnd','gridColumnSpan','gridColumnStart','lineClamp','lineHeight','opacity','order','orphans','tabSize','widows','zIndex','zoom','fillOpacity','floodOpacity','stopOpacity','strokeDasharray','strokeDashoffset','strokeMiterlimit','strokeOpacity','strokeWidth']);
|
|
35
|
-
return Object.entries(v).map(([k, val]) => {
|
|
36
|
-
const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
37
|
-
if (typeof val === 'number' && !UNITLESS.has(k)) return prop + ':' + val + 'px';
|
|
38
|
-
return prop + ':' + val;
|
|
39
|
-
}).join(';');
|
|
40
|
-
}
|
|
41
39
|
return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
42
40
|
};
|
|
41
|
+
// Keep in sync with cssName/cssValue/UNITLESS in @sigil-dev/runtime.
|
|
42
|
+
const __UNITLESS = new Set(['animation-iteration-count','aspect-ratio','border-image-outset','border-image-slice','border-image-width','column-count','columns','fill-opacity','flex','flex-grow','flex-shrink','flood-opacity','font-weight','grid-area','grid-column','grid-column-end','grid-column-start','grid-row','grid-row-end','grid-row-start','line-clamp','line-height','opacity','order','orphans','scale','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-miterlimit','stroke-opacity','stroke-width','tab-size','widows','z-index','zoom']);
|
|
43
|
+
const __css = (v) => {
|
|
44
|
+
if (v === null || v === undefined || v === false) return '';
|
|
45
|
+
if (typeof v !== 'object') return String(v);
|
|
46
|
+
return Object.entries(v).map(([k, val]) => {
|
|
47
|
+
let name = k.startsWith('--') ? k : k.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
|
|
48
|
+
if (name.startsWith('ms-')) name = '-' + name;
|
|
49
|
+
if (val === null || val === undefined || val === false || val === '') return '';
|
|
50
|
+
if (typeof val === 'number' && val !== 0 && !name.startsWith('--') && !__UNITLESS.has(name)) val = val + 'px';
|
|
51
|
+
return name + ':' + val;
|
|
52
|
+
}).filter(Boolean).join(';');
|
|
53
|
+
};
|
|
54
|
+
// One attribute. null/undefined drop it; booleans are presence for plain
|
|
55
|
+
// names (checked, hidden) and spelled out for dashed ones (aria-hidden).
|
|
56
|
+
const __a = (n, v) => {
|
|
57
|
+
if (v === null || v === undefined) return '';
|
|
58
|
+
if (n === 'style') { const c = __css(v); return c ? ' style="' + __e(c) + '"' : ''; }
|
|
59
|
+
if (typeof v === 'boolean') return n.includes('-') ? ' ' + n + '="' + v + '"' : v ? ' ' + n : '';
|
|
60
|
+
return ' ' + n + '="' + __e(v) + '"';
|
|
61
|
+
};
|
|
62
|
+
const __SPREAD_ATTR = { className: 'class', htmlFor: 'for' };
|
|
63
|
+
// <div {...props}>: what the client's spread() would set, as HTML.
|
|
64
|
+
const __s = (o) => {
|
|
65
|
+
if (!o) return '';
|
|
66
|
+
let out = '';
|
|
67
|
+
for (const [k, v] of Object.entries(o)) {
|
|
68
|
+
if (typeof v === 'function' || k === 'children' || k === 'key' || k === 'innerHTML') continue;
|
|
69
|
+
out += __a(__SPREAD_ATTR[k] ?? k, v);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
// <select value={v}>: mark the matching <option>s selected.
|
|
74
|
+
const __sel = (html, v) => {
|
|
75
|
+
if (v === null || v === undefined) return html;
|
|
76
|
+
const want = new Set((Array.isArray(v) ? v : [v]).map(__e));
|
|
77
|
+
return html.replace(/<option\\b[^>]*>/g, (tag) => {
|
|
78
|
+
const m = / value="([^"]*)"/.exec(tag);
|
|
79
|
+
return m && want.has(m[1]) && !/ selected[ >=]/.test(tag) ? tag.slice(0, -1) + ' selected>' : tag;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
43
82
|
`);
|
|
44
83
|
/**
|
|
45
84
|
* Recursively convert key={expr} to data-key={expr} in JSX tree.
|
|
@@ -102,6 +141,8 @@ export default function sigilPlugin(): PluginObject {
|
|
|
102
141
|
enter(path, state) {
|
|
103
142
|
signals.clear();
|
|
104
143
|
storeSignals.clear();
|
|
144
|
+
propObjects.clear();
|
|
145
|
+
importRoots.clear();
|
|
105
146
|
const opts = (state.opts || {}) as SigilOptions;
|
|
106
147
|
scopedHash = opts.hash;
|
|
107
148
|
didTransform = false;
|
|
@@ -119,9 +160,17 @@ export default function sigilPlugin(): PluginObject {
|
|
|
119
160
|
path.unshiftContainer("body", SSR_HELPERS);
|
|
120
161
|
return;
|
|
121
162
|
}
|
|
163
|
+
liftDestructuredProps(path, signals);
|
|
164
|
+
collectImportRoots(path);
|
|
165
|
+
if (rewriteImportReads(path)) didTransform = true;
|
|
122
166
|
},
|
|
123
167
|
exit(path, state) {
|
|
124
|
-
warnDeadReactivity(
|
|
168
|
+
warnDeadReactivity(
|
|
169
|
+
path,
|
|
170
|
+
signals,
|
|
171
|
+
new Set([...storeSignals, ...propObjects]),
|
|
172
|
+
state.filename,
|
|
173
|
+
);
|
|
125
174
|
|
|
126
175
|
if (asyncDefaultPath && !isSSR) {
|
|
127
176
|
let fnNode: t.FunctionDeclaration;
|
|
@@ -203,6 +252,11 @@ export default function sigilPlugin(): PluginObject {
|
|
|
203
252
|
"mountTransition",
|
|
204
253
|
"removeAnimated",
|
|
205
254
|
"setProp",
|
|
255
|
+
"setStyleProp",
|
|
256
|
+
"spread",
|
|
257
|
+
"isClaimed",
|
|
258
|
+
"liveSlot",
|
|
259
|
+
"readStore",
|
|
206
260
|
...(asyncDefaultPath ? ["__asyncBoundary"] : []),
|
|
207
261
|
];
|
|
208
262
|
const runtimeImport = path.node.body.find(
|
|
@@ -256,18 +310,17 @@ export default function sigilPlugin(): PluginObject {
|
|
|
256
310
|
: null;
|
|
257
311
|
|
|
258
312
|
if (name === "$state" || name === "$store" || name === "$state.raw") {
|
|
259
|
-
if (name === "$state.raw") {
|
|
260
|
-
if (!isSSR) {
|
|
261
|
-
const args = init.node.arguments;
|
|
262
|
-
init.replaceWith(
|
|
263
|
-
t.callExpression(t.identifier(MAGIC.createRawSignal), args),
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
313
|
isSSR
|
|
269
314
|
? handleStateSSR(path, declarator, init, signals)
|
|
270
|
-
: handleState(
|
|
315
|
+
: handleState(
|
|
316
|
+
path,
|
|
317
|
+
declarator,
|
|
318
|
+
init,
|
|
319
|
+
signals,
|
|
320
|
+
isDev,
|
|
321
|
+
filename,
|
|
322
|
+
name === "$state.raw",
|
|
323
|
+
);
|
|
271
324
|
if (name === "$store" && varName) {
|
|
272
325
|
storeSignals.add(varName);
|
|
273
326
|
}
|
|
@@ -458,7 +511,7 @@ export default function sigilPlugin(): PluginObject {
|
|
|
458
511
|
convertKeyToDataKey(path.node);
|
|
459
512
|
path.replaceWith(
|
|
460
513
|
t.callExpression(t.identifier("__h"), [
|
|
461
|
-
processElementSSR(path.node, signals),
|
|
514
|
+
processElementSSR(path.node, signals, scopedHash),
|
|
462
515
|
]),
|
|
463
516
|
);
|
|
464
517
|
} catch (e) {
|
|
@@ -516,7 +569,7 @@ export default function sigilPlugin(): PluginObject {
|
|
|
516
569
|
if (isSSR) {
|
|
517
570
|
path.replaceWith(
|
|
518
571
|
t.callExpression(t.identifier("__h"), [
|
|
519
|
-
processFragmentSSR(path.node, signals),
|
|
572
|
+
processFragmentSSR(path.node, signals, scopedHash),
|
|
520
573
|
]),
|
|
521
574
|
);
|
|
522
575
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { types as t } from "@babel/core";
|
|
2
2
|
import { processElement } from "./element";
|
|
3
3
|
import { processFragment } from "./fragment";
|
|
4
|
+
import { containsSignal, hasJSX, jsxText } from "./utils";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Collect children from a JSX element into a children expression.
|
|
@@ -29,8 +30,8 @@ export function collectChildren(
|
|
|
29
30
|
|
|
30
31
|
for (const child of node.children) {
|
|
31
32
|
if (t.isJSXText(child)) {
|
|
32
|
-
const text = child
|
|
33
|
-
if (
|
|
33
|
+
const text = jsxText(child);
|
|
34
|
+
if (text === null) continue;
|
|
34
35
|
childrenElements.push(t.stringLiteral(text));
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
@@ -43,6 +44,13 @@ export function collectChildren(
|
|
|
43
44
|
if (t.isJSXEmptyExpression(expr)) continue;
|
|
44
45
|
if (t.isJSXElement(expr) || t.isJSXFragment(expr)) {
|
|
45
46
|
jsx = expr;
|
|
47
|
+
} else if (containsSignal(expr, signals) || (hydrate && hasJSX(expr))) {
|
|
48
|
+
// <Box>{on ? <A /> : <B />}</Box>: compile it as <>{…}</> so it
|
|
49
|
+
// stays reactive (a plain value is read once, when the call is
|
|
50
|
+
// built) and, like JSX children, claims inside the component.
|
|
51
|
+
jsx = t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [
|
|
52
|
+
child,
|
|
53
|
+
]);
|
|
46
54
|
} else {
|
|
47
55
|
// Plain expression child — pass the value straight through.
|
|
48
56
|
hasOnlyText = false;
|