@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.
Files changed (41) hide show
  1. package/index.ts +35 -34
  2. package/package.json +8 -6
  3. package/preload.js +3 -2
  4. package/server.ts +13 -13
  5. package/src/client/head.ts +29 -29
  6. package/src/client/router.ts +120 -53
  7. package/src/dev/compile-module.ts +173 -0
  8. package/src/dev/effect-registry.ts +23 -0
  9. package/src/dev/graph.ts +114 -0
  10. package/src/dev/hmr-client.ts +158 -0
  11. package/src/dev/hmr-server.ts +187 -0
  12. package/src/dev/loader.ts +47 -0
  13. package/src/dev/paths.ts +14 -0
  14. package/src/dev/runtime-bundle.ts +49 -0
  15. package/src/dev/watcher.ts +44 -0
  16. package/src/integrations/vite.ts +73 -72
  17. package/src/rendering/hydrate.ts +102 -64
  18. package/src/rendering/index.ts +296 -199
  19. package/src/rendering/ssrPlugin.ts +67 -53
  20. package/src/routing/manifest-gen.ts +42 -39
  21. package/src/routing/router.ts +109 -106
  22. package/src/routing/scanner.ts +141 -135
  23. package/src/routing/transform-routes.ts +101 -101
  24. package/src/server/build.ts +239 -147
  25. package/src/server/coordinator.ts +306 -306
  26. package/src/server/index.ts +260 -50
  27. package/src/server/worker.ts +59 -59
  28. package/src/typegen/index.ts +356 -353
  29. package/src/types.ts +270 -269
  30. package/test/context.test.ts +52 -52
  31. package/test/hydration.test.ts +119 -119
  32. package/test/middleware.test.ts +223 -223
  33. package/test/rendering.test.ts +579 -425
  34. package/test/routing.test.ts +81 -83
  35. package/test/scanning.test.ts +200 -181
  36. package/test/scope.test.ts +24 -8
  37. package/test/server.test.ts +249 -229
  38. package/test/streaming.test.ts +125 -106
  39. package/test/transform-routes.test.ts +84 -84
  40. package/test/typegen.test.ts +35 -25
  41. package/tsconfig.json +1 -0
package/index.ts CHANGED
@@ -1,34 +1,35 @@
1
- export { Head } from "./src/rendering/head";
2
- export type { RouteFile, RouteTree } from "./src/routing/scanner";
3
- export type { ErrorResult } from "./src/sentinels/error.ts";
4
- export { error, isErrorResult } from "./src/sentinels/error.ts";
5
- export type { FailResult } from "./src/sentinels/fail.ts";
6
- export { fail, isFailResult } from "./src/sentinels/fail.ts";
7
- export type { RedirectResult } from "./src/sentinels/redirect.ts";
8
- export { isRedirectResult, redirect } from "./src/sentinels/redirect.ts";
9
- export type {
10
- CookieOptions,
11
- Cookies,
12
- Handle,
13
- RequestEvent,
14
- ResolveFunction,
15
- } from "./src/server/hooks";
16
- export { createHooks, sequence } from "./src/server/hooks";
17
- export type {
18
- BuiltinWorkerMode,
19
- CoordinatorContext,
20
- GrimoireConfig,
21
- GrimoirePlugin,
22
- LayoutServerLoad,
23
- LoadContext,
24
- PageServerLoad,
25
- RenderContext,
26
- RequestHandler,
27
- RouteInfo,
28
- TypedLoadContext,
29
- WorkerDescriptor,
30
- WorkerEnv,
31
- WorkerMode,
32
- WsRouteHandler,
33
- } from "./src/types";
34
- export { defineConfig } from "./src/types";
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.6",
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/ssr/plugin.ts"
2
- registerSSRPlugin()
1
+ import { registerSSRPlugin } from "./src/rendering/ssrPlugin.ts";
2
+
3
+ registerSSRPlugin();
package/server.ts CHANGED
@@ -1,13 +1,13 @@
1
- export { createServer } from "./src/server";
2
- export { buildProject } from "./src/server/build";
3
- export { parseCookies } from "./src/server/cookie-utils";
4
- export { startCoordinator } from "./src/server/coordinator.ts";
5
- export {
6
- runDeserializeLocals,
7
- runRouteRequest,
8
- runSerializeLocals,
9
- runWorkerSpawn,
10
- } from "./src/server/plugins";
11
- export { scanRoutes } from "./src/routing/scanner";
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";
@@ -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
+ }
@@ -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<string, (props: any) => any> = {};
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
- async function navigate(path: string) {
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
- 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
- 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<string, any>,
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
- // C2: Save initial scroll position
182
- saveScrollPosition();
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
+ }