@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.
Files changed (58) hide show
  1. package/.grimoire/_routes.dom.js +8 -0
  2. package/.grimoire/_routes.hydrate.js +8 -0
  3. package/.grimoire/tsconfig.generated.json +11 -0
  4. package/.grimoire/types/ambient.d.ts +59 -0
  5. package/.grimoire/types/api/hello/$types.d.ts +50 -0
  6. package/.grimoire/types/api/items/$types.d.ts +50 -0
  7. package/.grimoire/types/echo/$types.d.ts +50 -0
  8. package/.grimoire/types/env-private.d.ts +5 -0
  9. package/.grimoire/types/env-public.d.ts +5 -0
  10. package/.grimoire/types/mixed/$types.d.ts +50 -0
  11. package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
  12. package/.grimoire/types/reject/$types.d.ts +50 -0
  13. package/index.ts +21 -20
  14. package/package.json +13 -7
  15. package/preload.js +3 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/server.ts +13 -13
  19. package/src/client/head.ts +29 -0
  20. package/src/client/router.ts +254 -40
  21. package/src/dev/compile-module.ts +173 -0
  22. package/src/dev/effect-registry.ts +23 -0
  23. package/src/dev/graph.ts +114 -0
  24. package/src/dev/hmr-client.ts +158 -0
  25. package/src/dev/hmr-server.ts +187 -0
  26. package/src/dev/loader.ts +47 -0
  27. package/src/dev/paths.ts +14 -0
  28. package/src/dev/runtime-bundle.ts +49 -0
  29. package/src/dev/watcher.ts +44 -0
  30. package/src/env/index.ts +25 -0
  31. package/src/env/plugin.ts +13 -0
  32. package/src/env/private.ts +5 -0
  33. package/src/env/public.ts +7 -0
  34. package/src/env/typegen.ts +51 -0
  35. package/src/integrations/vite.ts +1 -0
  36. package/src/rendering/head.ts +22 -2
  37. package/src/rendering/hydrate.ts +111 -18
  38. package/src/rendering/index.ts +263 -153
  39. package/src/rendering/ssrPlugin.ts +59 -39
  40. package/src/routing/manifest-gen.ts +18 -2
  41. package/src/routing/router.ts +94 -83
  42. package/src/routing/scanner.ts +26 -14
  43. package/src/routing/transform-routes.ts +68 -68
  44. package/src/server/build.ts +225 -76
  45. package/src/server/coordinator.ts +9 -0
  46. package/src/server/hooks.ts +24 -3
  47. package/src/server/index.ts +388 -104
  48. package/src/typegen/index.ts +30 -14
  49. package/src/types.ts +12 -2
  50. package/test/middleware.test.ts +6 -4
  51. package/test/rendering.test.ts +510 -356
  52. package/test/routing.test.ts +36 -0
  53. package/test/scanning.test.ts +39 -8
  54. package/test/scope.test.ts +24 -8
  55. package/test/server.test.ts +27 -7
  56. package/test/streaming.test.ts +117 -98
  57. package/test/typegen.test.ts +52 -24
  58. package/tsconfig.json +1 -0
@@ -1,47 +1,67 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { transformSync } from "@babel/core";
2
3
  import sigilPlugin from "@sigil-dev/compiler/babel";
3
- import { createHash } from "node:crypto";
4
4
  import type { GrimoirePlugin } from "../types";
5
5
 
6
6
  let registered = false;
7
7
 
