@anaemia/bundler 0.3.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/aliases.js +1 -1
  4. package/dist/analyzer/ast-utils.d.ts +5 -0
  5. package/dist/analyzer/ast-utils.d.ts.map +1 -0
  6. package/dist/analyzer/ast-utils.js +16 -0
  7. package/dist/analyzer/ast-walker.d.ts +15 -0
  8. package/dist/analyzer/ast-walker.d.ts.map +1 -0
  9. package/dist/analyzer/ast-walker.js +43 -0
  10. package/dist/analyzer/checks/env-access.d.ts +3 -0
  11. package/dist/analyzer/checks/env-access.d.ts.map +1 -0
  12. package/dist/analyzer/checks/env-access.js +63 -0
  13. package/dist/analyzer/checks/route-metadata.d.ts +12 -0
  14. package/dist/analyzer/checks/route-metadata.d.ts.map +1 -0
  15. package/dist/analyzer/checks/route-metadata.js +74 -0
  16. package/dist/analyzer/checks/server-functions.d.ts +3 -0
  17. package/dist/analyzer/checks/server-functions.d.ts.map +1 -0
  18. package/dist/analyzer/checks/server-functions.js +75 -0
  19. package/dist/analyzer/index.d.ts +7 -0
  20. package/dist/analyzer/index.d.ts.map +1 -0
  21. package/dist/analyzer/index.js +49 -0
  22. package/dist/analyzer/parser.d.ts +4 -0
  23. package/dist/analyzer/parser.d.ts.map +1 -0
  24. package/dist/analyzer/parser.js +96 -0
  25. package/dist/analyzer/types.d.ts +48 -0
  26. package/dist/analyzer/types.d.ts.map +1 -0
  27. package/dist/analyzer/types.js +1 -0
  28. package/dist/env-loader.d.ts +2 -0
  29. package/dist/env-loader.d.ts.map +1 -0
  30. package/dist/env-loader.js +10 -0
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +89 -15
  34. package/dist/optimization.d.ts.map +1 -1
  35. package/dist/optimization.js +17 -4
  36. package/dist/plugins/babel-transform-server.d.ts.map +1 -1
  37. package/dist/plugins/babel-transform-server.js +1 -4
  38. package/dist/router/generate-entry.d.ts.map +1 -1
  39. package/dist/router/generate-entry.js +3 -3
  40. package/dist/router/generate-server-routes.d.ts.map +1 -1
  41. package/dist/router/generate-server-routes.js +15 -5
  42. package/dist/router/manifest.d.ts +3 -2
  43. package/dist/router/manifest.d.ts.map +1 -1
  44. package/dist/router/manifest.js +20 -20
  45. package/dist/router/scan.d.ts.map +1 -1
  46. package/dist/router/scan.js +5 -6
  47. package/dist/rules.d.ts +16 -1
  48. package/dist/rules.d.ts.map +1 -1
  49. package/dist/rules.js +37 -5
  50. package/package.json +11 -3
  51. package/src/aliases.ts +2 -2
  52. package/src/analyzer/ast-utils.ts +22 -0
  53. package/src/analyzer/ast-walker.ts +63 -0
  54. package/src/analyzer/checks/env-access.ts +77 -0
  55. package/src/analyzer/checks/route-metadata.ts +91 -0
  56. package/src/analyzer/checks/server-functions.ts +85 -0
  57. package/src/analyzer/index.ts +70 -0
  58. package/src/analyzer/parser.ts +103 -0
  59. package/src/analyzer/types.ts +55 -0
  60. package/src/env-loader.ts +13 -0
  61. package/src/index.ts +119 -19
  62. package/src/optimization.ts +18 -5
  63. package/src/plugins/babel-transform-server.ts +1 -4
  64. package/src/plugins/rspack-manifest-hydration.ts +3 -3
  65. package/src/router/generate-entry.ts +15 -6
  66. package/src/router/generate-server-routes.ts +16 -5
  67. package/src/router/manifest.ts +24 -38
  68. package/src/router/scan.ts +9 -10
  69. package/src/rules.ts +48 -8
  70. package/test/analyzer.test.mjs +308 -0
  71. package/test/rspack-config.test.mjs +5 -2
  72. package/test/server-functions.test.mjs +25 -22
  73. package/tsconfig.json +1 -1
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { Configuration, rspack } from "@rspack/core";
2
- import path from "path";
1
+ import type { Configuration } from "@rspack/core";
2
+ import { rspack } from "@rspack/core";
3
+ import path from "node:path";
3
4
  import fs from "node:fs";
