@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/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { rspack } from "@rspack/core";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
+ import pc from "picocolors";
6
7
  import clientServerFnTransform from "./plugins/babel-transform-server.js";
7
8
  import serverHashInjector from "./plugins/babel-hash-injector-server.js";
8
9
  import { AnaemiaManifestHydrationPlugin } from "./plugins/rspack-manifest-hydration.js";
@@ -11,53 +12,93 @@ import { writeManifest } from "./router/manifest.js";
11
12
  import { generateRouterEntry } from "./router/generate-entry.js";
12
13
  import { generateServerRoutes } from "./router/generate-server-routes.js";
13
14
  import { getAliases } from "./aliases.js";
14
- import { createStyleRules, createBabelRule } from "./rules.js";
15
+ import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
15
16
  import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
17
+ import loadEnvFiles from "./env-loader.js";
18
+ import { analyzeApp } from "./analyzer/index.js";
16
19
  const require = createRequire(import.meta.url);
17
20
  const __filename = fileURLToPath(import.meta.url);
18
21
  const __dirname = path.dirname(__filename);
19
22
  export async function getRspackConfig(appRoot, config = {}) {
20
23
  const isDev = process.env.NODE_ENV !== "production";
21
- const rawEnv = process.env;
24
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
25
+ // run the analyzer to collect route metadata and other information about the app that we can use to optimize the build
26
+ const analysis = await analyzeApp(appRoot, {
27
+ mode: isDev ? "development" : "production",
28
+ });
29
+ // flush diagnostics to console
30
+ const tag = pc.dim("[anaemia-analyzer]");
31
+ for (const diagnostic of analysis.diagnostics) {
32
+ const prefix = diagnostic.severity === "error"
33
+ ? pc.red("✖ [error]")
34
+ : diagnostic.severity === "warning"
35
+ ? pc.yellow("⚠ [warning]")
36
+ : pc.cyan("› [info]");
37
+ const loc = diagnostic.line ? pc.dim(`:${diagnostic.line}`) : "";
38
+ const file = pc.bold(diagnostic.filePath);
39
+ const msg = diagnostic.severity === "error"
40
+ ? pc.red(diagnostic.message)
41
+ : diagnostic.severity === "warning"
42
+ ? pc.yellow(diagnostic.message)
43
+ : diagnostic.message;
44
+ // eslint-disable-next-line no-console
45
+ console.log(`${tag} ${prefix} ${file}${loc} - ${msg}`);
46
+ // eslint-disable-next-line no-console
47
+ if (diagnostic.help)
48
+ console.log(` ${pc.dim(`hint: ${diagnostic.help}`)}`);
49
+ }
22
50
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
23
51
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
24
52
  const routes = await scanRoutes(appRoot);
25
53
  const serverRoutes = scanServerRoutes(appRoot);
26
- writeManifest(appRoot, routes);
54
+ writeManifest(appRoot, routes, analysis.routeMetadata);
27
55
  const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
28
56
  if (!fs.existsSync(frameworkInternalDir)) {
29
57
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
30
58
  }
59
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
31
60
  const entryFile = generateRouterEntry(appRoot, routes);
32
61
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
33
62
  const styleRules = createStyleRules(config);
63
+ const assetRules = createAssetRules(isDev);
64
+ // 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
34
65
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
35
66
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
36
67
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
37
- // env processing
68
+ /**
69
+ * env processing:
70
+ *
71
+ * - we inject some default env vars like MODE, DEV, and PROD for convenience
72
+ * - for the server, we expose all env vars
73
+ * - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
74
+ * - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
75
+ */
38
76
  const serverEnv = {
39
77
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
40
78
  DEV: JSON.stringify(isDev),
41
79
  PROD: JSON.stringify(!isDev),
42
80
  };
43
- for (const key in rawEnv) {
44
- serverEnv[key] = JSON.stringify(rawEnv[key]);
81
+ for (const key in process.env) {
82
+ serverEnv[key] = JSON.stringify(process.env[key]);
45
83
  }
46
84
  const clientEnv = {
47
85
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
48
86
  DEV: JSON.stringify(isDev),
49
87
  PROD: JSON.stringify(!isDev),
50
88
  };
51
- for (const key in rawEnv) {
89
+ for (const key in process.env) {
52
90
  if (key.startsWith("PUBLIC_")) {
53
- clientEnv[key] = JSON.stringify(rawEnv[key]);
91
+ clientEnv[key] = JSON.stringify(process.env[key]);
54
92
  }
55
93
  }
94
+ // shared resolve config
95
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
56
96
  const sharedResolve = {
57
97
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
58
98
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
59
99
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
60
100
  };
101
+ // client and server configurations
61
102
  let clientConfig = {
62
103
  name: "client",
63
104
  context: appRoot,
@@ -84,7 +125,14 @@ export async function getRspackConfig(appRoot, config = {}) {
84
125
  "solid-refresh": require.resolve("solid-refresh"),
85
126
  [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
86
127
  },
87
- fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
128
+ fallback: {
129
+ async_hooks: false,
130
+ "node:async_hooks": false,
131
+ fs: false,
132
+ "node:fs": false,
133
+ path: false,
134
+ "node:path": false,
135
+ },
88
136
  },
89
137
  devServer: isDev
90
138
  ? {
@@ -97,9 +145,17 @@ export async function getRspackConfig(appRoot, config = {}) {
97
145
  }
98
146
  : undefined,
99
147
  plugins: [
100
- new rspack.HtmlRspackPlugin({ template: path.resolve(appRoot, "./index.html"), filename: "index.html", inject: false }),
148
+ new rspack.HtmlRspackPlugin({
149
+ template: path.resolve(appRoot, "./index.html"),
150
+ filename: "index.html",
151
+ inject: false,
152
+ }),
101
153
  new rspack.DefinePlugin({
102
- __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({ port: config.port, assets: config.assets, styles: config.styles }),
154
+ __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
155
+ port: config.port,
156
+ assets: config.assets,
157
+ styles: config.styles,
158
+ }),
103
159
  ...config.define?.client,
104
160
  "import.meta.env": clientEnv,
105
161
  }),
@@ -118,8 +174,13 @@ export async function getRspackConfig(appRoot, config = {}) {
118
174
  parser: { "css/auto": { namedExports: false } },
119
175
  rules: [
120
176
  styleRules.client,
177
+ ...assetRules.client,
121
178
  {
122
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
179
+ ...createBabelRule({
180
+ isServer: false,
181
+ isDev,
182
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
183
+ }),
123
184
  exclude: (modulePath) => {
124
185
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
125
186
  return false;
@@ -137,7 +198,13 @@ export async function getRspackConfig(appRoot, config = {}) {
137
198
  context: appRoot,
138
199
  target: "node",
139
200
  entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
140
- output: { path: path.resolve(appRoot, "./dist/server"), filename: "index.js", module: true, chunkFormat: "module", chunkLoading: "import" },
201
+ output: {
202
+ path: path.resolve(appRoot, "./dist/server"),
203
+ filename: "index.js",
204
+ module: true,
205
+ chunkFormat: "module",
206
+ chunkLoading: "import",
207
+ },
141
208
  optimization: { nodeEnv: false },
142
209
  resolve: {
143
210
  ...sharedResolve,
@@ -156,8 +223,13 @@ export async function getRspackConfig(appRoot, config = {}) {
156
223
  parser: { "css/auto": { namedExports: false } },
157
224
  rules: [
158
225
  styleRules.server,
226
+ ...assetRules.server,
159
227
  {
160
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
228
+ ...createBabelRule({
229
+ isServer: true,
230
+ isDev,
231
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
232
+ }),
161
233
  exclude: (modulePath) => {
162
234
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
163
235
  return false;
@@ -169,6 +241,7 @@ export async function getRspackConfig(appRoot, config = {}) {
169
241
  ],
170
242
  },
171
243
  };
244
+ // allow plugins to modify the client and server configurations before they are returned
172
245
  for (const plugin of config.plugins ?? []) {
173
246
  if (plugin.clientRspackConfig)
174
247
  clientConfig = plugin.clientRspackConfig(clientConfig);
@@ -179,3 +252,4 @@ export async function getRspackConfig(appRoot, config = {}) {
179
252
  }
180
253
  export { scanRoutes } from "./router/scan.js";
181
254
  export { writeManifest } from "./router/manifest.js";
255
+ export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"optimization.d.ts","sourceRoot":"","sources":["../src/optimization.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;uBAkC0wI,CAAC;;;;;EAN9zI;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;EAInD"}
1
+ {"version":3,"file":"optimization.d.ts","sourceRoot":"","sources":["../src/optimization.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;uBAgDwhI,CAAC;;;;;EAP5kI;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO;;;;;;;;EAInD"}
@@ -1,8 +1,8 @@
1
1
  import { rspack } from "@rspack/core";
2
2
  export function getClientOptimization(isDev) {
3
3
  return {
4
- sideEffects: true,
5
- usedExports: true,
4
+ sideEffects: !isDev,
5
+ usedExports: !isDev,
6
6
  splitChunks: isDev
7
7
  ? false
8
8
  : {
@@ -13,7 +13,7 @@ export function getClientOptimization(isDev) {
13
13
  framework: {
14
14
  chunks: "all",
15
15
  name: "framework",
16
- test: /[\\/]node_modules[\\/](solid-js|@solidjs[\\/]router)[\\/]/,
16
+ test: /[\\/]node_modules[\\/](solid-js|@solidjs)[\\/]/,
17
17
  priority: 40,
18
18
  enforce: true,
19
19
  },
@@ -25,7 +25,20 @@ export function getClientOptimization(isDev) {
25
25
  },
26
26
  },
27
27
  },
28
- minimizer: isDev ? [] : [new rspack.SwcJsMinimizerRspackPlugin()],
28
+ minimizer: isDev
29
+ ? []
30
+ : [
31
+ new rspack.SwcJsMinimizerRspackPlugin({
32
+ minimizerOptions: {
33
+ mangle: {
34
+ keep_fnames: true,
35
+ },
36
+ compress: {
37
+ keep_fnames: true,
38
+ },
39
+ },
40
+ }),
41
+ ],
29
42
  };
30
43
  }
31
44
  export function getPerformanceProfile(isDev) {
@@ -1 +1 @@
1
- {"version":3,"file":"babel-transform-server.d.ts","sourceRoot":"","sources":["../../src/plugins/babel-transform-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAY,SAAS,EAAE,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,UAAU,WAAY,SAAQ,UAAU;IACtC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,UAAU,CAAA;CAAE,GAAG,SAAS,CAAC,WAAW,CAAC,CA6IlH"}
1
+ {"version":3,"file":"babel-transform-server.d.ts","sourceRoot":"","sources":["../../src/plugins/babel-transform-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAY,SAAS,EAAE,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,UAAU,WAAY,SAAQ,UAAU;IACtC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,UAAU,CAAA;CAAE,GAAG,SAAS,CAAC,WAAW,CAAC,CA0IlH"}
@@ -19,14 +19,11 @@ export default function clientServerFnTransform({ types: t }) {
19
19
  if (t.isIdentifier(path.node.callee) && path.node.callee.name === "runOnServer") {
20
20
  state.hasRunOnServer = true;
21
21
  const filename = state.file.opts.filename || "unknown";
22
- const serverFunctionCallback = path.node.arguments[0];
23
22
  const explicitId = path.node.arguments[1];
24
23
  const functionHash = t.isStringLiteral(explicitId)
25
24
  ? explicitId.value
26
25
  : createServerFunctionId(filename, path.node.start);
27
- if (serverFunctionCallback) {
28
- path.get('arguments.0').remove();
29
- }
26
+ path.get('arguments.0').remove();
30
27
  // this whole thing is just magic bro
31
28
  // wtf is this ast manipulation
32
29
  path.replaceWith(t.callExpression(t.arrowFunctionExpression([], t.blockStatement([
@@ -1 +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;AAiHpD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,CAqMzF"}
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;AA0HpD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,CAqMzF"}
@@ -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 { transform } from "sucrase";
4
4
  function buildTree(routes, strippedLayouts, routeIndices, routesDir, allLayouts, parentPrefix) {
5
5
  const nodes = [];
@@ -66,7 +66,7 @@ function renderTree(nodes, indent = 6) {
66
66
  }
67
67
  return `${pad}<Route path="${routePath}" component={Route${node.routeIdx}Wrapped} />`;
68
68
  }
69
- let layoutPath = node.relativePath;
69
+ const layoutPath = node.relativePath;
70
70
  const inner = renderTree(node.children, indent + 2);
71
71
  return [`${pad}<Route path="${layoutPath}" component={Layout${node.layoutIdx}}>`, inner, `${pad}</Route>`].join("\n");
72
72
  })
@@ -1 +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"}
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;AAGlD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAyCxF"}
@@ -1,11 +1,14 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { transform } from "sucrase";
3
4
  export function generateServerRoutes(appRoot, routes) {
4
5
  const outDir = path.resolve(appRoot, "./.anaemia");
5
- const outPath = path.resolve(outDir, "./__anaemia_server_routes__.ts");
6
+ const isTs = fs.existsSync(path.resolve(appRoot, "tsconfig.json"));
7
+ const ext = isTs ? "ts" : "js";
8
+ const outPath = path.resolve(outDir, "./__anaemia_server_routes__." + ext);
6
9
  const imports = routes.map((r, i) => `import * as ServerRoute${i} from "${r.filePath}";`).join("\n");
7
10
  const registrations = routes.map((r, i) => ` registerRoute(app, "${r.urlPattern}", ServerRoute${i});`).join("\n");
8
- const code = `
11
+ const rawCode = `
9
12
  // @ts-nocheck
10
13
  // auto-generated by anaemia - do not edit!!
11
14
  import type { Hono } from "hono";
@@ -25,6 +28,13 @@ export function registerServerRoutes(app: Hono) {
25
28
  ${registrations}
26
29
  }
27
30
  `.trimStart();
28
- fs.writeFileSync(outPath, code);
31
+ const finalCode = isTs
32
+ ? rawCode
33
+ : transform(rawCode.replace("// @ts-nocheck\n", ""), {
34
+ transforms: ["typescript", "jsx"],
35
+ jsxRuntime: "preserve",
36
+ production: true,
37
+ }).code;
38
+ fs.writeFileSync(outPath, finalCode);
29
39
  return outPath;
30
40
  }
@@ -1,3 +1,4 @@
1
- import type { RouteManifestEntry } from "./scan.js";
2
- export declare function writeManifest(appRoot: string, routes: RouteManifestEntry[]): void;
1
+ import type { RouteManifestEntry } from "../router/scan.js";
2
+ import type { RouteMetadata } from "../analyzer/checks/route-metadata.js";
3
+ export declare function writeManifest(appRoot: string, routes: RouteManifestEntry[], routeMetadata: RouteMetadata[]): void;
3
4
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +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;AAUpD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,CA6BjF"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/router/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAE1E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,aAAa,EAAE,aAAa,EAAE,QAsB1G"}
@@ -1,23 +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"));
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function writeManifest(appRoot, routes, routeMetadata) {
4
+ const metadataMap = new Map(routeMetadata.map((m) => [path.resolve(appRoot, m.filePath), m]));
14
5
  const manifest = {
15
- routes: conventionalRoutes,
16
- errors,
17
- chunks: {}, // rspack fills this in via ManifestPlugin
18
- buildTime: new Date().toISOString(),
6
+ routes: routes.map((route) => {
7
+ const meta = metadataMap.get(route.filePath);
8
+ return {
9
+ ...route,
10
+ isStatic: meta?.isStatic ?? false,
11
+ hasLoader: meta?.hasLoader ?? false,
12
+ hasGuard: meta?.hasGuard ?? false,
13
+ serverFunctionIds: meta?.serverFunctionIds ?? [],
14
+ };
15
+ }),
16
+ chunks: {},
19
17
  };
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));
18
+ const manifestPath = path.resolve(appRoot, "./dist/route-manifest.json");
19
+ const manifestDir = path.dirname(manifestPath);
20
+ if (!fs.existsSync(manifestDir))
21
+ fs.mkdirSync(manifestDir, { recursive: true });
22
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
23
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/router/scan.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAkBpE;AAUD,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiE/E"}
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/router/scan.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAgBpE;AAUD,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiE/E"}
@@ -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";
@@ -17,9 +17,7 @@ export function scanServerRoutes(appRoot) {
17
17
  const files = glob.sync("**/_route.{ts,tsx,js,jsx}", { cwd: routesDir, posix: true });
18
18
  return files.map((file) => {
19
19
  const dir = path.dirname(file);
20
- const normalizedDir = dir
21
- .replace(/\[\.\.\.(.+?)\]/g, "*")
22
- .replace(/\[(.+?)\]/g, ":$1");
20
+ const normalizedDir = dir.replace(/\[\.\.\.(.+?)\]/g, "*").replace(/\[(.+?)\]/g, ":$1");
23
21
  const urlPattern = normalizedDir === "." ? "/" : `/${normalizedDir}`;
24
22
  return {
25
23
  urlPattern,
@@ -47,7 +45,7 @@ export async function scanRoutes(appRoot) {
47
45
  let layoutGuards = [];
48
46
  try {
49
47
  const layoutModule = (await jiti.import(resolveConfigPath(absolutePath)));
50
- if (layoutModule?.config?.guards) {
48
+ if (layoutModule.config?.guards) {
51
49
  layoutGuards = layoutModule.config.guards;
52
50
  }
53
51
  }
@@ -73,7 +71,7 @@ export async function scanRoutes(appRoot) {
73
71
  let pageGuards = [];
74
72
  try {
75
73
  const pageModule = (await jiti.import(resolveConfigPath(absolutePagePath)));
76
- if (pageModule?.config?.guards) {
74
+ if (pageModule.config?.guards) {
77
75
  pageGuards = pageModule.config.guards;
78
76
  }
79
77
  }
@@ -148,6 +146,7 @@ function parseFilePath(file) {
148
146
  function resolveLayoutChain(dir, layoutMap) {
149
147
  const layouts = [];
150
148
  let current = dir;
149
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
151
150
  while (true) {
152
151
  const layoutEntry = layoutMap.get(current);
153
152
  if (layoutEntry)
package/dist/rules.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AnaemiaConfig } from "@anaemia/core";
2
- import { PluginItem } from "@babel/core";
2
+ import type { PluginItem } from "@babel/core";
3
+ import type { RuleSetRule } from "@rspack/core";
3
4
  export declare function createStyleRules(config: AnaemiaConfig): {
4
5
  client: {
5
6
  test: RegExp;
@@ -10,6 +11,11 @@ export declare function createStyleRules(config: AnaemiaConfig): {
10
11
  api: string;
11
12
  };
12
13
  }[];
14
+ parser: {
15
+ cssModules: {
16
+ localIdentName: string;
17
+ };
18
+ } | undefined;
13
19
  };
14
20
  server: {
15
21
  test: RegExp;
@@ -25,6 +31,11 @@ export declare function createStyleRules(config: AnaemiaConfig): {
25
31
  api: string;
26
32
  };
27
33
  }[];
34
+ parser: {
35
+ cssModules: {
36
+ localIdentName: string;
37
+ };
38
+ } | undefined;
28
39
  };
29
40
  };
30
41
  export declare function createBabelRule({ isServer, isDev, plugins, }: {
@@ -45,4 +56,8 @@ export declare function createBabelRule({ isServer, isDev, plugins, }: {
45
56
  };
46
57
  }[];
47
58
  };
59
+ export declare function createAssetRules(isDev: boolean): {
60
+ client: RuleSetRule[];
61
+ server: RuleSetRule[];
62
+ };
48
63
  //# sourceMappingURL=rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,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,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;;;;;;;;;;;;;EAkBA"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBrD;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,KAAK,EACL,OAAY,GACb,EAAE;IACD,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;;;;;;;;;;;;;EAkBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO;;;EAsC9C"}
package/dist/rules.js CHANGED
@@ -4,21 +4,21 @@ export function createStyleRules(config) {
4
4
  const useSass = config.styles?.sass !== false;
5
5
  const useModules = config.styles?.modules ?? true;
6
6
  const baseLoaders = useSass ? [{ loader: require.resolve("sass-loader"), options: { api: "modern" } }] : [];
7
+ const localIdentName = config.styles?.modulesLocalIdentName ?? "[name]__[local]__[hash:base64:5]";
8
+ const cssParser = useModules ? { cssModules: { localIdentName } } : undefined;
7
9
  return {
8
10
  client: {
9
11
  test: /\.(c|sc|sa)ss$/,
10
12
  type: useModules ? "css/auto" : "css",
11
13
  use: baseLoaders,
14
+ parser: cssParser,
12
15
  },
13
16
  server: {
14
17
  test: /\.(c|sc|sa)ss$/,
15
18
  type: useModules ? "css/auto" : "css",
16
- generator: {
17
- css: {
18
- exportOnlyLocals: true,
19
- },
20
- },
19
+ generator: { css: { exportOnlyLocals: true } },
21
20
  use: baseLoaders,
21
+ parser: cssParser,
22
22
  },
23
23
  };
24
24
  }
@@ -40,3 +40,35 @@ export function createBabelRule({ isServer, isDev, plugins = [], }) {
40
40
  ],
41
41
  };
42
42
  }
43
+ export function createAssetRules(isDev) {
44
+ const filename = isDev ? "assets/[name][ext]" : "assets/[name].[contenthash:8][ext]";
45
+ const sharedRawRule = {
46
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg|json)$/i,
47
+ resourceQuery: /raw/,
48
+ type: "asset/source",
49
+ };
50
+ const sharedUrlRule = {
51
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
52
+ resourceQuery: /url/,
53
+ type: "asset/resource",
54
+ generator: { filename },
55
+ };
56
+ const sharedInlineRule = {
57
+ test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i,
58
+ resourceQuery: /inline/,
59
+ type: "asset/inline",
60
+ };
61
+ const sharedAssetRules = [sharedRawRule, sharedUrlRule, sharedInlineRule];
62
+ const clientRules = [
63
+ ...sharedAssetRules,
64
+ { test: /\.(png|jpe?g|gif|webp|avif|ico)$/i, type: "asset/resource", generator: { filename } },
65
+ { test: /\.svg$/i, type: "asset", parser: { dataUrlCondition: { maxSize: 8192 } }, generator: { filename } },
66
+ { test: /\.json$/i, type: "json" },
67
+ ];
68
+ const serverRules = [
69
+ ...sharedAssetRules,
70
+ { test: /\.(png|jpe?g|gif|webp|avif|ico|svg)$/i, type: "asset/source" },
71
+ { test: /\.json$/i, type: "json" },
72
+ ];
73
+ return { client: clientRules, server: serverRules };
74
+ }
package/package.json CHANGED
@@ -1,24 +1,32 @@
1
1
  {
2
2
  "name": "@anaemia/bundler",
3
- "version": "0.3.7",
3
+ "version": "0.5.0",
4
+ "type": "module",
4
5
  "main": "./dist/index.js",
5
6
  "types": "./dist/index.d.ts",
6
- "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "default": "./dist/index.js"
11
+ },
12
+ "./analyzer": {
13
+ "types": "./dist/analyzer/index.d.ts",
14
+ "default": "./dist/analyzer/index.js"
11
15
  }
12
16
  },
13
17
  "dependencies": {
14
- "@anaemia/core": "^0.3.7",
18
+ "@anaemia/core": "^0.5.0",
15
19
  "@babel/core": "^7.29.7",
16
20
  "@babel/preset-typescript": "^7.29.7",
17
21
  "@rspack/core": "^2.0.5",
18
22
  "babel-loader": "^10.1.1",
19
23
  "babel-preset-solid": "^1.9.12",
24
+ "dotenv": "^17.4.2",
25
+ "dotenv-expand": "^13.0.0",
20
26
  "glob": "^13.0.6",
21
27
  "jiti": "^2.7.0",
28
+ "oxc-parser": "^0.133.0",
29
+ "picocolors": "^1.1.1",
22
30
  "sass": "^1.100.0",
23
31
  "sass-loader": "^17.0.0",
24
32
  "solid-refresh": "^0.7.8",
package/src/aliases.ts CHANGED
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
 
3
3
  export function getAliases(appRoot: string) {
4
4
  return {
@@ -8,4 +8,4 @@ export function getAliases(appRoot: string) {
8
8
  "@features": path.resolve(appRoot, "./src/features"),
9
9
  "@routes": path.resolve(appRoot, "./src/routes"),
10
10
  };
11
- }
11
+ }
@@ -0,0 +1,22 @@
1
+ import type { AstNode } from "./ast-walker.js";
2
+
3
+ export function prop<T = unknown>(node: AstNode, key: string): T {
4
+ return (node as Record<string, unknown>)[key] as T;
5
+ }
6
+
7
+ export function child(node: AstNode, key: string): AstNode | null {
8
+ const value = (node as Record<string, unknown>)[key];
9
+ if (value && typeof value === "object" && typeof (value as AstNode).type === "string") {
10
+ return value as AstNode;
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export function children(node: AstNode, key: string): AstNode[] {
16
+ const value = (node as Record<string, unknown>)[key];
17
+ if (!Array.isArray(value)) return [];
18
+ return value.filter(
19
+ (v): v is AstNode =>
20
+ v !== null && v !== undefined && typeof v === "object" && typeof (v as Record<string, unknown>).type === "string",
21
+ );
22
+ }