@sigil-dev/grimoire 0.4.0 → 0.5.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.
Files changed (43) hide show
  1. package/index.ts +29 -23
  2. package/package.json +1 -1
  3. package/public/__grimoire__/client.js +5 -36
  4. package/public/__grimoire__/hydrate.js +7 -45
  5. package/src/build.ts +88 -0
  6. package/src/cookie-utils.ts +66 -66
  7. package/src/plugins.ts +19 -5
  8. package/src/renderer.ts +12 -1
  9. package/src/server.ts +81 -81
  10. package/src/ssrPlugin.ts +7 -2
  11. package/src/transform-routes.ts +6 -1
  12. package/src/typegen.ts +77 -3
  13. package/src/types.ts +45 -1
  14. package/test/fail.test.ts +46 -46
  15. package/test/headers.test.ts +96 -96
  16. package/test/middleware.test.ts +217 -217
  17. package/test/redirect-error.test.ts +112 -112
  18. package/test/rendering.test.ts +310 -310
  19. package/test/server.test.ts +120 -0
  20. package/test/streaming.test.ts +132 -132
  21. package/test/typegen.test.ts +6 -6
  22. package/.grimoire/_routes.dom.js +0 -4
  23. package/.grimoire/_routes.hydrate.js +0 -4
  24. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_protected__page.dom.js +0 -9
  25. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_protected__page.hydrate.js +0 -11
  26. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_protected__page.dom.js +0 -4
  27. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_protected__page.hydrate.js +0 -4
  28. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_login.dom.js +0 -4
  29. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_login.hydrate.js +0 -4
  30. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_login.dom.js +0 -4
  31. package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_login.hydrate.js +0 -4
  32. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_login__page.dom.js +0 -8
  33. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_login__page.hydrate.js +0 -9
  34. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_login__page.dom.js +0 -4
  35. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_login__page.hydrate.js +0 -4
  36. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_protected.dom.js +0 -4
  37. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_protected.hydrate.js +0 -4
  38. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_protected.dom.js +0 -4
  39. package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_protected.hydrate.js +0 -4
  40. package/.grimoire/tsconfig.generated.json +0 -11
  41. package/.grimoire/types/ambient.d.ts +0 -6
  42. package/.grimoire/types/api/hello/$types.d.ts +0 -29
  43. package/.grimoire/types/api/items/$types.d.ts +0 -29
package/index.ts CHANGED
@@ -1,23 +1,29 @@
1
- export type { RouteFile, RouteTree } from "./src/scanner";
2
- export { scanRoutes } from "./src/scanner";
3
- export { createServer } from "./src/server";
4
- export { fail, isFailResult } from "./src/fail";
5
- export type { FailResult } from "./src/fail";
6
- export { redirect, isRedirectResult } from "./src/redirect";
7
- export type { RedirectResult } from "./src/redirect";
8
- export { error, isErrorResult } from "./src/error";
9
- export type { ErrorResult } from "./src/error";
10
- export type { Handle, RequestEvent, Cookies, CookieOptions, ResolveFunction } from "./src/hooks";
11
- export { sequence, createHooks } from "./src/hooks";
12
- export type { TypegenConfig } from "./src/typegen";
13
- export { generateTypes } from "./src/typegen";
14
- export type {
15
- GrimoireConfig,
16
- GrimoirePlugin,
17
- LoadContext,
18
- RenderContext,
19
- RouteInfo,
20
- TypedLoadContext,
21
- } from "./src/types";
22
- export { defineConfig } from "./src/types";
23
- export { Head } from "./src/head";
1
+ export type { RouteFile, RouteTree } from "./src/scanner";
2
+ export { scanRoutes } from "./src/scanner";
3
+ export { buildProject } from "./src/build";
4
+ export { createServer } from "./src/server";
5
+ export { fail, isFailResult } from "./src/fail";
6
+ export type { FailResult } from "./src/fail";
7
+ export { redirect, isRedirectResult } from "./src/redirect";
8
+ export type { RedirectResult } from "./src/redirect";
9
+ export { error, isErrorResult } from "./src/error";
10
+ export type { ErrorResult } from "./src/error";
11
+ export type { Handle, RequestEvent, Cookies, CookieOptions, ResolveFunction } from "./src/hooks";
12
+ export { parseCookies } from "./src/cookie-utils";
13
+ export { sequence, createHooks } from "./src/hooks";
14
+ export type { TypegenConfig } from "./src/typegen";
15
+ export { generateTypes } from "./src/typegen";
16
+ export type {
17
+ GrimoireConfig,
18
+ GrimoirePlugin,
19
+ LoadContext,
20
+ RenderContext,
21
+ RouteInfo,
22
+ TypedLoadContext,
23
+ PageServerLoad,
24
+ LayoutServerLoad,
25
+ RequestHandler,
26
+ WsRouteHandler,
27
+ } from "./src/types";
28
+ export { defineConfig } from "./src/types";
29
+ export { Head } from "./src/head";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
5
  "private": false,
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
7
7
  "devDependencies": {
8
8
  "@types/bun": "latest"
9
9
  },
