@sigil-dev/compiler 0.9.3 → 0.9.4
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 +8 -8
- package/src/babel/index.ts +5 -0
- package/src/babel/jsx/anchor-mount.ts +3 -7
- package/src/babel/jsx/element.ts +20 -1
- package/src/babel/jsx/keyed-list.ts +41 -10
- package/src/babel/jsx/ssr.ts +5 -0
- package/test/hydration.test.ts +1 -1
- package/test/jsx.test.ts +28 -0
- package/test/keyed-lists.test.ts +44 -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.4",
|
|
6
6
|
"private": false,
|
|
7
7
|
"description": "Compiler for the Sigil framework",
|
|
8
8
|
"peerDependencies": {
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
"./bun": "./src/bun-plugin.ts"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@babel/core": "
|
|
19
|
-
"@babel/plugin-syntax-jsx": "
|
|
20
|
-
"@babel/plugin-syntax-typescript": "
|
|
21
|
-
"@babel/traverse": "
|
|
22
|
-
"@babel/types": "
|
|
18
|
+
"@babel/core": "8.0.0-rc.6",
|
|
19
|
+
"@babel/plugin-syntax-jsx": "8.0.0-rc.6",
|
|
20
|
+
"@babel/plugin-syntax-typescript": "8.0.0-rc.6",
|
|
21
|
+
"@babel/traverse": "8.0.0-rc.6",
|
|
22
|
+
"@babel/types": "8.0.0-rc.6"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@babel/generator": "^7.29.7",
|
|
26
26
|
"@babel/parser": "^7.29.7",
|
|
27
|
-
"@babel/plugin-transform-typescript": "
|
|
28
|
-
"@babel/preset-typescript": "
|
|
27
|
+
"@babel/plugin-transform-typescript": "8.0.0-rc.6",
|
|
28
|
+
"@babel/preset-typescript": "8.0.0-rc.6"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/babel/index.ts
CHANGED
|
@@ -175,14 +175,19 @@ export default function sigilPlugin(): PluginObject {
|
|
|
175
175
|
"claimComment",
|
|
176
176
|
"hydrateKeyedList",
|
|
177
177
|
"insert",
|
|
178
|
+
"applyDirective",
|
|
178
179
|
"getHydrationNodes",
|
|
179
180
|
"pushHydrationNodes",
|
|
180
181
|
"popHydrationNodes",
|
|
181
182
|
"snapshot",
|
|
182
183
|
"tracking",
|
|
184
|
+
"untrack",
|
|
185
|
+
"createRoot",
|
|
183
186
|
"createEffectPre",
|
|
184
187
|
"createInspectEffect",
|
|
185
188
|
"withEffectScope",
|
|
189
|
+
"mountTransition",
|
|
190
|
+
"removeAnimated",
|
|
186
191
|
...(asyncDefaultPath ? ["__asyncBoundary"] : []),
|
|
187
192
|
];
|
|
188
193
|
const runtimeImport = path.node.body.find(
|
|
@@ -349,13 +349,9 @@ export function buildAnchorMount(
|
|
|
349
349
|
]),
|
|
350
350
|
t.identifier(prevVar),
|
|
351
351
|
t.expressionStatement(
|
|
352
|
-
t.callExpression(
|
|
353
|
-
t.
|
|
354
|
-
|
|
355
|
-
t.identifier("remove"),
|
|
356
|
-
),
|
|
357
|
-
[],
|
|
358
|
-
),
|
|
352
|
+
t.callExpression(t.identifier("removeAnimated"), [
|
|
353
|
+
t.identifier("_n"),
|
|
354
|
+
]),
|
|
359
355
|
),
|
|
360
356
|
),
|
|
361
357
|
t.expressionStatement(
|
package/src/babel/jsx/element.ts
CHANGED
|
@@ -243,7 +243,26 @@ export function processElement(
|
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
seenAttrs.add(realAttr);
|
|
246
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
(attrNamespace === "transition" ||
|
|
248
|
+
attrNamespace === "in" ||
|
|
249
|
+
attrNamespace === "out") &&
|
|
250
|
+
t.isJSXExpressionContainer(attr.value)
|
|
251
|
+
) {
|
|
252
|
+
// transition:fade={fade} / in:appear={x} / out:leave={[y, params]}
|
|
253
|
+
const expr = attr.value.expression as t.Expression;
|
|
254
|
+
statements.push(
|
|
255
|
+
t.expressionStatement(
|
|
256
|
+
t.callExpression(t.identifier("mountTransition"), [
|
|
257
|
+
t.identifier(varName),
|
|
258
|
+
expr,
|
|
259
|
+
t.stringLiteral(
|
|
260
|
+
attrNamespace as "transition" | "in" | "out",
|
|
261
|
+
),
|
|
262
|
+
]),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
} else if (attrName === "use") {
|
|
247
266
|
// use={directive} or use={[directive, params]}
|
|
248
267
|
const expr = (attr.value as t.JSXExpressionContainer)
|
|
249
268
|
.expression as t.Expression;
|
|
@@ -12,18 +12,21 @@ export type ProcessElementFn = (
|
|
|
12
12
|
) => string;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Detect pattern: signal.map(item => <Tag key={expr} />)
|
|
15
|
+
* Detect pattern: signal.map(item => <Tag key={expr} />) — or, since unkeyed
|
|
16
|
+
* maps are also reconciled (index-keyed), signal.map(item => <Tag />).
|
|
16
17
|
* Returns info needed to generate keyed list code, or null.
|
|
18
|
+
* `hasKey` is false for the unkeyed form; buildKeyedList then keys by index.
|
|
17
19
|
*/
|
|
18
20
|
export function findKeyedMapExpr(
|
|
19
21
|
expr: t.Expression,
|
|
20
22
|
signals: Set<string>,
|
|
21
23
|
): {
|
|
22
24
|
callExpr: t.CallExpression;
|
|
23
|
-
keyExpr: t.Expression;
|
|
25
|
+
keyExpr: t.Expression | null;
|
|
24
26
|
itemParam: t.Identifier;
|
|
25
27
|
jsxBody: t.JSXElement;
|
|
26
28
|
signalExpr: t.Expression;
|
|
29
|
+
hasKey: boolean;
|
|
27
30
|
} | null {
|
|
28
31
|
if (!t.isCallExpression(expr)) return null;
|
|
29
32
|
const callee = expr.callee;
|
|
@@ -49,12 +52,30 @@ export function findKeyedMapExpr(
|
|
|
49
52
|
(a) =>
|
|
50
53
|
t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "key",
|
|
51
54
|
);
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
if (
|
|
56
|
+
keyAttr &&
|
|
57
|
+
t.isJSXAttribute(keyAttr) &&
|
|
58
|
+
t.isJSXExpressionContainer(keyAttr.value)
|
|
59
|
+
) {
|
|
60
|
+
return {
|
|
61
|
+
callExpr: expr,
|
|
62
|
+
keyExpr: keyAttr.value.expression as t.Expression,
|
|
63
|
+
itemParam,
|
|
64
|
+
jsxBody: body,
|
|
65
|
+
signalExpr: obj,
|
|
66
|
+
hasKey: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
56
69
|
|
|
57
|
-
|
|
70
|
+
// Unkeyed map — still reconciled, keyed by position.
|
|
71
|
+
return {
|
|
72
|
+
callExpr: expr,
|
|
73
|
+
keyExpr: null,
|
|
74
|
+
itemParam,
|
|
75
|
+
jsxBody: body,
|
|
76
|
+
signalExpr: obj,
|
|
77
|
+
hasKey: false,
|
|
78
|
+
};
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
/**
|
|
@@ -76,6 +97,16 @@ export function buildKeyedList(
|
|
|
76
97
|
const anchorVar = genId();
|
|
77
98
|
const keyMapVar = genId();
|
|
78
99
|
|
|
100
|
+
// Key extractor: the user's key={} expression wrapped in an arrow, or
|
|
101
|
+
// index-keying for unkeyed maps. reconcile passes (item, index) so
|
|
102
|
+
// (_item, _i) => _i works.
|
|
103
|
+
const keyFn = t.arrowFunctionExpression(
|
|
104
|
+
keyed.hasKey
|
|
105
|
+
? [keyed.itemParam]
|
|
106
|
+
: [t.identifier("_item"), t.identifier("_i")],
|
|
107
|
+
keyed.keyExpr ?? t.identifier("_i"),
|
|
108
|
+
);
|
|
109
|
+
|
|
79
110
|
// Strip key prop from JSX before processing.
|
|
80
111
|
// In SSR mode, the Babel plugin already converted key → data-key.
|
|
81
112
|
// In DOM/hydrate mode, we don't need either.
|
|
@@ -119,7 +150,7 @@ export function buildKeyedList(
|
|
|
119
150
|
const hydrateArgs: t.Expression[] = [
|
|
120
151
|
t.identifier(nodesVar ?? "__nodes"),
|
|
121
152
|
keyed.signalExpr,
|
|
122
|
-
|
|
153
|
+
keyFn,
|
|
123
154
|
t.arrowFunctionExpression(
|
|
124
155
|
[keyed.itemParam],
|
|
125
156
|
t.blockStatement([
|
|
@@ -170,7 +201,7 @@ export function buildKeyedList(
|
|
|
170
201
|
t.identifier(anchorVar),
|
|
171
202
|
t.identifier(keyMapVar),
|
|
172
203
|
keyed.signalExpr,
|
|
173
|
-
|
|
204
|
+
keyFn,
|
|
174
205
|
t.arrowFunctionExpression(
|
|
175
206
|
[keyed.itemParam],
|
|
176
207
|
t.blockStatement([
|
|
@@ -260,7 +291,7 @@ export function buildKeyedList(
|
|
|
260
291
|
t.identifier(anchorVar),
|
|
261
292
|
t.identifier(keyMapVar),
|
|
262
293
|
keyed.signalExpr,
|
|
263
|
-
|
|
294
|
+
keyFn,
|
|
264
295
|
t.arrowFunctionExpression(
|
|
265
296
|
[keyed.itemParam],
|
|
266
297
|
t.blockStatement([
|
package/src/babel/jsx/ssr.ts
CHANGED
|
@@ -137,6 +137,11 @@ export function processElementSSR(
|
|
|
137
137
|
// attributes
|
|
138
138
|
for (const attr of node.openingElement.attributes) {
|
|
139
139
|
if (!t.isJSXAttribute(attr)) continue;
|
|
140
|
+
|
|
141
|
+
// namespaced attributes are client-only directives
|
|
142
|
+
// (transition:/in:/out:/bind:) — SSR drops them entirely
|
|
143
|
+
if (t.isJSXNamespacedName(attr.name)) continue;
|
|
144
|
+
|
|
140
145
|
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
141
146
|
|
|
142
147
|
if (attrName === "innerHTML") {
|
package/test/hydration.test.ts
CHANGED
|
@@ -112,7 +112,7 @@ describe("Hydration", () => {
|
|
|
112
112
|
test("imports claim and claimText from @sigil-dev/runtime", () => {
|
|
113
113
|
const code = transform(`const el = <div></div>`, { mode: "hydrate" });
|
|
114
114
|
expect(code).toContain(
|
|
115
|
-
"createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, createEffectPre, createInspectEffect, withEffectScope",
|
|
115
|
+
"createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert, applyDirective, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, untrack, createRoot, createEffectPre, createInspectEffect, withEffectScope, mountTransition, removeAnimated",
|
|
116
116
|
);
|
|
117
117
|
});
|
|
118
118
|
|
package/test/jsx.test.ts
CHANGED
|
@@ -195,3 +195,31 @@ describe("JSX", () => {
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
|
+
describe("transition directives", () => {
|
|
199
|
+
test("transition: emits mountTransition with transition kind", () => {
|
|
200
|
+
const result = transform(`
|
|
201
|
+
const fade = (el, p, d) => {};
|
|
202
|
+
const el = <div transition:fade={fade}>hi</div>;
|
|
203
|
+
`);
|
|
204
|
+
expect(result).toContain("mountTransition(");
|
|
205
|
+
expect(result).toMatch(/mountTransition\(_el\d+, fade, "transition"\)/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("in: and out: emit their kinds", () => {
|
|
209
|
+
const result = transform(`
|
|
210
|
+
const appear = () => {};
|
|
211
|
+
const leave = () => {};
|
|
212
|
+
const el = <div in:appear={appear} out:leave={leave}>hi</div>;
|
|
213
|
+
`);
|
|
214
|
+
expect(result).toMatch(/mountTransition\(_el\d+, appear, "in"\)/);
|
|
215
|
+
expect(result).toMatch(/mountTransition\(_el\d+, leave, "out"\)/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("tuple params pass through", () => {
|
|
219
|
+
const result = transform(`
|
|
220
|
+
const fly = () => {};
|
|
221
|
+
const el = <div in:fly={[fly, { y: 20 }]}>hi</div>;
|
|
222
|
+
`);
|
|
223
|
+
expect(result).toMatch(/mountTransition\(_el\d+, \[fly, \{\s*y: 20\s*\}\], "in"\)/);
|
|
224
|
+
});
|
|
225
|
+
});
|
package/test/keyed-lists.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import * as runtime from "../../runtime/index.ts";
|
|
2
3
|
import { transform } from "./helpers/transform.ts";
|
|
3
4
|
|
|
4
5
|
describe("Keyed Lists", () => {
|
|
@@ -65,16 +66,53 @@ describe("Keyed Lists", () => {
|
|
|
65
66
|
expect(result).toContain("reconcile(");
|
|
66
67
|
});
|
|
67
68
|
|
|
68
|
-
test("unkeyed .map()
|
|
69
|
+
test("unkeyed .map() reconciles with index keys", () => {
|
|
69
70
|
const result = transform(`
|
|
70
71
|
let items = $state([1, 2, 3]);
|
|
71
72
|
const el = <ul>{items.map(i => <li>{i}</li>)}</ul>;
|
|
72
73
|
`);
|
|
73
|
-
// No key prop →
|
|
74
|
-
expect(result).
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
// No key prop → still reconcile, but keyed by position
|
|
75
|
+
expect(result).toContain("reconcile(");
|
|
76
|
+
expect(result).toContain("(_item, _i) => _i");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("unkeyed map preserves DOM nodes on shift (behavioral)", async () => {
|
|
80
|
+
// minimal structural view of the DOM the compiled code touches — this
|
|
81
|
+
// package's compiler options have no DOM lib
|
|
82
|
+
type TextLike = { textContent: string };
|
|
83
|
+
type ListEl = {
|
|
84
|
+
querySelector(selectors: string): { firstChild: TextLike | null } | null;
|
|
85
|
+
querySelectorAll(selectors: string): { length: number };
|
|
86
|
+
};
|
|
87
|
+
const src = `
|
|
88
|
+
let items = $state(["a", "b", "c"]);
|
|
89
|
+
const el = <ul>{items.map(i => <li>{i}</li>)}</ul>;
|
|
90
|
+
`;
|
|
91
|
+
const code = transform(src);
|
|
92
|
+
const stripped = code.replace(
|
|
93
|
+
/import\s*\{[^}]*\}\s*from\s*["']@sigil-dev\/runtime["'];?/g,
|
|
94
|
+
"",
|
|
95
|
+
);
|
|
96
|
+
const factory = new Function(
|
|
97
|
+
"document",
|
|
98
|
+
...Object.keys(runtime),
|
|
99
|
+
`${stripped};\nreturn { el, items };`,
|
|
100
|
+
);
|
|
101
|
+
const doc = (globalThis as { document: unknown }).document;
|
|
102
|
+
const result = factory(doc, ...Object.values(runtime)) as {
|
|
103
|
+
el: ListEl;
|
|
104
|
+
items: { set: (v: string[]) => void };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const firstText = result.el.querySelector("li")!.firstChild!;
|
|
108
|
+
expect(firstText.textContent).toBe("a");
|
|
109
|
+
|
|
110
|
+
result.items.set(["a", "c"]); // remove middle item
|
|
111
|
+
await runtime.tick();
|
|
112
|
+
|
|
113
|
+
expect(result.el.querySelectorAll("li").length).toBe(2);
|
|
114
|
+
// first item's Text node survived — no full remount
|
|
115
|
+
expect(result.el.querySelector("li")!.firstChild).toBe(firstText);
|
|
78
116
|
});
|
|
79
117
|
|
|
80
118
|
test("complex key expression", () => {
|