@sigil-dev/grimoire 0.7.5 → 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 (58) hide show
  1. package/.grimoire/_routes.dom.js +8 -0
  2. package/.grimoire/_routes.hydrate.js +8 -0
  3. package/.grimoire/tsconfig.generated.json +11 -0
  4. package/.grimoire/types/ambient.d.ts +59 -0
  5. package/.grimoire/types/api/hello/$types.d.ts +50 -0
  6. package/.grimoire/types/api/items/$types.d.ts +50 -0
  7. package/.grimoire/types/echo/$types.d.ts +50 -0
  8. package/.grimoire/types/env-private.d.ts +5 -0
  9. package/.grimoire/types/env-public.d.ts +5 -0
  10. package/.grimoire/types/mixed/$types.d.ts +50 -0
  11. package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
  12. package/.grimoire/types/reject/$types.d.ts +50 -0
  13. package/index.ts +21 -20
  14. package/package.json +13 -7
  15. package/preload.js +3 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/server.ts +13 -13
  19. package/src/client/head.ts +29 -0
  20. package/src/client/router.ts +254 -40
  21. package/src/dev/compile-module.ts +173 -0
  22. package/src/dev/effect-registry.ts +23 -0
  23. package/src/dev/graph.ts +114 -0
  24. package/src/dev/hmr-client.ts +158 -0
  25. package/src/dev/hmr-server.ts +187 -0
  26. package/src/dev/loader.ts +47 -0
  27. package/src/dev/paths.ts +14 -0
  28. package/src/dev/runtime-bundle.ts +49 -0
  29. package/src/dev/watcher.ts +44 -0
  30. package/src/env/index.ts +25 -0
  31. package/src/env/plugin.ts +13 -0
  32. package/src/env/private.ts +5 -0
  33. package/src/env/public.ts +7 -0
  34. package/src/env/typegen.ts +51 -0
  35. package/src/integrations/vite.ts +1 -0
  36. package/src/rendering/head.ts +22 -2
  37. package/src/rendering/hydrate.ts +111 -18
  38. package/src/rendering/index.ts +263 -153
  39. package/src/rendering/ssrPlugin.ts +59 -39
  40. package/src/routing/manifest-gen.ts +18 -2
  41. package/src/routing/router.ts +94 -83
  42. package/src/routing/scanner.ts +26 -14
  43. package/src/routing/transform-routes.ts +68 -68
  44. package/src/server/build.ts +225 -76
  45. package/src/server/coordinator.ts +9 -0
  46. package/src/server/hooks.ts +24 -3
  47. package/src/server/index.ts +388 -104
  48. package/src/typegen/index.ts +30 -14
  49. package/src/types.ts +12 -2
  50. package/test/middleware.test.ts +6 -4
  51. package/test/rendering.test.ts +510 -356
  52. package/test/routing.test.ts +36 -0
  53. package/test/scanning.test.ts +39 -8
  54. package/test/scope.test.ts +24 -8
  55. package/test/server.test.ts +27 -7
  56. package/test/streaming.test.ts +117 -98
  57. package/test/typegen.test.ts +52 -24
  58. package/tsconfig.json +1 -0
@@ -1,6 +1,8 @@
1
1
  import { sigil } from "@sigil-dev/compiler/bun";
2
2
  import { mkdir } from "fs/promises";
3
3
  import { isAbsolute, join } from "path";
4
+ import { cwd } from "process";
5
+ import { normalizePath } from "../dev/paths";
4
6
  import { generateManifest } from "../routing/manifest-gen";
5
7
  import type { RouteTree } from "../routing/scanner";
6
8
  import { scanRoutes } from "../routing/scanner";
@@ -9,82 +11,229 @@ import { generateTypes } from "../typegen";
9
11
  import type { BuildResult, GrimoireConfig, GrimoirePlugin } from "../types";
10
12
  import { runHook } from "./plugins";
