@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.
- package/index.ts +35 -34
- package/package.json +8 -6
- package/preload.js +3 -2
- package/server.ts +13 -13
- package/src/client/head.ts +29 -29
- package/src/client/router.ts +290 -224
- package/src/dev/compile-module.ts +173 -0
- package/src/dev/effect-registry.ts +23 -0
- package/src/dev/graph.ts +114 -0
- package/src/dev/hmr-client.ts +158 -0
- package/src/dev/hmr-server.ts +187 -0
- package/src/dev/loader.ts +47 -0
- package/src/dev/paths.ts +14 -0
- package/src/dev/runtime-bundle.ts +49 -0
- package/src/dev/watcher.ts +44 -0
- package/src/integrations/vite.ts +73 -72
- package/src/rendering/hydrate.ts +120 -81
- package/src/rendering/index.ts +296 -199
- package/src/rendering/ssrPlugin.ts +67 -53
- package/src/routing/manifest-gen.ts +42 -39
- package/src/routing/router.ts +109 -106
- package/src/routing/scanner.ts +141 -135
- package/src/routing/transform-routes.ts +101 -101
- package/src/server/build.ts +239 -147
- package/src/server/coordinator.ts +306 -306
- package/src/server/index.ts +260 -50
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +356 -353
- package/src/types.ts +270 -269
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -223
- package/test/rendering.test.ts +579 -425
- package/test/routing.test.ts +81 -83
- package/test/scanning.test.ts +200 -181
- package/test/scope.test.ts +24 -8
- package/test/server.test.ts +249 -229
- package/test/streaming.test.ts +125 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +35 -25
- package/tsconfig.json +1 -0
package/src/dev/paths.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function normalizePath(p: string) {
|
|
4
|
+
let n = resolve(p).replace(/\\/g, "/");
|
|
5
|
+
if (process.platform === "win32") n = n.toLowerCase();
|
|
6
|
+
return n;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function keyFromUrl(pathname: string, prefix: string) {
|
|
10
|
+
// __grimoire__/m/src/lib/Example.tsx.js -> src/lib/Example.tsx
|
|
11
|
+
return decodeURIComponent(pathname.slice(prefix.length))
|
|
12
|
+
.replace(/\.js(\?.*)?$/, "")
|
|
13
|
+
.replace(/\\/g, "");
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function ensureRuntimeBundle(projectRoot: string): Promise<void> {
|
|
5
|
+
const outDir = join(projectRoot, "public/__grimoire__");
|
|
6
|
+
mkdirSync(outDir, { recursive: true });
|
|
7
|
+
|
|
8
|
+
// runtime
|
|
9
|
+
const runtimeOut = join(outDir, "runtime.js");
|
|
10
|
+
if (!(await Bun.file(runtimeOut).exists())) {
|
|
11
|
+
console.log("[sigil hmr] bundling runtime...");
|
|
12
|
+
const result = await Bun.build({
|
|
13
|
+
entrypoints: [
|
|
14
|
+
join(projectRoot, "node_modules/@sigil-dev/runtime/index.ts"),
|
|
15
|
+
],
|
|
16
|
+
outdir: outDir,
|
|
17
|
+
naming: "runtime.js",
|
|
18
|
+
target: "browser",
|
|
19
|
+
format: "esm",
|
|
20
|
+
minify: false,
|
|
21
|
+
});
|
|
22
|
+
if (!result.success)
|
|
23
|
+
console.error("[sigil hmr] runtime bundle failed:", result.logs);
|
|
24
|
+
else console.log("[sigil hmr] runtime bundled");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// grimoire client
|
|
28
|
+
const grimClientOut = join(outDir, "grimoire-client.js");
|
|
29
|
+
const ref = Bun.file(grimClientOut);
|
|
30
|
+
if (await ref.exists()) await ref.delete();
|
|
31
|
+
await Bun.write(
|
|
32
|
+
grimClientOut,
|
|
33
|
+
`
|
|
34
|
+
// Grimoire client shim for HMR dev mode
|
|
35
|
+
export function Head({ children }) {
|
|
36
|
+
if (children instanceof Node) {
|
|
37
|
+
document.head.appendChild(children);
|
|
38
|
+
}
|
|
39
|
+
return document.createComment("head");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const navigate = (...args) => window.__grimoire_navigate__?.(...args);
|
|
43
|
+
export const beforeNavigate = (cb) => window.__grimoire_beforeNavigate__?.(cb);
|
|
44
|
+
export const onNavigate = (cb) => window.__grimoire_onNavigate__?.(cb);
|
|
45
|
+
export const afterNavigate = (cb) => window.__grimoire_afterNavigate__?.(cb);
|
|
46
|
+
`.trim(),
|
|
47
|
+
);
|
|
48
|
+
console.log("[sigil hmr] grimoire client shim written");
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { normalizePath } from "./paths";
|
|
4
|
+
|
|
5
|
+
export type ChangeHandler = (filePath: string) => Promise<void>;
|
|
6
|
+
|
|
7
|
+
export function startWatcher(
|
|
8
|
+
srcDir: string,
|
|
9
|
+
onChange: ChangeHandler,
|
|
10
|
+
): () => void {
|
|
11
|
+
const pending = new Set<string>();
|
|
12
|
+
let debounce: Timer | null = null;
|
|
13
|
+
|
|
14
|
+
const flush = async () => {
|
|
15
|
+
const files = [...pending];
|
|
16
|
+
pending.clear();
|
|
17
|
+
for (const f of files) {
|
|
18
|
+
await onChange(f);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const watcher = watch(srcDir, { recursive: true }, (_evt, filename) => {
|
|
23
|
+
console.log("[sigil hmr] fs.watch fired:", _evt, filename);
|
|
24
|
+
if (!filename) return;
|
|
25
|
+
const raw = join(srcDir, filename.toString());
|
|
26
|
+
pending.add(normalizePath(raw));
|
|
27
|
+
if (debounce) clearTimeout(debounce);
|
|
28
|
+
debounce = setTimeout(flush, 60);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return () => watcher.close();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read file with atomic-write retry — VSCode on Windows writes temp-then-rename */
|
|
35
|
+
export async function safeRead(filePath: string): Promise<string | null> {
|
|
36
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
37
|
+
const text = await Bun.file(filePath)
|
|
38
|
+
.text()
|
|
39
|
+
.catch(() => null);
|
|
40
|
+
if (text && text.length > 0) return text;
|
|
41
|
+
await Bun.sleep(30);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
package/src/integrations/vite.ts
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
1
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
-
import type { Plugin } from "vite";
|
|
3
|
-
import { renderRoute } from "../rendering";
|
|
4
|
-
import { matchRoute } from "../routing/router.ts";
|
|
5
|
-
import { scanRoutes } from "../routing/scanner.ts";
|
|
6
|
-
|
|
7
|
-
const CLIENT_ENTRY = resolve(import.meta.dir, "./index.ts");
|
|
8
|
-
|
|
9
|
-
export function grimoire(options: { routes?: string } = {}): Plugin {
|
|
10
|
-
let isBuild = false;
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
name: "grimoire",
|
|
14
|
-
|
|
15
|
-
configResolved(config) {
|
|
16
|
-
isBuild = config.command === "build";
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
configureServer(vite) {
|
|
20
|
-
const routesDir = isAbsolute(options.routes ?? "src/routes")
|
|
21
|
-
? options.routes!
|
|
22
|
-
: join(process.cwd(), options.routes ?? "src/routes");
|
|
23
|
-
|
|
24
|
-
// client entry
|
|
25
|
-
vite.middlewares.use("/__grimoire__/client.js", async (req, res) => {
|
|
26
|
-
const result = await vite.transformRequest(CLIENT_ENTRY);
|
|
27
|
-
if (!result) {
|
|
28
|
-
res.statusCode = 404;
|
|
29
|
-
res.end();
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
33
|
-
res.end(result.code);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// page routes
|
|
37
|
-
vite.middlewares.use(async (req, res, next) => {
|
|
38
|
-
const url = new URL(req.url!, "http://localhost");
|
|
39
|
-
if (url.pathname.startsWith("/__") || url.pathname.includes(".")) {
|
|
40
|
-
return next();
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
const tree = await scanRoutes(routesDir, process.cwd());
|
|
44
|
-
const matched = matchRoute(tree, url);
|
|
45
|
-
if (!matched) return next();
|
|
46
|
-
|
|
47
|
-
const response = await renderRoute(
|
|
48
|
-
matched,
|
|
49
|
-
new Request(`http://localhost${req.url}`),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
res.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
import { renderRoute } from "../rendering";
|
|
4
|
+
import { matchRoute } from "../routing/router.ts";
|
|
5
|
+
import { scanRoutes } from "../routing/scanner.ts";
|
|
6
|
+
|
|
7
|
+
const CLIENT_ENTRY = resolve(import.meta.dir, "./index.ts");
|
|
8
|
+
|
|
9
|
+
export function grimoire(options: { routes?: string } = {}): Plugin {
|
|
10
|
+
let isBuild = false;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
name: "grimoire",
|
|
14
|
+
|
|
15
|
+
configResolved(config) {
|
|
16
|
+
isBuild = config.command === "build";
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
configureServer(vite) {
|
|
20
|
+
const routesDir = isAbsolute(options.routes ?? "src/routes")
|
|
21
|
+
? options.routes!
|
|
22
|
+
: join(process.cwd(), options.routes ?? "src/routes");
|
|
23
|
+
|
|
24
|
+
// client entry
|
|
25
|
+
vite.middlewares.use("/__grimoire__/client.js", async (req, res) => {
|
|
26
|
+
const result = await vite.transformRequest(CLIENT_ENTRY);
|
|
27
|
+
if (!result) {
|
|
28
|
+
res.statusCode = 404;
|
|
29
|
+
res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
33
|
+
res.end(result.code);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// page routes
|
|
37
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
38
|
+
const url = new URL(req.url!, "http://localhost");
|
|
39
|
+
if (url.pathname.startsWith("/__") || url.pathname.includes(".")) {
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const tree = await scanRoutes(routesDir, process.cwd());
|
|
44
|
+
const matched = matchRoute(tree, url);
|
|
45
|
+
if (!matched) return next();
|
|
46
|
+
|
|
47
|
+
const response = await renderRoute(
|
|
48
|
+
matched,
|
|
49
|
+
new Request(`http://localhost${req.url}`),
|
|
50
|
+
[],
|
|
51
|
+
(path) => vite.ssrLoadModule(path), // Vite transforms SSR files correctly
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const html = await response.text();
|
|
55
|
+
res.setHeader("Content-Type", "text/html");
|
|
56
|
+
res.end(html);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
vite.ssrFixStacktrace(e as Error);
|
|
59
|
+
next(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
buildStart() {
|
|
65
|
+
if (!isBuild) return;
|
|
66
|
+
this.emitFile({
|
|
67
|
+
type: "chunk",
|
|
68
|
+
id: CLIENT_ENTRY,
|
|
69
|
+
fileName: "client.js",
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/rendering/hydrate.ts
CHANGED
|
@@ -1,81 +1,120 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
//@ts-expect-error compiler generated
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
claim,
|
|
5
|
+
getHydrationNodes,
|
|
6
|
+
insert,
|
|
7
|
+
popHydrationNodes,
|
|
8
|
+
pushHydrationNodes,
|
|
9
|
+
} from "@sigil-dev/runtime";
|
|
10
|
+
import { layouts, routes } from "#grimoire-routes";
|
|
11
|
+
import {
|
|
12
|
+
afterNavigate,
|
|
13
|
+
beforeNavigate,
|
|
14
|
+
navigate,
|
|
15
|
+
onNavigate,
|
|
16
|
+
} from "../client/router";
|
|
17
|
+
import { initRouter } from "../client/router.ts";
|
|
18
|
+
import { withEffectScope } from "../client/scope.ts";
|
|
19
|
+
import { Head } from "./head";
|
|
20
|
+
|
|
21
|
+
// expose for HMR module shim
|
|
22
|
+
(globalThis as any).__grimoire_Head__ = Head;
|
|
23
|
+
(globalThis as any).__grimoire_navigate__ = navigate;
|
|
24
|
+
(globalThis as any).__grimoire_beforeNavigate__ = beforeNavigate;
|
|
25
|
+
(globalThis as any).__grimoire_onNavigate__ = onNavigate;
|
|
26
|
+
(globalThis as any).__grimoire_afterNavigate__ = afterNavigate;
|
|
27
|
+
|
|
28
|
+
const stateEl = document.getElementById("__grimoire_state__");
|
|
29
|
+
let initialDispose: (() => void) | undefined;
|
|
30
|
+
|
|
31
|
+
if (stateEl) {
|
|
32
|
+
const state = JSON.parse(stateEl.textContent!);
|
|
33
|
+
const Page = routes[state.pattern];
|
|
34
|
+
if (Page) {
|
|
35
|
+
const slot = document.getElementById("grimoire-root");
|
|
36
|
+
if (slot) {
|
|
37
|
+
const matchedLayouts = layouts
|
|
38
|
+
.filter(
|
|
39
|
+
(l: any) =>
|
|
40
|
+
l.path === "/" ||
|
|
41
|
+
state.pattern === l.path ||
|
|
42
|
+
state.pattern.startsWith(l.path + "/"),
|
|
43
|
+
)
|
|
44
|
+
.sort((a: any, b: any) => a.path.length - b.path.length);
|
|
45
|
+
|
|
46
|
+
// When layouts are involved, nested <!--g--> delimiters in the flat SSR pool
|
|
47
|
+
// cause anchor-mount to claim the wrong anchor and remove layout DOM nodes.
|
|
48
|
+
// Clear SSR content and re-render in DOM mode (empty pool) instead.
|
|
49
|
+
const hasLayouts = matchedLayouts.length > 0;
|
|
50
|
+
if (hasLayouts) {
|
|
51
|
+
slot.replaceChildren();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ssrClones = hasLayouts
|
|
55
|
+
? []
|
|
56
|
+
: Array.from(slot.childNodes).map((n) => n.cloneNode(true));
|
|
57
|
+
pushHydrationNodes(
|
|
58
|
+
hasLayouts ? [] : (Array.from(slot.childNodes) as ChildNode[]),
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
initialDispose = withEffectScope(() => {
|
|
62
|
+
try {
|
|
63
|
+
let renderFn = () => {
|
|
64
|
+
const pageDiv = claim(getHydrationNodes(), "div");
|
|
65
|
+
pageDiv.id = "grimoire-page";
|
|
66
|
+
|
|
67
|
+
if (pageDiv.childNodes.length > 0) {
|
|
68
|
+
pushHydrationNodes(
|
|
69
|
+
Array.from(pageDiv.childNodes) as ChildNode[],
|
|
70
|
+
);
|
|
71
|
+
const pageNode = Page({
|
|
72
|
+
data: state.data,
|
|
73
|
+
params: state.params,
|
|
74
|
+
});
|
|
75
|
+
popHydrationNodes();
|
|
76
|
+
insert(pageDiv, pageNode);
|
|
77
|
+
} else {
|
|
78
|
+
const pageNode = Page({
|
|
79
|
+
data: state.data,
|
|
80
|
+
params: state.params,
|
|
81
|
+
});
|
|
82
|
+
insert(pageDiv, pageNode);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return pageDiv;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
89
|
+
const LayoutComponent = matchedLayouts[i].component;
|
|
90
|
+
const innerRender = renderFn;
|
|
91
|
+
const layoutData = state.layoutData?.[i];
|
|
92
|
+
renderFn = () => {
|
|
93
|
+
// Pre-render inner content so children is a Node (insert() doesn't call functions)
|
|
94
|
+
const childNode = innerRender();
|
|
95
|
+
return LayoutComponent({
|
|
96
|
+
data: layoutData,
|
|
97
|
+
params: state.params,
|
|
98
|
+
children: childNode,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rootNode = renderFn();
|
|
104
|
+
insert(slot, rootNode);
|
|
105
|
+
} finally {
|
|
106
|
+
popHydrationNodes();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn("[grimoire] hydration error:", e);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!slot.hasChildNodes() && ssrClones.length > 0) {
|
|
114
|
+
slot.replaceChildren(...ssrClones);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
initRouter(routes, layouts, initialDispose);
|