@sigil-dev/grimoire 0.7.6 → 0.8.0
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/index.ts +35 -34
- package/package.json +8 -6
- package/preload.js +3 -2
- package/server.ts +13 -13
- package/src/client/head.ts +29 -29
- package/src/client/router.ts +120 -53
- package/src/dev/compile-module.ts +173 -0
- package/src/dev/effect-registry.ts +23 -0
- package/src/dev/graph.ts +114 -0
- package/src/dev/hmr-client.ts +158 -0
- package/src/dev/hmr-server.ts +187 -0
- package/src/dev/loader.ts +47 -0
- package/src/dev/paths.ts +14 -0
- package/src/dev/runtime-bundle.ts +49 -0
- package/src/dev/watcher.ts +44 -0
- package/src/integrations/vite.ts +73 -72
- package/src/rendering/hydrate.ts +102 -64
- package/src/rendering/index.ts +296 -199
- package/src/rendering/ssrPlugin.ts +67 -53
- package/src/routing/manifest-gen.ts +42 -39
- package/src/routing/router.ts +109 -106
- package/src/routing/scanner.ts +141 -135
- package/src/routing/transform-routes.ts +101 -101
- package/src/server/build.ts +239 -147
- package/src/server/coordinator.ts +306 -306
- package/src/server/index.ts +260 -50
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +356 -353
- package/src/types.ts +270 -269
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -223
- package/test/rendering.test.ts +579 -425
- package/test/routing.test.ts +81 -83
- package/test/scanning.test.ts +200 -181
- package/test/scope.test.ts +24 -8
- package/test/server.test.ts +249 -229
- package/test/streaming.test.ts +125 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +35 -25
- package/tsconfig.json +1 -0
package/src/rendering/index.ts
CHANGED
|
@@ -1,199 +1,296 @@
|
|
|
1
|
-
import { SafeHtml } from "@sigil-dev/runtime";
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
e.status,
|
|
106
|
-
e.
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
1
|
+
import { SafeHtml } from "@sigil-dev/runtime";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { findClosestError, type MatchedRoute } from "../routing/router";
|
|
4
|
+
import type { RouteFile } from "../routing/scanner";
|
|
5
|
+
import { isErrorResult } from "../sentinels/error.ts";
|
|
6
|
+
import { isRedirectResult } from "../sentinels/redirect.ts";
|
|
7
|
+
import { runWithContext } from "../server/context";
|
|
8
|
+
import { runHook } from "../server/plugins";
|
|
9
|
+
import type { GrimoirePlugin, LoadContext, Route } from "../types";
|
|
10
|
+
import { collectHead, initHead } from "./head";
|
|
11
|
+
|
|
12
|
+
export type ModuleLoader = (path: string) => Promise<any>;
|
|
13
|
+
|
|
14
|
+
async function renderErrorPage(
|
|
15
|
+
errorRoutes: RouteFile[],
|
|
16
|
+
pathname: string,
|
|
17
|
+
status: number,
|
|
18
|
+
message: string,
|
|
19
|
+
error?: unknown,
|
|
20
|
+
): Promise<Response | null> {
|
|
21
|
+
const errorPage = findClosestError(errorRoutes, pathname);
|
|
22
|
+
if (!errorPage) return null;
|
|
23
|
+
const mod = await import(errorPage.filePath);
|
|
24
|
+
const html = mod.default({ status, message, error, route: pathname });
|
|
25
|
+
return new Response(html, {
|
|
26
|
+
status,
|
|
27
|
+
headers: { "Content-Type": "text/html" },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function renderRoute(
|
|
32
|
+
matched: MatchedRoute,
|
|
33
|
+
req: Request,
|
|
34
|
+
errorRoutes: RouteFile[] = [],
|
|
35
|
+
loadModule: ModuleLoader = (path) => import(path),
|
|
36
|
+
locals: Record<string, any> = {},
|
|
37
|
+
plugins: GrimoirePlugin[] = [],
|
|
38
|
+
cspNonce?: string,
|
|
39
|
+
dev?: boolean,
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
return runWithContext(async () => {
|
|
42
|
+
const context: LoadContext = {
|
|
43
|
+
request: req,
|
|
44
|
+
params: matched.params,
|
|
45
|
+
url: new URL(req.url),
|
|
46
|
+
locals,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
initHead(cspNonce);
|
|
50
|
+
|
|
51
|
+
const route: Route = {
|
|
52
|
+
path: matched.route.path,
|
|
53
|
+
params: matched.params,
|
|
54
|
+
filePath: matched.route.filePath,
|
|
55
|
+
loadPath: matched.pageServer?.filePath,
|
|
56
|
+
layoutPath: matched.layouts.at(-1)?.filePath,
|
|
57
|
+
};
|
|
58
|
+
await runHook(plugins, "onRouteLoad", route, context);
|
|
59
|
+
|
|
60
|
+
const layoutPairs: { layout: RouteFile; mod: any; data: unknown }[] = [];
|
|
61
|
+
for (const layout of matched.layouts) {
|
|
62
|
+
const layoutMod = await loadModule(layout.filePath);
|
|
63
|
+
|
|
64
|
+
if (layoutMod.canMatch) {
|
|
65
|
+
try {
|
|
66
|
+
const result = await layoutMod.canMatch(context);
|
|
67
|
+
if (result === false)
|
|
68
|
+
return new Response("Not Found", { status: 404 });
|
|
69
|
+
if (isRedirectResult(result))
|
|
70
|
+
return new Response(null, {
|
|
71
|
+
status: result.status,
|
|
72
|
+
headers: { Location: result.location },
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (isRedirectResult(e))
|
|
76
|
+
return new Response(null, {
|
|
77
|
+
status: e.status,
|
|
78
|
+
headers: { Location: e.location },
|
|
79
|
+
});
|
|
80
|
+
if (isErrorResult(e))
|
|
81
|
+
return (
|
|
82
|
+
(await renderErrorPage(
|
|
83
|
+
errorRoutes,
|
|
84
|
+
context.url.pathname,
|
|
85
|
+
e.status,
|
|
86
|
+
e.message,
|
|
87
|
+
e,
|
|
88
|
+
)) ?? new Response(e.message, { status: e.status })
|
|
89
|
+
);
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const layoutServer = matched.layoutServers.find(
|
|
95
|
+
(ls) => ls.path === layout.path,
|
|
96
|
+
);
|
|
97
|
+
let data: unknown;
|
|
98
|
+
if (layoutServer) {
|
|
99
|
+
try {
|
|
100
|
+
const mod = await loadModule(layoutServer.filePath);
|
|
101
|
+
data = await mod.load?.(context);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (isRedirectResult(e))
|
|
104
|
+
return new Response(null, {
|
|
105
|
+
status: e.status,
|
|
106
|
+
headers: { Location: e.location },
|
|
107
|
+
});
|
|
108
|
+
if (isErrorResult(e))
|
|
109
|
+
return (
|
|
110
|
+
(await renderErrorPage(
|
|
111
|
+
errorRoutes,
|
|
112
|
+
context.url.pathname,
|
|
113
|
+
e.status,
|
|
114
|
+
e.message,
|
|
115
|
+
e,
|
|
116
|
+
)) ?? new Response(e.message, { status: e.status })
|
|
117
|
+
);
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const universalLayoutData = layoutMod.load
|
|
122
|
+
? await layoutMod.load(context)
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
125
|
+
const mergedLayoutData =
|
|
126
|
+
universalLayoutData !== undefined
|
|
127
|
+
? //@ts-expect-error User provded data is always unknown.
|
|
128
|
+
{ ...universalLayoutData, ...data }
|
|
129
|
+
: data;
|
|
130
|
+
|
|
131
|
+
layoutPairs.push({ layout, mod: layoutMod, data: mergedLayoutData });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let pageData: unknown;
|
|
135
|
+
if (matched.pageServer) {
|
|
136
|
+
try {
|
|
137
|
+
const mod = await loadModule(matched.pageServer.filePath);
|
|
138
|
+
pageData = await mod.load?.(context);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (isRedirectResult(e)) {
|
|
141
|
+
return new Response(null, {
|
|
142
|
+
status: e.status,
|
|
143
|
+
headers: { Location: e.location },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (isErrorResult(e)) {
|
|
147
|
+
return (
|
|
148
|
+
(await renderErrorPage(
|
|
149
|
+
errorRoutes,
|
|
150
|
+
context.url.pathname,
|
|
151
|
+
e.status,
|
|
152
|
+
e.message,
|
|
153
|
+
e,
|
|
154
|
+
)) ?? new Response(e.message, { status: e.status })
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pageMod = await loadModule(matched.route.filePath);
|
|
162
|
+
if (pageMod.canMatch) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await pageMod.canMatch(context);
|
|
165
|
+
if (result === false) return new Response("Not Found", { status: 404 });
|
|
166
|
+
if (isRedirectResult(result))
|
|
167
|
+
return new Response(null, {
|
|
168
|
+
status: result.status,
|
|
169
|
+
headers: { Location: result.location },
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (isRedirectResult(e))
|
|
173
|
+
return new Response(null, {
|
|
174
|
+
status: e.status,
|
|
175
|
+
headers: { Location: e.location },
|
|
176
|
+
});
|
|
177
|
+
if (isErrorResult(e))
|
|
178
|
+
return (
|
|
179
|
+
(await renderErrorPage(
|
|
180
|
+
errorRoutes,
|
|
181
|
+
context.url.pathname,
|
|
182
|
+
e.status,
|
|
183
|
+
e.message,
|
|
184
|
+
e,
|
|
185
|
+
)) ?? new Response(e.message, { status: e.status })
|
|
186
|
+
);
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const universalData = pageMod.load
|
|
192
|
+
? await pageMod.load(context).catch((e) => {
|
|
193
|
+
if (isRedirectResult(e)) throw e;
|
|
194
|
+
if (isErrorResult(e)) throw e;
|
|
195
|
+
throw e;
|
|
196
|
+
})
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
const mergedData =
|
|
200
|
+
universalData !== undefined
|
|
201
|
+
? //@ts-expect-error User provded data is always unknown.
|
|
202
|
+
{ ...universalData, ...pageData }
|
|
203
|
+
: pageData;
|
|
204
|
+
|
|
205
|
+
const pageHtml = pageMod.default({
|
|
206
|
+
data: mergedData,
|
|
207
|
+
params: matched.params,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// collect head AFTER page render so <Head> calls are captured
|
|
211
|
+
const headHtml = collectHead();
|
|
212
|
+
|
|
213
|
+
// navigation request: return JSON, client handles rendering
|
|
214
|
+
if (req.headers.get("x-grimoire-navigate") === "1") {
|
|
215
|
+
return Response.json({
|
|
216
|
+
data: mergedData ?? {},
|
|
217
|
+
layoutData: layoutPairs.map((l) => l.data),
|
|
218
|
+
params: matched.params,
|
|
219
|
+
pattern: matched.route.path,
|
|
220
|
+
head: headHtml,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const wrappedPage = `<div id="grimoire-page">${String(pageHtml)}</div>`;
|
|
225
|
+
|
|
226
|
+
let bodyHtml: string = wrappedPage;
|
|
227
|
+
for (const { mod: layoutMod, data } of [...layoutPairs].reverse()) {
|
|
228
|
+
bodyHtml = String(
|
|
229
|
+
layoutMod.default({
|
|
230
|
+
data,
|
|
231
|
+
children: new SafeHtml(bodyHtml),
|
|
232
|
+
params: matched.params,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bodyHtml = `<div id="grimoire-root">${bodyHtml}</div>`;
|
|
238
|
+
const csrfToken = randomBytes(32).toString("hex");
|
|
239
|
+
const stateJson = JSON.stringify({
|
|
240
|
+
params: matched.params,
|
|
241
|
+
data: mergedData,
|
|
242
|
+
layoutData: layoutPairs.map((l) => l.data),
|
|
243
|
+
pattern: matched.route.path,
|
|
244
|
+
});
|
|
245
|
+
const csrfInput = `<input type="hidden" name="_csrf" value="${csrfToken}">`;
|
|
246
|
+
bodyHtml = bodyHtml.replace(
|
|
247
|
+
/<form([^>]*action=[^>]*)>/gi,
|
|
248
|
+
`<form$1>${csrfInput}`,
|
|
249
|
+
);
|
|
250
|
+
// --- Streaming SSR ---
|
|
251
|
+
// Send DOCTYPE + head skeleton immediately (browser starts resource fetch).
|
|
252
|
+
// Then stream body + full head content + state as they become available.
|
|
253
|
+
const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
|
|
254
|
+
const stream = new ReadableStream({
|
|
255
|
+
start(controller) {
|
|
256
|
+
// 1. Document skeleton — browser starts parsing, fetches CSS/JS
|
|
257
|
+
controller.enqueue(
|
|
258
|
+
`<!DOCTYPE html>
|
|
259
|
+
<html>
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="UTF-8" />
|
|
262
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
263
|
+
<script type="module" src="/__grimoire__/hydrate.js"${nonceAttr}></script>
|
|
264
|
+
${dev ? `<script src="/__grimoire__/hmr-client.js"${nonceAttr}></script>` : ""}`,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 2. Head content (captured from <Head> component calls during render)
|
|
268
|
+
if (headHtml) {
|
|
269
|
+
controller.enqueue(`\n${headHtml}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
controller.enqueue(`\n</head>
|
|
273
|
+
<body>
|
|
274
|
+
<div id="app">`);
|
|
275
|
+
|
|
276
|
+
// 3. Page body
|
|
277
|
+
controller.enqueue(bodyHtml);
|
|
278
|
+
|
|
279
|
+
// 4. State script + closing tags
|
|
280
|
+
controller.enqueue(`</div>
|
|
281
|
+
<script id="__grimoire_state__" type="application/json"${nonceAttr}>${stateJson}</script>
|
|
282
|
+
</body>
|
|
283
|
+
</html>`);
|
|
284
|
+
|
|
285
|
+
controller.close();
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return new Response(stream, {
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "text/html",
|
|
292
|
+
"Set-Cookie": "_csrf=${csrfToken}; SameSite=Strict; Path=/",
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -1,53 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import type { GrimoirePlugin } from "../types";
|
|
5
|
-
|
|
6
|
-
let registered = false;
|
|
7
|
-
|
|
8
|
-
export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { transformSync } from "@babel/core";
|
|
3
|
+
import sigilPlugin from "@sigil-dev/compiler/babel";
|
|
4
|
+
import type { GrimoirePlugin } from "../types";
|
|
5
|
+
|
|
6
|
+
let registered = false;
|
|
7
|
+
|
|
8
|
+
export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
|
|
9
|
+
if (registered) return;
|
|
10
|
+
registered = true;
|
|
11
|
+
|
|
12
|
+
Bun.plugin({
|
|
13
|
+
name: "sigil-ssr",
|
|
14
|
+
|
|
15
|
+
setup(build) {
|
|
16
|
+
// loader: "ts" not "tsx" — Babel already consumed the JSX,
|
|
17
|
+
// Bun only needs to strip remaining TypeScript types
|
|
18
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
19
|
+
build.onResolve({ filter: /^@sigil-dev\/runtime($|\/)/ }, (args) => {
|
|
20
|
+
try {
|
|
21
|
+
const resolved = Bun.resolveSync(args.path, process.cwd());
|
|
22
|
+
return { path: resolved.replaceAll("\\", "/") };
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
|
|
29
|
+
if (path.includes("index.tsx"))
|
|
30
|
+
console.log("[ssr-plugin] onLoad:", path);
|
|
31
|
+
const source = await Bun.file(path).text();
|
|
32
|
+
|
|
33
|
+
// node_modules and .grimoire files are plain TypeScript (no sigil JSX).
|
|
34
|
+
// Still must return { contents, loader } — never return undefined from onLoad.
|
|
35
|
+
if (path.includes("node_modules") || path.includes(".grimoire")) {
|
|
36
|
+
return {
|
|
37
|
+
contents: transpiler.transformSync(source),
|
|
38
|
+
loader: "js" as const,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.env.SIGIL_VERBOSE) {
|
|
43
|
+
console.log("[sigil-ssr] XFORM:", path);
|
|
44
|
+
}
|
|
45
|
+
const hash = createHash("md5").update(path).digest("hex").slice(0, 8);
|
|
46
|
+
const result = transformSync(source, {
|
|
47
|
+
configFile: false,
|
|
48
|
+
babelrc: false,
|
|
49
|
+
parserOpts: {
|
|
50
|
+
plugins: ["typescript", "jsx"],
|
|
51
|
+
},
|
|
52
|
+
plugins: [[sigilPlugin, { mode: "ssr", hash }]],
|
|
53
|
+
filename: path,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let contents = transpiler.transformSync(result?.code ?? "");
|
|
57
|
+
|
|
58
|
+
for (const plugin of plugins) {
|
|
59
|
+
if (plugin.transform)
|
|
60
|
+
contents = (await plugin.transform(contents, path)) ?? contents;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { contents, loader: "js" as const };
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|