@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/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 [
|
|
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
package/dist/aliases.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AstNode } from "./ast-walker.js";
|
|
2
|
+
export declare function prop<T = unknown>(node: AstNode, key: string): T;
|
|
3
|
+
export declare function child(node: AstNode, key: string): AstNode | null;
|
|
4
|
+
export declare function children(node: AstNode, key: string): AstNode[];
|
|
5
|
+
//# sourceMappingURL=ast-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/analyzer/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,wBAAgB,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,CAE/D;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAMhE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,CAO9D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function prop(node, key) {
|
|
2
|
+
return node[key];
|
|
3
|
+
}
|
|
4
|
+
export function child(node, key) {
|
|
5
|
+
const value = node[key];
|
|
6
|
+
if (value && typeof value === "object" && typeof value.type === "string") {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
export function children(node, key) {
|
|
12
|
+
const value = node[key];
|
|
13
|
+
if (!Array.isArray(value))
|
|
14
|
+
return [];
|
|
15
|
+
return value.filter((v) => v !== null && v !== undefined && typeof v === "object" && typeof v.type === "string");
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Program } from "oxc-parser";
|
|
2
|
+
export type AstNode = {
|
|
3
|
+
type: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type WalkController = {
|
|
7
|
+
skip: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type AstWalker = {
|
|
10
|
+
enter?: (node: AstNode, parent: AstNode | null, controller: WalkController) => void;
|
|
11
|
+
leave?: (node: AstNode, parent: AstNode | null) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function walkAst(root: AstNode | Program, walker: AstWalker): void;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=ast-walker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-walker.d.ts","sourceRoot":"","sources":["../../src/analyzer/ast-walker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IACpF,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;CACzD,CAAC;AAkBF,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA6BxE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { visitorKeys } from "oxc-parser";
|
|
2
|
+
function isAstNode(value) {
|
|
3
|
+
return Boolean(value && typeof value === "object" && typeof value.type === "string");
|
|
4
|
+
}
|
|
5
|
+
function childKeysForNode(node) {
|
|
6
|
+
const keysByNodeType = visitorKeys;
|
|
7
|
+
const configuredKeys = keysByNodeType[node.type];
|
|
8
|
+
if (configuredKeys)
|
|
9
|
+
return configuredKeys;
|
|
10
|
+
return Object.keys(node).filter((key) => {
|
|
11
|
+
if (key === "type" || key === "start" || key === "end" || key === "range" || key === "loc")
|
|
12
|
+
return false;
|
|
13
|
+
const value = node[key];
|
|
14
|
+
return isAstNode(value) || (Array.isArray(value) && value.some(isAstNode));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function walkAst(root, walker) {
|
|
18
|
+
const visit = (node, parent) => {
|
|
19
|
+
const state = { skipped: false };
|
|
20
|
+
const controller = {
|
|
21
|
+
skip() {
|
|
22
|
+
state.skipped = true;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
walker.enter?.(node, parent, controller);
|
|
26
|
+
if (!state.skipped) {
|
|
27
|
+
for (const key of childKeysForNode(node)) {
|
|
28
|
+
const value = node[key];
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const child of value) {
|
|
31
|
+
if (isAstNode(child))
|
|
32
|
+
visit(child, node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (isAstNode(value)) {
|
|
36
|
+
visit(value, node);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
walker.leave?.(node, parent);
|
|
41
|
+
};
|
|
42
|
+
visit(root, null);
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-access.d.ts","sourceRoot":"","sources":["../../../src/analyzer/checks/env-access.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAe1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,EAAE,CA0D7E"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { walkAst } from "../ast-walker.js";
|
|
2
|
+
import { prop, child } from "../ast-utils.js";
|
|
3
|
+
const SERVER_FILE_PATTERNS = [/\.server\.(ts|tsx|js|jsx)$/, /server\//, /\/api\//, /_route\.(ts|tsx|js|jsx)$/];
|
|
4
|
+
function isClientFile(filePath) {
|
|
5
|
+
return !SERVER_FILE_PATTERNS.some((p) => p.test(filePath));
|
|
6
|
+
}
|
|
7
|
+
const ALWAYS_SAFE = new Set(["NODE_ENV", "MODE", "DEV", "PROD"]);
|
|
8
|
+
function startLine(node) {
|
|
9
|
+
const loc = prop(node, "loc");
|
|
10
|
+
return loc?.start.line;
|
|
11
|
+
}
|
|
12
|
+
export function checkEnvAccess(file) {
|
|
13
|
+
if (!file.program || !isClientFile(file.relativePath))
|
|
14
|
+
return [];
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
walkAst(file.program, {
|
|
17
|
+
enter(node) {
|
|
18
|
+
if (node.type !== "MemberExpression")
|
|
19
|
+
return;
|
|
20
|
+
const obj = child(node, "object");
|
|
21
|
+
const propNode = child(node, "property");
|
|
22
|
+
if (!obj || !propNode)
|
|
23
|
+
return;
|
|
24
|
+
// process.env
|
|
25
|
+
if (obj.type === "Identifier" &&
|
|
26
|
+
prop(obj, "name") === "process" &&
|
|
27
|
+
propNode.type === "Identifier" &&
|
|
28
|
+
prop(propNode, "name") === "env") {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
code: "PROCESS_ENV_ACCESS",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: "using process.env is not recommended in anaemia apps. Use import.meta.env instead.",
|
|
33
|
+
filePath: file.relativePath,
|
|
34
|
+
line: startLine(node),
|
|
35
|
+
help: "replace process.env with import.meta.env and ensure client variables are prefixed with PUBLIC_",
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// import.meta.env.KEY
|
|
40
|
+
if (obj.type === "MemberExpression") {
|
|
41
|
+
const innerObj = child(obj, "object");
|
|
42
|
+
const innerProp = child(obj, "property");
|
|
43
|
+
if (innerObj?.type === "MetaProperty" &&
|
|
44
|
+
innerProp?.type === "Identifier" &&
|
|
45
|
+
prop(innerProp, "name") === "env" &&
|
|
46
|
+
propNode.type === "Identifier") {
|
|
47
|
+
const envKey = prop(propNode, "name");
|
|
48
|
+
if (!envKey.startsWith("PUBLIC_") && !ALWAYS_SAFE.has(envKey)) {
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
code: "ENV_NOT_PUBLIC",
|
|
51
|
+
severity: "warning",
|
|
52
|
+
message: `import.meta.env.${envKey} is not prefixed with PUBLIC_ and will be undefined on the client`,
|
|
53
|
+
filePath: file.relativePath,
|
|
54
|
+
line: startLine(node),
|
|
55
|
+
help: `rename to PUBLIC_${envKey} or move this code to a .server.ts file`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return diagnostics;
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ParsedAnalyzerFile } from "../types.js";
|
|
2
|
+
export interface RouteMetadata {
|
|
3
|
+
filePath: string;
|
|
4
|
+
hasServerFunctions: boolean;
|
|
5
|
+
hasLoader: boolean;
|
|
6
|
+
hasGuard: boolean;
|
|
7
|
+
isStatic: boolean;
|
|
8
|
+
serverFunctionIds: string[];
|
|
9
|
+
params: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function extractRouteMetadata(file: ParsedAnalyzerFile): RouteMetadata;
|
|
12
|
+
//# sourceMappingURL=route-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-metadata.d.ts","sourceRoot":"","sources":["../../../src/analyzer/checks/route-metadata.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,aAAa,CAuE5E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { walkAst } from "../ast-walker.js";
|
|
2
|
+
import { prop, child, children } from "../ast-utils.js";
|
|
3
|
+
export function extractRouteMetadata(file) {
|
|
4
|
+
if (!file.program) {
|
|
5
|
+
return {
|
|
6
|
+
filePath: file.relativePath,
|
|
7
|
+
hasServerFunctions: false,
|
|
8
|
+
hasLoader: false,
|
|
9
|
+
hasGuard: false,
|
|
10
|
+
isStatic: true,
|
|
11
|
+
serverFunctionIds: [],
|
|
12
|
+
params: extractParamsFromPath(file.relativePath),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
let hasServerFunctions = false;
|
|
16
|
+
let hasLoader = false;
|
|
17
|
+
let hasGuard = false;
|
|
18
|
+
const serverFunctionIds = [];
|
|
19
|
+
walkAst(file.program, {
|
|
20
|
+
enter(node) {
|
|
21
|
+
if (node.type === "ImportDeclaration") {
|
|
22
|
+
const source = child(node, "source");
|
|
23
|
+
if (source && /\.server/.test(prop(source, "value"))) {
|
|
24
|
+
hasServerFunctions = true;
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
29
|
+
const decl = child(node, "declaration");
|
|
30
|
+
if (decl?.type === "VariableDeclaration") {
|
|
31
|
+
for (const d of children(decl, "declarations")) {
|
|
32
|
+
const id = child(d, "id");
|
|
33
|
+
if (!id)
|
|
34
|
+
continue;
|
|
35
|
+
const name = prop(id, "name");
|
|
36
|
+
if (name === "loader")
|
|
37
|
+
hasLoader = true;
|
|
38
|
+
if (name === "guard")
|
|
39
|
+
hasGuard = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (node.type === "CallExpression") {
|
|
45
|
+
const callee = child(node, "callee");
|
|
46
|
+
const args = children(node, "arguments");
|
|
47
|
+
const idArg = args[1];
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
49
|
+
if (!idArg)
|
|
50
|
+
return;
|
|
51
|
+
if (callee?.type === "Identifier" &&
|
|
52
|
+
prop(callee, "name") === "runOnServer" &&
|
|
53
|
+
idArg.type === "Literal") {
|
|
54
|
+
const value = prop(idArg, "value");
|
|
55
|
+
if (typeof value === "string")
|
|
56
|
+
serverFunctionIds.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
filePath: file.relativePath,
|
|
63
|
+
hasServerFunctions,
|
|
64
|
+
hasLoader,
|
|
65
|
+
hasGuard,
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
67
|
+
isStatic: !hasServerFunctions && !hasLoader && !hasGuard,
|
|
68
|
+
serverFunctionIds,
|
|
69
|
+
params: extractParamsFromPath(file.relativePath),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function extractParamsFromPath(filePath) {
|
|
73
|
+
return [...filePath.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1]);
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-functions.d.ts","sourceRoot":"","sources":["../../../src/analyzer/checks/server-functions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA8D1E,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,kBAAkB,EAAE,CAmB5F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { walkAst } from "../ast-walker.js";
|
|
2
|
+
import { prop, child, children } from "../ast-utils.js";
|
|
3
|
+
function collectServerFunctionDefinitions(files) {
|
|
4
|
+
const definitions = new Map();
|
|
5
|
+
for (const file of files) {
|
|
6
|
+
if (!file.program)
|
|
7
|
+
continue;
|
|
8
|
+
walkAst(file.program, {
|
|
9
|
+
enter(node) {
|
|
10
|
+
if (node.type !== "CallExpression")
|
|
11
|
+
return;
|
|
12
|
+
const callee = child(node, "callee");
|
|
13
|
+
const args = children(node, "arguments");
|
|
14
|
+
const idArg = args[1];
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
16
|
+
if (!idArg)
|
|
17
|
+
return;
|
|
18
|
+
if (callee?.type === "Identifier" &&
|
|
19
|
+
prop(callee, "name") === "runOnServer" &&
|
|
20
|
+
idArg.type === "Literal") {
|
|
21
|
+
const value = prop(idArg, "value");
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
definitions.set(value, {
|
|
24
|
+
filePath: file.relativePath,
|
|
25
|
+
line: prop(node, "loc")?.start.line,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return definitions;
|
|
33
|
+
}
|
|
34
|
+
function collectServerFunctionImports(files) {
|
|
35
|
+
const imports = new Set();
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
if (!file.program)
|
|
38
|
+
continue;
|
|
39
|
+
walkAst(file.program, {
|
|
40
|
+
enter(node) {
|
|
41
|
+
if (node.type !== "ImportDeclaration")
|
|
42
|
+
return;
|
|
43
|
+
const source = child(node, "source");
|
|
44
|
+
if (!source || !/\.server/.test(prop(source, "value")))
|
|
45
|
+
return;
|
|
46
|
+
for (const specifier of children(node, "specifiers")) {
|
|
47
|
+
if (specifier.type === "ImportSpecifier") {
|
|
48
|
+
const local = child(specifier, "local");
|
|
49
|
+
if (local)
|
|
50
|
+
imports.add(prop(local, "name"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return imports;
|
|
57
|
+
}
|
|
58
|
+
export function checkUnusedServerFunctions(files) {
|
|
59
|
+
const definitions = collectServerFunctionDefinitions(files);
|
|
60
|
+
const imports = collectServerFunctionImports(files);
|
|
61
|
+
const diagnostics = [];
|
|
62
|
+
for (const [id, { filePath, line }] of definitions) {
|
|
63
|
+
if (!imports.has(id)) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
code: "UNUSED_SERVER_FUNCTION",
|
|
66
|
+
severity: "info",
|
|
67
|
+
message: `server function "${id}" is defined but never imported`,
|
|
68
|
+
filePath,
|
|
69
|
+
line,
|
|
70
|
+
help: `remove it or check if it's being imported under a different name`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return diagnostics;
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnalyzeAppOptions, AnalyzerResult } from "./types.js";
|
|
2
|
+
export declare function collectAnalyzerFiles(appRoot: string, include?: string[]): string[];
|
|
3
|
+
export declare function analyzeApp(appRoot: string, options?: AnalyzeAppOptions): Promise<AnalyzerResult>;
|
|
4
|
+
export { parseAnalyzerFile } from "./parser.js";
|
|
5
|
+
export { walkAst } from "./ast-walker.js";
|
|
6
|
+
export type { AnalyzeAppOptions, AnalyzerDiagnostic, AnalyzerFileKind, AnalyzerResult, ParsedAnalyzerFile, } from "./types.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analyzer/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAgBpE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,WAA4B,GAAG,MAAM,EAAE,CAYnG;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyB1G;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { parseAnalyzerFile } from "./parser.js";
|
|
5
|
+
import { checkUnusedServerFunctions } from "./checks/server-functions.js";
|
|
6
|
+
import { extractRouteMetadata } from "./checks/route-metadata.js";
|
|
7
|
+
const DEFAULT_ANALYZER_PATTERNS = [
|
|
8
|
+
"anaemia.config.{ts,js,mjs,cjs}",
|
|
9
|
+
"src/root.{tsx,jsx}",
|
|
10
|
+
"src/routes/**/*.{ts,tsx,js,jsx}",
|
|
11
|
+
"src/**/*.{ts,tsx,js,jsx}",
|
|
12
|
+
];
|
|
13
|
+
function uniqueFilePaths(filePaths) {
|
|
14
|
+
return [...new Set(filePaths.map((filePath) => path.resolve(filePath)))].sort();
|
|
15
|
+
}
|
|
16
|
+
export function collectAnalyzerFiles(appRoot, include = DEFAULT_ANALYZER_PATTERNS) {
|
|
17
|
+
const files = include.flatMap((pattern) => glob.sync(pattern, {
|
|
18
|
+
cwd: appRoot,
|
|
19
|
+
absolute: true,
|
|
20
|
+
nodir: true,
|
|
21
|
+
posix: true,
|
|
22
|
+
ignore: ["node_modules/**", "dist/**", ".anaemia/**"],
|
|
23
|
+
}));
|
|
24
|
+
return uniqueFilePaths(files).filter((filePath) => fs.existsSync(filePath));
|
|
25
|
+
}
|
|
26
|
+
export async function analyzeApp(appRoot, options = {}) {
|
|
27
|
+
const normalizedRoot = path.resolve(appRoot);
|
|
28
|
+
const files = collectAnalyzerFiles(normalizedRoot, options.include);
|
|
29
|
+
const parsedFiles = files.map((filePath) => parseAnalyzerFile(normalizedRoot, filePath));
|
|
30
|
+
// per-file diagnostics
|
|
31
|
+
const diagnostics = parsedFiles.flatMap((file) => file.diagnostics);
|
|
32
|
+
// cross-file diagnostics
|
|
33
|
+
const unusedServerFnDiagnostics = checkUnusedServerFunctions(parsedFiles);
|
|
34
|
+
// route metadata for manifest
|
|
35
|
+
const routeFiles = parsedFiles.filter((f) => /src\/routes\//.test(f.filePath));
|
|
36
|
+
const routeMetadata = routeFiles.map(extractRouteMetadata);
|
|
37
|
+
return {
|
|
38
|
+
appRoot: normalizedRoot,
|
|
39
|
+
build: {
|
|
40
|
+
mode: options.mode ?? process.env.NODE_ENV ?? "development",
|
|
41
|
+
analyzedAt: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
files: parsedFiles,
|
|
44
|
+
diagnostics: [...diagnostics, ...unusedServerFnDiagnostics],
|
|
45
|
+
routeMetadata,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export { parseAnalyzerFile } from "./parser.js";
|
|
49
|
+
export { walkAst } from "./ast-walker.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/analyzer/parser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAwC,kBAAkB,EAA+B,MAAM,YAAY,CAAC;AA8DxH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAoCpH"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseSync } from "oxc-parser";
|
|
4
|
+
import { checkEnvAccess } from "./checks/env-access.js";
|
|
5
|
+
const DEFAULT_PARSE_OPTIONS = {
|
|
6
|
+
sourceType: "module",
|
|
7
|
+
astType: "ts",
|
|
8
|
+
range: true,
|
|
9
|
+
preserveParens: false,
|
|
10
|
+
};
|
|
11
|
+
function offsetToLocation(source, offset) {
|
|
12
|
+
const boundedOffset = Math.max(0, Math.min(offset, source.length));
|
|
13
|
+
let line = 1;
|
|
14
|
+
let column = 1;
|
|
15
|
+
for (let i = 0; i < boundedOffset; i++) {
|
|
16
|
+
if (source.charCodeAt(i) === 10) {
|
|
17
|
+
line++;
|
|
18
|
+
column = 1;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
column++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { line, column };
|
|
25
|
+
}
|
|
26
|
+
function severityFromOxc(error) {
|
|
27
|
+
if (error.severity === "Error")
|
|
28
|
+
return "error";
|
|
29
|
+
if (error.severity === "Warning")
|
|
30
|
+
return "warning";
|
|
31
|
+
return "info";
|
|
32
|
+
}
|
|
33
|
+
function diagnosticFromOxcError(filePath, source, error) {
|
|
34
|
+
const label = error.labels[0];
|
|
35
|
+
const start = label.start;
|
|
36
|
+
const location = typeof start === "number" ? offsetToLocation(source, start) : undefined;
|
|
37
|
+
return {
|
|
38
|
+
code: "parse",
|
|
39
|
+
severity: severityFromOxc(error),
|
|
40
|
+
message: label.message ? `${error.message}: ${label.message}` : error.message,
|
|
41
|
+
filePath,
|
|
42
|
+
start,
|
|
43
|
+
end: label.end,
|
|
44
|
+
line: location?.line,
|
|
45
|
+
column: location?.column,
|
|
46
|
+
help: error.helpMessage ?? undefined,
|
|
47
|
+
codeframe: error.codeframe ?? undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function inferAnalyzerFileKind(appRoot, filePath) {
|
|
51
|
+
const relativePath = path.relative(appRoot, filePath).replace(/\\/g, "/");
|
|
52
|
+
if (/^anaemia\.config\.[cm]?[jt]s$/.test(relativePath))
|
|
53
|
+
return "config";
|
|
54
|
+
if (relativePath === "src/root.tsx" || relativePath === "src/root.jsx")
|
|
55
|
+
return "root";
|
|
56
|
+
if (/^src\/routes\/.*\/?_route\.[jt]sx?$/.test(relativePath))
|
|
57
|
+
return "server-route";
|
|
58
|
+
if (/^src\/routes\/.*\.[jt]sx$/.test(relativePath))
|
|
59
|
+
return "route";
|
|
60
|
+
return "source";
|
|
61
|
+
}
|
|
62
|
+
export function parseAnalyzerFile(appRoot, filePath, options = {}) {
|
|
63
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
64
|
+
const relativePath = path.relative(appRoot, filePath).replace(/\\/g, "/");
|
|
65
|
+
const kind = inferAnalyzerFileKind(appRoot, filePath);
|
|
66
|
+
const parseOptions = { ...DEFAULT_PARSE_OPTIONS, ...options };
|
|
67
|
+
try {
|
|
68
|
+
const result = parseSync(filePath, source, parseOptions);
|
|
69
|
+
const parseDiagnostics = result.errors.map((error) => diagnosticFromOxcError(filePath, source, error));
|
|
70
|
+
const file = {
|
|
71
|
+
filePath,
|
|
72
|
+
relativePath,
|
|
73
|
+
kind,
|
|
74
|
+
source,
|
|
75
|
+
program: result.program,
|
|
76
|
+
comments: result.comments,
|
|
77
|
+
module: result.module,
|
|
78
|
+
diagnostics: [],
|
|
79
|
+
};
|
|
80
|
+
const diagnostics = [...parseDiagnostics, ...checkEnvAccess(file)];
|
|
81
|
+
return { ...file, diagnostics };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
return {
|
|
86
|
+
filePath,
|
|
87
|
+
relativePath,
|
|
88
|
+
kind,
|
|
89
|
+
source,
|
|
90
|
+
program: null,
|
|
91
|
+
comments: [],
|
|
92
|
+
module: null,
|
|
93
|
+
diagnostics: [{ code: "parse:thrown", severity: "error", message, filePath }],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Comment, EcmaScriptModule, OxcError, Program } from "oxc-parser";
|
|
2
|
+
import type { RouteMetadata } from "./checks/route-metadata.js";
|
|
3
|
+
type AnalyzerSeverity = "error" | "warning" | "info";
|
|
4
|
+
export type AnalyzerDiagnostic = {
|
|
5
|
+
code: string;
|
|
6
|
+
severity: AnalyzerSeverity;
|
|
7
|
+
message: string;
|
|
8
|
+
filePath: string;
|
|
9
|
+
start?: number;
|
|
10
|
+
end?: number;
|
|
11
|
+
line?: number;
|
|
12
|
+
column?: number;
|
|
13
|
+
help?: string;
|
|
14
|
+
codeframe?: string;
|
|
15
|
+
};
|
|
16
|
+
export type AnalyzerFileKind = "config" | "route" | "server-route" | "root" | "source";
|
|
17
|
+
export type SourceLocation = {
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
};
|
|
21
|
+
export type ParsedAnalyzerFile = {
|
|
22
|
+
filePath: string;
|
|
23
|
+
relativePath: string;
|
|
24
|
+
kind: AnalyzerFileKind;
|
|
25
|
+
source: string;
|
|
26
|
+
program: Program | null;
|
|
27
|
+
comments: Comment[];
|
|
28
|
+
module: EcmaScriptModule | null;
|
|
29
|
+
diagnostics: AnalyzerDiagnostic[];
|
|
30
|
+
};
|
|
31
|
+
export type AnalyzeAppOptions = {
|
|
32
|
+
mode?: string;
|
|
33
|
+
include?: string[];
|
|
34
|
+
};
|
|
35
|
+
type AnalyzerBuildInfo = {
|
|
36
|
+
mode: string;
|
|
37
|
+
analyzedAt: string;
|
|
38
|
+
};
|
|
39
|
+
export type AnalyzerResult = {
|
|
40
|
+
appRoot: string;
|
|
41
|
+
build: AnalyzerBuildInfo;
|
|
42
|
+
files: ParsedAnalyzerFile[];
|
|
43
|
+
diagnostics: AnalyzerDiagnostic[];
|
|
44
|
+
routeMetadata: RouteMetadata[];
|
|
45
|
+
};
|
|
46
|
+
export type ParserError = OxcError;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/analyzer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,KAAK,gBAAgB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAErD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,aAAa,EAAE,aAAa,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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, quiet: true });
|
|
8
|
+
expandDotenv(result);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
5
5
|
export { writeManifest } from "./router/manifest.js";
|
|
6
|
+
export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
|
|
7
|
+
export type { AnalyzeAppOptions, AnalyzerDiagnostic, AnalyzerFileKind, AnalyzerResult, ParsedAnalyzerFile, } from "./analyzer/index.js";
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,
|
|
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;AAyB1D,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CA2PzC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnG,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GACnB,MAAM,qBAAqB,CAAC"}
|