@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.
Files changed (198) 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 +158 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.js +15 -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.d.ts +22 -0
  46. package/dist/passes/jsx.js +89 -0
  47. package/dist/passes/path-expression.d.ts +26 -0
  48. package/dist/passes/path-expression.js +265 -0
  49. package/dist/passes/route-config.d.ts +24 -0
  50. package/dist/passes/route-config.js +152 -0
  51. package/dist/passes/route-description.d.ts +13 -0
  52. package/dist/passes/route-description.js +23 -0
  53. package/dist/passes/route-elevation.d.ts +18 -0
  54. package/dist/passes/route-elevation.js +136 -0
  55. package/dist/passes/route-handler.d.ts +27 -0
  56. package/dist/passes/route-handler.js +87 -0
  57. package/dist/passes/route-location.d.ts +16 -0
  58. package/dist/passes/route-location.js +58 -0
  59. package/dist/passes/route-relation-resolve.d.ts +51 -0
  60. package/dist/passes/route-relation-resolve.js +172 -0
  61. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  62. package/dist/passes/route-relations-diagnostics.js +79 -0
  63. package/dist/passes/route-relations-follow.d.ts +16 -0
  64. package/dist/passes/route-relations-follow.js +116 -0
  65. package/dist/passes/route-relations-usage.d.ts +50 -0
  66. package/dist/passes/route-relations-usage.js +44 -0
  67. package/dist/passes/route-relations-walk.d.ts +25 -0
  68. package/dist/passes/route-relations-walk.js +199 -0
  69. package/dist/passes/route-relations.d.ts +17 -0
  70. package/dist/passes/route-relations.js +43 -0
  71. package/dist/passes/route-table.d.ts +21 -0
  72. package/dist/passes/route-table.js +104 -0
  73. package/dist/passes/route-tables.d.ts +10 -0
  74. package/dist/passes/route-tables.js +42 -0
  75. package/dist/passes/routes.d.ts +18 -0
  76. package/dist/passes/routes.js +152 -0
  77. package/dist/passes/schema-column-types.d.ts +9 -0
  78. package/dist/passes/schema-column-types.js +55 -0
  79. package/dist/passes/schema-columns.d.ts +35 -0
  80. package/dist/passes/schema-columns.js +197 -0
  81. package/dist/passes/schema-dsl.d.ts +24 -0
  82. package/dist/passes/schema-dsl.js +117 -0
  83. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  84. package/dist/passes/schema-file-links-ddl.js +106 -0
  85. package/dist/passes/schema-file-links.d.ts +19 -0
  86. package/dist/passes/schema-file-links.js +144 -0
  87. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  88. package/dist/passes/schema-foreign-keys.js +80 -0
  89. package/dist/passes/schema-policies.d.ts +15 -0
  90. package/dist/passes/schema-policies.js +201 -0
  91. package/dist/passes/schema-re-exports.d.ts +5 -0
  92. package/dist/passes/schema-re-exports.js +77 -0
  93. package/dist/passes/schema-relations.d.ts +8 -0
  94. package/dist/passes/schema-relations.js +182 -0
  95. package/dist/passes/schema-scope.d.ts +7 -0
  96. package/dist/passes/schema-scope.js +20 -0
  97. package/dist/passes/schema-tables.d.ts +13 -0
  98. package/dist/passes/schema-tables.js +149 -0
  99. package/dist/passes/schema.d.ts +9 -0
  100. package/dist/passes/schema.js +55 -0
  101. package/dist/passes/screen-components.d.ts +22 -0
  102. package/dist/passes/screen-components.js +143 -0
  103. package/dist/passes/screen-preview.d.ts +29 -0
  104. package/dist/passes/screen-preview.js +159 -0
  105. package/dist/passes/screens.d.ts +12 -0
  106. package/dist/passes/screens.js +278 -0
  107. package/dist/passes/static-read.d.ts +47 -0
  108. package/dist/passes/static-read.js +151 -0
  109. package/dist/passes/views.d.ts +20 -0
  110. package/dist/passes/views.js +101 -0
  111. package/dist/passes/workflow-tables.d.ts +18 -0
  112. package/dist/passes/workflow-tables.js +71 -0
  113. package/dist/passes/workflows.d.ts +8 -0
  114. package/dist/passes/workflows.js +160 -0
  115. package/dist/path-resolver.d.ts +20 -0
  116. package/dist/path-resolver.js +134 -0
  117. package/dist/revision.d.ts +2 -0
  118. package/dist/revision.js +23 -0
  119. package/dist/source.d.ts +4 -0
  120. package/dist/source.js +13 -0
  121. package/dist/static-eval.d.ts +36 -0
  122. package/dist/static-eval.js +215 -0
  123. package/dist/types-graph.d.ts +130 -0
  124. package/dist/types-graph.js +4 -0
  125. package/dist/types-jobs.d.ts +28 -0
  126. package/dist/types-jobs.js +14 -0
  127. package/dist/types-relations.d.ts +49 -0
  128. package/dist/types-relations.js +11 -0
  129. package/dist/types-workflows.d.ts +36 -0
  130. package/dist/types-workflows.js +3 -0
  131. package/dist/types.d.ts +226 -0
  132. package/dist/types.js +1 -0
  133. package/package.json +49 -4
  134. package/src/analyze.ts +119 -0
  135. package/src/ast/node.ts +102 -0
  136. package/src/ast/walk.ts +37 -0
  137. package/src/diagnostics.ts +22 -0
  138. package/src/fs.ts +59 -0
  139. package/src/graph-edges.ts +167 -0
  140. package/src/graph-ids.ts +31 -0
  141. package/src/graph.ts +190 -0
  142. package/src/index.ts +24 -0
  143. package/src/json.ts +34 -0
  144. package/src/module-graph.ts +117 -0
  145. package/src/module-info.ts +162 -0
  146. package/src/node.ts +54 -0
  147. package/src/parser.ts +19 -0
  148. package/src/passes/clients.ts +233 -0
  149. package/src/passes/config.ts +278 -0
  150. package/src/passes/file-facts.ts +142 -0
  151. package/src/passes/i18n.ts +66 -0
  152. package/src/passes/job-relations-diagnostics.ts +60 -0
  153. package/src/passes/job-relations.ts +76 -0
  154. package/src/passes/jobs.ts +181 -0
  155. package/src/passes/jsx.ts +92 -0
  156. package/src/passes/path-expression.ts +297 -0
  157. package/src/passes/route-config.ts +188 -0
  158. package/src/passes/route-description.ts +33 -0
  159. package/src/passes/route-elevation.ts +161 -0
  160. package/src/passes/route-handler.ts +105 -0
  161. package/src/passes/route-location.ts +62 -0
  162. package/src/passes/route-relation-resolve.ts +228 -0
  163. package/src/passes/route-relations-diagnostics.ts +115 -0
  164. package/src/passes/route-relations-follow.ts +146 -0
  165. package/src/passes/route-relations-usage.ts +98 -0
  166. package/src/passes/route-relations-walk.ts +255 -0
  167. package/src/passes/route-relations.ts +62 -0
  168. package/src/passes/route-table.ts +123 -0
  169. package/src/passes/route-tables.ts +50 -0
  170. package/src/passes/routes.ts +188 -0
  171. package/src/passes/schema-column-types.ts +62 -0
  172. package/src/passes/schema-columns.ts +287 -0
  173. package/src/passes/schema-dsl.ts +136 -0
  174. package/src/passes/schema-file-links-ddl.ts +125 -0
  175. package/src/passes/schema-file-links.ts +213 -0
  176. package/src/passes/schema-foreign-keys.ts +121 -0
  177. package/src/passes/schema-policies.ts +270 -0
  178. package/src/passes/schema-re-exports.ts +95 -0
  179. package/src/passes/schema-relations.ts +239 -0
  180. package/src/passes/schema-scope.ts +29 -0
  181. package/src/passes/schema-tables.ts +216 -0
  182. package/src/passes/schema.ts +67 -0
  183. package/src/passes/screen-components.ts +162 -0
  184. package/src/passes/screen-preview.ts +268 -0
  185. package/src/passes/screens.ts +291 -0
  186. package/src/passes/static-read.ts +196 -0
  187. package/src/passes/views.ts +125 -0
  188. package/src/passes/workflow-tables.ts +87 -0
  189. package/src/passes/workflows.ts +213 -0
  190. package/src/path-resolver.ts +146 -0
  191. package/src/revision.ts +25 -0
  192. package/src/source.ts +15 -0
  193. package/src/static-eval.ts +242 -0
  194. package/src/types-graph.ts +161 -0
  195. package/src/types-jobs.ts +40 -0
  196. package/src/types-relations.ts +64 -0
  197. package/src/types-workflows.ts +42 -0
  198. package/src/types.ts +299 -0