4
5
  import type { AnaemiaConfig } from "@anaemia/core/config";
5
6
  import { createRequire } from "node:module";
6
7
  import { fileURLToPath } from "node:url";
8
+ import pc from "picocolors";
7
9
 
8
10
  import clientServerFnTransform from "./plugins/babel-transform-server.js";
9
11
  import serverHashInjector from "./plugins/babel-hash-injector-server.js";
@@ -15,43 +17,94 @@ import { generateRouterEntry } from "./router/generate-entry.js";
15
17
  import { generateServerRoutes } from "./router/generate-server-routes.js";
16
18
  import { getAliases } from "./aliases.js";
17
19
 
18
- import { createStyleRules, createBabelRule } from "./rules.js";
20
+ import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
19
21
  import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
22
+ import loadEnvFiles from "./env-loader.js";
23
+
24
+ import { analyzeApp } from "./analyzer/index.js";
20
25
 
21
26
  const require = createRequire(import.meta.url);
22
27
  const __filename = fileURLToPath(import.meta.url);
23
28
  const __dirname = path.dirname(__filename);
24
29
 
25
- export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {}): Promise<[Configuration, Configuration]> {
30
+ export async function getRspackConfig(
31
+ appRoot: string,
32
+ config: AnaemiaConfig = {},
33
+ ): Promise<[Configuration, Configuration]> {
26
34
  const isDev = process.env.NODE_ENV !== "production";
27
- const rawEnv = process.env;
35
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
36
+
37
+ // run the analyzer to collect route metadata and other information about the app that we can use to optimize the build
38
+ const analysis = await analyzeApp(appRoot, {
39
+ mode: isDev ? "development" : "production",
40
+ });
41
+
42
+ // flush diagnostics to console
43
+ const tag = pc.dim("[anaemia-analyzer]");
44
+
45
+ for (const diagnostic of analysis.diagnostics) {
46
+ const prefix =
47
+ diagnostic.severity === "error"
48
+ ? pc.red("✖ [error]")
49
+ : diagnostic.severity === "warning"
50
+ ? pc.yellow("⚠ [warning]")
51
+ : pc.cyan("› [info]");
52
+
53
+ const loc = diagnostic.line ? pc.dim(`:${diagnostic.line}`) : "";
54
+ const file = pc.bold(diagnostic.filePath);
55
+ const msg =
56
+ diagnostic.severity === "error"
57
+ ? pc.red(diagnostic.message)
58
+ : diagnostic.severity === "warning"
59
+ ? pc.yellow(diagnostic.message)
60
+ : diagnostic.message;
61
+
62
+ // eslint-disable-next-line no-console
63
+ console.log(`${tag} ${prefix} ${file}${loc} - ${msg}`);
64
+ // eslint-disable-next-line no-console
65
+ if (diagnostic.help) console.log(` ${pc.dim(`hint: ${diagnostic.help}`)}`);
66
+ }
67
+
28
68
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
29
69
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
30
70
 
31
71
  const routes = await scanRoutes(appRoot);
32
72
  const serverRoutes = scanServerRoutes(appRoot);
33
- writeManifest(appRoot, routes);
73
+ writeManifest(appRoot, routes, analysis.routeMetadata);
34
74
 
35
75
  const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
36
76
  if (!fs.existsSync(frameworkInternalDir)) {
37
77
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
38
78
  }
39
79
 
80
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
40
81
  const entryFile = generateRouterEntry(appRoot, routes);
41
82
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
83
+
42
84
  const styleRules = createStyleRules(config);
85
+ const assetRules = createAssetRules(isDev);
86
+
87
+ // allow users to inject additional babel plugins via the config, which is useful for things like macros or other code transforms that need to run at compile time
43
88
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
44
89
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
45
90
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
46
91
 
47
- // env processing
92
+ /**
93
+ * env processing:
94
+ *
95
+ * - we inject some default env vars like MODE, DEV, and PROD for convenience
96
+ * - for the server, we expose all env vars
97
+ * - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
98
+ * - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
99
+ */
48
100
  const serverEnv: Record<string, string> = {
49
101
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
50
102
  DEV: JSON.stringify(isDev),
51
103
  PROD: JSON.stringify(!isDev),
52
104
  };
53
- for (const key in rawEnv) {
54
- serverEnv[key] = JSON.stringify(rawEnv[key]);
105
+
106
+ for (const key in process.env) {
107
+ serverEnv[key] = JSON.stringify(process.env[key]);
55
108
  }
56
109
 
57
110
  const clientEnv: Record<string, string> = {
@@ -59,18 +112,22 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
59
112
  DEV: JSON.stringify(isDev),
60
113
  PROD: JSON.stringify(!isDev),
61
114
  };
62
- for (const key in rawEnv) {
115
+
116
+ for (const key in process.env) {
63
117
  if (key.startsWith("PUBLIC_")) {
64
- clientEnv[key] = JSON.stringify(rawEnv[key]);
118
+ clientEnv[key] = JSON.stringify(process.env[key]);
65
119
  }
66
120
  }
67
121
 
122
+ // shared resolve config
123
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
68
124
  const sharedResolve = {
69
125
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
70
126
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
71
127
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
72
128
  };
73
129
 
130
+ // client and server configurations
74
131
  let clientConfig: Configuration = {
75
132
  name: "client",
76
133
  context: appRoot,
@@ -95,9 +152,19 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
95
152
  alias: {
96
153
  ...sharedResolve.alias,
97
154
  "solid-refresh": require.resolve("solid-refresh"),
98
- [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
155
+ [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(
156
+ coreRuntimeDir,
157
+ "./dist/runtime/context.browser.js",
158
+ ),
159
+ },
160
+ fallback: {
161
+ async_hooks: false,
162
+ "node:async_hooks": false,
163
+ fs: false,
164
+ "node:fs": false,
165
+ path: false,
166
+ "node:path": false,
99
167
  },
100
- fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
101
168
  },
102
169
  devServer: isDev
103
170
  ? {
@@ -110,9 +177,17 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
110
177
  }
111
178
  : undefined,
112
179
  plugins: [
113
- new rspack.HtmlRspackPlugin({ template: path.resolve(appRoot, "./index.html"), filename: "index.html", inject: false }),
180
+ new rspack.HtmlRspackPlugin({
181
+ template: path.resolve(appRoot, "./index.html"),
182
+ filename: "index.html",
183
+ inject: false,
184
+ }),
114
185
  new rspack.DefinePlugin({
115
- __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({ port: config.port, assets: config.assets, styles: config.styles }),
186
+ __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
187
+ port: config.port,
188
+ assets: config.assets,
189
+ styles: config.styles,
190
+ }),
116
191
  ...config.define?.client,
117
192
  "import.meta.env": clientEnv,
118
193
  }),
@@ -126,7 +201,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
126
201
  if (fs.existsSync(srcPath)) return srcPath;
127
202
 
128
203
  return path.resolve(__dirname, "../src/runtime/empty-module.cjs");
129
- })()
204
+ })(),
130
205
  ),
