@facilis.dev/analyze 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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 +158 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -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.d.ts +22 -0
- package/dist/passes/jsx.js +89 -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-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +159 -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 +130 -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 +190 -0
- package/src/index.ts +24 -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.ts +92 -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-preview.ts +268 -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 +161 -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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static screen facts for the project graph: route-table paths, a bounded component preview
|
|
3
|
+
* per page and explicit page-to-page navigation links.
|
|
4
|
+
*
|
|
5
|
+
* Only explicit client navigation counts (react-router `Link`, `NavLink`, `useNavigate`).
|
|
6
|
+
* Guards and `<Navigate>` redirects are intentionally out of scope for the graph: they are
|
|
7
|
+
* conditional control flow, not a link the page declares.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { asNode, nodeArray, unwrapExpression, type AstNode } from "../ast/node.ts";
|
|
11
|
+
import { walk } from "../ast/walk.ts";
|
|
12
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
13
|
+
import { evaluateStatic } from "../static-eval.ts";
|
|
14
|
+
import type { PageFact, PageLink, PagePreview, ScreenFacts } from "../types.ts";
|
|
15
|
+
import { elementName, jsxAttribute, jsxAttributeValue } from "./jsx.ts";
|
|
16
|
+
import { buildRouteTable, type RouteEntry } from "./route-table.ts";
|
|
17
|
+
import { buildPagePreview, pageComponentName, type PreviewRoot } from "./screen-preview.ts";
|
|
18
|
+
|
|
19
|
+
function resolvePage(graph: ModuleGraph, originFile: string, name: string): string | null {
|
|
20
|
+
const local = name.includes(".") ? name.slice(0, name.indexOf(".")) : name;
|
|
21
|
+
const binding = graph.resolveBinding(originFile, local);
|
|
22
|
+
if (binding === null) return null;
|
|
23
|
+
const file = graph.file(binding.file);
|
|
24
|
+
return file !== undefined && file.path.startsWith("src/pages/") ? file.path : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** First component in a route element that resolves to a file under src/pages/. */
|
|
28
|
+
function pagePathOfElement(
|
|
29
|
+
graph: ModuleGraph,
|
|
30
|
+
originFile: string,
|
|
31
|
+
element: AstNode,
|
|
32
|
+
): string | null {
|
|
33
|
+
let found: string | null = null;
|
|
34
|
+
walk(element, (node) => {
|
|
35
|
+
if (found !== null || node.type !== "JSXElement") return;
|
|
36
|
+
const name = elementName(node);
|
|
37
|
+
if (name === null || name[0] === name[0]?.toLowerCase()) return;
|
|
38
|
+
found = resolvePage(graph, originFile, name);
|
|
39
|
+
});
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function appFileOf(graph: ModuleGraph): ProjectFile | null {
|
|
44
|
+
const direct = graph.file("src/app.tsx");
|
|
45
|
+
if (direct !== undefined) return direct;
|
|
46
|
+
for (const file of graph.files.values()) {
|
|
47
|
+
if (!file.path.startsWith("src/")) continue;
|
|
48
|
+
if (buildRouteTable(graph, file).length > 0) return file;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Only page/layout/app components own page links; providers and guards are shared chrome. */
|
|
54
|
+
function isLinkSourceFile(file: string): boolean {
|
|
55
|
+
if (file.startsWith("src/components/ui/")) return false;
|
|
56
|
+
return (
|
|
57
|
+
file.startsWith("src/pages/") ||
|
|
58
|
+
file.startsWith("src/layouts/") ||
|
|
59
|
+
file.startsWith("src/components/")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface PageRoute {
|
|
64
|
+
readonly index: number;
|
|
65
|
+
readonly path: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function routesByPage(
|
|
69
|
+
graph: ModuleGraph,
|
|
70
|
+
appFile: ProjectFile,
|
|
71
|
+
entries: readonly RouteEntry[],
|
|
72
|
+
): Map<string, PageRoute[]> {
|
|
73
|
+
const byPage = new Map<string, PageRoute[]>();
|
|
74
|
+
entries.forEach((entry, index) => {
|
|
75
|
+
if (entry.element === null) return;
|
|
76
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
77
|
+
if (pagePath === null) return;
|
|
78
|
+
const list = byPage.get(pagePath) ?? [];
|
|
79
|
+
list.push({ index, path: entry.path });
|
|
80
|
+
byPage.set(pagePath, list);
|
|
81
|
+
});
|
|
82
|
+
return byPage;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function previewRoot(
|
|
86
|
+
entries: readonly RouteEntry[],
|
|
87
|
+
pageEntry: number,
|
|
88
|
+
appPath: string,
|
|
89
|
+
): PreviewRoot | null {
|
|
90
|
+
let root: PreviewRoot | null = null;
|
|
91
|
+
let current: number | null = pageEntry;
|
|
92
|
+
while (current !== null) {
|
|
93
|
+
const entry: RouteEntry | undefined = entries[current];
|
|
94
|
+
if (entry === undefined) break;
|
|
95
|
+
if (entry.element !== null) {
|
|
96
|
+
root = { element: entry.element, originFile: appPath, outletChild: root };
|
|
97
|
+
}
|
|
98
|
+
current = entry.parentIndex;
|
|
99
|
+
}
|
|
100
|
+
return root;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pathSegments(path: string): string[] {
|
|
104
|
+
return path.split("/").filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pathMatches(pattern: string, target: string): boolean {
|
|
108
|
+
if (pattern === target) return true;
|
|
109
|
+
if (pattern === "*") return false;
|
|
110
|
+
const patternParts = pathSegments(pattern);
|
|
111
|
+
const targetParts = pathSegments(target);
|
|
112
|
+
if (patternParts.length !== targetParts.length) return false;
|
|
113
|
+
return patternParts.every((part, index) => {
|
|
114
|
+
if (part === "*") return true;
|
|
115
|
+
if (part.startsWith(":") || (part.startsWith("{") && part.endsWith("}"))) return true;
|
|
116
|
+
return part === targetParts[index];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function targetPathOf(
|
|
121
|
+
node: AstNode | undefined,
|
|
122
|
+
graph: ModuleGraph,
|
|
123
|
+
file: ProjectFile,
|
|
124
|
+
): string | null {
|
|
125
|
+
if (node === undefined) return null;
|
|
126
|
+
const evaluated = evaluateStatic(node, { graph, file: file.path });
|
|
127
|
+
if (!evaluated.ok) return null;
|
|
128
|
+
const value = evaluated.value;
|
|
129
|
+
if (typeof value === "string") return value;
|
|
130
|
+
if (typeof value === "object" && value !== null && "pathname" in value) {
|
|
131
|
+
const pathname = (value as { pathname?: unknown }).pathname;
|
|
132
|
+
return typeof pathname === "string" ? pathname : null;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripLocation(path: string): string {
|
|
138
|
+
const cut = path.search(/[?#]/);
|
|
139
|
+
return cut === -1 ? path : path.slice(0, cut);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface LinkBindings {
|
|
143
|
+
readonly links: ReadonlyMap<string, "link" | "navlink">;
|
|
144
|
+
readonly navigate: ReadonlySet<string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function linkBindings(file: ProjectFile): LinkBindings {
|
|
148
|
+
const links = new Map<string, "link" | "navlink">();
|
|
149
|
+
const useNavigate = new Set<string>();
|
|
150
|
+
for (const imported of file.module.imports) {
|
|
151
|
+
if (imported.specifier !== "react-router-dom") continue;
|
|
152
|
+
if (imported.imported === "Link") links.set(imported.local, "link");
|
|
153
|
+
if (imported.imported === "NavLink") links.set(imported.local, "navlink");
|
|
154
|
+
if (imported.imported === "useNavigate") useNavigate.add(imported.local);
|
|
155
|
+
}
|
|
156
|
+
const navigate = new Set<string>();
|
|
157
|
+
if (useNavigate.size === 0) return { links, navigate };
|
|
158
|
+
walk(file.program, (node) => {
|
|
159
|
+
if (node.type !== "VariableDeclarator") return;
|
|
160
|
+
const id = asNode(node.id);
|
|
161
|
+
const init = unwrapExpression(asNode(node.init));
|
|
162
|
+
if (id === undefined || typeof id.name !== "string" || init === undefined) return;
|
|
163
|
+
if (init.type !== "CallExpression") return;
|
|
164
|
+
const callee = unwrapExpression(init.callee);
|
|
165
|
+
if (
|
|
166
|
+
callee?.type === "Identifier" &&
|
|
167
|
+
useNavigate.has(typeof callee.name === "string" ? callee.name : "")
|
|
168
|
+
) {
|
|
169
|
+
navigate.add(id.name);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return { links, navigate };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function linksInFile(
|
|
176
|
+
graph: ModuleGraph,
|
|
177
|
+
file: ProjectFile,
|
|
178
|
+
): { to: string; kind: "link" | "navlink" | "navigate"; source: { file: string; line: number } }[] {
|
|
179
|
+
const bindings = linkBindings(file);
|
|
180
|
+
if (bindings.links.size === 0 && bindings.navigate.size === 0) return [];
|
|
181
|
+
const found: {
|
|
182
|
+
to: string;
|
|
183
|
+
kind: "link" | "navlink" | "navigate";
|
|
184
|
+
source: { file: string; line: number };
|
|
185
|
+
}[] = [];
|
|
186
|
+
walk(file.program, (node) => {
|
|
187
|
+
if (node.type === "JSXElement") {
|
|
188
|
+
const name = elementName(node);
|
|
189
|
+
const kind = name === null ? undefined : bindings.links.get(name);
|
|
190
|
+
if (kind === undefined) return;
|
|
191
|
+
const value = jsxAttributeValue(jsxAttribute(node, "to"));
|
|
192
|
+
const to = targetPathOf(value, graph, file);
|
|
193
|
+
if (to !== null) {
|
|
194
|
+
found.push({ to, kind, source: lineOf(file, node) });
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (node.type !== "CallExpression") return;
|
|
199
|
+
const callee = unwrapExpression(node.callee);
|
|
200
|
+
if (callee?.type !== "Identifier" || typeof callee.name !== "string") return;
|
|
201
|
+
if (!bindings.navigate.has(callee.name)) return;
|
|
202
|
+
const to = targetPathOf(nodeArray(node, "arguments")[0], graph, file);
|
|
203
|
+
if (to !== null) found.push({ to, kind: "navigate", source: lineOf(file, node) });
|
|
204
|
+
});
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lineOf(file: ProjectFile, node: AstNode): { file: string; line: number } {
|
|
209
|
+
const offset = typeof node.start === "number" ? node.start : 0;
|
|
210
|
+
let line = 1;
|
|
211
|
+
for (let index = 0; index < offset && index < file.source.length; index += 1) {
|
|
212
|
+
if (file.source.charCodeAt(index) === 10) line += 1;
|
|
213
|
+
}
|
|
214
|
+
return { file: file.path, line };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Build route paths, page previews and explicit page links from the project snapshot. */
|
|
218
|
+
export function buildScreens(graph: ModuleGraph, pages: readonly PageFact[]): ScreenFacts {
|
|
219
|
+
const appFile = appFileOf(graph);
|
|
220
|
+
const entries = appFile === null ? [] : buildRouteTable(graph, appFile);
|
|
221
|
+
const byPage =
|
|
222
|
+
appFile === null ? new Map<string, PageRoute[]>() : routesByPage(graph, appFile, entries);
|
|
223
|
+
const pathToPage = new Map<string, string>();
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
if (entry.element === null || appFile === null) continue;
|
|
226
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
227
|
+
if (pagePath !== null && !pathToPage.has(entry.path)) pathToPage.set(entry.path, pagePath);
|
|
228
|
+
}
|
|
229
|
+
const resolveTarget = (target: string): string | null => {
|
|
230
|
+
const path = stripLocation(target);
|
|
231
|
+
if (!path.startsWith("/") && path !== "*") return null;
|
|
232
|
+
const exact = pathToPage.get(path);
|
|
233
|
+
if (exact !== undefined) return exact;
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (!pathMatches(entry.path, path)) continue;
|
|
236
|
+
if (entry.element === null || appFile === null) continue;
|
|
237
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
238
|
+
if (pagePath !== null) return pagePath;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const previews: Record<string, PagePreview> = {};
|
|
244
|
+
for (const page of pages) {
|
|
245
|
+
if (page.kind !== "page") continue;
|
|
246
|
+
const pageRoutes = byPage.get(page.path) ?? [];
|
|
247
|
+
const first = pageRoutes[0];
|
|
248
|
+
const root =
|
|
249
|
+
appFile === null || first === undefined
|
|
250
|
+
? null
|
|
251
|
+
: previewRoot(entries, first.index, appFile.path);
|
|
252
|
+
previews[page.path] = buildPagePreview({
|
|
253
|
+
graph,
|
|
254
|
+
pagePath: page.path,
|
|
255
|
+
root,
|
|
256
|
+
pageName: root === null ? pageComponentName(graph, page.path) : null,
|
|
257
|
+
routePaths: pageRoutes.map((entry) => entry.path),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const links: PageLink[] = [];
|
|
262
|
+
const seen = new Set<string>();
|
|
263
|
+
for (const page of pages) {
|
|
264
|
+
if (page.kind !== "page") continue;
|
|
265
|
+
const preview = previews[page.path];
|
|
266
|
+
const files = new Set<string>([page.path]);
|
|
267
|
+
for (const node of preview?.nodes ?? []) {
|
|
268
|
+
if (node.file !== null && isLinkSourceFile(node.file)) files.add(node.file);
|
|
269
|
+
}
|
|
270
|
+
for (const filePath of files) {
|
|
271
|
+
const file = graph.file(filePath);
|
|
272
|
+
if (file === undefined) continue;
|
|
273
|
+
for (const link of linksInFile(graph, file)) {
|
|
274
|
+
const target = resolveTarget(link.to);
|
|
275
|
+
if (target === null || target === page.path) continue;
|
|
276
|
+
const key = page.path + "\u0000" + target + "\u0000" + link.kind + "\u0000" + link.to;
|
|
277
|
+
if (seen.has(key)) continue;
|
|
278
|
+
seen.add(key);
|
|
279
|
+
links.push({
|
|
280
|
+
from: page.path,
|
|
281
|
+
to: target,
|
|
282
|
+
toPath: stripLocation(link.to),
|
|
283
|
+
kind: link.kind,
|
|
284
|
+
source: link.source,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { previews, links };
|
|
291
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the analyzer passes: the static object reader (§4 of the AST DSL
|
|
3
|
+
* contract) and deterministic diagnostic ordering.
|
|
4
|
+
*
|
|
5
|
+
* The reader is deliberately partial: every property is evaluated on its own so one computed
|
|
6
|
+
* value cannot hide its siblings. It never executes project code and it never defaults an
|
|
7
|
+
* unreadable value - the caller decides whether the unknown is a security error.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
identifierName,
|
|
12
|
+
objectEntries,
|
|
13
|
+
propertyName,
|
|
14
|
+
propertyValue,
|
|
15
|
+
unwrapExpression,
|
|
16
|
+
type AstNode,
|
|
17
|
+
} from "../ast/node.ts";
|
|
18
|
+
import { declarationNames, defaultExportDeclaration, localInitializer } from "../module-info.ts";
|
|
19
|
+
import type { ProjectFile } from "../module-graph.ts";
|
|
20
|
+
import { sourceRef } from "../source.ts";
|
|
21
|
+
import { evaluateStatic, type StaticContext } from "../static-eval.ts";
|
|
22
|
+
import type { Diagnostic, SourceRef, UnknownFact } from "../types.ts";
|
|
23
|
+
|
|
24
|
+
export interface StaticField {
|
|
25
|
+
readonly value: unknown;
|
|
26
|
+
readonly ok: boolean;
|
|
27
|
+
readonly node: AstNode | undefined;
|
|
28
|
+
/** Why the value is not static; null when ok. */
|
|
29
|
+
readonly reason: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StaticObject {
|
|
33
|
+
readonly fields: ReadonlyMap<string, StaticField>;
|
|
34
|
+
readonly unknown: readonly UnknownFact[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StaticReadOptions {
|
|
38
|
+
/** Diagnostic/unknown code recorded for constructs outside the static subset. */
|
|
39
|
+
readonly code: string;
|
|
40
|
+
/** Whether a field that fails to evaluate is reported (sections re-read for detail). */
|
|
41
|
+
readonly report: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
45
|
+
|
|
46
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function projectPath(root: string, path: string): string {
|
|
51
|
+
return root.length === 0 ? path : root + "/" + path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Project-relative path with the graph root stripped (the opposite of projectPath). */
|
|
55
|
+
export function relativePath(root: string, path: string): string {
|
|
56
|
+
return root.length === 0 || !path.startsWith(root + "/") ? path : path.slice(root.length + 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function refOf(context: StaticContext, node: AstNode | undefined): SourceRef {
|
|
60
|
+
const file = context.graph.file(context.file);
|
|
61
|
+
const offset = node !== undefined && typeof node.start === "number" ? node.start : 0;
|
|
62
|
+
return sourceRef(context.file, file?.source ?? "", offset);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function objectLiteral(
|
|
66
|
+
node: AstNode | undefined,
|
|
67
|
+
context: StaticContext,
|
|
68
|
+
depth = 0,
|
|
69
|
+
): AstNode | undefined {
|
|
70
|
+
if (node === undefined || depth > MAX_RESOLVE_DEPTH) return undefined;
|
|
71
|
+
const unwrapped = unwrapExpression(node);
|
|
72
|
+
if (unwrapped === undefined) return undefined;
|
|
73
|
+
if (unwrapped.type === "ObjectExpression") return unwrapped;
|
|
74
|
+
const file = context.graph.file(context.file);
|
|
75
|
+
const name = identifierName(unwrapped);
|
|
76
|
+
if (file === undefined || name === undefined) return undefined;
|
|
77
|
+
return objectLiteral(localInitializer(file.program, name), context, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Partial, non-executing reader for object literals in the static subset. */
|
|
81
|
+
export function readStaticObject(
|
|
82
|
+
node: AstNode | undefined,
|
|
83
|
+
context: StaticContext,
|
|
84
|
+
options: StaticReadOptions,
|
|
85
|
+
): StaticObject {
|
|
86
|
+
const fields = new Map<string, StaticField>();
|
|
87
|
+
const unknown: UnknownFact[] = [];
|
|
88
|
+
if (node === undefined) return { fields, unknown };
|
|
89
|
+
const record = (reason: string, at: AstNode | undefined, structural: boolean): void => {
|
|
90
|
+
if (structural || options.report) {
|
|
91
|
+
unknown.push({ code: options.code, reason, source: refOf(context, at) });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const literal = objectLiteral(node, context);
|
|
96
|
+
if (literal !== undefined) {
|
|
97
|
+
for (const entry of objectEntries(literal)) {
|
|
98
|
+
if (entry.type === "SpreadElement") {
|
|
99
|
+
record("spread in an object literal cannot be read statically", entry, true);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const key = propertyName(entry);
|
|
103
|
+
if (key === undefined) {
|
|
104
|
+
record("computed object key cannot be read statically", entry, true);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const valueNode = propertyValue(entry);
|
|
108
|
+
const evaluated = evaluateStatic(valueNode, context);
|
|
109
|
+
if (evaluated.ok) {
|
|
110
|
+
fields.set(key, { value: evaluated.value, ok: true, node: valueNode, reason: null });
|
|
111
|
+
} else {
|
|
112
|
+
fields.set(key, { value: undefined, ok: false, node: valueNode, reason: evaluated.reason });
|
|
113
|
+
record(evaluated.reason, valueNode ?? entry, false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { fields, unknown };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const evaluated = evaluateStatic(node, context);
|
|
120
|
+
if (evaluated.ok && isRecord(evaluated.value)) {
|
|
121
|
+
for (const [key, value] of Object.entries(evaluated.value)) {
|
|
122
|
+
fields.set(key, { value, ok: true, node: undefined, reason: null });
|
|
123
|
+
}
|
|
124
|
+
return { fields, unknown };
|
|
125
|
+
}
|
|
126
|
+
record(evaluated.ok ? "value is not an object" : evaluated.reason, node, true);
|
|
127
|
+
return { fields, unknown };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The default export, unwrapped and resolved through a same-file const or a
|
|
132
|
+
* `export { handler as default }` statement.
|
|
133
|
+
*/
|
|
134
|
+
export function defaultExportExpression(file: ProjectFile): AstNode | undefined {
|
|
135
|
+
const declaration = defaultExportDeclaration(file.program);
|
|
136
|
+
if (declaration !== undefined) {
|
|
137
|
+
const unwrapped = unwrapExpression(declaration);
|
|
138
|
+
const name = unwrapped?.type === "Identifier" ? identifierName(unwrapped) : undefined;
|
|
139
|
+
return name === undefined ? unwrapped : unwrapExpression(localInitializer(file.program, name));
|
|
140
|
+
}
|
|
141
|
+
const entry = file.module.exports.find(
|
|
142
|
+
(item) => item.exported === "default" && item.local !== null,
|
|
143
|
+
);
|
|
144
|
+
if (entry === undefined || entry.local === null) return undefined;
|
|
145
|
+
return unwrapExpression(localInitializer(file.program, entry.local));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A top-level function/class declaration by name (localInitializer covers variables only). */
|
|
149
|
+
export function findDeclaration(program: AstNode, name: string): AstNode | undefined {
|
|
150
|
+
for (const raw of Array.isArray(program.body) ? program.body : []) {
|
|
151
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
152
|
+
const statement = raw as AstNode;
|
|
153
|
+
const declaration =
|
|
154
|
+
statement.type === "ExportNamedDeclaration" &&
|
|
155
|
+
statement.declaration !== null &&
|
|
156
|
+
typeof statement.declaration === "object"
|
|
157
|
+
? (statement.declaration as AstNode)
|
|
158
|
+
: statement;
|
|
159
|
+
if (
|
|
160
|
+
(declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") &&
|
|
161
|
+
identifierName(declaration.id) === name
|
|
162
|
+
) {
|
|
163
|
+
return declaration;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** True when the module declares the name itself: a local shadow invalidates an import. */
|
|
170
|
+
export function isLocallyDeclared(file: ProjectFile, name: string): boolean {
|
|
171
|
+
const body = Array.isArray(file.program.body) ? file.program.body : [];
|
|
172
|
+
for (const raw of body) {
|
|
173
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
174
|
+
const statement = raw as AstNode;
|
|
175
|
+
const declaration =
|
|
176
|
+
statement.type === "ExportNamedDeclaration" &&
|
|
177
|
+
statement.declaration !== null &&
|
|
178
|
+
typeof statement.declaration === "object"
|
|
179
|
+
? (statement.declaration as AstNode)
|
|
180
|
+
: statement;
|
|
181
|
+
if (declarationNames(declaration).includes(name)) return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Stable diagnostic order (file, line, code, message) so snapshots never flap. */
|
|
187
|
+
export function sortDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[] {
|
|
188
|
+
return [...diagnostics].toSorted((left, right) => {
|
|
189
|
+
const fileOrder = (left.source?.file ?? "").localeCompare(right.source?.file ?? "");
|
|
190
|
+
if (fileOrder !== 0) return fileOrder;
|
|
191
|
+
const lineOrder = (left.source?.line ?? 0) - (right.source?.line ?? 0);
|
|
192
|
+
if (lineOrder !== 0) return lineOrder;
|
|
193
|
+
const codeOrder = left.code.localeCompare(right.code);
|
|
194
|
+
return codeOrder !== 0 ? codeOrder : left.message.localeCompare(right.message);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `db/views/**` -> ViewFacts (D-157, SPEC §11.2).
|
|
3
|
+
*
|
|
4
|
+
* A view is declared one per file with `defineView({ name, columns, query, tables?,
|
|
5
|
+
* permission? })` from @facilis.dev/db (D-097/D-098). The route-relations pass resolves a view
|
|
6
|
+
* object used in `from(...)` to this fact, so a route declares the view's SQL name in
|
|
7
|
+
* `dbTables` instead of its base tables. D-160 adds the view nodes and their edges to the
|
|
8
|
+
* project graph; this pass stays facts-only.
|
|
9
|
+
*
|
|
10
|
+
* The declaration is read through the bounded static subset; an unreadable name/tables/
|
|
11
|
+
* permission is an explicit unknown, never a guessed value.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { callArguments, identifierName, unwrapExpression, type AstNode } from "../ast/node.ts";
|
|
15
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
16
|
+
import type { Diagnostic, UnknownFact, ViewFact } from "../types.ts";
|
|
17
|
+
import {
|
|
18
|
+
createDslResolver,
|
|
19
|
+
isViewFile,
|
|
20
|
+
nodeRef,
|
|
21
|
+
schemaDiagnostics,
|
|
22
|
+
topLevelDeclarators,
|
|
23
|
+
type DslResolver,
|
|
24
|
+
} from "./schema-dsl.ts";
|
|
25
|
+
import { readStaticObject } from "./static-read.ts";
|
|
26
|
+
|
|
27
|
+
export interface ViewScan {
|
|
28
|
+
readonly views: readonly ViewFact[];
|
|
29
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function viewUnknown(reason: string, source: ViewFact["source"]): UnknownFact {
|
|
33
|
+
return { code: "view-static", reason, source };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stringArray(value: unknown): string[] | null {
|
|
37
|
+
if (!Array.isArray(value)) return null;
|
|
38
|
+
return value.every((entry): entry is string => typeof entry === "string") ? [...value] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The `defineView(...)` call an initializer holds, when it is the @facilis.dev/db DSL. */
|
|
42
|
+
function viewCall(init: unknown, resolver: DslResolver): AstNode | null {
|
|
43
|
+
const expression = unwrapExpression(init);
|
|
44
|
+
if (expression?.type !== "CallExpression") return null;
|
|
45
|
+
const symbol = resolver(expression.callee);
|
|
46
|
+
return symbol?.pkg === "facilis-db" && symbol.name === "defineView" ? expression : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildViewDraft(
|
|
50
|
+
graph: ModuleGraph,
|
|
51
|
+
file: ProjectFile,
|
|
52
|
+
declarator: AstNode,
|
|
53
|
+
call: AstNode,
|
|
54
|
+
): { view: ViewFact; diagnostics: readonly Diagnostic[] } {
|
|
55
|
+
const source = nodeRef(file, call);
|
|
56
|
+
const binding = identifierName(declarator.id) ?? null;
|
|
57
|
+
const unknowns: UnknownFact[] = [];
|
|
58
|
+
const options = readStaticObject(
|
|
59
|
+
callArguments(call)[0],
|
|
60
|
+
{ graph, file: file.path },
|
|
61
|
+
{ code: "view-static", report: false },
|
|
62
|
+
);
|
|
63
|
+
unknowns.push(...options.unknown);
|
|
64
|
+
|
|
65
|
+
const nameField = options.fields.get("name");
|
|
66
|
+
let name = binding ?? "unknown";
|
|
67
|
+
if (nameField !== undefined && nameField.ok && typeof nameField.value === "string") {
|
|
68
|
+
if (nameField.value.length > 0) name = nameField.value;
|
|
69
|
+
else unknowns.push(viewUnknown("view name is not a static string", source));
|
|
70
|
+
} else if (nameField !== undefined && !nameField.ok) {
|
|
71
|
+
unknowns.push(viewUnknown("view name is missing or not statically readable", source));
|
|
72
|
+
} else {
|
|
73
|
+
unknowns.push(viewUnknown("view name is missing or not a static string", source));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let tables: string[] = [];
|
|
77
|
+
const tablesField = options.fields.get("tables");
|
|
78
|
+
if (tablesField !== undefined) {
|
|
79
|
+
const read = tablesField.ok ? stringArray(tablesField.value) : null;
|
|
80
|
+
if (read === null) {
|
|
81
|
+
unknowns.push(viewUnknown("view tables are not a static string[]", source));
|
|
82
|
+
} else {
|
|
83
|
+
tables = read;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const permissionField = options.fields.get("permission");
|
|
88
|
+
let permission: string | null = null;
|
|
89
|
+
let access: ViewFact["access"] = "invoker";
|
|
90
|
+
if (permissionField !== undefined) {
|
|
91
|
+
if (permissionField.ok && typeof permissionField.value === "string") {
|
|
92
|
+
permission = permissionField.value;
|
|
93
|
+
access = "definer";
|
|
94
|
+
} else {
|
|
95
|
+
// We cannot tell an invoker declaration from a definer one: keep the mode unknown.
|
|
96
|
+
access = null;
|
|
97
|
+
unknowns.push(viewUnknown("view permission is not a static string", source));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
view: { name, binding, tables, permission, access, source, unknown: unknowns },
|
|
103
|
+
diagnostics: schemaDiagnostics(unknowns),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read `db/views/**` into view facts. */
|
|
108
|
+
export function buildViewFacts(graph: ModuleGraph): ViewScan {
|
|
109
|
+
const files = [...graph.files.values()]
|
|
110
|
+
.filter((file) => isViewFile(graph, file.path))
|
|
111
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
112
|
+
const views: ViewFact[] = [];
|
|
113
|
+
const diagnostics: Diagnostic[] = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const resolver = createDslResolver(graph, file);
|
|
116
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
117
|
+
const call = viewCall(declarator.init, resolver);
|
|
118
|
+
if (call === null) continue;
|
|
119
|
+
const draft = buildViewDraft(graph, file, declarator, call);
|
|
120
|
+
views.push(draft.view);
|
|
121
|
+
diagnostics.push(...draft.diagnostics);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { views, diagnostics };
|
|
125
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow table surface, derived from the jobs its steps reference (user decision: derived,
|
|
3
|
+
* never declared). A workflow declares no tables of its own: the union of the referenced jobs'
|
|
4
|
+
* `dbTables` is what the workflow can reach, and the runtime inherits each job's guard.
|
|
5
|
+
*
|
|
6
|
+
* Approval steps contribute nothing. A referenced job with an unreadable `dbTables` makes the
|
|
7
|
+
* union explicitly unknown; a referenced job with no definition keeps its name visible in
|
|
8
|
+
* `unknownJobs` and is a warning, never a silent hole.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { warning } from "../diagnostics.ts";
|
|
12
|
+
import type { Diagnostic, SourceRef, WorkflowFact } from "../types.ts";
|
|
13
|
+
import type { JobFact } from "../types-jobs.ts";
|
|
14
|
+
import type { WorkflowTablesFact } from "../types-workflows.ts";
|
|
15
|
+
import { sortDiagnostics } from "./static-read.ts";
|
|
16
|
+
|
|
17
|
+
export interface WorkflowTablesResult {
|
|
18
|
+
readonly facts: readonly WorkflowTablesFact[];
|
|
19
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One job definition per name; the manifest enforces uniqueness, first wins here. */
|
|
23
|
+
function jobRegistry(jobs: readonly JobFact[]): Map<string, JobFact> {
|
|
24
|
+
const byName = new Map<string, JobFact>();
|
|
25
|
+
for (const job of jobs) {
|
|
26
|
+
if (!byName.has(job.name)) byName.set(job.name, job);
|
|
27
|
+
}
|
|
28
|
+
return byName;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Derive every workflow's table surface from its job steps. */
|
|
32
|
+
export function buildWorkflowTableFacts(
|
|
33
|
+
workflows: readonly WorkflowFact[],
|
|
34
|
+
jobs: readonly JobFact[],
|
|
35
|
+
): WorkflowTablesResult {
|
|
36
|
+
const registry = jobRegistry(jobs);
|
|
37
|
+
const facts: WorkflowTablesFact[] = [];
|
|
38
|
+
const diagnostics: Diagnostic[] = [];
|
|
39
|
+
for (const workflow of workflows) {
|
|
40
|
+
const tables = new Set<string>();
|
|
41
|
+
const unknownJobs = new Set<string>();
|
|
42
|
+
const missingAt = new Map<string, SourceRef>();
|
|
43
|
+
let unknown = false;
|
|
44
|
+
for (const step of workflow.steps) {
|
|
45
|
+
if (step.kind !== "job") continue;
|
|
46
|
+
// A job step whose name is unreadable is already a workflow-definition unknown, but the
|
|
47
|
+
// derived surface must not pretend it is complete.
|
|
48
|
+
if (step.name === null) {
|
|
49
|
+
unknown = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const job = registry.get(step.name);
|
|
53
|
+
if (job === undefined) {
|
|
54
|
+
unknownJobs.add(step.name);
|
|
55
|
+
if (!missingAt.has(step.name)) missingAt.set(step.name, step.source);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (job.dbTables === null) {
|
|
59
|
+
unknown = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const table of job.dbTables) tables.add(table);
|
|
63
|
+
}
|
|
64
|
+
for (const name of [...unknownJobs].toSorted()) {
|
|
65
|
+
diagnostics.push(
|
|
66
|
+
warning(
|
|
67
|
+
"workflow-job-unknown",
|
|
68
|
+
'workflow "' +
|
|
69
|
+
workflow.name +
|
|
70
|
+
'" references job "' +
|
|
71
|
+
name +
|
|
72
|
+
'" but no definition exists in server/jobs',
|
|
73
|
+
missingAt.get(name) ?? workflow.source,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
facts.push({
|
|
78
|
+
name: workflow.name,
|
|
79
|
+
sourceFile: workflow.sourceFile,
|
|
80
|
+
source: workflow.source,
|
|
81
|
+
tables: [...tables].toSorted(),
|
|
82
|
+
unknownJobs: [...unknownJobs].toSorted(),
|
|
83
|
+
unknown,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return { facts, diagnostics: sortDiagnostics(diagnostics) };
|
|
87
|
+
}
|