@sigil-dev/grimoire 0.7.4 → 0.7.6

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 (50) 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 +34 -34
  14. package/package.json +8 -4
  15. package/preload.js +2 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/src/client/head.ts +29 -0
  19. package/src/client/router.ts +224 -76
  20. package/src/env/index.ts +25 -0
  21. package/src/env/plugin.ts +13 -0
  22. package/src/env/private.ts +5 -0
  23. package/src/env/public.ts +7 -0
  24. package/src/env/typegen.ts +51 -0
  25. package/src/integrations/vite.ts +72 -72
  26. package/src/rendering/head.ts +22 -2
  27. package/src/rendering/hydrate.ts +81 -26
  28. package/src/rendering/index.ts +199 -186
  29. package/src/rendering/ssrPlugin.ts +53 -42
  30. package/src/routing/manifest-gen.ts +39 -26
  31. package/src/routing/router.ts +106 -98
  32. package/src/routing/scanner.ts +135 -129
  33. package/src/routing/transform-routes.ts +101 -96
  34. package/src/server/build.ts +147 -90
  35. package/src/server/coordinator.ts +306 -297
  36. package/src/server/hooks.ts +24 -3
  37. package/src/server/index.ts +148 -71
  38. package/src/server/worker.ts +59 -59
  39. package/src/typegen/index.ts +353 -340
  40. package/src/types.ts +269 -260
  41. package/test/context.test.ts +52 -52
  42. package/test/hydration.test.ts +119 -119
  43. package/test/middleware.test.ts +223 -221
  44. package/test/rendering.test.ts +425 -425
  45. package/test/routing.test.ts +83 -45
  46. package/test/scanning.test.ts +181 -169
  47. package/test/server.test.ts +229 -229
  48. package/test/streaming.test.ts +106 -106
  49. package/test/transform-routes.test.ts +84 -84
  50. package/test/typegen.test.ts +19 -1
@@ -1,96 +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
- const transpiler = new Bun.Transpiler({ loader: "tsx", target: "browser" });
64
- await Promise.all(
65
- routes.map(async (route, index) => {
66
- let code = await Bun.file(route.filePath).text();
67
- const hash = computeHash(route.filePath);
68
- // Strip <style> blocks — CSS injection is handled by the sigil plugin
69
- // inside bun-plugin.ts at runtime; we just need clean JS here.
70
- code = code.replace(STYLE_RE, "");
71
- const res = transformSync(code, {
72
- parserOpts: {
73
- plugins: [["typescript", { isTSX: true }], "jsx"],
74
- },
75
- plugins: [
76
- [sigilPlugin, { hash, mode }],
77
- rewriteRelativeImports(route.filePath),
78
- ],
79
- filename: route.filePath,
80
- });
81
- let out = transpiler.transformSync(res?.code ?? "");
82
- for (const plugin of plugins) {
83
- if (plugin.transform)
84
- out = (await plugin.transform(out, route.filePath)) ?? out;
85
- }
86
- const rel = relative(process.cwd(), route.filePath)
87
- .replace(/\.[cm]?[jt]sx?$/, "")
88
- .replace(/[^a-zA-Z0-9._-]/g, "_");
89
- const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
90
- const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
91
- await Bun.write(outPath, out);
92
- map.set(route.filePath, outPath);
93
- }),
94
- );
95
- return map;
96
- }
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,90 +1,147 @@
1
- import { sigil } from "@sigil-dev/compiler/bun";
2
- import { mkdir } from "fs/promises";
3
- import { isAbsolute, join } from "path";
4
- import { generateManifest } from "../routing/manifest-gen";
5
- import type { RouteTree } from "../routing/scanner";
6
- import { scanRoutes } from "../routing/scanner";
7
- import { transformRoutes } from "../routing/transform-routes";
8
- import { generateTypes } from "../typegen";
9
- import type { BuildResult, GrimoireConfig, GrimoirePlugin } from "../types";
10
- import { runHook } from "./plugins";
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, "../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 };
90
- }
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
+ }