@sigil-dev/grimoire 0.7.6 → 0.8.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.
- 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 +120 -53
- 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 +102 -64
- 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/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,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
claim,
|
|
3
|
+
getHydrationNodes,
|
|
4
|
+
insert,
|
|
5
|
+
popHydrationNodes,
|
|
6
|
+
pushHydrationNodes,
|
|
7
|
+
} from "@sigil-dev/runtime";
|
|
8
|
+
//@ts-expect-error compiler generated
|
|
9
|
+
import { layouts, routes } from "#grimoire-routes";
|
|
10
|
+
import {
|
|
11
|
+
afterNavigate,
|
|
12
|
+
beforeNavigate,
|
|
13
|
+
navigate,
|
|
14
|
+
onNavigate,
|
|
15
|
+
} from "../client/router";
|
|
3
16
|
import { initRouter } from "../client/router.ts";
|
|
4
17
|
import { withEffectScope } from "../client/scope.ts";
|
|
18
|
+
import { Head } from "./head";
|
|
19
|
+
|
|
20
|
+
// expose for HMR module shim
|
|
21
|
+
(globalThis as any).__grimoire_Head__ = Head;
|
|
22
|
+
(globalThis as any).__grimoire_navigate__ = navigate;
|
|
23
|
+
(globalThis as any).__grimoire_beforeNavigate__ = beforeNavigate;
|
|
24
|
+
(globalThis as any).__grimoire_onNavigate__ = onNavigate;
|
|
25
|
+
(globalThis as any).__grimoire_afterNavigate__ = afterNavigate;
|
|
5
26
|
|
|
6
27
|
const stateEl = document.getElementById("__grimoire_state__");
|
|
7
28
|
let initialDispose: (() => void) | undefined;
|
|
8
29
|
|
|
9
30
|
if (stateEl) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
const state = JSON.parse(stateEl.textContent!);
|
|
32
|
+
const Page = routes[state.pattern];
|
|
33
|
+
if (Page) {
|
|
34
|
+
const slot = document.getElementById("grimoire-root");
|
|
35
|
+
if (slot) {
|
|
36
|
+
const matchedLayouts = layouts
|
|
37
|
+
.filter(
|
|
38
|
+
(l: any) =>
|
|
39
|
+
l.path === "/" ||
|
|
40
|
+
state.pattern === l.path ||
|
|
41
|
+
state.pattern.startsWith(l.path + "/"),
|
|
42
|
+
)
|
|
43
|
+
.sort((a: any, b: any) => a.path.length - b.path.length);
|
|
18
44
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
// When layouts are involved, nested <!--g--> delimiters in the flat SSR pool
|
|
46
|
+
// cause anchor-mount to claim the wrong anchor and remove layout DOM nodes.
|
|
47
|
+
// Clear SSR content and re-render in DOM mode (empty pool) instead.
|
|
48
|
+
const hasLayouts = matchedLayouts.length > 0;
|
|
49
|
+
if (hasLayouts) {
|
|
50
|
+
slot.replaceChildren();
|
|
51
|
+
}
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
const ssrClones = hasLayouts
|
|
54
|
+
? []
|
|
55
|
+
: Array.from(slot.childNodes).map((n) => n.cloneNode(true));
|
|
56
|
+
pushHydrationNodes(
|
|
57
|
+
hasLayouts ? [] : (Array.from(slot.childNodes) as ChildNode[]),
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
initialDispose = withEffectScope(() => {
|
|
61
|
+
try {
|
|
62
|
+
let renderFn = () => {
|
|
63
|
+
const pageDiv = claim(getHydrationNodes(), "div");
|
|
64
|
+
pageDiv.id = "grimoire-page";
|
|
35
65
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
if (pageDiv.childNodes.length > 0) {
|
|
67
|
+
pushHydrationNodes(
|
|
68
|
+
Array.from(pageDiv.childNodes) as ChildNode[],
|
|
69
|
+
);
|
|
70
|
+
const pageNode = Page({
|
|
71
|
+
data: state.data,
|
|
72
|
+
params: state.params,
|
|
73
|
+
});
|
|
74
|
+
popHydrationNodes();
|
|
75
|
+
insert(pageDiv, pageNode);
|
|
76
|
+
} else {
|
|
77
|
+
const pageNode = Page({
|
|
78
|
+
data: state.data,
|
|
79
|
+
params: state.params,
|
|
80
|
+
});
|
|
81
|
+
insert(pageDiv, pageNode);
|
|
82
|
+
}
|
|
45
83
|
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
return pageDiv;
|
|
85
|
+
};
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
88
|
+
const LayoutComponent = matchedLayouts[i].component;
|
|
89
|
+
const innerRender = renderFn;
|
|
90
|
+
const layoutData = state.layoutData?.[i];
|
|
91
|
+
renderFn = () => {
|
|
92
|
+
// Pre-render inner content so children is a Node (insert() doesn't call functions)
|
|
93
|
+
const childNode = innerRender();
|
|
94
|
+
return LayoutComponent({
|
|
95
|
+
data: layoutData,
|
|
96
|
+
params: state.params,
|
|
97
|
+
children: childNode,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
}
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
102
|
+
const rootNode = renderFn();
|
|
103
|
+
insert(slot, rootNode);
|
|
104
|
+
} finally {
|
|
105
|
+
popHydrationNodes();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn("[grimoire] hydration error:", e);
|
|
110
|
+
}
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
if (!slot.hasChildNodes() && ssrClones.length > 0) {
|
|
113
|
+
slot.replaceChildren(...ssrClones);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
79
117
|
}
|
|
80
118
|
|
|
81
119
|
initRouter(routes, layouts, initialDispose);
|