@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,167 @@
1
+ // The project graph's edges (D-092, views since D-160), split from graph.ts so both files stay
2
+ // under the 300-line source limit. Every edge points at a node the same analysis produced: the
3
+ // caller passes the node ids and an edge whose endpoint is missing is dropped, because a
4
+ // declaration that names a relation the project does not declare is not a graph fact.
5
+ import type { GraphEdge, RouteFact, TableFact } from "./types.ts";
6
+ import type { GraphInput } from "./graph.ts";
7
+ import { pageId, routeId, tableId, viewId } from "./graph-ids.ts";
8
+
9
+ function fkLabel(
10
+ table: TableFact,
11
+ targetTable: string,
12
+ targetColumns: readonly string[],
13
+ onDelete: string | null,
14
+ ): string {
15
+ let label = table.name + " -> " + targetTable + "." + targetColumns.join(", ");
16
+ if (onDelete !== null) label += " on delete " + onDelete;
17
+ return label;
18
+ }
19
+
20
+ function relationLabel(cardinality: "one" | "many"): string {
21
+ return cardinality;
22
+ }
23
+
24
+ function buildTableLookupMap(tables: readonly TableFact[]): Map<string, TableFact> {
25
+ const byName = new Map<string, TableFact>();
26
+ for (const table of tables) {
27
+ if (!byName.has(table.name)) byName.set(table.name, table);
28
+ }
29
+ return byName;
30
+ }
31
+
32
+ function elevatedTables(route: RouteFact): ReadonlySet<string> {
33
+ const tables = new Set<string>();
34
+ for (const elevation of route.elevations) {
35
+ for (const table of elevation.tables) tables.add(table);
36
+ }
37
+ return tables;
38
+ }
39
+
40
+ export function collectEdges(input: GraphInput, nodeIds: ReadonlySet<string>): GraphEdge[] {
41
+ const edges = new Map<string, GraphEdge>();
42
+ const tablesByName = buildTableLookupMap(input.tables);
43
+ // Tables and views share the SQL namespace, so a relation name is at most one of the two.
44
+ const viewNames = new Set(input.views.map((view) => view.name));
45
+ const add = (edge: GraphEdge): void => {
46
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) return;
47
+ if (edges.has(edge.id)) return;
48
+ edges.set(edge.id, edge);
49
+ };
50
+
51
+ for (const table of input.tables) {
52
+ for (const foreignKey of table.foreignKeys) {
53
+ add({
54
+ id:
55
+ "fk:" +
56
+ table.name +
57
+ ":" +
58
+ foreignKey.columns.join(",") +
59
+ ":" +
60
+ foreignKey.targetTable +
61
+ ":" +
62
+ foreignKey.targetColumns.join(","),
63
+ kind: "fk",
64
+ source: tableId(table.name),
65
+ target: tableId(foreignKey.targetTable),
66
+ label: fkLabel(
67
+ table,
68
+ foreignKey.targetTable,
69
+ foreignKey.targetColumns,
70
+ foreignKey.onDelete,
71
+ ),
72
+ });
73
+ }
74
+ }
75
+
76
+ for (const relation of input.relations) {
77
+ const id = ["relation", relation.from, relation.to, relation.cardinality];
78
+ if (relation.fields.length > 0) id.push(relation.fields.join(","));
79
+ if (relation.references.length > 0) id.push(relation.references.join(","));
80
+ add({
81
+ id: id.join(":"),
82
+ kind: "relation",
83
+ source: tableId(relation.from),
84
+ target: tableId(relation.to),
85
+ label: relationLabel(relation.cardinality),
86
+ });
87
+ }
88
+
89
+ // A view depends on the base tables its declaration names (metadata only, D-097). The target
90
+ // is a view when a view declares another view as its base, so the edge lands on the node that
91
+ // exists rather than on a phantom table.
92
+ for (const view of input.views) {
93
+ const source = viewId(view.name);
94
+ for (const base of view.tables) {
95
+ if (base === view.name) continue;
96
+ add({
97
+ id: "view-base:" + view.name + ":" + base,
98
+ kind: "view-base",
99
+ source,
100
+ target: viewNames.has(base) ? viewId(base) : tableId(base),
101
+ label: null,
102
+ });
103
+ }
104
+ }
105
+
106
+ for (const route of input.routes) {
107
+ const routeNodeId = routeId(route);
108
+ const elevated = elevatedTables(route);
109
+ // A route declares every relation it touches in dbTables (D-157), tables and views alike:
110
+ // the target kind is decided by the relation the name denotes.
111
+ for (const name of route.dbTables ?? []) {
112
+ const isView = viewNames.has(name);
113
+ add({
114
+ id: (isView ? "route-view:" : "route-table:") + routeNodeId + ":dbTables:" + name,
115
+ kind: isView ? "route-view" : "route-table",
116
+ source: routeNodeId,
117
+ target: isView ? viewId(name) : tableId(name),
118
+ label: "dbTables",
119
+ ...(elevated.has(name) ? { access: "elevated" as const } : {}),
120
+ });
121
+ }
122
+ for (const entry of route.dbColumns ?? []) {
123
+ const separator = entry.indexOf(".");
124
+ if (separator <= 0) continue;
125
+ const tableName = entry.slice(0, separator);
126
+ const columnName = entry.slice(separator + 1);
127
+ const table = tablesByName.get(tableName);
128
+ const knownColumn = table?.columns.some((column) => column.property === columnName);
129
+ if (knownColumn !== true) continue;
130
+ add({
131
+ id: "route-table:" + routeNodeId + ":dbColumns:" + tableName,
132
+ kind: "route-table",
133
+ source: routeNodeId,
134
+ target: tableId(tableName),
135
+ label: "dbColumns",
136
+ ...(elevated.has(tableName) ? { access: "elevated" as const } : {}),
137
+ });
138
+ }
139
+ }
140
+
141
+ for (const page of input.pages) {
142
+ const source = pageId(page.path);
143
+ for (const call of page.calls) {
144
+ for (const matchedRouteId of call.matchedRouteIds) {
145
+ add({
146
+ id: "page-route:" + page.path + ":" + matchedRouteId,
147
+ kind: "page-route",
148
+ source,
149
+ target: matchedRouteId,
150
+ label: null,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ for (const link of input.screens?.links ?? []) {
157
+ add({
158
+ id: "page-link:" + link.from + ":" + link.to + ":" + link.kind + ":" + link.toPath,
159
+ kind: "page-link",
160
+ source: pageId(link.from),
161
+ target: pageId(link.to),
162
+ label: link.toPath,
163
+ });
164
+ }
165
+
166
+ return [...edges.values()];
167
+ }
@@ -0,0 +1,31 @@
1
+ // The project graph's node ids (D-092): one spelling shared by the node builder in graph.ts
2
+ // and the edge builder in graph-edges.ts, so an edge endpoint can never drift from its node.
3
+ import type { RouteFact } from "./types.ts";
4
+
5
+ const NODE_PREFIX = {
6
+ page: "page:",
7
+ route: "route:",
8
+ table: "table:",
9
+ view: "view:",
10
+ workflow: "workflow:",
11
+ } as const;
12
+
13
+ export function tableId(name: string): string {
14
+ return NODE_PREFIX.table + name;
15
+ }
16
+
17
+ export function viewId(name: string): string {
18
+ return NODE_PREFIX.view + name;
19
+ }
20
+
21
+ export function routeId(route: RouteFact): string {
22
+ return NODE_PREFIX.route + route.method + " " + route.path;
23
+ }
24
+
25
+ export function pageId(path: string): string {
26
+ return NODE_PREFIX.page + path;
27
+ }
28
+
29
+ export function workflowId(name: string): string {
30
+ return NODE_PREFIX.workflow + name;
31
+ }
package/src/graph.ts ADDED
@@ -0,0 +1,190 @@
1
+ import type {
2
+ GraphNode,
3
+ PageFact,
4
+ PageNodeData,
5
+ ProjectGraph,
6
+ RelationFact,
7
+ RouteFact,
8
+ RouteNodeData,
9
+ ScreenFacts,
10
+ TableFact,
11
+ TableNodeData,
12
+ ViewFact,
13
+ ViewNodeData,
14
+ WorkflowFact,
15
+ WorkflowNodeData,
16
+ } from "./types.ts";
17
+ import { pageId, routeId, tableId, viewId, workflowId } from "./graph-ids.ts";
18
+ import { collectEdges } from "./graph-edges.ts";
19
+
20
+ export interface GraphInput {
21
+ readonly routes: readonly RouteFact[];
22
+ readonly tables: readonly TableFact[];
23
+ readonly relations: readonly RelationFact[];
24
+ readonly pages: readonly PageFact[];
25
+ /** Declarative views (D-097/D-098): nodes plus view-base edges since D-160. */
26
+ readonly views: readonly ViewFact[];
27
+ /** Route paths, bounded page previews and explicit page links. */
28
+ readonly screens?: ScreenFacts;
29
+ /** Workflow definitions read from `server/workflows/**` (D-135 W3). */
30
+ readonly workflows?: readonly WorkflowFact[];
31
+ }
32
+
33
+ function tableNode(table: TableFact): GraphNode {
34
+ const data: TableNodeData = {
35
+ kind: "table",
36
+ table: table.name,
37
+ columns: table.columns,
38
+ rlsEnabled: table.rlsEnabled,
39
+ scope: table.scope,
40
+ framework: table.source.file.startsWith("node_modules/"),
41
+ unknown:
42
+ table.unknown.length > 0 ||
43
+ table.columns.some((column) => column.unknown.length > 0) ||
44
+ table.policies.some((policy) => policy.unknown.length > 0),
45
+ };
46
+ return {
47
+ id: tableId(table.name),
48
+ kind: "table",
49
+ label: table.name,
50
+ sourceFile: table.source.file,
51
+ data,
52
+ };
53
+ }
54
+
55
+ function viewNode(view: ViewFact): GraphNode {
56
+ const data: ViewNodeData = {
57
+ kind: "view",
58
+ view: view.name,
59
+ tables: view.tables,
60
+ permission: view.permission,
61
+ access: view.access,
62
+ unknown: view.unknown.length > 0 || view.access === null,
63
+ };
64
+ return {
65
+ id: viewId(view.name),
66
+ kind: "view",
67
+ label: view.name,
68
+ sourceFile: view.source.file,
69
+ data,
70
+ };
71
+ }
72
+
73
+ function workflowNode(workflow: WorkflowFact): GraphNode {
74
+ const data: WorkflowNodeData = {
75
+ kind: "workflow",
76
+ name: workflow.name,
77
+ steps: workflow.steps.map((step) => ({
78
+ index: step.index,
79
+ kind: step.kind,
80
+ name: step.name,
81
+ permission: step.permission,
82
+ onRejectStep: step.onRejectStep,
83
+ })),
84
+ unknown: workflow.unknown.length > 0 || workflow.steps.some((step) => step.name === null),
85
+ };
86
+ return {
87
+ id: workflowId(workflow.name),
88
+ kind: "workflow",
89
+ label: workflow.name,
90
+ sourceFile: workflow.sourceFile,
91
+ data,
92
+ };
93
+ }
94
+
95
+ function routeNode(route: RouteFact, id: string): GraphNode {
96
+ const data: RouteNodeData = {
97
+ kind: "route",
98
+ method: route.method,
99
+ path: route.path,
100
+ permission: route.permission ?? [],
101
+ wrapper: route.wrapper,
102
+ input: route.input,
103
+ dbTables: route.dbTables ?? [],
104
+ dbColumns: route.dbColumns ?? [],
105
+ elevations: route.elevations,
106
+ description: route.description,
107
+ unknown:
108
+ route.unknown.length > 0 ||
109
+ route.wrapper === "unknown" ||
110
+ route.permission === null ||
111
+ route.dbTables === null,
112
+ };
113
+ return {
114
+ id,
115
+ kind: "route",
116
+ label: route.method + " " + route.path,
117
+ sourceFile: route.sourceFile,
118
+ data,
119
+ };
120
+ }
121
+
122
+ function pageNode(page: PageFact, id: string, screens: ScreenFacts): GraphNode {
123
+ const preview = screens.previews[page.path];
124
+ const data: PageNodeData = {
125
+ kind: "page",
126
+ pagePath: page.path,
127
+ pageKind: page.kind,
128
+ callCount: page.calls.length,
129
+ ...(preview === undefined ? {} : { routePaths: preview.routePaths, preview }),
130
+ };
131
+ return {
132
+ id,
133
+ kind: "page",
134
+ label: page.path,
135
+ sourceFile: page.path,
136
+ data,
137
+ };
138
+ }
139
+
140
+ function collectNodes(input: GraphInput): GraphNode[] {
141
+ const nodes: GraphNode[] = [];
142
+ const tableNames = new Set<string>();
143
+ for (const table of input.tables) {
144
+ if (tableNames.has(table.name)) continue;
145
+ tableNames.add(table.name);
146
+ nodes.push(tableNode(table));
147
+ }
148
+ const viewNames = new Set<string>();
149
+ for (const view of input.views) {
150
+ if (viewNames.has(view.name)) continue;
151
+ viewNames.add(view.name);
152
+ nodes.push(viewNode(view));
153
+ }
154
+ const routeIds = new Set<string>();
155
+ for (const route of input.routes) {
156
+ const id = routeId(route);
157
+ if (routeIds.has(id)) continue;
158
+ routeIds.add(id);
159
+ nodes.push(routeNode(route, id));
160
+ }
161
+ const pageIds = new Set<string>();
162
+ const screens = input.screens ?? { previews: {}, links: [] };
163
+ for (const page of input.pages) {
164
+ // The graph's page nodes are real pages only (the user's "pagine e basta"): components and
165
+ // lib files that happen to call an API stay analyzer facts, not boxes on the canvas.
166
+ if (page.kind !== "page") continue;
167
+ const id = pageId(page.path);
168
+ if (pageIds.has(id)) continue;
169
+ pageIds.add(id);
170
+ nodes.push(pageNode(page, id, screens));
171
+ }
172
+ const workflowIds = new Set<string>();
173
+ for (const workflow of input.workflows ?? []) {
174
+ const id = workflowId(workflow.name);
175
+ if (workflowIds.has(id)) continue;
176
+ workflowIds.add(id);
177
+ nodes.push(workflowNode(workflow));
178
+ }
179
+ return nodes;
180
+ }
181
+
182
+ /** Build the single project graph from facts. Ids and ordering are stable across runs. */
183
+ export function buildProjectGraph(input: GraphInput): ProjectGraph {
184
+ const nodes = collectNodes(input);
185
+ const nodeIds = new Set(nodes.map((node) => node.id));
186
+ const edges = collectEdges(input, nodeIds);
187
+ nodes.sort((left, right) => left.id.localeCompare(right.id));
188
+ edges.sort((left, right) => left.id.localeCompare(right.id));
189
+ return { nodes, edges };
190
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @facilis.dev/analyze — static facts for the Facilis project DSL.
3
+ *
4
+ * Zero build-time execution: the caller supplies a file map and an injected parser; the
5
+ * analyzer interprets the declared DSL (routes, config, Drizzle schema, relations, pages).
6
+ * The Node parser adapter lives on the `@facilis.dev/analyze/node` subpath so the main entry
7
+ * stays browser-safe.
8
+ */
9
+ export { analyzeProject, DEFAULT_TRANSPORT } from "./analyze.ts";
10
+ export type { AnalyzeInput } from "./analyze.ts";
11
+ export { buildProjectGraph } from "./graph.ts";
12
+ export type { GraphInput } from "./graph.ts";
13
+ export { ROUTE_DESCRIPTION_MAX_CHARS } from "./passes/route-description.ts";
14
+ export { createModuleGraph } from "./module-graph.ts";
15
+ export type { Binding, ModuleGraph, ProjectFile } from "./module-graph.ts";
16
+ export { createPathResolver, normalizePath } from "./path-resolver.ts";
17
+ export type { PathAlias } from "./path-resolver.ts";
18
+ export type { Parser, ParseError, ParsedModule } from "./parser.ts";
19
+ export { diagnostic, error, info, warning } from "./diagnostics.ts";
20
+ export { computeRevision } from "./revision.ts";
21
+ export type { TransportSpec } from "./passes/clients.ts";
22
+ // Job facts live in types-jobs.ts (the 300-line split of types.ts), so export them here.
23
+ export type { JobFact, JobRelationsFact } from "./types-jobs.ts";
24
+ export type * from "./types.ts";
package/src/json.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { isAstNode } from "./ast/node.ts";
2
+
3
+ /** Parse a JSON file from the project file map; null when absent or malformed. */
4
+ export function readJson(files: ReadonlyMap<string, string>, path: string): unknown {
5
+ const source = files.get(path);
6
+ if (source === undefined) return null;
7
+ try {
8
+ return JSON.parse(source) as unknown;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ export function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ /** Module source extensions the analyzer can parse. */
19
+ export const SOURCE_EXTENSIONS = [
20
+ ".ts",
21
+ ".tsx",
22
+ ".js",
23
+ ".jsx",
24
+ ".mjs",
25
+ ".cjs",
26
+ ".mts",
27
+ ".cts",
28
+ ] as const;
29
+
30
+ export function isSourceFile(path: string): boolean {
31
+ return SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension));
32
+ }
33
+
34
+ export { isAstNode };
@@ -0,0 +1,117 @@
1
+ import { isSourceFile, readJson as parseJson } from "./json.ts";
2
+ import { parseModuleInfo, type ModuleInfo } from "./module-info.ts";
3
+ import { createPathResolver, normalizePath, type PathAlias } from "./path-resolver.ts";
4
+ import type { ParseError, Parser } from "./parser.ts";
5
+ import type { AstNode } from "./ast/node.ts";
6
+
7
+ export interface ProjectFile {
8
+ readonly path: string;
9
+ readonly source: string;
10
+ readonly program: AstNode;
11
+ readonly errors: readonly ParseError[];
12
+ readonly module: ModuleInfo;
13
+ }
14
+
15
+ export interface Binding {
16
+ readonly file: string;
17
+ readonly name: string;
18
+ }
19
+
20
+ export interface ModuleGraph {
21
+ readonly files: ReadonlyMap<string, ProjectFile>;
22
+ /** Every input file (including JSON/tsconfig/package.json), path -> source. */
23
+ readonly allFiles: ReadonlyMap<string, string>;
24
+ readonly root: string;
25
+ file(path: string): ProjectFile | undefined;
26
+ resolve(from: string, specifier: string): string | null;
27
+ resolveBinding(file: string, local: string): Binding | null;
28
+ resolveExport(file: string, name: string): Binding | null;
29
+ readJson(path: string): unknown;
30
+ }
31
+
32
+ export interface ModuleGraphInput {
33
+ readonly files: ReadonlyMap<string, string>;
34
+ readonly parser: Parser;
35
+ readonly root?: string;
36
+ readonly aliases?: readonly PathAlias[];
37
+ }
38
+
39
+ export function createModuleGraph(input: ModuleGraphInput): ModuleGraph {
40
+ const root = normalizePath(input.root ?? "");
41
+ const sources = new Map<string, string>();
42
+ const files = new Map<string, ProjectFile>();
43
+ for (const [rawPath, source] of input.files) {
44
+ const path = normalizePath(rawPath);
45
+ sources.set(path, source);
46
+ if (!isSourceFile(path)) continue;
47
+ const parsed = input.parser.parse(path, source);
48
+ files.set(path, {
49
+ path,
50
+ source,
51
+ program: parsed.program,
52
+ errors: parsed.errors,
53
+ module: parseModuleInfo(path, source, parsed.program),
54
+ });
55
+ }
56
+
57
+ const resolver = createPathResolver({
58
+ exists: (path) => sources.has(path),
59
+ readJson: (path) => parseJson(sources, path),
60
+ root,
61
+ ...(input.aliases === undefined ? {} : { aliases: input.aliases }),
62
+ });
63
+
64
+ const resolveExport = (file: string, name: string, seen = new Set<string>()): Binding | null => {
65
+ const key = file + "#" + name;
66
+ if (seen.has(key)) return null;
67
+ seen.add(key);
68
+ const module = files.get(file);
69
+ if (module === undefined) return null;
70
+ const direct = module.module.exports.find((entry) => entry.exported === name);
71
+ if (direct !== undefined) {
72
+ if (direct.source !== null) {
73
+ const target = resolver.resolve(file, direct.source);
74
+ if (target === null) return null;
75
+ if (direct.kind === "all") return resolveExport(target, "*", seen);
76
+ return resolveExport(target, direct.imported ?? name, seen);
77
+ }
78
+ if (direct.local !== null) return resolveBinding(file, direct.local, seen);
79
+ return { file, name };
80
+ }
81
+ for (const entry of module.module.exports) {
82
+ if (entry.kind !== "all" || entry.source === null) continue;
83
+ const target = resolver.resolve(file, entry.source);
84
+ if (target !== null) {
85
+ const found = resolveExport(target, name, new Set(seen));
86
+ if (found !== null) return found;
87
+ }
88
+ }
89
+ return null;
90
+ };
91
+
92
+ const resolveBinding = (
93
+ file: string,
94
+ local: string,
95
+ seen = new Set<string>(),
96
+ ): Binding | null => {
97
+ const module = files.get(file);
98
+ if (module === undefined) return null;
99
+ const imported = module.module.imports.find((entry) => entry.local === local);
100
+ if (imported === undefined) return { file, name: local };
101
+ if (imported.kind === "namespace") return null;
102
+ const target = resolver.resolve(file, imported.specifier);
103
+ if (target === null) return null;
104
+ return resolveExport(target, imported.imported ?? "default", new Set(seen));
105
+ };
106
+
107
+ return {
108
+ files,
109
+ allFiles: sources,
110
+ root,
111
+ file: (path) => files.get(normalizePath(path)),
112
+ resolve: resolver.resolve,
113
+ resolveBinding: (file, local) => resolveBinding(normalizePath(file), local),
114
+ resolveExport: (file, name) => resolveExport(normalizePath(file), name),
115
+ readJson: (path) => parseJson(sources, normalizePath(path)),
116
+ };
117
+ }