131
206
  new AnaemiaManifestHydrationPlugin({ appRoot }),
132
207
  ],
@@ -134,8 +209,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
134
209
  parser: { "css/auto": { namedExports: false } },
135
210
  rules: [
136
211
  styleRules.client,
212
+ ...assetRules.client,
137
213
  {
138
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
214
+ ...createBabelRule({
215
+ isServer: false,
216
+ isDev,
217
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
218
+ }),
139
219
  exclude: (modulePath: string) => {
140
220
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
141
221
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -152,7 +232,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
152
232
  context: appRoot,
153
233
  target: "node",
154
234
  entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
155
- output: { path: path.resolve(appRoot, "./dist/server"), filename: "index.js", module: true, chunkFormat: "module", chunkLoading: "import" },
235
+ output: {
236
+ path: path.resolve(appRoot, "./dist/server"),
237
+ filename: "index.js",
238
+ module: true,
239
+ chunkFormat: "module",
240
+ chunkLoading: "import",
241
+ },
156
242
  optimization: { nodeEnv: false },
157
243
  resolve: {
158
244
  ...sharedResolve,
@@ -171,8 +257,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
171
257
  parser: { "css/auto": { namedExports: false } },
172
258
  rules: [
173
259
  styleRules.server,
260
+ ...assetRules.server,
174
261
  {
175
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
262
+ ...createBabelRule({
263
+ isServer: true,
264
+ isDev,
265
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
266
+ }),
176
267
  exclude: (modulePath: string) => {
177
268
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
178
269
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -183,6 +274,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
183
274
  },
184
275
  };
185
276
 
277
+ // allow plugins to modify the client and server configurations before they are returned
186
278
  for (const plugin of config.plugins ?? []) {
187
279
  if (plugin.clientRspackConfig) clientConfig = plugin.clientRspackConfig(clientConfig);
188
280
  if (plugin.serverRspackConfig) serverConfig = plugin.serverRspackConfig(serverConfig);
@@ -193,3 +285,11 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
193
285
 
194
286
  export { scanRoutes } from "./router/scan.js";
195
287
  export { writeManifest } from "./router/manifest.js";
288
+ export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
289
+ export type {
290
+ AnalyzeAppOptions,
291
+ AnalyzerDiagnostic,
292
+ AnalyzerFileKind,
293
+ AnalyzerResult,
294
+ ParsedAnalyzerFile,
295
+ } from "./analyzer/index.js";
@@ -2,8 +2,8 @@ import { rspack } from "@rspack/core";
2
2
 
3
3
  export function getClientOptimization(isDev: boolean) {
4
4
  return {
5
- sideEffects: true,
6
- usedExports: true,
5
+ sideEffects: !isDev,
6
+ usedExports: !isDev,
7
7
  splitChunks: isDev
8
8
  ? (false as const)
9
9
  : ({
@@ -14,7 +14,7 @@ export function getClientOptimization(isDev: boolean) {
14
14
  framework: {
15
15
  chunks: "all" as const,
16
16
  name: "framework",
17
- test: /[\\/]node_modules[\\/](solid-js|@solidjs[\\/]router)[\\/]/,
17
+ test: /[\\/]node_modules[\\/](solid-js|@solidjs)[\\/]/,
18
18
  priority: 40,
19
19
  enforce: true,
20
20
  },
@@ -26,7 +26,20 @@ export function getClientOptimization(isDev: boolean) {
26
26
  },
27
27
  },
28
28
  } as const),
29
- minimizer: isDev ? [] : [new rspack.SwcJsMinimizerRspackPlugin()],
29
+ minimizer: isDev
30
+ ? []
31
+ : [
32
+ new rspack.SwcJsMinimizerRspackPlugin({
33
+ minimizerOptions: {
34
+ mangle: {
35
+ keep_fnames: true,
36
+ },
37
+ compress: {
38
+ keep_fnames: true,
39
+ },
40
+ },
41
+ }),
42
+ ],
30
43
  };
31
44
  }
32
45
 
@@ -34,4 +47,4 @@ export function getPerformanceProfile(isDev: boolean) {
34
47
  return isDev
35
48
  ? { hints: false as const, maxAssetSize: 1000000, maxEntrypointSize: 1000000 }
36
49
  : { hints: "warning" as const, maxAssetSize: 307200, maxEntrypointSize: 512000 };
37
- }
50
+ }
@@ -28,16 +28,13 @@ export default function clientServerFnTransform({ types: t }: { types: typeof Ba
28
28
  state.hasRunOnServer = true;
29
29
  const filename = state.file.opts.filename || "unknown";
30
30
 
31
- const serverFunctionCallback = path.node.arguments[0];
32
31
  const explicitId = path.node.arguments[1];
33
32
 
34
33
  const functionHash = t.isStringLiteral(explicitId)
35
34
  ? explicitId.value
36
35
  : createServerFunctionId(filename, path.node.start);
37
36
 
38
- if (serverFunctionCallback) {
39
- path.get('arguments.0').remove();
40
- }
37
+ path.get('arguments.0').remove();
41
38
 
42
39
  // this whole thing is just magic bro
43
40
  // wtf is this ast manipulation
@@ -32,11 +32,11 @@ export class AnaemiaManifestHydrationPlugin implements RspackPluginInstance {
32
32
  const files = Array.from(chunk.files);
33
33
 
34
34
  const jsFiles = files.filter(
35
- (f) => f.endsWith(".js") && !f.includes(".hot-update.") && !f.endsWith(".js.map")
35
+ (f) => f.endsWith(".js") && !f.includes(".hot-update.") && !f.endsWith(".js.map"),
36
36
  );
37
37
 
38
38
  const cssFiles = files.filter(
39
- (f) => f.endsWith(".css") && !f.includes(".hot-update.") && !f.endsWith(".css.map")
39
+ (f) => f.endsWith(".css") && !f.includes(".hot-update.") && !f.endsWith(".css.map"),
40
40
  );
41
41
 
42
42
  if (jsFiles.length > 0 || cssFiles.length > 0) {
@@ -54,4 +54,4 @@ export class AnaemiaManifestHydrationPlugin implements RspackPluginInstance {
54
54
  }
55
55
  });
56
56
  }
57
- }
57
+ }
@@ -1,5 +1,5 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
3
  import type { RouteManifestEntry } from "./scan.js";
4
4
  import { transform } from "sucrase";
5
5
 
@@ -21,7 +21,14 @@ type PageNode = {
21
21
 
22
22
  type TreeNode = LayoutNode | PageNode;
23
23
 
24
- function buildTree(routes: RouteManifestEntry[], strippedLayouts: string[][], routeIndices: number[], routesDir: string, allLayouts: Map<string, number>, parentPrefix: string): TreeNode[] {
24
+ function buildTree(
25
+ routes: RouteManifestEntry[],
26
+ strippedLayouts: string[][],
27
+ routeIndices: number[],
28
+ routesDir: string,
29
+ allLayouts: Map<string, number>,
30
+ parentPrefix: string,
31
+ ): TreeNode[] {
25
32
  const nodes: TreeNode[] = [];
26
33
  const leafIndices = strippedLayouts.map((l, i) => (l.length === 0 ? i : -1)).filter((i) => i !== -1);
27
34
 
@@ -94,10 +101,12 @@ function renderTree(nodes: TreeNode[], indent = 6): string {
94
101
  return `${pad}<Route path="${routePath}" component={Route${node.routeIdx}Wrapped} />`;
95
102
  }
96
103
 
97
- let layoutPath = node.relativePath;
104
+ const layoutPath = node.relativePath;
98
105
  const inner = renderTree(node.children, indent + 2);
99
106
 
100
- return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join("\n");
107
+ return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join(
108
+ "\n",
109
+ );
101
110
  })
102
111
  .join("\n");
103
112
  }
@@ -193,7 +202,7 @@ const Route${i}Wrapped = (props) => (
193
202
  conventionalRoutes.map((_, i) => i),
194
203
  routesDir,
195
204
  allLayouts,
196
- "/"
205
+ "/",
197
206
  );
198
207
 
199
208
  let routeJsx = renderTree(tree, 6);
@@ -1,16 +1,19 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
3
  import type { ServerRouteEntry } from "./scan.js";
4
+ import { transform } from "sucrase";
4
5
 
5
6
  export function generateServerRoutes(appRoot: string, routes: ServerRouteEntry[]): string {
6
7
  const outDir = path.resolve(appRoot, "./.anaemia");
7
- const outPath = path.resolve(outDir, "./__anaemia_server_routes__.ts");
8
+ const isTs = fs.existsSync(path.resolve(appRoot, "tsconfig.json"));
9
+ const ext = isTs ? "ts" : "js";
10
+ const outPath = path.resolve(outDir, "./__anaemia_server_routes__." + ext);
8
11
 
9
12
  const imports = routes.map((r, i) => `import * as ServerRoute${i} from "${r.filePath}";`).join("\n");
10
13
 
11
14
  const registrations = routes.map((r, i) => ` registerRoute(app, "${r.urlPattern}", ServerRoute${i});`).join("\n");
12
15
 
13
- const code = `
16
+ const rawCode = `
14
17
  // @ts-nocheck
15
18
  // auto-generated by anaemia - do not edit!!
16
19
  import type { Hono } from "hono";
@@ -31,6 +34,14 @@ ${registrations}
31
34
  }
32
35
  `.trimStart();
33
36
 
34
- fs.writeFileSync(outPath, code);
37
+ const finalCode = isTs
38
+ ? rawCode
39
+ : transform(rawCode.replace("// @ts-nocheck\n", ""), {
40
+ transforms: ["typescript", "jsx"],
41
+ jsxRuntime: "preserve",
42
+ production: true,
43
+ }).code;
44
+
45
+ fs.writeFileSync(outPath, finalCode);
35
46
  return outPath;
36
47
  }
@@ -1,42 +1,28 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import type { RouteManifestEntry } from "./scan.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { RouteManifestEntry } from "../router/scan.js";
4
+ import type { RouteMetadata } from "../analyzer/checks/route-metadata.js";
4
5
 
5
- 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
- }
6
+ export function writeManifest(appRoot: string, routes: RouteManifestEntry[], routeMetadata: RouteMetadata[]) {
7
+ const metadataMap = new Map(routeMetadata.map((m) => [path.resolve(appRoot, m.filePath), m]));
24
8
 
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(),
9
+ const manifest = {
10
+ routes: routes.map((route) => {
11
+ const meta = metadataMap.get(route.filePath);
12
+ return {
13
+ ...route,
14
+ isStatic: meta?.isStatic ?? false,
15
+ hasLoader: meta?.hasLoader ?? false,
16
+ hasGuard: meta?.hasGuard ?? false,
17
+ serverFunctionIds: meta?.serverFunctionIds ?? [],
18
+ };
19
+ }),
20
+ chunks: {},
34
21
  };
35
22
 
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
- }
23
+ const manifestPath = path.resolve(appRoot, "./dist/route-manifest.json");
24
+ const manifestDir = path.dirname(manifestPath);
25
+ if (!fs.existsSync(manifestDir)) fs.mkdirSync(manifestDir, { recursive: true });
26
+
27
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
28
+ }
@@ -1,5 +1,5 @@
1
1
  import { glob } from "glob";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import { createJiti } from "jiti";
4
4
  import fs from "node:fs";
5
5
  import { getAliases } from "../aliases.js";
@@ -51,16 +51,14 @@ const DYNAMIC_SEGMENT = /^\[(.+?)\]\.(tsx|jsx)$/;
51
51
  export function scanServerRoutes(appRoot: string): ServerRouteEntry[] {
52
52
  const routesDir = path.resolve(appRoot, "./src/routes");
53
53
  const files = glob.sync("**/_route.{ts,tsx,js,jsx}", { cwd: routesDir, posix: true });
54
-
54
+
55
55
  return files.map((file) => {
56
56
  const dir = path.dirname(file);
57
-
58
- const normalizedDir = dir
59
- .replace(/\[\.\.\.(.+?)\]/g, "*")
60
- .replace(/\[(.+?)\]/g, ":$1");
57
+
58
+ const normalizedDir = dir.replace(/\[\.\.\.(.+?)\]/g, "*").replace(/\[(.+?)\]/g, ":$1");
61
59
 
62
60
  const urlPattern = normalizedDir === "." ? "/" : `/${normalizedDir}`;
63
-
61
+
64
62
  return {
65
63
  urlPattern,
66
64
  filePath: path.resolve(routesDir, file),
@@ -92,7 +90,7 @@ export async function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]>
92
90
 
93
91
  try {
94
92
  const layoutModule = (await jiti.import(resolveConfigPath(absolutePath))) as RouteModule;
95
- if (layoutModule?.config?.guards) {
93
+ if (layoutModule.config?.guards) {
96
94
  layoutGuards = layoutModule.config.guards;
97
95
  }
98
96
  } catch {
@@ -122,7 +120,7 @@ export async function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]>
122
120
 
123
121
  try {
124
122
  const pageModule = (await jiti.import(resolveConfigPath(absolutePagePath))) as RouteModule;
125
- if (pageModule?.config?.guards) {
123
+ if (pageModule.config?.guards) {
126
124
  pageGuards = pageModule.config.guards;
127
125
  }
128
126
  } catch {
@@ -207,6 +205,7 @@ function resolveLayoutChain(dir: string, layoutMap: Map<string, LayoutManifestEn
207
205
  const layouts: LayoutManifestEntry[] = [];
208
206
  let current = dir;
209
207
 
208
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
210
209
  while (true) {
211
210
  const layoutEntry = layoutMap.get(current);
212
211
  if (layoutEntry) layouts.unshift(layoutEntry);
@@ -215,4 +214,4 @@ function resolveLayoutChain(dir: string, layoutMap: Map<string, LayoutManifestEn
215
214
  }
216
215
 
217
216
  return layouts;
218
- }
217
+ }
package/src/rules.ts CHANGED
@@ -1,30 +1,30 @@
1
1
  import { createRequire } from "node:module";
2
2
  import type { AnaemiaConfig } from "@anaemia/core";
3
- import { PluginItem } from "@babel/core";
3
+ import type { PluginItem } from "@babel/core";
4
+ import type { RuleSetRule } from "@rspack/core";
4
5
 
5
6
  const require = createRequire(import.meta.url);
6
7
 
7
8
  export function createStyleRules(config: AnaemiaConfig) {
8
9
  const useSass = config.styles?.sass !== false;
9
10
  const useModules = config.styles?.modules ?? true;
10
-
11
11
  const baseLoaders = useSass ? [{ loader: require.resolve("sass-loader"), options: { api: "modern" } }] : [];
12
+ const localIdentName = config.styles?.modulesLocalIdentName ?? "[name]__[local]__[hash:base64:5]";
13
+ const cssParser = useModules ? { cssModules: { localIdentName } } : undefined;
12
14
 
13
15
  return {
14
16
  client: {
15
17
  test: /\.(c|sc|sa)ss$/,
16
18
  type: useModules ? "css/auto" : ("css" as const),
17
19
  use: baseLoaders,
20
+ parser: cssParser,
18
21
  },
19
22
  server: {
20
23
  test: /\.(c|sc|sa)ss$/,
21
24
  type: useModules ? "css/auto" : ("css" as const),
22
- generator: {
23
- css: {
24
- exportOnlyLocals: true,
25
- },
26
- },
25
+ generator: { css: { exportOnlyLocals: true } },
27
26
  use: baseLoaders,
27
+ parser: cssParser,
28
28
  },
29
29
  };
30
30
  }
@@ -55,4 +55,44 @@ export function createBabelRule({
55
55
  },
56
56
  ],
57
57
  };
58
- }
58
+ }
59
+
60
+ export function createAssetRules(isDev: boolean) {
61
+ const filename = isDev ? "assets/[name][ext]" : "assets/[name].[contenthash:8][ext]";
62
+
63
+ const sharedRawRule: RuleSetRule = {
64
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg|json)$/i,
65
+ resourceQuery: /raw/,
66
+ type: "asset/source",
67
+ };
68
+
69
+ const sharedUrlRule: RuleSetRule = {
70
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
71
+ resourceQuery: /url/,
72
+ type: "asset/resource",
73
+ generator: { filename },
74
+ };
75
+
76
+ const sharedInlineRule: RuleSetRule = {
77
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
78
+ resourceQuery: /inline/,
79
+ type: "asset/inline",
80
+ };
81
+
82
+ const sharedAssetRules: RuleSetRule[] = [sharedRawRule, sharedUrlRule, sharedInlineRule];
83
+
84
+ const clientRules: RuleSetRule[] = [
85
+ ...sharedAssetRules,
86
+ { test: /\.(png|jpe?g|gif|webp|avif|ico)$/i, type: "asset/resource", generator: { filename } },
87
+ { test: /\.svg$/i, type: "asset", parser: { dataUrlCondition: { maxSize: 8192 } }, generator: { filename } },
88
+ { test: /\.json$/i, type: "json" },
89
+ ];
90
+
91
+ const serverRules: RuleSetRule[] = [
92
+ ...sharedAssetRules,
93
+ { test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i, type: "asset/source" },
94
+ { test: /\.json$/i, type: "json" },
95
+ ];
96
+
97
+ return { client: clientRules, server: serverRules };
98
+ }