8
8
  export function registerSSRPlugin(plugins: GrimoirePlugin[] = []) {
9
- if (registered) return;
10
- registered = true;
11
-
12
- Bun.plugin({
13
- name: "sigil-ssr",
14
- setup(build) {
15
- // loader: "ts" not "tsx" — Babel already consumed the JSX,
16
- // Bun only needs to strip remaining TypeScript types
17
- const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" });
18
-
19
- build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
20
- if (path.includes(".grimoire") || path.includes("node_modules")) return;
21
- const source = await Bun.file(path).text();
22
- const hash = createHash("md5").update(path).digest("hex").slice(0, 8);
23
- // console.log("[sigil-ssr] transforming", path);
24
- const result = transformSync(source, {
25
- configFile: false,
26
- babelrc: false,
27
- parserOpts: {
28
- plugins: ["typescript", "jsx"],
29
- },
30
- plugins: [[sigilPlugin, { mode: "ssr", hash }]],
31
- filename: path,
32
- });
33
- const output = result?.code ?? "";
34
- const lines = output.split("\n");
35
-
36
- let contents = transpiler.transformSync(result?.code ?? "");
37
-
38
- for (const plugin of plugins) {
39
- if (plugin.transform)
40
- contents = (await plugin.transform(contents, path)) ?? contents;
41
- }
42
-
43
- return { contents, loader: "js" as const };
44
- });
45
- },
46
- });
9
+ if (registered) return;
10
+ registered = true;
11
+
12
+ Bun.plugin({
13
+ name: "sigil-ssr",
14
+
15
+ setup(build) {
16
+ // loader: "ts" not "tsx" Babel already consumed the JSX,
17
+ // Bun only needs to strip remaining TypeScript types
18
+ const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" });
19
+ build.onResolve({ filter: /^@sigil-dev\/runtime($|\/)/ }, (args) => {
20
+ try {
21
+ const resolved = Bun.resolveSync(args.path, process.cwd());
22
+ return { path: resolved.replaceAll("\\", "/") };
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ });
27
+
28
+ build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
29
+ if (path.includes("index.tsx"))
30
+ console.log("[ssr-plugin] onLoad:", path);
31
+ const source = await Bun.file(path).text();
32
+
33
+ // node_modules and .grimoire files are plain TypeScript (no sigil JSX).
34
+ // Still must return { contents, loader } — never return undefined from onLoad.
35
+ if (path.includes("node_modules") || path.includes(".grimoire")) {
36
+ return {
37
+ contents: transpiler.transformSync(source),
38
+ loader: "js" as const,
39
+ };
40
+ }
41
+
42
+ if (process.env.SIGIL_VERBOSE) {
43
+ console.log("[sigil-ssr] XFORM:", path);
44
+ }
45
+ const hash = createHash("md5").update(path).digest("hex").slice(0, 8);
46
+ const result = transformSync(source, {
47
+ configFile: false,
48
+ babelrc: false,
49
+ parserOpts: {
50
+ plugins: ["typescript", "jsx"],
51
+ },
52
+ plugins: [[sigilPlugin, { mode: "ssr", hash }]],
53
+ filename: path,
54
+ });
55
+
56
+ let contents = transpiler.transformSync(result?.code ?? "");
57
+
58
+ for (const plugin of plugins) {
59
+ if (plugin.transform)
60
+ contents = (await plugin.transform(contents, path)) ?? contents;
61
+ }
62
+
63
+ return { contents, loader: "js" as const };
64
+ });
65
+ },
66
+ });
47
67
  }
@@ -13,14 +13,30 @@ export function generateManifest(
13
13
  fileMap?: Map<string, string>,
14
14
  ): string {
15
15
  const pages = routes.filter((r) => r.type === "page" || r.type === "simple");
16
- const imports = pages
16
+ const layouts = routes.filter((r) => r.type === "layout");
17
+
18
+ const pageImports = pages
17
19
  .map((r, i) => {
18
20
  const path = fileMap?.get(r.filePath) ?? r.filePath;
19
21
  return `import __Page${i} from ${JSON.stringify(path)};`;
20
22
  })
21
23
  .join("\n");
24
+ const layoutImports = layouts
25
+ .map((r, i) => {
26
+ const path = fileMap?.get(r.filePath) ?? r.filePath;
27
+ return `import __Layout${i} from ${JSON.stringify(path)};`;
28
+ })
29
+ .join("\n");
30
+
22
31
  const map = pages
23
32
  .map((r, i) => ` ${JSON.stringify(r.path)}: __Page${i},`)
24
33
  .join("\n");
25
- return `${imports}\nexport const routes = {\n${map}\n};\n`;
34
+ const layoutMap = layouts
35
+ .map(
36
+ (r, i) =>
37
+ ` { path: ${JSON.stringify(r.path)}, component: __Layout${i}, load: __Layout${i}.load },`,
38
+ )
39
+ .join("\n");
40
+
41
+ return `${pageImports}\n${layoutImports}\nexport const routes = {\n${map}\n};\nexport const layouts = [\n${layoutMap}\n];\n`;
26
42
  }
