ox-dep-lint 0.2.0 → 0.3.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/CHANGELOG.md +10 -0
- package/dist/{check-BIxs2Gr0.mjs → check-F4cLmfYv.mjs} +81 -26
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{config-ClNl5rro.d.mts → config-DO_LRXM8.d.mts} +1 -0
- package/dist/{engine-C1qSxYaL.mjs → engine-Dc5tRuHT.mjs} +391 -144
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/plugin.mjs +8 -10
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7af0fef: An internal refactor with no change in behaviour for configurations written as plain data (objects, arrays and strings, without getters, Proxies or custom iterators): every finding and every message is the same as in 0.2.0, in the same order, and so is every configuration error. Output differs only where 0.2.0 crashed: a hole in a sparse `fileClasses` or `baseline` list (`[ , ]` in an `.mjs` file) is now reported as `fileClasses must be a list of objects` or `baseline must be a list of objects`, also when a Symbol elsewhere would have crashed first; and a chain of thousands of packages, which exhausted 0.2.0's stack, is now checked. A Symbol written into a message (a baseline entry's file, a kind in `mayUse`) still crashes (exit 2) when no such hole is present, though the TypeError's wording can differ when another value that cannot become a string (an object without a usable `toString`) is also present, because the checks run in another order. A configuration built from getters, Proxies or custom iterators can now be read differently, because validation reads values (including those of unknown keys) that 0.2.0 never read.
|
|
8
|
+
|
|
9
|
+
The configuration is validated by valibot schemas whose issues map to 0.2's messages, cycles between packages come from Tarjan's strongly connected components (iterative, so a long chain of packages cannot exhaust the stack) with the same routes as before, and the walk through development files reads its queue from a head index instead of `shift()`.
|
|
10
|
+
|
|
11
|
+
New runtime dependency: `valibot` 1.5.0 (MIT). The public types are unchanged and written out as before; the emitted declarations do not import valibot.
|
|
12
|
+
|
|
3
13
|
## 0.2.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findingsForFile, c as withoutBaselined, n as createEngine, r as edgeRefusal, s as reads, t as baselineCovers, u as directoriesMatching } from "./engine-
|
|
1
|
+
import { a as findingsForFile, c as withoutBaselined, n as createEngine, r as edgeRefusal, s as reads, t as baselineCovers, u as directoriesMatching } from "./engine-Dc5tRuHT.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
// src/graph.ts
|
|
@@ -87,37 +87,92 @@ const graphFindings = (engine) => {
|
|
|
87
87
|
findings.push(...packageCycles(engine));
|
|
88
88
|
return findings;
|
|
89
89
|
};
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Tarjan's strongly connected components, iterative so that a long chain of packages cannot exhaust the stack. The
|
|
92
|
+
* search starts from `roots` in order and follows each node's `successors` in order, so components and routes are
|
|
93
|
+
* deterministic.
|
|
94
|
+
*/
|
|
95
|
+
const stronglyConnectedComponents = (roots, successors) => {
|
|
96
|
+
const index = /* @__PURE__ */ new Map();
|
|
97
|
+
const lowLink = /* @__PURE__ */ new Map();
|
|
98
|
+
const stack = [];
|
|
99
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
100
|
+
const frames = [];
|
|
101
|
+
const pathPosition = /* @__PURE__ */ new Map();
|
|
102
|
+
const components = [];
|
|
103
|
+
const routes = [];
|
|
104
|
+
const enter = (name) => {
|
|
105
|
+
const order = index.size;
|
|
106
|
+
index.set(name, order);
|
|
107
|
+
lowLink.set(name, order);
|
|
108
|
+
stack.push(name);
|
|
109
|
+
onStack.add(name);
|
|
110
|
+
pathPosition.set(name, frames.length);
|
|
111
|
+
frames.push({
|
|
112
|
+
name,
|
|
113
|
+
next: 0
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
for (const root of roots) {
|
|
117
|
+
if (index.has(root)) continue;
|
|
118
|
+
enter(root);
|
|
119
|
+
for (let frame = frames.at(-1); frame !== void 0; frame = frames.at(-1)) {
|
|
120
|
+
const target = (successors.get(frame.name) ?? [])[frame.next];
|
|
121
|
+
if (target !== void 0) {
|
|
122
|
+
frame.next += 1;
|
|
123
|
+
if (!index.has(target)) enter(target);
|
|
124
|
+
else if (onStack.has(target)) {
|
|
125
|
+
lowLink.set(frame.name, Math.min(lowLink.get(frame.name) ?? 0, index.get(target) ?? 0));
|
|
126
|
+
const position = pathPosition.get(target);
|
|
127
|
+
if (position !== void 0) routes.push([...frames.slice(position).map((onPath) => onPath.name), target]);
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
frames.pop();
|
|
132
|
+
pathPosition.delete(frame.name);
|
|
133
|
+
const link = lowLink.get(frame.name) ?? 0;
|
|
134
|
+
if (link === index.get(frame.name)) {
|
|
135
|
+
const component = [];
|
|
136
|
+
for (let member = stack.pop(); member !== void 0; member = member === frame.name ? void 0 : stack.pop()) {
|
|
137
|
+
onStack.delete(member);
|
|
138
|
+
component.push(member);
|
|
139
|
+
}
|
|
140
|
+
components.push(component);
|
|
141
|
+
}
|
|
142
|
+
const parent = frames.at(-1);
|
|
143
|
+
if (parent !== void 0) lowLink.set(parent.name, Math.min(lowLink.get(parent.name) ?? 0, link));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
components,
|
|
148
|
+
routes
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Cycles between workspace packages through their runtime dependency fields. Every route the search closes is a
|
|
153
|
+
* finding, once per set of packages, starting at the package the search reached first; so a component with several
|
|
154
|
+
* cycles can have several findings.
|
|
155
|
+
*/
|
|
91
156
|
const packageCycles = (engine) => {
|
|
92
157
|
const { workspace } = engine;
|
|
93
158
|
const edges = new Map(workspace.packages.map((entry) => [entry.name, [...new Set(runtimeFields.flatMap((field) => entry.manifest[field]))].filter((name) => workspace.byName.has(name) && name !== entry.name)]));
|
|
94
|
-
const state = /* @__PURE__ */ new Map();
|
|
95
159
|
const findings = [];
|
|
96
160
|
const seen = /* @__PURE__ */ new Set();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
message: `dependency cycle: ${cycle.join(" -> ")}`,
|
|
111
|
-
target: key
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
return;
|
|
161
|
+
for (const cycle of stronglyConnectedComponents([...edges.keys()].toSorted(), edges).routes) {
|
|
162
|
+
const key = cycle.slice(0, -1).toSorted().join(" ");
|
|
163
|
+
if (!seen.has(key)) {
|
|
164
|
+
seen.add(key);
|
|
165
|
+
const first = cycle[0] ?? "";
|
|
166
|
+
findings.push({
|
|
167
|
+
rule: "package-cycle",
|
|
168
|
+
file: `${workspace.byName.get(first)?.directory ?? first}/package.json`,
|
|
169
|
+
line: 1,
|
|
170
|
+
column: 0,
|
|
171
|
+
message: `dependency cycle: ${cycle.join(" -> ")}`,
|
|
172
|
+
target: key
|
|
173
|
+
});
|
|
115
174
|
}
|
|
116
|
-
|
|
117
|
-
for (const next of edges.get(name) ?? []) visit(next, [...path, name]);
|
|
118
|
-
state.set(name, "done");
|
|
119
|
-
};
|
|
120
|
-
for (const name of [...edges.keys()].toSorted()) visit(name, []);
|
|
175
|
+
}
|
|
121
176
|
return findings;
|
|
122
177
|
};
|
|
123
178
|
/** Baseline entries that no finding in `findings` needs; each is a finding of its own, so the baseline only shrinks. */
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Config } from "./config-
|
|
1
|
+
import { n as Config } from "./config-DO_LRXM8.mjs";
|
|
2
2
|
export declare const loadConfig: (path: string) => Promise<Config>;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { d as findWorkspaceRoot } from "./engine-
|
|
3
|
-
import { n as formatFinding, t as checkWorkspace } from "./check-
|
|
2
|
+
import { d as findWorkspaceRoot } from "./engine-Dc5tRuHT.mjs";
|
|
3
|
+
import { n as formatFinding, t as checkWorkspace } from "./check-F4cLmfYv.mjs";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import nodePath from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
@@ -113,6 +113,7 @@ interface ResolvedConfig {
|
|
|
113
113
|
declare const defaultFileClasses: readonly FileClass[];
|
|
114
114
|
/** Identity, for type checking a configuration file. */
|
|
115
115
|
declare const defineConfig: (config: Config) => Config;
|
|
116
|
+
/** Problems that make a configuration unusable, as messages: the outline's alone when it does not hold, else every other. */
|
|
116
117
|
declare const configProblems: (config: Config) => string[];
|
|
117
118
|
declare const resolveConfig: (config: Config) => ResolvedConfig;
|
|
118
119
|
export { ImportPolicy as a, PackageRule as c, configProblems as d, defaultFileClasses as f, FilePolicy as i, ResolvedConfig as l, resolveConfig as m, Config as n, KindRule as o, defineConfig as p, FileClass as r, ManifestField as s, BaselineEntry as t, RuleName as u };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { builtinModules } from "node:module";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
|
+
import * as v from "valibot";
|
|
4
5
|
import { ResolverFactory } from "oxc-resolver";
|
|
5
6
|
import { Visitor, parseSync } from "oxc-parser";
|
|
6
7
|
// src/config.ts
|
|
@@ -50,137 +51,367 @@ const allManifestFields = [
|
|
|
50
51
|
];
|
|
51
52
|
/** Identity, for type checking a configuration file. */
|
|
52
53
|
const defineConfig = (config) => config;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
kind: [
|
|
68
|
-
"packages",
|
|
69
|
-
"mayUse",
|
|
70
|
-
"externals",
|
|
71
|
-
"runtimeModules"
|
|
72
|
-
],
|
|
73
|
-
package: [
|
|
74
|
-
"uses",
|
|
75
|
-
"externals",
|
|
76
|
-
"typeOnlyUses",
|
|
77
|
-
"valueSpecifiers",
|
|
78
|
-
"testSupportFiles"
|
|
79
|
-
],
|
|
80
|
-
fileClass: ["files", "policy"],
|
|
81
|
-
runtimeModules: ["prefixes", "names"],
|
|
82
|
-
imports: [
|
|
83
|
-
"computed",
|
|
84
|
-
"selfByName",
|
|
85
|
-
"unresolved",
|
|
86
|
-
"manifestEdgeFields",
|
|
87
|
-
"requireDeclared",
|
|
88
|
-
"shippedFields",
|
|
89
|
-
"typeOnly"
|
|
90
|
-
],
|
|
91
|
-
baseline: [
|
|
92
|
-
"file",
|
|
93
|
-
"rule",
|
|
94
|
-
"target",
|
|
95
|
-
"reason"
|
|
96
|
-
]
|
|
97
|
-
};
|
|
98
|
-
const choices = {
|
|
99
|
-
computed: [
|
|
54
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
const plainObject = v.custom(isPlainObject);
|
|
56
|
+
/** `schema` itself, which fails to type check unless it accepts exactly `Output`. */
|
|
57
|
+
const schemaOf = () => (schema) => schema;
|
|
58
|
+
const filePolicySchema = schemaOf()(v.picklist([
|
|
59
|
+
"shipped",
|
|
60
|
+
"development",
|
|
61
|
+
"unchecked"
|
|
62
|
+
]));
|
|
63
|
+
const manifestFieldSchema = schemaOf()(v.picklist(allManifestFields));
|
|
64
|
+
const ruleNameSchema = schemaOf()(v.picklist(ruleNames));
|
|
65
|
+
/** The values each import policy may take. */
|
|
66
|
+
const policies = {
|
|
67
|
+
computed: schemaOf()(v.picklist([
|
|
100
68
|
"error-in-shipped",
|
|
101
69
|
"error",
|
|
102
70
|
"allow"
|
|
103
|
-
],
|
|
104
|
-
selfByName: ["error", "allow"],
|
|
105
|
-
unresolved: ["error", "ignore"],
|
|
106
|
-
requireDeclared:
|
|
107
|
-
typeOnly: ["relaxed", "as-values"]
|
|
71
|
+
])),
|
|
72
|
+
selfByName: schemaOf()(v.picklist(["error", "allow"])),
|
|
73
|
+
unresolved: schemaOf()(v.picklist(["error", "ignore"])),
|
|
74
|
+
requireDeclared: schemaOf()(v.boolean()),
|
|
75
|
+
typeOnly: schemaOf()(v.picklist(["relaxed", "as-values"]))
|
|
108
76
|
};
|
|
77
|
+
/** 0.2's test for a list of strings. `every` skips the holes of a sparse array, where `v.array` reads `undefined`. */
|
|
109
78
|
const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
110
|
-
const
|
|
111
|
-
/**
|
|
112
|
-
const
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
const stringList = v.custom(isStringList);
|
|
80
|
+
/** A list of manifest fields, holes skipped as in 0.2. */
|
|
81
|
+
const manifestFieldList = v.custom((value) => isStringList(value) && value.every((field) => v.is(manifestFieldSchema, field)));
|
|
82
|
+
/**
|
|
83
|
+
* A value in a message, written as 0.2 wrote it: a template literal, which throws for a Symbol. 0.2 threw that way
|
|
84
|
+
* (exit 2 with the TypeError's message), so the check still does.
|
|
85
|
+
*/
|
|
86
|
+
const asText = (value) => `${value}`;
|
|
87
|
+
/**
|
|
88
|
+
* Every own entry of an object, each value checked by `value`. Not `v.record`, which skips the keys `constructor`,
|
|
89
|
+
* `prototype` and `__proto__` that 0.2 checked. In an issue's path, the entry is its index and then 1.
|
|
90
|
+
*/
|
|
91
|
+
const ownEntries = (value) => v.pipe(plainObject, v.transform((input) => Object.entries(input)), v.array(v.tuple([v.string(), value])));
|
|
92
|
+
/** 0.2's outline test for a list of objects, which skips holes as `every` does. */
|
|
93
|
+
const objectList = v.custom((value) => Array.isArray(value) && value.every(isPlainObject));
|
|
94
|
+
/** The outline (objects where objects belong, lists where lists belong), checked first: nothing else is read until it holds. */
|
|
95
|
+
const outlineSchema = v.pipe(plainObject, v.looseObject({
|
|
96
|
+
kinds: ownEntries(plainObject),
|
|
97
|
+
packages: v.optional(ownEntries(plainObject)),
|
|
98
|
+
imports: v.optional(plainObject),
|
|
99
|
+
runtimeModules: v.optional(plainObject),
|
|
100
|
+
fileClasses: v.optional(objectList),
|
|
101
|
+
baseline: v.optional(objectList)
|
|
102
|
+
}));
|
|
103
|
+
const kindRuleSchema = v.strictObject({
|
|
104
|
+
packages: v.pipe(v.array(v.unknown()), v.minLength(1)),
|
|
105
|
+
mayUse: v.union([v.literal("any"), stringList]),
|
|
106
|
+
externals: v.optional(v.union([v.literal("any"), stringList])),
|
|
107
|
+
runtimeModules: v.optional(v.boolean())
|
|
108
|
+
});
|
|
109
|
+
const packageRuleSchema = v.strictObject({
|
|
110
|
+
uses: v.exactOptional(stringList),
|
|
111
|
+
externals: v.exactOptional(stringList),
|
|
112
|
+
typeOnlyUses: v.exactOptional(stringList),
|
|
113
|
+
valueSpecifiers: v.exactOptional(stringList),
|
|
114
|
+
testSupportFiles: v.exactOptional(stringList)
|
|
115
|
+
});
|
|
116
|
+
const fileClassSchema = v.strictObject({
|
|
117
|
+
files: stringList,
|
|
118
|
+
policy: filePolicySchema
|
|
119
|
+
});
|
|
120
|
+
const runtimeModulesSchema = v.strictObject({
|
|
121
|
+
prefixes: v.exactOptional(stringList),
|
|
122
|
+
names: v.exactOptional(stringList)
|
|
123
|
+
});
|
|
124
|
+
const importPolicySchema = v.strictObject({
|
|
125
|
+
computed: v.optional(policies.computed),
|
|
126
|
+
selfByName: v.optional(policies.selfByName),
|
|
127
|
+
unresolved: v.optional(policies.unresolved),
|
|
128
|
+
manifestEdgeFields: v.optional(manifestFieldList),
|
|
129
|
+
requireDeclared: v.optional(policies.requireDeclared),
|
|
130
|
+
shippedFields: v.optional(manifestFieldList),
|
|
131
|
+
typeOnly: v.optional(policies.typeOnly)
|
|
132
|
+
});
|
|
133
|
+
const baselineEntrySchema = v.strictObject({
|
|
134
|
+
file: v.optional(v.unknown()),
|
|
135
|
+
rule: ruleNameSchema,
|
|
136
|
+
target: v.pipe(v.string(), v.nonEmpty()),
|
|
137
|
+
reason: v.pipe(v.string(), v.check((reason) => reason.trim() !== ""))
|
|
138
|
+
});
|
|
139
|
+
const configSchema = v.strictObject({
|
|
140
|
+
workspaceScopes: stringList,
|
|
141
|
+
kinds: ownEntries(kindRuleSchema),
|
|
142
|
+
packages: v.optional(ownEntries(packageRuleSchema)),
|
|
143
|
+
fileClasses: v.optional(v.array(fileClassSchema)),
|
|
144
|
+
runtimeModules: v.optional(runtimeModulesSchema),
|
|
145
|
+
virtualModules: v.optional(stringList),
|
|
146
|
+
testOnlySpecifiers: v.optional(stringList),
|
|
147
|
+
imports: v.optional(importPolicySchema),
|
|
148
|
+
baseline: v.optional(v.array(baselineEntrySchema)),
|
|
149
|
+
ignore: v.optional(stringList),
|
|
150
|
+
output: v.optional(stringList)
|
|
151
|
+
});
|
|
152
|
+
/** The sections of the messages, in 0.2's order. */
|
|
153
|
+
const section = {
|
|
154
|
+
configurationKeys: 0,
|
|
155
|
+
kinds: 1,
|
|
156
|
+
packages: 2,
|
|
157
|
+
workspaceScopes: 3,
|
|
158
|
+
testOnlySpecifiers: 4,
|
|
159
|
+
virtualModules: 5,
|
|
160
|
+
ignore: 6,
|
|
161
|
+
output: 7,
|
|
162
|
+
runtimeModuleValues: 8,
|
|
163
|
+
importFields: 9,
|
|
164
|
+
fileClasses: 10,
|
|
165
|
+
runtimeModuleKeys: 11,
|
|
166
|
+
importKeys: 12,
|
|
167
|
+
importPolicies: 13,
|
|
168
|
+
baseline: 14
|
|
169
|
+
};
|
|
170
|
+
/** Within a kind: its unknown keys, then these fields, then the kinds it names that do not exist. */
|
|
171
|
+
const kindFieldOrder = [
|
|
172
|
+
"packages",
|
|
173
|
+
"mayUse",
|
|
174
|
+
"runtimeModules",
|
|
175
|
+
"externals"
|
|
176
|
+
];
|
|
177
|
+
const importFieldOrder = ["manifestEdgeFields", "shippedFields"];
|
|
178
|
+
const importPolicyOrder = [
|
|
179
|
+
"computed",
|
|
180
|
+
"selfByName",
|
|
181
|
+
"unresolved",
|
|
182
|
+
"requireDeclared",
|
|
183
|
+
"typeOnly"
|
|
184
|
+
];
|
|
185
|
+
const baselineFieldOrder = [
|
|
186
|
+
"file",
|
|
187
|
+
"rule",
|
|
188
|
+
"target",
|
|
189
|
+
"reason"
|
|
190
|
+
];
|
|
191
|
+
/** The values a policy may take, as its message lists them. */
|
|
192
|
+
const policyValues = (field) => {
|
|
193
|
+
const schema = policies[field];
|
|
194
|
+
return "options" in schema ? schema.options : [true, false];
|
|
195
|
+
};
|
|
196
|
+
/** One message per key of `object` that `entries` does not know, in the object's order. */
|
|
197
|
+
const unknownKeyProblems = (where, object, entries, order) => Object.keys(object).flatMap((key, position) => Object.hasOwn(entries, key) ? [] : [{
|
|
198
|
+
order: [...order, position],
|
|
199
|
+
message: `${where} has the unknown key ${key} (known: ${Object.keys(entries).join(", ")})`
|
|
200
|
+
}]);
|
|
201
|
+
/** For an object whose every value is a list of strings: the unknown keys whose value is not one, in the object's order. */
|
|
202
|
+
const unknownListProblems = (object, entries, order, message) => Object.entries(object).flatMap(([key, value], position) => Object.hasOwn(entries, key) || v.is(stringList, value) ? [] : [{
|
|
203
|
+
order: [...order, position],
|
|
204
|
+
message: message(key)
|
|
205
|
+
}]);
|
|
206
|
+
/** The value an issue's path passes through at `index`, as an object (an entry of `ownEntries` is a pair). */
|
|
207
|
+
const objectAt = (path, index) => {
|
|
208
|
+
const value = path[index]?.value;
|
|
209
|
+
return isPlainObject(value) ? value : {};
|
|
210
|
+
};
|
|
211
|
+
/** The key and value of the `ownEntries` entry at `index` of an issue's path. */
|
|
212
|
+
const entryAt = (path, index) => {
|
|
213
|
+
const pair = path[index]?.value;
|
|
214
|
+
const [name, value] = Array.isArray(pair) ? pair : [];
|
|
215
|
+
return {
|
|
216
|
+
name: String(name),
|
|
217
|
+
value: isPlainObject(value) ? value : {}
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
const outlineMessage = (path) => {
|
|
221
|
+
const field = path[0]?.key;
|
|
222
|
+
if (field === void 0) return "the configuration must be an object";
|
|
223
|
+
if (field === "kinds") return path.length === 1 ? "kinds must map each kind to its rule" : `kind ${entryAt(path, 1).name} must be an object`;
|
|
224
|
+
if (field === "packages") return path.length === 1 ? "packages must map package names to their rules" : `package ${entryAt(path, 1).name} must be an object`;
|
|
225
|
+
if (field === "imports" || field === "runtimeModules") return `${field} must be an object`;
|
|
226
|
+
return `${field === "fileClasses" ? "fileClasses" : "baseline"} must be a list of objects`;
|
|
227
|
+
};
|
|
228
|
+
const kindProblems = (path, unknownKey, holder) => {
|
|
229
|
+
const index = Number(path[1]?.key);
|
|
230
|
+
const { name } = entryAt(path, 1);
|
|
231
|
+
if (unknownKey) return unknownKeyProblems(`kind ${name}`, holder, kindRuleSchema.entries, [
|
|
232
|
+
section.kinds,
|
|
233
|
+
index,
|
|
234
|
+
0
|
|
235
|
+
]);
|
|
236
|
+
const field = String(path[3]?.key);
|
|
237
|
+
const message = {
|
|
238
|
+
packages: `kind ${name} names no packages`,
|
|
239
|
+
mayUse: `kind ${name}: mayUse must be "any" or a list of kinds`,
|
|
240
|
+
runtimeModules: `kind ${name}: runtimeModules must be true or false`,
|
|
241
|
+
externals: `kind ${name}: externals must be "any" or a list of package names`
|
|
242
|
+
}[field];
|
|
243
|
+
return message === void 0 ? [] : [{
|
|
244
|
+
order: [
|
|
245
|
+
section.kinds,
|
|
246
|
+
index,
|
|
247
|
+
kindFieldOrder.indexOf(field) + 1
|
|
248
|
+
],
|
|
249
|
+
message
|
|
250
|
+
}];
|
|
251
|
+
};
|
|
252
|
+
/** The kinds a kind may use that are not kinds: a reference across entries, which no schema of one entry sees. */
|
|
253
|
+
const kindReferenceProblems = (kinds) => {
|
|
254
|
+
const known = new Set(Object.keys(kinds));
|
|
117
255
|
const problems = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
256
|
+
for (const [index, [name, kind]] of Object.entries(kinds).entries()) {
|
|
257
|
+
const mayUse = isPlainObject(kind) ? kind.mayUse : void 0;
|
|
258
|
+
if (!Array.isArray(mayUse)) continue;
|
|
259
|
+
for (const [position, used] of [...mayUse].entries()) if (typeof used !== "string" || !known.has(used)) problems.push({
|
|
260
|
+
order: [
|
|
261
|
+
section.kinds,
|
|
262
|
+
index,
|
|
263
|
+
kindFieldOrder.length + 1,
|
|
264
|
+
position
|
|
265
|
+
],
|
|
266
|
+
message: `kind ${name} may use ${asText(used)}, which is not a kind`
|
|
267
|
+
});
|
|
268
|
+
}
|
|
124
269
|
return problems;
|
|
125
270
|
};
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
271
|
+
const packageProblems = (path, unknownKey, holder) => {
|
|
272
|
+
const index = Number(path[1]?.key);
|
|
273
|
+
const { name, value } = entryAt(path, 1);
|
|
274
|
+
const listMessage = (key) => `package ${name}: ${key} must be a list of strings`;
|
|
275
|
+
if (unknownKey) return [...unknownKeyProblems(`package ${name}`, holder, packageRuleSchema.entries, [
|
|
276
|
+
section.packages,
|
|
277
|
+
index,
|
|
278
|
+
0
|
|
279
|
+
]), ...unknownListProblems(holder, packageRuleSchema.entries, [
|
|
280
|
+
section.packages,
|
|
281
|
+
index,
|
|
282
|
+
1
|
|
283
|
+
], listMessage)];
|
|
284
|
+
const field = String(path[3]?.key);
|
|
285
|
+
return [{
|
|
286
|
+
order: [
|
|
287
|
+
section.packages,
|
|
288
|
+
index,
|
|
289
|
+
1,
|
|
290
|
+
Object.keys(value).indexOf(field)
|
|
291
|
+
],
|
|
292
|
+
message: listMessage(field)
|
|
293
|
+
}];
|
|
294
|
+
};
|
|
295
|
+
const runtimeModulesProblems = (path, unknownKey, holder) => {
|
|
296
|
+
const listMessage = (key) => `runtimeModules.${key} must be a list of strings`;
|
|
297
|
+
if (unknownKey) return [...unknownKeyProblems("runtimeModules", holder, runtimeModulesSchema.entries, [section.runtimeModuleKeys]), ...unknownListProblems(holder, runtimeModulesSchema.entries, [section.runtimeModuleValues], listMessage)];
|
|
298
|
+
const field = String(path[1]?.key);
|
|
299
|
+
return [{
|
|
300
|
+
order: [section.runtimeModuleValues, Object.keys(objectAt(path, 0)).indexOf(field)],
|
|
301
|
+
message: listMessage(field)
|
|
302
|
+
}];
|
|
303
|
+
};
|
|
304
|
+
const importProblems = (path, unknownKey, holder) => {
|
|
305
|
+
if (unknownKey) return unknownKeyProblems("imports", holder, importPolicySchema.entries, [section.importKeys]);
|
|
306
|
+
const field = String(path[1]?.key);
|
|
307
|
+
if (importFieldOrder.includes(field)) return [{
|
|
308
|
+
order: [section.importFields, importFieldOrder.indexOf(field)],
|
|
309
|
+
message: `imports.${field} must be a list of ${allManifestFields.join(", ")}`
|
|
310
|
+
}];
|
|
311
|
+
return [{
|
|
312
|
+
order: [section.importPolicies, importPolicyOrder.indexOf(field)],
|
|
313
|
+
message: `imports.${field} is ${JSON.stringify(path[1]?.value)}; it must be one of ${policyValues(field).join(", ")}`
|
|
314
|
+
}];
|
|
315
|
+
};
|
|
316
|
+
const fileClassProblems = (path, unknownKey, holder) => {
|
|
317
|
+
const index = Number(path[1]?.key);
|
|
318
|
+
if (unknownKey) return unknownKeyProblems(`file class ${index}`, holder, fileClassSchema.entries, [
|
|
319
|
+
section.fileClasses,
|
|
320
|
+
index,
|
|
321
|
+
0
|
|
322
|
+
]);
|
|
323
|
+
return path[2]?.key === "files" ? [{
|
|
324
|
+
order: [
|
|
325
|
+
section.fileClasses,
|
|
326
|
+
index,
|
|
327
|
+
1
|
|
328
|
+
],
|
|
329
|
+
message: `file class ${index}: files must be a list of globs`
|
|
330
|
+
}] : [{
|
|
331
|
+
order: [
|
|
332
|
+
section.fileClasses,
|
|
333
|
+
index,
|
|
334
|
+
2
|
|
335
|
+
],
|
|
336
|
+
message: `file class ${index} has the unknown policy ${String(objectAt(path, 1).policy)}`
|
|
337
|
+
}];
|
|
338
|
+
};
|
|
339
|
+
const baselineProblems = (path, unknownKey, holder) => {
|
|
340
|
+
const index = Number(path[1]?.key);
|
|
341
|
+
const entry = objectAt(path, 1);
|
|
342
|
+
const file = asText(entry.file);
|
|
343
|
+
if (unknownKey) return unknownKeyProblems(`the baseline entry for ${file}`, holder, baselineEntrySchema.entries, [
|
|
344
|
+
section.baseline,
|
|
345
|
+
index,
|
|
346
|
+
0
|
|
347
|
+
]);
|
|
348
|
+
const field = String(path[2]?.key);
|
|
349
|
+
const message = {
|
|
350
|
+
rule: () => `the baseline entry for ${file} names the unknown rule ${String(entry.rule)}`,
|
|
351
|
+
target: () => `the baseline entry for ${asText(entry.rule)} in ${file} names no target, so it would hide every future finding of that rule there`,
|
|
352
|
+
reason: () => `the baseline entry for ${asText(entry.rule)} in ${file} has no reason`
|
|
353
|
+
}[field]?.();
|
|
354
|
+
return message === void 0 ? [] : [{
|
|
355
|
+
order: [
|
|
356
|
+
section.baseline,
|
|
357
|
+
index,
|
|
358
|
+
baselineFieldOrder.indexOf(field)
|
|
359
|
+
],
|
|
360
|
+
message
|
|
361
|
+
}];
|
|
362
|
+
};
|
|
363
|
+
/** The messages for one issue of `configSchema`. */
|
|
364
|
+
const problemsOf = (issue) => {
|
|
365
|
+
const path = issue.path ?? [];
|
|
366
|
+
const unknownKey = issue.type === "strict_object" && issue.expected === "never";
|
|
367
|
+
const at = unknownKey ? path.slice(0, -1) : path;
|
|
368
|
+
const holder = unknownKey ? path.at(-1)?.input : void 0;
|
|
369
|
+
const object = isPlainObject(holder) ? holder : {};
|
|
370
|
+
const field = at[0]?.key;
|
|
371
|
+
switch (field) {
|
|
372
|
+
case void 0: return unknownKey ? unknownKeyProblems("the configuration", object, configSchema.entries, [section.configurationKeys]) : [];
|
|
373
|
+
case "kinds": return kindProblems(path, unknownKey, object);
|
|
374
|
+
case "packages": return packageProblems(path, unknownKey, object);
|
|
375
|
+
case "runtimeModules": return runtimeModulesProblems(path, unknownKey, object);
|
|
376
|
+
case "imports": return importProblems(path, unknownKey, object);
|
|
377
|
+
case "fileClasses": return fileClassProblems(path, unknownKey, object);
|
|
378
|
+
case "baseline": return baselineProblems(path, unknownKey, object);
|
|
379
|
+
case "workspaceScopes": return [{
|
|
380
|
+
order: [section.workspaceScopes],
|
|
381
|
+
message: "workspaceScopes must be a list of scopes"
|
|
382
|
+
}];
|
|
383
|
+
case "testOnlySpecifiers":
|
|
384
|
+
case "virtualModules":
|
|
385
|
+
case "ignore":
|
|
386
|
+
case "output": return [{
|
|
387
|
+
order: [section[field]],
|
|
388
|
+
message: `${field} must be a list of strings`
|
|
389
|
+
}];
|
|
390
|
+
default: return [];
|
|
176
391
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
392
|
+
};
|
|
393
|
+
const compareOrders = (left, right) => {
|
|
394
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
395
|
+
const difference = (left[index] ?? -1) - (right[index] ?? -1);
|
|
396
|
+
if (difference !== 0) return difference;
|
|
182
397
|
}
|
|
183
|
-
return
|
|
398
|
+
return 0;
|
|
399
|
+
};
|
|
400
|
+
/** Problems that make a configuration unusable, as messages: the outline's alone when it does not hold, else every other. */
|
|
401
|
+
const configProblems = (config) => {
|
|
402
|
+
const outline = v.safeParse(outlineSchema, config);
|
|
403
|
+
if (!outline.success) return [...new Set(outline.issues.map((issue) => outlineMessage(issue.path ?? [])))];
|
|
404
|
+
const sparse = ["fileClasses", "baseline"].filter((key) => [...config[key] ?? []].includes(void 0));
|
|
405
|
+
if (sparse.length > 0) return sparse.map((key) => `${key} must be a list of objects`);
|
|
406
|
+
for (const entry of config.baseline ?? []) asText(entry.file);
|
|
407
|
+
const problems = [...(v.safeParse(configSchema, config).issues ?? []).flatMap(problemsOf), ...kindReferenceProblems(config.kinds)].toSorted((left, right) => compareOrders(left.order, right.order));
|
|
408
|
+
const seen = /* @__PURE__ */ new Set();
|
|
409
|
+
return problems.flatMap((problem) => {
|
|
410
|
+
const key = problem.order.join(" ");
|
|
411
|
+
if (seen.has(key)) return [];
|
|
412
|
+
seen.add(key);
|
|
413
|
+
return [problem.message];
|
|
414
|
+
});
|
|
184
415
|
};
|
|
185
416
|
const resolveConfig = (config) => {
|
|
186
417
|
const problems = configProblems(config);
|
|
@@ -270,20 +501,36 @@ const globExpression = (pattern) => {
|
|
|
270
501
|
const matchesGlob = (path, pattern) => globExpression(pattern).test(path);
|
|
271
502
|
/** Whether `path` matches any of `patterns`. */
|
|
272
503
|
const matchesAny = (path, patterns) => patterns.some((pattern) => matchesGlob(path, pattern));
|
|
504
|
+
// src/result.ts
|
|
505
|
+
const success = (value) => ({
|
|
506
|
+
ok: true,
|
|
507
|
+
value
|
|
508
|
+
});
|
|
509
|
+
const failure = (error) => ({
|
|
510
|
+
ok: false,
|
|
511
|
+
error
|
|
512
|
+
});
|
|
513
|
+
/** Runs `operation`, with what it throws as the failure. */
|
|
514
|
+
const attempt = (operation) => {
|
|
515
|
+
try {
|
|
516
|
+
return success(operation());
|
|
517
|
+
} catch (cause) {
|
|
518
|
+
return failure(cause);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
/** The message of something thrown, which need not be an Error. */
|
|
522
|
+
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
273
523
|
// src/workspace.ts
|
|
274
524
|
/**
|
|
275
525
|
* The workspace as `package.json` files declare it: the root's `workspaces` globs, and each package's real name (never
|
|
276
526
|
* guessed from its directory) and dependency fields.
|
|
277
527
|
*/
|
|
528
|
+
/** The manifest at `path`, or why it cannot be read. */
|
|
278
529
|
const readManifest = (path) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
throw new Error(`${path}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
284
|
-
}
|
|
285
|
-
if (typeof parsed !== "object" || parsed === null) throw new Error(`${path} is not a JSON object`);
|
|
286
|
-
return parsed;
|
|
530
|
+
const parsed = attempt(() => JSON.parse(readFileSync(path, "utf8")));
|
|
531
|
+
if (!parsed.ok) return failure(`${path}: ${messageOf(parsed.error)}`);
|
|
532
|
+
if (typeof parsed.value !== "object" || parsed.value === null) return failure(`${path} is not a JSON object`);
|
|
533
|
+
return success(parsed.value);
|
|
287
534
|
};
|
|
288
535
|
const namesOf = (field) => typeof field === "object" && field !== null && !Array.isArray(field) ? Object.keys(field).toSorted() : [];
|
|
289
536
|
const fieldsOf = (manifest) => ({
|
|
@@ -311,12 +558,9 @@ const workspaceGlobs = (manifest) => {
|
|
|
311
558
|
if (Array.isArray(field)) return field.filter((entry) => typeof entry === "string");
|
|
312
559
|
if (typeof field === "object" && field !== null && "packages" in field && Array.isArray(field.packages)) return field.packages.filter((entry) => typeof entry === "string");
|
|
313
560
|
};
|
|
314
|
-
const workspacesOf = (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} catch {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
561
|
+
const workspacesOf = (path) => {
|
|
562
|
+
const manifest = readManifest(path);
|
|
563
|
+
return manifest.ok ? workspaceGlobs(manifest.value) ?? [] : [];
|
|
320
564
|
};
|
|
321
565
|
/** Whether `globs` (from the manifest in `candidate`) hold `directory`: one of them matches it or a directory above it. */
|
|
322
566
|
const holds = (candidate, globs, directory) => {
|
|
@@ -366,7 +610,9 @@ const directoriesMatching = (root, pattern) => {
|
|
|
366
610
|
return found;
|
|
367
611
|
};
|
|
368
612
|
const loadWorkspace = (root) => {
|
|
369
|
-
const
|
|
613
|
+
const rootRead = readManifest(nodePath.join(root, "package.json"));
|
|
614
|
+
if (!rootRead.ok) throw new Error(rootRead.error);
|
|
615
|
+
const rootManifest = rootRead.value;
|
|
370
616
|
const globs = workspaceGlobs(rootManifest) ?? [];
|
|
371
617
|
const directories = /* @__PURE__ */ new Set();
|
|
372
618
|
for (const pattern of globs.filter((glob) => !glob.startsWith("!"))) for (const directory of directoriesMatching(root, pattern)) if (existsSync(nodePath.join(root, directory, "package.json"))) directories.add(directory);
|
|
@@ -375,16 +621,15 @@ const loadWorkspace = (root) => {
|
|
|
375
621
|
const problems = [];
|
|
376
622
|
const byName = /* @__PURE__ */ new Map();
|
|
377
623
|
for (const directory of [...directories].toSorted()) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
manifest = readManifest(nodePath.join(root, directory, "package.json"));
|
|
381
|
-
} catch (error) {
|
|
624
|
+
const read = readManifest(nodePath.join(root, directory, "package.json"));
|
|
625
|
+
if (!read.ok) {
|
|
382
626
|
problems.push({
|
|
383
627
|
directory,
|
|
384
|
-
message: `package.json cannot be read: ${
|
|
628
|
+
message: `package.json cannot be read: ${read.error}`
|
|
385
629
|
});
|
|
386
630
|
continue;
|
|
387
631
|
}
|
|
632
|
+
const manifest = read.value;
|
|
388
633
|
if (typeof manifest.name !== "string" || manifest.name === "") {
|
|
389
634
|
problems.push({
|
|
390
635
|
directory,
|
|
@@ -903,9 +1148,11 @@ const developmentFileBreaks = (engine, file) => {
|
|
|
903
1148
|
if (known !== void 0 && [...known.read].every(([path, time]) => modifiedTime(nodePath.join(engine.workspace.root, path)) === time)) return known.breaks;
|
|
904
1149
|
const read = /* @__PURE__ */ new Map();
|
|
905
1150
|
const queue = [file];
|
|
1151
|
+
let head = 0;
|
|
906
1152
|
let breaks;
|
|
907
|
-
while (queue.length
|
|
908
|
-
const next = queue
|
|
1153
|
+
while (head < queue.length && breaks === void 0) {
|
|
1154
|
+
const next = queue[head] ?? file;
|
|
1155
|
+
head += 1;
|
|
909
1156
|
const absolute = nodePath.join(engine.workspace.root, next);
|
|
910
1157
|
const time = modifiedTime(absolute);
|
|
911
1158
|
if (read.has(next) || time === void 0) continue;
|
|
@@ -1064,4 +1311,4 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
1064
1311
|
const baselineCovers = (entry, finding) => entry.file === finding.file && entry.rule === finding.rule && entry.target === finding.target;
|
|
1065
1312
|
/** `findings` less those the baseline covers. */
|
|
1066
1313
|
const withoutBaselined = (engine, findings) => findings.filter((finding) => !engine.config.baseline.some((entry) => baselineCovers(entry, finding)));
|
|
1067
|
-
export {
|
|
1314
|
+
export { resolveConfig as S, matchesAny as _, findingsForFile as a, defaultFileClasses as b, withoutBaselined as c, findWorkspaceRoot as d, loadWorkspace as f, failure as g, attempt as h, effectivePolicy as i, importForms as l, packageOfFile as m, createEngine as n, policyOf as o, packageNameOf as p, edgeRefusal as r, reads as s, baselineCovers as t, directoriesMatching as u, matchesGlob as v, defineConfig as x, configProblems as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ImportPolicy, c as PackageRule, d as configProblems, f as defaultFileClasses, i as FilePolicy, l as ResolvedConfig, m as resolveConfig, n as Config, o as KindRule, p as defineConfig, r as FileClass, s as ManifestField, t as BaselineEntry, u as RuleName } from "./config-
|
|
1
|
+
import { a as ImportPolicy, c as PackageRule, d as configProblems, f as defaultFileClasses, i as FilePolicy, l as ResolvedConfig, m as resolveConfig, n as Config, o as KindRule, p as defineConfig, r as FileClass, s as ManifestField, t as BaselineEntry, u as RuleName } from "./config-DO_LRXM8.mjs";
|
|
2
2
|
interface WorkspacePackage {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
/** Repository-relative directory, `/`-separated (`libraries/parcel`). */
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-
|
|
1
|
+
import { S as resolveConfig, _ as matchesAny, a as findingsForFile, b as defaultFileClasses, c as withoutBaselined, d as findWorkspaceRoot, f as loadWorkspace, i as effectivePolicy, l as importForms, m as packageOfFile, n as createEngine, o as policyOf, p as packageNameOf, s as reads, v as matchesGlob, x as defineConfig, y as configProblems } from "./engine-Dc5tRuHT.mjs";
|
|
2
|
+
import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-F4cLmfYv.mjs";
|
|
3
3
|
export { checkWorkspace, configProblems, createEngine, defaultFileClasses, defineConfig, effectivePolicy, findWorkspaceRoot, findingsForFile, formatFinding, graphFindings, importForms, listSourceFiles, loadWorkspace, matchesAny, matchesGlob, packageNameOf, packageOfFile, policyOf, reads, resolveConfig, staleBaseline, withoutBaselined };
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findingsForFile, c as withoutBaselined, d as findWorkspaceRoot, n as createEngine } from "./engine-
|
|
1
|
+
import { a as findingsForFile, c as withoutBaselined, d as findWorkspaceRoot, g as failure, h as attempt, n as createEngine } from "./engine-Dc5tRuHT.mjs";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
// src/plugin.ts
|
|
@@ -23,7 +23,7 @@ const modified = (path) => {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
const manifestsOf = (root, engine) => {
|
|
26
|
-
const directories = engine
|
|
26
|
+
const directories = engine.ok ? engine.value.workspace.packages.map((entry) => nodePath.join(root, entry.directory)) : [];
|
|
27
27
|
const paths = [
|
|
28
28
|
nodePath.join(root, "package.json"),
|
|
29
29
|
...directories.map((directory) => nodePath.join(directory, "package.json")),
|
|
@@ -32,11 +32,8 @@ const manifestsOf = (root, engine) => {
|
|
|
32
32
|
return new Map(paths.map((path) => [path, modified(path)]));
|
|
33
33
|
};
|
|
34
34
|
const build = (root, config) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
39
|
-
}
|
|
35
|
+
const built = attempt(() => createEngine(root, config));
|
|
36
|
+
return built.ok ? built : failure(built.error instanceof Error ? built.error : new Error(String(built.error)));
|
|
40
37
|
};
|
|
41
38
|
/** The engine for `root` and `config`, rebuilt when a package.json it read has changed. */
|
|
42
39
|
const engineFor = (root, config) => {
|
|
@@ -90,14 +87,15 @@ const plugin = {
|
|
|
90
87
|
});
|
|
91
88
|
return;
|
|
92
89
|
}
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
90
|
+
const built = engineFor(root, config);
|
|
91
|
+
if (!built.ok) {
|
|
95
92
|
context.report({
|
|
96
|
-
message:
|
|
93
|
+
message: built.error.message,
|
|
97
94
|
loc: { start }
|
|
98
95
|
});
|
|
99
96
|
return;
|
|
100
97
|
}
|
|
98
|
+
const engine = built.value;
|
|
101
99
|
const file = nodePath.relative(root, context.filename).split(nodePath.sep).join("/");
|
|
102
100
|
for (const finding of withoutBaselined(engine, findingsForFile(engine, file, context.sourceCode.text))) {
|
|
103
101
|
const message = `${finding.rule}: ${finding.message}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ox-dep-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Module dependency graph rules for oxlint: package kinds, outside packages, runtime and test-only modules, undeclared, escaping and computed imports",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Philip B. Krogh",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"oxc-parser": "0.153.0",
|
|
41
|
-
"oxc-resolver": "11.24.2"
|
|
41
|
+
"oxc-resolver": "11.24.2",
|
|
42
|
+
"valibot": "1.5.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@changesets/cli": "3.0.3",
|