@sigil-dev/compiler 0.9.10 → 0.9.12
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 +75 -26
- package/src/babel/jsx/children.ts +10 -2
- package/src/babel/jsx/element.ts +205 -110
- package/src/babel/jsx/fragment.ts +15 -3
- package/src/babel/jsx/keyed-list.ts +10 -2
- package/src/babel/jsx/ssr.ts +136 -253
- 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.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.12",
|
|
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');
|
|
@@ -25,25 +32,53 @@ const SSR_HELPERS = template.statements.ast(`
|
|
|
25
32
|
if (v === false || v === null || v === undefined) return '';
|
|
26
33
|
return String(v).replace(new RegExp('</(' + tag + ')', 'gi'), '<\\\\/$1');
|
|
27
34
|
};
|
|
28
|
-
const __b = (n, v) => {
|
|
29
|
-
if (v === true) return ' ' + n;
|
|
30
|
-
if (v === false || v === null || v === undefined) return '';
|
|
31
|
-
return ' ' + n + '="' + __e(v) + '"';
|
|
32
|
-
};
|
|
33
35
|
const __e = (v) => {
|
|
34
36
|
if (v?.[__SAFE]) return v.v;
|
|
35
37
|
if (Array.isArray(v)) return v.map(x => x?.[__SAFE] ? x.v : __e(x)).join('');
|
|
36
38
|
if (v === false || v === null || v === undefined) return '';
|
|
37
|
-
if (typeof v === 'object' && v.constructor === Object) {
|
|
38
|
-
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']);
|
|
39
|
-
return Object.entries(v).map(([k, val]) => {
|
|
40
|
-
const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
41
|
-
if (typeof val === 'number' && !UNITLESS.has(k)) return prop + ':' + val + 'px';
|
|
42
|
-
return prop + ':' + val;
|
|
43
|
-
}).join(';');
|
|
44
|
-
}
|
|
45
39
|
return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
46
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
|
+
};
|
|
47
82
|
`);
|
|
48
83
|
/**
|
|
49
84
|
* Recursively convert key={expr} to data-key={expr} in JSX tree.
|
|
@@ -106,6 +141,8 @@ export default function sigilPlugin(): PluginObject {
|
|
|
106
141
|
enter(path, state) {
|
|
107
142
|
signals.clear();
|
|
108
143
|
storeSignals.clear();
|
|
144
|
+
propObjects.clear();
|
|
145
|
+
importRoots.clear();
|
|
109
146
|
const opts = (state.opts || {}) as SigilOptions;
|
|
110
147
|
scopedHash = opts.hash;
|
|
111
148
|
didTransform = false;
|
|
@@ -123,9 +160,17 @@ export default function sigilPlugin(): PluginObject {
|
|
|
123
160
|
path.unshiftContainer("body", SSR_HELPERS);
|
|
124
161
|
return;
|
|
125
162
|
}
|
|
163
|
+
liftDestructuredProps(path, signals);
|
|
164
|
+
collectImportRoots(path);
|
|
165
|
+
if (rewriteImportReads(path)) didTransform = true;
|
|
126
166
|
},
|
|
127
167
|
exit(path, state) {
|
|
128
|
-
warnDeadReactivity(
|
|
168
|
+
warnDeadReactivity(
|
|
169
|
+
path,
|
|
170
|
+
signals,
|
|
171
|
+
new Set([...storeSignals, ...propObjects]),
|
|
172
|
+
state.filename,
|
|
173
|
+
);
|
|
129
174
|
|
|
130
175
|
if (asyncDefaultPath && !isSSR) {
|
|
131
176
|
let fnNode: t.FunctionDeclaration;
|
|
@@ -207,6 +252,11 @@ export default function sigilPlugin(): PluginObject {
|
|
|
207
252
|
"mountTransition",
|
|
208
253
|
"removeAnimated",
|
|
209
254
|
"setProp",
|
|
255
|
+
"setStyleProp",
|
|
256
|
+
"spread",
|
|
257
|
+
"isClaimed",
|
|
258
|
+
"liveSlot",
|
|
259
|
+
"readStore",
|
|
210
260
|
...(asyncDefaultPath ? ["__asyncBoundary"] : []),
|
|
211
261
|
];
|
|
212
262
|
const runtimeImport = path.node.body.find(
|
|
@@ -260,18 +310,17 @@ export default function sigilPlugin(): PluginObject {
|
|
|
260
310
|
: null;
|
|
261
311
|
|
|
262
312
|
if (name === "$state" || name === "$store" || name === "$state.raw") {
|
|
263
|
-
if (name === "$state.raw") {
|
|
264
|
-
if (!isSSR) {
|
|
265
|
-
const args = init.node.arguments;
|
|
266
|
-
init.replaceWith(
|
|
267
|
-
t.callExpression(t.identifier(MAGIC.createRawSignal), args),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
313
|
isSSR
|
|
273
314
|
? handleStateSSR(path, declarator, init, signals)
|
|
274
|
-
: handleState(
|
|
315
|
+
: handleState(
|
|
316
|
+
path,
|
|
317
|
+
declarator,
|
|
318
|
+
init,
|
|
319
|
+
signals,
|
|
320
|
+
isDev,
|
|
321
|
+
filename,
|
|
322
|
+
name === "$state.raw",
|
|
323
|
+
);
|
|
275
324
|
if (name === "$store" && varName) {
|
|
276
325
|
storeSignals.add(varName);
|
|
277
326
|
}
|
|
@@ -462,7 +511,7 @@ export default function sigilPlugin(): PluginObject {
|
|
|
462
511
|
convertKeyToDataKey(path.node);
|
|
463
512
|
path.replaceWith(
|
|
464
513
|
t.callExpression(t.identifier("__h"), [
|
|
465
|
-
processElementSSR(path.node, signals),
|
|
514
|
+
processElementSSR(path.node, signals, scopedHash),
|
|
466
515
|
]),
|
|
467
516
|
);
|
|
468
517
|
} catch (e) {
|
|
@@ -520,7 +569,7 @@ export default function sigilPlugin(): PluginObject {
|
|
|
520
569
|
if (isSSR) {
|
|
521
570
|
path.replaceWith(
|
|
522
571
|
t.callExpression(t.identifier("__h"), [
|
|
523
|
-
processFragmentSSR(path.node, signals),
|
|
572
|
+
processFragmentSSR(path.node, signals, scopedHash),
|
|
524
573
|
]),
|
|
525
574
|
);
|
|
526
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;
|