@sigil-dev/grimoire 0.7.5 → 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/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +21 -20
- package/package.json +13 -7
- package/preload.js +3 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/server.ts +13 -13
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +254 -40
- 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/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +1 -0
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +111 -18
- package/src/rendering/index.ts +263 -153
- package/src/rendering/ssrPlugin.ts +59 -39
- package/src/routing/manifest-gen.ts +18 -2
- package/src/routing/router.ts +94 -83
- package/src/routing/scanner.ts +26 -14
- package/src/routing/transform-routes.ts +68 -68
- package/src/server/build.ts +225 -76
- package/src/server/coordinator.ts +9 -0
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +388 -104
- package/src/typegen/index.ts +30 -14
- package/src/types.ts +12 -2
- package/test/middleware.test.ts +6 -4
- package/test/rendering.test.ts +510 -356
- package/test/routing.test.ts +36 -0
- package/test/scanning.test.ts +39 -8
- package/test/scope.test.ts +24 -8
- package/test/server.test.ts +27 -7
- package/test/streaming.test.ts +117 -98
- package/test/typegen.test.ts +52 -24
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export const HMR_CLIENT_SOURCE = `
|
|
2
|
+
(function() {
|
|
3
|
+
let version = 0;
|
|
4
|
+
|
|
5
|
+
const staleRoutes = new Set();
|
|
6
|
+
|
|
7
|
+
async function applySwap(p) {
|
|
8
|
+
try {
|
|
9
|
+
const url = '/__grimoire__/m/' + p.modUrl + '?v=' + p.version;
|
|
10
|
+
const mod = await import(url);
|
|
11
|
+
|
|
12
|
+
// update route map — accessed via grimoire's initRouter export
|
|
13
|
+
if (window.__grimoire_routes__) {
|
|
14
|
+
window.__grimoire_routes__[p.pattern] = mod.default;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// re-render if currently on this pattern
|
|
18
|
+
const currentPattern = window.__grimoire_current_pattern__;
|
|
19
|
+
if (currentPattern === p.pattern && window.__grimoire_rerender__) {
|
|
20
|
+
await window.__grimoire_rerender__();
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error('[sigil hmr] swap failed, reloading:', e);
|
|
24
|
+
location.reload();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function refetchCurrent() {
|
|
29
|
+
if (window.__grimoire_navigate__) {
|
|
30
|
+
await window.__grimoire_navigate__(location.pathname, true);
|
|
31
|
+
} else {
|
|
32
|
+
location.reload();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyCss(hash, text) {
|
|
37
|
+
let el = document.getElementById('sigil-' + hash);
|
|
38
|
+
if (!el) {
|
|
39
|
+
el = document.createElement('style');
|
|
40
|
+
el.id = 'sigil-' + hash;
|
|
41
|
+
document.head.appendChild(el);
|
|
42
|
+
}
|
|
43
|
+
el.textContent = text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function connect() {
|
|
47
|
+
const ws = new WebSocket(
|
|
48
|
+
(location.protocol === 'https:' ? 'wss' : 'ws') +
|
|
49
|
+
'://' + location.host + '/__grimoire__/hmr'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
ws.onopen = () => {
|
|
53
|
+
console.log('[sigil hmr] connected');
|
|
54
|
+
document.getElementById('__grimoire_overlay__')?.remove();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
ws.onmessage = async (ev) => {
|
|
58
|
+
const msg = JSON.parse(ev.data);
|
|
59
|
+
if (msg.type === 'error') { showOverlay(msg.message, msg.loc); return; }
|
|
60
|
+
if (msg.v !== undefined && msg.v <= version) return;
|
|
61
|
+
if (msg.v !== undefined) version = msg.v;
|
|
62
|
+
|
|
63
|
+
if (msg.type === 'batch') {
|
|
64
|
+
document.getElementById('__grimoire_overlay__')?.remove();
|
|
65
|
+
|
|
66
|
+
let hasReload = false;
|
|
67
|
+
let swapPayload = null;
|
|
68
|
+
const cssPayloads = [];
|
|
69
|
+
const stalePatterns = [];
|
|
70
|
+
|
|
71
|
+
for (const p of msg.payloads) {
|
|
72
|
+
if (p.kind === 'reload') { hasReload = true; break; }
|
|
73
|
+
if (p.kind === 'css') cssPayloads.push(p);
|
|
74
|
+
if (p.kind === 'swap') swapPayload = p;
|
|
75
|
+
if (p.kind === 'stale') stalePatterns.push(...p.patterns);
|
|
76
|
+
if (p.kind === 'invalidate') refetchCurrent();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (hasReload) { location.reload(); return; }
|
|
80
|
+
|
|
81
|
+
// apply CSS instantly
|
|
82
|
+
for (const p of cssPayloads) applyCss(p.hash, p.text);
|
|
83
|
+
|
|
84
|
+
// mark stale routes
|
|
85
|
+
for (const pattern of stalePatterns) staleRoutes.add(pattern);
|
|
86
|
+
|
|
87
|
+
// apply swap
|
|
88
|
+
if (swapPayload) {
|
|
89
|
+
await applySwap(swapPayload);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
ws.onclose = () => {
|
|
95
|
+
console.log('[sigil hmr] disconnected, reconnecting...');
|
|
96
|
+
const retry = setInterval(async () => {
|
|
97
|
+
try {
|
|
98
|
+
await fetch('/', { method: 'HEAD' });
|
|
99
|
+
clearInterval(retry);
|
|
100
|
+
location.reload();
|
|
101
|
+
} catch {}
|
|
102
|
+
}, 300);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripAnsi(str) {
|
|
107
|
+
return str.replace(/\u001b[[0-9;]*m/g, '');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function unescapeHtml(str) {
|
|
111
|
+
let result = str;
|
|
112
|
+
// run twice to handle double-escaped entities
|
|
113
|
+
for (let i = 0; i < 2; i++) {
|
|
114
|
+
result = result
|
|
115
|
+
.replace(/&/g, '&')
|
|
116
|
+
.replace(/</g, '<')
|
|
117
|
+
.replace(/>/g, '>')
|
|
118
|
+
.replace(/"/g, '"')
|
|
119
|
+
.replace(/'/g, "'");
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function showOverlay(message, loc) {
|
|
125
|
+
document.getElementById('__grimoire_overlay__')?.remove();
|
|
126
|
+
const clean = unescapeHtml(stripAnsi(message));
|
|
127
|
+
const d = document.createElement('div');
|
|
128
|
+
d.id = '__grimoire_overlay__';
|
|
129
|
+
d.style.cssText = [
|
|
130
|
+
'position:fixed;inset:0;z-index:99999',
|
|
131
|
+
'background:rgba(15,15,20,.94)',
|
|
132
|
+
'color:#ff8a8a',
|
|
133
|
+
'font:13px/1.6 monospace',
|
|
134
|
+
'padding:40px',
|
|
135
|
+
'overflow:auto',
|
|
136
|
+
'white-space:pre-wrap',
|
|
137
|
+
].join(';');
|
|
138
|
+
|
|
139
|
+
d.textContent = '[sigil] compile error\\n\\n' + clean;
|
|
140
|
+
|
|
141
|
+
if (loc) {
|
|
142
|
+
const link = document.createElement('a');
|
|
143
|
+
link.href = '/__grimoire__/open?file=' + encodeURIComponent(loc.file) +
|
|
144
|
+
'&line=' + loc.line + '&col=' + loc.col;
|
|
145
|
+
link.style.cssText = 'color:#88aaff;text-decoration:underline;cursor:pointer;display:block;margin:8px 0 16px';
|
|
146
|
+
link.textContent = loc.file + ':' + loc.line + ':' + loc.col;
|
|
147
|
+
link.onclick = (e) => { e.preventDefault(); fetch(link.href); };
|
|
148
|
+
d.appendChild(link);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
d.title = 'Click anywhere to dismiss';
|
|
152
|
+
d.onclick = (ev) => { if (ev.target === d) d.remove(); };
|
|
153
|
+
document.body.appendChild(d);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
connect();
|
|
157
|
+
})();
|
|
158
|
+
`;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import type { ServerWebSocket } from "bun";
|
|
4
|
+
import { compileForBrowser } from "./compile-module";
|
|
5
|
+
import type { DevGraph } from "./graph";
|
|
6
|
+
import { normalizePath } from "./paths";
|
|
7
|
+
import { safeRead } from "./watcher";
|
|
8
|
+
|
|
9
|
+
export class HmrServer {
|
|
10
|
+
private clients = new Set<ServerWebSocket<any>>();
|
|
11
|
+
private v = 0;
|
|
12
|
+
get clientCount() {
|
|
13
|
+
return this.clients.size;
|
|
14
|
+
}
|
|
15
|
+
addClient(ws: ServerWebSocket<any>) {
|
|
16
|
+
this.clients.add(ws);
|
|
17
|
+
ws.send(JSON.stringify({ type: "connected" }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
removeClient(ws: ServerWebSocket<any>) {
|
|
21
|
+
this.clients.delete(ws);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
send(msg: object) {
|
|
25
|
+
const s = JSON.stringify(msg);
|
|
26
|
+
for (const c of this.clients) c.send(s);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
batch(payloads: object[]) {
|
|
30
|
+
this.send({ type: "batch", payloads, v: ++this.v });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
reload() {
|
|
34
|
+
console.log("[sigil hmr] sending reload to", this.clients.size, "clients");
|
|
35
|
+
this.batch([{ kind: "reload" }]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
error(message: string, loc?: { file: string; line: number; col: number }) {
|
|
39
|
+
this.send({ type: "error", message, loc });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractLoc(
|
|
44
|
+
e: any,
|
|
45
|
+
filePath: string,
|
|
46
|
+
): { file: string; line: number; col: number } | undefined {
|
|
47
|
+
// Babel errors have e.loc.line / e.loc.column
|
|
48
|
+
if (e?.loc?.line) {
|
|
49
|
+
return {
|
|
50
|
+
file: filePath,
|
|
51
|
+
line: e.loc.line,
|
|
52
|
+
col: e.loc.column ?? 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// fallback: parse "file:line:col" from message
|
|
56
|
+
const m = String(e?.message ?? "").match(/:(\d+):(\d+)$/);
|
|
57
|
+
if (m) {
|
|
58
|
+
return {
|
|
59
|
+
file: filePath,
|
|
60
|
+
line: Number(m[1]),
|
|
61
|
+
col: Number(m[2]),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function compileAndSwap(
|
|
68
|
+
filePath: string,
|
|
69
|
+
graph: DevGraph,
|
|
70
|
+
hmr: HmrServer,
|
|
71
|
+
projectRoot: string,
|
|
72
|
+
routeTree: any,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const key = normalizePath(filePath);
|
|
75
|
+
const node = graph.get(key);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await compileForBrowser(filePath, projectRoot);
|
|
79
|
+
|
|
80
|
+
// write to dev-modules cache
|
|
81
|
+
const devModulesDir = join(projectRoot, ".grimoire/dev-modules");
|
|
82
|
+
mkdirSync(devModulesDir, { recursive: true });
|
|
83
|
+
const { createHash } = await import("node:crypto");
|
|
84
|
+
const fileHash = createHash("md5")
|
|
85
|
+
.update(filePath)
|
|
86
|
+
.digest("hex")
|
|
87
|
+
.slice(0, 8);
|
|
88
|
+
const v = graph.bump(key);
|
|
89
|
+
const cachePath = join(devModulesDir, `${fileHash}_v${v}.js`);
|
|
90
|
+
writeFileSync(cachePath, result.js);
|
|
91
|
+
|
|
92
|
+
// store in graph node
|
|
93
|
+
if (node) {
|
|
94
|
+
node.compiled = {
|
|
95
|
+
js: result.js,
|
|
96
|
+
css: result.css,
|
|
97
|
+
cssHash: result.cssHash,
|
|
98
|
+
specifiers: [], // populated in Step 4
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// find affected route pattern
|
|
103
|
+
const affectedRoutes = graph.affectedRoutes(key);
|
|
104
|
+
const routeFile = node?.isRoute ?? affectedRoutes[0] ?? null;
|
|
105
|
+
|
|
106
|
+
if (!routeFile) {
|
|
107
|
+
// no route found — fall back to reload
|
|
108
|
+
console.log("[sigil hmr] no route affected, reloading");
|
|
109
|
+
hmr.reload();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// client-relative path for the module URL
|
|
114
|
+
const clientPath = relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
115
|
+
const modUrl = `${clientPath}.js`;
|
|
116
|
+
|
|
117
|
+
console.log("[sigil hmr] compiled:", filePath, `v=${v}`);
|
|
118
|
+
|
|
119
|
+
const payloads: object[] = [];
|
|
120
|
+
|
|
121
|
+
// CSS payload if style changed
|
|
122
|
+
if (result.css) {
|
|
123
|
+
payloads.push({
|
|
124
|
+
kind: "css",
|
|
125
|
+
hash: result.cssHash,
|
|
126
|
+
text: result.css,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// swap payload
|
|
131
|
+
payloads.push({
|
|
132
|
+
kind: "swap",
|
|
133
|
+
pattern: routeFile.path,
|
|
134
|
+
modUrl,
|
|
135
|
+
version: v,
|
|
136
|
+
cachePath: `${fileHash}_v${v}.js`, // server-side cache filename
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// stale payloads for other affected routes
|
|
140
|
+
const otherRoutes = affectedRoutes.filter((r) => r !== routeFile);
|
|
141
|
+
if (otherRoutes.length > 0) {
|
|
142
|
+
payloads.push({
|
|
143
|
+
kind: "stale",
|
|
144
|
+
patterns: otherRoutes.map((r) => r.path),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
console.log(
|
|
148
|
+
"[sigil hmr] sending batch:",
|
|
149
|
+
JSON.stringify(payloads, null, 2),
|
|
150
|
+
);
|
|
151
|
+
hmr.batch(payloads);
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
const loc = extractLoc(e, filePath);
|
|
154
|
+
console.error("[sigil hmr] compile error:", e.message);
|
|
155
|
+
hmr.error(e.message ?? String(e), loc);
|
|
156
|
+
// no bump — old version stays live
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function handleChange(
|
|
161
|
+
filePath: string,
|
|
162
|
+
graph: DevGraph,
|
|
163
|
+
hmr: HmrServer,
|
|
164
|
+
srcDir: string,
|
|
165
|
+
projectRoot: string,
|
|
166
|
+
routeTree: any,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
const key = normalizePath(filePath);
|
|
169
|
+
const source = await safeRead(filePath);
|
|
170
|
+
if (!source) {
|
|
171
|
+
hmr.reload();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (graph.hasCycle(key)) {
|
|
175
|
+
hmr.reload();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const node = graph.get(key);
|
|
179
|
+
if (!node) {
|
|
180
|
+
console.log("[sigil hmr] unknown file, reloading:", filePath);
|
|
181
|
+
hmr.reload();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log("[sigil hmr] changed:", filePath);
|
|
186
|
+
await compileAndSwap(filePath, graph, hmr, projectRoot, routeTree);
|
|
187
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { disposeModuleEffects } from "./effect-registry";
|
|
5
|
+
import type { DevGraph } from "./graph";
|
|
6
|
+
import { normalizePath } from "./paths";
|
|
7
|
+
|
|
8
|
+
export function makeDevLoader(graph: DevGraph, projectRoot: string) {
|
|
9
|
+
const cacheDir = join(projectRoot, ".grimoire/dev-cache");
|
|
10
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
return async function devLoadModule(filePath: string): Promise<any> {
|
|
13
|
+
const key = normalizePath(filePath);
|
|
14
|
+
disposeModuleEffects(key);
|
|
15
|
+
const v = graph.get(key)?.version ?? 0;
|
|
16
|
+
|
|
17
|
+
if (v === 0) {
|
|
18
|
+
// first load — let Bun handle it normally via SSR plugin
|
|
19
|
+
return import(filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// versioned load — read the source, let the SSR plugin transform it,
|
|
23
|
+
// write transformed output to a unique cache path, import that
|
|
24
|
+
// We can't re-trigger the plugin, so we invoke the compile pipeline directly
|
|
25
|
+
const source = await Bun.file(filePath).text();
|
|
26
|
+
const { transformSync } = await import("@babel/core");
|
|
27
|
+
const sigilPlugin = (await import("@sigil-dev/compiler/babel")).default;
|
|
28
|
+
const { createHash } = await import("node:crypto");
|
|
29
|
+
const hash = createHash("md5").update(filePath).digest("hex").slice(0, 8);
|
|
30
|
+
|
|
31
|
+
const result = transformSync(source, {
|
|
32
|
+
configFile: false,
|
|
33
|
+
babelrc: false,
|
|
34
|
+
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
35
|
+
plugins: [[sigilPlugin, { mode: "ssr", hash }]],
|
|
36
|
+
filename: filePath,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
40
|
+
const compiled = transpiler.transformSync(result?.code ?? "");
|
|
41
|
+
|
|
42
|
+
const cachePath = join(cacheDir, `${hash}_v${v}.js`);
|
|
43
|
+
writeFileSync(cachePath, compiled);
|
|
44
|
+
|
|
45
|
+
return import(pathToFileURL(cachePath).href);
|
|
46
|
+
};
|
|
47
|
+
}
|
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/env/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const publicPrefix = "PUBLIC_";
|
|
2
|
+
|
|
3
|
+
export function filterPublic(
|
|
4
|
+
env: Record<string, string | undefined>,
|
|
5
|
+
): Record<string, string> {
|
|
6
|
+
const result: Record<string, string> = {};
|
|
7
|
+
for (const [key, value] of Object.entries(env)) {
|
|
8
|
+
if (key.startsWith(publicPrefix) && value !== undefined) {
|
|
9
|
+
result[key] = value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function filterPrivate(
|
|
16
|
+
env: Record<string, string | undefined>,
|
|
17
|
+
): Record<string, string> {
|
|
18
|
+
const result: Record<string, string> = {};
|
|
19
|
+
for (const [key, value] of Object.entries(env)) {
|
|
20
|
+
if (!key.startsWith(publicPrefix) && value !== undefined) {
|
|
21
|
+
result[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const envPlugin = () => ({
|
|
4
|
+
name: "grimoire-env",
|
|
5
|
+
setup(build: any) {
|
|
6
|
+
build.onResolve({ filter: /^\$env\/static\/public$/ }, () => ({
|
|
7
|
+
path: join(import.meta.dir, "public.ts"),
|
|
8
|
+
}));
|
|
9
|
+
build.onResolve({ filter: /^\$env\/static\/private$/ }, () => ({
|
|
10
|
+
path: join(import.meta.dir, "private.ts"),
|
|
11
|
+
}));
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { filterPublic } from "./index";
|
|
2
|
+
|
|
3
|
+
// Inlined at build time — values captured from process.env during the build.
|
|
4
|
+
// For runtime reads (when not compiled), fall back to live process.env.
|
|
5
|
+
export const env = filterPublic(
|
|
6
|
+
typeof process !== "undefined" ? process.env : {},
|
|
7
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function generateEnvTypes(
|
|
5
|
+
projectRoot: string,
|
|
6
|
+
outDir: string,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const envPath = join(projectRoot, ".env");
|
|
9
|
+
let envContent = "";
|
|
10
|
+
try {
|
|
11
|
+
envContent = await readFile(envPath, "utf-8");
|
|
12
|
+
} catch {
|
|
13
|
+
// no .env file — generate empty types
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const publicVars: string[] = [];
|
|
17
|
+
const privateVars: string[] = [];
|
|
18
|
+
|
|
19
|
+
for (const line of envContent.split("\n")) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
22
|
+
const eqIndex = trimmed.indexOf("=");
|
|
23
|
+
if (eqIndex === -1) continue;
|
|
24
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
25
|
+
if (!key) continue;
|
|
26
|
+
if (key.startsWith("PUBLIC_")) {
|
|
27
|
+
publicVars.push(key);
|
|
28
|
+
} else {
|
|
29
|
+
privateVars.push(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const publicTypeContent = `// Auto-generated by Grimoire. Do not edit.
|
|
34
|
+
declare module "$env/static/public" {
|
|
35
|
+
${publicVars.map((v) => `\texport const ${v}: string;`).join("\n")}
|
|
36
|
+
${publicVars.length === 0 ? "\texport {};" : ""}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const privateTypeContent = `// Auto-generated by Grimoire. Do not edit.
|
|
41
|
+
declare module "$env/static/private" {
|
|
42
|
+
${privateVars.map((v) => `\texport const ${v}: string;`).join("\n")}
|
|
43
|
+
${privateVars.length === 0 ? "\texport {};" : ""}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
await Promise.all([
|
|
48
|
+
writeFile(join(outDir, "env-public.d.ts"), publicTypeContent, "utf-8"),
|
|
49
|
+
writeFile(join(outDir, "env-private.d.ts"), privateTypeContent, "utf-8"),
|
|
50
|
+
]);
|
|
51
|
+
}
|
package/src/integrations/vite.ts
CHANGED
package/src/rendering/head.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { createContext, getContext, setContext } from "@sigil-dev/runtime";
|
|
2
2
|
|
|
3
3
|
const HeadKey = createContext<string[]>();
|
|
4
|
+
const NonceKey = createContext<string>();
|
|
4
5
|
|
|
5
|
-
export function initHead(): void {
|
|
6
|
+
export function initHead(nonce?: string): void {
|
|
6
7
|
setContext(HeadKey, []);
|
|
8
|
+
if (nonce) setContext(NonceKey, nonce);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setHeadNonce(nonce: string): void {
|
|
12
|
+
setContext(NonceKey, nonce);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getHeadNonce(): string {
|
|
16
|
+
return getContext(NonceKey) ?? "";
|
|
7
17
|
}
|
|
8
18
|
|
|
9
19
|
export function collectHead(): string {
|
|
@@ -17,7 +27,17 @@ export function Head({
|
|
|
17
27
|
}): string | Comment {
|
|
18
28
|
if (typeof document === "undefined") {
|
|
19
29
|
const buf = getContext(HeadKey);
|
|
20
|
-
if (buf)
|
|
30
|
+
if (buf) {
|
|
31
|
+
const nonce = getContext(NonceKey);
|
|
32
|
+
if (nonce) {
|
|
33
|
+
const withNonce = String(children)
|
|
34
|
+
.replace(/<script /g, `<script nonce="${nonce}" `)
|
|
35
|
+
.replace(/<style /g, `<style nonce="${nonce}" `);
|
|
36
|
+
buf.push(withNonce);
|
|
37
|
+
} else {
|
|
38
|
+
buf.push(children as string);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
21
41
|
return "";
|
|
22
42
|
}
|
|
23
43
|
if (children instanceof Node) {
|