@anaemia/bundler 0.4.0 → 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/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.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/router/manifest.d.ts +3 -2
- package/dist/router/manifest.d.ts.map +1 -1
- package/dist/router/manifest.js +18 -18
- package/package.json +8 -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 +1 -1
- package/src/index.ts +43 -1
- package/src/router/manifest.ts +21 -30
- package/test/analyzer.test.mjs +308 -0
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import type { AnaemiaConfig } from "@anaemia/core/config";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import pc from "picocolors";
|
|
8
9
|
|
|
9
10
|
import clientServerFnTransform from "./plugins/babel-transform-server.js";
|
|
10
11
|
import serverHashInjector from "./plugins/babel-hash-injector-server.js";
|
|
@@ -20,6 +21,8 @@ import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js"
|
|
|
20
21
|
import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
|
|
21
22
|
import loadEnvFiles from "./env-loader.js";
|
|
22
23
|
|
|
24
|
+
import { analyzeApp } from "./analyzer/index.js";
|
|
25
|
+
|
|
23
26
|
const require = createRequire(import.meta.url);
|
|
24
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
28
|
const __dirname = path.dirname(__filename);
|
|
@@ -31,12 +34,43 @@ export async function getRspackConfig(
|
|
|
31
34
|
const isDev = process.env.NODE_ENV !== "production";
|
|
32
35
|
loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
|
|
33
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
|
+
|
|
34
68
|
const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
|
|
35
69
|
const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
|
|
36
70
|
|
|
37
71
|
const routes = await scanRoutes(appRoot);
|
|
38
72
|
const serverRoutes = scanServerRoutes(appRoot);
|
|
39
|
-
writeManifest(appRoot, routes);
|
|
73
|
+
writeManifest(appRoot, routes, analysis.routeMetadata);
|
|
40
74
|
|
|
41
75
|
const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
|
|
42
76
|
if (!fs.existsSync(frameworkInternalDir)) {
|
|
@@ -251,3 +285,11 @@ export async function getRspackConfig(
|
|
|
251
285
|
|
|
252
286
|
export { scanRoutes } from "./router/scan.js";
|
|
253
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/router/manifest.ts
CHANGED
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { RouteManifestEntry } from "
|
|
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
|
-
|
|
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: {},
|
|
32
21
|
};
|
|
33
22
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
fs.
|
|
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));
|
|
37
28
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { analyzeApp, walkAst } from "../dist/analyzer/index.js";
|
|
8
|
+
|
|
9
|
+
function createTmpProject() {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-analyzer-test-"));
|
|
11
|
+
fs.mkdirSync(path.join(dir, "src/routes/users/[id]"), { recursive: true });
|
|
12
|
+
fs.mkdirSync(path.join(dir, "src/features/auth/components"), { recursive: true });
|
|
13
|
+
fs.mkdirSync(path.join(dir, "src/shared/utils"), { recursive: true });
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(dir, "anaemia.config.ts"),
|
|
17
|
+
`
|
|
18
|
+
import { defineConfig } from "@anaemia/core";
|
|
19
|
+
export default defineConfig({ port: 3005 });
|
|
20
|
+
`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(path.join(dir, "src/root.tsx"), `export default function Root(props) { return props.children; }`);
|
|
24
|
+
|
|
25
|
+
// route with PUBLIC_ env - valid
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(dir, "src/routes/users/[id]/index.tsx"),
|
|
28
|
+
`export default function UserPage() { return <h1>{import.meta.env.PUBLIC_API_URL}</h1>; }`,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// route with non-PUBLIC_ env - should warn
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(dir, "src/routes/index.tsx"),
|
|
34
|
+
`export default function Home() { return <div>{import.meta.env.SECRET_KEY}</div>; }`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// route with process.env - should warn
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(dir, "src/routes/about.tsx"),
|
|
40
|
+
`export default function About() { return <div>{process.env.API_KEY}</div>; }`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// server route - env access should not warn
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(dir, "src/routes/users/[id]/_route.ts"),
|
|
46
|
+
`export const GET = () => new Response(import.meta.env.SECRET_KEY);`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// .server file - env access should not warn
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(dir, "src/features/auth/components/auth.server.ts"),
|
|
52
|
+
`export const getToken = () => import.meta.env.SECRET_TOKEN;`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// feature component with non-PUBLIC_ env - should warn
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(dir, "src/features/auth/components/Login.tsx"),
|
|
58
|
+
`export function Login() { return <form action={import.meta.env.AUTH_URL} />; }`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// server function definition
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(dir, "src/features/auth/components/actions.server.ts"),
|
|
64
|
+
`
|
|
65
|
+
import { runOnServer } from "@anaemia/core";
|
|
66
|
+
export const getUser = runOnServer(async (id) => ({ id }), "getUser");
|
|
67
|
+
export const unusedFn = runOnServer(async () => {}, "unusedFn");
|
|
68
|
+
`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// only imports getUser, not unusedFn
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(dir, "src/routes/users/[id]/index.tsx"),
|
|
74
|
+
`
|
|
75
|
+
import { getUser } from "../../features/auth/components/actions.server";
|
|
76
|
+
export default function UserPage() { return <h1>{import.meta.env.PUBLIC_API_URL}</h1>; }
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return dir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// file classification
|
|
84
|
+
|
|
85
|
+
test("analyzeApp classifies file kinds correctly", async () => {
|
|
86
|
+
const dir = createTmpProject();
|
|
87
|
+
try {
|
|
88
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
89
|
+
const kinds = new Map(result.files.map((f) => [f.relativePath, f.kind]));
|
|
90
|
+
|
|
91
|
+
assert.equal(kinds.get("anaemia.config.ts"), "config");
|
|
92
|
+
assert.equal(kinds.get("src/root.tsx"), "root");
|
|
93
|
+
assert.equal(kinds.get("src/routes/users/[id]/index.tsx"), "route");
|
|
94
|
+
assert.equal(kinds.get("src/routes/users/[id]/_route.ts"), "server-route");
|
|
95
|
+
assert.equal(result.build.mode, "test");
|
|
96
|
+
} finally {
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// defineConfig
|
|
102
|
+
|
|
103
|
+
test("analyzeApp handles defineConfig in anaemia.config.ts without errors", async () => {
|
|
104
|
+
const dir = createTmpProject();
|
|
105
|
+
try {
|
|
106
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
107
|
+
const configFile = result.files.find((f) => f.relativePath === "anaemia.config.ts");
|
|
108
|
+
|
|
109
|
+
assert.ok(configFile, "config file should be present");
|
|
110
|
+
assert.ok(configFile.program !== null, "config file should parse without error");
|
|
111
|
+
|
|
112
|
+
const configErrors = configFile.diagnostics.filter((d) => d.severity === "error");
|
|
113
|
+
assert.equal(configErrors.length, 0, "config file should have no parse errors");
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// env access
|
|
120
|
+
|
|
121
|
+
test("env check: PUBLIC_ prefix passes without warning", async () => {
|
|
122
|
+
const dir = createTmpProject();
|
|
123
|
+
try {
|
|
124
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
125
|
+
const warnings = result.diagnostics.filter(
|
|
126
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("users/[id]/index.tsx"),
|
|
127
|
+
);
|
|
128
|
+
assert.equal(warnings.length, 0);
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("env check: non-PUBLIC_ prefix in route warns", async () => {
|
|
135
|
+
const dir = createTmpProject();
|
|
136
|
+
try {
|
|
137
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
138
|
+
const warnings = result.diagnostics.filter(
|
|
139
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.filePath?.includes("routes/index.tsx"),
|
|
140
|
+
);
|
|
141
|
+
assert.equal(warnings.length, 1);
|
|
142
|
+
assert.ok(warnings[0].message.includes("SECRET_KEY"));
|
|
143
|
+
} finally {
|
|
144
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("env check: non-PUBLIC_ prefix in feature component warns", async () => {
|
|
149
|
+
const dir = createTmpProject();
|
|
150
|
+
try {
|
|
151
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
152
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC" && d.filePath?.includes("Login.tsx"));
|
|
153
|
+
assert.equal(warnings.length, 1);
|
|
154
|
+
assert.ok(warnings[0].message.includes("AUTH_URL"));
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("env check: server route does not warn for non-PUBLIC_ env", async () => {
|
|
161
|
+
const dir = createTmpProject();
|
|
162
|
+
try {
|
|
163
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
164
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("_route.ts"));
|
|
165
|
+
assert.equal(warnings.length, 0);
|
|
166
|
+
} finally {
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("env check: .server.ts file does not warn for non-PUBLIC_ env", async () => {
|
|
172
|
+
const dir = createTmpProject();
|
|
173
|
+
try {
|
|
174
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
175
|
+
const warnings = result.diagnostics.filter(
|
|
176
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("auth.server.ts"),
|
|
177
|
+
);
|
|
178
|
+
assert.equal(warnings.length, 0);
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("env check: process.env usage warns", async () => {
|
|
185
|
+
const dir = createTmpProject();
|
|
186
|
+
try {
|
|
187
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
188
|
+
const warnings = result.diagnostics.filter(
|
|
189
|
+
(d) => d.code === "PROCESS_ENV_ACCESS" && d.filePath?.includes("about.tsx"),
|
|
190
|
+
);
|
|
191
|
+
assert.equal(warnings.length, 1);
|
|
192
|
+
} finally {
|
|
193
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("env check: ALWAYS_SAFE keys do not warn", async () => {
|
|
198
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-analyzer-test-"));
|
|
199
|
+
try {
|
|
200
|
+
fs.mkdirSync(path.join(dir, "src/routes"), { recursive: true });
|
|
201
|
+
fs.writeFileSync(
|
|
202
|
+
path.join(dir, "src/routes/index.tsx"),
|
|
203
|
+
`
|
|
204
|
+
export default function Page() {
|
|
205
|
+
return <div>{import.meta.env.NODE_ENV}{import.meta.env.MODE}{import.meta.env.DEV}{import.meta.env.PROD}</div>;
|
|
206
|
+
}
|
|
207
|
+
`,
|
|
208
|
+
);
|
|
209
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
210
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC");
|
|
211
|
+
assert.equal(warnings.length, 0);
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// unused server functions
|
|
218
|
+
|
|
219
|
+
test("unused server functions: imported function does not warn", async () => {
|
|
220
|
+
const dir = createTmpProject();
|
|
221
|
+
try {
|
|
222
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
223
|
+
const warnings = result.diagnostics.filter(
|
|
224
|
+
(d) => d.code === "UNUSED_SERVER_FUNCTION" && d.message?.includes("getUser"),
|
|
225
|
+
);
|
|
226
|
+
assert.equal(warnings.length, 0);
|
|
227
|
+
} finally {
|
|
228
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("unused server functions: unimported function warns", async () => {
|
|
233
|
+
const dir = createTmpProject();
|
|
234
|
+
try {
|
|
235
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
236
|
+
const warnings = result.diagnostics.filter(
|
|
237
|
+
(d) => d.code === "UNUSED_SERVER_FUNCTION" && d.message?.includes("unusedFn"),
|
|
238
|
+
);
|
|
239
|
+
assert.equal(warnings.length, 1);
|
|
240
|
+
} finally {
|
|
241
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// route metadata
|
|
246
|
+
|
|
247
|
+
test("route metadata: static route detected correctly", async () => {
|
|
248
|
+
const dir = createTmpProject();
|
|
249
|
+
try {
|
|
250
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
251
|
+
const aboutMeta = result.routeMetadata?.find((m) => m.filePath.includes("about.tsx"));
|
|
252
|
+
assert.ok(aboutMeta);
|
|
253
|
+
assert.equal(aboutMeta.isStatic, true);
|
|
254
|
+
assert.equal(aboutMeta.hasLoader, false);
|
|
255
|
+
assert.equal(aboutMeta.hasGuard, false);
|
|
256
|
+
} finally {
|
|
257
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("route metadata: route with server function import is not static", async () => {
|
|
262
|
+
const dir = createTmpProject();
|
|
263
|
+
try {
|
|
264
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
265
|
+
const userMeta = result.routeMetadata?.find((m) => m.filePath.includes("users/[id]/index.tsx"));
|
|
266
|
+
assert.ok(userMeta);
|
|
267
|
+
assert.equal(userMeta.isStatic, false);
|
|
268
|
+
assert.equal(userMeta.hasServerFunctions, true);
|
|
269
|
+
} finally {
|
|
270
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("route metadata: params extracted from file path", async () => {
|
|
275
|
+
const dir = createTmpProject();
|
|
276
|
+
try {
|
|
277
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
278
|
+
const userMeta = result.routeMetadata?.find((m) => m.filePath.includes("users/[id]"));
|
|
279
|
+
assert.ok(userMeta);
|
|
280
|
+
assert.ok(userMeta.params.includes("id"));
|
|
281
|
+
} finally {
|
|
282
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// walkAst
|
|
287
|
+
|
|
288
|
+
test("walkAst visits all expected node types", async () => {
|
|
289
|
+
const dir = createTmpProject();
|
|
290
|
+
try {
|
|
291
|
+
const result = await analyzeApp(dir, { include: ["src/routes/**/*.tsx"] });
|
|
292
|
+
const routeFile = result.files.find((f) => f.relativePath === "src/routes/users/[id]/index.tsx");
|
|
293
|
+
|
|
294
|
+
assert.ok(routeFile);
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
walkAst(routeFile.program, {
|
|
297
|
+
enter(node) {
|
|
298
|
+
seen.add(node.type);
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
assert.ok(seen.has("Program"));
|
|
303
|
+
assert.ok(seen.has("ImportDeclaration"));
|
|
304
|
+
assert.ok(seen.has("ExportDefaultDeclaration"));
|
|
305
|
+
} finally {
|
|
306
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|