@anaemia/bundler 0.3.6 → 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 +76 -9
  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 +7 -5
  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 +94 -13
  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 +11 -6
  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,CAgJ1H;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,13 +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";
22
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
21
23
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
22
24
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
23
25
  const routes = await scanRoutes(appRoot);
@@ -27,17 +29,49 @@ export async function getRspackConfig(appRoot, config = {}) {
27
29
  if (!fs.existsSync(frameworkInternalDir)) {
28
30
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
29
31
  }
32
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
30
33
  const entryFile = generateRouterEntry(appRoot, routes);
31
34
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
32
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
33
38
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
34
39
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
35
40
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
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
+ */
49
+ const serverEnv = {
50
+ MODE: JSON.stringify(process.env.NODE_ENV || "development"),
51
+ DEV: JSON.stringify(isDev),
52
+ PROD: JSON.stringify(!isDev),
53
+ };
54
+ for (const key in process.env) {
55
+ serverEnv[key] = JSON.stringify(process.env[key]);
56
+ }
57
+ const clientEnv = {
58
+ MODE: JSON.stringify(process.env.NODE_ENV || "development"),
59
+ DEV: JSON.stringify(isDev),
60
+ PROD: JSON.stringify(!isDev),
61
+ };
62
+ for (const key in process.env) {
63
+ if (key.startsWith("PUBLIC_")) {
64
+ clientEnv[key] = JSON.stringify(process.env[key]);
65
+ }
66
+ }
67
+ // shared resolve config
68
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
36
69
  const sharedResolve = {
37
70
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
38
71
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
39
72
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
40
73
  };
74
+ // client and server configurations
41
75
  let clientConfig = {
42
76
  name: "client",
43
77
  context: appRoot,
@@ -64,7 +98,14 @@ export async function getRspackConfig(appRoot, config = {}) {
64
98
  "solid-refresh": require.resolve("solid-refresh"),
65
99
  [path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(coreRuntimeDir, "./dist/runtime/context.browser.js"),
66
100
  },
67
- 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
+ },
68
109
  },
69
110
  devServer: isDev
70
111
  ? {
@@ -77,10 +118,19 @@ export async function getRspackConfig(appRoot, config = {}) {
77
118
  }
78
119
  : undefined,
79
120
  plugins: [
80
- 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
+ }),
81
126
  new rspack.DefinePlugin({
82
- __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
+ }),
83
132
  ...config.define?.client,
133
+ "import.meta.env": clientEnv,
84
134
  }),