@@ -1,98 +1,109 @@
1
1
  import type { RouteFile, RouteTree } from "./scanner";
2
2
 
3
3
  export interface MatchedRoute {
4
- route: RouteFile;
5
- params: Record<string, string>;
6
- layouts: RouteFile[]; // was: layout?: RouteFile
7
- layoutServers: RouteFile[]; // was: layoutServer?: RouteFile
8
- pageServer?: RouteFile;
4
+ route: RouteFile;
5
+ params: Record<string, string>;
6
+ layouts: RouteFile[]; // was: layout?: RouteFile
7
+ layoutServers: RouteFile[]; // was: layoutServer?: RouteFile
8
+ pageServer?: RouteFile;
9
9
  }
10
10
 
11
11
  export function matchRoute(tree: RouteTree, url: URL): MatchedRoute | null {
12
- const pathname = url.pathname;
13
-
14
- // check server routes first
15
- for (const server of tree.servers) {
16
- const params = matchPattern(server.path, pathname);
17
- if (params !== null) return {
18
- route: server,
19
- params,
20
- layouts: [],
21
- layoutServers: []
22
- };
23
- }
24
-
25
- // then pages
26
- for (const route of tree.routes) {
27
- if (route.type === "pageServer") continue;
28
- const params = matchPattern(route.path, pathname);
29
- if (params === null) continue;
30
-
31
- // find applicable layout (longest matching prefix wins)
32
- const layouts = tree.layouts
33
- .filter(
34
- (l) =>
35
- l.type === "layout" &&
36
- pathname.startsWith(l.path === "/" ? "/" : l.path),
37
- )
38
- .sort((a, b) => a.path.length - b.path.length); // root first, innermost last
39
-
40
- const layoutServers = tree.layouts
41
- .filter(
42
- (l) =>
43
- l.type === "layoutServer" &&
44
- pathname.startsWith(l.path === "/" ? "/" : l.path),
45
- )
46
- .sort((a, b) => a.path.length - b.path.length); // root first, innermost last
47
-
48
- const pageServer = tree.routes.find(
49
- (r) => r.type === "pageServer" && r.path === route.path,
50
- );
51
-
52
- return { route, params, layouts, layoutServers, pageServer };
53
- }
54
-
55
- return null;
12
+ const pathname = url.pathname;
13
+
14
+ // check server routes first
15
+ for (const server of tree.servers) {
16
+ const params = matchPattern(server.path, pathname);
17
+ if (params !== null)
18
+ return {
19
+ route: server,
20
+ params,
21
+ layouts: [],
22
+ layoutServers: [],
23
+ };
24
+ }
25
+
26
+ // then pages
27
+ for (const route of tree.routes) {
28
+ if (route.type === "pageServer") continue;
29
+ const params = matchPattern(route.path, pathname);
30
+ if (params === null) continue;
31
+
32
+ // find applicable layouts by matching route pattern prefix, not URL pathname.
33
+ // pathname.startsWith(l.path) would fail for parameterized layouts like /w/:workspace.
34
+ const layoutMatches = (l: RouteFile) =>
35
+ l.path === "/" ||
36
+ route.path === l.path ||
37
+ route.path.startsWith(l.path + "/");
38
+
39
+ const layouts = tree.layouts
40
+ .filter((l) => l.type === "layout" && layoutMatches(l))
41
+ .sort((a, b) => a.path.length - b.path.length); // root first, innermost last
42
+
43
+ const layoutServers = tree.layouts
44
+ .filter((l) => l.type === "layoutServer" && layoutMatches(l))
45
+ .sort((a, b) => a.path.length - b.path.length); // root first, innermost last
46
+
47
+ const pageServer = tree.routes.find(
48
+ (r) => r.type === "pageServer" && r.path === route.path,
49
+ );
50
+
51
+ return { route, params, layouts, layoutServers, pageServer };
52
+ }
53
+
54
+ return null;
56
55
  }
57
56
 
