@sigil-dev/grimoire 0.7.6 → 0.7.7

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.
Files changed (41) hide show
  1. package/index.ts +35 -34
  2. package/package.json +8 -6
  3. package/preload.js +3 -2
  4. package/server.ts +13 -13
  5. package/src/client/head.ts +29 -29
  6. package/src/client/router.ts +290 -224
  7. package/src/dev/compile-module.ts +173 -0
  8. package/src/dev/effect-registry.ts +23 -0
  9. package/src/dev/graph.ts +114 -0
  10. package/src/dev/hmr-client.ts +158 -0
  11. package/src/dev/hmr-server.ts +187 -0
  12. package/src/dev/loader.ts +47 -0
  13. package/src/dev/paths.ts +14 -0
  14. package/src/dev/runtime-bundle.ts +49 -0
  15. package/src/dev/watcher.ts +44 -0
  16. package/src/integrations/vite.ts +73 -72
  17. package/src/rendering/hydrate.ts +120 -81
  18. package/src/rendering/index.ts +296 -199
  19. package/src/rendering/ssrPlugin.ts +67 -53
  20. package/src/routing/manifest-gen.ts +42 -39
  21. package/src/routing/router.ts +109 -106
  22. package/src/routing/scanner.ts +141 -135
  23. package/src/routing/transform-routes.ts +101 -101
  24. package/src/server/build.ts +239 -147
  25. package/src/server/coordinator.ts +306 -306
  26. package/src/server/index.ts +260 -50
  27. package/src/server/worker.ts +59 -59
  28. package/src/typegen/index.ts +356 -353
  29. package/src/types.ts +270 -269
  30. package/test/context.test.ts +52 -52
  31. package/test/hydration.test.ts +119 -119
  32. package/test/middleware.test.ts +223 -223
  33. package/test/rendering.test.ts +579 -425
  34. package/test/routing.test.ts +81 -83
  35. package/test/scanning.test.ts +200 -181
  36. package/test/scope.test.ts +24 -8
  37. package/test/server.test.ts +249 -229
  38. package/test/streaming.test.ts +125 -106
  39. package/test/transform-routes.test.ts +84 -84
  40. package/test/typegen.test.ts +35 -25
  41. package/tsconfig.json +1 -0
