@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.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/analyze.d.ts +17 -0
  4. package/dist/analyze.js +99 -0
  5. package/dist/ast/node.d.ts +29 -0
  6. package/dist/ast/node.js +85 -0
  7. package/dist/ast/walk.d.ts +8 -0
  8. package/dist/ast/walk.js +40 -0
  9. package/dist/diagnostics.d.ts +5 -0
  10. package/dist/diagnostics.js +12 -0
  11. package/dist/fs.d.ts +13 -0
  12. package/dist/fs.js +51 -0
  13. package/dist/graph-edges.d.ts +3 -0
  14. package/dist/graph-edges.js +148 -0
  15. package/dist/graph-ids.d.ts +6 -0
  16. package/dist/graph-ids.js +22 -0
  17. package/dist/graph.d.ts +15 -0
  18. package/dist/graph.js +162 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.js +17 -0
  21. package/dist/json.d.ts +8 -0
  22. package/dist/json.js +31 -0
  23. package/dist/module-graph.d.ts +33 -0
  24. package/dist/module-graph.js +86 -0
  25. package/dist/module-info.d.ts +27 -0
  26. package/dist/module-info.js +143 -0
  27. package/dist/node.d.ts +16 -0
  28. package/dist/node.js +41 -0
  29. package/dist/parser.d.ts +16 -0
  30. package/dist/parser.js +1 -0
  31. package/dist/passes/clients.d.ts +22 -0
  32. package/dist/passes/clients.js +167 -0
  33. package/dist/passes/config.d.ts +16 -0
  34. package/dist/passes/config.js +229 -0
  35. package/dist/passes/file-facts.d.ts +13 -0
  36. package/dist/passes/file-facts.js +130 -0
  37. package/dist/passes/i18n.d.ts +14 -0
  38. package/dist/passes/i18n.js +55 -0
  39. package/dist/passes/job-relations-diagnostics.d.ts +9 -0
  40. package/dist/passes/job-relations-diagnostics.js +38 -0
  41. package/dist/passes/job-relations.d.ts +18 -0
  42. package/dist/passes/job-relations.js +52 -0
  43. package/dist/passes/jobs.d.ts +20 -0
  44. package/dist/passes/jobs.js +133 -0
  45. package/dist/passes/jsx-text.d.ts +28 -0
  46. package/dist/passes/jsx-text.js +121 -0
  47. package/dist/passes/jsx.d.ts +24 -0
  48. package/dist/passes/jsx.js +135 -0
  49. package/dist/passes/path-expression.d.ts +26 -0
  50. package/dist/passes/path-expression.js +265 -0
  51. package/dist/passes/route-config.d.ts +24 -0
  52. package/dist/passes/route-config.js +152 -0
  53. package/dist/passes/route-description.d.ts +13 -0
  54. package/dist/passes/route-description.js +23 -0
  55. package/dist/passes/route-elevation.d.ts +18 -0
  56. package/dist/passes/route-elevation.js +136 -0
  57. package/dist/passes/route-handler.d.ts +27 -0
  58. package/dist/passes/route-handler.js +87 -0
  59. package/dist/passes/route-location.d.ts +16 -0
  60. package/dist/passes/route-location.js +58 -0
  61. package/dist/passes/route-relation-resolve.d.ts +51 -0
  62. package/dist/passes/route-relation-resolve.js +172 -0
  63. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  64. package/dist/passes/route-relations-diagnostics.js +79 -0
  65. package/dist/passes/route-relations-follow.d.ts +16 -0
  66. package/dist/passes/route-relations-follow.js +116 -0
  67. package/dist/passes/route-relations-usage.d.ts +50 -0
  68. package/dist/passes/route-relations-usage.js +44 -0
  69. package/dist/passes/route-relations-walk.d.ts +25 -0
  70. package/dist/passes/route-relations-walk.js +199 -0
  71. package/dist/passes/route-relations.d.ts +17 -0
  72. package/dist/passes/route-relations.js +43 -0
  73. package/dist/passes/route-table.d.ts +21 -0
  74. package/dist/passes/route-table.js +104 -0
  75. package/dist/passes/route-tables.d.ts +10 -0
  76. package/dist/passes/route-tables.js +42 -0
  77. package/dist/passes/routes.d.ts +18 -0
  78. package/dist/passes/routes.js +152 -0
  79. package/dist/passes/schema-column-types.d.ts +9 -0
  80. package/dist/passes/schema-column-types.js +55 -0
  81. package/dist/passes/schema-columns.d.ts +35 -0
  82. package/dist/passes/schema-columns.js +197 -0
  83. package/dist/passes/schema-dsl.d.ts +24 -0
  84. package/dist/passes/schema-dsl.js +117 -0
  85. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  86. package/dist/passes/schema-file-links-ddl.js +106 -0
  87. package/dist/passes/schema-file-links.d.ts +19 -0
  88. package/dist/passes/schema-file-links.js +144 -0
  89. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  90. package/dist/passes/schema-foreign-keys.js +80 -0
  91. package/dist/passes/schema-policies.d.ts +15 -0
  92. package/dist/passes/schema-policies.js +201 -0
  93. package/dist/passes/schema-re-exports.d.ts +5 -0
  94. package/dist/passes/schema-re-exports.js +77 -0
  95. package/dist/passes/schema-relations.d.ts +8 -0
  96. package/dist/passes/schema-relations.js +182 -0
  97. package/dist/passes/schema-scope.d.ts +7 -0
  98. package/dist/passes/schema-scope.js +20 -0
  99. package/dist/passes/schema-tables.d.ts +13 -0
  100. package/dist/passes/schema-tables.js +149 -0
  101. package/dist/passes/schema.d.ts +9 -0
  102. package/dist/passes/schema.js +55 -0
  103. package/dist/passes/screen-components.d.ts +22 -0
  104. package/dist/passes/screen-components.js +143 -0
  105. package/dist/passes/screen-intrinsics.d.ts +18 -0
  106. package/dist/passes/screen-intrinsics.js +21 -0
  107. package/dist/passes/screen-preview.d.ts +29 -0
  108. package/dist/passes/screen-preview.js +174 -0
  109. package/dist/passes/screens.d.ts +12 -0
  110. package/dist/passes/screens.js +278 -0
  111. package/dist/passes/static-read.d.ts +47 -0
  112. package/dist/passes/static-read.js +151 -0
  113. package/dist/passes/views.d.ts +20 -0
  114. package/dist/passes/views.js +101 -0
  115. package/dist/passes/workflow-tables.d.ts +18 -0
  116. package/dist/passes/workflow-tables.js +71 -0
  117. package/dist/passes/workflows.d.ts +8 -0
  118. package/dist/passes/workflows.js +160 -0
  119. package/dist/path-resolver.d.ts +20 -0
  120. package/dist/path-resolver.js +134 -0
  121. package/dist/revision.d.ts +2 -0
  122. package/dist/revision.js +23 -0
  123. package/dist/source.d.ts +4 -0
  124. package/dist/source.js +13 -0
  125. package/dist/static-eval.d.ts +36 -0
  126. package/dist/static-eval.js +215 -0
  127. package/dist/types-graph.d.ts +144 -0
  128. package/dist/types-graph.js +4 -0
  129. package/dist/types-jobs.d.ts +28 -0
  130. package/dist/types-jobs.js +14 -0
  131. package/dist/types-relations.d.ts +49 -0
  132. package/dist/types-relations.js +11 -0
  133. package/dist/types-workflows.d.ts +36 -0
  134. package/dist/types-workflows.js +3 -0
  135. package/dist/types.d.ts +226 -0
  136. package/dist/types.js +1 -0
  137. package/package.json +49 -4
  138. package/src/analyze.ts +119 -0
  139. package/src/ast/node.ts +102 -0
  140. package/src/ast/walk.ts +37 -0
  141. package/src/diagnostics.ts +22 -0
  142. package/src/fs.ts +59 -0
  143. package/src/graph-edges.ts +167 -0
  144. package/src/graph-ids.ts +31 -0
  145. package/src/graph.ts +194 -0
  146. package/src/index.ts +26 -0
  147. package/src/json.ts +34 -0
  148. package/src/module-graph.ts +117 -0
  149. package/src/module-info.ts +162 -0
  150. package/src/node.ts +54 -0
  151. package/src/parser.ts +19 -0
  152. package/src/passes/clients.ts +233 -0
  153. package/src/passes/config.ts +278 -0
  154. package/src/passes/file-facts.ts +142 -0
  155. package/src/passes/i18n.ts +66 -0
  156. package/src/passes/job-relations-diagnostics.ts +60 -0
  157. package/src/passes/job-relations.ts +76 -0
  158. package/src/passes/jobs.ts +181 -0
  159. package/src/passes/jsx-text.ts +126 -0
  160. package/src/passes/jsx.ts +142 -0
  161. package/src/passes/path-expression.ts +297 -0
  162. package/src/passes/route-config.ts +188 -0
  163. package/src/passes/route-description.ts +33 -0
  164. package/src/passes/route-elevation.ts +161 -0
  165. package/src/passes/route-handler.ts +105 -0
  166. package/src/passes/route-location.ts +62 -0
  167. package/src/passes/route-relation-resolve.ts +228 -0
  168. package/src/passes/route-relations-diagnostics.ts +115 -0
  169. package/src/passes/route-relations-follow.ts +146 -0
  170. package/src/passes/route-relations-usage.ts +98 -0
  171. package/src/passes/route-relations-walk.ts +255 -0
  172. package/src/passes/route-relations.ts +62 -0
  173. package/src/passes/route-table.ts +123 -0
  174. package/src/passes/route-tables.ts +50 -0
  175. package/src/passes/routes.ts +188 -0
  176. package/src/passes/schema-column-types.ts +62 -0
  177. package/src/passes/schema-columns.ts +287 -0
  178. package/src/passes/schema-dsl.ts +136 -0
  179. package/src/passes/schema-file-links-ddl.ts +125 -0
  180. package/src/passes/schema-file-links.ts +213 -0
  181. package/src/passes/schema-foreign-keys.ts +121 -0
  182. package/src/passes/schema-policies.ts +270 -0
  183. package/src/passes/schema-re-exports.ts +95 -0
  184. package/src/passes/schema-relations.ts +239 -0
  185. package/src/passes/schema-scope.ts +29 -0
  186. package/src/passes/schema-tables.ts +216 -0
  187. package/src/passes/schema.ts +67 -0
  188. package/src/passes/screen-components.ts +162 -0
  189. package/src/passes/screen-intrinsics.ts +29 -0
  190. package/src/passes/screen-preview.ts +295 -0
  191. package/src/passes/screens.ts +291 -0
  192. package/src/passes/static-read.ts +196 -0
  193. package/src/passes/views.ts +125 -0
  194. package/src/passes/workflow-tables.ts +87 -0
  195. package/src/passes/workflows.ts +213 -0
  196. package/src/path-resolver.ts +146 -0
  197. package/src/revision.ts +25 -0
  198. package/src/source.ts +15 -0
  199. package/src/static-eval.ts +242 -0
  200. package/src/types-graph.ts +176 -0
  201. package/src/types-jobs.ts +40 -0
  202. package/src/types-relations.ts +64 -0
  203. package/src/types-workflows.ts +42 -0
  204. package/src/types.ts +299 -0
