@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,11 @@
1
+ import type { RspackPluginInstance, Compiler } from "@rspack/core";
2
+ interface HydrationPluginOptions {
3
+ appRoot: string;
4
+ }
5
+ export declare class AnaemiaManifestHydrationPlugin implements RspackPluginInstance {
6
+ private appRoot;
7
+ constructor(options: HydrationPluginOptions);
8
+ apply(compiler: Compiler): void;
9
+ }
10
+ export {};
11
+ //# sourceMappingURL=rspack-manifest-hydration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rspack-manifest-hydration.d.ts","sourceRoot":"","sources":["../../src/plugins/rspack-manifest-hydration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEnE,UAAU,sBAAsB;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,8BAA+B,YAAW,oBAAoB;IACzE,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,sBAAsB;IAI3C,KAAK,CAAC,QAAQ,EAAE,QAAQ;CAwCzB"}
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export class AnaemiaManifestHydrationPlugin {
4
+ appRoot;
5
+ constructor(options) {
6
+ this.appRoot = options.appRoot;
7
+ }
8
+ apply(compiler) {
9
+ compiler.hooks.emit.tap("AnaemiaManifestHydrationPlugin", (compilation) => {
10
+ const manifestPath = path.resolve(this.appRoot, "./dist/route-manifest.json");
11
+ if (!fs.existsSync(manifestPath))
12
+ return;
13
+ try {
14
+ const currentManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
15
+ if (!currentManifest.chunks) {
16
+ currentManifest.chunks = {};
17
+ }
18
+ for (const chunk of compilation.chunks) {
19
+ if (!chunk.name)
20
+ continue;
21
+ const files = Array.from(chunk.files);
22
+ const jsFiles = files.filter((f) => f.endsWith(".js") && !f.includes(".hot-update.") && !f.endsWith(".js.map"));
23
+ const cssFiles = files.filter((f) => f.endsWith(".css") && !f.includes(".hot-update.") && !f.endsWith(".css.map"));
24
+ if (jsFiles.length > 0 || cssFiles.length > 0) {
25
+ currentManifest.chunks[chunk.name] = {
26
+ js: jsFiles.map((f) => `/${f}`),
27
+ css: cssFiles.map((f) => `/${f}`),
28
+ };
29
+ }
30
+ }
31
+ fs.writeFileSync(manifestPath, JSON.stringify(currentManifest, null, 2));
32
+ }
33
+ catch (e) {
34
+ console.error("[anaemia compiler] failed updating route-manifest with assets:", e.message);
35
+ }
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ export declare function createServerFunctionId(filename: string, start: number | null | undefined): string;
2
+ //# sourceMappingURL=server-function-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-function-id.d.ts","sourceRoot":"","sources":["../../src/plugins/server-function-id.ts"],"names":[],"mappings":"AAAA,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,UAExF"}
@@ -0,0 +1,3 @@
1
+ export function createServerFunctionId(filename, start) {
2
+ return Buffer.from(`${filename}:${start ?? 0}`).toString("base64url");
3
+ }
@@ -0,0 +1,3 @@
1
+ import type { RouteManifestEntry } from "./scan.js";
2
+ export declare function generateRouterEntry(appRoot: string, routes: RouteManifestEntry[]): string;
3
+ //# sourceMappingURL=generate-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-entry.d.ts","sourceRoot":"","sources":["../../src/router/generate-entry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAgHpD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,CA+LzF"}
@@ -0,0 +1,243 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ function buildTree(routes, strippedLayouts, routeIndices, routesDir, allLayouts, parentPrefix) {
4
+ const nodes = [];
5
+ const leafIndices = strippedLayouts.map((l, i) => (l.length === 0 ? i : -1)).filter((i) => i !== -1);
6
+ for (const i of leafIndices) {
7
+ const route = routes[routeIndices[i]];
8
+ const relativePath = toRelativePath(route.urlPattern, parentPrefix);
9
+ nodes.push({
10
+ kind: "page",
11
+ route,
12
+ routeIdx: routeIndices[i],
13
+ relativePath,
14
+ });
15
+ }
16
+ const byLayout = new Map();
17
+ for (let i = 0; i < strippedLayouts.length; i++) {
18
+ if (strippedLayouts[i].length === 0)
19
+ continue;
20
+ const nextLayout = strippedLayouts[i][0];
21
+ if (!byLayout.has(nextLayout))
22
+ byLayout.set(nextLayout, { ri: [], sl: [] });
23
+ byLayout.get(nextLayout).ri.push(routeIndices[i]);
24
+ byLayout.get(nextLayout).sl.push(strippedLayouts[i].slice(1));
25
+ }
26
+ for (const [layoutFile, { ri, sl }] of byLayout) {
27
+ const layoutIdx = allLayouts.get(layoutFile);
28
+ const layoutDir = path.dirname(path.relative(routesDir, layoutFile));
29
+ const prefixPath = layoutDir === "." ? "/" : `/${layoutDir.replace(/\\/g, "/")}`;
30
+ const relativePath = toRelativePath(prefixPath, parentPrefix);
31
+ const children = buildTree(routes, sl, ri, routesDir, allLayouts, prefixPath);
32
+ nodes.push({ kind: "layout", layoutFile, layoutIdx, prefixPath, children, relativePath });
33
+ }
34
+ return nodes;
35
+ }
36
+ function toRelativePath(absolute, parentPrefix) {
37
+ if (parentPrefix === "/" || parentPrefix === "")
38
+ return absolute;
39
+ if (absolute.startsWith(parentPrefix)) {
40
+ const rel = absolute.slice(parentPrefix.length) || "/";
41
+ return rel.startsWith("/") ? rel : `/${rel}`;
42
+ }
43
+ return absolute;
44
+ }
45
+ function renderTree(nodes, indent = 6) {
46
+ const pad = " ".repeat(indent);
47
+ return nodes
48
+ .map((node) => {
49
+ if (node.kind === "page") {
50
+ let routePath = node.relativePath;
51
+ if (node.route.type === "catch-all") {
52
+ const paramName = node.route.params[0] || "any";
53
+ if (routePath.endsWith("*")) {
54
+ routePath = routePath.slice(0, -1) + `*${paramName}`;
55
+ }
56
+ }
57
+ if (indent > 6 && routePath === "/") {
58
+ routePath = "";
59
+ }
60
+ else if (indent > 6 && routePath.startsWith("/") && routePath !== "/") {
61
+ routePath = routePath.slice(1);
62
+ }
63
+ if (routePath === "") {
64
+ return `${pad}<Route component={Route${node.routeIdx}Wrapped} />`;
65
+ }
66
+ return `${pad}<Route path="${routePath}" component={Route${node.routeIdx}Wrapped} />`;
67
+ }
68
+ let layoutPath = node.relativePath;
69
+ const inner = renderTree(node.children, indent + 2);
70
+ return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join("\n");
71
+ })
72
+ .join("\n");
73
+ }
74
+ function buildPreloadMapString(routes, allLayouts) {
75
+ const mapLines = routes.map((r, i) => {
76
+ const layoutTokens = r.layouts.map((l) => `Layout${allLayouts.get(l.filePath)}`);
77
+ const pageToken = `Route${i}`;
78
+ const tokensArrayString = `[${[...layoutTokens, pageToken].join(", ")}]`;
79
+ return ` "${r.urlPattern}": ${tokensArrayString}`;
80
+ });
81
+ return `const chunkPreloadRegistry = {\n${mapLines.join(",\n")}\n};`;
82
+ }
83
+ export function generateRouterEntry(appRoot, routes) {
84
+ const routesDir = path.resolve(appRoot, "./src/routes");
85
+ const outDir = path.resolve(appRoot, "./.anaemia");
86
+ const outPath = path.resolve(outDir, "./__anaemia_entry__.tsx");
87
+ const conventionalRoutes = routes.filter((r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx"));
88
+ const errorRoutes = routes.filter((r) => r.filePath.endsWith("404.tsx") || r.filePath.endsWith("500.tsx"));
89
+ const allLayouts = new Map();
90
+ let layoutIndex = 0;
91
+ for (const entry of conventionalRoutes) {
92
+ for (const layout of entry.layouts) {
93
+ if (!allLayouts.has(layout.filePath)) {
94
+ allLayouts.set(layout.filePath, layoutIndex++);
95
+ }
96
+ }
97
+ }
98
+ const routeImports = conventionalRoutes
99
+ .map((r, i) => {
100
+ const relativeToRoutes = path.relative(routesDir, r.filePath);
101
+ const chunkName = relativeToRoutes
102
+ .replace(/\.[jt]sx?$/, "")
103
+ .replace(/[^a-zA-Z0-9-_\[\]]/g, "-")
104
+ .toLowerCase();
105
+ const guardSources = [...r.layouts.map((l) => l.filePath), r.filePath]
106
+ .map((fp) => {
107
+ const configPath = fp.replace(/\.(tsx|jsx)$/, ".config.ts");
108
+ const guardPath = fs.existsSync(configPath) ? configPath : fp.replace(/\.(jsx)$/, ".config.js");
109
+ const resolvedGuardPath = fs.existsSync(guardPath) ? guardPath : fp;
110
+ return `() => import("${resolvedGuardPath.replace(/\\/g, "/")}").then(m => m?.config?.guards ?? [])`;
111
+ })
112
+ .join(",\n ");
113
+ return `
114
+ const Route${i} = lazy(() => import(/* webpackChunkName: "${chunkName}" */ "${r.filePath.replace(/\\/g, "/")}"));
115
+ const Route${i}Loader = async (args) => {
116
+ const _guardSources = [
117
+ ${guardSources}
118
+ ];
119
+ for (const loadGuards of _guardSources) {
120
+ const guards = await loadGuards();
121
+ for (const guard of guards) {
122
+ const result = await guard({ params: args.params, request: args.request, url: args.location?.pathname ?? "" });
123
+ if (result?.redirect) {
124
+ throw Object.assign(new Error("guard:redirect"), { redirect: result.redirect, status: result.status ?? 302 });
125
+ }
126
+ if (result?.status) {
127
+ throw Object.assign(new Error("guard:error"), { status: result.status });
128
+ }
129
+ }
130
+ }
131
+ return import(/* webpackChunkName: "${chunkName}" */ "${r.filePath.replace(/\\/g, "/")}").then(mod => {
132
+ return mod.loader ? mod.loader(args) : null;
133
+ });
134
+ };
135
+ const Route${i}Wrapped = (props) => (
136
+ <RouteDataController loader={Route${i}Loader}>
137
+ <Route${i} {...props} />
138
+ </RouteDataController>
139
+ );`.trim();
140
+ })
141
+ .join("\n");
142
+ const layoutImports = [...allLayouts.entries()]
143
+ .map(([file, i]) => {
144
+ const relativeToRoutes = path.relative(routesDir, file);
145
+ const chunkName = ("layout-" + relativeToRoutes.replace(/\.[jt]sx?$/, "").replace(/[^a-zA-Z0-9-_\[\]]/g, "-")).toLowerCase();
146
+ return `import Layout${i} from "${file.replace(/\\/g, "/")}";`;
147
+ })
148
+ .join("\n");
149
+ const tree = buildTree(conventionalRoutes, conventionalRoutes.map((r) => r.layouts.map((l) => l.filePath)), conventionalRoutes.map((_, i) => i), routesDir, allLayouts, "/");
150
+ let routeJsx = renderTree(tree, 6);
151
+ const has404 = errorRoutes.some((r) => r.filePath.endsWith("404.tsx"));
152
+ if (has404) {
153
+ const idx = routes.findIndex((r) => r.filePath.endsWith("404.tsx"));
154
+ routeJsx += `\n <Route path="*any" component={Route${idx}} />`;
155
+ }
156
+ const rootWrapperPath = path.resolve(appRoot, "./src/root.tsx");
157
+ const hasRootWrapper = fs.existsSync(rootWrapperPath);
158
+ const rootImport = hasRootWrapper ? `import RootWrapper from "../src/root.tsx";` : ``;
159
+ const rootWrapperCode = hasRootWrapper
160
+ ? `const RootWrapperComponent = (props) => (
161
+ <RootWrapper {...props} />
162
+ );`
163
+ : ``;
164
+ const finalJsx = hasRootWrapper
165
+ ? ` <Route component={RootWrapperComponent}>
166
+ ${routeJsx}
167
+ </Route>`
168
+ : routeJsx;
169
+ const registryEntries = routes
170
+ .map((r) => {
171
+ return ` ["${r.urlPattern}", async (args) => {
172
+ const mod = await import("${r.filePath.replace(/\\/g, "/")}");
173
+ return mod.loader ? mod.loader(args) : null;
174
+ }]`;
175
+ })
176
+ .join(",\n");
177
+ const guardRegistryEntries = conventionalRoutes
178
+ .map((r) => {
179
+ const sources = [...r.layouts.map((l) => l.filePath), r.filePath];
180
+ const loaders = sources
181
+ .map((fp) => {
182
+ const configPath = fp.replace(/\.(tsx|jsx)$/, ".config.ts");
183
+ const guardPath = fs.existsSync(configPath) ? configPath : fp.replace(/\.jsx$/, ".config.js");
184
+ const resolvedGuardPath = fs.existsSync(guardPath) ? guardPath : fp;
185
+ return `async () => { const m = await import("${resolvedGuardPath.replace(/\\/g, "/")}"); return m?.config?.guards ?? []; }`;
186
+ })
187
+ .join(",\n ");
188
+ return ` ["${r.urlPattern}", [\n ${loaders}\n ]]`;
189
+ })
190
+ .join(",\n");
191
+ const chunkPreloadRegistryCode = buildPreloadMapString(conventionalRoutes, allLayouts);
192
+ const preloadFnCode = `
193
+ export async function preloadActiveClientRoute(pathname: string) {
194
+ // Simple match logic against parameters or route patterns
195
+ const pattern = Object.keys(chunkPreloadRegistry).find(p => {
196
+ if (p === pathname) return true;
197
+ const regexStr = p.replace(/:([a-zA-Z0-9_-]+)/g, "([^/]+)").replace(/\\*([a-zA-Z0-9_-]*)/g, "(.*)");
198
+ return new RegExp("^" + regexStr + "$").test(pathname);
199
+ });
200
+
201
+ const componentsToPreload = pattern ? chunkPreloadRegistry[pattern] : [];
202
+ const preloads = componentsToPreload
203
+ .filter(c => typeof c.preload === "function")
204
+ .map(c => c.preload());
205
+
206
+ await Promise.all(preloads);
207
+ }
208
+ `.trim();
209
+ const code = `
210
+ // @ts-nocheck
211
+ // auto-generated by anaemia - do not edit!!
212
+ import { lazy } from "solid-js";
213
+ import { Route } from "@solidjs/router";
214
+ import { RouteDataController } from "@anaemia/core";
215
+
216
+ ${rootImport}
217
+
218
+ ${rootWrapperCode}
219
+
220
+ ${routeImports}
221
+
222
+ ${layoutImports}
223
+
224
+ ${chunkPreloadRegistryCode}
225
+
226
+ ${preloadFnCode}
227
+
228
+ export const serverLoaderRegistry = new Map([\n${registryEntries}\n]);
229
+
230
+ export const serverGuardRegistry = new Map([\n${guardRegistryEntries}\n]);
231
+
232
+ export default function AnaemiaRoutes() {
233
+ return (
234
+ ${finalJsx}
235
+ );
236
+ }
237
+ `.trimStart();
238
+ if (!fs.existsSync(outDir)) {
239
+ fs.mkdirSync(outDir, { recursive: true });
240
+ }
241
+ fs.writeFileSync(outPath, code);
242
+ return outPath;
243
+ }
@@ -0,0 +1,3 @@
1
+ import type { ServerRouteEntry } from "./scan.js";
2
+ export declare function generateServerRoutes(appRoot: string, routes: ServerRouteEntry[]): string;
3
+ //# sourceMappingURL=generate-server-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-server-routes.d.ts","sourceRoot":"","sources":["../../src/router/generate-server-routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,CA+BxF"}
@@ -0,0 +1,30 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export function generateServerRoutes(appRoot, routes) {
4
+ const outDir = path.resolve(appRoot, "./.anaemia");
5
+ const outPath = path.resolve(outDir, "./__anaemia_server_routes__.ts");
6
+ const imports = routes.map((r, i) => `import * as ServerRoute${i} from "${r.filePath}";`).join("\n");
7
+ const registrations = routes.map((r, i) => ` registerRoute(app, "${r.urlPattern}", ServerRoute${i});`).join("\n");
8
+ const code = `
9
+ // @ts-nocheck
10
+ // auto-generated by anaemia - do not edit!!
11
+ import type { Hono } from "hono";
12
+
13
+ ${imports}
14
+
15
+ function registerRoute(app: any, pattern: string, mod: any) {
16
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
17
+ for (const m of methods) {
18
+ if (m in mod && typeof mod[m] === "function") {
19
+ app[m.toLowerCase()](pattern, mod[m]);
20
+ }
21
+ }
22
+ }
23
+
24
+ export function registerServerRoutes(app: Hono) {
25
+ ${registrations}
26
+ }
27
+ `.trimStart();
28
+ fs.writeFileSync(outPath, code);
29
+ return outPath;
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { RouteManifestEntry } from "./scan.js";
2
+ export interface BuildManifest {
3
+ routes: RouteManifestEntry[];
4
+ chunks: Record<string, {
5
+ js: string;
6
+ css?: string;
7
+ }>;
8
+ errors: Record<string, string>;
9
+ buildTime: string;
10
+ }
11
+ export declare function writeManifest(appRoot: string, routes: RouteManifestEntry[]): void;
12
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/router/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAE7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,CA6BjF"}
@@ -0,0 +1,23 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export function writeManifest(appRoot, routes) {
4
+ const errors = {};
5
+ for (const route of routes) {
6
+ if (route.filePath.endsWith("404.tsx")) {
7
+ errors["404"] = route.urlPattern;
8
+ }
9
+ if (route.filePath.endsWith("500.tsx")) {
10
+ errors["500"] = route.urlPattern;
11
+ }
12
+ }
13
+ const conventionalRoutes = routes.filter((r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx"));
14
+ const manifest = {
15
+ routes: conventionalRoutes,
16
+ errors,
17
+ chunks: {}, // rspack fills this in via ManifestPlugin
18
+ buildTime: new Date().toISOString(),
19
+ };
20
+ const outDir = path.resolve(appRoot, "./dist");
21
+ fs.mkdirSync(outDir, { recursive: true });
22
+ fs.writeFileSync(path.resolve(outDir, "route-manifest.json"), JSON.stringify(manifest, null, 2));
23
+ }
@@ -0,0 +1,22 @@
1
+ export declare function createScanJiti(appRoot: string): import("jiti").Jiti;
2
+ export type RouteType = "page" | "layout" | "catch-all";
3
+ export interface LayoutManifestEntry {
4
+ filePath: string;
5
+ guards: any[];
6
+ }
7
+ export interface RouteManifestEntry {
8
+ urlPattern: string;
9
+ filePath: string;
10
+ chunkName: string;
11
+ layouts: LayoutManifestEntry[];
12
+ guards: any[];
13
+ type: RouteType;
14
+ params: string[];
15
+ }
16
+ export interface ServerRouteEntry {
17
+ urlPattern: string;
18
+ filePath: string;
19
+ }
20
+ export declare function scanServerRoutes(appRoot: string): ServerRouteEntry[];
21
+ export declare function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]>;
22
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/router/scan.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,uBAK7C;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAGjC,UAAU,EAAE,MAAM,CAAC;IAGnB,QAAQ,EAAE,MAAM,CAAC;IAGjB,SAAS,EAAE,MAAM,CAAC;IAIlB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAG/B,MAAM,EAAE,GAAG,EAAE,CAAC;IAEd,IAAI,EAAE,SAAS,CAAC;IAIhB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAYpE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAgF/E"}
@@ -0,0 +1,164 @@
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
+ export function createScanJiti(appRoot) {
7
+ return createJiti(import.meta.url, {
8
+ interopDefault: true,
9
+ alias: getAliases(appRoot),
10
+ });
11
+ }
12
+ const LAYOUT_FILE = /^_layout\.(tsx|jsx)$/;
13
+ const CATCH_ALL_FILE = /^\[\.\.\.(.+?)\]\.(tsx|jsx)$/;
14
+ const DYNAMIC_SEGMENT = /^\[(.+?)\]\.(tsx|jsx)$/;
15
+ export function scanServerRoutes(appRoot) {
16
+ const routesDir = path.resolve(appRoot, "./src/routes");
17
+ const files = glob.sync("**/_route.{ts,tsx}", { cwd: routesDir, posix: true });
18
+ return files.map((file) => {
19
+ const dir = path.dirname(file);
20
+ const urlPattern = dir === "." ? "/" : `/${dir}`;
21
+ return {
22
+ urlPattern,
23
+ filePath: path.resolve(routesDir, file),
24
+ };
25
+ });
26
+ }
27
+ export async function scanRoutes(appRoot) {
28
+ const jiti = createScanJiti(appRoot);
29
+ const routesDir = path.resolve(appRoot, "./src/routes");
30
+ const files = glob.sync("**/*.{tsx,jsx}", { cwd: routesDir, posix: true });
31
+ const layoutMap = new Map();
32
+ for (const file of files) {
33
+ const filename = path.basename(file);
34
+ if (LAYOUT_FILE.test(filename)) {
35
+ const dir = path.dirname(file);
36
+ const absolutePath = path.resolve(routesDir, file);
37
+ let layoutGuards = [];
38
+ const configPath = absolutePath
39
+ .replace(/\.(tsx|jsx)$/, ".config.$1")
40
+ .replace(".config.tsx", ".config.ts")
41
+ .replace(".config.jsx", ".config.js");
42
+ const moduleToScan = fs.existsSync(configPath) ? configPath : absolutePath;
43
+ try {
44
+ const layoutModule = jiti.import(moduleToScan);
45
+ if (layoutModule?.config?.guards) {
46
+ layoutGuards = layoutModule.config.guards;
47
+ }
48
+ }
49
+ catch (err) {
50
+ console.warn(`[anaemia bundler warning]: Failed parsing config flags on layout: ${file}`);
51
+ }
52
+ layoutMap.set(dir, {
53
+ filePath: absolutePath,
54
+ guards: layoutGuards,
55
+ });
56
+ }
57
+ }
58
+ const entries = [];
59
+ for (const file of files) {
60
+ const filename = path.basename(file);
61
+ const dir = path.dirname(file);
62
+ if (LAYOUT_FILE.test(filename))
63
+ continue;
64
+ if (filename.includes(".config."))
65
+ continue;
66
+ const absolutePagePath = path.resolve(routesDir, file);
67
+ const { urlPattern, chunkName, params, type } = parseFilePath(file);
68
+ let pageGuards = [];
69
+ const pageConfigPath = absolutePagePath
70
+ .replace(/\.(tsx|jsx)$/, ".config.$1")
71
+ .replace(".config.tsx", ".config.ts")
72
+ .replace(".config.jsx", ".config.js");
73
+ const pageModuleToScan = fs.existsSync(pageConfigPath) ? pageConfigPath : absolutePagePath;
74
+ try {
75
+ const pageModule = (await jiti.import(pageModuleToScan));
76
+ if (pageModule?.config?.guards) {
77
+ pageGuards = pageModule.config.guards;
78
+ }
79
+ }
80
+ catch (err) {
81
+ // quietly ignore parsing problems for pure components lacking a config wrapper
82
+ }
83
+ const layouts = resolveLayoutChain(dir, layoutMap);
84
+ entries.push({
85
+ urlPattern,
86
+ filePath: absolutePagePath,
87
+ chunkName,
88
+ layouts,
89
+ guards: pageGuards,
90
+ type,
91
+ params,
92
+ });
93
+ }
94
+ return entries;
95
+ }
96
+ function parseFilePath(file) {
97
+ const segments = file.split("/");
98
+ const urlParts = [];
99
+ const params = [];
100
+ let type = "page";
101
+ for (let i = 0; i < segments.length; i++) {
102
+ const segment = segments[i];
103
+ const isLast = i === segments.length - 1;
104
+ if (isLast) {
105
+ const cleanName = segment.replace(/\.(tsx|jsx)$/, "");
106
+ if (CATCH_ALL_FILE.test(segment)) {
107
+ const match = segment.match(CATCH_ALL_FILE);
108
+ params.push(match[1]);
109
+ urlParts.push(`*`);
110
+ type = "catch-all";
111
+ }
112
+ else if (DYNAMIC_SEGMENT.test(segment)) {
113
+ const match = segment.match(DYNAMIC_SEGMENT);
114
+ params.push(match[1]);
115
+ urlParts.push(`:${match[1]}`);
116
+ }
117
+ else if (cleanName !== "index") {
118
+ urlParts.push(cleanName);
119
+ }
120
+ }
121
+ else {
122
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
123
+ const param = segment.slice(4, -1);
124
+ params.push(param);
125
+ urlParts.push(`*`);
126
+ type = "catch-all";
127
+ }
128
+ else if (segment.startsWith("[") && segment.endsWith("]")) {
129
+ const param = segment.slice(1, -1);
130
+ params.push(param);
131
+ urlParts.push(`:${param}`);
132
+ }
133
+ else {
134
+ urlParts.push(segment);
135
+ }
136
+ }
137
+ }
138
+ const filteredParts = urlParts.filter((part) => part !== "" && part !== ".");
139
+ let urlPattern = "/" + filteredParts.join("/");
140
+ if (urlPattern === "") {
141
+ urlPattern = "/";
142
+ }
143
+ const chunkName = file
144
+ .replace(/\.(tsx|jsx)$/, "")
145
+ .replace(/\[\.\.\.(.+?)\]/g, "catchall-$1")
146
+ .replace(/\[(.+?)\]/g, "param-$1")
147
+ .replace(/\//g, "-")
148
+ .replace(/^-+|-+$/g, "")
149
+ .toLowerCase();
150
+ return { urlPattern, chunkName, params, type };
151
+ }
152
+ function resolveLayoutChain(dir, layoutMap) {
153
+ const layouts = [];
154
+ let current = dir;
155
+ while (true) {
156
+ const layoutEntry = layoutMap.get(current);
157
+ if (layoutEntry)
158
+ layouts.unshift(layoutEntry);
159
+ if (current === "." || current === "")
160
+ break;
161
+ current = path.dirname(current);
162
+ }
163
+ return layouts;
164
+ }
@@ -0,0 +1,47 @@
1
+ import type { AnaemiaConfig } from "@anaemia/core";
2
+ export declare function createStyleRules(config: AnaemiaConfig): {
3
+ client: {
4
+ test: RegExp;
5
+ type: string;
6
+ use: {
7
+ loader: string;
8
+ options: {
9
+ api: string;
10
+ };
11
+ }[];
12
+ };
13
+ server: {
14
+ test: RegExp;
15
+ type: string;
16
+ generator: {
17
+ css: {
18
+ exportOnlyLocals: boolean;
19
+ };
20
+ };
21
+ use: {
22
+ loader: string;
23
+ options: {
24
+ api: string;
25
+ };
26
+ }[];
27
+ };
28
+ };
29
+ export declare function createBabelRule({ isServer, isDev, plugins, }: {
30
+ isServer: boolean;
31
+ isDev: boolean;
32
+ plugins: any[];
33
+ }): {
34
+ test: RegExp;
35
+ use: {
36
+ loader: string;
37
+ options: {
38
+ presets: (string | (string | {
39
+ generate: string;
40
+ hydratable: boolean;
41
+ dev: boolean;
42
+ })[])[];
43
+ plugins: any[];
44
+ };
45
+ }[];
46
+ };
47
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;EAuBrD;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,KAAK,EACL,OAAY,GACb,EAAE;IACD,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,GAAG,EAAE,CAAC;CAChB;;;;;;;;;;;;;EAkBA"}