@sigil-dev/grimoire 0.3.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/.grimoire/_routes.dom.js +4 -0
- package/.grimoire/_routes.hydrate.js +4 -0
- package/.grimoire/_routes.ts +4 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +6 -0
- package/.grimoire/types/api/hello/$types.d.ts +29 -0
- package/README.md +1 -0
- package/index.ts +22 -0
- package/package.json +36 -0
- package/public/__grimoire__/client.js +86 -0
- package/public/__grimoire__/hydrate.js +101 -0
- package/src/client-router.ts +77 -0
- package/src/client.ts +4 -0
- package/src/context.ts +10 -0
- package/src/cookie-utils.ts +66 -0
- package/src/enhance.ts +97 -0
- package/src/error.ts +52 -0
- package/src/fail.ts +41 -0
- package/src/head.ts +27 -0
- package/src/headers.ts +114 -0
- package/src/hooks.ts +93 -0
- package/src/hydrate.ts +22 -0
- package/src/manifest-gen.ts +26 -0
- package/src/plugins.ts +25 -0
- package/src/redirect.ts +35 -0
- package/src/renderer.ts +142 -0
- package/src/router.ts +94 -0
- package/src/scanner.ts +97 -0
- package/src/scope.ts +22 -0
- package/src/server.ts +318 -0
- package/src/ssrPlugin.ts +26 -0
- package/src/sync.ts +18 -0
- package/src/transform-routes.ts +90 -0
- package/src/typegen.ts +263 -0
- package/src/types.ts +85 -0
- package/src/vite-plugin.ts +72 -0
- package/test/context.test.ts +52 -0
- package/test/fail.test.ts +46 -0
- package/test/headers.test.ts +96 -0
- package/test/hydration.test.ts +119 -0
- package/test/middleware.test.ts +217 -0
- package/test/preload.ts +5 -0
- package/test/redirect-error.test.ts +112 -0
- package/test/rendering.test.ts +172 -0
- package/test/routing.test.ts +45 -0
- package/test/scanning.test.ts +55 -0
- package/test/scope.test.ts +164 -0
- package/test/server.test.ts +30 -0
- package/test/streaming.test.ts +132 -0
- package/test/transform-routes.test.ts +84 -0
- package/test/typegen.test.ts +652 -0
- package/tsconfig.json +7 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { isFailResult } from "./fail";
|
|
2
|
+
import { isErrorResult } from "./error";
|
|
3
|
+
import type { Handle, InitFunction, RequestEvent, ResolveFunction } from "./hooks";
|
|
4
|
+
import { isRedirectResult } from "./redirect";
|
|
5
|
+
import { createCookies } from "./cookie-utils";
|
|
6
|
+
import { sigil } from "@sigil-dev/compiler/bun";
|
|
7
|
+
import { mkdir } from "fs/promises";
|
|
8
|
+
import { isAbsolute, join } from "path";
|
|
9
|
+
import { generateManifest } from "./manifest-gen";
|
|
10
|
+
import { runHook, runRequestHooks } from "./plugins";
|
|
11
|
+
import { renderRoute } from "./renderer";
|
|
12
|
+
import { findClosestError, matchRoute } from "./router";
|
|
13
|
+
import { scanRoutes } from "./scanner";
|
|
14
|
+
import { registerSSRPlugin } from "./ssrPlugin";
|
|
15
|
+
import { transformRoutes } from "./transform-routes";
|
|
16
|
+
import { generateTypes } from "./typegen";
|
|
17
|
+
import type { GrimoireConfig } from "./types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Try to load hooks.server.ts from the project root.
|
|
21
|
+
*/
|
|
22
|
+
async function loadHooks(
|
|
23
|
+
projectRoot: string,
|
|
24
|
+
): Promise<{ handle?: Handle; init?: InitFunction }> {
|
|
25
|
+
const hooksPath = join(projectRoot, "hooks.server.ts");
|
|
26
|
+
try {
|
|
27
|
+
const mod = await import(hooksPath);
|
|
28
|
+
return { handle: mod.handle, init: mod.init };
|
|
29
|
+
} catch {
|
|
30
|
+
// no hooks file — that's fine
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function createServer(config: GrimoireConfig = {}) {
|
|
36
|
+
|
|
37
|
+
registerSSRPlugin();
|
|
38
|
+
const {
|
|
39
|
+
port = 3000,
|
|
40
|
+
host = "localhost",
|
|
41
|
+
plugins = [],
|
|
42
|
+
routes = "src/routes",
|
|
43
|
+
} = config;
|
|
44
|
+
|
|
45
|
+
// run config hooks
|
|
46
|
+
let finalConfig = config;
|
|
47
|
+
for (const plugin of plugins) {
|
|
48
|
+
finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// scan routes once at startup
|
|
52
|
+
const routesDir = isAbsolute(routes)
|
|
53
|
+
? routes.replace(/\0/g, "")
|
|
54
|
+
: join(process.cwd(), routes).replace(/\0/g, "");
|
|
55
|
+
const tree = await scanRoutes(routesDir, process.cwd());
|
|
56
|
+
|
|
57
|
+
await mkdir(join(process.cwd(), ".grimoire"), { recursive: true });
|
|
58
|
+
|
|
59
|
+
await generateTypes(tree, {
|
|
60
|
+
projectRoot: process.cwd(),
|
|
61
|
+
routesDir,
|
|
62
|
+
outDir: join(process.cwd(), ".grimoire/types"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const compiledDir = join(process.cwd(), ".grimoire/compiled");
|
|
66
|
+
await mkdir(compiledDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
// Bun 1.3.13 bug: Bun.build's native parser runs before onLoad hooks fire,
|
|
69
|
+
// so .tsx files with TS syntax crash the bundler even with a sigil plugin.
|
|
70
|
+
// Fix: pre-transform route files to plain JS before Bun.build sees them,
|
|
71
|
+
// then generate per-mode manifests pointing at the compiled .js files.
|
|
72
|
+
const pageRoutes = tree.routes.filter(
|
|
73
|
+
(r) => r.type === "page" || r.type === "simple",
|
|
74
|
+
);
|
|
75
|
+
const [hydrateFiles, domFiles] = await Promise.all([
|
|
76
|
+
transformRoutes(pageRoutes, compiledDir, "hydrate"),
|
|
77
|
+
transformRoutes(pageRoutes, compiledDir, "dom"),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
|
|
81
|
+
const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
|
|
82
|
+
await Promise.all([
|
|
83
|
+
Bun.write(hydrateManifest, generateManifest(pageRoutes, hydrateFiles)),
|
|
84
|
+
Bun.write(domManifest, generateManifest(pageRoutes, domFiles)),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const makeRoutesPlugin = (manifestPath: string) => ({
|
|
88
|
+
name: "grimoire-routes",
|
|
89
|
+
setup(build: any) {
|
|
90
|
+
build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
|
|
91
|
+
path: manifestPath,
|
|
92
|
+
}));
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const [hydrateResult, domResult] = await Promise.all([
|
|
97
|
+
Bun.build({
|
|
98
|
+
entrypoints: [join(import.meta.dir, "./hydrate.ts")],
|
|
99
|
+
outdir: join(process.cwd(), "public/__grimoire__"),
|
|
100
|
+
plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
|
|
101
|
+
}),
|
|
102
|
+
Bun.build({
|
|
103
|
+
entrypoints: [join(import.meta.dir, "./client.ts")],
|
|
104
|
+
outdir: join(process.cwd(), "public/__grimoire__"),
|
|
105
|
+
plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
if (!hydrateResult.success) {
|
|
110
|
+
for (const log of hydrateResult.logs) console.error(log);
|
|
111
|
+
throw new Error("Grimoire: hydrate bundle build failed");
|
|
112
|
+
}
|
|
113
|
+
if (!domResult.success) {
|
|
114
|
+
for (const log of domResult.logs) console.error(log);
|
|
115
|
+
throw new Error("Grimoire: dom bundle build failed");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Load hooks.server.ts
|
|
119
|
+
const { handle: hooksHandle, init: hooksInit } = await loadHooks(process.cwd());
|
|
120
|
+
|
|
121
|
+
// Run init hook if present
|
|
122
|
+
await hooksInit?.();
|
|
123
|
+
|
|
124
|
+
const server = Bun.serve({
|
|
125
|
+
port,
|
|
126
|
+
hostname: host,
|
|
127
|
+
fetch: async (req) => {
|
|
128
|
+
return runRequestHooks(plugins, req, async () => {
|
|
129
|
+
const url = new URL(req.url);
|
|
130
|
+
|
|
131
|
+
const publicFile = Bun.file(
|
|
132
|
+
join(process.cwd(), "public", url.pathname),
|
|
133
|
+
);
|
|
134
|
+
if (await publicFile.exists()) {
|
|
135
|
+
return new Response(publicFile);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const matched = matchRoute(tree, url);
|
|
139
|
+
|
|
140
|
+
if (!matched) {
|
|
141
|
+
const error = findClosestError(tree, url.pathname);
|
|
142
|
+
if (error) {
|
|
143
|
+
const mod = await import(error.filePath);
|
|
144
|
+
const html = mod.default({ status: 404, message: "Not Found" });
|
|
145
|
+
return new Response(html, {
|
|
146
|
+
status: 404,
|
|
147
|
+
headers: { "Content-Type": "text/html" },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return new Response("Not Found", { status: 404 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// API routes — no hooks
|
|
154
|
+
if (matched.route.type === "server") {
|
|
155
|
+
const mod = await import(matched.route.filePath);
|
|
156
|
+
const handler = mod[req.method];
|
|
157
|
+
if (!handler) {
|
|
158
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
159
|
+
}
|
|
160
|
+
return handler({ request: req, params: matched.params, url });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const HTTP_METHODS = [
|
|
164
|
+
"GET",
|
|
165
|
+
"POST",
|
|
166
|
+
"PUT",
|
|
167
|
+
"PATCH",
|
|
168
|
+
"DELETE",
|
|
169
|
+
"HEAD",
|
|
170
|
+
"OPTIONS",
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
// Build RequestEvent for hooks
|
|
174
|
+
const cookieHeader = req.headers.get("cookie") ?? "";
|
|
175
|
+
const cookies = createCookies(cookieHeader);
|
|
176
|
+
const setHeadersMap: Record<string, string> = {};
|
|
177
|
+
|
|
178
|
+
const event: RequestEvent = {
|
|
179
|
+
request: req,
|
|
180
|
+
url,
|
|
181
|
+
params: matched.params,
|
|
182
|
+
locals: {},
|
|
183
|
+
cookies,
|
|
184
|
+
setHeaders: (headers) => Object.assign(setHeadersMap, headers),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Resolve function: runs the actual page/form action logic
|
|
188
|
+
const resolve: ResolveFunction = async (evt) => {
|
|
189
|
+
// form actions
|
|
190
|
+
if (matched.pageServer && HTTP_METHODS.includes(evt.request.method as any)) {
|
|
191
|
+
const mod = await import(matched.pageServer.filePath);
|
|
192
|
+
const handler = mod[evt.request.method];
|
|
193
|
+
if (handler) {
|
|
194
|
+
let result: any;
|
|
195
|
+
try {
|
|
196
|
+
result = await handler({
|
|
197
|
+
request: evt.request,
|
|
198
|
+
params: matched.params,
|
|
199
|
+
url: evt.url,
|
|
200
|
+
locals: evt.locals,
|
|
201
|
+
});
|
|
202
|
+
} catch (e) {
|
|
203
|
+
if (isRedirectResult(e)) {
|
|
204
|
+
return new Response(null, {
|
|
205
|
+
status: e.status,
|
|
206
|
+
headers: { Location: e.location },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (isErrorResult(e)) {
|
|
210
|
+
const isApi = evt.request.headers.get("accept")?.includes("application/json");
|
|
211
|
+
if (isApi) {
|
|
212
|
+
return Response.json(
|
|
213
|
+
{ error: true, status: e.status, message: e.message },
|
|
214
|
+
{ status: e.status },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const errorPage = findClosestError(tree, evt.url.pathname);
|
|
218
|
+
if (errorPage) {
|
|
219
|
+
const errMod = await import(errorPage.filePath);
|
|
220
|
+
const html = errMod.default({ status: e.status, message: e.message });
|
|
221
|
+
return new Response(html, {
|
|
222
|
+
status: e.status,
|
|
223
|
+
headers: { "Content-Type": "text/html" },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return new Response(e.message, { status: e.status });
|
|
227
|
+
}
|
|
228
|
+
throw e;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (isFailResult(result)) {
|
|
232
|
+
return Response.json(
|
|
233
|
+
{ fail: true, status: result.status, data: result.data },
|
|
234
|
+
{ status: result.status },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const redirectTo =
|
|
239
|
+
result?.redirect ?? evt.request.headers.get("referer") ?? "/";
|
|
240
|
+
return new Response(null, {
|
|
241
|
+
status: 303,
|
|
242
|
+
headers: { Location: redirectTo },
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (evt.request.method !== "GET") {
|
|
246
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// page routes
|
|
251
|
+
const response = await renderRoute(matched, evt.request, undefined, evt.locals);
|
|
252
|
+
|
|
253
|
+
if (evt.request.headers.get("x-grimoire-navigate") === "1") {
|
|
254
|
+
return response;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Streaming SSR: if onRouteRender plugins exist, consume stream
|
|
258
|
+
const hasRenderPlugins = plugins.some((p) => p.onRouteRender);
|
|
259
|
+
if (hasRenderPlugins) {
|
|
260
|
+
let html = await response.text();
|
|
261
|
+
for (const plugin of plugins) {
|
|
262
|
+
html =
|
|
263
|
+
(await plugin.onRouteRender?.(html, {
|
|
264
|
+
route: matched.route,
|
|
265
|
+
request: evt.request,
|
|
266
|
+
params: matched.params,
|
|
267
|
+
data: undefined,
|
|
268
|
+
})) ?? html;
|
|
269
|
+
}
|
|
270
|
+
return new Response(html, {
|
|
271
|
+
headers: { "Content-Type": "text/html" },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return response;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// If no hooks handle, resolve directly
|
|
279
|
+
if (!hooksHandle) {
|
|
280
|
+
return resolve(event);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Run through hooks chain
|
|
284
|
+
let response = await hooksHandle({
|
|
285
|
+
event,
|
|
286
|
+
resolve,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Apply setHeaders
|
|
290
|
+
const setCookieHeaders = cookies.toHeaders();
|
|
291
|
+
if (setCookieHeaders.length > 0 || Object.keys(setHeadersMap).length > 0) {
|
|
292
|
+
const headers = new Headers(response.headers);
|
|
293
|
+
for (const [k, v] of Object.entries(setHeadersMap)) {
|
|
294
|
+
headers.set(k, v);
|
|
295
|
+
}
|
|
296
|
+
for (const sc of setCookieHeaders) {
|
|
297
|
+
headers.append("Set-Cookie", sc);
|
|
298
|
+
}
|
|
299
|
+
response = new Response(response.body, {
|
|
300
|
+
status: response.status,
|
|
301
|
+
statusText: response.statusText,
|
|
302
|
+
headers,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return response;
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await runHook(plugins, "onStart", {
|
|
312
|
+
port: server.port,
|
|
313
|
+
hostname: server.hostname,
|
|
314
|
+
stop: () => server.stop(),
|
|
315
|
+
});
|
|
316
|
+
console.log(`Grimoire running at http://${host}:${port}`);
|
|
317
|
+
return server;
|
|
318
|
+
}
|
package/src/ssrPlugin.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// packages/grimoire/src/ssr-plugin.ts
|
|
2
|
+
import { transformSync } from "@babel/core";
|
|
3
|
+
import sigilPlugin from "@sigil-dev/compiler/babel";
|
|
4
|
+
|
|
5
|
+
export function registerSSRPlugin() {
|
|
6
|
+
Bun.plugin({
|
|
7
|
+
name: "sigil-ssr",
|
|
8
|
+
setup(build) {
|
|
9
|
+
const transpiler = new Bun.Transpiler({ loader: "tsx", target: "bun" });
|
|
10
|
+
build.onLoad({ filter: /\.[jt]sx$/ }, async ({ path }) => {
|
|
11
|
+
const source = await Bun.file(path).text();
|
|
12
|
+
const result = transformSync(source, {
|
|
13
|
+
parserOpts: {
|
|
14
|
+
plugins: [["typescript", { isTSX: true }], "jsx"],
|
|
15
|
+
},
|
|
16
|
+
plugins: [[sigilPlugin, { mode: "ssr" }]],
|
|
17
|
+
filename: path,
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
contents: transpiler.transformSync(result?.code ?? ""),
|
|
21
|
+
loader: "js" as const,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { scanRoutes } from "./scanner";
|
|
5
|
+
import { generateTypes } from "./typegen";
|
|
6
|
+
|
|
7
|
+
const routes = process.env.GRIMOIRE_ROUTES ?? "src/routes";
|
|
8
|
+
const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
|
|
9
|
+
|
|
10
|
+
await mkdir(join(process.cwd(), ".grimoire"), { recursive: true });
|
|
11
|
+
const tree = await scanRoutes(routesDir, process.cwd());
|
|
12
|
+
await generateTypes(tree, {
|
|
13
|
+
projectRoot: process.cwd(),
|
|
14
|
+
routesDir,
|
|
15
|
+
outDir: join(process.cwd(), ".grimoire/types"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log("Grimoire: types generated");
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { transformSync } from "@babel/core";
|
|
2
|
+
import sigilPlugin from "@sigil-dev/compiler/babel";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "path";
|
|
4
|
+
import type { RouteFile } from "./scanner";
|
|
5
|
+
|
|
6
|
+
const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
7
|
+
|
|
8
|
+
// Inline the same hash helper used in bun-plugin.ts to avoid importing
|
|
9
|
+
// unexported internals from @sigil-dev/compiler.
|
|
10
|
+
function computeHash(filePath: string): string {
|
|
11
|
+
let h = 0x811c9dc5;
|
|
12
|
+
for (let i = 0; i < filePath.length; i++) {
|
|
13
|
+
h ^= filePath.charCodeAt(i);
|
|
14
|
+
h = (h * 0x01000193) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return h.toString(36);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rewriteRelativeImports(filePath: string) {
|
|
20
|
+
const basedir = dirname(filePath);
|
|
21
|
+
const rewrite = (source?: { value: string }) => {
|
|
22
|
+
if (!source?.value.startsWith(".")) return;
|
|
23
|
+
source.value = resolve(basedir, source.value);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: "sigil-rewrite-relative-route-imports",
|
|
28
|
+
visitor: {
|
|
29
|
+
ImportDeclaration(path: any) {
|
|
30
|
+
rewrite(path.node.source);
|
|
31
|
+
},
|
|
32
|
+
ExportNamedDeclaration(path: any) {
|
|
33
|
+
rewrite(path.node.source);
|
|
34
|
+
},
|
|
35
|
+
ExportAllDeclaration(path: any) {
|
|
36
|
+
rewrite(path.node.source);
|
|
37
|
+
},
|
|
38
|
+
CallExpression(path: any) {
|
|
39
|
+
if (path.node.callee.type !== "Import") return;
|
|
40
|
+
rewrite(path.node.arguments[0]);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pre-transforms route .tsx/.jsx files to plain JS using the Babel sigil
|
|
48
|
+
* pipeline, writing the output to `outDir`. Returns a Map from the original
|
|
49
|
+
* filePath to the compiled .js path.
|
|
50
|
+
*
|
|
51
|
+
* Workaround for Bun 1.3.13: Bun.build's native parser runs before onLoad
|
|
52
|
+
* hooks fire, crashing on TypeScript syntax. Pre-transforming to JS means
|
|
53
|
+
* Bun.build only ever sees plain JavaScript.
|
|
54
|
+
*/
|
|
55
|
+
export async function transformRoutes(
|
|
56
|
+
routes: RouteFile[],
|
|
57
|
+
outDir: string,
|
|
58
|
+
mode: "hydrate" | "dom",
|
|
59
|
+
): Promise<Map<string, string>> {
|
|
60
|
+
const map = new Map<string, string>();
|
|
61
|
+
const transpiler = new Bun.Transpiler({ loader: "tsx", target: "browser" });
|
|
62
|
+
await Promise.all(
|
|
63
|
+
routes.map(async (route, index) => {
|
|
64
|
+
let code = await Bun.file(route.filePath).text();
|
|
65
|
+
const hash = computeHash(route.filePath);
|
|
66
|
+
// Strip <style> blocks — CSS injection is handled by the sigil plugin
|
|
67
|
+
// inside bun-plugin.ts at runtime; we just need clean JS here.
|
|
68
|
+
code = code.replace(STYLE_RE, "");
|
|
69
|
+
const res = transformSync(code, {
|
|
70
|
+
parserOpts: {
|
|
71
|
+
plugins: [["typescript", { isTSX: true }], "jsx"],
|
|
72
|
+
},
|
|
73
|
+
plugins: [
|
|
74
|
+
[sigilPlugin, { hash, mode }],
|
|
75
|
+
rewriteRelativeImports(route.filePath),
|
|
76
|
+
],
|
|
77
|
+
filename: route.filePath,
|
|
78
|
+
});
|
|
79
|
+
const out = transpiler.transformSync(res?.code ?? "");
|
|
80
|
+
const rel = relative(process.cwd(), route.filePath)
|
|
81
|
+
.replace(/\.[cm]?[jt]sx?$/, "")
|
|
82
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
83
|
+
const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
|
|
84
|
+
const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
|
|
85
|
+
await Bun.write(outPath, out);
|
|
86
|
+
map.set(route.filePath, outPath);
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
return map;
|
|
90
|
+
}
|