11
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
+
12
105
  export async function buildProject(
13
- config: GrimoireConfig,
14
- plugins: GrimoirePlugin[] = [],
106
+ config: GrimoireConfig,
107
+ plugins: GrimoirePlugin[] = [],
15
108
  ): Promise<{ result: BuildResult; tree: RouteTree }> {
16
- await runHook(plugins, "onBuildStart");
17
-
18
- const { routes = "src/routes" } = config;
19
-
20
- const routesDir = isAbsolute(routes)
21
- ? routes.replace(/\0/g, "")
22
- : join(process.cwd(), routes).replace(/\0/g, "");
23
- const tree = await scanRoutes(routesDir, process.cwd());
24
-
25
- await mkdir(join(process.cwd(), ".grimoire"), { recursive: true });
26
-
27
- await generateTypes(tree, {
28
- projectRoot: process.cwd(),
29
- routesDir,
30
- outDir: join(process.cwd(), ".grimoire/types"),
31
- });
32
-
33
- const compiledDir = join(process.cwd(), ".grimoire/compiled");
34
- await mkdir(compiledDir, { recursive: true });
35
-
36
- const pageRoutes = tree.routes.filter(
37
- (r) => r.type === "page" || r.type === "simple",
38
- );
39
- const [hydrateFiles, domFiles] = await Promise.all([
40
- transformRoutes(pageRoutes, compiledDir, "hydrate", plugins),
41
- transformRoutes(pageRoutes, compiledDir, "dom", plugins),
42
- ]);
43
-
44
- const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
45
- const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
46
- await Promise.all([
47
- Bun.write(hydrateManifest, generateManifest(pageRoutes, hydrateFiles)),
48
- Bun.write(domManifest, generateManifest(pageRoutes, domFiles)),
49
- ]);
50
-
51
- const makeRoutesPlugin = (manifestPath: string) => ({
52
- name: "grimoire-routes",
53
- setup(build: any) {
54
- build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
55
- path: manifestPath,
56
- }));
57
- },
58
- });
59
-
60
- const [hydrateResult, domResult] = await Promise.all([
61
- Bun.build({
62
- entrypoints: [join(import.meta.dir, "../rendering/hydrate.ts")],
63
- outdir: join(process.cwd(), "public/__grimoire__"),
64
- plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
65
- }),
66
- Bun.build({
67
- entrypoints: [join(import.meta.dir, "../client/index.ts")],
68
- outdir: join(process.cwd(), "public/__grimoire__"),
69
- plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
70
- }),
71
- ]);
72
-
73
- if (!hydrateResult.success) {
74
- for (const log of hydrateResult.logs) console.error(log);
75
- }
76
- if (!domResult.success) {
77
- for (const log of domResult.logs) console.error(log);
78
- }
79
-
80
- const result: BuildResult = {
81
- success: hydrateResult.success && domResult.success,
82
- outputs: [...hydrateResult.outputs, ...domResult.outputs].map(
83
- (o) => o.path,
84
- ),
85
- errors: [...hydrateResult.logs, ...domResult.logs].map(String),
86
- };
87
-
88
- await runHook(plugins, "onBuildEnd", result);
89
- return { result, tree };
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 };
90
239
  }
@@ -168,10 +168,19 @@ export async function startCoordinator(options: CoordinatorOptions) {
168
168
  const workerEntry = join(import.meta.dir, "./worker.ts");
169
169
  if (workerEnv.name) descriptor.name = workerEnv.name;
170
170
 
171
+ // Inject env vars prefixed with PUBLIC_ so they're available to client bundles
172
+ const publicEnvVars: Record<string, string> = {};
173
+ for (const [key, value] of Object.entries(process.env)) {
174
+ if (key.startsWith("PUBLIC_") && value !== undefined) {
175
+ publicEnvVars[key] = value;
176
+ }
177
+ }
178
+
171
179
  const proc = spawn(["bun", "run", workerEntry], {
172
180
  env: {
173
181
  ...process.env,
174
182
  ...workerEnv.env,
183
+ ...publicEnvVars,
175
184
  GRIMOIRE_MODE: mode,
176
185
  GRIMOIRE_WORKER_INDEX: String(i),
177
186
  GRIMOIRE_WORKER_PORT: String(workerPort),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SvelteKit-style server hooks.
3
3
  *
4
- * Create a hooks.index.ts in your project root:
4
+ * Create a hooks.server.ts in your project root:
5
5
  *
6
6
  * import type { Handle } from "@sigil-dev/grimoire/hooks";
7
7
  *
@@ -12,6 +12,10 @@
12
12
  * return response;
13
13
  * };
14
14
  *
15
+ * export const handleError: HandleError = ({ error, status, message }) => {
16
+ * console.error(`[${status}] ${message}`, error);
17
+ * };
18
+ *
15
19
  * // optional: runs once at server start
16
20
  * export const init = () => {
17
21
  * console.log("Server started");
@@ -32,8 +36,10 @@ export interface RequestEvent {
32
36
  request: Request;
33
37
  url: URL;
34
38
  params: Record<string, string>;
35
- locals: Record<string, any>;
39
+ locals: App.Locals;
36
40
  cookies: Cookies;
41
+ route: { id: string };
42
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
37
43
  setHeaders: (headers: Record<string, string>) => void;
38
44
  }
39
45
 
@@ -60,6 +66,21 @@ export type Handle = (input: {
60
66
  resolve: ResolveFunction;
61
67
  }) => MaybePromise<Response>;
62
68
 
69
+ export interface HandleErrorInput {
70
+ error: unknown;
71
+ event: RequestEvent;
72
+ status: number;
73
+ message: string;
74
+ }
75
+ export type HandleError = (input: HandleErrorInput) => void | Promise<void>;
76
+
77
+ export interface HandleFetchInput {
78
+ request: Request;
79
+ fetch: typeof globalThis.fetch;
80
+ event: RequestEvent;
81
+ }
82
+ export type HandleFetch = (input: HandleFetchInput) => MaybePromise<Response>;
83
+
63
84
  export type InitFunction = () => void | Promise<void>;
64
85
 
65
86
  /**
@@ -73,7 +94,7 @@ export function sequence(...handlers: Handle[]): Handle {
73
94
  const next: ResolveFunction = async (evt) => {
74
95
  if (i < handlers.length) {
75
96
  const handler = handlers[i++];
76
- return handler({ event: evt, resolve: next });
97
+ return handler!({ event: evt, resolve: next });
77
98
  }
78
99
  return resolve(evt);
79
100
  };