@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.
- package/LICENSE +201 -0
- package/README.md +3 -0
- package/dist/aliases.d.ts +8 -0
- package/dist/aliases.d.ts.map +1 -0
- package/dist/aliases.js +10 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/optimization.d.ts +41 -0
- package/dist/optimization.d.ts.map +1 -0
- package/dist/optimization.js +35 -0
- package/dist/plugins/babel-hash-injector-server.d.ts +7 -0
- package/dist/plugins/babel-hash-injector-server.d.ts.map +1 -0
- package/dist/plugins/babel-hash-injector-server.js +17 -0
- package/dist/plugins/babel-transform-client.d.ts +6 -0
- package/dist/plugins/babel-transform-client.d.ts.map +1 -0
- package/dist/plugins/babel-transform-client.js +49 -0
- package/dist/plugins/babel-transform-server.d.ts +11 -0
- package/dist/plugins/babel-transform-server.d.ts.map +1 -0
- package/dist/plugins/babel-transform-server.js +60 -0
- package/dist/plugins/rspack-manifest-hydration.d.ts +11 -0
- package/dist/plugins/rspack-manifest-hydration.d.ts.map +1 -0
- package/dist/plugins/rspack-manifest-hydration.js +38 -0
- package/dist/plugins/server-function-id.d.ts +2 -0
- package/dist/plugins/server-function-id.d.ts.map +1 -0
- package/dist/plugins/server-function-id.js +3 -0
- package/dist/router/generate-entry.d.ts +3 -0
- package/dist/router/generate-entry.d.ts.map +1 -0
- package/dist/router/generate-entry.js +243 -0
- package/dist/router/generate-server-routes.d.ts +3 -0
- package/dist/router/generate-server-routes.d.ts.map +1 -0
- package/dist/router/generate-server-routes.js +30 -0
- package/dist/router/manifest.d.ts +12 -0
- package/dist/router/manifest.d.ts.map +1 -0
- package/dist/router/manifest.js +23 -0
- package/dist/router/scan.d.ts +22 -0
- package/dist/router/scan.d.ts.map +1 -0
- package/dist/router/scan.js +164 -0
- package/dist/rules.d.ts +47 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +42 -0
- package/dist/server-function-id.d.ts +2 -0
- package/dist/server-function-id.d.ts.map +1 -0
- package/dist/server-function-id.js +3 -0
- package/package.json +36 -0
- package/src/aliases.ts +11 -0
- package/src/index.ts +170 -0
- package/src/optimization.ts +37 -0
- package/src/plugins/babel-hash-injector-server.ts +19 -0
- package/src/plugins/babel-transform-server.ts +144 -0
- package/src/plugins/rspack-manifest-hydration.ts +56 -0
- package/src/router/generate-entry.ts +306 -0
- package/src/router/generate-server-routes.ts +36 -0
- package/src/router/manifest.ts +42 -0
- package/src/router/scan.ts +228 -0
- package/src/rules.ts +58 -0
- package/src/runtime/empty-module.cjs +10 -0
- package/src/server-function-id.ts +3 -0
- package/test/dev-config.test.mjs +30 -0
- package/test/server-functions.test.mjs +77 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -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"}
|