@facilis.dev/analyze 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/analyze.d.ts +17 -0
- package/dist/analyze.js +99 -0
- package/dist/ast/node.d.ts +29 -0
- package/dist/ast/node.js +85 -0
- package/dist/ast/walk.d.ts +8 -0
- package/dist/ast/walk.js +40 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +12 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.js +51 -0
- package/dist/graph-edges.d.ts +3 -0
- package/dist/graph-edges.js +148 -0
- package/dist/graph-ids.d.ts +6 -0
- package/dist/graph-ids.js +22 -0
- package/dist/graph.d.ts +15 -0
- package/dist/graph.js +162 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +17 -0
- package/dist/json.d.ts +8 -0
- package/dist/json.js +31 -0
- package/dist/module-graph.d.ts +33 -0
- package/dist/module-graph.js +86 -0
- package/dist/module-info.d.ts +27 -0
- package/dist/module-info.js +143 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +41 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +1 -0
- package/dist/passes/clients.d.ts +22 -0
- package/dist/passes/clients.js +167 -0
- package/dist/passes/config.d.ts +16 -0
- package/dist/passes/config.js +229 -0
- package/dist/passes/file-facts.d.ts +13 -0
- package/dist/passes/file-facts.js +130 -0
- package/dist/passes/i18n.d.ts +14 -0
- package/dist/passes/i18n.js +55 -0
- package/dist/passes/job-relations-diagnostics.d.ts +9 -0
- package/dist/passes/job-relations-diagnostics.js +38 -0
- package/dist/passes/job-relations.d.ts +18 -0
- package/dist/passes/job-relations.js +52 -0
- package/dist/passes/jobs.d.ts +20 -0
- package/dist/passes/jobs.js +133 -0
- package/dist/passes/jsx-text.d.ts +28 -0
- package/dist/passes/jsx-text.js +121 -0
- package/dist/passes/jsx.d.ts +24 -0
- package/dist/passes/jsx.js +135 -0
- package/dist/passes/path-expression.d.ts +26 -0
- package/dist/passes/path-expression.js +265 -0
- package/dist/passes/route-config.d.ts +24 -0
- package/dist/passes/route-config.js +152 -0
- package/dist/passes/route-description.d.ts +13 -0
- package/dist/passes/route-description.js +23 -0
- package/dist/passes/route-elevation.d.ts +18 -0
- package/dist/passes/route-elevation.js +136 -0
- package/dist/passes/route-handler.d.ts +27 -0
- package/dist/passes/route-handler.js +87 -0
- package/dist/passes/route-location.d.ts +16 -0
- package/dist/passes/route-location.js +58 -0
- package/dist/passes/route-relation-resolve.d.ts +51 -0
- package/dist/passes/route-relation-resolve.js +172 -0
- package/dist/passes/route-relations-diagnostics.d.ts +15 -0
- package/dist/passes/route-relations-diagnostics.js +79 -0
- package/dist/passes/route-relations-follow.d.ts +16 -0
- package/dist/passes/route-relations-follow.js +116 -0
- package/dist/passes/route-relations-usage.d.ts +50 -0
- package/dist/passes/route-relations-usage.js +44 -0
- package/dist/passes/route-relations-walk.d.ts +25 -0
- package/dist/passes/route-relations-walk.js +199 -0
- package/dist/passes/route-relations.d.ts +17 -0
- package/dist/passes/route-relations.js +43 -0
- package/dist/passes/route-table.d.ts +21 -0
- package/dist/passes/route-table.js +104 -0
- package/dist/passes/route-tables.d.ts +10 -0
- package/dist/passes/route-tables.js +42 -0
- package/dist/passes/routes.d.ts +18 -0
- package/dist/passes/routes.js +152 -0
- package/dist/passes/schema-column-types.d.ts +9 -0
- package/dist/passes/schema-column-types.js +55 -0
- package/dist/passes/schema-columns.d.ts +35 -0
- package/dist/passes/schema-columns.js +197 -0
- package/dist/passes/schema-dsl.d.ts +24 -0
- package/dist/passes/schema-dsl.js +117 -0
- package/dist/passes/schema-file-links-ddl.d.ts +18 -0
- package/dist/passes/schema-file-links-ddl.js +106 -0
- package/dist/passes/schema-file-links.d.ts +19 -0
- package/dist/passes/schema-file-links.js +144 -0
- package/dist/passes/schema-foreign-keys.d.ts +21 -0
- package/dist/passes/schema-foreign-keys.js +80 -0
- package/dist/passes/schema-policies.d.ts +15 -0
- package/dist/passes/schema-policies.js +201 -0
- package/dist/passes/schema-re-exports.d.ts +5 -0
- package/dist/passes/schema-re-exports.js +77 -0
- package/dist/passes/schema-relations.d.ts +8 -0
- package/dist/passes/schema-relations.js +182 -0
- package/dist/passes/schema-scope.d.ts +7 -0
- package/dist/passes/schema-scope.js +20 -0
- package/dist/passes/schema-tables.d.ts +13 -0
- package/dist/passes/schema-tables.js +149 -0
- package/dist/passes/schema.d.ts +9 -0
- package/dist/passes/schema.js +55 -0
- package/dist/passes/screen-components.d.ts +22 -0
- package/dist/passes/screen-components.js +143 -0
- package/dist/passes/screen-intrinsics.d.ts +18 -0
- package/dist/passes/screen-intrinsics.js +21 -0
- package/dist/passes/screen-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +174 -0
- package/dist/passes/screens.d.ts +12 -0
- package/dist/passes/screens.js +278 -0
- package/dist/passes/static-read.d.ts +47 -0
- package/dist/passes/static-read.js +151 -0
- package/dist/passes/views.d.ts +20 -0
- package/dist/passes/views.js +101 -0
- package/dist/passes/workflow-tables.d.ts +18 -0
- package/dist/passes/workflow-tables.js +71 -0
- package/dist/passes/workflows.d.ts +8 -0
- package/dist/passes/workflows.js +160 -0
- package/dist/path-resolver.d.ts +20 -0
- package/dist/path-resolver.js +134 -0
- package/dist/revision.d.ts +2 -0
- package/dist/revision.js +23 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.js +13 -0
- package/dist/static-eval.d.ts +36 -0
- package/dist/static-eval.js +215 -0
- package/dist/types-graph.d.ts +144 -0
- package/dist/types-graph.js +4 -0
- package/dist/types-jobs.d.ts +28 -0
- package/dist/types-jobs.js +14 -0
- package/dist/types-relations.d.ts +49 -0
- package/dist/types-relations.js +11 -0
- package/dist/types-workflows.d.ts +36 -0
- package/dist/types-workflows.js +3 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.js +1 -0
- package/package.json +49 -4
- package/src/analyze.ts +119 -0
- package/src/ast/node.ts +102 -0
- package/src/ast/walk.ts +37 -0
- package/src/diagnostics.ts +22 -0
- package/src/fs.ts +59 -0
- package/src/graph-edges.ts +167 -0
- package/src/graph-ids.ts +31 -0
- package/src/graph.ts +194 -0
- package/src/index.ts +26 -0
- package/src/json.ts +34 -0
- package/src/module-graph.ts +117 -0
- package/src/module-info.ts +162 -0
- package/src/node.ts +54 -0
- package/src/parser.ts +19 -0
- package/src/passes/clients.ts +233 -0
- package/src/passes/config.ts +278 -0
- package/src/passes/file-facts.ts +142 -0
- package/src/passes/i18n.ts +66 -0
- package/src/passes/job-relations-diagnostics.ts +60 -0
- package/src/passes/job-relations.ts +76 -0
- package/src/passes/jobs.ts +181 -0
- package/src/passes/jsx-text.ts +126 -0
- package/src/passes/jsx.ts +142 -0
- package/src/passes/path-expression.ts +297 -0
- package/src/passes/route-config.ts +188 -0
- package/src/passes/route-description.ts +33 -0
- package/src/passes/route-elevation.ts +161 -0
- package/src/passes/route-handler.ts +105 -0
- package/src/passes/route-location.ts +62 -0
- package/src/passes/route-relation-resolve.ts +228 -0
- package/src/passes/route-relations-diagnostics.ts +115 -0
- package/src/passes/route-relations-follow.ts +146 -0
- package/src/passes/route-relations-usage.ts +98 -0
- package/src/passes/route-relations-walk.ts +255 -0
- package/src/passes/route-relations.ts +62 -0
- package/src/passes/route-table.ts +123 -0
- package/src/passes/route-tables.ts +50 -0
- package/src/passes/routes.ts +188 -0
- package/src/passes/schema-column-types.ts +62 -0
- package/src/passes/schema-columns.ts +287 -0
- package/src/passes/schema-dsl.ts +136 -0
- package/src/passes/schema-file-links-ddl.ts +125 -0
- package/src/passes/schema-file-links.ts +213 -0
- package/src/passes/schema-foreign-keys.ts +121 -0
- package/src/passes/schema-policies.ts +270 -0
- package/src/passes/schema-re-exports.ts +95 -0
- package/src/passes/schema-relations.ts +239 -0
- package/src/passes/schema-scope.ts +29 -0
- package/src/passes/schema-tables.ts +216 -0
- package/src/passes/schema.ts +67 -0
- package/src/passes/screen-components.ts +162 -0
- package/src/passes/screen-intrinsics.ts +29 -0
- package/src/passes/screen-preview.ts +295 -0
- package/src/passes/screens.ts +291 -0
- package/src/passes/static-read.ts +196 -0
- package/src/passes/views.ts +125 -0
- package/src/passes/workflow-tables.ts +87 -0
- package/src/passes/workflows.ts +213 -0
- package/src/path-resolver.ts +146 -0
- package/src/revision.ts +25 -0
- package/src/source.ts +15 -0
- package/src/static-eval.ts +242 -0
- package/src/types-graph.ts +176 -0
- package/src/types-jobs.ts +40 -0
- package/src/types-relations.ts +64 -0
- package/src/types-workflows.ts +42 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Project-relative path handling and module specifier resolution (no Node APIs). */
|
|
2
|
+
|
|
3
|
+
export interface PathAlias {
|
|
4
|
+
readonly prefix: string;
|
|
5
|
+
readonly target: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PathResolverInput {
|
|
9
|
+
readonly exists: (path: string) => boolean;
|
|
10
|
+
readonly readJson: (path: string) => unknown;
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly aliases?: readonly PathAlias[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PathResolver {
|
|
16
|
+
resolve(from: string, specifier: string): string | null;
|
|
17
|
+
tryFile(path: string): string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SOURCE_EXTENSIONS = [
|
|
21
|
+
".ts",
|
|
22
|
+
".tsx",
|
|
23
|
+
".js",
|
|
24
|
+
".jsx",
|
|
25
|
+
".mjs",
|
|
26
|
+
".cjs",
|
|
27
|
+
".mts",
|
|
28
|
+
".cts",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export function normalizePath(path: string): string {
|
|
32
|
+
const parts = path.replace(/\\/g, "/").split("/");
|
|
33
|
+
const out: string[] = [];
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
if (part === "" || part === ".") continue;
|
|
36
|
+
if (part === "..") {
|
|
37
|
+
out.pop();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
out.push(part);
|
|
41
|
+
}
|
|
42
|
+
return out.join("/");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function dirname(path: string): string {
|
|
46
|
+
const index = path.lastIndexOf("/");
|
|
47
|
+
return index === -1 ? "" : path.slice(0, index);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function joinPath(base: string, relative: string): string {
|
|
51
|
+
return normalizePath(base.length === 0 ? relative : base + "/" + relative);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packageSpecifier(specifier: string): { pkg: string; subpath: string } {
|
|
55
|
+
const parts = specifier.split("/");
|
|
56
|
+
if (specifier.startsWith("@")) {
|
|
57
|
+
return { pkg: parts.slice(0, 2).join("/"), subpath: parts.slice(2).join("/") };
|
|
58
|
+
}
|
|
59
|
+
return { pkg: parts[0] ?? specifier, subpath: parts.slice(1).join("/") };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exportTargets(exports: unknown): string[] {
|
|
63
|
+
if (typeof exports === "string") return [exports];
|
|
64
|
+
if (Array.isArray(exports)) return exports.flatMap(exportTargets);
|
|
65
|
+
if (typeof exports !== "object" || exports === null) return [];
|
|
66
|
+
const record = exports as Record<string, unknown>;
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
for (const key of ["import", "default", "browser", "node"]) {
|
|
69
|
+
if (key in record) out.push(...exportTargets(record[key]));
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createPathResolver(input: PathResolverInput): PathResolver {
|
|
75
|
+
const root = normalizePath(input.root);
|
|
76
|
+
const aliases: PathAlias[] = [
|
|
77
|
+
{ prefix: "~/", target: root },
|
|
78
|
+
{ prefix: "~", target: root },
|
|
79
|
+
...(input.aliases ?? []),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const tryFile = (path: string): string | null => {
|
|
83
|
+
if (input.exists(path)) return path;
|
|
84
|
+
for (const extension of SOURCE_EXTENSIONS) {
|
|
85
|
+
if (input.exists(path + extension)) return path + extension;
|
|
86
|
+
}
|
|
87
|
+
for (const extension of SOURCE_EXTENSIONS) {
|
|
88
|
+
const index = joinPath(path, "index" + extension);
|
|
89
|
+
if (input.exists(index)) return index;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolvePackage = (specifier: string): string | null => {
|
|
95
|
+
const { pkg, subpath } = packageSpecifier(specifier);
|
|
96
|
+
const prefix = "node_modules/" + pkg;
|
|
97
|
+
const packageJson = input.readJson(prefix + "/package.json");
|
|
98
|
+
if (typeof packageJson !== "object" || packageJson === null) return null;
|
|
99
|
+
const record = packageJson as Record<string, unknown>;
|
|
100
|
+
if (subpath.length > 0) {
|
|
101
|
+
const exports = record["exports"];
|
|
102
|
+
if (typeof exports === "object" && exports !== null) {
|
|
103
|
+
const entry = (exports as Record<string, unknown>)["./" + subpath];
|
|
104
|
+
for (const target of exportTargets(entry)) {
|
|
105
|
+
const resolved = tryFile(joinPath(prefix, target));
|
|
106
|
+
if (resolved !== null) return resolved;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return tryFile(joinPath(prefix, subpath));
|
|
110
|
+
}
|
|
111
|
+
const exports = record["exports"];
|
|
112
|
+
if (typeof exports === "object" && exports !== null) {
|
|
113
|
+
const entry = (exports as Record<string, unknown>)["."];
|
|
114
|
+
for (const target of exportTargets(entry)) {
|
|
115
|
+
const resolved = tryFile(joinPath(prefix, target));
|
|
116
|
+
if (resolved !== null) return resolved;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const target of ["main", "module"]) {
|
|
120
|
+
const value = record[target];
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
const resolved = tryFile(joinPath(prefix, value));
|
|
123
|
+
if (resolved !== null) return resolved;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return tryFile(joinPath(prefix, "src/index.ts")) ?? tryFile(joinPath(prefix, "index.ts"));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const resolve = (from: string, rawSpecifier: string): string | null => {
|
|
130
|
+
const specifier = rawSpecifier.trim();
|
|
131
|
+
if (specifier.length === 0) return null;
|
|
132
|
+
for (const alias of aliases) {
|
|
133
|
+
if (specifier === alias.prefix || specifier.startsWith(alias.prefix)) {
|
|
134
|
+
const rest = specifier.slice(alias.prefix.length);
|
|
135
|
+
const target = alias.target.length === 0 ? rest : joinPath(alias.target, rest);
|
|
136
|
+
const resolved = tryFile(target);
|
|
137
|
+
if (resolved !== null) return resolved;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (specifier.startsWith(".")) return tryFile(joinPath(dirname(from), specifier));
|
|
141
|
+
if (specifier.startsWith("/")) return tryFile(normalizePath(specifier));
|
|
142
|
+
return resolvePackage(specifier);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return { resolve, tryFile };
|
|
146
|
+
}
|
package/src/revision.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { normalizePath } from "./path-resolver.ts";
|
|
2
|
+
|
|
3
|
+
/** Deterministic, browser-safe revision of the analyzed file set. */
|
|
4
|
+
export function computeRevision(files: ReadonlyMap<string, string>): string {
|
|
5
|
+
const entries = [...files]
|
|
6
|
+
.map(([path, source]) => [normalizePath(path), source] as const)
|
|
7
|
+
.toSorted(([left], [right]) => left.localeCompare(right));
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
for (const [path, source] of entries) {
|
|
10
|
+
hash = mix(hash, path);
|
|
11
|
+
hash = mix(hash, "\u0000");
|
|
12
|
+
hash = mix(hash, source);
|
|
13
|
+
hash = mix(hash, "\u0001");
|
|
14
|
+
}
|
|
15
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mix(hash: number, value: string): number {
|
|
19
|
+
let next = hash;
|
|
20
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
21
|
+
next ^= value.charCodeAt(index);
|
|
22
|
+
next = Math.imul(next, 0x01000193) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
}
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SourceRef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** 1-based line number of a source offset. */
|
|
4
|
+
export function lineOf(source: string, offset: number): number {
|
|
5
|
+
let line = 1;
|
|
6
|
+
const end = Math.min(Math.max(offset, 0), source.length);
|
|
7
|
+
for (let index = 0; index < end; index += 1) {
|
|
8
|
+
if (source.charCodeAt(index) === 10) line += 1;
|
|
9
|
+
}
|
|
10
|
+
return line;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sourceRef(file: string, source: string, offset: number): SourceRef {
|
|
14
|
+
return { file, line: lineOf(source, offset) };
|
|
15
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asNode,
|
|
3
|
+
identifierName,
|
|
4
|
+
literalString,
|
|
5
|
+
memberName,
|
|
6
|
+
nodeArray,
|
|
7
|
+
objectEntries,
|
|
8
|
+
propertyName,
|
|
9
|
+
propertyValue,
|
|
10
|
+
unwrapExpression,
|
|
11
|
+
type AstNode,
|
|
12
|
+
} from "./ast/node.ts";
|
|
13
|
+
import { defaultExportDeclaration, localInitializer } from "./module-info.ts";
|
|
14
|
+
import type { ModuleGraph } from "./module-graph.ts";
|
|
15
|
+
|
|
16
|
+
export type StaticEvaluation =
|
|
17
|
+
{ readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly reason: string };
|
|
18
|
+
|
|
19
|
+
export interface StaticContext {
|
|
20
|
+
readonly graph: ModuleGraph;
|
|
21
|
+
readonly file: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAX_DEPTH = 16;
|
|
25
|
+
|
|
26
|
+
function fail(reason: string): StaticEvaluation {
|
|
27
|
+
return { ok: false, reason };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fromValue(value: unknown): StaticEvaluation {
|
|
31
|
+
return { ok: true, value };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate the static subset (docs/findings/ast-dsl-contract.md §4): literals, arrays,
|
|
36
|
+
* objects, templates without dynamic parts, same-file consts and one-hop project imports.
|
|
37
|
+
* Never executes code; anything else fails and the caller records an unknown fact.
|
|
38
|
+
*/
|
|
39
|
+
export function evaluateStatic(
|
|
40
|
+
node: AstNode | undefined,
|
|
41
|
+
context: StaticContext,
|
|
42
|
+
depth = 0,
|
|
43
|
+
): StaticEvaluation {
|
|
44
|
+
if (node === undefined) return fail("missing value");
|
|
45
|
+
if (depth > MAX_DEPTH) return fail("static evaluation is too deep or cyclic");
|
|
46
|
+
const unwrapped = unwrapExpression(node) ?? node;
|
|
47
|
+
switch (unwrapped.type) {
|
|
48
|
+
case "Literal":
|
|
49
|
+
return "value" in unwrapped ? fromValue(unwrapped.value) : fail("unsupported literal");
|
|
50
|
+
case "TemplateLiteral":
|
|
51
|
+
return evaluateTemplate(unwrapped, context, depth);
|
|
52
|
+
case "ArrayExpression":
|
|
53
|
+
return evaluateArray(unwrapped, context, depth);
|
|
54
|
+
case "ObjectExpression":
|
|
55
|
+
return evaluateObject(unwrapped, context, depth);
|
|
56
|
+
case "UnaryExpression":
|
|
57
|
+
return evaluateUnary(unwrapped, context, depth);
|
|
58
|
+
case "BinaryExpression":
|
|
59
|
+
return evaluateBinary(unwrapped, context, depth);
|
|
60
|
+
case "MemberExpression":
|
|
61
|
+
return evaluateMember(unwrapped, context, depth);
|
|
62
|
+
case "Identifier":
|
|
63
|
+
return evaluateIdentifier(unwrapped, context, depth);
|
|
64
|
+
default:
|
|
65
|
+
return fail('"' + unwrapped.type + '" is not in the static subset');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function evaluateIdentifier(
|
|
70
|
+
node: AstNode,
|
|
71
|
+
context: StaticContext,
|
|
72
|
+
depth: number,
|
|
73
|
+
): StaticEvaluation {
|
|
74
|
+
const name = identifierName(node);
|
|
75
|
+
if (name === undefined) return fail("missing identifier name");
|
|
76
|
+
if (name === "undefined") return fromValue(undefined);
|
|
77
|
+
const current = context.graph.file(context.file);
|
|
78
|
+
if (current === undefined) return fail("unknown file " + context.file);
|
|
79
|
+
const local = localInitializer(current.program, name);
|
|
80
|
+
if (local !== undefined) return evaluateStatic(local, context, depth + 1);
|
|
81
|
+
const binding = context.graph.resolveBinding(context.file, name);
|
|
82
|
+
if (binding === null) return fail('cannot resolve "' + name + '" statically');
|
|
83
|
+
const target = context.graph.file(binding.file);
|
|
84
|
+
if (target === undefined) return fail("unknown module " + binding.file);
|
|
85
|
+
if (binding.name === "default") {
|
|
86
|
+
const declaration = defaultExportDeclaration(target.program);
|
|
87
|
+
if (declaration === undefined) return fail("module has no default export");
|
|
88
|
+
return evaluateStatic(declaration, { graph: context.graph, file: target.path }, depth + 1);
|
|
89
|
+
}
|
|
90
|
+
const initializer = localInitializer(target.program, binding.name);
|
|
91
|
+
if (initializer === undefined) return fail('cannot resolve "' + binding.name + '" statically');
|
|
92
|
+
return evaluateStatic(initializer, { graph: context.graph, file: target.path }, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function evaluateTemplate(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
96
|
+
const quasis = nodeArray(node, "quasis");
|
|
97
|
+
const expressions = nodeArray(node, "expressions");
|
|
98
|
+
let out = "";
|
|
99
|
+
for (const [index, quasi] of quasis.entries()) {
|
|
100
|
+
// TemplateElement.value is a plain { raw, cooked } record, not an AST node.
|
|
101
|
+
const value = quasi?.value as { cooked?: unknown } | undefined;
|
|
102
|
+
const cooked = value?.cooked;
|
|
103
|
+
out += typeof cooked === "string" ? cooked : "";
|
|
104
|
+
const expression = expressions[index];
|
|
105
|
+
if (expression === undefined) continue;
|
|
106
|
+
const evaluated = evaluateStatic(expression, context, depth + 1);
|
|
107
|
+
if (!evaluated.ok) return evaluated;
|
|
108
|
+
if (
|
|
109
|
+
typeof evaluated.value !== "string" &&
|
|
110
|
+
typeof evaluated.value !== "number" &&
|
|
111
|
+
typeof evaluated.value !== "boolean"
|
|
112
|
+
) {
|
|
113
|
+
return fail("template interpolation is not a primitive");
|
|
114
|
+
}
|
|
115
|
+
out += String(evaluated.value);
|
|
116
|
+
}
|
|
117
|
+
return fromValue(out);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function evaluateArray(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
121
|
+
const out: unknown[] = [];
|
|
122
|
+
for (const element of nodeArray(node, "elements")) {
|
|
123
|
+
const evaluated = evaluateStatic(element, context, depth + 1);
|
|
124
|
+
if (!evaluated.ok) return evaluated;
|
|
125
|
+
out.push(evaluated.value);
|
|
126
|
+
}
|
|
127
|
+
return fromValue(out);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function evaluateObject(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
131
|
+
const out: Record<string, unknown> = {};
|
|
132
|
+
for (const entry of objectEntries(node)) {
|
|
133
|
+
if (entry.type === "SpreadElement") {
|
|
134
|
+
const argument = asNode(entry.argument);
|
|
135
|
+
const spread = evaluateStatic(argument, context, depth + 1);
|
|
136
|
+
if (!spread.ok) return spread;
|
|
137
|
+
if (typeof spread.value !== "object" || spread.value === null) {
|
|
138
|
+
return fail("object spread is not an object literal");
|
|
139
|
+
}
|
|
140
|
+
Object.assign(out, spread.value);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const key = propertyName(entry);
|
|
144
|
+
if (key === undefined) return fail("computed object key is not in the static subset");
|
|
145
|
+
const evaluated = evaluateStatic(propertyValue(entry), context, depth + 1);
|
|
146
|
+
if (!evaluated.ok) return evaluated;
|
|
147
|
+
out[key] = evaluated.value;
|
|
148
|
+
}
|
|
149
|
+
return fromValue(out);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function evaluateUnary(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
153
|
+
const argument = asNode(node.argument);
|
|
154
|
+
const evaluated = evaluateStatic(argument, context, depth + 1);
|
|
155
|
+
if (!evaluated.ok) return evaluated;
|
|
156
|
+
if (node.operator === "-" && typeof evaluated.value === "number") {
|
|
157
|
+
return fromValue(-evaluated.value);
|
|
158
|
+
}
|
|
159
|
+
if (node.operator === "!" && typeof evaluated.value === "boolean") {
|
|
160
|
+
return fromValue(!evaluated.value);
|
|
161
|
+
}
|
|
162
|
+
return fail("unsupported unary expression");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const NUMERIC_OPERATORS = new Set(["+", "-", "*", "/", "%"]);
|
|
166
|
+
|
|
167
|
+
function evaluateBinary(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
168
|
+
const operator = node.operator;
|
|
169
|
+
if (typeof operator !== "string" || !NUMERIC_OPERATORS.has(operator)) {
|
|
170
|
+
return fail("unsupported binary expression");
|
|
171
|
+
}
|
|
172
|
+
const left = evaluateStatic(asNode(node.left), context, depth + 1);
|
|
173
|
+
if (!left.ok) return left;
|
|
174
|
+
const right = evaluateStatic(asNode(node.right), context, depth + 1);
|
|
175
|
+
if (!right.ok) return right;
|
|
176
|
+
if (operator === "+" && (typeof left.value === "string" || typeof right.value === "string")) {
|
|
177
|
+
if (typeof left.value !== "string" && typeof left.value !== "number") {
|
|
178
|
+
return fail("binary + operands are not primitives");
|
|
179
|
+
}
|
|
180
|
+
if (typeof right.value !== "string" && typeof right.value !== "number") {
|
|
181
|
+
return fail("binary + operands are not primitives");
|
|
182
|
+
}
|
|
183
|
+
return fromValue(String(left.value) + String(right.value));
|
|
184
|
+
}
|
|
185
|
+
if (typeof left.value !== "number" || typeof right.value !== "number") {
|
|
186
|
+
return fail("binary " + operator + " operands are not numbers");
|
|
187
|
+
}
|
|
188
|
+
switch (operator) {
|
|
189
|
+
case "+":
|
|
190
|
+
return fromValue(left.value + right.value);
|
|
191
|
+
case "-":
|
|
192
|
+
return fromValue(left.value - right.value);
|
|
193
|
+
case "*":
|
|
194
|
+
return fromValue(left.value * right.value);
|
|
195
|
+
case "/":
|
|
196
|
+
return fromValue(left.value / right.value);
|
|
197
|
+
case "%":
|
|
198
|
+
return fromValue(left.value % right.value);
|
|
199
|
+
default:
|
|
200
|
+
return fail("unsupported binary expression");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function evaluateMember(node: AstNode, context: StaticContext, depth: number): StaticEvaluation {
|
|
205
|
+
const name = memberName(node);
|
|
206
|
+
if (name === undefined) return fail("unsupported member expression");
|
|
207
|
+
const object = evaluateStatic(asNode(node.object), context, depth + 1);
|
|
208
|
+
if (!object.ok) return object;
|
|
209
|
+
if (typeof object.value !== "object" || object.value === null) {
|
|
210
|
+
return fail("member access on a non-object");
|
|
211
|
+
}
|
|
212
|
+
const record = object.value as Record<string, unknown>;
|
|
213
|
+
return Object.hasOwn(record, name) ? fromValue(record[name]) : fail("missing property " + name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convenience: a string or undefined, with the failure reason. */
|
|
217
|
+
export function staticString(
|
|
218
|
+
node: AstNode | undefined,
|
|
219
|
+
context: StaticContext,
|
|
220
|
+
): { ok: true; value: string } | { ok: false; reason: string } {
|
|
221
|
+
const evaluated = evaluateStatic(node, context);
|
|
222
|
+
if (!evaluated.ok) return evaluated;
|
|
223
|
+
return typeof evaluated.value === "string"
|
|
224
|
+
? { ok: true, value: evaluated.value }
|
|
225
|
+
: { ok: false, reason: "expected a string literal" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Convenience: a string[] or undefined, with the failure reason. */
|
|
229
|
+
export function staticStringArray(
|
|
230
|
+
node: AstNode | undefined,
|
|
231
|
+
context: StaticContext,
|
|
232
|
+
): { ok: true; value: string[] } | { ok: false; reason: string } {
|
|
233
|
+
const evaluated = evaluateStatic(node, context);
|
|
234
|
+
if (!evaluated.ok) return evaluated;
|
|
235
|
+
if (!Array.isArray(evaluated.value)) return { ok: false, reason: "expected a string[] literal" };
|
|
236
|
+
if (!evaluated.value.every((item): item is string => typeof item === "string")) {
|
|
237
|
+
return { ok: false, reason: "expected a string[] literal" };
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, value: [...evaluated.value] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export { literalString };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Graph-facing fact shapes (the project graph's public model), split from types.ts so the
|
|
2
|
+
// base fact model stays under the 300-line source limit. Static screen preview/link facts live
|
|
3
|
+
// here too because they are attached to page nodes and page-link edges.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ColumnFact,
|
|
7
|
+
ElevationFact,
|
|
8
|
+
HttpMethod,
|
|
9
|
+
PageKind,
|
|
10
|
+
RouteInputFact,
|
|
11
|
+
ScopeFact,
|
|
12
|
+
SourceRef,
|
|
13
|
+
WrapperState,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export type GraphNodeKind = "page" | "route" | "table" | "view" | "workflow";
|
|
17
|
+
|
|
18
|
+
export interface PageNodeData {
|
|
19
|
+
readonly kind: "page";
|
|
20
|
+
readonly pagePath: string;
|
|
21
|
+
readonly pageKind: PageKind;
|
|
22
|
+
readonly callCount: number;
|
|
23
|
+
/** URL paths from the explicit route table; absent when the page is unrouted. */
|
|
24
|
+
readonly routePaths?: readonly string[];
|
|
25
|
+
/** Static component structure used by the graph's page inspector. */
|
|
26
|
+
readonly preview?: PagePreview;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RouteNodeData {
|
|
30
|
+
readonly kind: "route";
|
|
31
|
+
readonly method: HttpMethod;
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly permission: readonly string[];
|
|
34
|
+
readonly wrapper: WrapperState;
|
|
35
|
+
/** Presence of the handler's input schemas (body/form/query/params), from the AST. */
|
|
36
|
+
readonly input: RouteInputFact;
|
|
37
|
+
readonly dbTables: readonly string[];
|
|
38
|
+
readonly dbColumns: readonly string[];
|
|
39
|
+
/** Audited sudo/system calls, carried for the graph's elevation badge. */
|
|
40
|
+
readonly elevations?: readonly ElevationFact[];
|
|
41
|
+
/** The wrapper config's one-sentence description, or null when absent (D-129). */
|
|
42
|
+
readonly description: string | null;
|
|
43
|
+
readonly unknown: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One policy as the graph names it (D-177): the card needs the grant/gate distinction, while the
|
|
48
|
+
* command and source stay in the inspector's facts.
|
|
49
|
+
*/
|
|
50
|
+
export interface TablePolicyNodeFact {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly restrictive: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TableNodeData {
|
|
56
|
+
readonly kind: "table";
|
|
57
|
+
readonly table: string;
|
|
58
|
+
readonly columns: readonly ColumnFact[];
|
|
59
|
+
readonly rlsEnabled: boolean;
|
|
60
|
+
readonly scope: ScopeFact | null;
|
|
61
|
+
/** The declared policy set, in declaration order (D-177). */
|
|
62
|
+
readonly policies: readonly TablePolicyNodeFact[];
|
|
63
|
+
/**
|
|
64
|
+
* True when an installed @facilis.dev package declares the table and the project only
|
|
65
|
+
* re-exports it: its source file lives under `node_modules/`.
|
|
66
|
+
*/
|
|
67
|
+
readonly framework: boolean;
|
|
68
|
+
readonly unknown: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A declarative view declared with defineView under db/views (D-097/D-098, D-160). */
|
|
72
|
+
export interface ViewNodeData {
|
|
73
|
+
readonly kind: "view";
|
|
74
|
+
/** The view's SQL name: what a route declares in `dbTables` (D-157). */
|
|
75
|
+
readonly view: string;
|
|
76
|
+
/** Base tables named by the declaration (metadata only, D-097); [] when absent. */
|
|
77
|
+
readonly tables: readonly string[];
|
|
78
|
+
/** The view's own permission; null keeps the invoker behaviour (D-098). */
|
|
79
|
+
readonly permission: string | null;
|
|
80
|
+
/** null when a declared permission could not be read, so the mode is unknown. */
|
|
81
|
+
readonly access: "invoker" | "definer" | null;
|
|
82
|
+
readonly unknown: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One step of a workflow definition, as the graph inspector shows it. */
|
|
86
|
+
export interface WorkflowStepNodeData {
|
|
87
|
+
readonly index: number;
|
|
88
|
+
readonly kind: "job" | "approval";
|
|
89
|
+
/** Job name or approval title key; null when the definition could not be read. */
|
|
90
|
+
readonly name: string | null;
|
|
91
|
+
readonly permission: string | null;
|
|
92
|
+
/** Forward step index declared by `onReject.step`, when present. */
|
|
93
|
+
readonly onRejectStep: number | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A workflow definition read from `server/workflows/**` (D-135 W3). */
|
|
97
|
+
export interface WorkflowNodeData {
|
|
98
|
+
readonly kind: "workflow";
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly steps: readonly WorkflowStepNodeData[];
|
|
101
|
+
readonly unknown: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type GraphNodeData =
|
|
105
|
+
PageNodeData | RouteNodeData | TableNodeData | ViewNodeData | WorkflowNodeData;
|
|
106
|
+
|
|
107
|
+
export interface GraphNode {
|
|
108
|
+
readonly id: string;
|
|
109
|
+
readonly kind: GraphNodeKind;
|
|
110
|
+
readonly label: string;
|
|
111
|
+
readonly sourceFile: string | null;
|
|
112
|
+
readonly data: GraphNodeData;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type GraphEdgeKind =
|
|
116
|
+
"fk" | "relation" | "route-table" | "page-route" | "page-link" | "route-view" | "view-base";
|
|
117
|
+
|
|
118
|
+
export interface GraphEdge {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly kind: GraphEdgeKind;
|
|
121
|
+
readonly source: string;
|
|
122
|
+
readonly target: string;
|
|
123
|
+
readonly label: string | null;
|
|
124
|
+
/** A route-table/route-view edge reached through audited sudo/system elevation. */
|
|
125
|
+
readonly access?: "elevated";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ProjectGraph {
|
|
129
|
+
readonly nodes: readonly GraphNode[];
|
|
130
|
+
readonly edges: readonly GraphEdge[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One node of the bounded static structure preview attached to a page. */
|
|
134
|
+
export interface PagePreviewNode {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
readonly parentId: string | null;
|
|
137
|
+
/** Component name as written in the JSX (`TodosPage`, `Page`, `header`). */
|
|
138
|
+
readonly name: string;
|
|
139
|
+
readonly kind: "component" | "intrinsic" | "slot";
|
|
140
|
+
/** Project file that defines the component, when it can be resolved statically. */
|
|
141
|
+
readonly file: string | null;
|
|
142
|
+
readonly source: SourceRef;
|
|
143
|
+
/** True when the node sits under a conditional/map branch. */
|
|
144
|
+
readonly dynamic: boolean;
|
|
145
|
+
/** Direct literal text child, trimmed and whitespace-collapsed; absent when none. */
|
|
146
|
+
readonly text?: string;
|
|
147
|
+
/** i18n key when that text child is a `t("key")` call (D-043); absent otherwise. */
|
|
148
|
+
readonly tKey?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Static component structure of one page, capped by the analyzer. */
|
|
152
|
+
export interface PagePreview {
|
|
153
|
+
/** URL paths resolved from the explicit route table. */
|
|
154
|
+
readonly routePaths: readonly string[];
|
|
155
|
+
readonly nodes: readonly PagePreviewNode[];
|
|
156
|
+
/** True when the maximum depth/node budget cut the structure. */
|
|
157
|
+
readonly truncated: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** An explicit client-side navigation target (`Link`, `NavLink`, `navigate()`). */
|
|
161
|
+
export interface PageLink {
|
|
162
|
+
/** Page file that renders the link. */
|
|
163
|
+
readonly from: string;
|
|
164
|
+
/** Target page file, resolved through the route table. */
|
|
165
|
+
readonly to: string;
|
|
166
|
+
/** URL path written in the source, for the edge label. */
|
|
167
|
+
readonly toPath: string;
|
|
168
|
+
readonly kind: "link" | "navlink" | "navigate";
|
|
169
|
+
readonly source: SourceRef;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Static screen facts consumed by the graph builder (not a separate analysis output). */
|
|
173
|
+
export interface ScreenFacts {
|
|
174
|
+
readonly previews: Readonly<Record<string, PagePreview>>;
|
|
175
|
+
readonly links: readonly PageLink[];
|
|
176
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Background-job fact model, split from types.ts so the base fact model stays under the
|
|
2
|
+
// 300-line source limit. A job always runs system-scoped (D-107), so D-157 applies to its
|
|
3
|
+
// `db` client without the elevation dimension routes have: no `ctx.sudo`/`ctx.system`.
|
|
4
|
+
|
|
5
|
+
import type { RouteRelationUse } from "./types-relations.ts";
|
|
6
|
+
import type { SourceRef, UnknownFact } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/** One `server/jobs/**` definition: the `defineJob({...})` default export. */
|
|
9
|
+
export interface JobFact {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly sourceFile: string;
|
|
12
|
+
readonly source: SourceRef;
|
|
13
|
+
/** Relations the definition declares; null when not statically readable (D-157). */
|
|
14
|
+
readonly dbTables: readonly string[] | null;
|
|
15
|
+
/** true allows `db.execute`; false when absent; null when unreadable (fail closed). */
|
|
16
|
+
readonly rawSql: boolean | null;
|
|
17
|
+
readonly unknown: readonly UnknownFact[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Static db usage of one job's `run(ctx)` body, following project-local calls (D-157). */
|
|
21
|
+
export interface JobRelationsFact {
|
|
22
|
+
readonly used: readonly RouteRelationUse[];
|
|
23
|
+
readonly unresolved: readonly SourceRef[];
|
|
24
|
+
/** `db.execute(...)` call sites; legal only with rawSql: true. */
|
|
25
|
+
readonly rawSql: readonly SourceRef[];
|
|
26
|
+
/** A project-external callee received the job's db client, so usage is incomplete. */
|
|
27
|
+
readonly opaque: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Usage of a job whose `run` the pass cannot inspect: explicitly empty, never guessed.
|
|
32
|
+
* `opaque` is true because "not observed" must not be reported as "never used" (the
|
|
33
|
+
* declared-but-unused warning and the AI rule both suppress on it).
|
|
34
|
+
*/
|
|
35
|
+
export const EMPTY_JOB_RELATIONS: JobRelationsFact = {
|
|
36
|
+
used: [],
|
|
37
|
+
unresolved: [],
|
|
38
|
+
rawSql: [],
|
|
39
|
+
opaque: true,
|
|
40
|
+
};
|