@sigil-dev/grimoire 0.7.7 → 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 +4 -3
- 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 +2 -3
- 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/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.8.2",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.ts",
|
|
9
9
|
"./server": "./server.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"./vite": "./src/integrations/vite.ts",
|
|
15
15
|
"./bun": "./src/rendering/ssrPlugin.ts",
|
|
16
16
|
"./env/public": "./src/env/public.ts",
|
|
17
|
-
"./env/private": "./src/env/private.ts"
|
|
17
|
+
"./env/private": "./src/env/private.ts",
|
|
18
|
+
"./logger": "./src/logger/index.ts"
|
|
18
19
|
},
|
|
19
20
|
"bin": {
|
|
20
21
|
"grimoire": "src/sync.ts"
|
|
@@ -32,13 +33,13 @@
|
|
|
32
33
|
"vite": "^8.0.16"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"@sigil-dev/compiler": "0.
|
|
36
|
-
"@sigil-dev/runtime": "0.
|
|
36
|
+
"@sigil-dev/compiler": "0.8.2",
|
|
37
|
+
"@sigil-dev/runtime": "0.8.2",
|
|
37
38
|
"typescript": "^5"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/bun": "latest",
|
|
41
|
-
"@sigil-dev/compiler": "0.
|
|
42
|
-
"@sigil-dev/runtime": "0.
|
|
42
|
+
"@sigil-dev/compiler": "0.8.2",
|
|
43
|
+
"@sigil-dev/runtime": "0.8.2"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/server.ts
CHANGED
package/src/client/router.ts
CHANGED
|
@@ -10,7 +10,7 @@ let disposeCurrentPage: (() => void) | null = null;
|
|
|
10
10
|
let currentPath = location.pathname;
|
|
11
11
|
|
|
12
12
|
// C1: Navigation hooks
|
|
13
|
-
let beforeNavigateCb: ((url: URL) => boolean |
|
|
13
|
+
let beforeNavigateCb: ((url: URL) => boolean | undefined) | null = null;
|
|
14
14
|
let onNavigateCb: ((url: URL) => void) | null = null;
|
|
15
15
|
let afterNavigateCb: ((url: URL) => void) | null = null;
|
|
16
16
|
|
|
@@ -36,7 +36,7 @@ export async function hmrRerender(): Promise<void> {
|
|
|
36
36
|
|
|
37
37
|
// (globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
38
38
|
|
|
39
|
-
export function beforeNavigate(cb: (url: URL) => boolean |
|
|
39
|
+
export function beforeNavigate(cb: (url: URL) => boolean | undefined): void {
|
|
40
40
|
beforeNavigateCb = cb;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -120,7 +120,7 @@ function buildRouteTree(state: typeof lastState): any {
|
|
|
120
120
|
(l) =>
|
|
121
121
|
l.path === "/" ||
|
|
122
122
|
pattern === l.path ||
|
|
123
|
-
pattern.startsWith(l.path
|
|
123
|
+
pattern.startsWith(`${l.path}/`),
|
|
124
124
|
)
|
|
125
125
|
.sort((a, b) => a.path.length - b.path.length);
|
|
126
126
|
|
|
@@ -212,6 +212,7 @@ export async function navigate(path: string) {
|
|
|
212
212
|
document.title = head?.title ?? document.title;
|
|
213
213
|
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
214
214
|
currentPath = path;
|
|
215
|
+
history.pushState(null, "", path);
|
|
215
216
|
restoreScrollPosition(path);
|
|
216
217
|
afterNavigateCb?.(url);
|
|
217
218
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { dirname,
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import type { DevGraph } from "./graph";
|
|
5
|
-
import { normalizePath } from "./paths";
|
|
6
4
|
|
|
7
5
|
const RUNTIME_URL = "/__grimoire__/runtime.js";
|
|
8
6
|
const MODULES_BASE = "/__grimoire__/m/";
|
|
9
7
|
const DEPS_BASE = "/__grimoire__/dep/";
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* Rewrite a single import specifier to a browser-servable URL.
|
|
13
10
|
* project-relative → /__grimoire__/m/...
|
|
@@ -72,7 +69,7 @@ function rewriteImports(
|
|
|
72
69
|
): string {
|
|
73
70
|
return code.replace(
|
|
74
71
|
/(from\s+|import\s+)(["'])([^"']+)\2/g,
|
|
75
|
-
(
|
|
72
|
+
(_match, keyword, quote, specifier) => {
|
|
76
73
|
const rewritten = rewriteSpecifier(specifier, filePath, projectRoot);
|
|
77
74
|
return `${keyword}${quote}${rewritten}${quote}`;
|
|
78
75
|
},
|
|
@@ -119,11 +116,6 @@ export async function compileForBrowser(
|
|
|
119
116
|
plugins: [[sigilPlugin, { mode: "dom", hash }]],
|
|
120
117
|
filename: filePath,
|
|
121
118
|
});
|
|
122
|
-
|
|
123
|
-
console.log(
|
|
124
|
-
"[compile] babel output first 500 chars:",
|
|
125
|
-
babelResult?.code?.slice(0, 500),
|
|
126
|
-
);
|
|
127
119
|
// strip TypeScript types
|
|
128
120
|
const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
|
|
129
121
|
let js = transpiler.transformSync(babelResult?.code ?? "");
|
|
@@ -157,13 +149,12 @@ export async function compileForBrowser(
|
|
|
157
149
|
// inject scoped CSS at runtime
|
|
158
150
|
if (css) {
|
|
159
151
|
//biome-ignore lint: bro shut up already
|
|
160
|
-
js =
|
|
161
|
-
`if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
|
|
152
|
+
js = `if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
|
|
162
153
|
const __s = document.createElement('style');
|
|
163
154
|
__s.id = 'sigil-${hash}';
|
|
164
155
|
__s.textContent = ${JSON.stringify(css)};
|
|
165
156
|
document.head.appendChild(__s);
|
|
166
|
-
}\n
|
|
157
|
+
}\n${js}`;
|
|
167
158
|
}
|
|
168
159
|
|
|
169
160
|
// rewrite imports to browser-servable URLs
|
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,5 +1,3 @@
|
|
|
1
|
-
//@ts-expect-error compiler generated
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
claim,
|
|
5
3
|
getHydrationNodes,
|
|
@@ -7,6 +5,7 @@ import {
|
|
|
7
5
|
popHydrationNodes,
|
|
8
6
|
pushHydrationNodes,
|
|
9
7
|
} from "@sigil-dev/runtime";
|
|
8
|
+
//@ts-expect-error compiler generated
|
|
10
9
|
import { layouts, routes } from "#grimoire-routes";
|
|
11
10
|
import {
|
|
12
11
|
afterNavigate,
|
|
@@ -39,7 +38,7 @@ if (stateEl) {
|
|
|
39
38
|
(l: any) =>
|
|
40
39
|
l.path === "/" ||
|
|
41
40
|
state.pattern === l.path ||
|
|
42
|
-
state.pattern.startsWith(l.path
|
|
41
|
+
state.pattern.startsWith(`${l.path}/`),
|
|
43
42
|
)
|
|
44
43
|
.sort((a: any, b: any) => a.path.length - b.path.length);
|
|
45
44
|
|
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";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { transformSync } from "@babel/core";
|
|
3
3
|
import sigilPlugin from "@sigil-dev/compiler/babel";
|
|
4
|
+
import { log } from "../logger/instance";
|
|
4
5
|
import type { GrimoirePlugin } from "../types";
|
|
5
6
|
|
|
7
|
+
const logger = log.scope("scope");
|
|
6
8
|
let registered = false;
|
|
7
9
|
|
|
8
10
|
export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
|
|
@@ -27,7 +29,7 @@ export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
|
|
|
27
29
|
|
|
28
30
|
build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
|
|
29
31
|
if (path.includes("index.tsx"))
|
|
30
|
-
|
|
32
|
+
logger.debug("[ssr-plugin] onLoad:", path);
|
|
31
33
|
const source = await Bun.file(path).text();
|
|
32
34
|
|
|
33
35
|
// node_modules and .grimoire files are plain TypeScript (no sigil JSX).
|
|
@@ -40,7 +42,7 @@ export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
if (process.env.SIGIL_VERBOSE) {
|
|
43
|
-
|
|
45
|
+
logger.debug("[sigil-ssr] XFORM:", path);
|
|
44
46
|
}
|
|
45
47
|
const hash = createHash("md5").update(path).digest("hex").slice(0, 8);
|
|
46
48
|
const result = transformSync(source, {
|
package/src/routing/router.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function matchRoute(tree: RouteTree, url: URL): MatchedRoute | null {
|
|
|
34
34
|
const layoutMatches = (l: RouteFile) =>
|
|
35
35
|
l.path === "/" ||
|
|
36
36
|
route.path === l.path ||
|
|
37
|
-
route.path.startsWith(l.path
|
|
37
|
+
route.path.startsWith(`${l.path}/`);
|
|
38
38
|
|
|
39
39
|
const layouts = tree.layouts
|
|
40
40
|
.filter((l) => l.type === "layout" && layoutMatches(l))
|
|
@@ -64,7 +64,7 @@ export function findClosestError(
|
|
|
64
64
|
while (segments.length >= 0) {
|
|
65
65
|
const prefix = `/${segments.join("/")}`;
|
|
66
66
|
const error = errors.find(
|
|
67
|
-
(e) => e.path === prefix || e.path === prefix
|
|
67
|
+
(e) => e.path === prefix || e.path === `${prefix}/`,
|
|
68
68
|
);
|
|
69
69
|
if (error) return error;
|
|
70
70
|
if (segments.length === 0) break;
|
package/src/routing/scanner.ts
CHANGED
|
@@ -81,7 +81,7 @@ export async function scanRoutes(
|
|
|
81
81
|
else if (name === "+error") type = "error";
|
|
82
82
|
else type = "simple";
|
|
83
83
|
|
|
84
|
-
const clientPath =
|
|
84
|
+
const clientPath = `/${relative(viteRoot, filePath).replace(/\\/g, "/")}`;
|
|
85
85
|
|
|
86
86
|
const routeFile: RouteFile = {
|
|
87
87
|
path: pattern,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
1
2
|
import { transformSync } from "@babel/core";
|
|
2
3
|
import sigilPlugin from "@sigil-dev/compiler/babel";
|
|
3
|
-
import { basename, dirname, join, relative, resolve } from "path";
|
|
4
4
|
import type { GrimoirePlugin } from "../types";
|
|
5
5
|
import type { RouteFile } from "./scanner";
|
|
6
6
|
|
package/src/server/build.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { cwd } from "node:process";
|
|
1
4
|
import { sigil } from "@sigil-dev/compiler/bun";
|
|
2
|
-
import { mkdir } from "fs/promises";
|
|
3
|
-
import { isAbsolute, join } from "path";
|
|
4
|
-
import { cwd } from "process";
|
|
5
5
|
import { normalizePath } from "../dev/paths";
|
|
6
6
|
import { generateManifest } from "../routing/manifest-gen";
|
|
7
7
|
import type { RouteTree } from "../routing/scanner";
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { type Subprocess, spawn } from "bun";
|
|
2
|
-
import { join } from "path";
|
|
3
3
|
import { matchRoute } from "../routing/router";
|
|
4
4
|
import { type RouteFile, type RouteTree, scanRoutes } from "../routing/scanner";
|
|
5
5
|
import type {
|
|
6
6
|
CoordinatorContext,
|
|
7
7
|
GrimoireConfig,
|
|
8
8
|
GrimoirePlugin,
|
|
9
|
-
Route,
|
|
10
9
|
WorkerDescriptor,
|
|
11
10
|
WorkerMode,
|
|
12
11
|
} from "../types";
|
|
@@ -194,7 +193,7 @@ export async function startCoordinator(options: CoordinatorOptions) {
|
|
|
194
193
|
runHook(plugins, "onWorkerReady", descriptor);
|
|
195
194
|
}
|
|
196
195
|
},
|
|
197
|
-
onExit(
|
|
196
|
+
onExit(_proc, exitCode) {
|
|
198
197
|
const intentional = stopping;
|
|
199
198
|
runHook(
|
|
200
199
|
plugins,
|
|
@@ -204,7 +203,7 @@ export async function startCoordinator(options: CoordinatorOptions) {
|
|
|
204
203
|
);
|
|
205
204
|
if (!intentional) {
|
|
206
205
|
console.error(
|
|
207
|
-
`[grimoire] worker ${descriptor.name ?? descriptor.mode
|
|
206
|
+
`[grimoire] worker ${descriptor.name ?? `${descriptor.mode}-${descriptor.index}`} crashed (exit ${exitCode}), respawning...`,
|
|
208
207
|
);
|
|
209
208
|
// respawn: same descriptor, same port
|
|
210
209
|
// (simplified — plugin-scale can override this behavior)
|
|
@@ -296,7 +295,7 @@ function defaultSerialize(locals: App.Locals, secret: string): string {
|
|
|
296
295
|
return Buffer.from(JSON.stringify({ payload, sig })).toString("base64");
|
|
297
296
|
}
|
|
298
297
|
|
|
299
|
-
function
|
|
298
|
+
function _defaultDeserialize(raw: string, secret: string): App.Locals {
|
|
300
299
|
const { payload, sig } = JSON.parse(Buffer.from(raw, "base64").toString());
|
|
301
300
|
const expected = new Bun.CryptoHasher("sha256")
|
|
302
301
|
.update(secret + payload)
|
package/src/server/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { HMR_CLIENT_SOURCE } from "../dev/hmr-client.ts";
|
|
|
6
6
|
import { HmrServer, handleChange } from "../dev/hmr-server.ts";
|
|
7
7
|
import { makeDevLoader } from "../dev/loader.ts";
|
|
8
8
|
import { startWatcher } from "../dev/watcher.ts";
|
|
9
|
+
import { log } from "../logger/instance.ts";
|
|
9
10
|
import { renderRoute } from "../rendering";
|
|
10
11
|
import { registerSSRPlugin } from "../rendering/ssrPlugin";
|
|
11
12
|
import { findClosestError, matchRoute } from "../routing/router";
|
|
@@ -13,7 +14,7 @@ import { isErrorResult } from "../sentinels/error.ts";
|
|
|
13
14
|
import { isFailResult } from "../sentinels/fail.ts";
|
|
14
15
|
import { isRedirectResult } from "../sentinels/redirect.ts";
|
|
15
16
|
import type { GrimoireConfig, WsRouteHandler } from "../types";
|
|
16
|
-
import { buildProject
|
|
17
|
+
import { buildProject } from "./build";
|
|
17
18
|
import { createCookies } from "./cookie-utils";
|
|
18
19
|
import type {
|
|
19
20
|
Handle,
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
} from "./hooks";
|
|
26
27
|
import { runDeserializeLocals, runHook, runRequestHooks } from "./plugins";
|
|
27
28
|
|
|
29
|
+
const logger = log.scope("server");
|
|
28
30
|
/**
|
|
29
31
|
* Try to load hooks.server.ts from the project root.
|
|
30
32
|
*/
|
|
@@ -226,33 +228,29 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
226
228
|
|
|
227
229
|
const depsDir = join(process.cwd(), ".grimoire/deps");
|
|
228
230
|
mkdirSync(depsDir, { recursive: true });
|
|
229
|
-
const outFile = join(depsDir, depName
|
|
231
|
+
const outFile = join(depsDir, `${depName}.js`);
|
|
230
232
|
|
|
231
233
|
if (!(await Bun.file(outFile).exists())) {
|
|
232
|
-
|
|
234
|
+
logger.debug("bundling dep:", pkgName);
|
|
233
235
|
try {
|
|
234
236
|
const entry = Bun.resolveSync(pkgName, process.cwd());
|
|
235
237
|
const result = await Bun.build({
|
|
236
238
|
entrypoints: [entry],
|
|
237
239
|
outdir: depsDir,
|
|
238
|
-
naming: depName
|
|
240
|
+
naming: `${depName}.js`,
|
|
239
241
|
target: "browser",
|
|
240
242
|
format: "esm",
|
|
241
243
|
minify: false,
|
|
242
244
|
external: ["@sigil-dev/runtime"],
|
|
243
245
|
});
|
|
244
246
|
if (!result.success) {
|
|
245
|
-
|
|
246
|
-
"[sigil hmr] dep bundle failed:",
|
|
247
|
-
pkgName,
|
|
248
|
-
result.logs,
|
|
249
|
-
);
|
|
247
|
+
logger.error("dep bundle failed:", pkgName, result.logs);
|
|
250
248
|
return new Response(
|
|
251
249
|
`throw new Error("failed to bundle dep: ${pkgName}")`,
|
|
252
250
|
{ headers: { "Content-Type": "application/javascript" } },
|
|
253
251
|
);
|
|
254
252
|
}
|
|
255
|
-
|
|
253
|
+
logger.debug("bundled dep:", pkgName);
|
|
256
254
|
} catch (e: any) {
|
|
257
255
|
return new Response(
|
|
258
256
|
`throw new Error("dep not found: ${pkgName}: ${e.message}")`,
|
|
@@ -648,6 +646,6 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
648
646
|
hostname: server.hostname,
|
|
649
647
|
stop: () => server.stop(),
|
|
650
648
|
});
|
|
651
|
-
console.log(`Grimoire running at http://${host}:${port}`);
|
|
649
|
+
// console.log(`Grimoire running at http://${host}:${port}`);
|
|
652
650
|
return server;
|
|
653
651
|
}
|
package/src/server/worker.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { GrimoireConfig, GrimoirePlugin, WorkerMode } from "../types";
|
|
3
3
|
import { createServer } from "./index";
|
|
4
|
-
import { runDeserializeLocals } from "./plugins";
|
|
5
4
|
|
|
6
5
|
export interface WorkerOptions {
|
|
7
6
|
config: GrimoireConfig;
|
package/test/context.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// packages/grimoire/src/context.test.ts
|
|
2
|
-
import {
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
3
|
import { createContext, getContext, setContext } from "@sigil-dev/runtime";
|
|
4
4
|
import { runWithContext } from "../src/server/context";
|
|
5
5
|
|
|
6
6
|
const UserKey = createContext<string>();
|
|
7
|
-
const
|
|
7
|
+
const _ThemeKey = createContext<string>();
|
|
8
8
|
|
|
9
9
|
// 1. Basic: does it work at all
|
|
10
10
|
test("getContext returns value set in same context", async () => {
|
package/test/hydration.test.ts
CHANGED
|
@@ -40,7 +40,7 @@ function runHydrate(routes: Record<string, (props: any) => any>) {
|
|
|
40
40
|
|
|
41
41
|
// simulate initRouter (same logic as router.ts)
|
|
42
42
|
let clickHandlerAttached = false;
|
|
43
|
-
function initRouter(
|
|
43
|
+
function initRouter(_routes: Record<string, (props: any) => any>) {
|
|
44
44
|
document.addEventListener("click", () => {});
|
|
45
45
|
clickHandlerAttached = true;
|
|
46
46
|
}
|
package/test/middleware.test.ts
CHANGED
|
@@ -42,7 +42,7 @@ describe("sequence()", () => {
|
|
|
42
42
|
const handler = sequence(a, b);
|
|
43
43
|
const res = await handler({
|
|
44
44
|
event: fakeEvent(),
|
|
45
|
-
resolve: async (
|
|
45
|
+
resolve: async (_evt) => new Response("OK"),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
expect(order).toEqual(["a-before", "b-before", "b-after", "a-after"]);
|
package/test/rendering.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir as osTmpDir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir as osTmpDir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { renderRoute } from "../src/rendering";
|
|
6
6
|
import { matchRoute } from "../src/routing/router";
|
|
7
7
|
import { scanRoutes } from "../src/routing/scanner";
|
package/test/scanning.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { filePathToRoutePath, scanRoutes } from "../src/routing/scanner";
|
|
6
6
|
|
|
7
7
|
describe("File scanning", () => {
|
package/test/server.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { createServer } from "../src/server";
|
|
6
6
|
|
|
7
7
|
// ── Sandbox note ─────────────────────────────────────────────────────────────
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import type { RouteFile } from "../src/routing/scanner";
|
|
6
6
|
import { transformRoutes } from "../src/routing/transform-routes";
|
|
7
7
|
|