@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,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job `run(ctx)` -> relation usage (D-157 applied to jobs). A job's `db` is the
|
|
3
|
+
* system-scoped client, so its usage has no elevation bucket: it is the route walk with the
|
|
4
|
+
* `{ db }` seed on the first parameter and elevations disabled. The walk and the name
|
|
5
|
+
* resolution are the route ones (`route-relations-walk.ts` / `route-relation-resolve.ts`),
|
|
6
|
+
* so the two passes cannot drift.
|
|
7
|
+
*/
|
|
8
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
9
|
+
import type { Diagnostic, TableFact, ViewFact } from "../types.js";
|
|
10
|
+
import type { JobFact, JobRelationsFact } from "../types-jobs.js";
|
|
11
|
+
export interface JobRelationsScan {
|
|
12
|
+
readonly jobs: readonly JobFact[];
|
|
13
|
+
/** Usage per job, keyed by the job name (unique across the project). */
|
|
14
|
+
readonly relations: Readonly<Record<string, JobRelationsFact>>;
|
|
15
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
/** Attach relation-usage facts to every scannable job and derive the D-157 diagnostics. */
|
|
18
|
+
export declare function buildJobRelationFacts(graph: ModuleGraph, jobs: readonly JobFact[], tables: readonly TableFact[], views: readonly ViewFact[]): JobRelationsScan;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job `run(ctx)` -> relation usage (D-157 applied to jobs). A job's `db` is the
|
|
3
|
+
* system-scoped client, so its usage has no elevation bucket: it is the route walk with the
|
|
4
|
+
* `{ db }` seed on the first parameter and elevations disabled. The walk and the name
|
|
5
|
+
* resolution are the route ones (`route-relations-walk.ts` / `route-relation-resolve.ts`),
|
|
6
|
+
* so the two passes cannot drift.
|
|
7
|
+
*/
|
|
8
|
+
import { callArguments, unwrapExpression } from "../ast/node.js";
|
|
9
|
+
import { EMPTY_JOB_RELATIONS } from "../types-jobs.js";
|
|
10
|
+
import { createRelationIndex } from "./route-relation-resolve.js";
|
|
11
|
+
import { scanFunctionUsage } from "./route-relations-walk.js";
|
|
12
|
+
import { jobRelationDiagnostics } from "./job-relations-diagnostics.js";
|
|
13
|
+
import { resolveJobCall } from "./jobs.js";
|
|
14
|
+
import { readStaticObject, sortDiagnostics } from "./static-read.js";
|
|
15
|
+
/** A job runs as `facilis_system`: `run`'s first parameter is the context, no elevations. */
|
|
16
|
+
const JOB_SEED = { contextParam: 0, elevations: false };
|
|
17
|
+
/** The function expression of the definition's `run` property, when inspectable. */
|
|
18
|
+
function runFunction(graph, file, call) {
|
|
19
|
+
const object = readStaticObject(callArguments(call)[0], { graph, file: file.path }, { code: "job-definition-static", report: false });
|
|
20
|
+
const value = unwrapExpression(object.fields.get("run")?.node);
|
|
21
|
+
return value?.type === "ArrowFunctionExpression" || value?.type === "FunctionExpression"
|
|
22
|
+
? value
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
function toJobRelations(usage) {
|
|
26
|
+
return {
|
|
27
|
+
used: usage.used,
|
|
28
|
+
unresolved: usage.unresolved,
|
|
29
|
+
rawSql: usage.rawSql,
|
|
30
|
+
opaque: usage.opaque,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Attach relation-usage facts to every scannable job and derive the D-157 diagnostics. */
|
|
34
|
+
export function buildJobRelationFacts(graph, jobs, tables, views) {
|
|
35
|
+
const index = createRelationIndex(tables, views);
|
|
36
|
+
const relations = {};
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
for (const job of jobs) {
|
|
39
|
+
const file = graph.file(job.sourceFile);
|
|
40
|
+
const call = file === undefined ? null : resolveJobCall(file);
|
|
41
|
+
const run = file !== undefined && call !== null ? runFunction(graph, file, call) : undefined;
|
|
42
|
+
if (file === undefined || run === undefined) {
|
|
43
|
+
// The definition warning is the finding; an empty usage avoids a false unused warning.
|
|
44
|
+
relations[job.name] = EMPTY_JOB_RELATIONS;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const usage = toJobRelations(scanFunctionUsage(graph, file, run, index, JOB_SEED));
|
|
48
|
+
relations[job.name] = usage;
|
|
49
|
+
diagnostics.push(...jobRelationDiagnostics(job, usage));
|
|
50
|
+
}
|
|
51
|
+
return { jobs, relations, diagnostics: sortDiagnostics(diagnostics) };
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `server/jobs/**` definitions -> JobFacts (D-157 applied to jobs).
|
|
3
|
+
*
|
|
4
|
+
* One default export per file: a `defineJob()` call from `@facilis.dev/core`. A file that is not
|
|
5
|
+
* a valid definition is a warning, not an error: jobs are optional, so a broken file must not
|
|
6
|
+
* hide the rest of the project. `dbTables` is the declaration the runtime guard enforces, so
|
|
7
|
+
* an unreadable one is explicit (null) and its warning keeps the jobs gate red.
|
|
8
|
+
*/
|
|
9
|
+
import { type AstNode } from "../ast/node.js";
|
|
10
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.js";
|
|
11
|
+
import type { Diagnostic } from "../types.js";
|
|
12
|
+
import type { JobFact } from "../types-jobs.js";
|
|
13
|
+
export interface JobResult {
|
|
14
|
+
readonly jobs: readonly JobFact[];
|
|
15
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
/** The `defineJob(...)` call of a valid definition, for later passes (D-157). */
|
|
18
|
+
export declare function resolveJobCall(file: ProjectFile): AstNode | null;
|
|
19
|
+
/** Read every job module under `server/jobs/` into facts (test files are skipped). */
|
|
20
|
+
export declare function buildJobFacts(graph: ModuleGraph): JobResult;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `server/jobs/**` definitions -> JobFacts (D-157 applied to jobs).
|
|
3
|
+
*
|
|
4
|
+
* One default export per file: a `defineJob()` call from `@facilis.dev/core`. A file that is not
|
|
5
|
+
* a valid definition is a warning, not an error: jobs are optional, so a broken file must not
|
|
6
|
+
* hide the rest of the project. `dbTables` is the declaration the runtime guard enforces, so
|
|
7
|
+
* an unreadable one is explicit (null) and its warning keeps the jobs gate red.
|
|
8
|
+
*/
|
|
9
|
+
import { callArguments, identifierName, unwrapExpression } from "../ast/node.js";
|
|
10
|
+
import { warning } from "../diagnostics.js";
|
|
11
|
+
import { defaultExportExpression, isLocallyDeclared, readStaticObject, refOf, relativePath, sortDiagnostics, } from "./static-read.js";
|
|
12
|
+
const CORE_SPECIFIER = "@facilis.dev/core";
|
|
13
|
+
const JOBS_DIR = "server/jobs/";
|
|
14
|
+
const STATIC_CODE = "job-definition-static";
|
|
15
|
+
/** The default export must be a `defineJob(...)` call of the core import. */
|
|
16
|
+
function resolveDefinition(file) {
|
|
17
|
+
const expression = defaultExportExpression(file);
|
|
18
|
+
if (expression === undefined) {
|
|
19
|
+
return { call: null, reason: "the job file has no default export" };
|
|
20
|
+
}
|
|
21
|
+
if (expression.type !== "CallExpression") {
|
|
22
|
+
return { call: null, reason: "the default export is not a defineJob() call" };
|
|
23
|
+
}
|
|
24
|
+
const callee = unwrapExpression(expression.callee);
|
|
25
|
+
const name = callee?.type === "Identifier" ? identifierName(callee) : undefined;
|
|
26
|
+
if (name === undefined)
|
|
27
|
+
return { call: null, reason: "the defineJob callee cannot be resolved" };
|
|
28
|
+
if (isLocallyDeclared(file, name)) {
|
|
29
|
+
return { call: null, reason: '"' + name + '" is shadowed by a local declaration' };
|
|
30
|
+
}
|
|
31
|
+
const imported = file.module.imports.find((entry) => entry.local === name);
|
|
32
|
+
if (imported === undefined ||
|
|
33
|
+
imported.specifier !== CORE_SPECIFIER ||
|
|
34
|
+
imported.imported !== "defineJob") {
|
|
35
|
+
return {
|
|
36
|
+
call: null,
|
|
37
|
+
reason: "the default export is not the defineJob import from @facilis.dev/core",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { call: expression, reason: "" };
|
|
41
|
+
}
|
|
42
|
+
/** The `defineJob(...)` call of a valid definition, for later passes (D-157). */
|
|
43
|
+
export function resolveJobCall(file) {
|
|
44
|
+
return resolveDefinition(file).call;
|
|
45
|
+
}
|
|
46
|
+
function stringArray(value) {
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
return null;
|
|
49
|
+
return value.every((entry) => typeof entry === "string") ? [...value] : null;
|
|
50
|
+
}
|
|
51
|
+
function scanFile(graph, file) {
|
|
52
|
+
const context = { graph, file: file.path };
|
|
53
|
+
const definition = resolveDefinition(file);
|
|
54
|
+
const ref = refOf(context, definition.call ?? defaultExportExpression(file));
|
|
55
|
+
if (definition.call === null) {
|
|
56
|
+
return { job: null, diagnostics: [warning("job-definition", definition.reason, ref)] };
|
|
57
|
+
}
|
|
58
|
+
const object = readStaticObject(callArguments(definition.call)[0], context, {
|
|
59
|
+
code: STATIC_CODE,
|
|
60
|
+
report: false,
|
|
61
|
+
});
|
|
62
|
+
// A structural read failure (spread/computed key) makes the whole definition suspect: keep
|
|
63
|
+
// it visible as an unknown and as a warning, as the route config reader does.
|
|
64
|
+
const unknown = [...object.unknown];
|
|
65
|
+
const diagnostics = object.unknown.map((item) => warning(item.code, item.reason, item.source));
|
|
66
|
+
const note = (reason, at) => {
|
|
67
|
+
const source = refOf(context, at);
|
|
68
|
+
unknown.push({ code: STATIC_CODE, reason, source });
|
|
69
|
+
diagnostics.push(warning(STATIC_CODE, reason, source));
|
|
70
|
+
};
|
|
71
|
+
const nameField = object.fields.get("name");
|
|
72
|
+
let name = null;
|
|
73
|
+
if (nameField !== undefined &&
|
|
74
|
+
nameField.ok &&
|
|
75
|
+
typeof nameField.value === "string" &&
|
|
76
|
+
nameField.value.length > 0) {
|
|
77
|
+
name = nameField.value;
|
|
78
|
+
}
|
|
79
|
+
if (name === null) {
|
|
80
|
+
diagnostics.push(warning("job-definition", "the job name cannot be read statically", refOf(context, nameField?.node)));
|
|
81
|
+
return { job: null, diagnostics };
|
|
82
|
+
}
|
|
83
|
+
const tablesField = object.fields.get("dbTables");
|
|
84
|
+
let dbTables = null;
|
|
85
|
+
if (tablesField === undefined) {
|
|
86
|
+
note("dbTables is missing; declare the relations the job touches ([] means none)", undefined);
|
|
87
|
+
}
|
|
88
|
+
else if (!tablesField.ok) {
|
|
89
|
+
note("dbTables cannot be read statically: " + (tablesField.reason ?? "unsupported"), tablesField.node);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
dbTables = stringArray(tablesField.value);
|
|
93
|
+
if (dbTables === null)
|
|
94
|
+
note("dbTables must be a string[]", tablesField.node);
|
|
95
|
+
}
|
|
96
|
+
const rawSqlField = object.fields.get("rawSql");
|
|
97
|
+
let rawSql = false;
|
|
98
|
+
if (rawSqlField !== undefined && !rawSqlField.ok) {
|
|
99
|
+
rawSql = null;
|
|
100
|
+
note("rawSql cannot be read statically: " + (rawSqlField.reason ?? "unsupported"), rawSqlField.node);
|
|
101
|
+
}
|
|
102
|
+
else if (rawSqlField !== undefined) {
|
|
103
|
+
if (typeof rawSqlField.value === "boolean")
|
|
104
|
+
rawSql = rawSqlField.value;
|
|
105
|
+
else {
|
|
106
|
+
// An unreadable derogation is never assumed: db.execute then fails closed (D-157).
|
|
107
|
+
rawSql = null;
|
|
108
|
+
note("rawSql must be a boolean", rawSqlField.node);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
job: { name, sourceFile: file.path, source: ref, dbTables, rawSql, unknown },
|
|
113
|
+
diagnostics,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Read every job module under `server/jobs/` into facts (test files are skipped). */
|
|
117
|
+
export function buildJobFacts(graph) {
|
|
118
|
+
const files = [...graph.files.values()]
|
|
119
|
+
.filter((file) => {
|
|
120
|
+
const relative = relativePath(graph.root, file.path);
|
|
121
|
+
return relative.startsWith(JOBS_DIR) && !relative.endsWith(".test.ts");
|
|
122
|
+
})
|
|
123
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
124
|
+
const jobs = [];
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const scanned = scanFile(graph, file);
|
|
128
|
+
if (scanned.job !== null)
|
|
129
|
+
jobs.push(scanned.job);
|
|
130
|
+
diagnostics.push(...scanned.diagnostics);
|
|
131
|
+
}
|
|
132
|
+
return { jobs, diagnostics: sortDiagnostics(diagnostics) };
|
|
133
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small ESTree/JSX helpers shared by the route-table and page-preview passes.
|
|
3
|
+
*
|
|
4
|
+
* The analyzer's parser contract is parser-independent ESTree, so these helpers only read
|
|
5
|
+
* structural keys (`openingElement.name`, `attributes`, `children`, `expression`); they never
|
|
6
|
+
* execute project code and never depend on oxc-specific node types.
|
|
7
|
+
*/
|
|
8
|
+
import { type AstNode } from "../ast/node.js";
|
|
9
|
+
/** `JSXIdentifier` / `JSXMemberExpression` / `JSXNamespacedName` -> display name. */
|
|
10
|
+
export declare function jsxName(node: AstNode | undefined): string | null;
|
|
11
|
+
export declare function openingElement(node: AstNode): AstNode | undefined;
|
|
12
|
+
export declare function elementName(node: AstNode): string | null;
|
|
13
|
+
export declare function jsxChildren(node: AstNode): AstNode[];
|
|
14
|
+
export declare function jsxAttributes(node: AstNode): AstNode[];
|
|
15
|
+
/** The first attribute with this name, or undefined. */
|
|
16
|
+
export declare function jsxAttribute(node: AstNode, name: string): AstNode | undefined;
|
|
17
|
+
/** The value expression of an attribute, unwrapped from `JSXExpressionContainer`. */
|
|
18
|
+
export declare function jsxAttributeValue(attribute: AstNode | undefined): AstNode | undefined;
|
|
19
|
+
/** A JSX element/fragment value, when the expression is one. */
|
|
20
|
+
export declare function jsxElementValue(value: AstNode | undefined): AstNode | undefined;
|
|
21
|
+
export declare function isJsxElement(node: AstNode | undefined): node is AstNode;
|
|
22
|
+
export declare function isSectionIntrinsic(name: string): boolean;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small ESTree/JSX helpers shared by the route-table and page-preview passes.
|
|
3
|
+
*
|
|
4
|
+
* The analyzer's parser contract is parser-independent ESTree, so these helpers only read
|
|
5
|
+
* structural keys (`openingElement.name`, `attributes`, `children`, `expression`); they never
|
|
6
|
+
* execute project code and never depend on oxc-specific node types.
|
|
7
|
+
*/
|
|
8
|
+
import { asNode, nodeArray } from "../ast/node.js";
|
|
9
|
+
/** `JSXIdentifier` / `JSXMemberExpression` / `JSXNamespacedName` -> display name. */
|
|
10
|
+
export function jsxName(node) {
|
|
11
|
+
if (node === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
if (node.type === "JSXIdentifier")
|
|
14
|
+
return typeof node.name === "string" ? node.name : null;
|
|
15
|
+
if (node.type === "JSXMemberExpression") {
|
|
16
|
+
const object = jsxName(asNode(node.object));
|
|
17
|
+
const property = jsxName(asNode(node.property));
|
|
18
|
+
return object === null || property === null ? null : object + "." + property;
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "JSXNamespacedName") {
|
|
21
|
+
const namespace = jsxName(asNode(node.namespace));
|
|
22
|
+
const name = jsxName(asNode(node.name));
|
|
23
|
+
return namespace === null || name === null ? null : namespace + ":" + name;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function openingElement(node) {
|
|
28
|
+
return node.type === "JSXElement" ? asNode(node.openingElement) : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function elementName(node) {
|
|
31
|
+
return jsxName(asNode(openingElement(node)?.name));
|
|
32
|
+
}
|
|
33
|
+
export function jsxChildren(node) {
|
|
34
|
+
return nodeArray(node, "children");
|
|
35
|
+
}
|
|
36
|
+
export function jsxAttributes(node) {
|
|
37
|
+
const opening = openingElement(node);
|
|
38
|
+
return opening === undefined ? [] : nodeArray(opening, "attributes");
|
|
39
|
+
}
|
|
40
|
+
/** The first attribute with this name, or undefined. */
|
|
41
|
+
export function jsxAttribute(node, name) {
|
|
42
|
+
for (const attribute of jsxAttributes(node)) {
|
|
43
|
+
if (attribute.type !== "JSXAttribute")
|
|
44
|
+
continue;
|
|
45
|
+
if (jsxName(asNode(attribute.name)) === name)
|
|
46
|
+
return attribute;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** The value expression of an attribute, unwrapped from `JSXExpressionContainer`. */
|
|
51
|
+
export function jsxAttributeValue(attribute) {
|
|
52
|
+
if (attribute === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const value = asNode(attribute.value);
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (value.type === "JSXExpressionContainer")
|
|
58
|
+
return asNode(value.expression);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
/** A JSX element/fragment value, when the expression is one. */
|
|
62
|
+
export function jsxElementValue(value) {
|
|
63
|
+
if (value === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (value.type === "JSXElement" || value.type === "JSXFragment")
|
|
66
|
+
return value;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
export function isJsxElement(node) {
|
|
70
|
+
return node !== undefined && (node.type === "JSXElement" || node.type === "JSXFragment");
|
|
71
|
+
}
|
|
72
|
+
/** JSX elements that carry a page section (`<div>`/`<span>` are flattened away). */
|
|
73
|
+
const SECTION_INTRINSICS = new Set([
|
|
74
|
+
"header",
|
|
75
|
+
"main",
|
|
76
|
+
"section",
|
|
77
|
+
"nav",
|
|
78
|
+
"aside",
|
|
79
|
+
"footer",
|
|
80
|
+
"form",
|
|
81
|
+
"table",
|
|
82
|
+
"ul",
|
|
83
|
+
"h1",
|
|
84
|
+
"h2",
|
|
85
|
+
"h3",
|
|
86
|
+
]);
|
|
87
|
+
export function isSectionIntrinsic(name) {
|
|
88
|
+
return SECTION_INTRINSICS.has(name);
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client path expressions -> static prefixes and route matches (§3.4 of the AST DSL
|
|
3
|
+
* contract).
|
|
4
|
+
*
|
|
5
|
+
* A path is read as a static prefix plus a dynamic tail: literal strings and const strings
|
|
6
|
+
* are exact, while concatenations and templates keep the leading static segments. One hop of
|
|
7
|
+
* a path-builder function is inlined (the builder's return expression); anything deeper is
|
|
8
|
+
* dynamic, not guessed.
|
|
9
|
+
*/
|
|
10
|
+
import { type AstNode } from "../ast/node.js";
|
|
11
|
+
import { type StaticContext } from "../static-eval.js";
|
|
12
|
+
import type { HttpMethod, PathKind, RouteFact, UnknownFact } from "../types.js";
|
|
13
|
+
export interface ExtractedPath {
|
|
14
|
+
readonly kind: PathKind;
|
|
15
|
+
/** Source text of the argument, for the inspector. */
|
|
16
|
+
readonly expression: string;
|
|
17
|
+
/** Leading static text; null when the path has no readable static part. */
|
|
18
|
+
readonly staticPrefix: string | null;
|
|
19
|
+
/** Fully static path, when the whole expression is known. */
|
|
20
|
+
readonly path: string | null;
|
|
21
|
+
readonly unknown: readonly UnknownFact[];
|
|
22
|
+
}
|
|
23
|
+
/** Extract the path of one client call; failures stay in the unknown channel. */
|
|
24
|
+
export declare function extractPathExpression(argument: AstNode | undefined, context: StaticContext, call: AstNode, code: string): ExtractedPath;
|
|
25
|
+
/** Route ids whose template can produce the call path for the given method. */
|
|
26
|
+
export declare function matchRouteIds(routes: readonly RouteFact[], path: string | null, staticPrefix: string | null, method: HttpMethod): string[];
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client path expressions -> static prefixes and route matches (§3.4 of the AST DSL
|
|
3
|
+
* contract).
|
|
4
|
+
*
|
|
5
|
+
* A path is read as a static prefix plus a dynamic tail: literal strings and const strings
|
|
6
|
+
* are exact, while concatenations and templates keep the leading static segments. One hop of
|
|
7
|
+
* a path-builder function is inlined (the builder's return expression); anything deeper is
|
|
8
|
+
* dynamic, not guessed.
|
|
9
|
+
*/
|
|
10
|
+
import { asNode, identifierName, isAstNode, nodeArray, unwrapExpression, } from "../ast/node.js";
|
|
11
|
+
import { defaultExportDeclaration, localInitializer } from "../module-info.js";
|
|
12
|
+
import { evaluateStatic } from "../static-eval.js";
|
|
13
|
+
import { routeFactId } from "./route-location.js";
|
|
14
|
+
import { findDeclaration, refOf } from "./static-read.js";
|
|
15
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
16
|
+
function sourceText(source, node) {
|
|
17
|
+
if (node === undefined || typeof node.start !== "number" || typeof node.end !== "number") {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
return source.slice(node.start, node.end).trim();
|
|
21
|
+
}
|
|
22
|
+
function cookedString(quasi) {
|
|
23
|
+
// TemplateElement.value is a plain { raw, cooked } record, not an AST node (asNode rejects it).
|
|
24
|
+
const value = quasi?.value;
|
|
25
|
+
if (typeof value !== "object" || value === null)
|
|
26
|
+
return "";
|
|
27
|
+
const cooked = value.cooked;
|
|
28
|
+
return typeof cooked === "string" ? cooked : "";
|
|
29
|
+
}
|
|
30
|
+
function evaluatePrimitive(node, context) {
|
|
31
|
+
const evaluated = evaluateStatic(node, context);
|
|
32
|
+
const value = evaluated.ok ? evaluated.value : undefined;
|
|
33
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
34
|
+
? String(value)
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
/** Leading static text of a literal, template or `+` chain, plus whether it is whole. */
|
|
38
|
+
function partialStaticPrefix(node, context, depth = 0) {
|
|
39
|
+
if (node === undefined || depth > MAX_RESOLVE_DEPTH)
|
|
40
|
+
return null;
|
|
41
|
+
const unwrapped = unwrapExpression(node);
|
|
42
|
+
if (unwrapped === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
if (unwrapped.type === "Literal") {
|
|
45
|
+
return typeof unwrapped.value === "string" ? { prefix: unwrapped.value, complete: true } : null;
|
|
46
|
+
}
|
|
47
|
+
if (unwrapped.type === "TemplateLiteral") {
|
|
48
|
+
const quasis = nodeArray(unwrapped, "quasis");
|
|
49
|
+
const expressions = nodeArray(unwrapped, "expressions");
|
|
50
|
+
let prefix = "";
|
|
51
|
+
for (const [index, quasi] of quasis.entries()) {
|
|
52
|
+
prefix += cookedString(quasi);
|
|
53
|
+
const expression = expressions[index];
|
|
54
|
+
if (expression === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
const value = evaluatePrimitive(expression, context);
|
|
57
|
+
if (value === null)
|
|
58
|
+
return { prefix, complete: false };
|
|
59
|
+
prefix += value;
|
|
60
|
+
}
|
|
61
|
+
return { prefix, complete: true };
|
|
62
|
+
}
|
|
63
|
+
if (unwrapped.type === "BinaryExpression" && unwrapped.operator === "+") {
|
|
64
|
+
const left = partialStaticPrefix(asNode(unwrapped.left), context, depth + 1);
|
|
65
|
+
if (left === null)
|
|
66
|
+
return null;
|
|
67
|
+
if (!left.complete)
|
|
68
|
+
return left;
|
|
69
|
+
const right = partialStaticPrefix(asNode(unwrapped.right), context, depth + 1);
|
|
70
|
+
return right === null
|
|
71
|
+
? { prefix: left.prefix, complete: false }
|
|
72
|
+
: { prefix: left.prefix + right.prefix, complete: right.complete };
|
|
73
|
+
}
|
|
74
|
+
const value = evaluatePrimitive(unwrapped, context);
|
|
75
|
+
return value === null ? null : { prefix: value, complete: true };
|
|
76
|
+
}
|
|
77
|
+
function firstReturn(node) {
|
|
78
|
+
if (node.type === "ReturnStatement")
|
|
79
|
+
return asNode(node.argument);
|
|
80
|
+
for (const key of Object.keys(node)) {
|
|
81
|
+
if (key === "type" || key === "start" || key === "end")
|
|
82
|
+
continue;
|
|
83
|
+
const value = node[key];
|
|
84
|
+
const children = (Array.isArray(value) ? value : [value]).filter(isAstNode);
|
|
85
|
+
for (const child of children) {
|
|
86
|
+
// A nested function owns its return statements; only this function's count.
|
|
87
|
+
if (child.type.endsWith("FunctionExpression") || child.type === "FunctionDeclaration") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const found = firstReturn(child);
|
|
91
|
+
if (found !== undefined)
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function functionReturnExpression(fn) {
|
|
98
|
+
const body = asNode(fn.body);
|
|
99
|
+
if (body === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
return body.type === "BlockStatement" ? firstReturn(body) : body;
|
|
102
|
+
}
|
|
103
|
+
/** Inline one hop of an imported path-builder function (its return expression). */
|
|
104
|
+
function resolvePathBuilder(call, context) {
|
|
105
|
+
const callee = unwrapExpression(call.callee);
|
|
106
|
+
const name = identifierName(callee);
|
|
107
|
+
const file = context.graph.file(context.file);
|
|
108
|
+
if (name === undefined || file === undefined)
|
|
109
|
+
return null;
|
|
110
|
+
const imported = file.module.imports.find((entry) => entry.local === name);
|
|
111
|
+
if (imported === undefined)
|
|
112
|
+
return null;
|
|
113
|
+
const target = context.graph.resolve(context.file, imported.specifier);
|
|
114
|
+
if (target === null || target.startsWith("node_modules/"))
|
|
115
|
+
return null;
|
|
116
|
+
const binding = context.graph.resolveExport(target, imported.imported ?? "default");
|
|
117
|
+
if (binding === null)
|
|
118
|
+
return null;
|
|
119
|
+
const module = context.graph.file(binding.file);
|
|
120
|
+
if (module === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const initializer = binding.name === "default"
|
|
123
|
+
? defaultExportDeclaration(module.program)
|
|
124
|
+
: (localInitializer(module.program, binding.name) ??
|
|
125
|
+
findDeclaration(module.program, binding.name));
|
|
126
|
+
const fn = unwrapExpression(initializer);
|
|
127
|
+
if (fn === undefined || !/Function/.test(fn.type))
|
|
128
|
+
return null;
|
|
129
|
+
const returned = functionReturnExpression(fn);
|
|
130
|
+
if (returned === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
return partialStaticPrefix(returned, { graph: context.graph, file: module.path });
|
|
133
|
+
}
|
|
134
|
+
/** Extract the path of one client call; failures stay in the unknown channel. */
|
|
135
|
+
export function extractPathExpression(argument, context, call, code) {
|
|
136
|
+
const file = context.graph.file(context.file);
|
|
137
|
+
const source = file?.source ?? "";
|
|
138
|
+
const callRef = refOf(context, call);
|
|
139
|
+
const argumentNode = unwrapExpression(argument);
|
|
140
|
+
const expression = sourceText(source, argumentNode) || sourceText(source, call);
|
|
141
|
+
if (argumentNode === undefined) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "dynamic",
|
|
144
|
+
expression,
|
|
145
|
+
staticPrefix: null,
|
|
146
|
+
path: null,
|
|
147
|
+
unknown: [{ code, reason: "the call has no path argument", source: callRef }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const unknown = [];
|
|
151
|
+
if (argumentNode.type === "CallExpression") {
|
|
152
|
+
const builder = resolvePathBuilder(argumentNode, context);
|
|
153
|
+
if (builder !== null) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "builder",
|
|
156
|
+
expression,
|
|
157
|
+
staticPrefix: builder.prefix.length === 0 ? null : builder.prefix,
|
|
158
|
+
path: builder.complete ? builder.prefix : null,
|
|
159
|
+
unknown,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
unknown.push({
|
|
163
|
+
code,
|
|
164
|
+
reason: "the path builder could not be resolved statically",
|
|
165
|
+
source: callRef,
|
|
166
|
+
});
|
|
167
|
+
return { kind: "unknown", expression, staticPrefix: null, path: null, unknown };
|
|
168
|
+
}
|
|
169
|
+
if (argumentNode.type === "TemplateLiteral" || argumentNode.type === "BinaryExpression") {
|
|
170
|
+
const partial = partialStaticPrefix(argumentNode, context);
|
|
171
|
+
if (partial !== null) {
|
|
172
|
+
return {
|
|
173
|
+
kind: argumentNode.type === "TemplateLiteral" ? "template" : "concat",
|
|
174
|
+
expression,
|
|
175
|
+
staticPrefix: partial.prefix.length === 0 ? null : partial.prefix,
|
|
176
|
+
path: partial.complete ? partial.prefix : null,
|
|
177
|
+
unknown,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const evaluated = evaluateStatic(argumentNode, context);
|
|
182
|
+
if (evaluated.ok && typeof evaluated.value === "string") {
|
|
183
|
+
return {
|
|
184
|
+
kind: "literal",
|
|
185
|
+
expression,
|
|
186
|
+
staticPrefix: evaluated.value.length === 0 ? null : evaluated.value,
|
|
187
|
+
path: evaluated.value,
|
|
188
|
+
unknown,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
unknown.push({
|
|
192
|
+
code,
|
|
193
|
+
reason: evaluated.ok ? "the path is not a string" : evaluated.reason,
|
|
194
|
+
source: callRef,
|
|
195
|
+
});
|
|
196
|
+
return { kind: "dynamic", expression, staticPrefix: null, path: null, unknown };
|
|
197
|
+
}
|
|
198
|
+
function pathSegments(path) {
|
|
199
|
+
const base = path.split("?")[0] ?? "";
|
|
200
|
+
return (base.split("#")[0] ?? "").split("/").filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
function isDynamicSegment(segment) {
|
|
203
|
+
return segment.startsWith("{") && segment.endsWith("}");
|
|
204
|
+
}
|
|
205
|
+
function templateMatches(template, segments) {
|
|
206
|
+
let index = 0;
|
|
207
|
+
for (; index < template.length; index += 1) {
|
|
208
|
+
const templateSegment = template[index] ?? "";
|
|
209
|
+
if (templateSegment.startsWith("{*"))
|
|
210
|
+
return segments.length > index;
|
|
211
|
+
if (isDynamicSegment(templateSegment)) {
|
|
212
|
+
if (index >= segments.length)
|
|
213
|
+
return false;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (segments[index] !== templateSegment)
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return segments.length === index;
|
|
220
|
+
}
|
|
221
|
+
function prefixMatches(template, prefixSegments, trailingSlash) {
|
|
222
|
+
for (const [index, prefixSegment] of prefixSegments.entries()) {
|
|
223
|
+
const templateSegment = template[index];
|
|
224
|
+
if (templateSegment === undefined)
|
|
225
|
+
return false;
|
|
226
|
+
if (templateSegment.startsWith("{*"))
|
|
227
|
+
return false;
|
|
228
|
+
if (isDynamicSegment(templateSegment))
|
|
229
|
+
continue;
|
|
230
|
+
if (templateSegment !== prefixSegment)
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
const rest = template.slice(prefixSegments.length);
|
|
234
|
+
// A trailing slash means at least one more (dynamic) segment at the call site.
|
|
235
|
+
if (trailingSlash && rest.length === 0)
|
|
236
|
+
return false;
|
|
237
|
+
return rest.every((segment) => isDynamicSegment(segment));
|
|
238
|
+
}
|
|
239
|
+
function routeMatches(routePath, path, staticPrefix) {
|
|
240
|
+
const template = pathSegments(routePath);
|
|
241
|
+
if (path !== null)
|
|
242
|
+
return templateMatches(template, pathSegments(path));
|
|
243
|
+
if (staticPrefix === null)
|
|
244
|
+
return false;
|
|
245
|
+
return prefixMatches(template, pathSegments(staticPrefix), staticPrefix.endsWith("/"));
|
|
246
|
+
}
|
|
247
|
+
/** Route ids whose template can produce the call path for the given method. */
|
|
248
|
+
export function matchRouteIds(routes, path, staticPrefix, method) {
|
|
249
|
+
if (path === null && staticPrefix === null)
|
|
250
|
+
return [];
|
|
251
|
+
const ids = [];
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
for (const route of routes) {
|
|
254
|
+
if (route.method !== "ALL" && route.method !== method)
|
|
255
|
+
continue;
|
|
256
|
+
if (!routeMatches(route.path, path, staticPrefix))
|
|
257
|
+
continue;
|
|
258
|
+
const id = routeFactId(route);
|
|
259
|
+
if (seen.has(id))
|
|
260
|
+
continue;
|
|
261
|
+
seen.add(id);
|
|
262
|
+
ids.push(id);
|
|
263
|
+
}
|
|
264
|
+
return ids;
|
|
265
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wrapper config subset: permission/crossSite/dbTables/dbColumns/description/input.
|
|
3
|
+
*
|
|
4
|
+
* Split from routes.ts so both modules stay under the 300-line limit; every failure is an
|
|
5
|
+
* explicit unknown (never an empty value) and a diagnostic, as in the rest of the analyzer.
|
|
6
|
+
*/
|
|
7
|
+
import { type AstNode } from "../ast/node.js";
|
|
8
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.js";
|
|
9
|
+
import type { ConfigFacts, Diagnostic, RouteInputFact, UnknownFact } from "../types.js";
|
|
10
|
+
export interface RouteConfigFacts {
|
|
11
|
+
readonly permission: readonly string[] | null;
|
|
12
|
+
/** True when the wrapper opts the route into cross-origin calls; null when unreadable. */
|
|
13
|
+
readonly crossSite: boolean | null;
|
|
14
|
+
/** rawSql: true allows `db.execute` (D-157); false when absent; null when unreadable. */
|
|
15
|
+
readonly rawSql: boolean | null;
|
|
16
|
+
readonly dbTables: readonly string[] | null;
|
|
17
|
+
readonly dbColumns: readonly string[] | null;
|
|
18
|
+
readonly description: string | null;
|
|
19
|
+
readonly input: RouteInputFact;
|
|
20
|
+
readonly unknown: readonly UnknownFact[];
|
|
21
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
22
|
+
}
|
|
23
|
+
/** Read the wrapper config from the static subset; every failure is explicit. */
|
|
24
|
+
export declare function routeConfigFacts(graph: ModuleGraph, file: ProjectFile, call: AstNode, config: ConfigFacts): RouteConfigFacts;
|