@anaemia/bundler 0.3.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/aliases.js +1 -1
- package/dist/analyzer/ast-utils.d.ts +5 -0
- package/dist/analyzer/ast-utils.d.ts.map +1 -0
- package/dist/analyzer/ast-utils.js +16 -0
- package/dist/analyzer/ast-walker.d.ts +15 -0
- package/dist/analyzer/ast-walker.d.ts.map +1 -0
- package/dist/analyzer/ast-walker.js +43 -0
- package/dist/analyzer/checks/env-access.d.ts +3 -0
- package/dist/analyzer/checks/env-access.d.ts.map +1 -0
- package/dist/analyzer/checks/env-access.js +63 -0
- package/dist/analyzer/checks/route-metadata.d.ts +12 -0
- package/dist/analyzer/checks/route-metadata.d.ts.map +1 -0
- package/dist/analyzer/checks/route-metadata.js +74 -0
- package/dist/analyzer/checks/server-functions.d.ts +3 -0
- package/dist/analyzer/checks/server-functions.d.ts.map +1 -0
- package/dist/analyzer/checks/server-functions.js +75 -0
- package/dist/analyzer/index.d.ts +7 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +49 -0
- package/dist/analyzer/parser.d.ts +4 -0
- package/dist/analyzer/parser.d.ts.map +1 -0
- package/dist/analyzer/parser.js +96 -0
- package/dist/analyzer/types.d.ts +48 -0
- package/dist/analyzer/types.d.ts.map +1 -0
- package/dist/analyzer/types.js +1 -0
- package/dist/env-loader.d.ts +2 -0
- package/dist/env-loader.d.ts.map +1 -0
- package/dist/env-loader.js +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -15
- package/dist/optimization.d.ts.map +1 -1
- package/dist/optimization.js +17 -4
- package/dist/plugins/babel-transform-server.d.ts.map +1 -1
- package/dist/plugins/babel-transform-server.js +1 -4
- package/dist/router/generate-entry.d.ts.map +1 -1
- package/dist/router/generate-entry.js +3 -3
- package/dist/router/generate-server-routes.d.ts.map +1 -1
- package/dist/router/generate-server-routes.js +15 -5
- package/dist/router/manifest.d.ts +3 -2
- package/dist/router/manifest.d.ts.map +1 -1
- package/dist/router/manifest.js +20 -20
- package/dist/router/scan.d.ts.map +1 -1
- package/dist/router/scan.js +5 -6
- package/dist/rules.d.ts +16 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +37 -5
- package/package.json +11 -3
- package/src/aliases.ts +2 -2
- package/src/analyzer/ast-utils.ts +22 -0
- package/src/analyzer/ast-walker.ts +63 -0
- package/src/analyzer/checks/env-access.ts +77 -0
- package/src/analyzer/checks/route-metadata.ts +91 -0
- package/src/analyzer/checks/server-functions.ts +85 -0
- package/src/analyzer/index.ts +70 -0
- package/src/analyzer/parser.ts +103 -0
- package/src/analyzer/types.ts +55 -0
- package/src/env-loader.ts +13 -0
- package/src/index.ts +119 -19
- package/src/optimization.ts +18 -5
- package/src/plugins/babel-transform-server.ts +1 -4
- package/src/plugins/rspack-manifest-hydration.ts +3 -3
- package/src/router/generate-entry.ts +15 -6
- package/src/router/generate-server-routes.ts +16 -5
- package/src/router/manifest.ts +24 -38
- package/src/router/scan.ts +9 -10
- package/src/rules.ts +48 -8
- package/test/analyzer.test.mjs +308 -0
- package/test/rspack-config.test.mjs +5 -2
- package/test/server-functions.test.mjs +25 -22
- package/tsconfig.json +1 -1
package/src/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Configuration
|
|
2
|
-
import
|
|
1
|
+
import type { Configuration } from "@rspack/core";
|
|
2
|
+
import { rspack } from "@rspack/core";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import type { AnaemiaConfig } from "@anaemia/core/config";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import pc from "picocolors";
|
|
7
9
|
|
|
8
10
|
import clientServerFnTransform from "./plugins/babel-transform-server.js";
|
|
9
11
|
import serverHashInjector from "./plugins/babel-hash-injector-server.js";
|
|
@@ -15,43 +17,94 @@ import { generateRouterEntry } from "./router/generate-entry.js";
|
|
|
15
17
|
import { generateServerRoutes } from "./router/generate-server-routes.js";
|
|
16
18
|
import { getAliases } from "./aliases.js";
|
|
17
19
|
|
|
18
|
-
import { createStyleRules, createBabelRule } from "./rules.js";
|
|
20
|
+
import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
|
|
19
21
|
import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
|
|
22
|
+
import loadEnvFiles from "./env-loader.js";
|
|
23
|
+
|
|
24
|
+
import { analyzeApp } from "./analyzer/index.js";
|
|
20
25
|
|
|
21
26
|
const require = createRequire(import.meta.url);
|
|
22
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
28
|
const __dirname = path.dirname(__filename);
|
|
24
29
|
|
|
25
|
-
export async function getRspackConfig(
|
|
30
|
+
export async function getRspackConfig(
|
|
31
|
+
appRoot: string,
|
|
32
|
+
config: AnaemiaConfig = {},
|
|
33
|
+
): Promise<[Configuration, Configuration]> {
|
|
26
34
|
const isDev = process.env.NODE_ENV !== "production";
|
|
27
|
-
|
|
35
|
+
loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
|
|
36
|
+
|
|
37
|
+
// run the analyzer to collect route metadata and other information about the app that we can use to optimize the build
|
|
38
|
+
const analysis = await analyzeApp(appRoot, {
|
|
39
|
+
mode: isDev ? "development" : "production",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// flush diagnostics to console
|
|
43
|
+
const tag = pc.dim("[anaemia-analyzer]");
|
|
44
|
+
|
|
45
|
+
for (const diagnostic of analysis.diagnostics) {
|
|
46
|
+
const prefix =
|
|
47
|
+
diagnostic.severity === "error"
|
|
48
|
+
? pc.red("✖ [error]")
|
|
49
|
+
: diagnostic.severity === "warning"
|
|
50
|
+
? pc.yellow("⚠ [warning]")
|
|
51
|
+
: pc.cyan("› [info]");
|
|
52
|
+
|
|
53
|
+
const loc = diagnostic.line ? pc.dim(`:${diagnostic.line}`) : "";
|
|
54
|
+
const file = pc.bold(diagnostic.filePath);
|
|
55
|
+
const msg =
|
|
56
|
+
diagnostic.severity === "error"
|
|
57
|
+
? pc.red(diagnostic.message)
|
|
58
|
+
: diagnostic.severity === "warning"
|
|
59
|
+
? pc.yellow(diagnostic.message)
|
|
60
|
+
: diagnostic.message;
|
|
61
|
+
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.log(`${tag} ${prefix} ${file}${loc} - ${msg}`);
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
if (diagnostic.help) console.log(` ${pc.dim(`hint: ${diagnostic.help}`)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
|
|
29
69
|
const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
|
|
30
70
|
|
|
31
71
|
const routes = await scanRoutes(appRoot);
|
|
32
72
|
const serverRoutes = scanServerRoutes(appRoot);
|
|
33
|
-
writeManifest(appRoot, routes);
|
|
73
|
+
writeManifest(appRoot, routes, analysis.routeMetadata);
|
|
34
74
|
|
|
35
75
|
const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
|
|
36
76
|
if (!fs.existsSync(frameworkInternalDir)) {
|
|
37
77
|
fs.mkdirSync(frameworkInternalDir, { recursive: true });
|
|
38
78
|
}
|
|
39
79
|
|
|
80
|
+
// bootstrap config entries and generate necessary files for the router based on the scanned routes
|
|
40
81
|
const entryFile = generateRouterEntry(appRoot, routes);
|
|
41
82
|
const serverRoutesFile = generateServerRoutes(appRoot, serverRoutes);
|
|
83
|
+
|
|
42
84
|
const styleRules = createStyleRules(config);
|
|
85
|
+
const assetRules = createAssetRules(isDev);
|
|
86
|
+
|
|
87
|
+
// allow users to inject additional babel plugins via the config, which is useful for things like macros or other code transforms that need to run at compile time
|
|
43
88
|
const extraClientBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.client ?? []) ?? [];
|
|
44
89
|
const extraServerBabelPlugins = config.plugins?.flatMap((p) => p.babelPlugins?.server ?? []) ?? [];
|
|
45
90
|
const solidRefreshPlugin = [require.resolve("solid-refresh/babel"), { bundler: "rspack-esm", jsx: false }];
|
|
46
91
|
|
|
47
|
-
|
|
92
|
+
/**
|
|
93
|
+
* env processing:
|
|
94
|
+
*
|
|
95
|
+
* - we inject some default env vars like MODE, DEV, and PROD for convenience
|
|
96
|
+
* - for the server, we expose all env vars
|
|
97
|
+
* - for the client, we only expose vars that start with PUBLIC_, as well as the same defaults
|
|
98
|
+
* - users can also define additional compile-time constants via config.define.client and config.define.server, which are merged into the rspack DefinePlugin config
|
|
99
|
+
*/
|
|
48
100
|
const serverEnv: Record<string, string> = {
|
|
49
101
|
MODE: JSON.stringify(process.env.NODE_ENV || "development"),
|
|
50
102
|
DEV: JSON.stringify(isDev),
|
|
51
103
|
PROD: JSON.stringify(!isDev),
|
|
52
104
|
};
|
|
53
|
-
|
|
54
|
-
|
|
105
|
+
|
|
106
|
+
for (const key in process.env) {
|
|
107
|
+
serverEnv[key] = JSON.stringify(process.env[key]);
|
|
55
108
|
}
|
|
56
109
|
|
|
57
110
|
const clientEnv: Record<string, string> = {
|
|
@@ -59,18 +112,22 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
59
112
|
DEV: JSON.stringify(isDev),
|
|
60
113
|
PROD: JSON.stringify(!isDev),
|
|
61
114
|
};
|
|
62
|
-
|
|
115
|
+
|
|
116
|
+
for (const key in process.env) {
|
|
63
117
|
if (key.startsWith("PUBLIC_")) {
|
|
64
|
-
clientEnv[key] = JSON.stringify(
|
|
118
|
+
clientEnv[key] = JSON.stringify(process.env[key]);
|
|
65
119
|
}
|
|
66
120
|
}
|
|
67
121
|
|
|
122
|
+
// shared resolve config
|
|
123
|
+
// we set up some shared resolve config between client and server here, like extension aliases and path aliases
|
|
68
124
|
const sharedResolve = {
|
|
69
125
|
extensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".scss", ".css"],
|
|
70
126
|
extensionAlias: { ".js": [".ts", ".js"], ".jsx": [".tsx", ".jsx"] },
|
|
71
127
|
alias: { "anaemia-user-app": entryFile, ...getAliases(appRoot) },
|
|
72
128
|
};
|
|
73
129
|
|
|
130
|
+
// client and server configurations
|
|
74
131
|
let clientConfig: Configuration = {
|
|
75
132
|
name: "client",
|
|
76
133
|
context: appRoot,
|
|
@@ -95,9 +152,19 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
95
152
|
alias: {
|
|
96
153
|
...sharedResolve.alias,
|
|
97
154
|
"solid-refresh": require.resolve("solid-refresh"),
|
|
98
|
-
[path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(
|
|
155
|
+
[path.resolve(coreRuntimeDir, "./dist/runtime/context.js")]: path.resolve(
|
|
156
|
+
coreRuntimeDir,
|
|
157
|
+
"./dist/runtime/context.browser.js",
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
fallback: {
|
|
161
|
+
async_hooks: false,
|
|
162
|
+
"node:async_hooks": false,
|
|
163
|
+
fs: false,
|
|
164
|
+
"node:fs": false,
|
|
165
|
+
path: false,
|
|
166
|
+
"node:path": false,
|
|
99
167
|
},
|
|
100
|
-
fallback: { async_hooks: false, "node:async_hooks": false, fs: false, "node:fs": false, path: false, "node:path": false },
|
|
101
168
|
},
|
|
102
169
|
devServer: isDev
|
|
103
170
|
? {
|
|
@@ -110,9 +177,17 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
110
177
|
}
|
|
111
178
|
: undefined,
|
|
112
179
|
plugins: [
|
|
113
|
-
new rspack.HtmlRspackPlugin({
|
|
180
|
+
new rspack.HtmlRspackPlugin({
|
|
181
|
+
template: path.resolve(appRoot, "./index.html"),
|
|
182
|
+
filename: "index.html",
|
|
183
|
+
inject: false,
|
|
184
|
+
}),
|
|
114
185
|
new rspack.DefinePlugin({
|
|
115
|
-
__ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
|
|
186
|
+
__ANAEMIA_RUNTIME_CONFIG__: JSON.stringify({
|
|
187
|
+
port: config.port,
|
|
188
|
+
assets: config.assets,
|
|
189
|
+
styles: config.styles,
|
|
190
|
+
}),
|
|
116
191
|
...config.define?.client,
|
|
117
192
|
"import.meta.env": clientEnv,
|
|
118
193
|
}),
|
|
@@ -126,7 +201,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
126
201
|
if (fs.existsSync(srcPath)) return srcPath;
|
|
127
202
|
|
|
128
203
|
return path.resolve(__dirname, "../src/runtime/empty-module.cjs");
|
|
129
|
-
})()
|
|
204
|
+
})(),
|
|
130
205
|
),
|
|
131
206
|
new AnaemiaManifestHydrationPlugin({ appRoot }),
|
|
132
207
|
],
|
|
@@ -134,8 +209,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
134
209
|
parser: { "css/auto": { namedExports: false } },
|
|
135
210
|
rules: [
|
|
136
211
|
styleRules.client,
|
|
212
|
+
...assetRules.client,
|
|
137
213
|
{
|
|
138
|
-
...createBabelRule({
|
|
214
|
+
...createBabelRule({
|
|
215
|
+
isServer: false,
|
|
216
|
+
isDev,
|
|
217
|
+
plugins: [clientServerFnTransform, ...(isDev ? [solidRefreshPlugin] : []), ...extraClientBabelPlugins],
|
|
218
|
+
}),
|
|
139
219
|
exclude: (modulePath: string) => {
|
|
140
220
|
if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
|
|
141
221
|
if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
|
|
@@ -152,7 +232,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
152
232
|
context: appRoot,
|
|
153
233
|
target: "node",
|
|
154
234
|
entry: { server: path.resolve(runtimeDir, "entry-server.jsx") },
|
|
155
|
-
output: {
|
|
235
|
+
output: {
|
|
236
|
+
path: path.resolve(appRoot, "./dist/server"),
|
|
237
|
+
filename: "index.js",
|
|
238
|
+
module: true,
|
|
239
|
+
chunkFormat: "module",
|
|
240
|
+
chunkLoading: "import",
|
|
241
|
+
},
|
|
156
242
|
optimization: { nodeEnv: false },
|
|
157
243
|
resolve: {
|
|
158
244
|
...sharedResolve,
|
|
@@ -171,8 +257,13 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
171
257
|
parser: { "css/auto": { namedExports: false } },
|
|
172
258
|
rules: [
|
|
173
259
|
styleRules.server,
|
|
260
|
+
...assetRules.server,
|
|
174
261
|
{
|
|
175
|
-
...createBabelRule({
|
|
262
|
+
...createBabelRule({
|
|
263
|
+
isServer: true,
|
|
264
|
+
isDev,
|
|
265
|
+
plugins: [serverHashInjector, ...extraServerBabelPlugins],
|
|
266
|
+
}),
|
|
176
267
|
exclude: (modulePath: string) => {
|
|
177
268
|
if (modulePath.includes("@anaemia") && modulePath.includes("core")) return false;
|
|
178
269
|
if (modulePath.includes("@solidjs") && modulePath.includes("router")) return false;
|
|
@@ -183,6 +274,7 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
183
274
|
},
|
|
184
275
|
};
|
|
185
276
|
|
|
277
|
+
// allow plugins to modify the client and server configurations before they are returned
|
|
186
278
|
for (const plugin of config.plugins ?? []) {
|
|
187
279
|
if (plugin.clientRspackConfig) clientConfig = plugin.clientRspackConfig(clientConfig);
|
|
188
280
|
if (plugin.serverRspackConfig) serverConfig = plugin.serverRspackConfig(serverConfig);
|
|
@@ -193,3 +285,11 @@ export async function getRspackConfig(appRoot: string, config: AnaemiaConfig = {
|
|
|
193
285
|
|
|
194
286
|
export { scanRoutes } from "./router/scan.js";
|
|
195
287
|
export { writeManifest } from "./router/manifest.js";
|
|
288
|
+
export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
|
|
289
|
+
export type {
|
|
290
|
+
AnalyzeAppOptions,
|
|
291
|
+
AnalyzerDiagnostic,
|
|
292
|
+
AnalyzerFileKind,
|
|
293
|
+
AnalyzerResult,
|
|
294
|
+
ParsedAnalyzerFile,
|
|
295
|
+
} from "./analyzer/index.js";
|
package/src/optimization.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { rspack } from "@rspack/core";
|
|
|
2
2
|
|
|
3
3
|
export function getClientOptimization(isDev: boolean) {
|
|
4
4
|
return {
|
|
5
|
-
sideEffects:
|
|
6
|
-
usedExports:
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/router/manifest.ts
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import type { RouteManifestEntry } from "
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { RouteManifestEntry } from "../router/scan.js";
|
|
4
|
+
import type { RouteMetadata } from "../analyzer/checks/route-metadata.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// filled in after rspack build - maps chunkName to hashed filename
|
|
8
|
-
chunks: Record<string, { js: string; css?: string }>;
|
|
9
|
-
errors: Record<string, string>;
|
|
10
|
-
buildTime: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function writeManifest(appRoot: string, routes: RouteManifestEntry[]): void {
|
|
14
|
-
const errors: Record<string, string> = {};
|
|
15
|
-
|
|
16
|
-
for (const route of routes) {
|
|
17
|
-
if (route.filePath.endsWith("404.tsx")) {
|
|
18
|
-
errors["404"] = route.urlPattern;
|
|
19
|
-
}
|
|
20
|
-
if (route.filePath.endsWith("500.tsx")) {
|
|
21
|
-
errors["500"] = route.urlPattern;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
6
|
+
export function writeManifest(appRoot: string, routes: RouteManifestEntry[], routeMetadata: RouteMetadata[]) {
|
|
7
|
+
const metadataMap = new Map(routeMetadata.map((m) => [path.resolve(appRoot, m.filePath), m]));
|
|
24
8
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
9
|
+
const manifest = {
|
|
10
|
+
routes: routes.map((route) => {
|
|
11
|
+
const meta = metadataMap.get(route.filePath);
|
|
12
|
+
return {
|
|
13
|
+
...route,
|
|
14
|
+
isStatic: meta?.isStatic ?? false,
|
|
15
|
+
hasLoader: meta?.hasLoader ?? false,
|
|
16
|
+
hasGuard: meta?.hasGuard ?? false,
|
|
17
|
+
serverFunctionIds: meta?.serverFunctionIds ?? [],
|
|
18
|
+
};
|
|
19
|
+
}),
|
|
20
|
+
chunks: {},
|
|
34
21
|
};
|
|
35
22
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
fs.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
23
|
+
const manifestPath = path.resolve(appRoot, "./dist/route-manifest.json");
|
|
24
|
+
const manifestDir = path.dirname(manifestPath);
|
|
25
|
+
if (!fs.existsSync(manifestDir)) fs.mkdirSync(manifestDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
28
|
+
}
|
package/src/router/scan.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|