@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.
Files changed (44) hide show
  1. package/dist/analyzer/ast-utils.d.ts +5 -0
  2. package/dist/analyzer/ast-utils.d.ts.map +1 -0
  3. package/dist/analyzer/ast-utils.js +16 -0
  4. package/dist/analyzer/ast-walker.d.ts +15 -0
  5. package/dist/analyzer/ast-walker.d.ts.map +1 -0
  6. package/dist/analyzer/ast-walker.js +43 -0
  7. package/dist/analyzer/checks/env-access.d.ts +3 -0
  8. package/dist/analyzer/checks/env-access.d.ts.map +1 -0
  9. package/dist/analyzer/checks/env-access.js +63 -0
  10. package/dist/analyzer/checks/route-metadata.d.ts +12 -0
  11. package/dist/analyzer/checks/route-metadata.d.ts.map +1 -0
  12. package/dist/analyzer/checks/route-metadata.js +74 -0
  13. package/dist/analyzer/checks/server-functions.d.ts +3 -0
  14. package/dist/analyzer/checks/server-functions.d.ts.map +1 -0
  15. package/dist/analyzer/checks/server-functions.js +75 -0
  16. package/dist/analyzer/index.d.ts +7 -0
  17. package/dist/analyzer/index.d.ts.map +1 -0
  18. package/dist/analyzer/index.js +49 -0
  19. package/dist/analyzer/parser.d.ts +4 -0
  20. package/dist/analyzer/parser.d.ts.map +1 -0
  21. package/dist/analyzer/parser.js +96 -0
  22. package/dist/analyzer/types.d.ts +48 -0
  23. package/dist/analyzer/types.d.ts.map +1 -0
  24. package/dist/analyzer/types.js +1 -0
  25. package/dist/env-loader.js +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +29 -1
  29. package/dist/router/manifest.d.ts +3 -2
  30. package/dist/router/manifest.d.ts.map +1 -1
  31. package/dist/router/manifest.js +18 -18
  32. package/package.json +8 -2
  33. package/src/analyzer/ast-utils.ts +22 -0
  34. package/src/analyzer/ast-walker.ts +63 -0
  35. package/src/analyzer/checks/env-access.ts +77 -0
  36. package/src/analyzer/checks/route-metadata.ts +91 -0
  37. package/src/analyzer/checks/server-functions.ts +85 -0
  38. package/src/analyzer/index.ts +70 -0
  39. package/src/analyzer/parser.ts +103 -0
  40. package/src/analyzer/types.ts +55 -0
  41. package/src/env-loader.ts +1 -1
  42. package/src/index.ts +43 -1
  43. package/src/router/manifest.ts +21 -30
  44. 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,3 @@
1
+ import type { AnalyzerDiagnostic, ParsedAnalyzerFile } from "../types.js";
2
+ export declare function checkEnvAccess(file: ParsedAnalyzerFile): AnalyzerDiagnostic[];
3
+ //# sourceMappingURL=env-access.d.ts.map
@@ -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,3 @@
1
+ import type { AnalyzerDiagnostic, ParsedAnalyzerFile } from "../types.js";
2
+ export declare function checkUnusedServerFunctions(files: ParsedAnalyzerFile[]): AnalyzerDiagnostic[];
3
+ //# sourceMappingURL=server-functions.d.ts.map
@@ -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,4 @@
1
+ import type { ParserOptions } from "oxc-parser";
2
+ import type { ParsedAnalyzerFile } from "./types.js";
3
+ export declare function parseAnalyzerFile(appRoot: string, filePath: string, options?: ParserOptions): ParsedAnalyzerFile;
4
+ //# sourceMappingURL=parser.d.ts.map
@@ -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 {};
@@ -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
@@ -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;AAsB1D,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CA4NzC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
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";