@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.
- 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 +158 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -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.d.ts +22 -0
- package/dist/passes/jsx.js +89 -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-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +159 -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 +130 -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 +190 -0
- package/src/index.ts +24 -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.ts +92 -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-preview.ts +268 -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 +161 -0
- package/src/types-jobs.ts +40 -0
- package/src/types-relations.ts +64 -0
- package/src/types-workflows.ts +42 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,101 @@
|
|
|
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 { callArguments, identifierName, unwrapExpression } from "../ast/node.js";
|
|
14
|
+
import { createDslResolver, isViewFile, nodeRef, schemaDiagnostics, topLevelDeclarators, } from "./schema-dsl.js";
|
|
15
|
+
import { readStaticObject } from "./static-read.js";
|
|
16
|
+
function viewUnknown(reason, source) {
|
|
17
|
+
return { code: "view-static", reason, source };
|
|
18
|
+
}
|
|
19
|
+
function stringArray(value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return null;
|
|
22
|
+
return value.every((entry) => typeof entry === "string") ? [...value] : null;
|
|
23
|
+
}
|
|
24
|
+
/** The `defineView(...)` call an initializer holds, when it is the @facilis.dev/db DSL. */
|
|
25
|
+
function viewCall(init, resolver) {
|
|
26
|
+
const expression = unwrapExpression(init);
|
|
27
|
+
if (expression?.type !== "CallExpression")
|
|
28
|
+
return null;
|
|
29
|
+
const symbol = resolver(expression.callee);
|
|
30
|
+
return symbol?.pkg === "facilis-db" && symbol.name === "defineView" ? expression : null;
|
|
31
|
+
}
|
|
32
|
+
function buildViewDraft(graph, file, declarator, call) {
|
|
33
|
+
const source = nodeRef(file, call);
|
|
34
|
+
const binding = identifierName(declarator.id) ?? null;
|
|
35
|
+
const unknowns = [];
|
|
36
|
+
const options = readStaticObject(callArguments(call)[0], { graph, file: file.path }, { code: "view-static", report: false });
|
|
37
|
+
unknowns.push(...options.unknown);
|
|
38
|
+
const nameField = options.fields.get("name");
|
|
39
|
+
let name = binding ?? "unknown";
|
|
40
|
+
if (nameField !== undefined && nameField.ok && typeof nameField.value === "string") {
|
|
41
|
+
if (nameField.value.length > 0)
|
|
42
|
+
name = nameField.value;
|
|
43
|
+
else
|
|
44
|
+
unknowns.push(viewUnknown("view name is not a static string", source));
|
|
45
|
+
}
|
|
46
|
+
else if (nameField !== undefined && !nameField.ok) {
|
|
47
|
+
unknowns.push(viewUnknown("view name is missing or not statically readable", source));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
unknowns.push(viewUnknown("view name is missing or not a static string", source));
|
|
51
|
+
}
|
|
52
|
+
let tables = [];
|
|
53
|
+
const tablesField = options.fields.get("tables");
|
|
54
|
+
if (tablesField !== undefined) {
|
|
55
|
+
const read = tablesField.ok ? stringArray(tablesField.value) : null;
|
|
56
|
+
if (read === null) {
|
|
57
|
+
unknowns.push(viewUnknown("view tables are not a static string[]", source));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
tables = read;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const permissionField = options.fields.get("permission");
|
|
64
|
+
let permission = null;
|
|
65
|
+
let access = "invoker";
|
|
66
|
+
if (permissionField !== undefined) {
|
|
67
|
+
if (permissionField.ok && typeof permissionField.value === "string") {
|
|
68
|
+
permission = permissionField.value;
|
|
69
|
+
access = "definer";
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// We cannot tell an invoker declaration from a definer one: keep the mode unknown.
|
|
73
|
+
access = null;
|
|
74
|
+
unknowns.push(viewUnknown("view permission is not a static string", source));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
view: { name, binding, tables, permission, access, source, unknown: unknowns },
|
|
79
|
+
diagnostics: schemaDiagnostics(unknowns),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Read `db/views/**` into view facts. */
|
|
83
|
+
export function buildViewFacts(graph) {
|
|
84
|
+
const files = [...graph.files.values()]
|
|
85
|
+
.filter((file) => isViewFile(graph, file.path))
|
|
86
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
87
|
+
const views = [];
|
|
88
|
+
const diagnostics = [];
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const resolver = createDslResolver(graph, file);
|
|
91
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
92
|
+
const call = viewCall(declarator.init, resolver);
|
|
93
|
+
if (call === null)
|
|
94
|
+
continue;
|
|
95
|
+
const draft = buildViewDraft(graph, file, declarator, call);
|
|
96
|
+
views.push(draft.view);
|
|
97
|
+
diagnostics.push(...draft.diagnostics);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { views, diagnostics };
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow table surface, derived from the jobs its steps reference (user decision: derived,
|
|
3
|
+
* never declared). A workflow declares no tables of its own: the union of the referenced jobs'
|
|
4
|
+
* `dbTables` is what the workflow can reach, and the runtime inherits each job's guard.
|
|
5
|
+
*
|
|
6
|
+
* Approval steps contribute nothing. A referenced job with an unreadable `dbTables` makes the
|
|
7
|
+
* union explicitly unknown; a referenced job with no definition keeps its name visible in
|
|
8
|
+
* `unknownJobs` and is a warning, never a silent hole.
|
|
9
|
+
*/
|
|
10
|
+
import type { Diagnostic, WorkflowFact } from "../types.js";
|
|
11
|
+
import type { JobFact } from "../types-jobs.js";
|
|
12
|
+
import type { WorkflowTablesFact } from "../types-workflows.js";
|
|
13
|
+
export interface WorkflowTablesResult {
|
|
14
|
+
readonly facts: readonly WorkflowTablesFact[];
|
|
15
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
/** Derive every workflow's table surface from its job steps. */
|
|
18
|
+
export declare function buildWorkflowTableFacts(workflows: readonly WorkflowFact[], jobs: readonly JobFact[]): WorkflowTablesResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow table surface, derived from the jobs its steps reference (user decision: derived,
|
|
3
|
+
* never declared). A workflow declares no tables of its own: the union of the referenced jobs'
|
|
4
|
+
* `dbTables` is what the workflow can reach, and the runtime inherits each job's guard.
|
|
5
|
+
*
|
|
6
|
+
* Approval steps contribute nothing. A referenced job with an unreadable `dbTables` makes the
|
|
7
|
+
* union explicitly unknown; a referenced job with no definition keeps its name visible in
|
|
8
|
+
* `unknownJobs` and is a warning, never a silent hole.
|
|
9
|
+
*/
|
|
10
|
+
import { warning } from "../diagnostics.js";
|
|
11
|
+
import { sortDiagnostics } from "./static-read.js";
|
|
12
|
+
/** One job definition per name; the manifest enforces uniqueness, first wins here. */
|
|
13
|
+
function jobRegistry(jobs) {
|
|
14
|
+
const byName = new Map();
|
|
15
|
+
for (const job of jobs) {
|
|
16
|
+
if (!byName.has(job.name))
|
|
17
|
+
byName.set(job.name, job);
|
|
18
|
+
}
|
|
19
|
+
return byName;
|
|
20
|
+
}
|
|
21
|
+
/** Derive every workflow's table surface from its job steps. */
|
|
22
|
+
export function buildWorkflowTableFacts(workflows, jobs) {
|
|
23
|
+
const registry = jobRegistry(jobs);
|
|
24
|
+
const facts = [];
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
for (const workflow of workflows) {
|
|
27
|
+
const tables = new Set();
|
|
28
|
+
const unknownJobs = new Set();
|
|
29
|
+
const missingAt = new Map();
|
|
30
|
+
let unknown = false;
|
|
31
|
+
for (const step of workflow.steps) {
|
|
32
|
+
if (step.kind !== "job")
|
|
33
|
+
continue;
|
|
34
|
+
// A job step whose name is unreadable is already a workflow-definition unknown, but the
|
|
35
|
+
// derived surface must not pretend it is complete.
|
|
36
|
+
if (step.name === null) {
|
|
37
|
+
unknown = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const job = registry.get(step.name);
|
|
41
|
+
if (job === undefined) {
|
|
42
|
+
unknownJobs.add(step.name);
|
|
43
|
+
if (!missingAt.has(step.name))
|
|
44
|
+
missingAt.set(step.name, step.source);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (job.dbTables === null) {
|
|
48
|
+
unknown = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const table of job.dbTables)
|
|
52
|
+
tables.add(table);
|
|
53
|
+
}
|
|
54
|
+
for (const name of [...unknownJobs].toSorted()) {
|
|
55
|
+
diagnostics.push(warning("workflow-job-unknown", 'workflow "' +
|
|
56
|
+
workflow.name +
|
|
57
|
+
'" references job "' +
|
|
58
|
+
name +
|
|
59
|
+
'" but no definition exists in server/jobs', missingAt.get(name) ?? workflow.source));
|
|
60
|
+
}
|
|
61
|
+
facts.push({
|
|
62
|
+
name: workflow.name,
|
|
63
|
+
sourceFile: workflow.sourceFile,
|
|
64
|
+
source: workflow.source,
|
|
65
|
+
tables: [...tables].toSorted(),
|
|
66
|
+
unknownJobs: [...unknownJobs].toSorted(),
|
|
67
|
+
unknown,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { facts, diagnostics: sortDiagnostics(diagnostics) };
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
2
|
+
import type { Diagnostic, WorkflowFact } from "../types.js";
|
|
3
|
+
export interface WorkflowResult {
|
|
4
|
+
readonly workflows: readonly WorkflowFact[];
|
|
5
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
6
|
+
}
|
|
7
|
+
/** Read every workflow definition the project declares; invalid files become warnings. */
|
|
8
|
+
export declare function buildWorkflowFacts(graph: ModuleGraph): WorkflowResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// `server/workflows/**` definitions -> WorkflowFacts (docs/WORKFLOWS.md §6, D-135).
|
|
2
|
+
//
|
|
3
|
+
// The default export is the only source: a `defineWorkflow()` call from `@facilis.dev/core`
|
|
4
|
+
// whose argument is an object literal. Resolvers (`input`, `params`) are functions, so the
|
|
5
|
+
// reader takes the static fields one by one instead of evaluating the whole definition.
|
|
6
|
+
import { callArguments, identifierName, nodeArray, objectEntries, propertyName, propertyValue, unwrapExpression, } from "../ast/node.js";
|
|
7
|
+
import { warning } from "../diagnostics.js";
|
|
8
|
+
import { sourceRef } from "../source.js";
|
|
9
|
+
import { evaluateStatic } from "../static-eval.js";
|
|
10
|
+
import { defaultExportExpression, isLocallyDeclared, isRecord, relativePath, sortDiagnostics, } from "./static-read.js";
|
|
11
|
+
const CORE_SPECIFIER = "@facilis.dev/core";
|
|
12
|
+
const WORKFLOW_DIR = "server/workflows/";
|
|
13
|
+
const STATIC_CODE = "workflow-definition-static";
|
|
14
|
+
/** Read every workflow definition the project declares; invalid files become warnings. */
|
|
15
|
+
export function buildWorkflowFacts(graph) {
|
|
16
|
+
const workflows = [];
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const file of graph.files.values()) {
|
|
19
|
+
const relative = relativePath(graph.root, file.path);
|
|
20
|
+
if (!relative.startsWith(WORKFLOW_DIR) || relative.endsWith(".test.ts"))
|
|
21
|
+
continue;
|
|
22
|
+
scanFile(graph, file, workflows, diagnostics);
|
|
23
|
+
}
|
|
24
|
+
return { workflows, diagnostics: sortDiagnostics(diagnostics) };
|
|
25
|
+
}
|
|
26
|
+
function scanFile(graph, file, workflows, diagnostics) {
|
|
27
|
+
const context = { graph, file: file.path };
|
|
28
|
+
const expression = defaultExportExpression(file);
|
|
29
|
+
const ref = sourceRef(file.path, file.source, expression?.start ?? 0);
|
|
30
|
+
const problem = definitionProblem(file, expression);
|
|
31
|
+
if (problem !== null || expression === undefined || expression.type !== "CallExpression") {
|
|
32
|
+
diagnostics.push(warning("workflow-definition", problem ?? "the default export is not a defineWorkflow() call", ref));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const literal = objectLiteral(callArguments(expression)[0]);
|
|
36
|
+
if (literal === undefined) {
|
|
37
|
+
diagnostics.push(warning("workflow-definition", "the definition argument is not an object literal", ref));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const fields = fieldMap(literal);
|
|
41
|
+
const name = staticString(fields.get("name"), context);
|
|
42
|
+
if (name === null) {
|
|
43
|
+
diagnostics.push(warning("workflow-definition", "the workflow name cannot be read statically", ref));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const unknown = [];
|
|
47
|
+
const steps = readSteps(fields.get("steps"), context, file.path, file.source, unknown);
|
|
48
|
+
workflows.push({ name, sourceFile: file.path, source: ref, steps, unknown });
|
|
49
|
+
}
|
|
50
|
+
/** The default export must be the `defineWorkflow` import from `@facilis.dev/core`. */
|
|
51
|
+
function definitionProblem(file, expression) {
|
|
52
|
+
if (expression === undefined)
|
|
53
|
+
return "the workflow file has no default export";
|
|
54
|
+
if (expression.type !== "CallExpression") {
|
|
55
|
+
return "the default export is not a defineWorkflow() call";
|
|
56
|
+
}
|
|
57
|
+
const callee = unwrapExpression(expression.callee);
|
|
58
|
+
const name = callee?.type === "Identifier" ? identifierName(callee) : undefined;
|
|
59
|
+
if (name === undefined)
|
|
60
|
+
return "the defineWorkflow callee cannot be resolved";
|
|
61
|
+
if (isLocallyDeclared(file, name))
|
|
62
|
+
return '"' + name + '" is shadowed by a local declaration';
|
|
63
|
+
const imported = file.module.imports.find((entry) => entry.local === name);
|
|
64
|
+
if (imported === undefined ||
|
|
65
|
+
imported.specifier !== CORE_SPECIFIER ||
|
|
66
|
+
imported.imported !== "defineWorkflow") {
|
|
67
|
+
return "the default export is not the defineWorkflow import from @facilis.dev/core";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function readSteps(node, context, file, source, unknown) {
|
|
72
|
+
const array = node === undefined ? undefined : unwrapExpression(node);
|
|
73
|
+
if (array === undefined || array.type !== "ArrayExpression") {
|
|
74
|
+
unknown.push({
|
|
75
|
+
code: STATIC_CODE,
|
|
76
|
+
reason: "steps must be an array literal",
|
|
77
|
+
source: sourceRef(file, source, array?.start ?? 0),
|
|
78
|
+
});
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const steps = [];
|
|
82
|
+
nodeArray(array, "elements").forEach((element, index) => {
|
|
83
|
+
const ref = sourceRef(file, source, element.start ?? 0);
|
|
84
|
+
const literal = objectLiteral(element);
|
|
85
|
+
const fields = literal === undefined ? new Map() : fieldMap(literal);
|
|
86
|
+
const job = fields.get("job");
|
|
87
|
+
const approval = fields.get("approval");
|
|
88
|
+
if (job !== undefined) {
|
|
89
|
+
const name = staticString(job, context);
|
|
90
|
+
unknownIfNull(unknown, ref, index, "job name", name);
|
|
91
|
+
steps.push({ index, kind: "job", name, permission: null, onRejectStep: null, source: ref });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (approval !== undefined) {
|
|
95
|
+
const approvalLiteral = objectLiteral(approval);
|
|
96
|
+
const approvalFields = approvalLiteral === undefined ? new Map() : fieldMap(approvalLiteral);
|
|
97
|
+
const titleKey = staticString(approvalFields.get("titleKey"), context);
|
|
98
|
+
const permission = staticString(approvalFields.get("permission"), context);
|
|
99
|
+
const onRejectStep = rejectStepOf(approvalFields.get("onReject"), context);
|
|
100
|
+
unknownIfNull(unknown, ref, index, "approval title key", titleKey);
|
|
101
|
+
steps.push({
|
|
102
|
+
index,
|
|
103
|
+
kind: "approval",
|
|
104
|
+
name: titleKey,
|
|
105
|
+
permission,
|
|
106
|
+
onRejectStep,
|
|
107
|
+
source: ref,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
unknown.push({
|
|
112
|
+
code: STATIC_CODE,
|
|
113
|
+
reason: "step " + index + " is neither a job nor an approval",
|
|
114
|
+
source: ref,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return steps;
|
|
118
|
+
}
|
|
119
|
+
function rejectStepOf(node, context) {
|
|
120
|
+
if (node === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const evaluated = evaluateStatic(node, context);
|
|
123
|
+
if (!evaluated.ok || !isRecord(evaluated.value))
|
|
124
|
+
return null;
|
|
125
|
+
const step = evaluated.value["step"];
|
|
126
|
+
return typeof step === "number" && Number.isInteger(step) && step >= 0 ? step : null;
|
|
127
|
+
}
|
|
128
|
+
function unknownIfNull(unknown, ref, index, label, value) {
|
|
129
|
+
if (value !== null)
|
|
130
|
+
return;
|
|
131
|
+
unknown.push({
|
|
132
|
+
code: STATIC_CODE,
|
|
133
|
+
reason: "step " + index + ": " + label + " cannot be read statically",
|
|
134
|
+
source: ref,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function staticString(node, context) {
|
|
138
|
+
if (node === undefined)
|
|
139
|
+
return null;
|
|
140
|
+
const evaluated = evaluateStatic(node, context);
|
|
141
|
+
return evaluated.ok && typeof evaluated.value === "string" && evaluated.value.length > 0
|
|
142
|
+
? evaluated.value
|
|
143
|
+
: null;
|
|
144
|
+
}
|
|
145
|
+
function objectLiteral(node) {
|
|
146
|
+
const value = node === undefined ? undefined : unwrapExpression(node);
|
|
147
|
+
return value?.type === "ObjectExpression" ? value : undefined;
|
|
148
|
+
}
|
|
149
|
+
function fieldMap(literal) {
|
|
150
|
+
const fields = new Map();
|
|
151
|
+
for (const entry of objectEntries(literal)) {
|
|
152
|
+
if (entry.type === "SpreadElement")
|
|
153
|
+
continue;
|
|
154
|
+
const key = propertyName(entry);
|
|
155
|
+
const value = propertyValue(entry);
|
|
156
|
+
if (key !== undefined && value !== undefined)
|
|
157
|
+
fields.set(key, value);
|
|
158
|
+
}
|
|
159
|
+
return fields;
|
|
160
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Project-relative path handling and module specifier resolution (no Node APIs). */
|
|
2
|
+
export interface PathAlias {
|
|
3
|
+
readonly prefix: string;
|
|
4
|
+
readonly target: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PathResolverInput {
|
|
7
|
+
readonly exists: (path: string) => boolean;
|
|
8
|
+
readonly readJson: (path: string) => unknown;
|
|
9
|
+
readonly root: string;
|
|
10
|
+
readonly aliases?: readonly PathAlias[];
|
|
11
|
+
}
|
|
12
|
+
export interface PathResolver {
|
|
13
|
+
resolve(from: string, specifier: string): string | null;
|
|
14
|
+
tryFile(path: string): string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare const SOURCE_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
17
|
+
export declare function normalizePath(path: string): string;
|
|
18
|
+
export declare function dirname(path: string): string;
|
|
19
|
+
export declare function joinPath(base: string, relative: string): string;
|
|
20
|
+
export declare function createPathResolver(input: PathResolverInput): PathResolver;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** Project-relative path handling and module specifier resolution (no Node APIs). */
|
|
2
|
+
export const SOURCE_EXTENSIONS = [
|
|
3
|
+
".ts",
|
|
4
|
+
".tsx",
|
|
5
|
+
".js",
|
|
6
|
+
".jsx",
|
|
7
|
+
".mjs",
|
|
8
|
+
".cjs",
|
|
9
|
+
".mts",
|
|
10
|
+
".cts",
|
|
11
|
+
];
|
|
12
|
+
export function normalizePath(path) {
|
|
13
|
+
const parts = path.replace(/\\/g, "/").split("/");
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (part === "" || part === ".")
|
|
17
|
+
continue;
|
|
18
|
+
if (part === "..") {
|
|
19
|
+
out.pop();
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
out.push(part);
|
|
23
|
+
}
|
|
24
|
+
return out.join("/");
|
|
25
|
+
}
|
|
26
|
+
export function dirname(path) {
|
|
27
|
+
const index = path.lastIndexOf("/");
|
|
28
|
+
return index === -1 ? "" : path.slice(0, index);
|
|
29
|
+
}
|
|
30
|
+
export function joinPath(base, relative) {
|
|
31
|
+
return normalizePath(base.length === 0 ? relative : base + "/" + relative);
|
|
32
|
+
}
|
|
33
|
+
function packageSpecifier(specifier) {
|
|
34
|
+
const parts = specifier.split("/");
|
|
35
|
+
if (specifier.startsWith("@")) {
|
|
36
|
+
return { pkg: parts.slice(0, 2).join("/"), subpath: parts.slice(2).join("/") };
|
|
37
|
+
}
|
|
38
|
+
return { pkg: parts[0] ?? specifier, subpath: parts.slice(1).join("/") };
|
|
39
|
+
}
|
|
40
|
+
function exportTargets(exports) {
|
|
41
|
+
if (typeof exports === "string")
|
|
42
|
+
return [exports];
|
|
43
|
+
if (Array.isArray(exports))
|
|
44
|
+
return exports.flatMap(exportTargets);
|
|
45
|
+
if (typeof exports !== "object" || exports === null)
|
|
46
|
+
return [];
|
|
47
|
+
const record = exports;
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const key of ["import", "default", "browser", "node"]) {
|
|
50
|
+
if (key in record)
|
|
51
|
+
out.push(...exportTargets(record[key]));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export function createPathResolver(input) {
|
|
56
|
+
const root = normalizePath(input.root);
|
|
57
|
+
const aliases = [
|
|
58
|
+
{ prefix: "~/", target: root },
|
|
59
|
+
{ prefix: "~", target: root },
|
|
60
|
+
...(input.aliases ?? []),
|
|
61
|
+
];
|
|
62
|
+
const tryFile = (path) => {
|
|
63
|
+
if (input.exists(path))
|
|
64
|
+
return path;
|
|
65
|
+
for (const extension of SOURCE_EXTENSIONS) {
|
|
66
|
+
if (input.exists(path + extension))
|
|
67
|
+
return path + extension;
|
|
68
|
+
}
|
|
69
|
+
for (const extension of SOURCE_EXTENSIONS) {
|
|
70
|
+
const index = joinPath(path, "index" + extension);
|
|
71
|
+
if (input.exists(index))
|
|
72
|
+
return index;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
const resolvePackage = (specifier) => {
|
|
77
|
+
const { pkg, subpath } = packageSpecifier(specifier);
|
|
78
|
+
const prefix = "node_modules/" + pkg;
|
|
79
|
+
const packageJson = input.readJson(prefix + "/package.json");
|
|
80
|
+
if (typeof packageJson !== "object" || packageJson === null)
|
|
81
|
+
return null;
|
|
82
|
+
const record = packageJson;
|
|
83
|
+
if (subpath.length > 0) {
|
|
84
|
+
const exports = record["exports"];
|
|
85
|
+
if (typeof exports === "object" && exports !== null) {
|
|
86
|
+
const entry = exports["./" + subpath];
|
|
87
|
+
for (const target of exportTargets(entry)) {
|
|
88
|
+
const resolved = tryFile(joinPath(prefix, target));
|
|
89
|
+
if (resolved !== null)
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return tryFile(joinPath(prefix, subpath));
|
|
94
|
+
}
|
|
95
|
+
const exports = record["exports"];
|
|
96
|
+
if (typeof exports === "object" && exports !== null) {
|
|
97
|
+
const entry = exports["."];
|
|
98
|
+
for (const target of exportTargets(entry)) {
|
|
99
|
+
const resolved = tryFile(joinPath(prefix, target));
|
|
100
|
+
if (resolved !== null)
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const target of ["main", "module"]) {
|
|
105
|
+
const value = record[target];
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
const resolved = tryFile(joinPath(prefix, value));
|
|
108
|
+
if (resolved !== null)
|
|
109
|
+
return resolved;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return tryFile(joinPath(prefix, "src/index.ts")) ?? tryFile(joinPath(prefix, "index.ts"));
|
|
113
|
+
};
|
|
114
|
+
const resolve = (from, rawSpecifier) => {
|
|
115
|
+
const specifier = rawSpecifier.trim();
|
|
116
|
+
if (specifier.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
for (const alias of aliases) {
|
|
119
|
+
if (specifier === alias.prefix || specifier.startsWith(alias.prefix)) {
|
|
120
|
+
const rest = specifier.slice(alias.prefix.length);
|
|
121
|
+
const target = alias.target.length === 0 ? rest : joinPath(alias.target, rest);
|
|
122
|
+
const resolved = tryFile(target);
|
|
123
|
+
if (resolved !== null)
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (specifier.startsWith("."))
|
|
128
|
+
return tryFile(joinPath(dirname(from), specifier));
|
|
129
|
+
if (specifier.startsWith("/"))
|
|
130
|
+
return tryFile(normalizePath(specifier));
|
|
131
|
+
return resolvePackage(specifier);
|
|
132
|
+
};
|
|
133
|
+
return { resolve, tryFile };
|
|
134
|
+
}
|
package/dist/revision.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizePath } from "./path-resolver.js";
|
|
2
|
+
/** Deterministic, browser-safe revision of the analyzed file set. */
|
|
3
|
+
export function computeRevision(files) {
|
|
4
|
+
const entries = [...files]
|
|
5
|
+
.map(([path, source]) => [normalizePath(path), source])
|
|
6
|
+
.toSorted(([left], [right]) => left.localeCompare(right));
|
|
7
|
+
let hash = 0x811c9dc5;
|
|
8
|
+
for (const [path, source] of entries) {
|
|
9
|
+
hash = mix(hash, path);
|
|
10
|
+
hash = mix(hash, "\u0000");
|
|
11
|
+
hash = mix(hash, source);
|
|
12
|
+
hash = mix(hash, "\u0001");
|
|
13
|
+
}
|
|
14
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
15
|
+
}
|
|
16
|
+
function mix(hash, value) {
|
|
17
|
+
let next = hash;
|
|
18
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
19
|
+
next ^= value.charCodeAt(index);
|
|
20
|
+
next = Math.imul(next, 0x01000193) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return next;
|
|
23
|
+
}
|
package/dist/source.d.ts
ADDED
package/dist/source.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 1-based line number of a source offset. */
|
|
2
|
+
export function lineOf(source, offset) {
|
|
3
|
+
let line = 1;
|
|
4
|
+
const end = Math.min(Math.max(offset, 0), source.length);
|
|
5
|
+
for (let index = 0; index < end; index += 1) {
|
|
6
|
+
if (source.charCodeAt(index) === 10)
|
|
7
|
+
line += 1;
|
|
8
|
+
}
|
|
9
|
+
return line;
|
|
10
|
+
}
|
|
11
|
+
export function sourceRef(file, source, offset) {
|
|
12
|
+
return { file, line: lineOf(source, offset) };
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { literalString, type AstNode } from "./ast/node.js";
|
|
2
|
+
import type { ModuleGraph } from "./module-graph.js";
|
|
3
|
+
export type StaticEvaluation = {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
readonly ok: false;
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
};
|
|
10
|
+
export interface StaticContext {
|
|
11
|
+
readonly graph: ModuleGraph;
|
|
12
|
+
readonly file: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate the static subset (docs/findings/ast-dsl-contract.md §4): literals, arrays,
|
|
16
|
+
* objects, templates without dynamic parts, same-file consts and one-hop project imports.
|
|
17
|
+
* Never executes code; anything else fails and the caller records an unknown fact.
|
|
18
|
+
*/
|
|
19
|
+
export declare function evaluateStatic(node: AstNode | undefined, context: StaticContext, depth?: number): StaticEvaluation;
|
|
20
|
+
/** Convenience: a string or undefined, with the failure reason. */
|
|
21
|
+
export declare function staticString(node: AstNode | undefined, context: StaticContext): {
|
|
22
|
+
ok: true;
|
|
23
|
+
value: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
reason: string;
|
|
27
|
+
};
|
|
28
|
+
/** Convenience: a string[] or undefined, with the failure reason. */
|
|
29
|
+
export declare function staticStringArray(node: AstNode | undefined, context: StaticContext): {
|
|
30
|
+
ok: true;
|
|
31
|
+
value: string[];
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
export { literalString };
|