@@ -0,0 +1,159 @@
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 { elementName, isSectionIntrinsic, jsxAttributeValue, jsxAttributes, jsxChildren, jsxElementValue, } from "./jsx.js";
11
+ import { canExpand, componentExpression, firstComponentName, lineRef, resolveComponent, returnedExpression, } from "./screen-components.js";
12
+ const MAX_NODES = 60;
13
+ const MAX_DEPTH = 4;
14
+ /** Build the bounded static structure for one page. */
15
+ export function buildPagePreview(input) {
16
+ const { graph, pagePath } = input;
17
+ const nodes = [];
18
+ const active = new Set();
19
+ let truncated = false;
20
+ const addNode = (parentId, name, kind, file, source, dynamic) => {
21
+ if (nodes.length >= MAX_NODES) {
22
+ truncated = true;
23
+ return null;
24
+ }
25
+ const id = "p" + nodes.length;
26
+ nodes.push({ id, parentId, name, kind, file, source, dynamic });
27
+ return id;
28
+ };
29
+ const appendChildren = (node, originFile, parentId, depth, dynamic, outletChild) => {
30
+ for (const child of jsxChildren(node)) {
31
+ if (child.type === "JSXExpressionContainer") {
32
+ appendExpression(asNode(child.expression), originFile, parentId, depth, dynamic, outletChild);
33
+ }
34
+ else if (child.type === "JSXElement" || child.type === "JSXFragment") {
35
+ appendJsx(child, originFile, parentId, depth, dynamic, outletChild);
36
+ }
37
+ }
38
+ };
39
+ const appendJsx = (node, originFile, parentId, depth, dynamic, outletChild) => {
40
+ if (node.type === "JSXFragment") {
41
+ appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
42
+ return;
43
+ }
44
+ const name = elementName(node);
45
+ if (name === null)
46
+ return;
47
+ if (name === "Outlet") {
48
+ if (outletChild !== null)
49
+ appendRoot(outletChild, parentId, depth, dynamic);
50
+ else
51
+ addNode(parentId, "Outlet", "slot", null, lineRef(graph, originFile, node), dynamic);
52
+ return;
53
+ }
54
+ const intrinsic = name[0] !== undefined && name[0] === name[0].toLowerCase();
55
+ if (intrinsic) {
56
+ if (!isSectionIntrinsic(name)) {
57
+ appendChildren(node, originFile, parentId, depth, dynamic, outletChild);
58
+ return;
59
+ }
60
+ const id = addNode(parentId, name, "intrinsic", null, lineRef(graph, originFile, node), dynamic);
61
+ if (id !== null)
62
+ appendChildren(node, originFile, id, depth + 1, dynamic, outletChild);
63
+ return;
64
+ }
65
+ const ref = resolveComponent(graph, originFile, name);
66
+ const id = addNode(parentId, name, "component", ref?.file ?? null, lineRef(graph, originFile, node), dynamic);
67
+ if (id === null)
68
+ return;
69
+ // JSX-valued props are real sections too (AuthShell footer, Page actions, ...).
70
+ for (const attribute of jsxAttributes(node)) {
71
+ if (attribute.type !== "JSXAttribute")
72
+ continue;
73
+ const value = jsxElementValue(jsxAttributeValue(attribute));
74
+ if (value === undefined)
75
+ continue;
76
+ const attributeName = asNode(attribute.name);
77
+ const slotName = attributeName !== undefined && typeof attributeName.name === "string"
78
+ ? attributeName.name
79
+ : "slot";
80
+ const slotId = addNode(id, slotName, "slot", null, lineRef(graph, originFile, node), dynamic);
81
+ if (slotId !== null)
82
+ appendJsx(value, originFile, slotId, depth + 1, dynamic, null);
83
+ }
84
+ // The component's own JSX children are its content slot; keep them visible even when the
85
+ // component definition is expanded (its `{children}` expression is not JSX we can splice).
86
+ appendChildren(node, originFile, id, depth + 1, dynamic, null);
87
+ const expand = ref !== null && canExpand(ref.file) && depth < MAX_DEPTH;
88
+ if (!expand)
89
+ return;
90
+ const key = ref.file + "#" + ref.name;
91
+ const expression = active.has(key) ? undefined : componentExpression(graph, ref.file, ref.name);
92
+ if (expression === undefined)
93
+ return;
94
+ active.add(key);
95
+ appendExpression(expression, ref.file, id, depth + 1, dynamic, outletChild);
96
+ active.delete(key);
97
+ };
98
+ const appendExpression = (node, originFile, parentId, depth, dynamic, outletChild) => {
99
+ if (node === undefined)
100
+ return;
101
+ const expression = unwrapExpression(node) ?? node;
102
+ if (expression.type === "JSXElement" || expression.type === "JSXFragment") {
103
+ appendJsx(expression, originFile, parentId, depth, dynamic, outletChild);
104
+ return;
105
+ }
106
+ if (expression.type === "ConditionalExpression") {
107
+ appendExpression(asNode(expression.consequent), originFile, parentId, depth, true, outletChild);
108
+ appendExpression(asNode(expression.alternate), originFile, parentId, depth, true, outletChild);
109
+ return;
110
+ }
111
+ if (expression.type === "LogicalExpression") {
112
+ appendExpression(asNode(expression.right), originFile, parentId, depth, true, outletChild);
113
+ return;
114
+ }
115
+ if (expression.type === "ArrayExpression") {
116
+ for (const element of nodeArray(expression, "elements")) {
117
+ appendExpression(element, originFile, parentId, depth, dynamic, outletChild);
118
+ }
119
+ return;
120
+ }
121
+ if (expression.type === "CallExpression") {
122
+ const callee = asNode(expression.callee);
123
+ const property = callee === undefined || callee.type !== "MemberExpression"
124
+ ? undefined
125
+ : asNode(callee.property);
126
+ if (property !== undefined && typeof property.name === "string" && property.name === "map") {
127
+ const callback = nodeArray(expression, "arguments")[0];
128
+ const body = callback === undefined ? undefined : asNode(callback.body);
129
+ const returned = body === undefined
130
+ ? undefined
131
+ : body.type === "BlockStatement"
132
+ ? returnedExpression(body)
133
+ : body;
134
+ appendExpression(returned, originFile, parentId, depth, true, outletChild);
135
+ }
136
+ }
137
+ };
138
+ const appendRoot = (root, parentId, depth, dynamic) => {
139
+ if (root === null)
140
+ return;
141
+ appendExpression(root.element, root.originFile, parentId, depth, dynamic, root.outletChild);
142
+ };
143
+ if (input.root !== null) {
144
+ appendRoot(input.root, null, 0, false);
145
+ }
146
+ else if (input.pageName !== null) {
147
+ const file = graph.file(pagePath);
148
+ if (file !== undefined) {
149
+ const id = addNode(null, input.pageName, "component", pagePath, lineRef(graph, pagePath, file.program), false);
150
+ const expression = id === null ? undefined : componentExpression(graph, pagePath, input.pageName);
151
+ if (id !== null && expression !== undefined)
152
+ appendExpression(expression, pagePath, id, 1, false, null);
153
+ }
154
+ }
155
+ return { routePaths: input.routePaths, nodes, truncated };
156
+ }
157
+ export function pageComponentName(graph, pagePath) {
158
+ return firstComponentName(graph, pagePath);
159
+ }
@@ -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[];
@@ -0,0 +1,151 @@
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 { identifierName, objectEntries, propertyName, propertyValue, unwrapExpression, } from "../ast/node.js";
10
+ import { declarationNames, defaultExportDeclaration, localInitializer } from "../module-info.js";
11
+ import { sourceRef } from "../source.js";
12
+ import { evaluateStatic } from "../static-eval.js";
13
+ const MAX_RESOLVE_DEPTH = 8;
14
+ export function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ export function projectPath(root, path) {
18
+ return root.length === 0 ? path : root + "/" + path;
19
+ }
20
+ /** Project-relative path with the graph root stripped (the opposite of projectPath). */
21
+ export function relativePath(root, path) {
22
+ return root.length === 0 || !path.startsWith(root + "/") ? path : path.slice(root.length + 1);
23
+ }
24
+ export function refOf(context, node) {
25
+ const file = context.graph.file(context.file);
26
+ const offset = node !== undefined && typeof node.start === "number" ? node.start : 0;
27
+ return sourceRef(context.file, file?.source ?? "", offset);
28
+ }
29
+ function objectLiteral(node, context, depth = 0) {
30
+ if (node === undefined || depth > MAX_RESOLVE_DEPTH)
31
+ return undefined;
32
+ const unwrapped = unwrapExpression(node);
33
+ if (unwrapped === undefined)
34
+ return undefined;
35
+ if (unwrapped.type === "ObjectExpression")
36
+ return unwrapped;
37
+ const file = context.graph.file(context.file);
38
+ const name = identifierName(unwrapped);
39
+ if (file === undefined || name === undefined)
40
+ return undefined;
41
+ return objectLiteral(localInitializer(file.program, name), context, depth + 1);
42
+ }
43
+ /** Partial, non-executing reader for object literals in the static subset. */
44
+ export function readStaticObject(node, context, options) {
45
+ const fields = new Map();
46
+ const unknown = [];
47
+ if (node === undefined)
48
+ return { fields, unknown };
49
+ const record = (reason, at, structural) => {
50
+ if (structural || options.report) {
51
+ unknown.push({ code: options.code, reason, source: refOf(context, at) });
52
+ }
53
+ };
54
+ const literal = objectLiteral(node, context);
55
+ if (literal !== undefined) {
56
+ for (const entry of objectEntries(literal)) {
57
+ if (entry.type === "SpreadElement") {
58
+ record("spread in an object literal cannot be read statically", entry, true);
59
+ continue;
60
+ }
61
+ const key = propertyName(entry);
62
+ if (key === undefined) {
63
+ record("computed object key cannot be read statically", entry, true);
64
+ continue;
65
+ }
66
+ const valueNode = propertyValue(entry);
67
+ const evaluated = evaluateStatic(valueNode, context);
68
+ if (evaluated.ok) {
69
+ fields.set(key, { value: evaluated.value, ok: true, node: valueNode, reason: null });
70
+ }
71
+ else {
72
+ fields.set(key, { value: undefined, ok: false, node: valueNode, reason: evaluated.reason });
73
+ record(evaluated.reason, valueNode ?? entry, false);
74
+ }
75
+ }
76
+ return { fields, unknown };
77
+ }
78
+ const evaluated = evaluateStatic(node, context);
79
+ if (evaluated.ok && isRecord(evaluated.value)) {
80
+ for (const [key, value] of Object.entries(evaluated.value)) {
81
+ fields.set(key, { value, ok: true, node: undefined, reason: null });
82
+ }
83
+ return { fields, unknown };
84
+ }
85
+ record(evaluated.ok ? "value is not an object" : evaluated.reason, node, true);
86
+ return { fields, unknown };
87
+ }
88
+ /**
89
+ * The default export, unwrapped and resolved through a same-file const or a
90
+ * `export { handler as default }` statement.
91
+ */
92
+ export function defaultExportExpression(file) {
93
+ const declaration = defaultExportDeclaration(file.program);
94
+ if (declaration !== undefined) {
95
+ const unwrapped = unwrapExpression(declaration);
96
+ const name = unwrapped?.type === "Identifier" ? identifierName(unwrapped) : undefined;
97
+ return name === undefined ? unwrapped : unwrapExpression(localInitializer(file.program, name));
98
+ }
99
+ const entry = file.module.exports.find((item) => item.exported === "default" && item.local !== null);
100
+ if (entry === undefined || entry.local === null)
101
+ return undefined;
102
+ return unwrapExpression(localInitializer(file.program, entry.local));
103
+ }
104
+ /** A top-level function/class declaration by name (localInitializer covers variables only). */
105
+ export function findDeclaration(program, name) {
106
+ for (const raw of Array.isArray(program.body) ? program.body : []) {
107
+ if (typeof raw !== "object" || raw === null)
108
+ continue;
109
+ const statement = raw;
110
+ const declaration = statement.type === "ExportNamedDeclaration" &&
111
+ statement.declaration !== null &&
112
+ typeof statement.declaration === "object"
113
+ ? statement.declaration
114
+ : statement;
115
+ if ((declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") &&
116
+ identifierName(declaration.id) === name) {
117
+ return declaration;
118
+ }
119
+ }
120
+ return undefined;
121
+ }
122
+ /** True when the module declares the name itself: a local shadow invalidates an import. */
123
+ export function isLocallyDeclared(file, name) {
124
+ const body = Array.isArray(file.program.body) ? file.program.body : [];
125
+ for (const raw of body) {
126
+ if (typeof raw !== "object" || raw === null)
127
+ continue;
128
+ const statement = raw;
129
+ const declaration = statement.type === "ExportNamedDeclaration" &&
130
+ statement.declaration !== null &&
131
+ typeof statement.declaration === "object"
132
+ ? statement.declaration
133
+ : statement;
134
+ if (declarationNames(declaration).includes(name))
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ /** Stable diagnostic order (file, line, code, message) so snapshots never flap. */
140
+ export function sortDiagnostics(diagnostics) {
141
+ return [...diagnostics].toSorted((left, right) => {
142
+ const fileOrder = (left.source?.file ?? "").localeCompare(right.source?.file ?? "");
143
+ if (fileOrder !== 0)
144
+ return fileOrder;
145
+ const lineOrder = (left.source?.line ?? 0) - (right.source?.line ?? 0);
146
+ if (lineOrder !== 0)
147
+ return lineOrder;
148
+ const codeOrder = left.code.localeCompare(right.code);
149
+ return codeOrder !== 0 ? codeOrder : left.message.localeCompare(right.message);
150
+ });
151
+ }
@@ -0,0 +1,20 @@
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
+ import type { ModuleGraph } from "../module-graph.js";
14
+ import type { Diagnostic, ViewFact } from "../types.js";
15
+ export interface ViewScan {
16
+ readonly views: readonly ViewFact[];
17
+ readonly diagnostics: readonly Diagnostic[];
18
+ }
19
+ /** Read `db/views/**` into view facts. */
20
+ export declare function buildViewFacts(graph: ModuleGraph): ViewScan;