@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,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded static page structure for the page inspector: JSX sections nested one inside the
|
|
3
|
+
* other, with component identifiers resolved through the module graph.
|
|
4
|
+
*
|
|
5
|
+
* This is not a rendering. It deliberately stops at explicit limits (depth, node count,
|
|
6
|
+
* component cycles) and marks conditional/map branches as dynamic; anything outside the
|
|
7
|
+
* bounded subset is omitted rather than guessed.
|
|
8
|
+
*/
|
|
9
|
+
import { type AstNode } from "../ast/node.js";
|
|
10
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
11
|
+
import type { PagePreview } from "../types-graph.js";
|
|
12
|
+
/** A route element, its source file and the child route that replaces its <Outlet>. */
|
|
13
|
+
export interface PreviewRoot {
|
|
14
|
+
readonly element: AstNode;
|
|
15
|
+
readonly originFile: string;
|
|
16
|
+
readonly outletChild: PreviewRoot | null;
|
|
17
|
+
}
|
|
18
|
+
export interface PagePreviewInput {
|
|
19
|
+
readonly graph: ModuleGraph;
|
|
20
|
+
readonly pagePath: string;
|
|
21
|
+
/** Route-table chain for this page; null for an unrouted page. */
|
|
22
|
+
readonly root: PreviewRoot | null;
|
|
23
|
+
/** Component name used when the page is not reached through the route table. */
|
|
24
|
+
readonly pageName: string | null;
|
|
25
|
+
readonly routePaths: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** Build the bounded static structure for one page. */
|
|
28
|
+
export declare function buildPagePreview(input: PagePreviewInput): PagePreview;
|
|
29
|
+
export declare function pageComponentName(graph: ModuleGraph, pagePath: string): string | null;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded static page structure for the page inspector: JSX sections nested one inside the
|
|
3
|
+
* other, with component identifiers resolved through the module graph.
|
|
4
|
+
*
|
|
5
|
+
* This is not a rendering. It deliberately stops at explicit limits (depth, node count,
|
|
6
|
+
* component cycles) and marks conditional/map branches as dynamic; anything outside the
|
|
7
|
+
* bounded subset is omitted rather than guessed.
|
|
8
|
+
*/
|
|
9
|
+
import { asNode, nodeArray, unwrapExpression } from "../ast/node.js";
|
|
10
|
+
import { jsxTextFields } from "./jsx-text.js";
|
|
11
|
+
import { elementName, jsxAttributeValue, jsxAttributes, jsxChildren, jsxElementValue, } from "./jsx.js";
|
|
12
|
+
import { canExpand, componentExpression, firstComponentName, lineRef, resolveComponent, returnedExpression, } from "./screen-components.js";
|
|
13
|
+
import { planIntrinsic } from "./screen-intrinsics.js";
|
|
14
|
+
const MAX_NODES = 60;
|
|
15
|
+
/**
|
|
16
|
+
* Content intrinsics spend their own smaller budget, so a copy-heavy page cannot displace the
|
|
17
|
+
* structural nodes (sections/components) the preview already drew before content facts existed.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_CONTENT_NODES = 30;
|
|
20
|
+
const MAX_DEPTH = 4;
|
|
21
|
+
/** Build the bounded static structure for one page. */
|
|
22
|
+
export function buildPagePreview(input) {
|
|
23
|
+
const { graph, pagePath } = input;
|
|
24
|
+
const nodes = [];
|
|
25
|
+
const active = new Set();
|
|
26
|
+
let truncated = false;
|
|
27
|
+
let structuralNodes = 0;
|
|
28
|
+
let contentNodes = 0;
|
|
29
|
+
const addNode = (parentId, name, kind, file, source, dynamic, text, content = false) => {
|
|
30
|
+
if (content ? contentNodes >= MAX_CONTENT_NODES : structuralNodes >= MAX_NODES) {
|
|
31
|
+
truncated = true;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (content)
|
|
35
|
+
contentNodes += 1;
|
|
36
|
+
else
|
|
37
|
+
structuralNodes += 1;
|
|
38
|
+
const id = "p" + nodes.length;
|
|
39
|
+
nodes.push({ id, parentId, name, kind, file, source, dynamic, ...text });
|
|
40
|
+
return id;
|
|
41
|
+
};
|
|
42
|
+
const appendChildren = (node, originFile, parentId, depth, dynamic, outletChild) => {
|
|
43
|
+
for (const child of jsxChildren(node)) {
|
|
44
|
+
if (child.type === "JSXExpressionContainer") {
|
|
45
|
+
appendExpression(asNode(child.expression), originFile, parentId, depth, dynamic, outletChild);
|
|
46
|
+
}
|
|
47
|
+
else if (child.type === "JSXElement" || child.type === "JSXFragment") {
|
|
48
|
+
appendJsx(child, originFile, parentId, depth, dynamic, outletChild);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const appendJsx = (node, originFile, parentId, depth, dynamic, outletChild) => {
|
|
53
|
+
if (node.type === "JSXFragment") {
|
|
54
|
+
appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const name = elementName(node);
|
|
58
|
+
if (name === null)
|
|
59
|
+
return;
|
|
60
|
+
if (name === "Outlet") {
|
|
61
|
+
if (outletChild !== null) {
|
|
62
|
+
appendRoot(outletChild, parentId, depth, dynamic);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
addNode(parentId, "Outlet", "slot", null, lineRef(graph, originFile, node), dynamic, jsxTextFields(node));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const intrinsic = name[0] !== undefined && name[0] === name[0].toLowerCase();
|
|
69
|
+
if (intrinsic) {
|
|
70
|
+
const plan = planIntrinsic(node, name);
|
|
71
|
+
if (plan === null) {
|
|
72
|
+
appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const id = addNode(parentId, name, "intrinsic", null, lineRef(graph, originFile, node), dynamic, plan.fields, plan.content);
|
|
76
|
+
if (id !== null)
|
|
77
|
+
appendChildren(node, originFile, id, depth + 1, dynamic, outletChild);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const ref = resolveComponent(graph, originFile, name);
|
|
81
|
+
const id = addNode(parentId, name, "component", ref?.file ?? null, lineRef(graph, originFile, node), dynamic, jsxTextFields(node));
|
|
82
|
+
if (id === null)
|
|
83
|
+
return;
|
|
84
|
+
// JSX-valued props are real sections too (AuthShell footer, Page actions, ...).
|
|
85
|
+
for (const attribute of jsxAttributes(node)) {
|
|
86
|
+
if (attribute.type !== "JSXAttribute")
|
|
87
|
+
continue;
|
|
88
|
+
const value = jsxElementValue(jsxAttributeValue(attribute));
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
const attributeName = asNode(attribute.name);
|
|
92
|
+
const slotName = attributeName !== undefined && typeof attributeName.name === "string"
|
|
93
|
+
? attributeName.name
|
|
94
|
+
: "slot";
|
|
95
|
+
const slotId = addNode(id, slotName, "slot", null, lineRef(graph, originFile, node), dynamic);
|
|
96
|
+
if (slotId !== null)
|
|
97
|
+
appendJsx(value, originFile, slotId, depth + 1, dynamic, null);
|
|
98
|
+
}
|
|
99
|
+
// The component's own JSX children are its content slot; keep them visible even when the
|
|
100
|
+
// component definition is expanded (its `{children}` expression is not JSX we can splice).
|
|
101
|
+
appendChildren(node, originFile, id, depth + 1, dynamic, null);
|
|
102
|
+
const expand = ref !== null && canExpand(ref.file) && depth < MAX_DEPTH;
|
|
103
|
+
if (!expand)
|
|
104
|
+
return;
|
|
105
|
+
const key = ref.file + "#" + ref.name;
|
|
106
|
+
const expression = active.has(key) ? undefined : componentExpression(graph, ref.file, ref.name);
|
|
107
|
+
if (expression === undefined)
|
|
108
|
+
return;
|
|
109
|
+
active.add(key);
|
|
110
|
+
appendExpression(expression, ref.file, id, depth + 1, dynamic, outletChild);
|
|
111
|
+
active.delete(key);
|
|
112
|
+
};
|
|
113
|
+
const appendExpression = (node, originFile, parentId, depth, dynamic, outletChild) => {
|
|
114
|
+
if (node === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const expression = unwrapExpression(node) ?? node;
|
|
117
|
+
if (expression.type === "JSXElement" || expression.type === "JSXFragment") {
|
|
118
|
+
appendJsx(expression, originFile, parentId, depth, dynamic, outletChild);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (expression.type === "ConditionalExpression") {
|
|
122
|
+
appendExpression(asNode(expression.consequent), originFile, parentId, depth, true, outletChild);
|
|
123
|
+
appendExpression(asNode(expression.alternate), originFile, parentId, depth, true, outletChild);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (expression.type === "LogicalExpression") {
|
|
127
|
+
appendExpression(asNode(expression.right), originFile, parentId, depth, true, outletChild);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (expression.type === "ArrayExpression") {
|
|
131
|
+
for (const element of nodeArray(expression, "elements")) {
|
|
132
|
+
appendExpression(element, originFile, parentId, depth, dynamic, outletChild);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (expression.type === "CallExpression") {
|
|
137
|
+
const callee = asNode(expression.callee);
|
|
138
|
+
const property = callee === undefined || callee.type !== "MemberExpression"
|
|
139
|
+
? undefined
|
|
140
|
+
: asNode(callee.property);
|
|
141
|
+
if (property !== undefined && typeof property.name === "string" && property.name === "map") {
|
|
142
|
+
const callback = nodeArray(expression, "arguments")[0];
|
|
143
|
+
const body = callback === undefined ? undefined : asNode(callback.body);
|
|
144
|
+
const returned = body === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: body.type === "BlockStatement"
|
|
147
|
+
? returnedExpression(body)
|
|
148
|
+
: body;
|
|
149
|
+
appendExpression(returned, originFile, parentId, depth, true, outletChild);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const appendRoot = (root, parentId, depth, dynamic) => {
|
|
154
|
+
if (root === null)
|
|
155
|
+
return;
|
|
156
|
+
appendExpression(root.element, root.originFile, parentId, depth, dynamic, root.outletChild);
|
|
157
|
+
};
|
|
158
|
+
if (input.root !== null) {
|
|
159
|
+
appendRoot(input.root, null, 0, false);
|
|
160
|
+
}
|
|
161
|
+
else if (input.pageName !== null) {
|
|
162
|
+
const file = graph.file(pagePath);
|
|
163
|
+
if (file !== undefined) {
|
|
164
|
+
const id = addNode(null, input.pageName, "component", pagePath, lineRef(graph, pagePath, file.program), false);
|
|
165
|
+
const expression = id === null ? undefined : componentExpression(graph, pagePath, input.pageName);
|
|
166
|
+
if (id !== null && expression !== undefined)
|
|
167
|
+
appendExpression(expression, pagePath, id, 1, false, null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { routePaths: input.routePaths, nodes, truncated };
|
|
171
|
+
}
|
|
172
|
+
export function pageComponentName(graph, pagePath) {
|
|
173
|
+
return firstComponentName(graph, pagePath);
|
|
174
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
10
|
+
import type { PageFact, ScreenFacts } from "../types.js";
|
|
11
|
+
/** Build route paths, page previews and explicit page links from the project snapshot. */
|
|
12
|
+
export declare function buildScreens(graph: ModuleGraph, pages: readonly PageFact[]): ScreenFacts;
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
import { asNode, nodeArray, unwrapExpression } from "../ast/node.js";
|
|
10
|
+
import { walk } from "../ast/walk.js";
|
|
11
|
+
import { evaluateStatic } from "../static-eval.js";
|
|
12
|
+
import { elementName, jsxAttribute, jsxAttributeValue } from "./jsx.js";
|
|
13
|
+
import { buildRouteTable } from "./route-table.js";
|
|
14
|
+
import { buildPagePreview, pageComponentName } from "./screen-preview.js";
|
|
15
|
+
function resolvePage(graph, originFile, name) {
|
|
16
|
+
const local = name.includes(".") ? name.slice(0, name.indexOf(".")) : name;
|
|
17
|
+
const binding = graph.resolveBinding(originFile, local);
|
|
18
|
+
if (binding === null)
|
|
19
|
+
return null;
|
|
20
|
+
const file = graph.file(binding.file);
|
|
21
|
+
return file !== undefined && file.path.startsWith("src/pages/") ? file.path : null;
|
|
22
|
+
}
|
|
23
|
+
/** First component in a route element that resolves to a file under src/pages/. */
|
|
24
|
+
function pagePathOfElement(graph, originFile, element) {
|
|
25
|
+
let found = null;
|
|
26
|
+
walk(element, (node) => {
|
|
27
|
+
if (found !== null || node.type !== "JSXElement")
|
|
28
|
+
return;
|
|
29
|
+
const name = elementName(node);
|
|
30
|
+
if (name === null || name[0] === name[0]?.toLowerCase())
|
|
31
|
+
return;
|
|
32
|
+
found = resolvePage(graph, originFile, name);
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
function appFileOf(graph) {
|
|
37
|
+
const direct = graph.file("src/app.tsx");
|
|
38
|
+
if (direct !== undefined)
|
|
39
|
+
return direct;
|
|
40
|
+
for (const file of graph.files.values()) {
|
|
41
|
+
if (!file.path.startsWith("src/"))
|
|
42
|
+
continue;
|
|
43
|
+
if (buildRouteTable(graph, file).length > 0)
|
|
44
|
+
return file;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** Only page/layout/app components own page links; providers and guards are shared chrome. */
|
|
49
|
+
function isLinkSourceFile(file) {
|
|
50
|
+
if (file.startsWith("src/components/ui/"))
|
|
51
|
+
return false;
|
|
52
|
+
return (file.startsWith("src/pages/") ||
|
|
53
|
+
file.startsWith("src/layouts/") ||
|
|
54
|
+
file.startsWith("src/components/"));
|
|
55
|
+
}
|
|
56
|
+
function routesByPage(graph, appFile, entries) {
|
|
57
|
+
const byPage = new Map();
|
|
58
|
+
entries.forEach((entry, index) => {
|
|
59
|
+
if (entry.element === null)
|
|
60
|
+
return;
|
|
61
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
62
|
+
if (pagePath === null)
|
|
63
|
+
return;
|
|
64
|
+
const list = byPage.get(pagePath) ?? [];
|
|
65
|
+
list.push({ index, path: entry.path });
|
|
66
|
+
byPage.set(pagePath, list);
|
|
67
|
+
});
|
|
68
|
+
return byPage;
|
|
69
|
+
}
|
|
70
|
+
function previewRoot(entries, pageEntry, appPath) {
|
|
71
|
+
let root = null;
|
|
72
|
+
let current = pageEntry;
|
|
73
|
+
while (current !== null) {
|
|
74
|
+
const entry = entries[current];
|
|
75
|
+
if (entry === undefined)
|
|
76
|
+
break;
|
|
77
|
+
if (entry.element !== null) {
|
|
78
|
+
root = { element: entry.element, originFile: appPath, outletChild: root };
|
|
79
|
+
}
|
|
80
|
+
current = entry.parentIndex;
|
|
81
|
+
}
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
function pathSegments(path) {
|
|
85
|
+
return path.split("/").filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
function pathMatches(pattern, target) {
|
|
88
|
+
if (pattern === target)
|
|
89
|
+
return true;
|
|
90
|
+
if (pattern === "*")
|
|
91
|
+
return false;
|
|
92
|
+
const patternParts = pathSegments(pattern);
|
|
93
|
+
const targetParts = pathSegments(target);
|
|
94
|
+
if (patternParts.length !== targetParts.length)
|
|
95
|
+
return false;
|
|
96
|
+
return patternParts.every((part, index) => {
|
|
97
|
+
if (part === "*")
|
|
98
|
+
return true;
|
|
99
|
+
if (part.startsWith(":") || (part.startsWith("{") && part.endsWith("}")))
|
|
100
|
+
return true;
|
|
101
|
+
return part === targetParts[index];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function targetPathOf(node, graph, file) {
|
|
105
|
+
if (node === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const evaluated = evaluateStatic(node, { graph, file: file.path });
|
|
108
|
+
if (!evaluated.ok)
|
|
109
|
+
return null;
|
|
110
|
+
const value = evaluated.value;
|
|
111
|
+
if (typeof value === "string")
|
|
112
|
+
return value;
|
|
113
|
+
if (typeof value === "object" && value !== null && "pathname" in value) {
|
|
114
|
+
const pathname = value.pathname;
|
|
115
|
+
return typeof pathname === "string" ? pathname : null;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function stripLocation(path) {
|
|
120
|
+
const cut = path.search(/[?#]/);
|
|
121
|
+
return cut === -1 ? path : path.slice(0, cut);
|
|
122
|
+
}
|
|
123
|
+
function linkBindings(file) {
|
|
124
|
+
const links = new Map();
|
|
125
|
+
const useNavigate = new Set();
|
|
126
|
+
for (const imported of file.module.imports) {
|
|
127
|
+
if (imported.specifier !== "react-router-dom")
|
|
128
|
+
continue;
|
|
129
|
+
if (imported.imported === "Link")
|
|
130
|
+
links.set(imported.local, "link");
|
|
131
|
+
if (imported.imported === "NavLink")
|
|
132
|
+
links.set(imported.local, "navlink");
|
|
133
|
+
if (imported.imported === "useNavigate")
|
|
134
|
+
useNavigate.add(imported.local);
|
|
135
|
+
}
|
|
136
|
+
const navigate = new Set();
|
|
137
|
+
if (useNavigate.size === 0)
|
|
138
|
+
return { links, navigate };
|
|
139
|
+
walk(file.program, (node) => {
|
|
140
|
+
if (node.type !== "VariableDeclarator")
|
|
141
|
+
return;
|
|
142
|
+
const id = asNode(node.id);
|
|
143
|
+
const init = unwrapExpression(asNode(node.init));
|
|
144
|
+
if (id === undefined || typeof id.name !== "string" || init === undefined)
|
|
145
|
+
return;
|
|
146
|
+
if (init.type !== "CallExpression")
|
|
147
|
+
return;
|
|
148
|
+
const callee = unwrapExpression(init.callee);
|
|
149
|
+
if (callee?.type === "Identifier" &&
|
|
150
|
+
useNavigate.has(typeof callee.name === "string" ? callee.name : "")) {
|
|
151
|
+
navigate.add(id.name);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return { links, navigate };
|
|
155
|
+
}
|
|
156
|
+
function linksInFile(graph, file) {
|
|
157
|
+
const bindings = linkBindings(file);
|
|
158
|
+
if (bindings.links.size === 0 && bindings.navigate.size === 0)
|
|
159
|
+
return [];
|
|
160
|
+
const found = [];
|
|
161
|
+
walk(file.program, (node) => {
|
|
162
|
+
if (node.type === "JSXElement") {
|
|
163
|
+
const name = elementName(node);
|
|
164
|
+
const kind = name === null ? undefined : bindings.links.get(name);
|
|
165
|
+
if (kind === undefined)
|
|
166
|
+
return;
|
|
167
|
+
const value = jsxAttributeValue(jsxAttribute(node, "to"));
|
|
168
|
+
const to = targetPathOf(value, graph, file);
|
|
169
|
+
if (to !== null) {
|
|
170
|
+
found.push({ to, kind, source: lineOf(file, node) });
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (node.type !== "CallExpression")
|
|
175
|
+
return;
|
|
176
|
+
const callee = unwrapExpression(node.callee);
|
|
177
|
+
if (callee?.type !== "Identifier" || typeof callee.name !== "string")
|
|
178
|
+
return;
|
|
179
|
+
if (!bindings.navigate.has(callee.name))
|
|
180
|
+
return;
|
|
181
|
+
const to = targetPathOf(nodeArray(node, "arguments")[0], graph, file);
|
|
182
|
+
if (to !== null)
|
|
183
|
+
found.push({ to, kind: "navigate", source: lineOf(file, node) });
|
|
184
|
+
});
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
function lineOf(file, node) {
|
|
188
|
+
const offset = typeof node.start === "number" ? node.start : 0;
|
|
189
|
+
let line = 1;
|
|
190
|
+
for (let index = 0; index < offset && index < file.source.length; index += 1) {
|
|
191
|
+
if (file.source.charCodeAt(index) === 10)
|
|
192
|
+
line += 1;
|
|
193
|
+
}
|
|
194
|
+
return { file: file.path, line };
|
|
195
|
+
}
|
|
196
|
+
/** Build route paths, page previews and explicit page links from the project snapshot. */
|
|
197
|
+
export function buildScreens(graph, pages) {
|
|
198
|
+
const appFile = appFileOf(graph);
|
|
199
|
+
const entries = appFile === null ? [] : buildRouteTable(graph, appFile);
|
|
200
|
+
const byPage = appFile === null ? new Map() : routesByPage(graph, appFile, entries);
|
|
201
|
+
const pathToPage = new Map();
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
if (entry.element === null || appFile === null)
|
|
204
|
+
continue;
|
|
205
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
206
|
+
if (pagePath !== null && !pathToPage.has(entry.path))
|
|
207
|
+
pathToPage.set(entry.path, pagePath);
|
|
208
|
+
}
|
|
209
|
+
const resolveTarget = (target) => {
|
|
210
|
+
const path = stripLocation(target);
|
|
211
|
+
if (!path.startsWith("/") && path !== "*")
|
|
212
|
+
return null;
|
|
213
|
+
const exact = pathToPage.get(path);
|
|
214
|
+
if (exact !== undefined)
|
|
215
|
+
return exact;
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (!pathMatches(entry.path, path))
|
|
218
|
+
continue;
|
|
219
|
+
if (entry.element === null || appFile === null)
|
|
220
|
+
continue;
|
|
221
|
+
const pagePath = pagePathOfElement(graph, appFile.path, entry.element);
|
|
222
|
+
if (pagePath !== null)
|
|
223
|
+
return pagePath;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
const previews = {};
|
|
228
|
+
for (const page of pages) {
|
|
229
|
+
if (page.kind !== "page")
|
|
230
|
+
continue;
|
|
231
|
+
const pageRoutes = byPage.get(page.path) ?? [];
|
|
232
|
+
const first = pageRoutes[0];
|
|
233
|
+
const root = appFile === null || first === undefined
|
|
234
|
+
? null
|
|
235
|
+
: previewRoot(entries, first.index, appFile.path);
|
|
236
|
+
previews[page.path] = buildPagePreview({
|
|
237
|
+
graph,
|
|
238
|
+
pagePath: page.path,
|
|
239
|
+
root,
|
|
240
|
+
pageName: root === null ? pageComponentName(graph, page.path) : null,
|
|
241
|
+
routePaths: pageRoutes.map((entry) => entry.path),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const links = [];
|
|
245
|
+
const seen = new Set();
|
|
246
|
+
for (const page of pages) {
|
|
247
|
+
if (page.kind !== "page")
|
|
248
|
+
continue;
|
|
249
|
+
const preview = previews[page.path];
|
|
250
|
+
const files = new Set([page.path]);
|
|
251
|
+
for (const node of preview?.nodes ?? []) {
|
|
252
|
+
if (node.file !== null && isLinkSourceFile(node.file))
|
|
253
|
+
files.add(node.file);
|
|
254
|
+
}
|
|
255
|
+
for (const filePath of files) {
|
|
256
|
+
const file = graph.file(filePath);
|
|
257
|
+
if (file === undefined)
|
|
258
|
+
continue;
|
|
259
|
+
for (const link of linksInFile(graph, file)) {
|
|
260
|
+
const target = resolveTarget(link.to);
|
|
261
|
+
if (target === null || target === page.path)
|
|
262
|
+
continue;
|
|
263
|
+
const key = page.path + "\u0000" + target + "\u0000" + link.kind + "\u0000" + link.to;
|
|
264
|
+
if (seen.has(key))
|
|
265
|
+
continue;
|
|
266
|
+
seen.add(key);
|
|
267
|
+
links.push({
|
|
268
|
+
from: page.path,
|
|
269
|
+
to: target,
|
|
270
|
+
toPath: stripLocation(link.to),
|
|
271
|
+
kind: link.kind,
|
|
272
|
+
source: link.source,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { previews, links };
|
|
278
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
import { type AstNode } from "../ast/node.js";
|
|
10
|
+
import type { ProjectFile } from "../module-graph.js";
|
|
11
|
+
import { type StaticContext } from "../static-eval.js";
|
|
12
|
+
import type { Diagnostic, SourceRef, UnknownFact } from "../types.js";
|
|
13
|
+
export interface StaticField {
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
readonly ok: boolean;
|
|
16
|
+
readonly node: AstNode | undefined;
|
|
17
|
+
/** Why the value is not static; null when ok. */
|
|
18
|
+
readonly reason: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface StaticObject {
|
|
21
|
+
readonly fields: ReadonlyMap<string, StaticField>;
|
|
22
|
+
readonly unknown: readonly UnknownFact[];
|
|
23
|
+
}
|
|
24
|
+
export interface StaticReadOptions {
|
|
25
|
+
/** Diagnostic/unknown code recorded for constructs outside the static subset. */
|
|
26
|
+
readonly code: string;
|
|
27
|
+
/** Whether a field that fails to evaluate is reported (sections re-read for detail). */
|
|
28
|
+
readonly report: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
31
|
+
export declare function projectPath(root: string, path: string): string;
|
|
32
|
+
/** Project-relative path with the graph root stripped (the opposite of projectPath). */
|
|
33
|
+
export declare function relativePath(root: string, path: string): string;
|
|
34
|
+
export declare function refOf(context: StaticContext, node: AstNode | undefined): SourceRef;
|
|
35
|
+
/** Partial, non-executing reader for object literals in the static subset. */
|
|
36
|
+
export declare function readStaticObject(node: AstNode | undefined, context: StaticContext, options: StaticReadOptions): StaticObject;
|
|
37
|
+
/**
|
|
38
|
+
* The default export, unwrapped and resolved through a same-file const or a
|
|
39
|
+
* `export { handler as default }` statement.
|
|
40
|
+
*/
|
|
41
|
+
export declare function defaultExportExpression(file: ProjectFile): AstNode | undefined;
|
|
42
|
+
/** A top-level function/class declaration by name (localInitializer covers variables only). */
|
|
43
|
+
export declare function findDeclaration(program: AstNode, name: string): AstNode | undefined;
|
|
44
|
+
/** True when the module declares the name itself: a local shadow invalidates an import. */
|
|
45
|
+
export declare function isLocallyDeclared(file: ProjectFile, name: string): boolean;
|
|
46
|
+
/** Stable diagnostic order (file, line, code, message) so snapshots never flap. */
|
|
47
|
+
export declare function sortDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[];
|