@@ -0,0 +1,295 @@
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
+
10
+ import { asNode, nodeArray, unwrapExpression, type AstNode } from "../ast/node.ts";
11
+ import type { ModuleGraph } from "../module-graph.ts";
12
+ import type { PagePreview, PagePreviewNode } from "../types-graph.ts";
13
+ import { jsxTextFields, type JsxTextFields } from "./jsx-text.ts";
14
+ import {
15
+ elementName,
16
+ jsxAttributeValue,
17
+ jsxAttributes,
18
+ jsxChildren,
19
+ jsxElementValue,
20
+ } from "./jsx.ts";
21
+ import {
22
+ canExpand,
23
+ componentExpression,
24
+ firstComponentName,
25
+ lineRef,
26
+ resolveComponent,
27
+ returnedExpression,
28
+ } from "./screen-components.ts";
29
+ import { planIntrinsic } from "./screen-intrinsics.ts";
30
+
31
+ const MAX_NODES = 60;
32
+ /**
33
+ * Content intrinsics spend their own smaller budget, so a copy-heavy page cannot displace the
34
+ * structural nodes (sections/components) the preview already drew before content facts existed.
35
+ */
36
+ const MAX_CONTENT_NODES = 30;
37
+ const MAX_DEPTH = 4;
38
+
39
+ /** A route element, its source file and the child route that replaces its <Outlet>. */
40
+ export interface PreviewRoot {
41
+ readonly element: AstNode;
42
+ readonly originFile: string;
43
+ readonly outletChild: PreviewRoot | null;
44
+ }
45
+
46
+ export interface PagePreviewInput {
47
+ readonly graph: ModuleGraph;
48
+ readonly pagePath: string;
49
+ /** Route-table chain for this page; null for an unrouted page. */
50
+ readonly root: PreviewRoot | null;
51
+ /** Component name used when the page is not reached through the route table. */
52
+ readonly pageName: string | null;
53
+ readonly routePaths: readonly string[];
54
+ }
55
+
56
+ /** Build the bounded static structure for one page. */
57
+ export function buildPagePreview(input: PagePreviewInput): PagePreview {
58
+ const { graph, pagePath } = input;
59
+ const nodes: PagePreviewNode[] = [];
60
+ const active = new Set<string>();
61
+ let truncated = false;
62
+ let structuralNodes = 0;
63
+ let contentNodes = 0;
64
+
65
+ const addNode = (
66
+ parentId: string | null,
67
+ name: string,
68
+ kind: PagePreviewNode["kind"],
69
+ file: string | null,
70
+ source: PagePreviewNode["source"],
71
+ dynamic: boolean,
72
+ text?: JsxTextFields,
73
+ content = false,
74
+ ): string | null => {
75
+ if (content ? contentNodes >= MAX_CONTENT_NODES : structuralNodes >= MAX_NODES) {
76
+ truncated = true;
77
+ return null;
78
+ }
79
+ if (content) contentNodes += 1;
80
+ else structuralNodes += 1;
81
+ const id = "p" + nodes.length;
82
+ nodes.push({ id, parentId, name, kind, file, source, dynamic, ...text });
83
+ return id;
84
+ };
85
+
86
+ const appendChildren = (
87
+ node: AstNode,
88
+ originFile: string,
89
+ parentId: string | null,
90
+ depth: number,
91
+ dynamic: boolean,
92
+ outletChild: PreviewRoot | null,
93
+ ): void => {
94
+ for (const child of jsxChildren(node)) {
95
+ if (child.type === "JSXExpressionContainer") {
96
+ appendExpression(
97
+ asNode(child.expression),
98
+ originFile,
99
+ parentId,
100
+ depth,
101
+ dynamic,
102
+ outletChild,
103
+ );
104
+ } else if (child.type === "JSXElement" || child.type === "JSXFragment") {
105
+ appendJsx(child, originFile, parentId, depth, dynamic, outletChild);
106
+ }
107
+ }
108
+ };
109
+
110
+ const appendJsx = (
111
+ node: AstNode,
112
+ originFile: string,
113
+ parentId: string | null,
114
+ depth: number,
115
+ dynamic: boolean,
116
+ outletChild: PreviewRoot | null,
117
+ ): void => {
118
+ if (node.type === "JSXFragment") {
119
+ appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
120
+ return;
121
+ }
122
+ const name = elementName(node);
123
+ if (name === null) return;
124
+ if (name === "Outlet") {
125
+ if (outletChild !== null) {
126
+ appendRoot(outletChild, parentId, depth, dynamic);
127
+ return;
128
+ }
129
+ addNode(
130
+ parentId,
131
+ "Outlet",
132
+ "slot",
133
+ null,
134
+ lineRef(graph, originFile, node),
135
+ dynamic,
136
+ jsxTextFields(node),
137
+ );
138
+ return;
139
+ }
140
+ const intrinsic = name[0] !== undefined && name[0] === name[0].toLowerCase();
141
+ if (intrinsic) {
142
+ const plan = planIntrinsic(node, name);
143
+ if (plan === null) {
144
+ appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
145
+ return;
146
+ }
147
+ const id = addNode(
148
+ parentId,
149
+ name,
150
+ "intrinsic",
151
+ null,
152
+ lineRef(graph, originFile, node),
153
+ dynamic,
154
+ plan.fields,
155
+ plan.content,
156
+ );
157
+ if (id !== null) appendChildren(node, originFile, id, depth + 1, dynamic, outletChild);
158
+ return;
159
+ }
160
+ const ref = resolveComponent(graph, originFile, name);
161
+ const id = addNode(
162
+ parentId,
163
+ name,
164
+ "component",
165
+ ref?.file ?? null,
166
+ lineRef(graph, originFile, node),
167
+ dynamic,
168
+ jsxTextFields(node),
169
+ );
170
+ if (id === null) return;
171
+ // JSX-valued props are real sections too (AuthShell footer, Page actions, ...).
172
+ for (const attribute of jsxAttributes(node)) {
173
+ if (attribute.type !== "JSXAttribute") continue;
174
+ const value = jsxElementValue(jsxAttributeValue(attribute));
175
+ if (value === undefined) continue;
176
+ const attributeName = asNode(attribute.name);
177
+ const slotName =
178
+ attributeName !== undefined && typeof attributeName.name === "string"
179
+ ? attributeName.name
180
+ : "slot";
181
+ const slotId = addNode(id, slotName, "slot", null, lineRef(graph, originFile, node), dynamic);
182
+ if (slotId !== null) appendJsx(value, originFile, slotId, depth + 1, dynamic, null);
183
+ }
184
+ // The component's own JSX children are its content slot; keep them visible even when the
185
+ // component definition is expanded (its `{children}` expression is not JSX we can splice).
186
+ appendChildren(node, originFile, id, depth + 1, dynamic, null);
187
+ const expand = ref !== null && canExpand(ref.file) && depth < MAX_DEPTH;
188
+ if (!expand) return;
189
+ const key = ref.file + "#" + ref.name;
190
+ const expression = active.has(key) ? undefined : componentExpression(graph, ref.file, ref.name);
191
+ if (expression === undefined) return;
192
+ active.add(key);
193
+ appendExpression(expression, ref.file, id, depth + 1, dynamic, outletChild);
194
+ active.delete(key);
195
+ };
196
+
197
+ const appendExpression = (
198
+ node: AstNode | undefined,
199
+ originFile: string,
200
+ parentId: string | null,
201
+ depth: number,
202
+ dynamic: boolean,
203
+ outletChild: PreviewRoot | null,
204
+ ): void => {
205
+ if (node === undefined) return;
206
+ const expression = unwrapExpression(node) ?? node;
207
+ if (expression.type === "JSXElement" || expression.type === "JSXFragment") {
208
+ appendJsx(expression, originFile, parentId, depth, dynamic, outletChild);
209
+ return;
210
+ }
211
+ if (expression.type === "ConditionalExpression") {
212
+ appendExpression(
213
+ asNode(expression.consequent),
214
+ originFile,
215
+ parentId,
216
+ depth,
217
+ true,
218
+ outletChild,
219
+ );
220
+ appendExpression(
221
+ asNode(expression.alternate),
222
+ originFile,
223
+ parentId,
224
+ depth,
225
+ true,
226
+ outletChild,
227
+ );
228
+ return;
229
+ }
230
+ if (expression.type === "LogicalExpression") {
231
+ appendExpression(asNode(expression.right), originFile, parentId, depth, true, outletChild);
232
+ return;
233
+ }
234
+ if (expression.type === "ArrayExpression") {
235
+ for (const element of nodeArray(expression, "elements")) {
236
+ appendExpression(element, originFile, parentId, depth, dynamic, outletChild);
237
+ }
238
+ return;
239
+ }
240
+ if (expression.type === "CallExpression") {
241
+ const callee = asNode(expression.callee);
242
+ const property =
243
+ callee === undefined || callee.type !== "MemberExpression"
244
+ ? undefined
245
+ : asNode(callee.property);
246
+ if (property !== undefined && typeof property.name === "string" && property.name === "map") {
247
+ const callback = nodeArray(expression, "arguments")[0];
248
+ const body = callback === undefined ? undefined : asNode(callback.body);
249
+ const returned =
250
+ body === undefined
251
+ ? undefined
252
+ : body.type === "BlockStatement"
253
+ ? returnedExpression(body)
254
+ : body;
255
+ appendExpression(returned, originFile, parentId, depth, true, outletChild);
256
+ }
257
+ }
258
+ };
259
+
260
+ const appendRoot = (
261
+ root: PreviewRoot | null,
262
+ parentId: string | null,
263
+ depth: number,
264
+ dynamic: boolean,
265
+ ): void => {
266
+ if (root === null) return;
267
+ appendExpression(root.element, root.originFile, parentId, depth, dynamic, root.outletChild);
268
+ };
269
+
270
+ if (input.root !== null) {
271
+ appendRoot(input.root, null, 0, false);
272
+ } else if (input.pageName !== null) {
273
+ const file = graph.file(pagePath);
274
+ if (file !== undefined) {
275
+ const id = addNode(
276
+ null,
277
+ input.pageName,
278
+ "component",
279
+ pagePath,
280
+ lineRef(graph, pagePath, file.program),
281
+ false,
282
+ );
283
+ const expression =
284
+ id === null ? undefined : componentExpression(graph, pagePath, input.pageName);
285
+ if (id !== null && expression !== undefined)
286
+ appendExpression(expression, pagePath, id, 1, false, null);
287
+ }
288
+ }
289
+
290
+ return { routePaths: input.routePaths, nodes, truncated };
291
+ }
292
+
293
+ export function pageComponentName(graph: ModuleGraph, pagePath: string): string | null {
294
+ return firstComponentName(graph, pagePath);
295
+ }
@@ -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
+ }