58
57
  export function findClosestError(
59
- errors: RouteFile[],
60
- pathname: string,
58
+ errors: RouteFile[],
59
+ pathname: string,
61
60
  ): RouteFile | null {
62
- const segments = pathname.split("/").filter(Boolean);
63
-
64
- // walk from most specific to root
65
- while (segments.length >= 0) {
66
- const prefix = `/${segments.join("/")}`;
67
- const error = errors.find(
68
- (e) => e.path === prefix || e.path === prefix + "/",
69
- );
70
- if (error) return error;
71
- if (segments.length === 0) break;
72
- segments.pop();
73
- }
74
- return null;
61
+ const segments = pathname.split("/").filter(Boolean);
62
+
63
+ // walk from most specific to root
64
+ while (segments.length >= 0) {
65
+ const prefix = `/${segments.join("/")}`;
66
+ const error = errors.find(
67
+ (e) => e.path === prefix || e.path === prefix + "/",
68
+ );
69
+ if (error) return error;
70
+ if (segments.length === 0) break;
71
+ segments.pop();
72
+ }
73
+ return null;
75
74
  }
76
75
 
77
76
  function matchPattern(
78
- pattern: string,
79
- pathname: string,
77
+ pattern: string,
78
+ pathname: string,
80
79
  ): Record<string, string> | null {
81
- const patternParts = pattern.split("/").filter(Boolean);
82
- const pathParts = pathname.split("/").filter(Boolean);
83
-
84
- if (patternParts.length !== pathParts.length) return null;
85
-
86
- const params: Record<string, string> = {};
87
- for (let i = 0; i < patternParts.length; i++) {
88
- const patternPart = patternParts[i]!;
89
- const pathPart = pathParts[i]!;
90
-
91
- if (patternPart.startsWith(":")) {
92
- params[patternPart.slice(1)] = pathPart;
93
- } else if (patternPart !== pathPart) {
94
- return null;
95
- }
96
- }
97
- return params;
80
+ const patternParts = pattern.split("/").filter(Boolean);
81
+ const pathParts = pathname.split("/").filter(Boolean);
82
+
83
+ const params: Record<string, string> = {};
84
+ let pi = 0;
85
+ let pj = 0;
86
+ while (pi < patternParts.length) {
87
+ const patternPart = patternParts[pi]!;
88
+
89
+ if (patternPart.startsWith("*")) {
90
+ // Rest param — captures zero or more remaining path segments joined by "/".
91
+ // Rest must be the last segment in the pattern.
92
+ params[patternPart.slice(1)] = pathParts.slice(pj).join("/");
93
+ return params;
94
+ }
95
+
96
+ if (pj >= pathParts.length) return null;
97
+ const pathPart = pathParts[pj]!;
98
+
99
+ if (patternPart.startsWith(":")) {
100
+ params[patternPart.slice(1)] = pathPart;
101
+ } else if (patternPart !== pathPart) {
102
+ return null;
103
+ }
104
+ pi++;
105
+ pj++;
106
+ }
107
+ if (pj < pathParts.length) return null;
108
+ return params;
98
109
  }
@@ -13,6 +13,7 @@ export interface RouteFile {
13
13
  | "error"
14
14
  | "simple";
15
15
  paramNames: string[]; // ["slug"] in /blog/:slug
16
+ restParamNames: string[]; // ["rest"] in /blog/*rest
16
17
  }
17
18
 