@@ -1,62 +1,31 @@
1
- // .grimoire/_routes.dom.js
1
+ // .grimoire/_routes.ts
2
2
  var routes = {};
3
3
 
4
- // src/scope.ts
5
- function withEffectScope(fn) {
6
- const prev = globalThis.__sigilEffectScope;
7
- const disposers = [];
8
- globalThis.__sigilEffectScope = disposers;
9
- try {
10
- fn();
11
- } finally {
12
- globalThis.__sigilEffectScope = prev;
13
- }
14
- return () => {
15
- for (const d of disposers)
16
- d();
17
- disposers.length = 0;
18
- };
19
- }
20
-
21
4
  // src/client-router.ts
22
5
  var routeMap = {};
23
- var disposeCurrentPage = null;
24
6
  async function navigate(path) {
25
7
  const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
26
8
  const json = await res.json();
9
+ console.log("navigate response:", json);
27
10
  const { data, params, pattern, head } = json;
28
11
  const Page = routeMap[pattern];
29
12
  if (!Page) {
30
13
  window.location.href = path;
31
14
  return;
32
15
  }
33
- disposeCurrentPage?.();
34
- disposeCurrentPage = null;
35
- globalThis.__nodes = [];
36
- let node;
37
- disposeCurrentPage = withEffectScope(() => {
38
- node = Page({ data, params });
39
- });
16
+ const node = Page({ ...data, params });
40
17
  const slot = document.getElementById("grimoire-page");
41
18
  if (!slot)
42
19
  return;
43
20
  slot.replaceChildren(node);
44
21
  updateHead(head);
45
22
  history.pushState({}, "", path);
46
- currentPath = path;
47
23
  window.scrollTo(0, 0);
48
24
  }
