@anaemia/bundler 0.3.7 → 0.4.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 (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/aliases.js +1 -1
  4. package/dist/env-loader.d.ts +2 -0
  5. package/dist/env-loader.d.ts.map +1 -0
  6. package/dist/env-loader.js +10 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +60 -14
  10. package/dist/optimization.d.ts.map +1 -1
  11. package/dist/optimization.js +17 -4
  12. package/dist/plugins/babel-transform-server.d.ts.map +1 -1
  13. package/dist/plugins/babel-transform-server.js +1 -4
  14. package/dist/router/generate-entry.d.ts.map +1 -1
  15. package/dist/router/generate-entry.js +3 -3
  16. package/dist/router/generate-server-routes.d.ts.map +1 -1
  17. package/dist/router/generate-server-routes.js +15 -5
  18. package/dist/router/manifest.d.ts.map +1 -1
  19. package/dist/router/manifest.js +2 -2
  20. package/dist/router/scan.d.ts.map +1 -1
  21. package/dist/router/scan.js +5 -6
  22. package/dist/rules.d.ts +16 -1
  23. package/dist/rules.d.ts.map +1 -1
  24. package/dist/rules.js +37 -5
  25. package/package.json +5 -3
  26. package/src/aliases.ts +2 -2
  27. package/src/env-loader.ts +13 -0
  28. package/src/index.ts +76 -18
  29. package/src/optimization.ts +18 -5
  30. package/src/plugins/babel-transform-server.ts +1 -4
  31. package/src/plugins/rspack-manifest-hydration.ts +3 -3
  32. package/src/router/generate-entry.ts +15 -6
  33. package/src/router/generate-server-routes.ts +16 -5
  34. package/src/router/manifest.ts +5 -10
  35. package/src/router/scan.ts +9 -10
  36. package/src/rules.ts +48 -8
  37. package/test/rspack-config.test.mjs +5 -2
  38. package/test/server-functions.test.mjs +25 -22
  39. package/tsconfig.json +1 -1
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright [2026] [colourlabs]
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # @anaemia/bundler
2
2
 
3
- the Rspack bundler pipelines for the [anaemia](https://github.com/colourlabs/anaemia) SolidJS SSR framework
3
+ the Rspack bundler pipelines for the [anaemia](https://github.com/colourlabs/anaemia) SolidJS SSR framework
package/dist/aliases.js CHANGED
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  export function getAliases(appRoot) {
3
3
  return {
4
4
  "~": path.resolve(appRoot, "./src"),
@@ -0,0 +1,2 @@
1
+ export default function loadEnvFiles(appRoot: string, mode: string): void;
2
+ //# sourceMappingURL=env-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-loader.d.ts","sourceRoot":"","sources":["../src/env-loader.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAOjE"}
@@ -0,0 +1,10 @@
1
+ import { config as loadDotenv } from "dotenv";
2
+ import { expand as expandDotenv } from "dotenv-expand";
3
+ import path from "node:path";
4
+ export default function loadEnvFiles(appRoot, mode) {
5
+ const files = [`.env`, `.env.local`, `.env.${mode}`, `.env.${mode}.local`];
6
+ for (const file of files) {
7
+ const result = loadDotenv({ path: path.resolve(appRoot, file), override: true });
8
+ expandDotenv(result);
9
+ }
10
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Configuration } from "@rspack/core";
1
+ import type { Configuration } from "@rspack/core";
2
2
  import type { AnaemiaConfig } from "@anaemia/core/config";
3
3
  export declare function getRspackConfig(appRoot: string, config?: AnaemiaConfig): Promise<[Configuration, Configuration]>;
4
4
  export { scanRoutes } from "./router/scan.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAU,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAqB1D,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CAuK1H;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAIlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAsB1D,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CA4NzC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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";
@@ -11,14 +11,15 @@ import { writeManifest } from "./router/manifest.js";
11
11
  import { generateRouterEntry } from "./router/generate-entry.js";
12
12
  import { generateServerRoutes } from "./router/generate-server-routes.js";
13
13
  import { getAliases } from "./aliases.js";
14
- import { createStyleRules, createBabelRule } from "./rules.js";
14
+ import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
15
15
  import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
16
+ import loadEnvFiles from "./env-loader.js";
16
17
  const require = createRequire(import.meta.url);
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
19
20
  export async function getRspackConfig(appRoot, config = {}) {
20
21
  const isDev = process.env.NODE_ENV !== "production";
21
- const rawEnv = process.env;
22
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
22
23
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
23
24
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
24
25
  const routes = await scanRoutes(appRoot);
@@ -28,36 +29,49 @@ export async function getRspackConfig(appRoot, config = {}) {
28
29
  if (!fs.existsSync(frameworkInternalDir)) {
29
30
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
30
31
  }
32
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
31
33
  const entryFile = generateRouterEntry(appRoot, routes);
32
34
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
33
35
  const styleRules = createStyleRules(config);
36
+ const assetRules = createAssetRules(isDev);
37
+ // 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
38
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
35
39
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
36
40
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
37
- // env processing
41
+ /**
42
+ * env processing:
43
+ *
44
+ * - we inject some default env vars like MODE, DEV, and PROD for convenience
45
+ * - for the server, we expose all env vars
46
+ * - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
47
+ * - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
48
+ */
38
49
  const serverEnv = {
39
50
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
40
51
  DEV: JSON.stringify(isDev),
41
52
  PROD: JSON.stringify(!isDev),
42
53
  };
43
- for (const key in rawEnv) {
44
- serverEnv[key] = JSON.stringify(rawEnv[key]);
54
+ for (const key in process.env) {
55
+ serverEnv[key] = JSON.stringify(process.env[key]);
45
56
  }
46
57
  const clientEnv = {
47
58
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
48
59
  DEV: JSON.stringify(isDev),
49
60
  PROD: JSON.stringify(!isDev),
50
61
  };
51
- for (const key in rawEnv) {
62
+ for (const key in process.env) {
52
63
  if (key.startsWith("PUBLIC_")) {
53
- clientEnv[key] = JSON.stringify(rawEnv[key]);
64
+ clientEnv[key] = JSON.stringify(process.env[key]);
54
65
  }
55
66
  }
67
+ // shared resolve config
68
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
56
69
  const sharedResolve = {
57
70
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
58
71
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
59
72
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
60
73
  };
74
+ // client and server configurations
61
75
  let clientConfig = {
62
76
  name: "client",
63
77
  context: appRoot,
@@ -84,7 +98,14 @@ export async function getRspackConfig(appRoot, config = {}) {
84
98
  "solid-refresh": require.resolve("solid-refresh"),
85
99
  [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
86
100
  },
87
- fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
101
+ fallback: {
102
+ async_hooks: false,
103
+ "node:async_hooks": false,
104
+ fs: false,
105
+ "node:fs": false,
106
+ path: false,
107
+ "node:path": false,
108
+ },
88
109
  },
89
110
  devServer: isDev
90
111
  ? {
@@ -97,9 +118,17 @@ export async function getRspackConfig(appRoot, config = {}) {
97
118
  }
98
119
  : undefined,
99
120
  plugins: [
100
- new rspack.HtmlRspackPlugin({ template: path.resolve(appRoot, "./index.html"), filename: "index.html", inject: false }),
121
+ new rspack.HtmlRspackPlugin({
122
+ template: path.resolve(appRoot, "./index.html"),
123
+ filename: "index.html",
124
+ inject: false,
125
+ }),
101
126
  new rspack.DefinePlugin({
102
- __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({ port: config.port, assets: config.assets, styles: config.styles }),
127
+ __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
128
+ port: config.port,
129
+ assets: config.assets,
130
+ styles: config.styles,
131
+ }),
103
132
  ...config.define?.client,
104
133
  "import.meta.env": clientEnv,
105
134
  }),
@@ -118,8 +147,13 @@ export async function getRspackConfig(appRoot, config = {}) {
118
147
  parser: { "css/auto": { namedExports: false } },
119
148
  rules: [
120
149
  styleRules.client,
150
+ ...assetRules.client,
121
151
  {
122
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
152
+ ...createBabelRule({
153
+ isServer: false,
154
+ isDev,
155
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
156
+ }),
123
157
  exclude: (modulePath) => {
124
158
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
125
159
  return false;
@@ -137,7 +171,13 @@ export async function getRspackConfig(appRoot, config = {}) {
137
171
  context: appRoot,
138
172
  target: "node",
139
173
  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" },
174
+ output: {
175
+ path: path.resolve(appRoot, "./dist/server"),
176
+ filename: "index.js",
177
+ module: true,
178
+ chunkFormat: "module",
179
+ chunkLoading: "import",
180
+ },
141
181
  optimization: { nodeEnv: false },
142
182
  resolve: {
143
183
  ...sharedResolve,
@@ -156,8 +196,13 @@ export async function getRspackConfig(appRoot, config = {}) {
156
196
  parser: { "css/auto": { namedExports: false } },
157
197
  rules: [
158
198
  styleRules.server,
199
+ ...assetRules.server,
159
200
  {
160
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
201
+ ...createBabelRule({
202
+ isServer: true,
203
+ isDev,
204
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
205
+ }),
161
206
  exclude: (modulePath) => {
162
207
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
163
208
  return false;
@@ -169,6 +214,7 @@ export async function getRspackConfig(appRoot, config = {}) {
169
214
  ],
170
215
  },
171
216
  };
217
+ // allow plugins to modify the client and server configurations before they are returned
172
218
  for (const plugin of config.plugins ?? []) {
173
219
  if (plugin.clientRspackConfig)
174
220
  clientConfig = plugin.clientRspackConfig(clientConfig);
@@ -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 +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,WAAW,CAAC;AAUpD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAwBjF"}
@@ -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
  export function writeManifest(appRoot, routes) {
4
4
  const errors = {};
5
5
  for (const route of routes) {
@@ -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,9 +1,9 @@
1
1
  {
2
2
  "name": "@anaemia/bundler",
3
- "version": "0.3.7",
3
+ "version": "0.4.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",
@@ -11,12 +11,14 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@anaemia/core": "^0.3.7",
14
+ "@anaemia/core": "^0.4.0",
15
15
  "@babel/core": "^7.29.7",
16
16
  "@babel/preset-typescript": "^7.29.7",
17
17
  "@rspack/core": "^2.0.5",
18
18
  "babel-loader": "^10.1.1",
19
19
  "babel-preset-solid": "^1.9.12",
20
+ "dotenv": "^17.4.2",
21
+ "dotenv-expand": "^13.0.0",
20
22
  "glob": "^13.0.6",
21
23
  "jiti": "^2.7.0",
22
24
  "sass": "^1.100.0",
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,13 @@
1
+ import { config as loadDotenv } from "dotenv";
2
+ import { expand as expandDotenv } from "dotenv-expand";
3
+
4
+ import path from "node:path";
5
+
6
+ export default function loadEnvFiles(appRoot: string, mode: string) {
7
+ const files = [`.env`, `.env.local`, `.env.${mode}`, `.env.${mode}.local`];
8
+
9
+ for (const file of files) {
10
+ const result = loadDotenv({ path: path.resolve(appRoot, file), override: true });
11
+ expandDotenv(result);
12
+ }
13
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
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";
@@ -15,16 +16,21 @@ import { generateRouterEntry } from "./router/generate-entry.js";
15
16
  import { generateServerRoutes } from "./router/generate-server-routes.js";
16
17
  import { getAliases } from "./aliases.js";
17
18
 
18
- import { createStyleRules, createBabelRule } from "./rules.js";
19
+ import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
19
20
  import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
21
+ import loadEnvFiles from "./env-loader.js";
20
22
 
21
23
  const require = createRequire(import.meta.url);
22
24
  const __filename = fileURLToPath(import.meta.url);
23
25
  const __dirname = path.dirname(__filename);
24
26
 
25
- export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {}): Promise<[Configuration, Configuration]> {
27
+ export async function getRspackConfig(
28
+ appRoot: string,
29
+ config: AnaemiaConfig = {},
30
+ ): Promise<[Configuration, Configuration]> {
26
31
  const isDev = process.env.NODE_ENV !== "production";
27
- const rawEnv = process.env;
32
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
33
+
28
34
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
29
35
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
30
36
 
@@ -37,21 +43,34 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
37
43
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
38
44
  }
39
45
 
46
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
40
47
  const entryFile = generateRouterEntry(appRoot, routes);
41
48
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
49
+
42
50
  const styleRules = createStyleRules(config);
51
+ const assetRules = createAssetRules(isDev);
52
+
53
+ // 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
54
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
44
55
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
45
56
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
46
57
 
47
- // env processing
58
+ /**
59
+ * env processing:
60
+ *
61
+ * - we inject some default env vars like MODE, DEV, and PROD for convenience
62
+ * - for the server, we expose all env vars
63
+ * - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
64
+ * - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
65
+ */
48
66
  const serverEnv: Record<string, string> = {
49
67
  MODE: JSON.stringify(process.env.NODE_ENV || "development"),
50
68
  DEV: JSON.stringify(isDev),
51
69
  PROD: JSON.stringify(!isDev),
52
70
  };
53
- for (const key in rawEnv) {
54
- serverEnv[key] = JSON.stringify(rawEnv[key]);
71
+
72
+ for (const key in process.env) {
73
+ serverEnv[key] = JSON.stringify(process.env[key]);
55
74
  }
56
75
 
57
76
  const clientEnv: Record<string, string> = {
@@ -59,18 +78,22 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
59
78
  DEV: JSON.stringify(isDev),
60
79
  PROD: JSON.stringify(!isDev),
61
80
  };
62
- for (const key in rawEnv) {
81
+
82
+ for (const key in process.env) {
63
83
  if (key.startsWith("PUBLIC_")) {
64
- clientEnv[key] = JSON.stringify(rawEnv[key]);
84
+ clientEnv[key] = JSON.stringify(process.env[key]);
65
85
  }
66
86
  }
67
87
 
88
+ // shared resolve config
89
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
68
90
  const sharedResolve = {
69
91
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
70
92
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
71
93
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
72
94
  };
73
95
 
96
+ // client and server configurations
74
97
  let clientConfig: Configuration = {
75
98
  name: "client",
76
99
  context: appRoot,
@@ -95,9 +118,19 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
95
118
  alias: {
96
119
  ...sharedResolve.alias,
97
120
  "solid-refresh": require.resolve("solid-refresh"),
98
- [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
121
+ [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(
122
+ coreRuntimeDir,
123
+ "./dist/runtime/context.browser.js",
124
+ ),
125
+ },
126
+ fallback: {
127
+ async_hooks: false,
128
+ "node:async_hooks": false,
129
+ fs: false,
130
+ "node:fs": false,
131
+ path: false,
132
+ "node:path": false,
99
133
  },
100
- fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
101
134
  },
102
135
  devServer: isDev
103
136
  ? {
@@ -110,9 +143,17 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
110
143
  }
111
144
  : undefined,
112
145
  plugins: [
113
- new rspack.HtmlRspackPlugin({ template: path.resolve(appRoot, "./index.html"), filename: "index.html", inject: false }),
146
+ new rspack.HtmlRspackPlugin({
147
+ template: path.resolve(appRoot, "./index.html"),
148
+ filename: "index.html",
149
+ inject: false,
150
+ }),
114
151
  new rspack.DefinePlugin({
115
- __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({ port: config.port, assets: config.assets, styles: config.styles }),
152
+ __ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
153
+ port: config.port,
154
+ assets: config.assets,
155
+ styles: config.styles,
156
+ }),
116
157
  ...config.define?.client,
117
158
  "import.meta.env": clientEnv,
118
159
  }),
@@ -126,7 +167,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
126
167
  if (fs.existsSync(srcPath)) return srcPath;
127
168
 
128
169
  return path.resolve(__dirname, "../src/runtime/empty-module.cjs");
129
- })()
170
+ })(),
130
171
  ),
131
172
  new AnaemiaManifestHydrationPlugin({ appRoot }),
132
173
  ],
@@ -134,8 +175,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
134
175
  parser: { "css/auto": { namedExports: false } },
135
176
  rules: [
136
177
  styleRules.client,
178
+ ...assetRules.client,
137
179
  {
138
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
180
+ ...createBabelRule({
181
+ isServer: false,
182
+ isDev,
183
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
184
+ }),
139
185
  exclude: (modulePath: string) => {
140
186
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
141
187
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -152,7 +198,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
152
198
  context: appRoot,
153
199
  target: "node",
154
200
  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" },
201
+ output: {
202
+ path: path.resolve(appRoot, "./dist/server"),
203
+ filename: "index.js",
204
+ module: true,
205
+ chunkFormat: "module",
206
+ chunkLoading: "import",
207
+ },
156
208
  optimization: { nodeEnv: false },
157
209
  resolve: {
158
210
  ...sharedResolve,
@@ -171,8 +223,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
171
223
  parser: { "css/auto": { namedExports: false } },
172
224
  rules: [
173
225
  styleRules.server,
226
+ ...assetRules.server,
174
227
  {
175
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
228
+ ...createBabelRule({
229
+ isServer: true,
230
+ isDev,
231
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
232
+ }),
176
233
  exclude: (modulePath: string) => {
177
234
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
178
235
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -183,6 +240,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
183
240
  },
184
241
  };
185
242
 
243
+ // allow plugins to modify the client and server configurations before they are returned
186
244
  for (const plugin of config.plugins ?? []) {
187
245
  if (plugin.clientRspackConfig) clientConfig = plugin.clientRspackConfig(clientConfig);
188
246
  if (plugin.serverRspackConfig) serverConfig = plugin.serverRspackConfig(serverConfig);
@@ -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,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
 
5
5
  interface BuildManifest {
@@ -22,9 +22,7 @@ export function writeManifest(appRoot: string, routes: RouteManifestEntry[]): vo
22
22
  }
23
23
  }
24
24
 
25
- const conventionalRoutes = routes.filter(
26
- (r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx")
27
- );
25
+ const conventionalRoutes = routes.filter((r) => !r.filePath.endsWith("404.tsx") && !r.filePath.endsWith("500.tsx"));
28
26
 
29
27
  const manifest: BuildManifest = {
30
28
  routes: conventionalRoutes,
@@ -35,8 +33,5 @@ export function writeManifest(appRoot: string, routes: RouteManifestEntry[]): vo
35
33
 
36
34
  const outDir = path.resolve(appRoot, "./dist");
37
35
  fs.mkdirSync(outDir, { recursive: true });
38
- fs.writeFileSync(
39
- path.resolve(outDir, "route-manifest.json"),
40
- JSON.stringify(manifest, null, 2)
41
- );
42
- }
36
+ fs.writeFileSync(path.resolve(outDir, "route-manifest.json"), JSON.stringify(manifest, null, 2));
37
+ }
@@ -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
+ }
@@ -9,7 +9,10 @@ async function createTmpProject(isTs = true) {
9
9
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-bundler-test-"));
10
10
 
11
11
  fs.mkdirSync(path.join(dir, "src/routes"), { recursive: true });
12
- fs.writeFileSync(path.join(dir, "src/routes/index.tsx"), `export default function Index() { return <div>hello</div>; }`);
12
+ fs.writeFileSync(
13
+ path.join(dir, "src/routes/index.tsx"),
14
+ `export default function Index() { return <div>hello</div>; }`,
15
+ );
13
16
  fs.writeFileSync(path.join(dir, "index.html"), `<html><body><div anaemia-entry></div></body></html>`);
14
17
 
15
18
  if (isTs) {
@@ -106,4 +109,4 @@ test("server config aliases point to dist not src", async () => {
106
109
  } finally {
107
110
  fs.rmSync(dir, { recursive: true, force: true });
108
111
  }
109
- });
112
+ });
@@ -15,12 +15,14 @@ const source = `
15
15
  `;
16
16
 
17
17
  function transform(plugin) {
18
- return transformSync(source, {
19
- filename,
20
- plugins: [plugin],
21
- configFile: false,
22
- babelrc: false,
23
- })?.code ?? "";
18
+ return (
19
+ transformSync(source, {
20
+ filename,
21
+ plugins: [plugin],
22
+ configFile: false,
23
+ babelrc: false,
24
+ })?.code ?? ""
25
+ );
24
26
  }
25
27
 
26
28
  test("client and server transforms generate the same server function id", () => {
@@ -42,36 +44,37 @@ test("client transform forwards call arguments to the RPC wrapper", () => {
42
44
  });
43
45
 
44
46
  test("client transform preserves explicit server function ids", () => {
45
- const code = transformSync(
46
- `
47
+ const code =
48
+ transformSync(
49
+ `
47
50
  import { runOnServer } from "@anaemia/core";
48
51
  export const ping = runOnServer(async () => "pong", "custom-id");
49
52
  `,
50
- {
51
- filename,
52
- plugins: [clientServerFnTransform],
53
- configFile: false,
54
- babelrc: false,
55
- }
56
- )?.code ?? "";
53
+ {
54
+ filename,
55
+ plugins: [clientServerFnTransform],
56
+ configFile: false,
57
+ babelrc: false,
58
+ },
59
+ )?.code ?? "";
57
60
 
58
61
  assert.match(code, /\$\$executeClientRpc\("custom-id"\)/);
59
62
  });
60
63
 
61
64
  test("should guarantee server-side logic never leaks to client assets", async () => {
62
65
  const clientAssetDir = path.resolve(process.cwd(), "dist/client/assets");
63
-
66
+
64
67
  if (!fs.existsSync(clientAssetDir)) return;
65
68
 
66
- const files = fs.readdirSync(clientAssetDir).filter(f => f.endsWith(".js"));
69
+ const files = fs.readdirSync(clientAssetDir).filter((f) => f.endsWith(".js"));
67
70
 
68
71
  for (const file of files) {
69
72
  const content = fs.readFileSync(path.join(clientAssetDir, file), "utf-8");
70
-
73
+
71
74
  assert.equal(
72
- content.includes("SELECT * FROM users"),
73
- false,
74
- `CRITICAL SECURITY LEAK: server logic found inside client asset: ${file}`
75
+ content.includes("SELECT * FROM users"),
76
+ false,
77
+ `CRITICAL SECURITY LEAK: server logic found inside client asset: ${file}`,
75
78
  );
76
79
  }
77
- });
80
+ });
package/tsconfig.json CHANGED
@@ -10,4 +10,4 @@
10
10
  "paths": {}
11
11
  },
12
12
  "include": ["src/**/*"]
13
- }
13
+ }