no-mistakes 0.47.0 → 0.48.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/README.md +6 -7
- package/flow-types.d.ts +12 -1
- package/index.d.ts +4 -0
- package/index.js +79 -3
- package/index.mjs +64 -0
- package/invocation-types.d.ts +4 -2
- package/package.json +12 -1
- package/planning.js +141 -3
- package/resolve-config-types.d.ts +50 -0
- package/test-types.d.ts +40 -17
- package/traversal-types.d.ts +9 -0
- package/types.d.ts +1 -0
- package/workflow-topology-index-helpers.js +28 -0
- package/workflow-topology-index-types.d.ts +7 -0
- package/workflow-topology-index.js +10 -0
package/README.md
CHANGED
|
@@ -43,8 +43,6 @@ const {
|
|
|
43
43
|
root: process.cwd(),
|
|
44
44
|
files: ["src/main.mts"],
|
|
45
45
|
relationships: ["import"],
|
|
46
|
-
timeout: 30,
|
|
47
|
-
lockTimeout: 30,
|
|
48
46
|
});
|
|
49
47
|
const tests = await dependents({
|
|
50
48
|
root: process.cwd(),
|
|
@@ -72,7 +70,7 @@ const {
|
|
|
72
70
|
changedFiles: ["src/utils.mts"],
|
|
73
71
|
});
|
|
74
72
|
// Complete changed-file inventory, including paths that selected no tests.
|
|
75
|
-
console.log(plan.
|
|
73
|
+
console.log(plan.changedFiles);
|
|
76
74
|
const targetCommands = await testsTargets({
|
|
77
75
|
root: process.cwd(),
|
|
78
76
|
framework: "vitest",
|
|
@@ -121,10 +119,11 @@ const {
|
|
|
121
119
|
|
|
122
120
|
CLI and Node analyses share a per-user machine-wide lock. CLI flags
|
|
123
121
|
`--timeout`, `--lock-timeout`, and `--fail-on-lock` have Node equivalents
|
|
124
|
-
`timeout`, `lockTimeout`, and `failOnLock`.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
the
|
|
122
|
+
`timeout`, `lockTimeout`, and `failOnLock`. CLI timeouts default to 30 seconds;
|
|
123
|
+
Node/N-API omits both deadlines unless you set them. `0` disables either CLI
|
|
124
|
+
timeout, while `0` or `null` disables it in Node. While waiting, stderr reports
|
|
125
|
+
the holder pid and elapsed seconds. Waiting does not alter successful output,
|
|
126
|
+
and Node lock/timeout failures reject the returned Promise.
|
|
128
127
|
|
|
129
128
|
Dependency graph, query, and test-planning resolution is per workspace by
|
|
130
129
|
default: when `tsconfig` is omitted, each import uses the config that owns its
|
package/flow-types.d.ts
CHANGED
|
@@ -15,7 +15,14 @@ export interface FlowOptions {
|
|
|
15
15
|
|
|
16
16
|
export interface FlowNode {
|
|
17
17
|
id: string;
|
|
18
|
-
kind:
|
|
18
|
+
kind:
|
|
19
|
+
| "file"
|
|
20
|
+
| "symbol"
|
|
21
|
+
| "module"
|
|
22
|
+
| "queue-job"
|
|
23
|
+
| "workflow-job"
|
|
24
|
+
| "workflow-step"
|
|
25
|
+
| "trpc-procedure";
|
|
19
26
|
depth: number;
|
|
20
27
|
file?: string;
|
|
21
28
|
symbol?: string;
|
|
@@ -27,6 +34,10 @@ export interface FlowNode {
|
|
|
27
34
|
job?: string;
|
|
28
35
|
/** Zero-based step index for a virtual GitHub Actions workflow step node. */
|
|
29
36
|
step?: number;
|
|
37
|
+
/** Router file for a virtual tRPC procedure node. */
|
|
38
|
+
routerFile?: string;
|
|
39
|
+
/** Dotted procedure path for a virtual tRPC procedure node (`user.get`). */
|
|
40
|
+
procedure?: string;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
export interface FlowEdge {
|
package/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ResolveCheckFilesOptions,
|
|
25
25
|
ResolveCheckResult,
|
|
26
26
|
ResolveCheckBatchResult,
|
|
27
|
+
ResolvedConfig,
|
|
27
28
|
GraphEdge,
|
|
28
29
|
PlaywrightOptions,
|
|
29
30
|
PlaywrightRelatedOptions,
|
|
@@ -98,6 +99,9 @@ export function resolveCheck(
|
|
|
98
99
|
export function fetches(options?: WithInvocationOptions<FetchesOptions>): Promise<FetchReport>;
|
|
99
100
|
export function flow(options: WithInvocationOptions<FlowOptions>): Promise<FlowReport>;
|
|
100
101
|
export function check(options?: WithInvocationOptions<CheckOptions>): Promise<CheckReport>;
|
|
102
|
+
export function resolveConfig(
|
|
103
|
+
options?: WithInvocationOptions<ProjectOptions>,
|
|
104
|
+
): Promise<ResolvedConfig>;
|
|
101
105
|
export function validateMermaidMarkdown(
|
|
102
106
|
options: WithInvocationOptions<MermaidValidationOptions>,
|
|
103
107
|
): Promise<MermaidValidationResult>;
|
package/index.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
|
|
6
6
|
const planning = require("./planning");
|
|
7
7
|
const { createWorkflowTopologyIndex } = require("./workflow-topology-index");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
8
10
|
|
|
9
11
|
async function callJson(fn, options) {
|
|
10
|
-
|
|
12
|
+
const input = Buffer.from(JSON.stringify(options || {}));
|
|
13
|
+
return JSON.parse(await fn(input));
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
function createJsonApis(descriptors) {
|
|
@@ -23,6 +26,7 @@ const jsonApis = createJsonApis({
|
|
|
23
26
|
analyzeProject: "analyzeProjectJson",
|
|
24
27
|
callSites: "callSitesJson",
|
|
25
28
|
check: "checkJson",
|
|
29
|
+
resolveConfig: "resolveConfigJson",
|
|
26
30
|
ciEnv: "ciEnvJson",
|
|
27
31
|
ciImpact: "ciImpactJson",
|
|
28
32
|
ciTopology: "ciTopologyJson",
|
|
@@ -57,18 +61,90 @@ const jsonApis = createJsonApis({
|
|
|
57
61
|
symbols: "symbolsJson",
|
|
58
62
|
});
|
|
59
63
|
|
|
64
|
+
const PLAN_INPUT_REPORTS = new Set(["testsComment", "testsGraph", "testsGraphMermaid"]);
|
|
65
|
+
const CAMELIZE_REPORTS = new Set(["testsPlan", "testsImpact", "testsTargets", "testsGraph"]);
|
|
66
|
+
|
|
67
|
+
async function analyzeProject(options = {}) {
|
|
68
|
+
const request = { ...options };
|
|
69
|
+
const generatedDirs = [];
|
|
70
|
+
try {
|
|
71
|
+
if (Array.isArray(request.reports)) {
|
|
72
|
+
request.reports = await Promise.all(
|
|
73
|
+
request.reports.map(async (report) => {
|
|
74
|
+
if (report.type === "testsWhy") {
|
|
75
|
+
const prepared = await planning.prepareWhyPlan(report);
|
|
76
|
+
if (prepared.generatedDir) generatedDirs.push(prepared.generatedDir);
|
|
77
|
+
return prepared.request;
|
|
78
|
+
}
|
|
79
|
+
return PLAN_INPUT_REPORTS.has(report.type)
|
|
80
|
+
? await planning.decamelizePlanOptions(report)
|
|
81
|
+
: report;
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const result = await jsonApis.analyzeProject(request);
|
|
86
|
+
for (const report of result.reports || []) {
|
|
87
|
+
if (report.type === "testsWhy") {
|
|
88
|
+
report.result = planning.camelizeWhy(report.result);
|
|
89
|
+
} else if (CAMELIZE_REPORTS.has(report.type)) {
|
|
90
|
+
report.result = planning.camelizeValue(report.result);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
} finally {
|
|
95
|
+
await Promise.all(generatedDirs.map((dir) => planning.removeGeneratedDir(dir)));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const topologyMemo = new Map();
|
|
100
|
+
|
|
101
|
+
async function ciTopology(options) {
|
|
102
|
+
const root = path.resolve((options && options.root) || process.cwd());
|
|
103
|
+
const configPath = path.resolve(root, (options && options.config) || ".no-mistakes.yml");
|
|
104
|
+
let mtime = 0;
|
|
105
|
+
try {
|
|
106
|
+
mtime = fs.statSync(configPath).mtimeMs;
|
|
107
|
+
} catch {
|
|
108
|
+
mtime = 0;
|
|
109
|
+
}
|
|
110
|
+
const workflows = JSON.stringify(
|
|
111
|
+
[]
|
|
112
|
+
.concat((options && options.workflows) || [])
|
|
113
|
+
.map(String)
|
|
114
|
+
.sort(),
|
|
115
|
+
);
|
|
116
|
+
const identity = `${root}\0${configPath}\0`;
|
|
117
|
+
const key = `${identity}${mtime}\0${workflows}`;
|
|
118
|
+
const stale = [];
|
|
119
|
+
for (const memoKey of topologyMemo.keys()) {
|
|
120
|
+
if (!memoKey.startsWith(identity)) continue;
|
|
121
|
+
const memoMtime = memoKey.slice(identity.length).split("\0")[0];
|
|
122
|
+
if (memoMtime !== String(mtime)) stale.push(memoKey);
|
|
123
|
+
}
|
|
124
|
+
for (const memoKey of stale) topologyMemo.delete(memoKey);
|
|
125
|
+
const cached = topologyMemo.get(key);
|
|
126
|
+
if (cached) return cached.then((value) => structuredClone(value));
|
|
127
|
+
const pending = jsonApis.ciTopology({ ...options, root }).catch((error) => {
|
|
128
|
+
topologyMemo.delete(key);
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
topologyMemo.set(key, pending);
|
|
132
|
+
return pending.then((value) => structuredClone(value));
|
|
133
|
+
}
|
|
134
|
+
|
|
60
135
|
async function version() {
|
|
61
136
|
return native.version();
|
|
62
137
|
}
|
|
63
138
|
|
|
64
139
|
module.exports.createWorkflowTopologyIndex = createWorkflowTopologyIndex;
|
|
65
140
|
module.exports.version = version;
|
|
66
|
-
module.exports.analyzeProject =
|
|
141
|
+
module.exports.analyzeProject = analyzeProject;
|
|
67
142
|
module.exports.callSites = jsonApis.callSites;
|
|
68
143
|
module.exports.check = jsonApis.check;
|
|
144
|
+
module.exports.resolveConfig = jsonApis.resolveConfig;
|
|
69
145
|
module.exports.ciEnv = jsonApis.ciEnv;
|
|
70
146
|
module.exports.ciImpact = jsonApis.ciImpact;
|
|
71
|
-
module.exports.ciTopology =
|
|
147
|
+
module.exports.ciTopology = ciTopology;
|
|
72
148
|
module.exports.dataPw = jsonApis.dataPw;
|
|
73
149
|
module.exports.deadExports = jsonApis.deadExports;
|
|
74
150
|
module.exports.dependencies = jsonApis.dependencies;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const cjs = require("./index.js");
|
|
5
|
+
|
|
6
|
+
export const {
|
|
7
|
+
analyzeProject,
|
|
8
|
+
callSites,
|
|
9
|
+
check,
|
|
10
|
+
ciEnv,
|
|
11
|
+
ciImpact,
|
|
12
|
+
ciTopology,
|
|
13
|
+
createWorkflowTopologyIndex,
|
|
14
|
+
dataPw,
|
|
15
|
+
deadExports,
|
|
16
|
+
dependencies,
|
|
17
|
+
dependents,
|
|
18
|
+
effects,
|
|
19
|
+
exportsOf,
|
|
20
|
+
fetches,
|
|
21
|
+
flow,
|
|
22
|
+
impactedChecks,
|
|
23
|
+
importUsages,
|
|
24
|
+
importers,
|
|
25
|
+
infraOutputs,
|
|
26
|
+
infraResourceRefs,
|
|
27
|
+
infraTestFor,
|
|
28
|
+
lockfileDiff,
|
|
29
|
+
playwrightCheck,
|
|
30
|
+
playwrightEdges,
|
|
31
|
+
playwrightRelated,
|
|
32
|
+
playwrightTests,
|
|
33
|
+
queueCheck,
|
|
34
|
+
queueEdges,
|
|
35
|
+
queueRelated,
|
|
36
|
+
queues,
|
|
37
|
+
reactAnalyze,
|
|
38
|
+
reactCheck,
|
|
39
|
+
reactUsages,
|
|
40
|
+
registryExtension,
|
|
41
|
+
related,
|
|
42
|
+
resolveCheck,
|
|
43
|
+
resolveConfig,
|
|
44
|
+
rscCallers,
|
|
45
|
+
serverContracts,
|
|
46
|
+
serverRouteEdges,
|
|
47
|
+
serverRouteList,
|
|
48
|
+
serverRouteRelated,
|
|
49
|
+
serverRoutes,
|
|
50
|
+
swiftImporters,
|
|
51
|
+
swiftTestTargets,
|
|
52
|
+
symbols,
|
|
53
|
+
testsComment,
|
|
54
|
+
testsGraph,
|
|
55
|
+
testsGraphMermaid,
|
|
56
|
+
testsImpact,
|
|
57
|
+
testsPlan,
|
|
58
|
+
testsTargets,
|
|
59
|
+
testsWhy,
|
|
60
|
+
validateMermaidMarkdown,
|
|
61
|
+
version,
|
|
62
|
+
} = cjs;
|
|
63
|
+
|
|
64
|
+
export default cjs;
|
package/invocation-types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Controls shared by every analysis invocation. Durations are in seconds. */
|
|
2
2
|
export interface InvocationOptions {
|
|
3
|
-
/** Command execution timeout
|
|
3
|
+
/** Command execution timeout in whole non-negative seconds. Omit, `0`, or `null` disables it. CLI default remains 30. */
|
|
4
4
|
timeout?: number | null;
|
|
5
|
-
/** Maximum time to wait for the machine-wide lock
|
|
5
|
+
/** Maximum time to wait for the machine-wide lock, in whole non-negative seconds. Omit, `0`, or `null` waits indefinitely. CLI default remains 30. */
|
|
6
6
|
lockTimeout?: number | null;
|
|
7
7
|
/** Fail immediately instead of waiting when another invocation holds the lock. */
|
|
8
8
|
failOnLock?: boolean;
|
|
@@ -11,6 +11,8 @@ export interface InvocationOptions {
|
|
|
11
11
|
* `0` uses the CPU count, matching CLI `--jobs 0`.
|
|
12
12
|
*/
|
|
13
13
|
jobs?: number | null;
|
|
14
|
+
/** `ci` sets unbounded command and lock timeouts. CLI `--profile ci` does the same. */
|
|
15
|
+
profile?: "ci";
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type WithInvocationOptions<T> = T & InvocationOptions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.1",
|
|
4
4
|
"description": "Static codebase analysis tools for TS/JS dependencies, dependents, and symbols",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
"bin/",
|
|
16
16
|
"*.d.ts",
|
|
17
17
|
"index.js",
|
|
18
|
+
"index.mjs",
|
|
18
19
|
"planning.js",
|
|
19
20
|
"workflow-topology-index.js",
|
|
21
|
+
"workflow-topology-index-helpers.js",
|
|
20
22
|
"scripts/install.js",
|
|
21
23
|
"scripts/install/",
|
|
22
24
|
"README.md",
|
|
@@ -24,6 +26,15 @@
|
|
|
24
26
|
],
|
|
25
27
|
"main": "index.js",
|
|
26
28
|
"types": "index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./index.d.ts",
|
|
32
|
+
"import": "./index.mjs",
|
|
33
|
+
"require": "./index.js"
|
|
34
|
+
},
|
|
35
|
+
"./planning.js": "./planning.js",
|
|
36
|
+
"./workflow-topology-index.js": "./workflow-topology-index.js"
|
|
37
|
+
},
|
|
27
38
|
"publishConfig": {
|
|
28
39
|
"access": "public"
|
|
29
40
|
},
|
package/planning.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
3
6
|
const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
|
|
4
7
|
|
|
5
8
|
async function callJson(fn, options) {
|
|
6
|
-
|
|
9
|
+
const input = Buffer.from(JSON.stringify(options || {}));
|
|
10
|
+
return JSON.parse(await fn(input));
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
function createJsonApis(descriptors) {
|
|
@@ -15,12 +19,110 @@ function createJsonApis(descriptors) {
|
|
|
15
19
|
);
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
function camelizeKey(key) {
|
|
23
|
+
return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decamelizeKey(key) {
|
|
27
|
+
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapKeys(value, mapKey) {
|
|
31
|
+
if (Array.isArray(value)) return value.map((item) => mapKeys(item, mapKey));
|
|
32
|
+
if (value && typeof value === "object") {
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.entries(value).map(([key, nested]) => [mapKey(key), mapKeys(nested, mapKey)]),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function camelizeValue(value) {
|
|
41
|
+
return mapKeys(value, camelizeKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function decamelizeValue(value) {
|
|
45
|
+
return mapKeys(value, decamelizeKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadPlanJson(planJson) {
|
|
49
|
+
let parsed = planJson;
|
|
50
|
+
if (typeof parsed === "string") {
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(parsed);
|
|
53
|
+
} catch {
|
|
54
|
+
return planJson;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (parsed && typeof parsed === "object") {
|
|
58
|
+
return decamelizeValue(parsed);
|
|
59
|
+
}
|
|
60
|
+
return planJson;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readPlanFile(planPath) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(await fs.readFile(planPath, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function decamelizePlanOptions(options = {}) {
|
|
72
|
+
const next = { ...options };
|
|
73
|
+
if (next.planJson != null) {
|
|
74
|
+
next.planJson = loadPlanJson(next.planJson);
|
|
75
|
+
} else if (typeof next.plan === "string") {
|
|
76
|
+
const document = await readPlanFile(next.plan);
|
|
77
|
+
if (document !== undefined) {
|
|
78
|
+
next.planJson = loadPlanJson(document);
|
|
79
|
+
delete next.plan;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function prepareWhyPlan(options = {}) {
|
|
86
|
+
const next = { ...options };
|
|
87
|
+
let document = next.planJson;
|
|
88
|
+
if (document == null && typeof next.plan === "string") {
|
|
89
|
+
document = await readPlanFile(next.plan);
|
|
90
|
+
if (document === undefined) return { request: next };
|
|
91
|
+
}
|
|
92
|
+
if (document == null) return { request: next };
|
|
93
|
+
const generatedDir = await fs.mkdtemp(path.join(os.tmpdir(), "no-mistakes-why-"));
|
|
94
|
+
await fs.writeFile(path.join(generatedDir, "plan.json"), JSON.stringify(loadPlanJson(document)));
|
|
95
|
+
next.plan = path.join(generatedDir, "plan.json");
|
|
96
|
+
delete next.planJson;
|
|
97
|
+
return { request: next, generatedDir };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function materializeWhyPlan(options = {}) {
|
|
101
|
+
return (await prepareWhyPlan(options)).request;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function removeGeneratedDir(generatedDir) {
|
|
105
|
+
if (!generatedDir) return;
|
|
106
|
+
await fs.rm(generatedDir, { recursive: true, force: true }).catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function camelizeWhy(value) {
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
111
|
+
return camelizeValue(value);
|
|
112
|
+
}
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(value).map(([key, nested]) => [key, camelizeValue(nested)]),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
18
118
|
async function testsComment(options) {
|
|
19
|
-
|
|
119
|
+
const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
|
|
120
|
+
return String(await native.testsCommentMarkdown(input));
|
|
20
121
|
}
|
|
21
122
|
|
|
22
123
|
async function testsGraphMermaid(options) {
|
|
23
|
-
|
|
124
|
+
const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
|
|
125
|
+
return String(await native.testsGraphMermaid(input));
|
|
24
126
|
}
|
|
25
127
|
|
|
26
128
|
const jsonApis = createJsonApis({
|
|
@@ -41,8 +143,44 @@ const jsonApis = createJsonApis({
|
|
|
41
143
|
testsWhy: "testsWhyJson",
|
|
42
144
|
});
|
|
43
145
|
|
|
146
|
+
async function testsPlan(options) {
|
|
147
|
+
return camelizeValue(await jsonApis.testsPlan(options));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function testsImpact(options) {
|
|
151
|
+
return camelizeValue(await jsonApis.testsImpact(options));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function testsTargets(options) {
|
|
155
|
+
return camelizeValue(await jsonApis.testsTargets(options));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function testsWhy(options) {
|
|
159
|
+
const { request, generatedDir } = await prepareWhyPlan(options);
|
|
160
|
+
try {
|
|
161
|
+
return camelizeWhy(await jsonApis.testsWhy(request));
|
|
162
|
+
} finally {
|
|
163
|
+
await removeGeneratedDir(generatedDir);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function testsGraph(options) {
|
|
168
|
+
return camelizeValue(await jsonApis.testsGraph(await decamelizePlanOptions(options)));
|
|
169
|
+
}
|
|
170
|
+
|
|
44
171
|
module.exports = {
|
|
172
|
+
camelizeValue,
|
|
173
|
+
camelizeWhy,
|
|
174
|
+
decamelizePlanOptions,
|
|
175
|
+
materializeWhyPlan,
|
|
176
|
+
prepareWhyPlan,
|
|
177
|
+
removeGeneratedDir,
|
|
45
178
|
testsComment,
|
|
46
179
|
testsGraphMermaid,
|
|
47
180
|
...jsonApis,
|
|
181
|
+
testsGraph,
|
|
182
|
+
testsImpact,
|
|
183
|
+
testsPlan,
|
|
184
|
+
testsTargets,
|
|
185
|
+
testsWhy,
|
|
48
186
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TestPlanFramework } from "./test-types";
|
|
2
|
+
|
|
3
|
+
export interface ResolvedConfig {
|
|
4
|
+
configPath?: string | null;
|
|
5
|
+
frontendApps: ResolvedFrontendApp[];
|
|
6
|
+
playwright: ResolvedPlaywright;
|
|
7
|
+
vitestFullSuiteTriggers: ResolvedTrigger[];
|
|
8
|
+
fullSuiteTriggers: ResolvedFrameworkTriggers[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResolvedFrontendApp {
|
|
12
|
+
project?: string | null;
|
|
13
|
+
root: string;
|
|
14
|
+
routeRoot: string;
|
|
15
|
+
selectorRoots: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResolvedPlaywright {
|
|
19
|
+
coverageRoutes: boolean;
|
|
20
|
+
coverageSelectors: boolean;
|
|
21
|
+
frontendRoot?: string | null;
|
|
22
|
+
selectorRoots: string[];
|
|
23
|
+
apps: ResolvedPlaywrightApp[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResolvedPlaywrightApp {
|
|
27
|
+
playwrightProject: string;
|
|
28
|
+
project?: string | null;
|
|
29
|
+
frontendRoot?: string | null;
|
|
30
|
+
selectorRoots: string[];
|
|
31
|
+
rewrites: ResolvedRewrite[];
|
|
32
|
+
ignoreRoutes: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResolvedRewrite {
|
|
36
|
+
source: string;
|
|
37
|
+
destination: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedTrigger {
|
|
41
|
+
name: string;
|
|
42
|
+
paths: string[];
|
|
43
|
+
targets: string[];
|
|
44
|
+
source: "triggers" | "projects";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ResolvedFrameworkTriggers {
|
|
48
|
+
framework: TestPlanFramework;
|
|
49
|
+
triggers: ResolvedTrigger[];
|
|
50
|
+
}
|
package/test-types.d.ts
CHANGED
|
@@ -10,7 +10,12 @@ export type TestPlanFramework =
|
|
|
10
10
|
| "go"
|
|
11
11
|
| "cargo"
|
|
12
12
|
| "rails"
|
|
13
|
-
| "php"
|
|
13
|
+
| "php"
|
|
14
|
+
| "java"
|
|
15
|
+
| "kotlin"
|
|
16
|
+
| "elixir"
|
|
17
|
+
| "dart"
|
|
18
|
+
| "jest";
|
|
14
19
|
|
|
15
20
|
interface TestsPlanOptionsBase {
|
|
16
21
|
framework?: TestPlanFramework;
|
|
@@ -36,6 +41,10 @@ interface TestsPlanOptionsBase {
|
|
|
36
41
|
limitPercent?: number;
|
|
37
42
|
limitFiles?: number;
|
|
38
43
|
globalConfigFallback?: boolean;
|
|
44
|
+
/** Include the markdown PR comment as `comment` on the returned plan. */
|
|
45
|
+
includeComment?: boolean;
|
|
46
|
+
/** Keep only selected tests whose relative path matches one of these globs. */
|
|
47
|
+
includeGlob?: string[];
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/**
|
|
@@ -85,16 +94,29 @@ export interface TestsTargetsOptions {
|
|
|
85
94
|
|
|
86
95
|
export interface TestPlan {
|
|
87
96
|
/** Complete deterministic changed-file inventory, relative to the request root. */
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
changedFiles: string[];
|
|
98
|
+
selectedTests: SelectedTest[];
|
|
90
99
|
groups?: TestPlanGroup[];
|
|
91
100
|
warnings: TestPlanWarning[];
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
fallbackTriggered: boolean;
|
|
102
|
+
fallbackReason?: string | null;
|
|
103
|
+
executionTargets?: GroupedExecutionTarget[];
|
|
104
|
+
comment?: string | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface GroupedExecutionTarget {
|
|
108
|
+
runner: TestPlanFramework;
|
|
109
|
+
config?: string | null;
|
|
110
|
+
project?: string | null;
|
|
111
|
+
/** Path-prefix display name, such as a Swift package root. */
|
|
112
|
+
name?: string;
|
|
113
|
+
baseCommand: string[];
|
|
114
|
+
runnerArgs: string[];
|
|
115
|
+
testFiles: string[];
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
export interface SelectedTest {
|
|
97
|
-
|
|
119
|
+
testFile: string;
|
|
98
120
|
confidence: "low" | "medium" | "high";
|
|
99
121
|
reasons: ImpactReason[];
|
|
100
122
|
targets?: TestExecutionTarget[];
|
|
@@ -106,24 +128,26 @@ export interface TestExecutionTarget {
|
|
|
106
128
|
/** True when config is a Vitest workspace/project-array source rendered with --workspace. */
|
|
107
129
|
workspace?: boolean;
|
|
108
130
|
project?: string | null;
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
/** Path-prefix display name, such as a Swift package root. */
|
|
132
|
+
name?: string;
|
|
133
|
+
baseCommand: string[];
|
|
134
|
+
runnerArgs: string[];
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
export interface ImpactReason {
|
|
114
|
-
|
|
138
|
+
changedFile: string;
|
|
115
139
|
path: string[];
|
|
116
140
|
via: string[];
|
|
117
141
|
/** When present, aligns index-for-index with `via`. */
|
|
118
|
-
|
|
142
|
+
viaDetails?: Array<ImpactEdgeDetail | null>;
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
export type ImpactEdgeDetail = ResourceImpactEdgeDetail | VitestSetupImpactEdgeDetail;
|
|
122
146
|
|
|
123
147
|
export interface ResourceImpactEdgeDetail {
|
|
124
148
|
type: "resource";
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
consumerFile: string;
|
|
150
|
+
callSites: ResourceCallSite[];
|
|
127
151
|
}
|
|
128
152
|
|
|
129
153
|
export interface VitestSetupImpactEdgeDetail {
|
|
@@ -132,7 +156,7 @@ export interface VitestSetupImpactEdgeDetail {
|
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
export interface ResourceCallSite {
|
|
135
|
-
|
|
159
|
+
callKind: ResourceCallKind;
|
|
136
160
|
line: number;
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -186,6 +210,7 @@ export interface TestsWhyOptions {
|
|
|
186
210
|
test: string;
|
|
187
211
|
changed?: string;
|
|
188
212
|
plan?: string;
|
|
213
|
+
planJson?: SavedTestPlan | string;
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
export interface WhyStep {
|
|
@@ -194,10 +219,8 @@ export interface WhyStep {
|
|
|
194
219
|
detail?: ImpactEdgeDetail | null;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
/** A current or pre-`
|
|
198
|
-
export type SavedTestPlan =
|
|
199
|
-
changed_files?: string[];
|
|
200
|
-
};
|
|
222
|
+
/** A current or pre-`changedFiles` plan accepted by saved-plan document APIs. */
|
|
223
|
+
export type SavedTestPlan = TestPlan;
|
|
201
224
|
|
|
202
225
|
export interface TestsPlanDocumentOptions {
|
|
203
226
|
plan?: string;
|
package/traversal-types.d.ts
CHANGED
|
@@ -31,7 +31,12 @@ export type Relationship =
|
|
|
31
31
|
| "rust"
|
|
32
32
|
| "ruby"
|
|
33
33
|
| "php"
|
|
34
|
+
| "java"
|
|
35
|
+
| "kotlin"
|
|
36
|
+
| "elixir"
|
|
37
|
+
| "dart"
|
|
34
38
|
| "resource"
|
|
39
|
+
| "trpc"
|
|
35
40
|
| "all";
|
|
36
41
|
|
|
37
42
|
export interface TraverseOptions {
|
|
@@ -59,6 +64,10 @@ export interface DependencyFile {
|
|
|
59
64
|
job?: string;
|
|
60
65
|
/** Zero-based step index for a virtual GitHub Actions workflow step node. */
|
|
61
66
|
step?: number;
|
|
67
|
+
/** Router file for a virtual tRPC procedure node. */
|
|
68
|
+
routerFile?: string;
|
|
69
|
+
/** Dotted procedure path for a virtual tRPC procedure node (`user.get`). */
|
|
70
|
+
procedure?: string;
|
|
62
71
|
module?: string;
|
|
63
72
|
depth: number;
|
|
64
73
|
via?: string[];
|
package/types.d.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function stepMatches(selector, step) {
|
|
4
|
+
return (
|
|
5
|
+
(selector.id === undefined || selector.id === step.id) &&
|
|
6
|
+
(selector.uses === undefined || selector.uses === step.uses) &&
|
|
7
|
+
(selector.name === undefined || selector.name === step.name)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function directCallerJobIdsForUses(jobsById, uses) {
|
|
12
|
+
const ids = [];
|
|
13
|
+
for (const job of jobsById.values()) {
|
|
14
|
+
if (job.steps.some((step) => step.uses === uses)) ids.push(job.id);
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze(ids.sort());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stepOrderIndexes(job, selectors) {
|
|
20
|
+
return Object.freeze(
|
|
21
|
+
selectors.map((selector) => {
|
|
22
|
+
const step = job.steps.find((candidate) => stepMatches(selector, candidate));
|
|
23
|
+
return step == null ? -1 : step.index;
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { directCallerJobIdsForUses, stepOrderIndexes };
|
|
@@ -24,6 +24,13 @@ export interface WorkflowTopologyIndex {
|
|
|
24
24
|
directDownstreamJobIds(jobId: string): readonly string[];
|
|
25
25
|
transitiveDownstreamJobIds(jobId: string): readonly string[];
|
|
26
26
|
directCallerJobIds(workflowPath: string): readonly string[];
|
|
27
|
+
/** Job ids whose steps `uses:` this action or workflow path. */
|
|
28
|
+
directCallerJobIdsForUses(uses: string): readonly string[];
|
|
29
|
+
/** Step indexes for `selectors` in `jobId`, or `-1` when a selector is missing. */
|
|
30
|
+
stepOrderIndexes(
|
|
31
|
+
jobId: string,
|
|
32
|
+
selectors: ReadonlyArray<{ id?: string; uses?: string; name?: string }>,
|
|
33
|
+
): readonly number[];
|
|
27
34
|
directCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
28
35
|
transitiveCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
29
36
|
directCalleeWorkflowPaths(workflowPath: string): readonly string[];
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
directCallerJobIdsForUses,
|
|
5
|
+
stepOrderIndexes,
|
|
6
|
+
} = require("./workflow-topology-index-helpers");
|
|
7
|
+
|
|
3
8
|
// A pure-JS query index rebuilt from the `ciTopology()` JSON, ported from
|
|
4
9
|
// the original engine's `topology-index.mts` + `frozen-topology.mts`. This
|
|
5
10
|
// stays JS-only by design: it returns closures over frozen `Map`s, which
|
|
@@ -214,6 +219,11 @@ function createWorkflowTopologyIndex(topology) {
|
|
|
214
219
|
transitiveWorkflowRunSubscriberPaths: workflowQuery(workflowRunSubscribers, true),
|
|
215
220
|
artifactProducersForConsumerJob: artifactEdgeQuery(artifactProducers),
|
|
216
221
|
artifactConsumersForProducerJob: artifactEdgeQuery(artifactConsumers),
|
|
222
|
+
directCallerJobIdsForUses: (uses) => directCallerJobIdsForUses(jobsById, uses),
|
|
223
|
+
stepOrderIndexes: (jobId, selectors) => {
|
|
224
|
+
assertKnown(jobsById, jobId, "workflow job");
|
|
225
|
+
return stepOrderIndexes(jobsById.get(jobId), selectors);
|
|
226
|
+
},
|
|
217
227
|
});
|
|
218
228
|
}
|
|
219
229
|
|