@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/index.ts
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
|
-
export { Head } from "./src/rendering/head";
|
|
2
|
-
export
|
|
3
|
-
export type {
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
export { Head } from "./src/rendering/head";
|
|
2
|
+
export { registerSSRPlugin } from "./src/rendering/ssrPlugin.ts";
|
|
3
|
+
export type { RouteFile, RouteTree } from "./src/routing/scanner";
|
|
4
|
+
export type { ErrorResult } from "./src/sentinels/error.ts";
|
|
5
|
+
export { error, isErrorResult } from "./src/sentinels/error.ts";
|
|
6
|
+
export type { FailResult } from "./src/sentinels/fail.ts";
|
|
7
|
+
export { fail, isFailResult } from "./src/sentinels/fail.ts";
|
|
8
|
+
export type { RedirectResult } from "./src/sentinels/redirect.ts";
|
|
9
|
+
export { isRedirectResult, redirect } from "./src/sentinels/redirect.ts";
|
|
10
|
+
export type {
|
|
11
|
+
CookieOptions,
|
|
12
|
+
Cookies,
|
|
13
|
+
Handle,
|
|
14
|
+
RequestEvent,
|
|
15
|
+
ResolveFunction,
|
|
16
|
+
} from "./src/server/hooks";
|
|
17
|
+
export { createHooks, sequence } from "./src/server/hooks";
|
|
18
|
+
export type {
|
|
19
|
+
BuiltinWorkerMode,
|
|
20
|
+
CoordinatorContext,
|
|
21
|
+
GrimoireConfig,
|
|
22
|
+
GrimoirePlugin,
|
|
23
|
+
LayoutServerLoad,
|
|
24
|
+
LoadContext,
|
|
25
|
+
PageServerLoad,
|
|
26
|
+
RenderContext,
|
|
27
|
+
RequestHandler,
|
|
28
|
+
RouteInfo,
|
|
29
|
+
TypedLoadContext,
|
|
30
|
+
WorkerDescriptor,
|
|
31
|
+
WorkerEnv,
|
|
32
|
+
WorkerMode,
|
|
33
|
+
WsRouteHandler,
|
|
34
|
+
} from "./src/types";
|
|
35
|
+
export { defineConfig } from "./src/types";
|
package/package.json
CHANGED
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "0.
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest"
|
|
9
|
-
},
|
|
6
|
+
"version": "0.8.0",
|
|
10
7
|
"exports": {
|
|
11
8
|
".": "./index.ts",
|
|
12
9
|
"./server": "./server.ts",
|
|
@@ -32,11 +29,16 @@
|
|
|
32
29
|
"@babel/plugin-syntax-typescript": "^8.0.0-rc.6",
|
|
33
30
|
"@babel/preset-typescript": "^8.0.0-rc.6",
|
|
34
31
|
"@babel/types": "^8.0.0-rc.6",
|
|
35
|
-
"@sigil-dev/compiler": "0.7.6",
|
|
36
|
-
"@sigil-dev/runtime": "0.7.6",
|
|
37
32
|
"vite": "^8.0.16"
|
|
38
33
|
},
|
|
39
34
|
"peerDependencies": {
|
|
35
|
+
"@sigil-dev/compiler": "0.8.0",
|
|
36
|
+
"@sigil-dev/runtime": "0.8.0",
|
|
40
37
|
"typescript": "^5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "latest",
|
|
41
|
+
"@sigil-dev/compiler": "0.8.0",
|
|
42
|
+
"@sigil-dev/runtime": "0.8.0"
|
|
41
43
|
}
|
|
42
44
|
}
|
package/preload.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { registerSSRPlugin } from "./src/
|
|
2
|
-
|
|
1
|
+
import { registerSSRPlugin } from "./src/rendering/ssrPlugin.ts";
|
|
2
|
+
|
|
3
|
+
registerSSRPlugin();
|
package/server.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type { TypegenConfig } from "./src/typegen";
|
|
13
|
-
export { generateTypes } from "./src/typegen";
|
|
1
|
+
export { scanRoutes } from "./src/routing/scanner";
|
|
2
|
+
export { createServer } from "./src/server";
|
|
3
|
+
export { buildProject } from "./src/server/build";
|
|
4
|
+
export { parseCookies } from "./src/server/cookie-utils";
|
|
5
|
+
export { startCoordinator } from "./src/server/coordinator.ts";
|
|
6
|
+
export {
|
|
7
|
+
runDeserializeLocals,
|
|
8
|
+
runRouteRequest,
|
|
9
|
+
runSerializeLocals,
|
|
10
|
+
runWorkerSpawn,
|
|
11
|
+
} from "./src/server/plugins";
|
|
12
|
+
export type { TypegenConfig } from "./src/typegen";
|
|
13
|
+
export { generateTypes } from "./src/typegen";
|
package/src/client/head.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
let headElements: (string | Node)[] = [];
|
|
2
|
-
|
|
3
|
-
export function resetHead(): void {
|
|
4
|
-
headElements = [];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function collectHead(): (string | Node)[] {
|
|
8
|
-
return headElements;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function applyHead(): void {
|
|
12
|
-
document.querySelectorAll("[data-sigil-head]").forEach((el) => el.remove());
|
|
13
|
-
for (const el of headElements) {
|
|
14
|
-
if (el instanceof Node) {
|
|
15
|
-
const clone = el.cloneNode(true) as HTMLElement;
|
|
16
|
-
clone.dataset.sigilHead = "1";
|
|
17
|
-
document.head.appendChild(clone);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Head({ children }: { children: string | Node }): Comment {
|
|
23
|
-
if (typeof document === "undefined") {
|
|
24
|
-
headElements.push(children);
|
|
25
|
-
} else if (children instanceof Node) {
|
|
26
|
-
(headElements as Node[]).push(children);
|
|
27
|
-
}
|
|
28
|
-
return document.createComment("head");
|
|
29
|
-
}
|
|
1
|
+
let headElements: (string | Node)[] = [];
|
|
2
|
+
|
|
3
|
+
export function resetHead(): void {
|
|
4
|
+
headElements = [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function collectHead(): (string | Node)[] {
|
|
8
|
+
return headElements;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function applyHead(): void {
|
|
12
|
+
document.querySelectorAll("[data-sigil-head]").forEach((el) => el.remove());
|
|
13
|
+
for (const el of headElements) {
|
|
14
|
+
if (el instanceof Node) {
|
|
15
|
+
const clone = el.cloneNode(true) as HTMLElement;
|
|
16
|
+
clone.dataset.sigilHead = "1";
|
|
17
|
+
document.head.appendChild(clone);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Head({ children }: { children: string | Node }): Comment {
|
|
23
|
+
if (typeof document === "undefined") {
|
|
24
|
+
headElements.push(children);
|
|
25
|
+
} else if (children instanceof Node) {
|
|
26
|
+
(headElements as Node[]).push(children);
|
|
27
|
+
}
|
|
28
|
+
return document.createComment("head");
|
|
29
|
+
}
|
package/src/client/router.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { popHydrationNodes, pushHydrationNodes } from "@sigil-dev/runtime";
|
|
2
2
|
import { withEffectScope } from "./scope";
|
|
3
3
|
|
|
4
|
-
let routeMap: Record<
|
|
4
|
+
let routeMap: Record<
|
|
5
|
+
string,
|
|
6
|
+
((props: any) => any) & { load?: (ctx: any) => Promise<any> }
|
|
7
|
+
> = {};
|
|
5
8
|
let layoutList: { path: string; component: (props: any) => any }[] = [];
|
|
6
9
|
let disposeCurrentPage: (() => void) | null = null;
|
|
7
10
|
let currentPath = location.pathname;
|
|
@@ -11,6 +14,28 @@ let beforeNavigateCb: ((url: URL) => boolean | void) | null = null;
|
|
|
11
14
|
let onNavigateCb: ((url: URL) => void) | null = null;
|
|
12
15
|
let afterNavigateCb: ((url: URL) => void) | null = null;
|
|
13
16
|
|
|
17
|
+
// // HMR
|
|
18
|
+
// (globalThis as any).__grimoire_routes__ = routeMap;
|
|
19
|
+
// (globalThis as any).__grimoire_current_pattern__ = currentPath;
|
|
20
|
+
// (globalThis as any).__grimoire_navigate__ = navigate;
|
|
21
|
+
|
|
22
|
+
export async function hmrRerender(): Promise<void> {
|
|
23
|
+
if (!lastState) {
|
|
24
|
+
location.reload();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
disposeCurrentPage?.();
|
|
28
|
+
pushHydrationNodes([]);
|
|
29
|
+
let rootNode: any;
|
|
30
|
+
disposeCurrentPage = withEffectScope(() => {
|
|
31
|
+
rootNode = buildRouteTree(lastState!);
|
|
32
|
+
});
|
|
33
|
+
popHydrationNodes();
|
|
34
|
+
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// (globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
38
|
+
|
|
14
39
|
export function beforeNavigate(cb: (url: URL) => boolean | void): void {
|
|
15
40
|
beforeNavigateCb = cb;
|
|
16
41
|
}
|
|
@@ -78,19 +103,60 @@ function setupPreload(): void {
|
|
|
78
103
|
});
|
|
79
104
|
}
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
let lastState: {
|
|
107
|
+
data: any;
|
|
108
|
+
layoutData: any[];
|
|
109
|
+
params: any;
|
|
110
|
+
pattern: string;
|
|
111
|
+
} | null = null;
|
|
112
|
+
|
|
113
|
+
function buildRouteTree(state: typeof lastState): any {
|
|
114
|
+
const { data, layoutData, params, pattern } = state!;
|
|
115
|
+
const Page = routeMap[pattern];
|
|
116
|
+
if (!Page) throw new Error(`no component for pattern ${pattern}`);
|
|
117
|
+
|
|
118
|
+
const matchedLayouts = layoutList
|
|
119
|
+
.filter(
|
|
120
|
+
(l) =>
|
|
121
|
+
l.path === "/" ||
|
|
122
|
+
pattern === l.path ||
|
|
123
|
+
pattern.startsWith(l.path + "/"),
|
|
124
|
+
)
|
|
125
|
+
.sort((a, b) => a.path.length - b.path.length);
|
|
126
|
+
|
|
127
|
+
let renderFn = () => {
|
|
128
|
+
const pageNode = Page({ data, params });
|
|
129
|
+
const pageDiv = document.createElement("div");
|
|
130
|
+
pageDiv.id = "grimoire-page";
|
|
131
|
+
pageDiv.appendChild(pageNode);
|
|
132
|
+
return pageDiv;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
136
|
+
const LayoutComponent = matchedLayouts[i].component;
|
|
137
|
+
const innerRender = renderFn;
|
|
138
|
+
const currentLayoutData = layoutData?.[i];
|
|
139
|
+
renderFn = () => {
|
|
140
|
+
const childNode = innerRender();
|
|
141
|
+
return LayoutComponent({
|
|
142
|
+
data: currentLayoutData,
|
|
143
|
+
params,
|
|
144
|
+
children: childNode,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return renderFn();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function navigate(path: string) {
|
|
82
153
|
const url = new URL(path, location.origin);
|
|
83
154
|
|
|
84
|
-
// C1: beforeNavigate — can cancel
|
|
85
155
|
if (beforeNavigateCb) {
|
|
86
156
|
const shouldProceed = beforeNavigateCb(url);
|
|
87
157
|
if (shouldProceed === false) return;
|
|
88
158
|
}
|
|
89
|
-
|
|
90
|
-
// C1: onNavigate
|
|
91
159
|
onNavigateCb?.(url);
|
|
92
|
-
|
|
93
|
-
// C2: Save scroll position before navigating away
|
|
94
160
|
saveScrollPosition();
|
|
95
161
|
|
|
96
162
|
const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
|
|
@@ -107,69 +173,55 @@ async function navigate(path: string) {
|
|
|
107
173
|
return;
|
|
108
174
|
}
|
|
109
175
|
|
|
176
|
+
let mergedData = data;
|
|
177
|
+
if (Page.load) {
|
|
178
|
+
try {
|
|
179
|
+
const clientData = await Page.load({
|
|
180
|
+
params,
|
|
181
|
+
url,
|
|
182
|
+
request: new Request(url),
|
|
183
|
+
});
|
|
184
|
+
mergedData = { ...clientData, ...data };
|
|
185
|
+
} catch (e: any) {
|
|
186
|
+
if (e?.location) {
|
|
187
|
+
window.location.href = e.location;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.error("[grimoire] universal load error:", e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// track state for HMR rerender
|
|
195
|
+
lastState = { data: mergedData, layoutData, params, pattern };
|
|
196
|
+
(globalThis as any).__grimoire_current_pattern__ = pattern;
|
|
197
|
+
|
|
110
198
|
disposeCurrentPage?.();
|
|
111
199
|
disposeCurrentPage = null;
|
|
112
|
-
|
|
113
|
-
// SPA navigation: empty pool so components create fresh DOM
|
|
114
200
|
pushHydrationNodes([]);
|
|
115
201
|
let rootNode: any;
|
|
116
202
|
disposeCurrentPage = withEffectScope(() => {
|
|
117
203
|
try {
|
|
118
|
-
|
|
119
|
-
.filter(
|
|
120
|
-
(l) =>
|
|
121
|
-
l.path === "/" ||
|
|
122
|
-
pattern === l.path ||
|
|
123
|
-
pattern.startsWith(l.path + "/"),
|
|
124
|
-
)
|
|
125
|
-
.sort((a, b) => a.path.length - b.path.length);
|
|
126
|
-
|
|
127
|
-
let renderFn = () => {
|
|
128
|
-
const pageNode = Page({ data, params });
|
|
129
|
-
const pageDiv = document.createElement("div");
|
|
130
|
-
pageDiv.id = "grimoire-page";
|
|
131
|
-
pageDiv.appendChild(pageNode);
|
|
132
|
-
return pageDiv;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
136
|
-
const LayoutComponent = matchedLayouts[i].component;
|
|
137
|
-
const innerRender = renderFn;
|
|
138
|
-
const currentLayoutData = layoutData?.[i];
|
|
139
|
-
renderFn = () => {
|
|
140
|
-
const childNode = innerRender();
|
|
141
|
-
return LayoutComponent({
|
|
142
|
-
data: currentLayoutData,
|
|
143
|
-
params,
|
|
144
|
-
children: childNode,
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
rootNode = renderFn();
|
|
204
|
+
rootNode = buildRouteTree(lastState!);
|
|
150
205
|
} catch (e) {
|
|
151
206
|
console.error(e);
|
|
152
207
|
}
|
|
153
208
|
});
|
|
154
209
|
popHydrationNodes();
|
|
155
210
|
|
|
156
|
-
// Update head from navigation response
|
|
157
211
|
updateHead(head);
|
|
158
|
-
|
|
159
212
|
document.title = head?.title ?? document.title;
|
|
160
213
|
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
161
|
-
|
|
162
214
|
currentPath = path;
|
|
163
|
-
|
|
164
|
-
// C2: Restore scroll position
|
|
215
|
+
history.pushState(null, "", path);
|
|
165
216
|
restoreScrollPosition(path);
|
|
166
|
-
|
|
167
|
-
// C1: afterNavigate
|
|
168
217
|
afterNavigateCb?.(url);
|
|
169
218
|
}
|
|
170
219
|
|
|
171
220
|
export function initRouter(
|
|
172
|
-
routes: Record<
|
|
221
|
+
routes: Record<
|
|
222
|
+
string,
|
|
223
|
+
((props: any) => any) & { load?: (ctx: any) => Promise<any> }
|
|
224
|
+
>,
|
|
173
225
|
layouts: any[],
|
|
174
226
|
initialDispose?: () => void,
|
|
175
227
|
) {
|
|
@@ -178,9 +230,24 @@ export function initRouter(
|
|
|
178
230
|
disposeCurrentPage = initialDispose ?? null;
|
|
179
231
|
currentPath = location.pathname;
|
|
180
232
|
|
|
181
|
-
//
|
|
182
|
-
|
|
233
|
+
// set initial HMR state from SSR
|
|
234
|
+
const stateEl = document.getElementById("__grimoire_state__");
|
|
235
|
+
if (stateEl) {
|
|
236
|
+
const state = JSON.parse(stateEl.textContent!);
|
|
237
|
+
lastState = {
|
|
238
|
+
data: state.data,
|
|
239
|
+
layoutData: state.layoutData,
|
|
240
|
+
params: state.params,
|
|
241
|
+
pattern: state.pattern,
|
|
242
|
+
};
|
|
243
|
+
(globalThis as any).__grimoire_current_pattern__ = state.pattern;
|
|
244
|
+
(globalThis as any).__grimoire_routes__ = routeMap;
|
|
245
|
+
(globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
246
|
+
(globalThis as any).__grimoire_navigate__ = (path: string) =>
|
|
247
|
+
navigate(path);
|
|
248
|
+
}
|
|
183
249
|
|
|
250
|
+
saveScrollPosition();
|
|
184
251
|
document.addEventListener("click", handleClick);
|
|
185
252
|
window.addEventListener("popstate", () => {
|
|
186
253
|
if (location.pathname !== currentPath) {
|
|
@@ -189,19 +256,19 @@ export function initRouter(
|
|
|
189
256
|
navigate(location.pathname);
|
|
190
257
|
}
|
|
191
258
|
});
|
|
192
|
-
|
|
193
|
-
// C3: Setup preloading
|
|
194
259
|
setupPreload();
|
|
195
260
|
}
|
|
196
261
|
|
|
197
262
|
function updateHead(headHtml: string) {
|
|
198
263
|
document
|
|
199
264
|
.querySelectorAll("[data-grimoire-head]")
|
|
265
|
+
//biome-ignore lint/suspicious/useIterableCallbackReturn: shut up
|
|
200
266
|
.forEach((el) => el.remove());
|
|
201
267
|
const parsed = headHtml
|
|
202
268
|
? new DOMParser().parseFromString(`<head>${headHtml}</head>`, "text/html")
|
|
203
269
|
: null;
|
|
204
270
|
const incomingTitle = parsed?.querySelector("title");
|
|
271
|
+
//biome-ignore lint/suspicious/useIterableCallbackReturn: shut up
|
|
205
272
|
document.querySelectorAll("title").forEach((el) => el.remove());
|
|
206
273
|
document.title = incomingTitle?.textContent ?? document.title;
|
|
207
274
|
if (parsed) {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, relative } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { DevGraph } from "./graph";
|
|
5
|
+
import { normalizePath } from "./paths";
|
|
6
|
+
|
|
7
|
+
const RUNTIME_URL = "/__grimoire__/runtime.js";
|
|
8
|
+
const MODULES_BASE = "/__grimoire__/m/";
|
|
9
|
+
const DEPS_BASE = "/__grimoire__/dep/";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite a single import specifier to a browser-servable URL.
|
|
13
|
+
* project-relative → /__grimoire__/m/...
|
|
14
|
+
* @sigil-dev/runtime → /__grimoire__/runtime.js
|
|
15
|
+
* bare npm → /__grimoire__/dep/...
|
|
16
|
+
* relative → /__grimoire__/m/... (resolved)
|
|
17
|
+
*/
|
|
18
|
+
function rewriteSpecifier(
|
|
19
|
+
specifier: string,
|
|
20
|
+
fromFile: string,
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
): string {
|
|
23
|
+
if (specifier === "@sigil-dev/runtime") return RUNTIME_URL;
|
|
24
|
+
// if (specifier.startsWith("@sigil-dev/")) return RUNTIME_URL; // all sigil packages
|
|
25
|
+
|
|
26
|
+
// relative import — resolve to absolute, then make a module URL
|
|
27
|
+
if (specifier.startsWith(".")) {
|
|
28
|
+
const resolved = join(dirname(fromFile), specifier);
|
|
29
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
30
|
+
const candidate = resolved.endsWith(ext) ? resolved : resolved + ext;
|
|
31
|
+
if (existsSync(candidate)) {
|
|
32
|
+
const rel = relative(projectRoot, candidate).replace(/\\/g, "/");
|
|
33
|
+
return `${MODULES_BASE}${rel}.js`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// already has extension
|
|
37
|
+
const rel = relative(projectRoot, resolved).replace(/\\/g, "/");
|
|
38
|
+
return `${MODULES_BASE}${rel}.js`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// $lib alias
|
|
42
|
+
if (specifier.startsWith("$lib/")) {
|
|
43
|
+
const rel = specifier.replace("$lib/", "src/lib/");
|
|
44
|
+
// try common extensions
|
|
45
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
46
|
+
const candidate = join(projectRoot, rel + ext);
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
return `${MODULES_BASE}${rel}${ext}.js`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return `${MODULES_BASE}${rel}.ts.js`; // fallback
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// $env — keep as-is, these are virtual modules handled elsewhere
|
|
55
|
+
if (specifier.startsWith("$env/")) return specifier;
|
|
56
|
+
|
|
57
|
+
// bare npm specifier → dep bundle
|
|
58
|
+
const pkgName = specifier.startsWith("@")
|
|
59
|
+
? specifier.split("/").slice(0, 2).join("/")
|
|
60
|
+
: specifier.split("/")[0];
|
|
61
|
+
return `${DEPS_BASE}${pkgName!.replace("/", "__")}.js`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rewrite all import/export from specifiers in compiled JS output.
|
|
66
|
+
* Works on the static pattern `from "..."` — sufficient for Babel output.
|
|
67
|
+
*/
|
|
68
|
+
function rewriteImports(
|
|
69
|
+
code: string,
|
|
70
|
+
filePath: string,
|
|
71
|
+
projectRoot: string,
|
|
72
|
+
): string {
|
|
73
|
+
return code.replace(
|
|
74
|
+
/(from\s+|import\s+)(["'])([^"']+)\2/g,
|
|
75
|
+
(match, keyword, quote, specifier) => {
|
|
76
|
+
const rewritten = rewriteSpecifier(specifier, filePath, projectRoot);
|
|
77
|
+
return `${keyword}${quote}${rewritten}${quote}`;
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CompileResult {
|
|
83
|
+
js: string;
|
|
84
|
+
css: string | null;
|
|
85
|
+
cssHash: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function compileForBrowser(
|
|
89
|
+
filePath: string,
|
|
90
|
+
projectRoot: string,
|
|
91
|
+
): Promise<CompileResult> {
|
|
92
|
+
const source = await Bun.file(filePath).text();
|
|
93
|
+
|
|
94
|
+
// extract scoped CSS
|
|
95
|
+
const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/i;
|
|
96
|
+
const { computeHash, scopeCSS } = await import(
|
|
97
|
+
pathToFileURL(
|
|
98
|
+
join(
|
|
99
|
+
projectRoot,
|
|
100
|
+
"node_modules/@sigil-dev/compiler/src/babel/util/css.ts",
|
|
101
|
+
),
|
|
102
|
+
).href
|
|
103
|
+
);
|
|
104
|
+
const cssHash = computeHash(filePath);
|
|
105
|
+
const styleMatch = source.match(STYLE_RE);
|
|
106
|
+
const css = styleMatch ? scopeCSS(styleMatch[1].trim(), cssHash) : null;
|
|
107
|
+
const sourceNoStyle = source.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
108
|
+
|
|
109
|
+
// Babel transform — dom mode
|
|
110
|
+
const { transformSync } = await import("@babel/core");
|
|
111
|
+
const sigilPlugin = (await import("@sigil-dev/compiler/babel")).default;
|
|
112
|
+
const { createHash } = await import("node:crypto");
|
|
113
|
+
const hash = createHash("md5").update(filePath).digest("hex").slice(0, 8);
|
|
114
|
+
|
|
115
|
+
const babelResult = transformSync(sourceNoStyle, {
|
|
116
|
+
configFile: false,
|
|
117
|
+
babelrc: false,
|
|
118
|
+
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
119
|
+
plugins: [[sigilPlugin, { mode: "dom", hash }]],
|
|
120
|
+
filename: filePath,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(
|
|
124
|
+
"[compile] babel output first 500 chars:",
|
|
125
|
+
babelResult?.code?.slice(0, 500),
|
|
126
|
+
);
|
|
127
|
+
// strip TypeScript types
|
|
128
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
|
|
129
|
+
let js = transpiler.transformSync(babelResult?.code ?? "");
|
|
130
|
+
|
|
131
|
+
const runtimeSpecifiers = [
|
|
132
|
+
"createSignal",
|
|
133
|
+
"createRawSignal",
|
|
134
|
+
"createEffect",
|
|
135
|
+
"createMemo",
|
|
136
|
+
"reconcile",
|
|
137
|
+
"claim",
|
|
138
|
+
"claimText",
|
|
139
|
+
"claimComment",
|
|
140
|
+
"hydrateKeyedList",
|
|
141
|
+
"insert",
|
|
142
|
+
"getHydrationNodes",
|
|
143
|
+
"pushHydrationNodes",
|
|
144
|
+
"popHydrationNodes",
|
|
145
|
+
"snapshot",
|
|
146
|
+
"tracking",
|
|
147
|
+
"createEffectPre",
|
|
148
|
+
"createInspectEffect",
|
|
149
|
+
"withEffectScope",
|
|
150
|
+
].join(", ");
|
|
151
|
+
|
|
152
|
+
js = js.replace(
|
|
153
|
+
/import\s*\{[^}]+\}\s*from\s*["']@sigil-dev\/runtime["'];?/,
|
|
154
|
+
`import { ${runtimeSpecifiers} } from "@sigil-dev/runtime";`,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// inject scoped CSS at runtime
|
|
158
|
+
if (css) {
|
|
159
|
+
//biome-ignore lint: bro shut up already
|
|
160
|
+
js =
|
|
161
|
+
`if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
|
|
162
|
+
const __s = document.createElement('style');
|
|
163
|
+
__s.id = 'sigil-${hash}';
|
|
164
|
+
__s.textContent = ${JSON.stringify(css)};
|
|
165
|
+
document.head.appendChild(__s);
|
|
166
|
+
}\n` + js;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// rewrite imports to browser-servable URLs
|
|
170
|
+
js = rewriteImports(js, filePath, projectRoot);
|
|
171
|
+
|
|
172
|
+
return { js, css, cssHash };
|
|
173
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizePath } from "./paths";
|
|
2
|
+
|
|
3
|
+
const registry = new Map<string, (() => void)[]>();
|
|
4
|
+
|
|
5
|
+
export function registerModuleEffect(
|
|
6
|
+
filePath: string,
|
|
7
|
+
dispose: () => void,
|
|
8
|
+
): void {
|
|
9
|
+
const key = normalizePath(filePath);
|
|
10
|
+
let list = registry.get(key);
|
|
11
|
+
if (!list) registry.set(key, (list = []));
|
|
12
|
+
list.push(dispose);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function disposeModuleEffects(filePath: string): void {
|
|
16
|
+
const key = normalizePath(filePath);
|
|
17
|
+
for (const d of registry.get(key) ?? []) {
|
|
18
|
+
try {
|
|
19
|
+
d();
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
registry.delete(key);
|
|
23
|
+
}
|