@facilis.dev/analyze 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/analyze.d.ts +17 -0
- package/dist/analyze.js +99 -0
- package/dist/ast/node.d.ts +29 -0
- package/dist/ast/node.js +85 -0
- package/dist/ast/walk.d.ts +8 -0
- package/dist/ast/walk.js +40 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +12 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.js +51 -0
- package/dist/graph-edges.d.ts +3 -0
- package/dist/graph-edges.js +148 -0
- package/dist/graph-ids.d.ts +6 -0
- package/dist/graph-ids.js +22 -0
- package/dist/graph.d.ts +15 -0
- package/dist/graph.js +162 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +17 -0
- package/dist/json.d.ts +8 -0
- package/dist/json.js +31 -0
- package/dist/module-graph.d.ts +33 -0
- package/dist/module-graph.js +86 -0
- package/dist/module-info.d.ts +27 -0
- package/dist/module-info.js +143 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +41 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +1 -0
- package/dist/passes/clients.d.ts +22 -0
- package/dist/passes/clients.js +167 -0
- package/dist/passes/config.d.ts +16 -0
- package/dist/passes/config.js +229 -0
- package/dist/passes/file-facts.d.ts +13 -0
- package/dist/passes/file-facts.js +130 -0
- package/dist/passes/i18n.d.ts +14 -0
- package/dist/passes/i18n.js +55 -0
- package/dist/passes/job-relations-diagnostics.d.ts +9 -0
- package/dist/passes/job-relations-diagnostics.js +38 -0
- package/dist/passes/job-relations.d.ts +18 -0
- package/dist/passes/job-relations.js +52 -0
- package/dist/passes/jobs.d.ts +20 -0
- package/dist/passes/jobs.js +133 -0
- package/dist/passes/jsx-text.d.ts +28 -0
- package/dist/passes/jsx-text.js +121 -0
- package/dist/passes/jsx.d.ts +24 -0
- package/dist/passes/jsx.js +135 -0
- package/dist/passes/path-expression.d.ts +26 -0
- package/dist/passes/path-expression.js +265 -0
- package/dist/passes/route-config.d.ts +24 -0
- package/dist/passes/route-config.js +152 -0
- package/dist/passes/route-description.d.ts +13 -0
- package/dist/passes/route-description.js +23 -0
- package/dist/passes/route-elevation.d.ts +18 -0
- package/dist/passes/route-elevation.js +136 -0
- package/dist/passes/route-handler.d.ts +27 -0
- package/dist/passes/route-handler.js +87 -0
- package/dist/passes/route-location.d.ts +16 -0
- package/dist/passes/route-location.js +58 -0
- package/dist/passes/route-relation-resolve.d.ts +51 -0
- package/dist/passes/route-relation-resolve.js +172 -0
- package/dist/passes/route-relations-diagnostics.d.ts +15 -0
- package/dist/passes/route-relations-diagnostics.js +79 -0
- package/dist/passes/route-relations-follow.d.ts +16 -0
- package/dist/passes/route-relations-follow.js +116 -0
- package/dist/passes/route-relations-usage.d.ts +50 -0
- package/dist/passes/route-relations-usage.js +44 -0
- package/dist/passes/route-relations-walk.d.ts +25 -0
- package/dist/passes/route-relations-walk.js +199 -0
- package/dist/passes/route-relations.d.ts +17 -0
- package/dist/passes/route-relations.js +43 -0
- package/dist/passes/route-table.d.ts +21 -0
- package/dist/passes/route-table.js +104 -0
- package/dist/passes/route-tables.d.ts +10 -0
- package/dist/passes/route-tables.js +42 -0
- package/dist/passes/routes.d.ts +18 -0
- package/dist/passes/routes.js +152 -0
- package/dist/passes/schema-column-types.d.ts +9 -0
- package/dist/passes/schema-column-types.js +55 -0
- package/dist/passes/schema-columns.d.ts +35 -0
- package/dist/passes/schema-columns.js +197 -0
- package/dist/passes/schema-dsl.d.ts +24 -0
- package/dist/passes/schema-dsl.js +117 -0
- package/dist/passes/schema-file-links-ddl.d.ts +18 -0
- package/dist/passes/schema-file-links-ddl.js +106 -0
- package/dist/passes/schema-file-links.d.ts +19 -0
- package/dist/passes/schema-file-links.js +144 -0
- package/dist/passes/schema-foreign-keys.d.ts +21 -0
- package/dist/passes/schema-foreign-keys.js +80 -0
- package/dist/passes/schema-policies.d.ts +15 -0
- package/dist/passes/schema-policies.js +201 -0
- package/dist/passes/schema-re-exports.d.ts +5 -0
- package/dist/passes/schema-re-exports.js +77 -0
- package/dist/passes/schema-relations.d.ts +8 -0
- package/dist/passes/schema-relations.js +182 -0
- package/dist/passes/schema-scope.d.ts +7 -0
- package/dist/passes/schema-scope.js +20 -0
- package/dist/passes/schema-tables.d.ts +13 -0
- package/dist/passes/schema-tables.js +149 -0
- package/dist/passes/schema.d.ts +9 -0
- package/dist/passes/schema.js +55 -0
- package/dist/passes/screen-components.d.ts +22 -0
- package/dist/passes/screen-components.js +143 -0
- package/dist/passes/screen-intrinsics.d.ts +18 -0
- package/dist/passes/screen-intrinsics.js +21 -0
- package/dist/passes/screen-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +174 -0
- package/dist/passes/screens.d.ts +12 -0
- package/dist/passes/screens.js +278 -0
- package/dist/passes/static-read.d.ts +47 -0
- package/dist/passes/static-read.js +151 -0
- package/dist/passes/views.d.ts +20 -0
- package/dist/passes/views.js +101 -0
- package/dist/passes/workflow-tables.d.ts +18 -0
- package/dist/passes/workflow-tables.js +71 -0
- package/dist/passes/workflows.d.ts +8 -0
- package/dist/passes/workflows.js +160 -0
- package/dist/path-resolver.d.ts +20 -0
- package/dist/path-resolver.js +134 -0
- package/dist/revision.d.ts +2 -0
- package/dist/revision.js +23 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.js +13 -0
- package/dist/static-eval.d.ts +36 -0
- package/dist/static-eval.js +215 -0
- package/dist/types-graph.d.ts +144 -0
- package/dist/types-graph.js +4 -0
- package/dist/types-jobs.d.ts +28 -0
- package/dist/types-jobs.js +14 -0
- package/dist/types-relations.d.ts +49 -0
- package/dist/types-relations.js +11 -0
- package/dist/types-workflows.d.ts +36 -0
- package/dist/types-workflows.js +3 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.js +1 -0
- package/package.json +49 -4
- package/src/analyze.ts +119 -0
- package/src/ast/node.ts +102 -0
- package/src/ast/walk.ts +37 -0
- package/src/diagnostics.ts +22 -0
- package/src/fs.ts +59 -0
- package/src/graph-edges.ts +167 -0
- package/src/graph-ids.ts +31 -0
- package/src/graph.ts +194 -0
- package/src/index.ts +26 -0
- package/src/json.ts +34 -0
- package/src/module-graph.ts +117 -0
- package/src/module-info.ts +162 -0
- package/src/node.ts +54 -0
- package/src/parser.ts +19 -0
- package/src/passes/clients.ts +233 -0
- package/src/passes/config.ts +278 -0
- package/src/passes/file-facts.ts +142 -0
- package/src/passes/i18n.ts +66 -0
- package/src/passes/job-relations-diagnostics.ts +60 -0
- package/src/passes/job-relations.ts +76 -0
- package/src/passes/jobs.ts +181 -0
- package/src/passes/jsx-text.ts +126 -0
- package/src/passes/jsx.ts +142 -0
- package/src/passes/path-expression.ts +297 -0
- package/src/passes/route-config.ts +188 -0
- package/src/passes/route-description.ts +33 -0
- package/src/passes/route-elevation.ts +161 -0
- package/src/passes/route-handler.ts +105 -0
- package/src/passes/route-location.ts +62 -0
- package/src/passes/route-relation-resolve.ts +228 -0
- package/src/passes/route-relations-diagnostics.ts +115 -0
- package/src/passes/route-relations-follow.ts +146 -0
- package/src/passes/route-relations-usage.ts +98 -0
- package/src/passes/route-relations-walk.ts +255 -0
- package/src/passes/route-relations.ts +62 -0
- package/src/passes/route-table.ts +123 -0
- package/src/passes/route-tables.ts +50 -0
- package/src/passes/routes.ts +188 -0
- package/src/passes/schema-column-types.ts +62 -0
- package/src/passes/schema-columns.ts +287 -0
- package/src/passes/schema-dsl.ts +136 -0
- package/src/passes/schema-file-links-ddl.ts +125 -0
- package/src/passes/schema-file-links.ts +213 -0
- package/src/passes/schema-foreign-keys.ts +121 -0
- package/src/passes/schema-policies.ts +270 -0
- package/src/passes/schema-re-exports.ts +95 -0
- package/src/passes/schema-relations.ts +239 -0
- package/src/passes/schema-scope.ts +29 -0
- package/src/passes/schema-tables.ts +216 -0
- package/src/passes/schema.ts +67 -0
- package/src/passes/screen-components.ts +162 -0
- package/src/passes/screen-intrinsics.ts +29 -0
- package/src/passes/screen-preview.ts +295 -0
- package/src/passes/screens.ts +291 -0
- package/src/passes/static-read.ts +196 -0
- package/src/passes/views.ts +125 -0
- package/src/passes/workflow-tables.ts +87 -0
- package/src/passes/workflows.ts +213 -0
- package/src/path-resolver.ts +146 -0
- package/src/revision.ts +25 -0
- package/src/source.ts +15 -0
- package/src/static-eval.ts +242 -0
- package/src/types-graph.ts +176 -0
- package/src/types-jobs.ts +40 -0
- package/src/types-relations.ts +64 -0
- package/src/types-workflows.ts +42 -0
- package/src/types.ts +299 -0
package/dist/graph.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { pageId, routeId, tableId, viewId, workflowId } from "./graph-ids.js";
|
|
2
|
+
import { collectEdges } from "./graph-edges.js";
|
|
3
|
+
function tableNode(table) {
|
|
4
|
+
const data = {
|
|
5
|
+
kind: "table",
|
|
6
|
+
table: table.name,
|
|
7
|
+
columns: table.columns,
|
|
8
|
+
rlsEnabled: table.rlsEnabled,
|
|
9
|
+
scope: table.scope,
|
|
10
|
+
policies: table.policies.map((policy) => ({
|
|
11
|
+
name: policy.name,
|
|
12
|
+
restrictive: policy.restrictive,
|
|
13
|
+
})),
|
|
14
|
+
framework: table.source.file.startsWith("node_modules/"),
|
|
15
|
+
unknown: table.unknown.length > 0 ||
|
|
16
|
+
table.columns.some((column) => column.unknown.length > 0) ||
|
|
17
|
+
table.policies.some((policy) => policy.unknown.length > 0),
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
id: tableId(table.name),
|
|
21
|
+
kind: "table",
|
|
22
|
+
label: table.name,
|
|
23
|
+
sourceFile: table.source.file,
|
|
24
|
+
data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function viewNode(view) {
|
|
28
|
+
const data = {
|
|
29
|
+
kind: "view",
|
|
30
|
+
view: view.name,
|
|
31
|
+
tables: view.tables,
|
|
32
|
+
permission: view.permission,
|
|
33
|
+
access: view.access,
|
|
34
|
+
unknown: view.unknown.length > 0 || view.access === null,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
id: viewId(view.name),
|
|
38
|
+
kind: "view",
|
|
39
|
+
label: view.name,
|
|
40
|
+
sourceFile: view.source.file,
|
|
41
|
+
data,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function workflowNode(workflow) {
|
|
45
|
+
const data = {
|
|
46
|
+
kind: "workflow",
|
|
47
|
+
name: workflow.name,
|
|
48
|
+
steps: workflow.steps.map((step) => ({
|
|
49
|
+
index: step.index,
|
|
50
|
+
kind: step.kind,
|
|
51
|
+
name: step.name,
|
|
52
|
+
permission: step.permission,
|
|
53
|
+
onRejectStep: step.onRejectStep,
|
|
54
|
+
})),
|
|
55
|
+
unknown: workflow.unknown.length > 0 || workflow.steps.some((step) => step.name === null),
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
id: workflowId(workflow.name),
|
|
59
|
+
kind: "workflow",
|
|
60
|
+
label: workflow.name,
|
|
61
|
+
sourceFile: workflow.sourceFile,
|
|
62
|
+
data,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function routeNode(route, id) {
|
|
66
|
+
const data = {
|
|
67
|
+
kind: "route",
|
|
68
|
+
method: route.method,
|
|
69
|
+
path: route.path,
|
|
70
|
+
permission: route.permission ?? [],
|
|
71
|
+
wrapper: route.wrapper,
|
|
72
|
+
input: route.input,
|
|
73
|
+
dbTables: route.dbTables ?? [],
|
|
74
|
+
dbColumns: route.dbColumns ?? [],
|
|
75
|
+
elevations: route.elevations,
|
|
76
|
+
description: route.description,
|
|
77
|
+
unknown: route.unknown.length > 0 ||
|
|
78
|
+
route.wrapper === "unknown" ||
|
|
79
|
+
route.permission === null ||
|
|
80
|
+
route.dbTables === null,
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
kind: "route",
|
|
85
|
+
label: route.method + " " + route.path,
|
|
86
|
+
sourceFile: route.sourceFile,
|
|
87
|
+
data,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function pageNode(page, id, screens) {
|
|
91
|
+
const preview = screens.previews[page.path];
|
|
92
|
+
const data = {
|
|
93
|
+
kind: "page",
|
|
94
|
+
pagePath: page.path,
|
|
95
|
+
pageKind: page.kind,
|
|
96
|
+
callCount: page.calls.length,
|
|
97
|
+
...(preview === undefined ? {} : { routePaths: preview.routePaths, preview }),
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
kind: "page",
|
|
102
|
+
label: page.path,
|
|
103
|
+
sourceFile: page.path,
|
|
104
|
+
data,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function collectNodes(input) {
|
|
108
|
+
const nodes = [];
|
|
109
|
+
const tableNames = new Set();
|
|
110
|
+
for (const table of input.tables) {
|
|
111
|
+
if (tableNames.has(table.name))
|
|
112
|
+
continue;
|
|
113
|
+
tableNames.add(table.name);
|
|
114
|
+
nodes.push(tableNode(table));
|
|
115
|
+
}
|
|
116
|
+
const viewNames = new Set();
|
|
117
|
+
for (const view of input.views) {
|
|
118
|
+
if (viewNames.has(view.name))
|
|
119
|
+
continue;
|
|
120
|
+
viewNames.add(view.name);
|
|
121
|
+
nodes.push(viewNode(view));
|
|
122
|
+
}
|
|
123
|
+
const routeIds = new Set();
|
|
124
|
+
for (const route of input.routes) {
|
|
125
|
+
const id = routeId(route);
|
|
126
|
+
if (routeIds.has(id))
|
|
127
|
+
continue;
|
|
128
|
+
routeIds.add(id);
|
|
129
|
+
nodes.push(routeNode(route, id));
|
|
130
|
+
}
|
|
131
|
+
const pageIds = new Set();
|
|
132
|
+
const screens = input.screens ?? { previews: {}, links: [] };
|
|
133
|
+
for (const page of input.pages) {
|
|
134
|
+
// The graph's page nodes are real pages only (the user's "pagine e basta"): components and
|
|
135
|
+
// lib files that happen to call an API stay analyzer facts, not boxes on the canvas.
|
|
136
|
+
if (page.kind !== "page")
|
|
137
|
+
continue;
|
|
138
|
+
const id = pageId(page.path);
|
|
139
|
+
if (pageIds.has(id))
|
|
140
|
+
continue;
|
|
141
|
+
pageIds.add(id);
|
|
142
|
+
nodes.push(pageNode(page, id, screens));
|
|
143
|
+
}
|
|
144
|
+
const workflowIds = new Set();
|
|
145
|
+
for (const workflow of input.workflows ?? []) {
|
|
146
|
+
const id = workflowId(workflow.name);
|
|
147
|
+
if (workflowIds.has(id))
|
|
148
|
+
continue;
|
|
149
|
+
workflowIds.add(id);
|
|
150
|
+
nodes.push(workflowNode(workflow));
|
|
151
|
+
}
|
|
152
|
+
return nodes;
|
|
153
|
+
}
|
|
154
|
+
/** Build the single project graph from facts. Ids and ordering are stable across runs. */
|
|
155
|
+
export function buildProjectGraph(input) {
|
|
156
|
+
const nodes = collectNodes(input);
|
|
157
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
158
|
+
const edges = collectEdges(input, nodeIds);
|
|
159
|
+
nodes.sort((left, right) => left.id.localeCompare(right.id));
|
|
160
|
+
edges.sort((left, right) => left.id.localeCompare(right.id));
|
|
161
|
+
return { nodes, edges };
|
|
162
|
+
}
|
package/dist/index.d.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.js";
|
|
10
|
+
export type { AnalyzeInput } from "./analyze.js";
|
|
11
|
+
export { buildProjectGraph } from "./graph.js";
|
|
12
|
+
export type { GraphInput } from "./graph.js";
|
|
13
|
+
export { ROUTE_DESCRIPTION_MAX_CHARS } from "./passes/route-description.js";
|
|
14
|
+
export { createModuleGraph } from "./module-graph.js";
|
|
15
|
+
export type { Binding, ModuleGraph, ProjectFile } from "./module-graph.js";
|
|
16
|
+
export { createPathResolver, normalizePath } from "./path-resolver.js";
|
|
17
|
+
export type { PathAlias } from "./path-resolver.js";
|
|
18
|
+
export type { Parser, ParseError, ParsedModule } from "./parser.js";
|
|
19
|
+
export { diagnostic, error, info, warning } from "./diagnostics.js";
|
|
20
|
+
export { computeRevision } from "./revision.js";
|
|
21
|
+
export { pageId, routeId, tableId, viewId, workflowId } from "./graph-ids.js";
|
|
22
|
+
export type { TransportSpec } from "./passes/clients.js";
|
|
23
|
+
export type { JobFact, JobRelationsFact } from "./types-jobs.js";
|
|
24
|
+
export type * from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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.js";
|
|
10
|
+
export { buildProjectGraph } from "./graph.js";
|
|
11
|
+
export { ROUTE_DESCRIPTION_MAX_CHARS } from "./passes/route-description.js";
|
|
12
|
+
export { createModuleGraph } from "./module-graph.js";
|
|
13
|
+
export { createPathResolver, normalizePath } from "./path-resolver.js";
|
|
14
|
+
export { diagnostic, error, info, warning } from "./diagnostics.js";
|
|
15
|
+
export { computeRevision } from "./revision.js";
|
|
16
|
+
// The one id spelling shared by graph nodes and edges (D-092), for UI joins by node id.
|
|
17
|
+
export { pageId, routeId, tableId, viewId, workflowId } from "./graph-ids.js";
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isAstNode } from "./ast/node.js";
|
|
2
|
+
/** Parse a JSON file from the project file map; null when absent or malformed. */
|
|
3
|
+
export declare function readJson(files: ReadonlyMap<string, string>, path: string): unknown;
|
|
4
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
5
|
+
/** Module source extensions the analyzer can parse. */
|
|
6
|
+
export declare const SOURCE_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
7
|
+
export declare function isSourceFile(path: string): boolean;
|
|
8
|
+
export { isAstNode };
|
package/dist/json.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isAstNode } from "./ast/node.js";
|
|
2
|
+
/** Parse a JSON file from the project file map; null when absent or malformed. */
|
|
3
|
+
export function readJson(files, path) {
|
|
4
|
+
const source = files.get(path);
|
|
5
|
+
if (source === undefined)
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(source);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
/** Module source extensions the analyzer can parse. */
|
|
18
|
+
export const SOURCE_EXTENSIONS = [
|
|
19
|
+
".ts",
|
|
20
|
+
".tsx",
|
|
21
|
+
".js",
|
|
22
|
+
".jsx",
|
|
23
|
+
".mjs",
|
|
24
|
+
".cjs",
|
|
25
|
+
".mts",
|
|
26
|
+
".cts",
|
|
27
|
+
];
|
|
28
|
+
export function isSourceFile(path) {
|
|
29
|
+
return SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension));
|
|
30
|
+
}
|
|
31
|
+
export { isAstNode };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ModuleInfo } from "./module-info.js";
|
|
2
|
+
import { type PathAlias } from "./path-resolver.js";
|
|
3
|
+
import type { ParseError, Parser } from "./parser.js";
|
|
4
|
+
import type { AstNode } from "./ast/node.js";
|
|
5
|
+
export interface ProjectFile {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly source: string;
|
|
8
|
+
readonly program: AstNode;
|
|
9
|
+
readonly errors: readonly ParseError[];
|
|
10
|
+
readonly module: ModuleInfo;
|
|
11
|
+
}
|
|
12
|
+
export interface Binding {
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ModuleGraph {
|
|
17
|
+
readonly files: ReadonlyMap<string, ProjectFile>;
|
|
18
|
+
/** Every input file (including JSON/tsconfig/package.json), path -> source. */
|
|
19
|
+
readonly allFiles: ReadonlyMap<string, string>;
|
|
20
|
+
readonly root: string;
|
|
21
|
+
file(path: string): ProjectFile | undefined;
|
|
22
|
+
resolve(from: string, specifier: string): string | null;
|
|
23
|
+
resolveBinding(file: string, local: string): Binding | null;
|
|
24
|
+
resolveExport(file: string, name: string): Binding | null;
|
|
25
|
+
readJson(path: string): unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface ModuleGraphInput {
|
|
28
|
+
readonly files: ReadonlyMap<string, string>;
|
|
29
|
+
readonly parser: Parser;
|
|
30
|
+
readonly root?: string;
|
|
31
|
+
readonly aliases?: readonly PathAlias[];
|
|
32
|
+
}
|
|
33
|
+
export declare function createModuleGraph(input: ModuleGraphInput): ModuleGraph;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isSourceFile, readJson as parseJson } from "./json.js";
|
|
2
|
+
import { parseModuleInfo } from "./module-info.js";
|
|
3
|
+
import { createPathResolver, normalizePath } from "./path-resolver.js";
|
|
4
|
+
export function createModuleGraph(input) {
|
|
5
|
+
const root = normalizePath(input.root ?? "");
|
|
6
|
+
const sources = new Map();
|
|
7
|
+
const files = new Map();
|
|
8
|
+
for (const [rawPath, source] of input.files) {
|
|
9
|
+
const path = normalizePath(rawPath);
|
|
10
|
+
sources.set(path, source);
|
|
11
|
+
if (!isSourceFile(path))
|
|
12
|
+
continue;
|
|
13
|
+
const parsed = input.parser.parse(path, source);
|
|
14
|
+
files.set(path, {
|
|
15
|
+
path,
|
|
16
|
+
source,
|
|
17
|
+
program: parsed.program,
|
|
18
|
+
errors: parsed.errors,
|
|
19
|
+
module: parseModuleInfo(path, source, parsed.program),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const resolver = createPathResolver({
|
|
23
|
+
exists: (path) => sources.has(path),
|
|
24
|
+
readJson: (path) => parseJson(sources, path),
|
|
25
|
+
root,
|
|
26
|
+
...(input.aliases === undefined ? {} : { aliases: input.aliases }),
|
|
27
|
+
});
|
|
28
|
+
const resolveExport = (file, name, seen = new Set()) => {
|
|
29
|
+
const key = file + "#" + name;
|
|
30
|
+
if (seen.has(key))
|
|
31
|
+
return null;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
const module = files.get(file);
|
|
34
|
+
if (module === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const direct = module.module.exports.find((entry) => entry.exported === name);
|
|
37
|
+
if (direct !== undefined) {
|
|
38
|
+
if (direct.source !== null) {
|
|
39
|
+
const target = resolver.resolve(file, direct.source);
|
|
40
|
+
if (target === null)
|
|
41
|
+
return null;
|
|
42
|
+
if (direct.kind === "all")
|
|
43
|
+
return resolveExport(target, "*", seen);
|
|
44
|
+
return resolveExport(target, direct.imported ?? name, seen);
|
|
45
|
+
}
|
|
46
|
+
if (direct.local !== null)
|
|
47
|
+
return resolveBinding(file, direct.local, seen);
|
|
48
|
+
return { file, name };
|
|
49
|
+
}
|
|
50
|
+
for (const entry of module.module.exports) {
|
|
51
|
+
if (entry.kind !== "all" || entry.source === null)
|
|
52
|
+
continue;
|
|
53
|
+
const target = resolver.resolve(file, entry.source);
|
|
54
|
+
if (target !== null) {
|
|
55
|
+
const found = resolveExport(target, name, new Set(seen));
|
|
56
|
+
if (found !== null)
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
const resolveBinding = (file, local, seen = new Set()) => {
|
|
63
|
+
const module = files.get(file);
|
|
64
|
+
if (module === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
const imported = module.module.imports.find((entry) => entry.local === local);
|
|
67
|
+
if (imported === undefined)
|
|
68
|
+
return { file, name: local };
|
|
69
|
+
if (imported.kind === "namespace")
|
|
70
|
+
return null;
|
|
71
|
+
const target = resolver.resolve(file, imported.specifier);
|
|
72
|
+
if (target === null)
|
|
73
|
+
return null;
|
|
74
|
+
return resolveExport(target, imported.imported ?? "default", new Set(seen));
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
files,
|
|
78
|
+
allFiles: sources,
|
|
79
|
+
root,
|
|
80
|
+
file: (path) => files.get(normalizePath(path)),
|
|
81
|
+
resolve: resolver.resolve,
|
|
82
|
+
resolveBinding: (file, local) => resolveBinding(normalizePath(file), local),
|
|
83
|
+
resolveExport: (file, name) => resolveExport(normalizePath(file), name),
|
|
84
|
+
readJson: (path) => parseJson(sources, normalizePath(path)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AstNode } from "./ast/node.js";
|
|
2
|
+
import type { SourceRef } from "./types.js";
|
|
3
|
+
export interface ImportBinding {
|
|
4
|
+
readonly specifier: string;
|
|
5
|
+
readonly imported: string | null;
|
|
6
|
+
readonly local: string;
|
|
7
|
+
readonly kind: "named" | "default" | "namespace";
|
|
8
|
+
readonly source: SourceRef;
|
|
9
|
+
}
|
|
10
|
+
export interface ExportBinding {
|
|
11
|
+
readonly exported: string;
|
|
12
|
+
readonly local: string | null;
|
|
13
|
+
readonly source: string | null;
|
|
14
|
+
readonly imported: string | null;
|
|
15
|
+
readonly kind: "named" | "default" | "reexport" | "all";
|
|
16
|
+
readonly ref: SourceRef;
|
|
17
|
+
}
|
|
18
|
+
export interface ModuleInfo {
|
|
19
|
+
readonly imports: readonly ImportBinding[];
|
|
20
|
+
readonly exports: readonly ExportBinding[];
|
|
21
|
+
}
|
|
22
|
+
export declare function declarationNames(declaration: AstNode): string[];
|
|
23
|
+
export declare function parseModuleInfo(file: string, source: string, program: AstNode): ModuleInfo;
|
|
24
|
+
/** The declaration of "export default ...", unwrapped from the export statement. */
|
|
25
|
+
export declare function defaultExportDeclaration(program: AstNode): AstNode | undefined;
|
|
26
|
+
/** The initializer of a top-level const/let/var binding, when declared exactly once. */
|
|
27
|
+
export declare function localInitializer(program: AstNode, name: string): AstNode | undefined;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { identifierName, isAstNode, literalString, nodeArray } from "./ast/node.js";
|
|
2
|
+
function refOf(file, source, node) {
|
|
3
|
+
const offset = typeof node.start === "number" ? node.start : 0;
|
|
4
|
+
let line = 1;
|
|
5
|
+
for (let index = 0; index < offset && index < source.length; index += 1) {
|
|
6
|
+
if (source.charCodeAt(index) === 10)
|
|
7
|
+
line += 1;
|
|
8
|
+
}
|
|
9
|
+
return { file, line };
|
|
10
|
+
}
|
|
11
|
+
function importBindings(file, source, program) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const statement of nodeArray(program, "body")) {
|
|
14
|
+
if (statement.type !== "ImportDeclaration")
|
|
15
|
+
continue;
|
|
16
|
+
const specifier = literalString(statement.source);
|
|
17
|
+
if (specifier === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
const ref = refOf(file, source, statement);
|
|
20
|
+
for (const item of nodeArray(statement, "specifiers")) {
|
|
21
|
+
if (item.type === "ImportDefaultSpecifier") {
|
|
22
|
+
const local = identifierName(item.local);
|
|
23
|
+
if (local !== undefined) {
|
|
24
|
+
out.push({ specifier, imported: "default", local, kind: "default", source: ref });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (item.type === "ImportNamespaceSpecifier") {
|
|
28
|
+
const local = identifierName(item.local);
|
|
29
|
+
if (local !== undefined) {
|
|
30
|
+
out.push({ specifier, imported: null, local, kind: "namespace", source: ref });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (item.type === "ImportSpecifier") {
|
|
34
|
+
const local = identifierName(item.local);
|
|
35
|
+
const imported = identifierName(item.imported) ?? literalString(item.imported);
|
|
36
|
+
if (local !== undefined && imported !== undefined) {
|
|
37
|
+
out.push({ specifier, imported, local, kind: "named", source: ref });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export function declarationNames(declaration) {
|
|
45
|
+
if (declaration.type === "VariableDeclaration") {
|
|
46
|
+
return nodeArray(declaration, "declarations")
|
|
47
|
+
.map((item) => identifierName(item.id))
|
|
48
|
+
.filter((name) => name !== undefined);
|
|
49
|
+
}
|
|
50
|
+
if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
|
|
51
|
+
const name = identifierName(declaration.id);
|
|
52
|
+
return name === undefined ? [] : [name];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
function exportBindings(file, source, program) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const statement of nodeArray(program, "body")) {
|
|
59
|
+
const ref = refOf(file, source, statement);
|
|
60
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
61
|
+
const declaration = isAstNode(statement.declaration) ? statement.declaration : undefined;
|
|
62
|
+
const local = declaration === undefined ? null : (identifierName(declaration) ?? null);
|
|
63
|
+
out.push({ exported: "default", local, source: null, imported: null, kind: "default", ref });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
67
|
+
const specifier = literalString(statement.source);
|
|
68
|
+
if (specifier !== undefined) {
|
|
69
|
+
out.push({
|
|
70
|
+
exported: "*",
|
|
71
|
+
local: null,
|
|
72
|
+
source: specifier,
|
|
73
|
+
imported: null,
|
|
74
|
+
kind: "all",
|
|
75
|
+
ref,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (statement.type !== "ExportNamedDeclaration")
|
|
81
|
+
continue;
|
|
82
|
+
const declaration = isAstNode(statement.declaration) ? statement.declaration : undefined;
|
|
83
|
+
if (declaration !== undefined) {
|
|
84
|
+
for (const name of declarationNames(declaration)) {
|
|
85
|
+
out.push({ exported: name, local: name, source: null, imported: null, kind: "named", ref });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const from = literalString(statement.source);
|
|
89
|
+
for (const item of nodeArray(statement, "specifiers")) {
|
|
90
|
+
if (item.type !== "ExportSpecifier")
|
|
91
|
+
continue;
|
|
92
|
+
const exported = identifierName(item.exported) ?? literalString(item.exported);
|
|
93
|
+
const local = identifierName(item.local) ?? literalString(item.local);
|
|
94
|
+
if (exported === undefined || local === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
out.push({
|
|
97
|
+
exported,
|
|
98
|
+
local: from === undefined ? local : null,
|
|
99
|
+
source: from ?? null,
|
|
100
|
+
imported: from === undefined ? null : local,
|
|
101
|
+
kind: from === undefined ? "named" : "reexport",
|
|
102
|
+
ref,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
export function parseModuleInfo(file, source, program) {
|
|
109
|
+
return {
|
|
110
|
+
imports: importBindings(file, source, program),
|
|
111
|
+
exports: exportBindings(file, source, program),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The declaration of "export default ...", unwrapped from the export statement. */
|
|
115
|
+
export function defaultExportDeclaration(program) {
|
|
116
|
+
for (const statement of nodeArray(program, "body")) {
|
|
117
|
+
if (statement.type !== "ExportDefaultDeclaration")
|
|
118
|
+
continue;
|
|
119
|
+
return isAstNode(statement.declaration) ? statement.declaration : undefined;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/** The initializer of a top-level const/let/var binding, when declared exactly once. */
|
|
124
|
+
export function localInitializer(program, name) {
|
|
125
|
+
let found;
|
|
126
|
+
for (const statement of nodeArray(program, "body")) {
|
|
127
|
+
const declaration = statement.type === "VariableDeclaration"
|
|
128
|
+
? statement
|
|
129
|
+
: statement.type === "ExportNamedDeclaration" && isAstNode(statement.declaration)
|
|
130
|
+
? statement.declaration
|
|
131
|
+
: undefined;
|
|
132
|
+
if (declaration === undefined || declaration.type !== "VariableDeclaration")
|
|
133
|
+
continue;
|
|
134
|
+
for (const item of nodeArray(declaration, "declarations")) {
|
|
135
|
+
if (identifierName(item.id) !== name)
|
|
136
|
+
continue;
|
|
137
|
+
if (found !== undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
found = isAstNode(item.init) ? item.init : undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return found;
|
|
143
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AnalyzeInput } from "./analyze.js";
|
|
2
|
+
import type { PathAlias } from "./path-resolver.js";
|
|
3
|
+
import { type LoadProjectOptions } from "./fs.js";
|
|
4
|
+
import type { Parser } from "./parser.js";
|
|
5
|
+
import type { Analysis } from "./types.js";
|
|
6
|
+
/** Node/CI parser adapter. The browser uses a wasm adapter over the same contract. */
|
|
7
|
+
export declare function createOxcParser(): Parser;
|
|
8
|
+
export interface AnalyzeDirectoryOptions {
|
|
9
|
+
readonly aliases?: readonly PathAlias[];
|
|
10
|
+
readonly transport?: AnalyzeInput["transport"];
|
|
11
|
+
readonly load?: LoadProjectOptions;
|
|
12
|
+
}
|
|
13
|
+
export { loadProjectFiles } from "./fs.js";
|
|
14
|
+
export type { LoadProjectOptions } from "./fs.js";
|
|
15
|
+
/** Convenience for Node/CI: load a directory and analyze it. The browser passes its own map. */
|
|
16
|
+
export declare function analyzeDirectory(root: string, options?: AnalyzeDirectoryOptions): Analysis;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseSync } from "oxc-parser";
|
|
2
|
+
import { analyzeProject } from "./analyze.js";
|
|
3
|
+
import { asNode } from "./ast/node.js";
|
|
4
|
+
import { loadProjectFiles } from "./fs.js";
|
|
5
|
+
import { lineOf } from "./source.js";
|
|
6
|
+
/** Node/CI parser adapter. The browser uses a wasm adapter over the same contract. */
|
|
7
|
+
export function createOxcParser() {
|
|
8
|
+
return {
|
|
9
|
+
parse(filename, source) {
|
|
10
|
+
const result = parseSync(filename, source, {
|
|
11
|
+
astType: "ts",
|
|
12
|
+
sourceType: "module",
|
|
13
|
+
preserveParens: true,
|
|
14
|
+
});
|
|
15
|
+
const program = asNode(result.program);
|
|
16
|
+
if (program === undefined)
|
|
17
|
+
throw new Error("oxc-parser returned no program");
|
|
18
|
+
const errors = result.errors.map((error) => {
|
|
19
|
+
const label = error.labels[0];
|
|
20
|
+
const start = label === undefined ? null : label.start;
|
|
21
|
+
return {
|
|
22
|
+
message: error.message,
|
|
23
|
+
start,
|
|
24
|
+
end: label === undefined ? null : label.end,
|
|
25
|
+
source: { file: filename, line: start === null ? 1 : lineOf(source, start) },
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
return { program, errors };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export { loadProjectFiles } from "./fs.js";
|
|
33
|
+
/** Convenience for Node/CI: load a directory and analyze it. The browser passes its own map. */
|
|
34
|
+
export function analyzeDirectory(root, options = {}) {
|
|
35
|
+
return analyzeProject({
|
|
36
|
+
files: loadProjectFiles(root, options.load),
|
|
37
|
+
parser: createOxcParser(),
|
|
38
|
+
...(options.aliases === undefined ? {} : { aliases: options.aliases }),
|
|
39
|
+
...(options.transport === undefined ? {} : { transport: options.transport }),
|
|
40
|
+
});
|
|
41
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AstNode } from "./ast/node.js";
|
|
2
|
+
import type { SourceRef } from "./types.js";
|
|
3
|
+
export interface ParseError {
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly start: number | null;
|
|
6
|
+
readonly end: number | null;
|
|
7
|
+
readonly source: SourceRef;
|
|
8
|
+
}
|
|
9
|
+
export interface ParsedModule {
|
|
10
|
+
readonly program: AstNode;
|
|
11
|
+
readonly errors: readonly ParseError[];
|
|
12
|
+
}
|
|
13
|
+
/** Injected parser contract: the analyzer never imports a parser package itself. */
|
|
14
|
+
export interface Parser {
|
|
15
|
+
parse(filename: string, source: string): ParsedModule;
|
|
16
|
+
}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client consumers -> PageFacts and page→route edges (§3.4 of the AST DSL contract).
|
|
3
|
+
*
|
|
4
|
+
* Exactly one transport is sanctioned (`api()` from `src/lib/api.ts` by default): the
|
|
5
|
+
* transport export is resolved through imports (aliases and namespace imports included) and
|
|
6
|
+
* every other network call in `src/**` is a `client-transport` error. The transport module
|
|
7
|
+
* itself is the only place allowed to call `fetch`, so it is not scanned as a consumer.
|
|
8
|
+
*/
|
|
9
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
10
|
+
import type { Diagnostic, PageFact, RouteFact } from "../types.js";
|
|
11
|
+
export interface TransportSpec {
|
|
12
|
+
/** Project-relative module, e.g. `src/lib/api.ts`. */
|
|
13
|
+
readonly module: string;
|
|
14
|
+
/** Exported function that performs API calls, e.g. `api`. */
|
|
15
|
+
readonly exportName: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ClientResult {
|
|
18
|
+
readonly pages: readonly PageFact[];
|
|
19
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
20
|
+
}
|
|
21
|
+
/** Map client files to the routes they call. */
|
|
22
|
+
export declare function buildPageFacts(graph: ModuleGraph, routes: readonly RouteFact[], transport: TransportSpec): ClientResult;
|