@anaemia/bundler 0.4.0 → 0.5.1
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
|
@@ -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 {};
|
package/dist/env-loader.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
export default function loadEnvFiles(appRoot, mode) {
|
|
5
5
|
const files = [`.env`, `.env.local`, `.env.${mode}`, `.env.${mode}.local`];
|
|
6
6
|
for (const file of files) {
|
|
7
|
-
const result = loadDotenv({ path: path.resolve(appRoot, file), override: true });
|
|
7
|
+
const result = loadDotenv({ path: path.resolve(appRoot, file), override: true, quiet: true });
|
|
8
8
|
expandDotenv(result);
|
|
9
9
|
}
|
|
10
10
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ 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,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAIlD,OAAO,KAAK,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;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"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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";
|
|
6
|
+
import pc from "picocolors";
|
|
6
7
|
import clientServerFnTransform from "./plugins/babel-transform-server.js";
|
|
7
8
|
import serverHashInjector from "./plugins/babel-hash-injector-server.js";
|
|
8
9
|
import { AnaemiaManifestHydrationPlugin } from "./plugins/rspack-manifest-hydration.js";
|
|
@@ -14,17 +15,43 @@ import { getAliases } from "./aliases.js";
|
|
|
14
15
|
import { createStyleRules, createBabelRule, createAssetRules } from "./rules.js";
|
|
15
16
|
import { getClientOptimization, getPerformanceProfile } from "./optimization.js";
|
|
16
17
|
import loadEnvFiles from "./env-loader.js";
|
|
18
|
+
import { analyzeApp } from "./analyzer/index.js";
|
|
17
19
|
const require = createRequire(import.meta.url);
|
|
18
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
21
|
const __dirname = path.dirname(__filename);
|
|
20
22
|
export async function getRspackConfig(appRoot, config = {}) {
|
|
21
23
|
const isDev = process.env.NODE_ENV !== "production";
|
|
22
24
|
loadEnvFiles(appRoot, process.env.NODE_ENV || "development");
|
|
25
|
+
// run the analyzer to collect route metadata and other information about the app that we can use to optimize the build
|
|
26
|
+
const analysis = await analyzeApp(appRoot, {
|
|
27
|
+
mode: isDev ? "development" : "production",
|
|
28
|
+
});
|
|
29
|
+
// flush diagnostics to console
|
|
30
|
+
const tag = pc.dim("[anaemia-analyzer]");
|
|
31
|
+
for (const diagnostic of analysis.diagnostics) {
|
|
32
|
+
const prefix = diagnostic.severity === "error"
|
|
33
|
+
? pc.red("✖ [error]")
|
|
34
|
+
: diagnostic.severity === "warning"
|
|
35
|
+
? pc.yellow("⚠ [warning]")
|
|
36
|
+
: pc.cyan("› [info]");
|
|
37
|
+
const loc = diagnostic.line ? pc.dim(`:${diagnostic.line}`) : "";
|
|
38
|
+
const file = pc.bold(diagnostic.filePath);
|
|
39
|
+
const msg = diagnostic.severity === "error"
|
|
40
|
+
? pc.red(diagnostic.message)
|
|
41
|
+
: diagnostic.severity === "warning"
|
|
42
|
+
? pc.yellow(diagnostic.message)
|
|
43
|
+
: diagnostic.message;
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.log(`${tag} ${prefix} ${file}${loc} - ${msg}`);
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
if (diagnostic.help)
|
|
48
|
+
console.log(` ${pc.dim(`hint: ${diagnostic.help}`)}`);
|
|
49
|
+
}
|
|
23
50
|
const coreRuntimeDir = path.dirname(require.resolve("@anaemia/core/package.json"));
|
|
24
51
|
const runtimeDir = path.resolve(coreRuntimeDir, "./dist/runtime");
|
|
25
52
|
const routes = await scanRoutes(appRoot);
|
|
26
53
|
const serverRoutes = scanServerRoutes(appRoot);
|
|
27
|
-
writeManifest(appRoot, routes);
|
|
54
|
+
writeManifest(appRoot, routes, analysis.routeMetadata);
|
|
28
55
|
const frameworkInternalDir = path.resolve(appRoot, "./.anaemia");
|
|
29
56
|
if (!fs.existsSync(frameworkInternalDir)) {
|
|
30
57
|
fs.mkdirSync(frameworkInternalDir, { recursive: true });
|
|
@@ -225,3 +252,4 @@ export async function getRspackConfig(appRoot, config = {}) {
|
|
|
225
252
|
}
|
|
226
253
|
export { scanRoutes } from "./router/scan.js";
|
|
227
254
|
export { writeManifest } from "./router/manifest.js";
|
|
255
|
+
export { analyzeApp, collectAnalyzerFiles, parseAnalyzerFile, walkAst } from "./analyzer/index.js";
|