49
- var currentPath = location.pathname;
50
- function initRouter(routes2, initialDispose) {
25
+ function initRouter(routes2) {
51
26
  routeMap = routes2;
52
- disposeCurrentPage = initialDispose ?? null;
53
27
  document.addEventListener("click", handleClick);
54
- window.addEventListener("popstate", () => {
55
- if (location.pathname !== currentPath) {
56
- currentPath = location.pathname;
57
- navigate(location.pathname);
58
- }
59
- });
28
+ window.addEventListener("popstate", () => navigate(location.pathname));
60
29
  }
61
30
  function updateHead(headHtml) {
62
31
  document.querySelectorAll("[data-grimoire-head]").forEach((el) => el.remove());
@@ -1,62 +1,31 @@
1
- // .grimoire/_routes.hydrate.js
1
+ // .grimoire/_routes.ts
2
2
  var routes = {};
3
3
 
4
- // src/scope.ts
5
- function withEffectScope(fn) {
6
- const prev = globalThis.__sigilEffectScope;
7
- const disposers = [];
8
- globalThis.__sigilEffectScope = disposers;
9
- try {
10
- fn();
11
- } finally {
12
- globalThis.__sigilEffectScope = prev;
13
- }
14
- return () => {
15
- for (const d of disposers)
16
- d();
17
- disposers.length = 0;
18
- };
19
- }
20
-
21
4
  // src/client-router.ts
22
5
  var routeMap = {};
23
- var disposeCurrentPage = null;
24
6
  async function navigate(path) {
25
7
  const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
26
8
  const json = await res.json();
9
+ console.log("navigate response:", json);
27
10
  const { data, params, pattern, head } = json;
28
11
  const Page = routeMap[pattern];
29
12
  if (!Page) {
30
13
  window.location.href = path;
31
14
  return;
32
15
  }
33
- disposeCurrentPage?.();
34
- disposeCurrentPage = null;
35
- globalThis.__nodes = [];
36
- let node;
37
- disposeCurrentPage = withEffectScope(() => {
38
- node = Page({ data, params });
39
- });
16
+ const node = Page({ ...data, params });
40
17
  const slot = document.getElementById("grimoire-page");
41
18
  if (!slot)
42
19
  return;
43
20
  slot.replaceChildren(node);
44
21
  updateHead(head);
45
22
  history.pushState({}, "", path);
46
- currentPath = path;
47
23
  window.scrollTo(0, 0);
48
24
  }
49
- var currentPath = location.pathname;
50
- function initRouter(routes2, initialDispose) {
25
+ function initRouter(routes2) {
51
26
  routeMap = routes2;
52
- disposeCurrentPage = initialDispose ?? null;
53
27
  document.addEventListener("click", handleClick);
54
- window.addEventListener("popstate", () => {
55
- if (location.pathname !== currentPath) {
56
- currentPath = location.pathname;
57
- navigate(location.pathname);
58
- }
59
- });
28
+ window.addEventListener("popstate", () => navigate(location.pathname));
60
29
  }
61
30
  function updateHead(headHtml) {
62
31
  document.querySelectorAll("[data-grimoire-head]").forEach((el) => el.remove());
@@ -84,18 +53,11 @@ function handleClick(e) {
84
53
 
85
54
  // src/hydrate.ts
86
55
  var stateEl = document.getElementById("__grimoire_state__");
87
- var initialDispose;
88
56
  if (stateEl) {
89
57
  const state = JSON.parse(stateEl.textContent);
90
58
  const Page = routes[state.pattern];
91
59
  if (Page) {
92
- const slot = document.getElementById("grimoire-page");
93
- if (slot) {
94
- globalThis.__nodes = Array.from(slot.childNodes);
95
- initialDispose = withEffectScope(() => {
96
- Page({ data: state.data, params: state.params });
97
- });
98
- }
60
+ Page({ ...state.data, params: state.params });
99
61
  }
100
62
  }
101
- initRouter(routes, initialDispose);
63
+ initRouter(routes);
package/src/build.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { mkdir } from "fs/promises";
2
+ import { isAbsolute, join } from "path";
3
+ import { sigil } from "@sigil-dev/compiler/bun";
4
+ import { generateManifest } from "./manifest-gen";
5
+ import { runHook } from "./plugins";
6
+ import { scanRoutes } from "./scanner";
7
+ import type { RouteTree } from "./scanner";
8
+ import { transformRoutes } from "./transform-routes";
9
+ import { generateTypes } from "./typegen";
10
+ import type { BuildResult, GrimoireConfig, GrimoirePlugin } from "./types";
11
+
12
+ export async function buildProject(
13
+ config: GrimoireConfig,
14
+ plugins: GrimoirePlugin[] = [],
15
+ ): 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, "./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.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((o) => o.path),
83
+ errors: [...hydrateResult.logs, ...domResult.logs].map(String),
84
+ };
85
+
86
+ await runHook(plugins, "onBuildEnd", result);
87
+ return { result, tree };
88
+ }
@@ -1,66 +1,66 @@
1
- /**
2
- * Parse cookies from a Cookie header string.
3
- */
4
- export function parseCookies(cookieHeader: string): Map<string, string> {
5
- const map = new Map<string, string>();
6
- if (!cookieHeader) return map;
7
- for (const pair of cookieHeader.split(";")) {
8
- const [name, ...rest] = pair.split("=");
9
- if (name) map.set(name.trim(), rest.join("=").trim());
10
- }
11
- return map;
12
- }
13
-
14
- /**
15
- * Build a Set-Cookie header from name, value, and options.
16
- */
17
- export function serializeCookie(
18
- name: string,
19
- value: string,
20
- options?: {
21
- path?: string;
22
- domain?: string;
23
- maxAge?: number;
24
- expires?: Date;
25
- httpOnly?: boolean;
26
- secure?: boolean;
27
- sameSite?: "strict" | "lax" | "none";
28
- },
29
- ): string {
30
- let cookie = `${name}=${encodeURIComponent(value)}`;
31
- if (options?.path) cookie += `; Path=${options.path}`;
32
- if (options?.domain) cookie += `; Domain=${options.domain}`;
33
- if (options?.maxAge != null) cookie += `; Max-Age=${options.maxAge}`;
34
- if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
35
- if (options?.httpOnly) cookie += "; HttpOnly";
36
- if (options?.secure) cookie += "; Secure";
37
- if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
38
- return cookie;
39
- }
40
-
41
- /**
42
- * Create a Cookies helper from a Cookie header string.
43
- */
44
- export function createCookies(
45
- cookieHeader: string,
46
- ): import("./hooks").Cookies & { toHeaders(): string[] } {
47
- const store = parseCookies(cookieHeader);
48
- const pending: string[] = [];
49
-
50
- return {
51
- get(name: string) {
52
- return store.get(name);
53
- },
54
- set(name: string, value: string, options?: any) {
55
- store.set(name, value);
56
- pending.push(serializeCookie(name, value, options));
57
- },
58
- delete(name: string) {
59
- store.delete(name);
60
- pending.push(serializeCookie(name, "", { maxAge: 0 }));
61
- },
62
- toHeaders() {
63
- return pending;
64
- },
65
- };
66
- }
1
+ /**
2
+ * Parse cookies from a Cookie header string.
3
+ */
4
+ export function parseCookies(cookieHeader: string): Map<string, string> {
5
+ const map = new Map<string, string>();
6
+ if (!cookieHeader) return map;
7
+ for (const pair of cookieHeader.split(";")) {
8
+ const [name, ...rest] = pair.split("=");
9
+ if (name) map.set(name.trim(), rest.join("=").trim());
10
+ }
11
+ return map;
12
+ }
13
+
14
+ /**
15
+ * Build a Set-Cookie header from name, value, and options.
16
+ */
17
+ export function serializeCookie(
18
+ name: string,
19
+ value: string,
20
+ options?: {
21
+ path?: string;
22
+ domain?: string;
23
+ maxAge?: number;
24
+ expires?: Date;
25
+ httpOnly?: boolean;
26
+ secure?: boolean;
27
+ sameSite?: "strict" | "lax" | "none";
28
+ },
29
+ ): string {
30
+ let cookie = `${name}=${encodeURIComponent(value)}`;
31
+ if (options?.path) cookie += `; Path=${options.path}`;
32
+ if (options?.domain) cookie += `; Domain=${options.domain}`;
33
+ if (options?.maxAge != null) cookie += `; Max-Age=${options.maxAge}`;
34
+ if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
35
+ if (options?.httpOnly) cookie += "; HttpOnly";
36
+ if (options?.secure) cookie += "; Secure";
37
+ if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
38
+ return cookie;
39
+ }
40
+
41
+ /**
42
+ * Create a Cookies helper from a Cookie header string.
43
+ */
44
+ export function createCookies(
45
+ cookieHeader: string,
46
+ ): import("./hooks").Cookies & { toHeaders(): string[] } {
47
+ const store = parseCookies(cookieHeader);
48
+ const pending: string[] = [];
49
+
50
+ return {
51
+ get(name: string) {
52
+ return store.get(name);
53
+ },
54
+ set(name: string, value: string, options?: any) {
55
+ store.set(name, value);
56
+ pending.push(serializeCookie(name, value, options));
57
+ },
58
+ delete(name: string) {
59
+ store.delete(name);
60
+ pending.push(serializeCookie(name, "", { maxAge: 0 }));
61
+ },
62
+ toHeaders() {
63
+ return pending;
64
+ },
65
+ };
66
+ }
package/src/plugins.ts CHANGED
@@ -1,12 +1,26 @@
1
- import type { GrimoirePlugin } from "./types";
1
+ import type { BuildResult, GrimoirePlugin, LoadContext, Route, Server } from "./types";
2
2
 
3
- export async function runHook(
3
+ // Fire-and-forget hooks routed through runHook.
4
+ // onRequest and onRouteRender are intentionally excluded: they have distinct
5
+ // calling conventions (middleware chain / inline transform loop) handled in server.ts.
6
+ // Keep HookArgs in sync with GrimoirePlugin in types.ts.
7
+ type HookArgs = {
8
+ onStart: [server: Server];
9
+ onStop: [reason: "shutdown" | "restart"];
10
+ onBuildStart: [];
11
+ onBuildEnd: [result: BuildResult];
12
+ onRouteLoad: [route: Route, context: LoadContext];
13
+ };
14
+ type FireAndForgetHook = keyof HookArgs;
15
+
16
+ export async function runHook<K extends FireAndForgetHook>(
4
17
  plugins: GrimoirePlugin[],
5
- hook: keyof GrimoirePlugin,
6
- ...args: any
18
+ hook: K,
19
+ ...args: HookArgs[K]
7
20
  ): Promise<void> {
8
21
  for (const plugin of plugins) {
9
- await (plugin[hook] as any)?.(...args);
22
+ const fn = plugin[hook] as ((...a: any[]) => any) | undefined;
23
+ await fn?.(...args);
10
24
  }
11
25
  }
12
26
 
package/src/renderer.ts CHANGED
@@ -2,10 +2,11 @@ import { SafeHtml } from "@sigil-dev/runtime";
2
2
  import { isErrorResult } from "./error";
3
3
  import { runWithContext } from "./context";
4
4
  import { collectHead, initHead } from "./head";
5
+ import { runHook } from "./plugins";
5
6
  import { isRedirectResult } from "./redirect";
6
7
  import { findClosestError, type MatchedRoute } from "./router";
7
8
  import type { RouteFile } from "./scanner";
8
- import type { LoadContext } from "./types";
9
+ import type { GrimoirePlugin, LoadContext, Route } from "./types";
9
10
 
10
11
  export type ModuleLoader = (path: string) => Promise<any>;
11
12
 
@@ -28,6 +29,7 @@ export async function renderRoute(
28
29
  errorRoutes: RouteFile[] = [],
29
30
  loadModule: ModuleLoader = (path) => import(path),
30
31
  locals: Record<string, any> = {},
32
+ plugins: GrimoirePlugin[] = [],
31
33
  ): Promise<Response> {
32
34
  return runWithContext(async () => {
33
35
  const context: LoadContext = {
@@ -39,6 +41,15 @@ export async function renderRoute(
39
41
 
40
42
  initHead();
41
43
 
44
+ const route: Route = {
45
+ path: matched.route.path,
46
+ params: matched.params,
47
+ filePath: matched.route.filePath,
48
+ loadPath: matched.pageServer?.filePath,
49
+ layoutPath: matched.layout?.filePath,
50
+ };
51
+ await runHook(plugins, "onRouteLoad", route, context);
52
+
42
53
  let layoutData: unknown;
43
54
  if (matched.layoutServer) {
44
55
  try {