85
135
  new rspack.NormalModuleReplacementPlugin(/^node:/, (resource) => {
86
136
  resource.request = resource.request.replace(/^node:/, "");
@@ -97,8 +147,13 @@ export async function getRspackConfig(appRoot, config = {}) {
97
147
  parser: { "css/auto": { namedExports: false } },
98
148
  rules: [
99
149
  styleRules.client,
150
+ ...assetRules.client,
100
151
  {
101
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
152
+ ...createBabelRule({
153
+ isServer: false,
154
+ isDev,
155
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
156
+ }),
102
157
  exclude: (modulePath) => {
103
158
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
104
159
  return false;
@@ -116,7 +171,13 @@ export async function getRspackConfig(appRoot, config = {}) {
116
171
  context: appRoot,
117
172
  target: "node",
118
173
  entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
119
- 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
+ },
120
181
  optimization: { nodeEnv: false },
121
182
  resolve: {
122
183
  ...sharedResolve,
@@ -130,13 +191,18 @@ export async function getRspackConfig(appRoot, config = {}) {
130
191
  __anaemia_server_routes__: serverRoutesFile,
131
192
  },
132
193
  },
133
- plugins: [new rspack.DefinePlugin({ ...config.define?.server })],
194
+ plugins: [new rspack.DefinePlugin({ ...config.define?.server, "import.meta.env": serverEnv })],
134
195
  module: {
135
196
  parser: { "css/auto": { namedExports: false } },
136
197
  rules: [
137
198
  styleRules.server,
199
+ ...assetRules.server,
138
200
  {
139
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
201
+ ...createBabelRule({
202
+ isServer: true,
203
+ isDev,
204
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
205
+ }),
140
206
  exclude: (modulePath) => {
141
207
  if (modulePath.includes("@anaemia") && modulePath.includes("core"))
142
208
  return false;
@@ -148,6 +214,7 @@ export async function getRspackConfig(appRoot, config = {}) {
148
214
  ],
149
215
  },
150
216
  };
217
+ // allow plugins to modify the client and server configurations before they are returned
151
218
  for (const plugin of config.plugins ?? []) {
152
219
  if (plugin.clientRspackConfig)
153
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,CAYpE;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";
@@ -14,10 +14,11 @@ const CATCH_ALL_FILE = /^\[\.\.\.(.+?)\]\.(tsx|jsx)$/;
14
14
  const DYNAMIC_SEGMENT = /^\[(.+?)\]\.(tsx|jsx)$/;
15
15
  export function scanServerRoutes(appRoot) {
16
16
  const routesDir = path.resolve(appRoot, "./src/routes");
17
- const files = glob.sync("**/_route.{ts,tsx}", { cwd: routesDir, posix: true });
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 urlPattern = dir === "." ? "/" : `/${dir}`;
20
+ const normalizedDir = dir.replace(/\[\.\.\.(.+?)\]/g, "*").replace(/\[(.+?)\]/g, ":$1");
21
+ const urlPattern = normalizedDir === "." ? "/" : `/${normalizedDir}`;
21
22
  return {
22
23
  urlPattern,
23
24
  filePath: path.resolve(routesDir, file),
@@ -44,7 +45,7 @@ export async function scanRoutes(appRoot) {
44
45
  let layoutGuards = [];
45
46
  try {
46
47
  const layoutModule = (await jiti.import(resolveConfigPath(absolutePath)));
47
- if (layoutModule?.config?.guards) {
48
+ if (layoutModule.config?.guards) {
48
49
  layoutGuards = layoutModule.config.guards;
49
50
  }
50
51
  }
@@ -70,7 +71,7 @@ export async function scanRoutes(appRoot) {
70
71
  let pageGuards = [];
71
72
  try {
72
73
  const pageModule = (await jiti.import(resolveConfigPath(absolutePagePath)));
73
- if (pageModule?.config?.guards) {
74
+ if (pageModule.config?.guards) {
74
75
  pageGuards = pageModule.config.guards;
75
76
  }
76
77
  }
@@ -145,6 +146,7 @@ function parseFilePath(file) {
145
146
  function resolveLayoutChain(dir, layoutMap) {
146
147
  const layouts = [];
147
148
  let current = dir;
149
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
148
150
  while (true) {
149
151
  const layoutEntry = layoutMap.get(current);
150
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.6",
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.6",
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,15 +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";
32
+ loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
33
+
27
34
  const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
28
35
  const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
29
36
 
@@ -36,19 +43,57 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
36
43
  fs.mkdirSync(frameworkInternalDir, { recursive: true });
37
44
  }
38
45
 
46
+ // bootstrap config entries and generate necessary files for the router based on the scanned routes
39
47
  const entryFile = generateRouterEntry(appRoot, routes);
40
48
  const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
49
+
41
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
42
54
  const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
43
55
  const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
44
56
  const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
45
57
 
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
+ */
66
+ const serverEnv: Record<string, string> = {
67
+ MODE: JSON.stringify(process.env.NODE_ENV || "development"),
68
+ DEV: JSON.stringify(isDev),
69
+ PROD: JSON.stringify(!isDev),
70
+ };
71
+
72
+ for (const key in process.env) {
73
+ serverEnv[key] = JSON.stringify(process.env[key]);
74
+ }
75
+
76
+ const clientEnv: Record<string, string> = {
77
+ MODE: JSON.stringify(process.env.NODE_ENV || "development"),
78
+ DEV: JSON.stringify(isDev),
79
+ PROD: JSON.stringify(!isDev),
80
+ };
81
+
82
+ for (const key in process.env) {
83
+ if (key.startsWith("PUBLIC_")) {
84
+ clientEnv[key] = JSON.stringify(process.env[key]);
85
+ }
86
+ }
87
+
88
+ // shared resolve config
89
+ // we set up some shared resolve config between client and server here, like extension aliases and path aliases
46
90
  const sharedResolve = {
47
91
  extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
48
92
  extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
49
93
  alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
50
94
  };
51
95
 
96
+ // client and server configurations
52
97
  let clientConfig: Configuration = {
53
98
  name: "client",
54
99
  context: appRoot,
@@ -73,9 +118,19 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
73
118
  alias: {
74
119
  ...sharedResolve.alias,
75
120
  "solid-refresh": require.resolve("solid-refresh"),
76
- [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,
77
133
  },
78
- fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
79
134
  },
80
135
  devServer: isDev
81
136
  ? {
@@ -88,10 +143,19 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
88
143
  }
89
144
  : undefined,
90
145
  plugins: [
91
- 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
+ }),
92
151
  new rspack.DefinePlugin({
93
- __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
+ }),
94
157
  ...config.define?.client,
158
+ "import.meta.env": clientEnv,
95
159
  }),
96
160
  new rspack.NormalModuleReplacementPlugin(/^node:/, (resource) => {
97
161
  resource.request = resource.request.replace(/^node:/, "");
@@ -103,7 +167,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
103
167
  if (fs.existsSync(srcPath)) return srcPath;
104
168
 
105
169
  return path.resolve(__dirname, "../src/runtime/empty-module.cjs");
106
- })()
170
+ })(),
107
171
  ),
108
172
  new AnaemiaManifestHydrationPlugin({ appRoot }),
109
173
  ],
@@ -111,8 +175,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
111
175
  parser: { "css/auto": { namedExports: false } },
112
176
  rules: [
113
177
  styleRules.client,
178
+ ...assetRules.client,
114
179
  {
115
- ...createBabelRule({ isServer: false, isDev, plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins] }),
180
+ ...createBabelRule({
181
+ isServer: false,
182
+ isDev,
183
+ plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
184
+ }),
116
185
  exclude: (modulePath: string) => {
117
186
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
118
187
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -129,7 +198,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
129
198
  context: appRoot,
130
199
  target: "node",
131
200
  entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
132
- 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
+ },
133
208
  optimization: { nodeEnv: false },
134
209
  resolve: {
135
210
  ...sharedResolve,
@@ -143,13 +218,18 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
143
218
  __anaemia_server_routes__: serverRoutesFile,
144
219
  },
145
220
  },
146
- plugins: [new rspack.DefinePlugin({ ...config.define?.server })],
221
+ plugins: [new rspack.DefinePlugin({ ...config.define?.server, "import.meta.env": serverEnv })],
147
222
  module: {
148
223
  parser: { "css/auto": { namedExports: false } },
149
224
  rules: [
150
225
  styleRules.server,
226
+ ...assetRules.server,
151
227
  {
152
- ...createBabelRule({ isServer: true, isDev, plugins: [serverHashInjector, ...extraServerBabelPlugins] }),
228
+ ...createBabelRule({
229
+ isServer: true,
230
+ isDev,
231
+ plugins: [serverHashInjector, ...extraServerBabelPlugins],
232
+ }),
153
233
  exclude: (modulePath: string) => {
154
234
  if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
155
235
  if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
@@ -160,6 +240,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
160
240
  },
161
241
  };
162
242
 
243
+ // allow plugins to modify the client and server configurations before they are returned
163
244
  for (const plugin of config.plugins ?? []) {
164
245
  if (plugin.clientRspackConfig) clientConfig = plugin.clientRspackConfig(clientConfig);
165
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";
@@ -50,11 +50,15 @@ const DYNAMIC_SEGMENT = /^\[(.+?)\]\.(tsx|jsx)$/;
50
50
 
51
51
  export function scanServerRoutes(appRoot: string): ServerRouteEntry[] {
52
52
  const routesDir = path.resolve(appRoot, "./src/routes");
53
- const files = glob.sync("**/_route.{ts,tsx}", { cwd: routesDir, posix: true });
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
- const urlPattern = dir === "." ? "/" : `/${dir}`;
57
+
58
+ const normalizedDir = dir.replace(/\[\.\.\.(.+?)\]/g, "*").replace(/\[(.+?)\]/g, ":$1");
59
+
60
+ const urlPattern = normalizedDir === "." ? "/" : `/${normalizedDir}`;
61
+
58
62
  return {
59
63
  urlPattern,
60
64
  filePath: path.resolve(routesDir, file),
@@ -86,7 +90,7 @@ export async function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]>
86
90
 
87
91
  try {
88
92
  const layoutModule = (await jiti.import(resolveConfigPath(absolutePath))) as RouteModule;
89
- if (layoutModule?.config?.guards) {
93
+ if (layoutModule.config?.guards) {
90
94
  layoutGuards = layoutModule.config.guards;
91
95
  }
92
96
  } catch {
@@ -116,7 +120,7 @@ export async function scanRoutes(appRoot: string): Promise<RouteManifestEntry[]>
116
120
 
117
121
  try {
118
122
  const pageModule = (await jiti.import(resolveConfigPath(absolutePagePath))) as RouteModule;
119
- if (pageModule?.config?.guards) {
123
+ if (pageModule.config?.guards) {
120
124
  pageGuards = pageModule.config.guards;
121
125
  }
122
126
  } catch {
@@ -201,6 +205,7 @@ function resolveLayoutChain(dir: string, layoutMap: Map<string, LayoutManifestEn
201
205
  const layouts: LayoutManifestEntry[] = [];
202
206
  let current = dir;
203
207
 
208
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
204
209
  while (true) {
205
210
  const layoutEntry = layoutMap.get(current);
206
211
  if (layoutEntry) layouts.unshift(layoutEntry);
@@ -209,4 +214,4 @@ function resolveLayoutChain(dir: string, layoutMap: Map<string, LayoutManifestEn
209
214
  }
210
215
 
211
216
  return layouts;
212
- }
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
+ }