@anaemia/bundler 0.0.1

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 (61) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/dist/aliases.d.ts +8 -0
  4. package/dist/aliases.d.ts.map +1 -0
  5. package/dist/aliases.js +10 -0
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +153 -0
  9. package/dist/optimization.d.ts +41 -0
  10. package/dist/optimization.d.ts.map +1 -0
  11. package/dist/optimization.js +35 -0
  12. package/dist/plugins/babel-hash-injector-server.d.ts +7 -0
  13. package/dist/plugins/babel-hash-injector-server.d.ts.map +1 -0
  14. package/dist/plugins/babel-hash-injector-server.js +17 -0
  15. package/dist/plugins/babel-transform-client.d.ts +6 -0
  16. package/dist/plugins/babel-transform-client.d.ts.map +1 -0
  17. package/dist/plugins/babel-transform-client.js +49 -0
  18. package/dist/plugins/babel-transform-server.d.ts +11 -0
  19. package/dist/plugins/babel-transform-server.d.ts.map +1 -0
  20. package/dist/plugins/babel-transform-server.js +60 -0
  21. package/dist/plugins/rspack-manifest-hydration.d.ts +11 -0
  22. package/dist/plugins/rspack-manifest-hydration.d.ts.map +1 -0
  23. package/dist/plugins/rspack-manifest-hydration.js +38 -0
  24. package/dist/plugins/server-function-id.d.ts +2 -0
  25. package/dist/plugins/server-function-id.d.ts.map +1 -0
  26. package/dist/plugins/server-function-id.js +3 -0
  27. package/dist/router/generate-entry.d.ts +3 -0
  28. package/dist/router/generate-entry.d.ts.map +1 -0
  29. package/dist/router/generate-entry.js +243 -0
  30. package/dist/router/generate-server-routes.d.ts +3 -0
  31. package/dist/router/generate-server-routes.d.ts.map +1 -0
  32. package/dist/router/generate-server-routes.js +30 -0
  33. package/dist/router/manifest.d.ts +12 -0
  34. package/dist/router/manifest.d.ts.map +1 -0
  35. package/dist/router/manifest.js +23 -0
  36. package/dist/router/scan.d.ts +22 -0
  37. package/dist/router/scan.d.ts.map +1 -0
  38. package/dist/router/scan.js +164 -0
  39. package/dist/rules.d.ts +47 -0
  40. package/dist/rules.d.ts.map +1 -0
  41. package/dist/rules.js +42 -0
  42. package/dist/server-function-id.d.ts +2 -0
  43. package/dist/server-function-id.d.ts.map +1 -0
  44. package/dist/server-function-id.js +3 -0
  45. package/package.json +36 -0
  46. package/src/aliases.ts +11 -0
  47. package/src/index.ts +170 -0
  48. package/src/optimization.ts +37 -0
  49. package/src/plugins/babel-hash-injector-server.ts +19 -0
  50. package/src/plugins/babel-transform-server.ts +144 -0
  51. package/src/plugins/rspack-manifest-hydration.ts +56 -0
  52. package/src/router/generate-entry.ts +306 -0
  53. package/src/router/generate-server-routes.ts +36 -0
  54. package/src/router/manifest.ts +42 -0
  55. package/src/router/scan.ts +228 -0
  56. package/src/rules.ts +58 -0
  57. package/src/runtime/empty-module.cjs +10 -0
  58. package/src/server-function-id.ts +3 -0
  59. package/test/dev-config.test.mjs +30 -0
  60. package/test/server-functions.test.mjs +77 -0
  61. package/tsconfig.json +13 -0
