@sigil-dev/grimoire 0.8.0 → 0.8.2
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/package.json +7 -6
- package/server.ts +1 -0
- package/src/client/router.ts +291 -291
- package/src/dev/compile-module.ts +4 -13
- package/src/dev/hmr-server.ts +11 -11
- package/src/dev/runtime-bundle.ts +8 -5
- package/src/integrations/vite.ts +1 -1
- package/src/logger/index.ts +6 -0
- package/src/logger/instance.ts +6 -0
- package/src/logger/logger.ts +113 -0
- package/src/logger/request-middleware.ts +24 -0
- package/src/logger/transports.ts +117 -0
- package/src/logger/types.ts +39 -0
- package/src/rendering/hydrate.ts +119 -119
- package/src/rendering/index.ts +1 -1
- package/src/rendering/ssrPlugin.ts +4 -2
- package/src/routing/router.ts +2 -2
- package/src/routing/scanner.ts +1 -1
- package/src/routing/transform-routes.ts +1 -1
- package/src/server/build.ts +3 -3
- package/src/server/coordinator.ts +4 -5
- package/src/server/index.ts +9 -11
- package/src/server/worker.ts +0 -1
- package/test/context.test.ts +2 -2
- package/test/hydration.test.ts +1 -1
- package/test/middleware.test.ts +1 -1
- package/test/rendering.test.ts +3 -3
- package/test/scanning.test.ts +3 -3
- package/test/server.test.ts +3 -3
- package/test/transform-routes.test.ts +3 -3
package/src/dev/hmr-server.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import type { ServerWebSocket } from "bun";
|
|
4
|
+
import { log } from "../logger/instance";
|
|
4
5
|
import { compileForBrowser } from "./compile-module";
|
|
5
6
|
import type { DevGraph } from "./graph";
|
|
6
7
|
import { normalizePath } from "./paths";
|
|
7
8
|
import { safeRead } from "./watcher";
|
|
8
9
|
|
|
10
|
+
const logger = log.scope("hmr");
|
|
11
|
+
|
|
9
12
|
export class HmrServer {
|
|
10
13
|
private clients = new Set<ServerWebSocket<any>>();
|
|
11
14
|
private v = 0;
|
|
@@ -31,7 +34,7 @@ export class HmrServer {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
reload() {
|
|
34
|
-
|
|
37
|
+
logger.info("sending reload to", this.clients.size, "clients");
|
|
35
38
|
this.batch([{ kind: "reload" }]);
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -69,7 +72,7 @@ export async function compileAndSwap(
|
|
|
69
72
|
graph: DevGraph,
|
|
70
73
|
hmr: HmrServer,
|
|
71
74
|
projectRoot: string,
|
|
72
|
-
|
|
75
|
+
_routeTree: any,
|
|
73
76
|
): Promise<void> {
|
|
74
77
|
const key = normalizePath(filePath);
|
|
75
78
|
const node = graph.get(key);
|
|
@@ -105,7 +108,7 @@ export async function compileAndSwap(
|
|
|
105
108
|
|
|
106
109
|
if (!routeFile) {
|
|
107
110
|
// no route found — fall back to reload
|
|
108
|
-
|
|
111
|
+
logger.info("no route affected, reloading");
|
|
109
112
|
hmr.reload();
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
@@ -114,7 +117,7 @@ export async function compileAndSwap(
|
|
|
114
117
|
const clientPath = relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
115
118
|
const modUrl = `${clientPath}.js`;
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
logger.info(`compiled: ${filePath}, v=${v}`);
|
|
118
121
|
|
|
119
122
|
const payloads: object[] = [];
|
|
120
123
|
|
|
@@ -144,10 +147,7 @@ export async function compileAndSwap(
|
|
|
144
147
|
patterns: otherRoutes.map((r) => r.path),
|
|
145
148
|
});
|
|
146
149
|
}
|
|
147
|
-
|
|
148
|
-
"[sigil hmr] sending batch:",
|
|
149
|
-
JSON.stringify(payloads, null, 2),
|
|
150
|
-
);
|
|
150
|
+
logger.info("sending batch:", JSON.stringify(payloads, null, 2));
|
|
151
151
|
hmr.batch(payloads);
|
|
152
152
|
} catch (e: any) {
|
|
153
153
|
const loc = extractLoc(e, filePath);
|
|
@@ -161,7 +161,7 @@ export async function handleChange(
|
|
|
161
161
|
filePath: string,
|
|
162
162
|
graph: DevGraph,
|
|
163
163
|
hmr: HmrServer,
|
|
164
|
-
|
|
164
|
+
_srcDir: string,
|
|
165
165
|
projectRoot: string,
|
|
166
166
|
routeTree: any,
|
|
167
167
|
): Promise<void> {
|
|
@@ -177,11 +177,11 @@ export async function handleChange(
|
|
|
177
177
|
}
|
|
178
178
|
const node = graph.get(key);
|
|
179
179
|
if (!node) {
|
|
180
|
-
|
|
180
|
+
logger.info("unknown file, reloading:", filePath);
|
|
181
181
|
hmr.reload();
|
|
182
182
|
return;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
logger.info("changed:", filePath);
|
|
186
186
|
await compileAndSwap(filePath, graph, hmr, projectRoot, routeTree);
|
|
187
187
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { log } from "../logger/instance";
|
|
4
|
+
|
|
5
|
+
const logger = log.scope("runtime-bundle");
|
|
3
6
|
|
|
4
7
|
export async function ensureRuntimeBundle(projectRoot: string): Promise<void> {
|
|
5
8
|
const outDir = join(projectRoot, "public/__grimoire__");
|
|
@@ -8,7 +11,7 @@ export async function ensureRuntimeBundle(projectRoot: string): Promise<void> {
|
|
|
8
11
|
// runtime
|
|
9
12
|
const runtimeOut = join(outDir, "runtime.js");
|
|
10
13
|
if (!(await Bun.file(runtimeOut).exists())) {
|
|
11
|
-
|
|
14
|
+
logger.debug("bundling runtime...");
|
|
12
15
|
const result = await Bun.build({
|
|
13
16
|
entrypoints: [
|
|
14
17
|
join(projectRoot, "node_modules/@sigil-dev/runtime/index.ts"),
|
|
@@ -20,8 +23,8 @@ export async function ensureRuntimeBundle(projectRoot: string): Promise<void> {
|
|
|
20
23
|
minify: false,
|
|
21
24
|
});
|
|
22
25
|
if (!result.success)
|
|
23
|
-
|
|
24
|
-
else
|
|
26
|
+
logger.error("[sigil hmr] runtime bundle failed:", result.logs);
|
|
27
|
+
else logger.debug("[sigil hmr] runtime bundled");
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
// grimoire client
|
|
@@ -45,5 +48,5 @@ export async function ensureRuntimeBundle(projectRoot: string): Promise<void> {
|
|
|
45
48
|
export const afterNavigate = (cb) => window.__grimoire_afterNavigate__?.(cb);
|
|
46
49
|
`.trim(),
|
|
47
50
|
);
|
|
48
|
-
|
|
51
|
+
logger.info("grimoire client shim written");
|
|
49
52
|
}
|
package/src/integrations/vite.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function grimoire(options: { routes?: string } = {}): Plugin {
|
|
|
22
22
|
: join(process.cwd(), options.routes ?? "src/routes");
|
|
23
23
|
|
|
24
24
|
// client entry
|
|
25
|
-
vite.middlewares.use("/__grimoire__/client.js", async (
|
|
25
|
+
vite.middlewares.use("/__grimoire__/client.js", async (_req, res) => {
|
|
26
26
|
const result = await vite.transformRequest(CLIENT_ENTRY);
|
|
27
27
|
if (!result) {
|
|
28
28
|
res.statusCode = 404;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// packages/grimoire/src/logger/logger.ts
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
LogEntry,
|
|
5
|
+
LoggerOptions,
|
|
6
|
+
LogLevel,
|
|
7
|
+
LogTransport,
|
|
8
|
+
ScopedLogger,
|
|
9
|
+
} from "./types.ts";
|
|
10
|
+
import { LOG_LEVELS } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export class Logger implements ScopedLogger {
|
|
13
|
+
private transports: LogTransport[];
|
|
14
|
+
private minLevel: number;
|
|
15
|
+
private filter?: (entry: LogEntry) => boolean;
|
|
16
|
+
private _scope: string;
|
|
17
|
+
private _worker?: string;
|
|
18
|
+
private _requestId?: string;
|
|
19
|
+
|
|
20
|
+
constructor(scope: string, options: LoggerOptions = {}, worker?: string) {
|
|
21
|
+
this._scope = scope;
|
|
22
|
+
this._worker = worker;
|
|
23
|
+
this.transports = options.transports ?? [];
|
|
24
|
+
this.minLevel =
|
|
25
|
+
LOG_LEVELS[
|
|
26
|
+
options.level ??
|
|
27
|
+
(process.env.LOG_LEVEL as LogLevel | undefined) ??
|
|
28
|
+
"info"
|
|
29
|
+
] ?? LOG_LEVELS.info;
|
|
30
|
+
this.filter = options.filter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// called by loom coordinator after forking workers
|
|
34
|
+
setWorker(name: string) {
|
|
35
|
+
this._worker = name;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private write(level: LogLevel, message: string, ...args: unknown[]) {
|
|
39
|
+
if (LOG_LEVELS[level] < this.minLevel) return;
|
|
40
|
+
|
|
41
|
+
const data =
|
|
42
|
+
args.length === 0 ? undefined : args.length === 1 ? args[0] : args;
|
|
43
|
+
|
|
44
|
+
const entry: LogEntry = {
|
|
45
|
+
level,
|
|
46
|
+
scope: this._scope,
|
|
47
|
+
message,
|
|
48
|
+
data,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
worker: this._worker,
|
|
51
|
+
requestId: this._requestId,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (this.filter && !this.filter(entry)) return;
|
|
55
|
+
for (const t of this.transports) {
|
|
56
|
+
try {
|
|
57
|
+
t.write(entry);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
debug(message: string, ...args: unknown[]) {
|
|
63
|
+
this.write("debug", message, ...args);
|
|
64
|
+
}
|
|
65
|
+
info(message: string, ...args: unknown[]) {
|
|
66
|
+
this.write("info", message, ...args);
|
|
67
|
+
}
|
|
68
|
+
warn(message: string, ...args: unknown[]) {
|
|
69
|
+
this.write("warn", message, ...args);
|
|
70
|
+
}
|
|
71
|
+
error(message: string, ...args: unknown[]) {
|
|
72
|
+
this.write("error", message, ...args);
|
|
73
|
+
}
|
|
74
|
+
fatal(message: string, ...args: unknown[]) {
|
|
75
|
+
this.write("fatal", message, ...args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
scope(name: string): ScopedLogger {
|
|
79
|
+
const child = new Logger(
|
|
80
|
+
`${this._scope}:${name}`,
|
|
81
|
+
{
|
|
82
|
+
transports: this.transports,
|
|
83
|
+
level: Object.keys(LOG_LEVELS).find(
|
|
84
|
+
(k) => LOG_LEVELS[k as LogLevel] === this.minLevel,
|
|
85
|
+
) as LogLevel,
|
|
86
|
+
filter: this.filter,
|
|
87
|
+
},
|
|
88
|
+
this._worker,
|
|
89
|
+
);
|
|
90
|
+
child._requestId = this._requestId;
|
|
91
|
+
return child;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
withRequest(requestId: string): ScopedLogger {
|
|
95
|
+
const child = new Logger(
|
|
96
|
+
this._scope,
|
|
97
|
+
{
|
|
98
|
+
transports: this.transports,
|
|
99
|
+
level: Object.keys(LOG_LEVELS).find(
|
|
100
|
+
(k) => LOG_LEVELS[k as LogLevel] === this.minLevel,
|
|
101
|
+
) as LogLevel,
|
|
102
|
+
filter: this.filter,
|
|
103
|
+
},
|
|
104
|
+
this._worker,
|
|
105
|
+
);
|
|
106
|
+
child._requestId = requestId;
|
|
107
|
+
return child;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
addTransport(t: LogTransport) {
|
|
111
|
+
this.transports.push(t);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// packages/grimoire/src/logger/request-middleware.ts
|
|
2
|
+
// Injected automatically by Grimoire before hooks.server.ts runs.
|
|
3
|
+
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import type { Logger } from "./logger.ts";
|
|
6
|
+
import type { ScopedLogger } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
declare module "@sigil-dev/grimoire" {
|
|
9
|
+
interface RequestLocals {
|
|
10
|
+
log: ScopedLogger;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createRequestMiddleware(root: Logger) {
|
|
15
|
+
return function requestMiddleware(
|
|
16
|
+
_req: Request,
|
|
17
|
+
locals: Record<string, unknown>,
|
|
18
|
+
) {
|
|
19
|
+
// 8-char hex ID — short enough to read in a terminal
|
|
20
|
+
const requestId = crypto.randomUUID().slice(0, 8);
|
|
21
|
+
locals.log = root.withRequest(requestId);
|
|
22
|
+
(locals.log as any)._requestId = requestId;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// packages/grimoire/src/logger/transports.ts
|
|
2
|
+
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import type { LogEntry, LogTransport } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
// ── colours (only applied when stdout is a TTY) ──────────────────────────────
|
|
7
|
+
|
|
8
|
+
const isTTY = process.stdout.isTTY;
|
|
9
|
+
const c = isTTY
|
|
10
|
+
? {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
bold: "\x1b[1m",
|
|
14
|
+
debug: "\x1b[90m", // grey
|
|
15
|
+
info: "\x1b[36m", // cyan
|
|
16
|
+
warn: "\x1b[33m", // yellow
|
|
17
|
+
error: "\x1b[31m", // red
|
|
18
|
+
fatal: "\x1b[35m", // magenta
|
|
19
|
+
scope: "\x1b[34m", // blue
|
|
20
|
+
req: "\x1b[32m", // green
|
|
21
|
+
}
|
|
22
|
+
: (Object.fromEntries(
|
|
23
|
+
[
|
|
24
|
+
"reset",
|
|
25
|
+
"dim",
|
|
26
|
+
"bold",
|
|
27
|
+
"debug",
|
|
28
|
+
"info",
|
|
29
|
+
"warn",
|
|
30
|
+
"error",
|
|
31
|
+
"fatal",
|
|
32
|
+
"scope",
|
|
33
|
+
"req",
|
|
34
|
+
].map((k) => [k, ""]),
|
|
35
|
+
) as Record<string, string>);
|
|
36
|
+
|
|
37
|
+
const LEVEL_LABEL: Record<string, string> = {
|
|
38
|
+
debug: "DBG",
|
|
39
|
+
info: "INF",
|
|
40
|
+
warn: "WRN",
|
|
41
|
+
error: "ERR",
|
|
42
|
+
fatal: "FTL",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function ts(timestamp: number): string {
|
|
46
|
+
return new Date(timestamp).toISOString().slice(11, 23); // HH:mm:ss.ms
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatData(data: unknown): string {
|
|
50
|
+
if (data === undefined) return "";
|
|
51
|
+
if (data instanceof Error) return `\n ${data.stack ?? data.message}`;
|
|
52
|
+
if (typeof data !== "object" || data === null) return ` ${String(data)}`;
|
|
53
|
+
|
|
54
|
+
// flat key=value for simple objects, JSON block only for nested/complex
|
|
55
|
+
const entries = Object.entries(data as Record<string, unknown>);
|
|
56
|
+
const isFlat = entries.every(([, v]) => typeof v !== "object" || v === null);
|
|
57
|
+
if (isFlat) {
|
|
58
|
+
return ` ${entries.map(([k, v]) => `${colors.dim(`${k}=`)}${v}`).join(" ")}`;
|
|
59
|
+
}
|
|
60
|
+
// nested — indent but don't pretty-print the whole thing
|
|
61
|
+
return `\n ${JSON.stringify(data).replace(/,/g, ", ")}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── ConsoleTransport ─────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export class ConsoleTransport implements LogTransport {
|
|
67
|
+
write(entry: LogEntry) {
|
|
68
|
+
const levelColor = c[entry.level] ?? c.info;
|
|
69
|
+
const label = LEVEL_LABEL[entry.level] ?? entry.level.toUpperCase();
|
|
70
|
+
|
|
71
|
+
const worker = entry.worker ? `${c.dim}[${entry.worker}]${c.reset} ` : "";
|
|
72
|
+
const requestId = entry.requestId
|
|
73
|
+
? `${c.req}#${entry.requestId.slice(0, 8)}${c.reset} `
|
|
74
|
+
: "";
|
|
75
|
+
const scope = `${c.scope}${entry.scope}${c.reset}`;
|
|
76
|
+
const time = `${c.dim}${ts(entry.timestamp)}${c.reset}`;
|
|
77
|
+
const lvl = `${levelColor}${c.bold}${label}${c.reset}`;
|
|
78
|
+
const msg = entry.message;
|
|
79
|
+
const data = formatData(entry.data);
|
|
80
|
+
|
|
81
|
+
const line = `${time} ${lvl} ${worker}${requestId}${scope} ${msg}${data}`;
|
|
82
|
+
|
|
83
|
+
if (entry.level === "error" || entry.level === "fatal") {
|
|
84
|
+
process.stderr.write(`${line}\n`);
|
|
85
|
+
} else {
|
|
86
|
+
process.stdout.write(`${line}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── FileTransport ────────────────────────────────────────────────────────────
|
|
92
|
+
// Writes newline-delimited JSON for ingestion by log collectors.
|
|
93
|
+
|
|
94
|
+
export class FileTransport implements LogTransport {
|
|
95
|
+
constructor(private path: string) {}
|
|
96
|
+
|
|
97
|
+
write(entry: LogEntry) {
|
|
98
|
+
try {
|
|
99
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
|
|
100
|
+
} catch {
|
|
101
|
+
// if the file write fails we can't really do anything useful
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── NullTransport ────────────────────────────────────────────────────────────
|
|
107
|
+
// For tests — captures entries instead of printing them.
|
|
108
|
+
|
|
109
|
+
export class NullTransport implements LogTransport {
|
|
110
|
+
readonly entries: LogEntry[] = [];
|
|
111
|
+
write(entry: LogEntry) {
|
|
112
|
+
this.entries.push(entry);
|
|
113
|
+
}
|
|
114
|
+
clear() {
|
|
115
|
+
this.entries.length = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
|
2
|
+
|
|
3
|
+
export const LOG_LEVELS: Record<LogLevel, number> = {
|
|
4
|
+
debug: 0,
|
|
5
|
+
info: 1,
|
|
6
|
+
warn: 2,
|
|
7
|
+
error: 3,
|
|
8
|
+
fatal: 4,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface LogEntry {
|
|
12
|
+
level: LogLevel;
|
|
13
|
+
scope: string;
|
|
14
|
+
message: string;
|
|
15
|
+
data?: unknown;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
worker?: string; // injected by loom, e.g. "violet"
|
|
18
|
+
requestId?: string; // injected per-request by middleware
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LogTransport {
|
|
22
|
+
write(entry: LogEntry): void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ScopedLogger {
|
|
26
|
+
debug(message: string, ...args: unknown[]): void;
|
|
27
|
+
info(message: string, ...args: unknown[]): void;
|
|
28
|
+
warn(message: string, ...args: unknown[]): void;
|
|
29
|
+
error(message: string, ...args: unknown[]): void;
|
|
30
|
+
fatal(message: string, ...args: unknown[]): void;
|
|
31
|
+
scope(name: string): ScopedLogger;
|
|
32
|
+
withRequest(requestId: string): ScopedLogger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LoggerOptions {
|
|
36
|
+
transports?: LogTransport[];
|
|
37
|
+
level?: LogLevel;
|
|
38
|
+
filter?: (entry: LogEntry) => boolean;
|
|
39
|
+
}
|
package/src/rendering/hydrate.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
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";
|
|
16
|
-
import { initRouter } from "../client/router.ts";
|
|
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;
|
|
26
|
-
|
|
27
|
-
const stateEl = document.getElementById("__grimoire_state__");
|
|
28
|
-
let initialDispose: (() => void) | undefined;
|
|
29
|
-
|
|
30
|
-
if (stateEl) {
|
|
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);
|
|
44
|
-
|
|
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
|
-
}
|
|
52
|
-
|
|
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";
|
|
65
|
-
|
|
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
|
-
}
|
|
83
|
-
|
|
84
|
-
return pageDiv;
|
|
85
|
-
};
|
|
86
|
-
|
|
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
|
-
}
|
|
101
|
-
|
|
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
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!slot.hasChildNodes() && ssrClones.length > 0) {
|
|
113
|
-
slot.replaceChildren(...ssrClones);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
initRouter(routes, layouts, initialDispose);
|
|
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";
|
|
16
|
+
import { initRouter } from "../client/router.ts";
|
|
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;
|
|
26
|
+
|
|
27
|
+
const stateEl = document.getElementById("__grimoire_state__");
|
|
28
|
+
let initialDispose: (() => void) | undefined;
|
|
29
|
+
|
|
30
|
+
if (stateEl) {
|
|
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);
|
|
44
|
+
|
|
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
|
+
}
|
|
52
|
+
|
|
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";
|
|
65
|
+
|
|
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
|
+
}
|
|
83
|
+
|
|
84
|
+
return pageDiv;
|
|
85
|
+
};
|
|
86
|
+
|
|
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
|
+
}
|
|
101
|
+
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!slot.hasChildNodes() && ssrClones.length > 0) {
|
|
113
|
+
slot.replaceChildren(...ssrClones);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
initRouter(routes, layouts, initialDispose);
|
package/src/rendering/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { SafeHtml } from "@sigil-dev/runtime";
|
|
2
|
-
import { randomBytes } from "crypto";
|
|
3
3
|
import { findClosestError, type MatchedRoute } from "../routing/router";
|
|
4
4
|
import type { RouteFile } from "../routing/scanner";
|
|
5
5
|
import { isErrorResult } from "../sentinels/error.ts";
|