@@ -1,101 +1,101 @@
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 { GrimoirePlugin } from "../types";
5
- import type { RouteFile } from "./scanner";
6
-
7
- const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
8
-
9
- // Inline the same hash helper used in bun-plugin.ts to avoid importing
10
- // unexported internals from @sigil-dev/compiler.
11
- function computeHash(filePath: string): string {
12
- let h = 0x811c9dc5;
13
- for (let i = 0; i < filePath.length; i++) {
14
- h ^= filePath.charCodeAt(i);
15
- h = (h * 0x01000193) >>> 0;
16
- }
17
- return h.toString(36);
18
- }
19
-
20
- function rewriteRelativeImports(filePath: string) {
21
- const basedir = dirname(filePath);
22
- const rewrite = (source?: { value: string }) => {
23
- if (!source?.value.startsWith(".")) return;
24
- source.value = resolve(basedir, source.value);
25
- };
26
-
27
- return {
28
- name: "sigil-rewrite-relative-route-imports",
29
- visitor: {
30
- ImportDeclaration(path: any) {
31
- rewrite(path.node.source);
32
- },
33
- ExportNamedDeclaration(path: any) {
34
- rewrite(path.node.source);
35
- },
36
- ExportAllDeclaration(path: any) {
37
- rewrite(path.node.source);
38
- },
39
- CallExpression(path: any) {
40
- if (path.node.callee.type !== "Import") return;
41
- rewrite(path.node.arguments[0]);
42
- },
43
- },
44
- };
45
- }
46
-
47
- /**
48
- * Pre-transforms route .tsx/.jsx files to plain JS using the Babel sigil
49
- * pipeline, writing the output to `outDir`. Returns a Map from the original
50
- * filePath to the compiled .js path.
51
- *
52
- * Workaround for Bun 1.3.13: Bun.build's native parser runs before onLoad
53
- * hooks fire, crashing on TypeScript syntax. Pre-transforming to JS means
54
- * Bun.build only ever sees plain JavaScript.
55
- */
56
- export async function transformRoutes(
57
- routes: RouteFile[],
58
- outDir: string,
59
- mode: "hydrate" | "dom",
60
- plugins: GrimoirePlugin[] = [],
61
- ): Promise<Map<string, string>> {
62
- const map = new Map<string, string>();
63
- // loader: "ts" — Babel already consumed JSX, Bun only strips remaining types
64
- const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
65
-
66
- await Promise.all(
67
- routes.map(async (route, index) => {
68
- let code = await Bun.file(route.filePath).text();
69
- const hash = computeHash(route.filePath);
70
- code = code.replace(STYLE_RE, "");
71
-
72
- const res = transformSync(code, {
73
- configFile: false,
74
- babelrc: false,
75
- parserOpts: {
76
- plugins: ["typescript", "jsx"], // no isTSX
77
- },
78
- plugins: [
79
- [sigilPlugin, { hash, mode }],
80
- rewriteRelativeImports(route.filePath),
81
- ],
82
- filename: route.filePath,
83
- });
84
- let out = transpiler.transformSync(res?.code ?? "");
85
- for (const plugin of plugins) {
86
- if (plugin.transform)
87
- out = (await plugin.transform(out, route.filePath)) ?? out;
88
- }
89
-
90
- const rel = relative(process.cwd(), route.filePath)
91
- .replace(/\.tsx?$/, "")
92
- .replace(/[^a-zA-Z0-9._-]/g, "_");
93
- const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
94
- const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
95
-
96
- await Bun.write(outPath, out);
97
- map.set(route.filePath, outPath);
98
- }),
99
- );
100
- return map;
101
- }
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 { GrimoirePlugin } from "../types";
5
+ import type { RouteFile } from "./scanner";
6
+
7
+ const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
8
+
9
+ // Inline the same hash helper used in bun-plugin.ts to avoid importing
10
+ // unexported internals from @sigil-dev/compiler.
11
+ function computeHash(filePath: string): string {
12
+ let h = 0x811c9dc5;
13
+ for (let i = 0; i < filePath.length; i++) {
14
+ h ^= filePath.charCodeAt(i);
15
+ h = (h * 0x01000193) >>> 0;
16
+ }
17
+ return h.toString(36);
18
+ }
19
+
20
+ function rewriteRelativeImports(filePath: string) {
21
+ const basedir = dirname(filePath);
22
+ const rewrite = (source?: { value: string }) => {
23
+ if (!source?.value.startsWith(".")) return;
24
+ source.value = resolve(basedir, source.value);
25
+ };
26
+
27
+ return {
28
+ name: "sigil-rewrite-relative-route-imports",
29
+ visitor: {
30
+ ImportDeclaration(path: any) {
31
+ rewrite(path.node.source);
32
+ },
33
+ ExportNamedDeclaration(path: any) {
34
+ rewrite(path.node.source);
35
+ },
36
+ ExportAllDeclaration(path: any) {
37
+ rewrite(path.node.source);
38
+ },
39
+ CallExpression(path: any) {
40
+ if (path.node.callee.type !== "Import") return;
41
+ rewrite(path.node.arguments[0]);
42
+ },
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Pre-transforms route .tsx/.jsx files to plain JS using the Babel sigil
49
+ * pipeline, writing the output to `outDir`. Returns a Map from the original
50
+ * filePath to the compiled .js path.
51
+ *
52
+ * Workaround for Bun 1.3.13: Bun.build's native parser runs before onLoad
53
+ * hooks fire, crashing on TypeScript syntax. Pre-transforming to JS means
54
+ * Bun.build only ever sees plain JavaScript.
55
+ */
56
+ export async function transformRoutes(
57
+ routes: RouteFile[],
58
+ outDir: string,
59
+ mode: "hydrate" | "dom",
60
+ plugins: GrimoirePlugin[] = [],
61
+ ): Promise<Map<string, string>> {
62
+ const map = new Map<string, string>();
63
+ // loader: "ts" — Babel already consumed JSX, Bun only strips remaining types
64
+ const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
65
+
66
+ await Promise.all(
67
+ routes.map(async (route, index) => {
68
+ let code = await Bun.file(route.filePath).text();
69
+ const hash = computeHash(route.filePath);
70
+ code = code.replace(STYLE_RE, "");
71
+
72
+ const res = transformSync(code, {
73
+ configFile: false,
74
+ babelrc: false,
75
+ parserOpts: {
76
+ plugins: ["typescript", "jsx"], // no isTSX
77
+ },
78
+ plugins: [
79
+ [sigilPlugin, { hash, mode }],
80
+ rewriteRelativeImports(route.filePath),
81
+ ],
82
+ filename: route.filePath,
83
+ });
84
+ let out = transpiler.transformSync(res?.code ?? "");
85
+ for (const plugin of plugins) {
86
+ if (plugin.transform)
87
+ out = (await plugin.transform(out, route.filePath)) ?? out;
88
+ }
89
+
90
+ const rel = relative(process.cwd(), route.filePath)
91
+ .replace(/\.tsx?$/, "")
92
+ .replace(/[^a-zA-Z0-9._-]/g, "_");
93
+ const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
94
+ const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
95
+
96
+ await Bun.write(outPath, out);
97
+ map.set(route.filePath, outPath);
98
+ }),
99
+ );
100
+ return map;
101
+ }
@@ -1,147 +1,239 @@
1
- import { sigil } from "@sigil-dev/compiler/bun";
2
- import { mkdir, writeFile } from "fs/promises";
3
- import { isAbsolute, join } from "path";
4
- import { cwd } from "process";
5
- import { generateManifest } from "../routing/manifest-gen";
6
- import type { RouteTree } from "../routing/scanner";
7
- import { scanRoutes } from "../routing/scanner";
8
- import { transformRoutes } from "../routing/transform-routes";
9
- import { generateTypes } from "../typegen";
10
- import type { BuildResult, GrimoireConfig, GrimoirePlugin } from "../types";
11
- import { runHook } from "./plugins";
12
-
13
- export async function buildProject(
14
- config: GrimoireConfig,
15
- plugins: GrimoirePlugin[] = [],
16
- ): Promise<{ result: BuildResult; tree: RouteTree }> {
17
- await runHook(plugins, "onBuildStart");
18
-
19
- const { routes = "src/routes" } = config;
20
-
21
- const routesDir = isAbsolute(routes)
22
- ? routes.replace(/\0/g, "")
23
- : join(process.cwd(), routes).replace(/\0/g, "");
24
- const tree = await scanRoutes(routesDir, process.cwd());
25
-
26
- const outDir = join(process.cwd(), ".grimoire");
27
- await mkdir(outDir, { recursive: true });
28
-
29
- await generateTypes(tree, {
30
- projectRoot: process.cwd(),
31
- routesDir,
32
- outDir: join(outDir, "types"),
33
- });
34
-
35
- // Generate $env type declarations from .env
36
- const { generateEnvTypes } = await import("../env/typegen");
37
- const envTypeDir = join(outDir, "types");
38
- await mkdir(envTypeDir, { recursive: true });
39
- await generateEnvTypes(process.cwd(), envTypeDir);
40
-
41
- const compiledDir = join(outDir, "compiled");
42
- await mkdir(compiledDir, { recursive: true });
43
-
44
- const clientRoutes = [
45
- ...tree.routes.filter((r) => r.type === "page" || r.type === "simple"),
46
- ...tree.layouts.filter((r) => r.type === "layout"),
47
- ];
48
-
49
- const [hydrateFiles, domFiles] = await Promise.all([
50
- transformRoutes(clientRoutes, compiledDir, "hydrate", plugins),
51
- transformRoutes(clientRoutes, compiledDir, "dom", plugins),
52
- ]);
53
-
54
- const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
55
- const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
56
- await Promise.all([
57
- Bun.write(hydrateManifest, generateManifest(clientRoutes, hydrateFiles)),
58
- Bun.write(domManifest, generateManifest(clientRoutes, domFiles)),
59
- ]);
60
-
61
- const makeRoutesPlugin = (manifestPath: string) => ({
62
- name: "grimoire-routes",
63
- setup(build: any) {
64
- build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
65
- path: manifestPath,
66
- }));
67
- },
68
- });
69
-
70
- const resolvedAlias: Record<string, string> = {
71
- ...Object.fromEntries(
72
- Object.entries(config.alias ?? {}).map(([k, v]) => [k, join(cwd(), v)]),
73
- ),
74
- "$env/static/public": join(import.meta.dir, "../env/public.ts"),
75
- "$env/static/private": join(import.meta.dir, "../env/private.ts"),
76
- };
77
-
78
- const bundleStrategy = config.bundleStrategy ?? "split";
79
- const publicDir = join(process.cwd(), "public/__grimoire__");
80
-
81
- if (bundleStrategy === "single") {
82
- // Single bundle: combine hydrate + dom in one output
83
- const singleResult = await Bun.build({
84
- entrypoints: [
85
- join(import.meta.dir, "../rendering/hydrate.ts"),
86
- join(import.meta.dir, "../client/index.ts"),
87
- ],
88
- outdir: publicDir,
89
- plugins: [
90
- sigil({ mode: "hydrate" }),
91
- sigil({ mode: "dom" }),
92
- makeRoutesPlugin(hydrateManifest),
93
- makeRoutesPlugin(domManifest),
94
- ],
95
- // @ts-expect-error alias is a valid Bun.build option not yet in types
96
- alias: resolvedAlias,
97
- });
98
-
99
- if (!singleResult.success) {
100
- for (const log of singleResult.logs) console.error(log);
101
- }
102
-
103
- const result: BuildResult = {
104
- success: singleResult.success,
105
- outputs: singleResult.outputs.map((o) => o.path),
106
- errors: singleResult.logs.map(String),
107
- };
108
-
109
- await runHook(plugins, "onBuildEnd", result);
110
- return { result, tree };
111
- }
112
-
113
- const [hydrateResult, domResult] = await Promise.all([
114
- Bun.build({
115
- entrypoints: [join(import.meta.dir, "../rendering/hydrate.ts")],
116
- outdir: publicDir,
117
- plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
118
- // @ts-expect-error alias is a valid Bun.build option not yet in types
119
- alias: resolvedAlias,
120
- }),
121
- Bun.build({
122
- entrypoints: [join(import.meta.dir, "../client/index.ts")],
123
- outdir: publicDir,
124
- plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
125
- // @ts-expect-error alias is a valid Bun.build option not yet in types
126
- alias: resolvedAlias,
127
- }),
128
- ]);
129
-
130
- if (!hydrateResult.success) {
131
- for (const log of hydrateResult.logs) console.error(log);
132
- }
133
- if (!domResult.success) {
134
- for (const log of domResult.logs) console.error(log);
135
- }
136
-
137
- const result: BuildResult = {
138
- success: hydrateResult.success && domResult.success,
139
- outputs: [...hydrateResult.outputs, ...domResult.outputs].map(
140
- (o) => o.path,
141
- ),
142
- errors: [...hydrateResult.logs, ...domResult.logs].map(String),
143
- };
144
-
145
- await runHook(plugins, "onBuildEnd", result);
146
- return { result, tree };
147
- }
1
+ import { sigil } from "@sigil-dev/compiler/bun";
2
+ import { mkdir } from "fs/promises";
3
+ import { isAbsolute, join } from "path";
4
+ import { cwd } from "process";
5
+ import { normalizePath } from "../dev/paths";
6
+ import { generateManifest } from "../routing/manifest-gen";
7
+ import type { RouteTree } from "../routing/scanner";
8
+ import { scanRoutes } from "../routing/scanner";
9
+ import { transformRoutes } from "../routing/transform-routes";
10
+ import { generateTypes } from "../typegen";
11
+ import type { BuildResult, GrimoireConfig, GrimoirePlugin } from "../types";
12
+ import { runHook } from "./plugins";
13
+
14
+ export async function rebuildForChange(
15
+ changedFilePath: string,
16
+ config: GrimoireConfig,
17
+ plugins: GrimoirePlugin[] = [],
18
+ ): Promise<{ result: BuildResult }> {
19
+ const { routes = "src/routes" } = config;
20
+ const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
21
+
22
+ // only retransform the changed file, not all routes
23
+ const tree = await scanRoutes(routesDir, process.cwd());
24
+ const changedRoute = [
25
+ ...tree.routes.filter((r) => r.type === "page" || r.type === "simple"),
26
+ ...tree.layouts.filter((r) => r.type === "layout"),
27
+ ].find((r) => normalizePath(r.filePath) === normalizePath(changedFilePath));
28
+
29
+ const compiledDir = join(process.cwd(), ".grimoire/compiled");
30
+
31
+ if (changedRoute) {
32
+ // retransform only the changed file
33
+ await Promise.all([
34
+ transformRoutes([changedRoute], compiledDir, "hydrate", plugins),
35
+ transformRoutes([changedRoute], compiledDir, "dom", plugins),
36
+ ]);
37
+ // update manifests with full route list but new compiled paths
38
+ // (other routes already have compiled files from initial build)
39
+ const allClientRoutes = [
40
+ ...tree.routes.filter((r) => r.type === "page" || r.type === "simple"),
41
+ ...tree.layouts.filter((r) => r.type === "layout"),
42
+ ];
43
+ // rebuild full manifests - cheap
44
+ const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
45
+ const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
46
+ const [allHFiles, allDFiles] = await Promise.all([
47
+ transformRoutes(allClientRoutes, compiledDir, "hydrate", plugins),
48
+ transformRoutes(allClientRoutes, compiledDir, "dom", plugins),
49
+ ]);
50
+ await Promise.all([
51
+ Bun.write(hydrateManifest, generateManifest(allClientRoutes, allHFiles)),
52
+ Bun.write(domManifest, generateManifest(allClientRoutes, allDFiles)),
53
+ ]);
54
+ }
55
+
56
+ // still need Bun.build — this is the slow part, Step 3 kills it
57
+ const publicDir = join(process.cwd(), "public/__grimoire__");
58
+ const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
59
+ const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
60
+ const resolvedAlias: Record<string, string> = {
61
+ ...Object.fromEntries(
62
+ Object.entries(config.alias ?? {}).map(([k, v]) => [k, join(cwd(), v)]),
63
+ ),
64
+ "$env/static/public": join(import.meta.dir, "../env/public.ts"),
65
+ "$env/static/private": join(import.meta.dir, "../env/private.ts"),
66
+ };
67
+
68
+ const makeRoutesPlugin = (manifestPath: string) => ({
69
+ name: "grimoire-routes",
70
+ setup(build: any) {
71
+ build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
72
+ path: manifestPath,
73
+ }));
74
+ },
75
+ });
76
+
77
+ const [hydrateResult, domResult] = await Promise.all([
78
+ Bun.build({
79
+ entrypoints: [join(import.meta.dir, "../rendering/hydrate.ts")],
80
+ outdir: publicDir,
81
+ plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
82
+ // @ts-expect-error
83
+ alias: resolvedAlias,
84
+ }),
85
+ Bun.build({
86
+ entrypoints: [join(import.meta.dir, "../client/index.ts")],
87
+ outdir: publicDir,
88
+ plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
89
+ // @ts-expect-error
90
+ alias: resolvedAlias,
91
+ }),
92
+ ]);
93
+
94
+ return {
95
+ result: {
96
+ success: hydrateResult.success && domResult.success,
97
+ outputs: [...hydrateResult.outputs, ...domResult.outputs].map(
98
+ (o) => o.path,
99
+ ),
100
+ errors: [...hydrateResult.logs, ...domResult.logs].map(String),
101
+ },
102
+ };
103
+ }
104
+
105
+ export async function buildProject(
106
+ config: GrimoireConfig,
107
+ plugins: GrimoirePlugin[] = [],
108
+ ): Promise<{ result: BuildResult; tree: RouteTree }> {
109
+ await runHook(plugins, "onBuildStart");
110
+
111
+ const { routes = "src/routes" } = config;
112
+
113
+ const routesDir = isAbsolute(routes)
114
+ ? routes.replace(/\0/g, "")
115
+ : join(process.cwd(), routes).replace(/\0/g, "");
116
+ const tree = await scanRoutes(routesDir, process.cwd());
117
+
118
+ const outDir = join(process.cwd(), ".grimoire");
119
+ await mkdir(outDir, { recursive: true });
120
+
121
+ await generateTypes(tree, {
122
+ projectRoot: process.cwd(),
123
+ routesDir,
124
+ outDir: join(outDir, "types"),
125
+ });
126
+
127
+ // Generate $env type declarations from .env
128
+ const { generateEnvTypes } = await import("../env/typegen");
129
+ const envTypeDir = join(outDir, "types");
130
+ await mkdir(envTypeDir, { recursive: true });
131
+ await generateEnvTypes(process.cwd(), envTypeDir);
132
+
133
+ const compiledDir = join(outDir, "compiled");
134
+ await mkdir(compiledDir, { recursive: true });
135
+
136
+ const clientRoutes = [
137
+ ...tree.routes.filter((r) => r.type === "page" || r.type === "simple"),
138
+ ...tree.layouts.filter((r) => r.type === "layout"),
139
+ ];
140
+
141
+ const [hydrateFiles, domFiles] = await Promise.all([
142
+ transformRoutes(clientRoutes, compiledDir, "hydrate", plugins),
143
+ transformRoutes(clientRoutes, compiledDir, "dom", plugins),
144
+ ]);
145
+
146
+ const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
147
+ const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
148
+ await Promise.all([
149
+ Bun.write(hydrateManifest, generateManifest(clientRoutes, hydrateFiles)),
150
+ Bun.write(domManifest, generateManifest(clientRoutes, domFiles)),
151
+ ]);
152
+
153
+ const makeRoutesPlugin = (manifestPath: string) => ({
154
+ name: "grimoire-routes",
155
+ setup(build: any) {
156
+ build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
157
+ path: manifestPath,
158
+ }));
159
+ },
160
+ });
161
+
162
+ const resolvedAlias: Record<string, string> = {
163
+ ...Object.fromEntries(
164
+ Object.entries(config.alias ?? {}).map(([k, v]) => [k, join(cwd(), v)]),
165
+ ),
166
+ "$env/static/public": join(import.meta.dir, "../env/public.ts"),
167
+ "$env/static/private": join(import.meta.dir, "../env/private.ts"),
168
+ };
169
+
170
+ const bundleStrategy = config.bundleStrategy ?? "split";
171
+ const publicDir = join(process.cwd(), "public/__grimoire__");
172
+
173
+ if (bundleStrategy === "single") {
174
+ // Single bundle: combine hydrate + dom in one output
175
+ const singleResult = await Bun.build({
176
+ entrypoints: [
177
+ join(import.meta.dir, "../rendering/hydrate.ts"),
178
+ join(import.meta.dir, "../client/index.ts"),
179
+ ],
180
+ outdir: publicDir,
181
+ plugins: [
182
+ sigil({ mode: "hydrate" }),
183
+ sigil({ mode: "dom" }),
184
+ makeRoutesPlugin(hydrateManifest),
185
+ makeRoutesPlugin(domManifest),
186
+ ],
187
+ // @ts-expect-error alias is a valid Bun.build option not yet in types
188
+ alias: resolvedAlias,
189
+ });
190
+
191
+ if (!singleResult.success) {
192
+ for (const log of singleResult.logs) console.error(log);
193
+ }
194
+
195
+ const result: BuildResult = {
196
+ success: singleResult.success,
197
+ outputs: singleResult.outputs.map((o) => o.path),
198
+ errors: singleResult.logs.map(String),
199
+ };
200
+
201
+ await runHook(plugins, "onBuildEnd", result);
202
+ return { result, tree };
203
+ }
204
+
205
+ const [hydrateResult, domResult] = await Promise.all([
206
+ Bun.build({
207
+ entrypoints: [join(import.meta.dir, "../rendering/hydrate.ts")],
208
+ outdir: publicDir,
209
+ plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
210
+ // @ts-expect-error alias is a valid Bun.build option not yet in types
211
+ alias: resolvedAlias,
212
+ }),
213
+ Bun.build({
214
+ entrypoints: [join(import.meta.dir, "../client/index.ts")],
215
+ outdir: publicDir,
216
+ plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
217
+ // @ts-expect-error alias is a valid Bun.build option not yet in types
218
+ alias: resolvedAlias,
219
+ }),
220
+ ]);
221
+
222
+ if (!hydrateResult.success) {
223
+ for (const log of hydrateResult.logs) console.error(log);
224
+ }
225
+ if (!domResult.success) {
226
+ for (const log of domResult.logs) console.error(log);
227
+ }
228
+
229
+ const result: BuildResult = {
230
+ success: hydrateResult.success && domResult.success,
231
+ outputs: [...hydrateResult.outputs, ...domResult.outputs].map(
232
+ (o) => o.path,
233
+ ),
234
+ errors: [...hydrateResult.logs, ...domResult.logs].map(String),
235
+ };
236
+
237
+ await runHook(plugins, "onBuildEnd", result);
238
+ return { result, tree };
239
+ }