@@ -0,0 +1,306 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { RouteManifestEntry } from "./scan.js";
4
+
5
+ type LayoutNode = {
6
+ kind: "layout";
7
+ layoutFile: string;
8
+ layoutIdx: number;
9
+ relativePath: string;
10
+ prefixPath: string;
11
+ children: TreeNode[];
12
+ };
13
+
14
+ type PageNode = {
15
+ kind: "page";
16
+ route: RouteManifestEntry;
17
+ routeIdx: number;
18
+ relativePath: string;
19
+ };
20
+
21
+ type TreeNode = LayoutNode | PageNode;
22
+
23
+ function buildTree(routes: RouteManifestEntry[], strippedLayouts: string[][], routeIndices: number[], routesDir: string, allLayouts: Map<string, number>, parentPrefix: string): TreeNode[] {
24
+ const nodes: TreeNode[] = [];
25
+ const leafIndices = strippedLayouts.map((l, i) => (l.length === 0 ? i : -1)).filter((i) => i !== -1);
26
+
27
+ for (const i of leafIndices) {
28
+ const route = routes[routeIndices[i]];
29
+ const relativePath = toRelativePath(route.urlPattern, parentPrefix);
30
+ nodes.push({
31
+ kind: "page",
32
+ route,
33
+ routeIdx: routeIndices[i],
34
+ relativePath,
35
+ });
36
+ }
37
+
38
+ const byLayout = new Map<string, { ri: number[]; sl: string[][] }>();
39
+ for (let i = 0; i < strippedLayouts.length; i++) {
40
+ if (strippedLayouts[i].length === 0) continue;
41
+ const nextLayout = strippedLayouts[i][0];
42
+ if (!byLayout.has(nextLayout)) byLayout.set(nextLayout, { ri: [], sl: [] });
43
+ byLayout.get(nextLayout)!.ri.push(routeIndices[i]);
44
+ byLayout.get(nextLayout)!.sl.push(strippedLayouts[i].slice(1));
45
+ }
46
+
47
+ for (const [layoutFile, { ri, sl }] of byLayout) {
48
+ const layoutIdx = allLayouts.get(layoutFile)!;
49
+ const layoutDir = path.dirname(path.relative(routesDir, layoutFile));
50
+ const prefixPath = layoutDir === "." ? "/" : `/${layoutDir.replace(/\\/g, "/")}`;
51
+ const relativePath = toRelativePath(prefixPath, parentPrefix);
52
+
53
+ const children = buildTree(routes, sl, ri, routesDir, allLayouts, prefixPath);
54
+ nodes.push({ kind: "layout", layoutFile, layoutIdx, prefixPath, children, relativePath });
55
+ }
56
+
57
+ return nodes;
58
+ }
59
+
60
+ function toRelativePath(absolute: string, parentPrefix: string): string {
61
+ if (parentPrefix === "/" || parentPrefix === "") return absolute;
62
+ if (absolute.startsWith(parentPrefix)) {
63
+ const rel = absolute.slice(parentPrefix.length) || "/";
64
+ return rel.startsWith("/") ? rel : `/${rel}`;
65
+ }
66
+ return absolute;
67
+ }
68
+
69
+ function renderTree(nodes: TreeNode[], indent = 6): string {
70
+ const pad = " ".repeat(indent);
71
+ return nodes
72
+ .map((node) => {
73
+ if (node.kind === "page") {
74
+ let routePath = node.relativePath;
75
+
76
+ if (node.route.type === "catch-all") {
77
+ const paramName = node.route.params[0] || "any";
78
+ if (routePath.endsWith("*")) {
79
+ routePath = routePath.slice(0, -1) + `*${paramName}`;
80
+ }
81
+ }
82
+
83
+ if (indent > 6 && routePath === "/") {
84
+ routePath = "";
85
+ } else if (indent > 6 && routePath.startsWith("/") && routePath !== "/") {
86
+ routePath = routePath.slice(1);
87
+ }
88
+
89
+ if (routePath === "") {
90
+ return `${pad}<Route component={Route${node.routeIdx}Wrapped} />`;
91
+ }
92
+
93
+ return `${pad}<Route path="${routePath}" component={Route${node.routeIdx}Wrapped} />`;
94
+ }
95
+
96
+ let layoutPath = (node as any).relativePath;
97
+ const inner = renderTree(node.children, indent + 2);
98
+
99
+ return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join("\n");
100
+ })
101
+ .join("\n");
102
+ }
103
+
104
+ function buildPreloadMapString(routes: RouteManifestEntry[], allLayouts: Map<string, number>): string {
105
+ const mapLines = routes.map((r, i) => {
106
+ const layoutTokens = r.layouts.map((l) => `Layout${allLayouts.get(l.filePath)}`);
107
+ const pageToken = `Route${i}`;
108
+ const tokensArrayString = `[${[...layoutTokens, pageToken].join(", ")}]`;
109
+ return ` "${r.urlPattern}": ${tokensArrayString}`;
110
+ });
111
+
112
+ return `const chunkPreloadRegistry = {\n${mapLines.join(",\n")}\n};`;
113
+ }
114
+
115
+ export function generateRouterEntry(appRoot: string, routes: RouteManifestEntry[]): string {
116
+ const routesDir = path.resolve(appRoot, "./src/routes");
117
+ const outDir = path.resolve(appRoot, "./.anaemia");
118
+ const outPath = path.resolve(outDir, "./__anaemia_entry__.tsx");
119
+
120
+ const conventionalRoutes = routes.filter((r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx"));
121
+ const errorRoutes = routes.filter((r) => r.filePath.endsWith("404.tsx") || r.filePath.endsWith("500.tsx"));
122
+
123
+ const allLayouts = new Map<string, number>();
124
+
125
+ let layoutIndex = 0;
126
+ for (const entry of conventionalRoutes) {
127
+ for (const layout of entry.layouts) {
128
+ if (!allLayouts.has(layout.filePath)) {
129
+ allLayouts.set(layout.filePath, layoutIndex++);
130
+ }
131
+ }
132
+ }
133
+
134
+ const routeImports = conventionalRoutes
135
+ .map((r, i) => {
136
+ const relativeToRoutes = path.relative(routesDir, r.filePath);
137
+ const chunkName = relativeToRoutes
138
+ .replace(/\.[jt]sx?$/, "")
139
+ .replace(/[^a-zA-Z0-9-_\[\]]/g, "-")
140
+ .toLowerCase();
141
+
142
+ const guardSources = [...r.layouts.map((l) => l.filePath), r.filePath]
143
+ .map((fp) => {
144
+ const configPath = fp.replace(/\.(tsx|jsx)$/, ".config.ts");
145
+ const guardPath = fs.existsSync(configPath) ? configPath : fp.replace(/\.(jsx)$/, ".config.js");
146
+ const resolvedGuardPath = fs.existsSync(guardPath) ? guardPath : fp;
147
+ return `() => import("${resolvedGuardPath.replace(/\\/g, "/")}").then(m => m?.config?.guards ?? [])`;
148
+ })
149
+ .join(",\n ");
150
+
151
+ return `
152
+ const Route${i} = lazy(() => import(/* webpackChunkName: "${chunkName}" */ "${r.filePath.replace(/\\/g, "/")}"));
153
+ const Route${i}Loader = async (args) => {
154
+ const _guardSources = [
155
+ ${guardSources}
156
+ ];
157
+ for (const loadGuards of _guardSources) {
158
+ const guards = await loadGuards();
159
+ for (const guard of guards) {
160
+ const result = await guard({ params: args.params, request: args.request, url: args.location?.pathname ?? "" });
161
+ if (result?.redirect) {
162
+ throw Object.assign(new Error("guard:redirect"), { redirect: result.redirect, status: result.status ?? 302 });
163
+ }
164
+ if (result?.status) {
165
+ throw Object.assign(new Error("guard:error"), { status: result.status });
166
+ }
167
+ }
168
+ }
169
+ return import(/* webpackChunkName: "${chunkName}" */ "${r.filePath.replace(/\\/g, "/")}").then(mod => {
170
+ return mod.loader ? mod.loader(args) : null;
171
+ });
172
+ };
173
+ const Route${i}Wrapped = (props) => (
174
+ <RouteDataController loader={Route${i}Loader}>
175
+ <Route${i} {...props} />
176
+ </RouteDataController>
177
+ );`.trim();
178
+ })
179
+ .join("\n");
180
+
181
+ const layoutImports = [...allLayouts.entries()]
182
+ .map(([file, i]) => {
183
+ const relativeToRoutes = path.relative(routesDir, file);
184
+ const chunkName = ("layout-" + relativeToRoutes.replace(/\.[jt]sx?$/, "").replace(/[^a-zA-Z0-9-_\[\]]/g, "-")).toLowerCase();
185
+
186
+ return `import Layout${i} from "${file.replace(/\\/g, "/")}";`;
187
+ })
188
+ .join("\n");
189
+
190
+ const tree = buildTree(
191
+ conventionalRoutes,
192
+ conventionalRoutes.map((r) => r.layouts.map((l) => l.filePath)),
193
+ conventionalRoutes.map((_, i) => i),
194
+ routesDir,
195
+ allLayouts,
196
+ "/"
197
+ );
198
+
199
+ let routeJsx = renderTree(tree, 6);
200
+
201
+ const has404 = errorRoutes.some((r) => r.filePath.endsWith("404.tsx"));
202
+ if (has404) {
203
+ const idx = routes.findIndex((r) => r.filePath.endsWith("404.tsx"));
204
+ routeJsx += `\n <Route path="*any" component={Route${idx}} />`;
205
+ }
206
+
207
+ const rootWrapperPath = path.resolve(appRoot, "./src/root.tsx");
208
+ const hasRootWrapper = fs.existsSync(rootWrapperPath);
209
+
210
+ const rootImport = hasRootWrapper ? `import RootWrapper from "../src/root.tsx";` : ``;
211
+
212
+ const rootWrapperCode = hasRootWrapper
213
+ ? `const RootWrapperComponent = (props) => (
214
+ <RootWrapper {...props} />
215
+ );`
216
+ : ``;
217
+
218
+ const finalJsx = hasRootWrapper
219
+ ? ` <Route component={RootWrapperComponent}>
220
+ ${routeJsx}
221
+ </Route>`
222
+ : routeJsx;
223
+
224
+ const registryEntries = routes
225
+ .map((r) => {
226
+ return ` ["${r.urlPattern}", async (args) => {
227
+ const mod = await import("${r.filePath.replace(/\\/g, "/")}");
228
+ return mod.loader ? mod.loader(args) : null;
229
+ }]`;
230
+ })
231
+ .join(",\n");
232
+
233
+ const guardRegistryEntries = conventionalRoutes
234
+ .map((r) => {
235
+ const sources = [...r.layouts.map((l) => l.filePath), r.filePath];
236
+
237
+ const loaders = sources
238
+ .map((fp) => {
239
+ const configPath = fp.replace(/\.(tsx|jsx)$/, ".config.ts");
240
+ const guardPath = fs.existsSync(configPath) ? configPath : fp.replace(/\.jsx$/, ".config.js");
241
+ const resolvedGuardPath = fs.existsSync(guardPath) ? guardPath : fp;
242
+ return `async () => { const m = await import("${resolvedGuardPath.replace(/\\/g, "/")}"); return m?.config?.guards ?? []; }`;
243
+ })
244
+ .join(",\n ");
245
+
246
+ return ` ["${r.urlPattern}", [\n ${loaders}\n ]]`;
247
+ })
248
+ .join(",\n");
249
+
250
+ const chunkPreloadRegistryCode = buildPreloadMapString(conventionalRoutes, allLayouts);
251
+
252
+ const preloadFnCode = `
253
+ export async function preloadActiveClientRoute(pathname: string) {
254
+ // Simple match logic against parameters or route patterns
255
+ const pattern = Object.keys(chunkPreloadRegistry).find(p => {
256
+ if (p === pathname) return true;
257
+ const regexStr = p.replace(/:([a-zA-Z0-9_-]+)/g, "([^/]+)").replace(/\\*([a-zA-Z0-9_-]*)/g, "(.*)");
258
+ return new RegExp("^" + regexStr + "$").test(pathname);
259
+ });
260
+
261
+ const componentsToPreload = pattern ? chunkPreloadRegistry[pattern] : [];
262
+ const preloads = componentsToPreload
263
+ .filter(c => typeof c.preload === "function")
264
+ .map(c => c.preload());
265
+
266
+ await Promise.all(preloads);
267
+ }
268
+ `.trim();
269
+
270
+ const code = `
271
+ // @ts-nocheck
272
+ // auto-generated by anaemia - do not edit!!
273
+ import { lazy } from "solid-js";
274
+ import { Route } from "@solidjs/router";
275
+ import { RouteDataController } from "@anaemia/core";
276
+
277
+ ${rootImport}
278
+
279
+ ${rootWrapperCode}
280
+
281
+ ${routeImports}
282
+
283
+ ${layoutImports}
284
+
285
+ ${chunkPreloadRegistryCode}
286
+
287
+ ${preloadFnCode}
288
+
289
+ export const serverLoaderRegistry = new Map([\n${registryEntries}\n]);
290
+
291
+ export const serverGuardRegistry = new Map([\n${guardRegistryEntries}\n]);
292
+
293
+ export default function AnaemiaRoutes() {
294
+ return (
295
+ ${finalJsx}
296
+ );
297
+ }
298
+ `.trimStart();
299
+
300
+ if (!fs.existsSync(outDir)) {
301
+ fs.mkdirSync(outDir, { recursive: true });
302
+ }
303
+
304
+ fs.writeFileSync(outPath, code);
305
+ return outPath;
306
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { ServerRouteEntry } from "./scan.js";
4
+
5
+ export function generateServerRoutes(appRoot: string, routes: ServerRouteEntry[]): string {
6
+ const outDir = path.resolve(appRoot, "./.anaemia");
7
+ const outPath = path.resolve(outDir, "./__anaemia_server_routes__.ts");
8
+
9
+ const imports = routes.map((r, i) => `import * as ServerRoute${i} from "${r.filePath}";`).join("\n");
10
+
11
+ const registrations = routes.map((r, i) => ` registerRoute(app, "${r.urlPattern}", ServerRoute${i});`).join("\n");
12
+
13
+ const code = `
14
+ // @ts-nocheck
15
+ // auto-generated by anaemia - do not edit!!
16
+ import type { Hono } from "hono";
17
+
18
+ ${imports}
19
+
20
+ function registerRoute(app: any, pattern: string, mod: any) {
21
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
22
+ for (const m of methods) {
23
+ if (m in mod && typeof mod[m] === "function") {
24
+ app[m.toLowerCase()](pattern, mod[m]);
25
+ }
26
+ }
27
+ }
28
+
29
+ export function registerServerRoutes(app: Hono) {
30
+ ${registrations}
31
+ }
32
+ `.trimStart();
33
+
34
+ fs.writeFileSync(outPath, code);
35
+ return outPath;
36
+ }
@@ -0,0 +1,42 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { RouteManifestEntry } from "./scan.js";
4
+
5
+ export interface BuildManifest {
6
+ routes: RouteManifestEntry[];
7
+ // filled in after rspack build - maps chunkName to hashed filename
8
+ chunks: Record<string, { js: string; css?: string }>;
9
+ errors: Record<string, string>;
10
+ buildTime: string;
11
+ }
12
+
13
+ export function writeManifest(appRoot: string, routes: RouteManifestEntry[]): void {
14
+ const errors: Record<string, string> = {};
15
+
16
+ for (const route of routes) {
17
+ if (route.filePath.endsWith("404.tsx")) {
18
+ errors["404"] = route.urlPattern;
19
+ }
20
+ if (route.filePath.endsWith("500.tsx")) {
21
+ errors["500"] = route.urlPattern;
22
+ }
23
+ }
24
+
25
+ const conventionalRoutes = routes.filter(
26
+ (r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx")
27
+ );
28
+
29
+ const manifest: BuildManifest = {
30
+ routes: conventionalRoutes,
31
+ errors,
32
+ chunks: {}, // rspack fills this in via ManifestPlugin
33
+ buildTime: new Date().toISOString(),
34
+ };
35
+
36
+ const outDir = path.resolve(appRoot, "./dist");
37
+ fs.mkdirSync(outDir, { recursive: true });
38
+ fs.writeFileSync(
39
+ path.resolve(outDir, "route-manifest.json"),
40
+ JSON.stringify(manifest, null, 2)
41
+ );
42
+ }
@@ -0,0 +1,228 @@
1
+ import { glob } from "glob";
2
+ import path from "path";
3
+ import { createJiti } from "jiti";
4
+ import fs from "node:fs";
5
+ import { getAliases } from "../aliases.js";
6
+
7
+ export function createScanJiti(appRoot: string) {
8
+ return createJiti(import.meta.url, {
9
+ interopDefault: true,
10
+ alias: getAliases(appRoot),
11
+ });
12
+ }
13
+
14
+ export type RouteType = "page" | "layout" | "catch-all";
15
+
16
+ export interface LayoutManifestEntry {
17
+ filePath: string;
18
+ guards: any[];
19
+ }
20
+
21
+ export interface RouteManifestEntry {
22
+ // the URL pattern this route matches
23
+ // e.g. /blog/:slug, /dashboard, /
24
+ urlPattern: string;
25
+
26
+ // absolute path to the route file
27
+ filePath: string;
28
+
29
+ // rspack chunk name derived from the path
30
+ chunkName: string;
31
+
32
+ // ordered list of layout files that wrap this route
33
+ // e.g. [root layout, dashboard layout]
34
+ layouts: LayoutManifestEntry[];
35
+
36
+ // holds page level guards
37
+ guards: any[];
38
+ // what type of file this is
39
+ type: RouteType;
40
+
41
+ // the dynamic params this route exposes
42
+ // e.g. /blog/[slug] -> ["slug"]
43
+ params: string[];
44
+ }
45
+
46
+ export interface ServerRouteEntry {
47
+ urlPattern: string;
48
+ filePath: string;
49
+ }
50
+
51
+ const LAYOUT_FILE = /^_layout\.(tsx|jsx)$/;
52
+ const CATCH_ALL_FILE = /^\[\.\.\.(.+?)\]\.(tsx|jsx)$/;
53
+ const DYNAMIC_SEGMENT = /^\[(.+?)\]\.(tsx|jsx)$/;
54
+
55
+ export function scanServerRoutes(appRoot: string): ServerRouteEntry[] {
56
+ const routesDir = path.resolve(appRoot, "./src/routes");
57
+ const files = glob.sync("**/_route.{ts,tsx}", { cwd: routesDir, posix: true });
58
+
59
+ return files.map((file) => {
60
+ const dir = path.dirname(file);
61
+ const urlPattern = dir === "." ? "/" : `/${dir}`;
62
+ return {
63
+ urlPattern,
64
+ filePath: path.resolve(routesDir, file),
65
+ };
66
+ });
67
+ }
68
+
69
+ export async function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]> {
70
+ const jiti = createScanJiti(appRoot);
71
+ const routesDir = path.resolve(appRoot, "./src/routes");
72
+ const files = glob.sync("**/*.{tsx,jsx}", { cwd: routesDir, posix: true });
73
+
74
+ const layoutMap = new Map<string, LayoutManifestEntry>();
75
+ for (const file of files) {
76
+ const filename = path.basename(file);
77
+ if (LAYOUT_FILE.test(filename)) {
78
+ const dir = path.dirname(file);
79
+ const absolutePath = path.resolve(routesDir, file);
80
+
81
+ let layoutGuards: any[] = [];
82
+ const configPath = absolutePath
83
+ .replace(/\.(tsx|jsx)$/, ".config.$1")
84
+ .replace(".config.tsx", ".config.ts")
85
+ .replace(".config.jsx", ".config.js");
86
+
87
+ const moduleToScan = fs.existsSync(configPath) ? configPath : absolutePath;
88
+
89
+ try {
90
+ const layoutModule = jiti.import(moduleToScan) as any;
91
+ if (layoutModule?.config?.guards) {
92
+ layoutGuards = layoutModule.config.guards;
93
+ }
94
+ } catch (err) {
95
+ console.warn(`[anaemia bundler warning]: Failed parsing config flags on layout: ${file}`);
96
+ }
97
+
98
+ layoutMap.set(dir, {
99
+ filePath: absolutePath,
100
+ guards: layoutGuards,
101
+ });
102
+ }
103
+ }
104
+
105
+ const entries: RouteManifestEntry[] = [];
106
+
107
+ for (const file of files) {
108
+ const filename = path.basename(file);
109
+ const dir = path.dirname(file);
110
+
111
+ if (LAYOUT_FILE.test(filename)) continue;
112
+ if (filename.includes(".config.")) continue;
113
+
114
+ const absolutePagePath = path.resolve(routesDir, file);
115
+ const { urlPattern, chunkName, params, type } = parseFilePath(file);
116
+
117
+ let pageGuards: any[] = [];
118
+
119
+ const pageConfigPath = absolutePagePath
120
+ .replace(/\.(tsx|jsx)$/, ".config.$1")
121
+ .replace(".config.tsx", ".config.ts")
122
+ .replace(".config.jsx", ".config.js");
123
+
124
+ const pageModuleToScan = fs.existsSync(pageConfigPath) ? pageConfigPath : absolutePagePath;
125
+
126
+ try {
127
+ const pageModule = (await jiti.import(pageModuleToScan)) as any;
128
+ if (pageModule?.config?.guards) {
129
+ pageGuards = pageModule.config.guards;
130
+ }
131
+ } catch (err) {
132
+ // quietly ignore parsing problems for pure components lacking a config wrapper
133
+ }
134
+
135
+ const layouts = resolveLayoutChain(dir, layoutMap);
136
+
137
+ entries.push({
138
+ urlPattern,
139
+ filePath: absolutePagePath,
140
+ chunkName,
141
+ layouts,
142
+ guards: pageGuards,
143
+ type,
144
+ params,
145
+ });
146
+ }
147
+
148
+ return entries;
149
+ }
150
+
151
+ function parseFilePath(file: string): {
152
+ urlPattern: string;
153
+ chunkName: string;
154
+ params: string[];
155
+ type: RouteType;
156
+ } {
157
+ const segments = file.split("/");
158
+ const urlParts: string[] = [];
159
+ const params: string[] = [];
160
+ let type: RouteType = "page";
161
+
162
+ for (let i = 0; i < segments.length; i++) {
163
+ const segment = segments[i];
164
+ const isLast = i === segments.length - 1;
165
+
166
+ if (isLast) {
167
+ const cleanName = segment.replace(/\.(tsx|jsx)$/, "");
168
+
169
+ if (CATCH_ALL_FILE.test(segment)) {
170
+ const match = segment.match(CATCH_ALL_FILE)!;
171
+ params.push(match[1]);
172
+ urlParts.push(`*`);
173
+ type = "catch-all";
174
+ } else if (DYNAMIC_SEGMENT.test(segment)) {
175
+ const match = segment.match(DYNAMIC_SEGMENT)!;
176
+ params.push(match[1]);
177
+ urlParts.push(`:${match[1]}`);
178
+ } else if (cleanName !== "index") {
179
+ urlParts.push(cleanName);
180
+ }
181
+ } else {
182
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
183
+ const param = segment.slice(4, -1);
184
+ params.push(param);
185
+ urlParts.push(`*`);
186
+ type = "catch-all";
187
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
188
+ const param = segment.slice(1, -1);
189
+ params.push(param);
190
+ urlParts.push(`:${param}`);
191
+ } else {
192
+ urlParts.push(segment);
193
+ }
194
+ }
195
+ }
196
+
197
+ const filteredParts = urlParts.filter((part) => part !== "" && part !== ".");
198
+ let urlPattern = "/" + filteredParts.join("/");
199
+
200
+ if (urlPattern === "") {
201
+ urlPattern = "/";
202
+ }
203
+
204
+ const chunkName = file
205
+ .replace(/\.(tsx|jsx)$/, "")
206
+ .replace(/\[\.\.\.(.+?)\]/g, "catchall-$1")
207
+ .replace(/\[(.+?)\]/g, "param-$1")
208
+ .replace(/\//g, "-")
209
+ .replace(/^-+|-+$/g, "")
210
+ .toLowerCase();
211
+
212
+ return { urlPattern, chunkName, params, type };
213
+ }
214
+
215
+ function resolveLayoutChain(dir: string, layoutMap: Map<string, LayoutManifestEntry>): LayoutManifestEntry[] {
216
+ const layouts: LayoutManifestEntry[] = [];
217
+ let current = dir;
218
+
219
+ while (true) {
220
+ const layoutEntry = layoutMap.get(current);
221
+ if (layoutEntry) layouts.unshift(layoutEntry);
222
+
223
+ if (current === "." || current === "") break;
224
+ current = path.dirname(current);
225
+ }
226
+
227
+ return layouts;
228
+ }
package/src/rules.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import type { AnaemiaConfig } from "@anaemia/core";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ export function createStyleRules(config: AnaemiaConfig) {
8
+ const useSass = config.styles?.sass !== false;
9
+ const useModules = config.styles?.modules ?? true;
10
+
11
+ const baseLoaders = useSass ? [{ loader: require.resolve("sass-loader"), options: { api: "modern" } }] : [];
12
+
13
+ return {
14
+ client: {
15
+ test: /\.(c|sc|sa)ss$/,
16
+ type: useModules ? "css/auto" : ("css" as const),
17
+ use: baseLoaders,
18
+ },
19
+ server: {
20
+ test: /\.(c|sc|sa)ss$/,
21
+ type: useModules ? "css/auto" : ("css" as const),
22
+ generator: {
23
+ css: {
24
+ exportOnlyLocals: true,
25
+ },
26
+ },
27
+ use: baseLoaders,
28
+ },
29
+ };
30
+ }
31
+
32
+ export function createBabelRule({
33
+ isServer,
34
+ isDev,
35
+ plugins = [],
36
+ }: {
37
+ isServer: boolean;
38
+ isDev: boolean;
39
+ plugins: any[];
40
+ }) {
41
+ const generateMode = isServer ? "ssr" : "dom";
42
+
43
+ return {
44
+ test: /\.[jt]sx?$/,
45
+ use: [
46
+ {
47
+ loader: require.resolve("babel-loader"),
48
+ options: {
49
+ presets: [
50
+ [require.resolve("babel-preset-solid"), { generate: generateMode, hydratable: true, dev: isDev }],
51
+ require.resolve("@babel/preset-typescript"),
52
+ ],
53
+ plugins: plugins,
54
+ },
55
+ },
56
+ ],
57
+ };
58
+ }
@@ -0,0 +1,10 @@
1
+ const createInfiniteProxy = () => {
2
+ return new Proxy(() => {}, {
3
+ get(target, prop) {
4
+ if (prop === "__esModule") return true;
5
+ return createInfiniteProxy();
6
+ },
7
+ });
8
+ };
9
+
10
+ module.exports = createInfiniteProxy();
@@ -0,0 +1,3 @@
1
+ export function createServerFunctionId(filename: string, start: number | null | undefined) {
2
+ return Buffer.from(`${filename}:${start ?? 0}`).toString("base64url");
3
+ }