18
19
  export interface RouteTree {
@@ -25,24 +26,32 @@ export interface RouteTree {
25
26
  export function filePathToRoutePath(
26
27
  filePath: string,
27
28
  routesDir: string,
28
- ): { pattern: string; params: string[] } {
29
+ ): { pattern: string; params: string[]; restParams: string[] } {
29
30
  const rel = relative(routesDir, filePath)
30
- .replace(/\\/g, "/") // windows
31
- .replace(/\.(tsx?|jsx?)$/, "");
31
+ .replace(/\\/g, "/")
32
+ .replace(/\.(tsx?|jsx?)$/, "")
33
+ .replace(/(?:^|\/)(\([^)]+\))(?=\/|$)/g, "") // strip (groupName) anywhere
34
+ .replace(/\/+/g, "/"); // collapse double slashes
32
35
 
33
36
  const params: string[] = [];
37
+ const restParams: string[] = [];
34
38
  const pattern =
35
39
  rel
36
40
  .replace(/\/?(\+\w+(\.\w+)?|index)$/, "") // index/+page → directory
37
- .replace(/\[([^\]]+)\]/g, (_, p) => {
41
+ .replace(/\[(\.\.\.)?([^\]]+)\]/g, (_, dots, name) => {
42
+ // [...rest] → *rest (captures zero or more remaining path segments)
43
+ if (dots) {
44
+ restParams.push(name);
45
+ return `*${name}`;
46
+ }
38
47
  // [param] → :param
39
- params.push(p);
40
- return `:${p}`;
48
+ params.push(name);
49
+ return `:${name}`;
41
50
  })
42
51
  .replace(/^\/?/, "/") || // ensure leading slash
43
52
  "/";
44
53
 
45
- return { pattern, params };
54
+ return { pattern, params, restParams };
46
55
  }
47
56
 
48
57
  export async function scanRoutes(
@@ -58,7 +67,10 @@ export async function scanRoutes(
58
67
  for await (const file of glob.scan(routesDir)) {
59
68
  const filePath = join(routesDir, file);
60
69
  const name = basename(file).replace(/\.(tsx?|jsx?)$/, "");
61
- const { pattern, params } = filePathToRoutePath(filePath, routesDir);
70
+ const { pattern, params, restParams } = filePathToRoutePath(
71
+ filePath,
72
+ routesDir,
73
+ );
62
74
 
63
75
  let type: RouteFile["type"];
64
76
  if (name === "+page") type = "page";
@@ -77,6 +89,7 @@ export async function scanRoutes(
77
89
  clientPath, // add this
78
90
  type,
79
91
  paramNames: params,
92
+ restParamNames: restParams,
80
93
  };
81
94
 
82
95
  if (type === "simple" || type === "page" || type === "pageServer")
@@ -86,12 +99,11 @@ export async function scanRoutes(
86
99
  if (type === "error") errors.push(routeFile);
87
100
  }
88
101
 
89
- // sort so static routes match before dynamic ones
90
- routes.sort((a, b) => {
91
- const aScore = a.paramNames.length;
92
- const bScore = b.paramNames.length;
93
- return aScore - bScore;
94
- });
102
+ // sort so static routes match before dynamic ones, rest params last
103
+ const scoreRoute = (r: RouteFile) =>
104
+ r.paramNames.length + r.restParamNames.length * 1000;
105
+ routes.sort((a, b) => scoreRoute(a) - scoreRoute(b));
106
+ servers.sort((a, b) => scoreRoute(a) - scoreRoute(b));
95
107
 
96
108
  assertNoDuplicatePaths(
97
109
  routes.filter((r) => r.type === "page" || r.type === "simple"),
@@ -9,39 +9,39 @@ const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
9
9
  // Inline the same hash helper used in bun-plugin.ts to avoid importing
10
10
  // unexported internals from @sigil-dev/compiler.
11
11
  function computeHash(filePath: string): string {
12
- let h = 0x811c9dc5;
13
- for (let i = 0; i < filePath.length; i++) {
14
- h ^= filePath.charCodeAt(i);
15
- h = (h * 0x01000193) >>> 0;
16
- }
17
- return h.toString(36);
12
+ let h = 0x811c9dc5;
13
+ for (let i = 0; i < filePath.length; i++) {
14
+ h ^= filePath.charCodeAt(i);
15
+ h = (h * 0x01000193) >>> 0;
16
+ }
17
+ return h.toString(36);
18
18
  }
19
19
 
20
20
  function rewriteRelativeImports(filePath: string) {
21
- const basedir = dirname(filePath);
22
- const rewrite = (source?: { value: string }) => {
23
- if (!source?.value.startsWith(".")) return;
24
- source.value = resolve(basedir, source.value);
25
- };
21
+ const basedir = dirname(filePath);
22
+ const rewrite = (source?: { value: string }) => {
23
+ if (!source?.value.startsWith(".")) return;
24
+ source.value = resolve(basedir, source.value);
25
+ };
26
26
 
27
- return {
28
- name: "sigil-rewrite-relative-route-imports",
29
- visitor: {
30
- ImportDeclaration(path: any) {
31
- rewrite(path.node.source);
32
- },
33
- ExportNamedDeclaration(path: any) {
34
- rewrite(path.node.source);
35
- },
36
- ExportAllDeclaration(path: any) {
37
- rewrite(path.node.source);
38
- },
39
- CallExpression(path: any) {
40
- if (path.node.callee.type !== "Import") return;
41
- rewrite(path.node.arguments[0]);
42
- },
43
- },
44
- };
27
+ return {
28
+ name: "sigil-rewrite-relative-route-imports",
29
+ visitor: {
30
+ ImportDeclaration(path: any) {
31
+ rewrite(path.node.source);
32
+ },
33
+ ExportNamedDeclaration(path: any) {
34
+ rewrite(path.node.source);
35
+ },
36
+ ExportAllDeclaration(path: any) {
37
+ rewrite(path.node.source);
38
+ },
39
+ CallExpression(path: any) {
40
+ if (path.node.callee.type !== "Import") return;
41
+ rewrite(path.node.arguments[0]);
42
+ },
43
+ },
44
+ };
45
45
  }
46
46
 
47
47
  /**
@@ -54,48 +54,48 @@ function rewriteRelativeImports(filePath: string) {
54
54
  * Bun.build only ever sees plain JavaScript.
55
55
  */
56
56
  export async function transformRoutes(
57
- routes: RouteFile[],
58
- outDir: string,
59
- mode: "hydrate" | "dom",
60
- plugins: GrimoirePlugin[] = [],
57
+ routes: RouteFile[],
58
+ outDir: string,
59
+ mode: "hydrate" | "dom",
60
+ plugins: GrimoirePlugin[] = [],
61
61
  ): Promise<Map<string, string>> {
62
- const map = new Map<string, string>();
63
- // loader: "ts" — Babel already consumed JSX, Bun only strips remaining types
64
- const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
62
+ const map = new Map<string, string>();
63
+ // loader: "ts" — Babel already consumed JSX, Bun only strips remaining types
64
+ const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser" });
65
65
 
66
- await Promise.all(
67
- routes.map(async (route, index) => {
68
- let code = await Bun.file(route.filePath).text();
69
- const hash = computeHash(route.filePath);
70
- code = code.replace(STYLE_RE, "");
66
+ await Promise.all(
67
+ routes.map(async (route, index) => {
68
+ let code = await Bun.file(route.filePath).text();
69
+ const hash = computeHash(route.filePath);
70
+ code = code.replace(STYLE_RE, "");
71
71
 
72
- const res = transformSync(code, {
73
- configFile: false,
74
- babelrc: false,
75
- parserOpts: {
76
- plugins: ["typescript", "jsx"], // no isTSX
77
- },
78
- plugins: [
79
- [sigilPlugin, { hash, mode }],
80
- rewriteRelativeImports(route.filePath),
81
- ],
82
- filename: route.filePath,
83
- });
84
- let out = transpiler.transformSync(res?.code ?? "");
85
- for (const plugin of plugins) {
86
- if (plugin.transform)
87
- out = (await plugin.transform(out, route.filePath)) ?? out;
88
- }
72
+ const res = transformSync(code, {
73
+ configFile: false,
74
+ babelrc: false,
75
+ parserOpts: {
76
+ plugins: ["typescript", "jsx"], // no isTSX
77
+ },
78
+ plugins: [
79
+ [sigilPlugin, { hash, mode }],
80
+ rewriteRelativeImports(route.filePath),
81
+ ],
82
+ filename: route.filePath,
83
+ });
84
+ let out = transpiler.transformSync(res?.code ?? "");
85
+ for (const plugin of plugins) {
86
+ if (plugin.transform)
87
+ out = (await plugin.transform(out, route.filePath)) ?? out;
88
+ }
89
89
 
90
- const rel = relative(process.cwd(), route.filePath)
91
- .replace(/\.tsx?$/, "")
92
- .replace(/[^a-zA-Z0-9._-]/g, "_");
93
- const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
94
- const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
90
+ const rel = relative(process.cwd(), route.filePath)
91
+ .replace(/\.tsx?$/, "")
92
+ .replace(/[^a-zA-Z0-9._-]/g, "_");
93
+ const nameBase = rel || basename(route.filePath).replace(/\.[jt]sx$/, "");
94
+ const outPath = join(outDir, `${index}-${nameBase}.${mode}.js`);
95
95
 
96
- await Bun.write(outPath, out);
97
- map.set(route.filePath, outPath);
98
- }),
99
- );
100
- return map;
96
+ await Bun.write(outPath, out);
97
+ map.set(route.filePath, outPath);
98
+ }),
99
+ );
100
+ return map;
101
101
  }