ox-dep-lint 0.1.1 → 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 CHANGED
@@ -1,5 +1,23 @@
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
+
13
+ ## 0.2.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 89ab407: New policy `imports.typeOnly`. With `"as-values"`, shipped code's type-only imports (every form, `/// <reference types>` included) are judged by `runtime-module`, `forbidden-external`, `undeclared-import` and `shipped-imports-development-file`. So `import type { Socket } from "bun"` fails in a kind with `runtimeModules: false`, and a vendor's types fail where the kind's `externals` do not allow it. A shipped `@types/x` still declares `x` for a type-only import. The default, `"relaxed"`, keeps 0.1's behaviour: types do not load, so those rules skip them.
18
+
19
+ A runtime's own types package (`node`, `@types/node`, `bun-types`, `@types/bun`, `@cloudflare/workers-types`, or a subpath of one) used type-only now counts as that runtime, in shipped and development files alike. So it is never reported as an outside package or as undeclared; until now an undeclared one gave `undeclared-import`. Under `"as-values"`, `runtime-module` judges it where the kind allows no runtime modules; under `"relaxed"` it passes, like other type-only imports.
20
+
3
21
  ## 0.1.1
4
22
 
5
23
  - 0.1.0 was published without its built `dist` files, so neither the CLI nor the plugin could load. A `prepack` script now builds before every pack and publish, so a package cannot go out without them. Use 0.1.1.
package/README.md CHANGED
@@ -79,6 +79,9 @@ A kind may use its own kind only when `mayUse` lists it. `imports` holds the pol
79
79
  | `manifestEdgeFields` | all four fields | any subset |
80
80
  | `requireDeclared` | `true` | `false` |
81
81
  | `shippedFields` | `dependencies`, `peerDependencies`, `optionalDependencies` | any subset |
82
+ | `typeOnly` | `relaxed` | `as-values` |
83
+
84
+ `typeOnly: "as-values"` judges shipped code's type-only imports (`import type`, `export type`, type-level `import()`, `declare module`, `import type X = require()`, `/// <reference types>`) by `runtime-module`, `forbidden-external`, `undeclared-import` and `shipped-imports-development-file`, so `import type { Socket } from "bun"` fails in a kind without runtime modules. Two differences from value imports remain. A shipped field that declares `@types/x` declares `x` for a type-only import, and such an `@types/x` is an outside package too, so `externals` lists both. A runtime's own types package (`node` or `@types/node`, `bun`, `bun-types` or `@types/bun`, `@cloudflare/workers-types`) counts as that runtime, so it is judged by `runtime-module` and never needs declaring. That includes a published package whose emitted declarations reference those types: declare `@types/node` or `bun-types` for its consumers yourself; this check does not ask for it. By default types do not load, so those rules skip type-only imports. Kind edges and `typeOnlyUses` always see the real form.
82
85
 
83
86
  Misspelled keys, unknown policies and baseline entries without a `target` are configuration errors, never silent defaults.
84
87
 
@@ -127,6 +130,12 @@ Where they can still differ: oxlint chooses which files it lints (its own ignore
127
130
  - `/// <reference path="..." />` and JSDoc `@import` types are not read.
128
131
  - A `require` passed around as a value (`const load = require; load("x")`) is not followed. Telling the global loader from a local of the same name needs scope analysis; until then the check reads `require` only where it is called.
129
132
 
133
+ ## Contributing and releases
134
+
135
+ - Commits and pull request titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, `chore:`); a `commit-msg` hook and CI check them with commitlint.
136
+ - Every pull request adds a changeset: `bunx changeset` and pick patch, minor or major, or `bunx changeset --empty` when nothing ships. Before 1.0, a breaking change is a minor bump.
137
+ - Versions follow [semantic versioning](https://semver.org/) and are never edited by hand. While changesets wait on main, the `release` workflow keeps a pull request `chore(release): vX.Y.Z` up to date with the bumped `package.json` and the new `CHANGELOG.md` entry. Merging it publishes that version to npm through trusted publishing (with provenance), tags it and makes a GitHub release. Empty changesets alone open no release; they are consumed with the next one.
138
+
130
139
  ## Licence
131
140
 
132
141
  MIT. The specifier reader and resolver are adapted from phibkro/vektorprogrammet-reimagined, and the rules from that repository and phibkro/file-transfer, all by the same author. The escape-sequence rule closes a bypass found in reebeat/reebeat's tier check. No code from reebeat was copied.
@@ -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-7N-4EtzS.mjs";
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
- /** Cycles between workspace packages through their runtime dependency fields, each reported once. */
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 visit = (name, path) => {
98
- if (state.get(name) === "done") return;
99
- if (state.get(name) === "visiting") {
100
- const cycle = [...path.slice(path.indexOf(name)), name];
101
- const key = cycle.slice(0, -1).toSorted().join(" ");
102
- if (!seen.has(key)) {
103
- seen.add(key);
104
- const first = workspace.byName.get(name);
105
- findings.push({
106
- rule: "package-cycle",
107
- file: `${first?.directory ?? name}/package.json`,
108
- line: 1,
109
- column: 0,
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
- state.set(name, "visiting");
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-CpuQhyZE.mjs";
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-7N-4EtzS.mjs";
3
- import { n as formatFinding, t as checkWorkspace } from "./check-GClvJhK3.mjs";
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";
@@ -63,6 +63,14 @@ interface ImportPolicy {
63
63
  readonly requireDeclared?: boolean;
64
64
  /** The fields that satisfy a shipped import. Default `dependencies`, `peerDependencies`, `optionalDependencies`. */
65
65
  readonly shippedFields?: readonly ManifestField[];
66
+ /**
67
+ * How shipped code's type-only imports (`import type`, `export type`, a type-level `import()`, `declare module`, a
68
+ * types reference) are judged by `runtime-module`, `forbidden-external`, `undeclared-import` and
69
+ * `shipped-imports-development-file`. `"relaxed"` (default): types do not load, so those rules skip them and
70
+ * devDependencies declare them. `"as-values"`: judged like value imports, for kinds whose public types must not name
71
+ * a runtime or vendor either. Kind edges and `typeOnlyUses` always see the real form.
72
+ */
73
+ readonly typeOnly?: "relaxed" | "as-values";
66
74
  }
67
75
  interface Config {
68
76
  /** Scopes whose specifiers must name a workspace package (`@file-transfer/`). */
@@ -105,6 +113,7 @@ interface ResolvedConfig {
105
113
  declare const defaultFileClasses: readonly FileClass[];
106
114
  /** Identity, for type checking a configuration file. */
107
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. */
108
117
  declare const configProblems: (config: Config) => string[];
109
118
  declare const resolveConfig: (config: Config) => ResolvedConfig;
110
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,135 +51,367 @@ const allManifestFields = [
50
51
  ];
51
52
  /** Identity, for type checking a configuration file. */
52
53
  const defineConfig = (config) => config;
53
- const allowedKeys = {
54
- config: [
55
- "workspaceScopes",
56
- "kinds",
57
- "packages",
58
- "fileClasses",
59
- "runtimeModules",
60
- "virtualModules",
61
- "testOnlySpecifiers",
62
- "imports",
63
- "baseline",
64
- "ignore",
65
- "output"
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
- ],
90
- baseline: [
91
- "file",
92
- "rule",
93
- "target",
94
- "reason"
95
- ]
96
- };
97
- const choices = {
98
- 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([
99
68
  "error-in-shipped",
100
69
  "error",
101
70
  "allow"
102
- ],
103
- selfByName: ["error", "allow"],
104
- unresolved: ["error", "ignore"],
105
- requireDeclared: [true, false]
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"]))
106
76
  };
77
+ /** 0.2's test for a list of strings. `every` skips the holes of a sparse array, where `v.array` reads `undefined`. */
107
78
  const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
108
- const unknownKeys = (where, value, allowed) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value).filter((key) => !allowed.includes(key)).map((key) => `${where} has the unknown key ${key} (known: ${allowed.join(", ")})`) : [`${where} is not an object`];
109
- /** Problems that make a configuration unusable, as messages. A misspelled key is a problem, never a silent default. */
110
- const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
111
- /** Problems with the configuration's outline (objects where objects belong, lists where lists belong), checked first. */
112
- const shapeProblems = (config) => {
113
- if (!isObject(config)) return ["the configuration must be an object"];
114
- const { kinds, packages, fileClasses, baseline } = config;
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));
115
255
  const problems = [];
116
- if (!isObject(kinds)) problems.push("kinds must map each kind to its rule");
117
- else for (const [name, kind] of Object.entries(kinds)) if (!isObject(kind)) problems.push(`kind ${name} must be an object`);
118
- if (packages !== void 0 && !isObject(packages)) problems.push("packages must map package names to their rules");
119
- else for (const [name, rule] of Object.entries(packages ?? {})) if (!isObject(rule)) problems.push(`package ${name} must be an object`);
120
- for (const [key, value] of [["imports", config.imports], ["runtimeModules", config.runtimeModules]]) if (value !== void 0 && !isObject(value)) problems.push(`${key} must be an object`);
121
- for (const [key, list] of [["fileClasses", fileClasses], ["baseline", baseline]]) if (list !== void 0 && (!Array.isArray(list) || !list.every(isObject))) problems.push(`${key} must be a list of objects`);
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
+ }
122
269
  return problems;
123
270
  };
124
- const configProblems = (config) => {
125
- const outline = shapeProblems(config);
126
- if (outline.length > 0) return outline;
127
- const problems = unknownKeys("the configuration", config, allowedKeys.config);
128
- const kinds = new Set(Object.keys(config.kinds));
129
- for (const [name, kind] of Object.entries(config.kinds)) {
130
- problems.push(...unknownKeys(`kind ${name}`, kind, allowedKeys.kind));
131
- if (!Array.isArray(kind.packages) || kind.packages.length === 0) problems.push(`kind ${name} names no packages`);
132
- if (kind.mayUse !== "any" && !isStringList(kind.mayUse)) problems.push(`kind ${name}: mayUse must be "any" or a list of kinds`);
133
- if (kind.runtimeModules !== void 0 && typeof kind.runtimeModules !== "boolean") problems.push(`kind ${name}: runtimeModules must be true or false`);
134
- if (kind.externals !== void 0 && kind.externals !== "any" && !isStringList(kind.externals)) problems.push(`kind ${name}: externals must be "any" or a list of package names`);
135
- if (kind.mayUse !== "any") {
136
- for (const used of Array.isArray(kind.mayUse) ? kind.mayUse : []) if (!kinds.has(used)) problems.push(`kind ${name} may use ${used}, which is not a kind`);
137
- }
138
- }
139
- for (const [name, refinements] of Object.entries(config.packages ?? {})) {
140
- problems.push(...unknownKeys(`package ${name}`, refinements, allowedKeys.package));
141
- for (const [key, value] of Object.entries(refinements)) if (!isStringList(value)) problems.push(`package ${name}: ${key} must be a list of strings`);
142
- }
143
- if (!isStringList(config.workspaceScopes)) problems.push("workspaceScopes must be a list of scopes");
144
- for (const key of [
145
- "testOnlySpecifiers",
146
- "virtualModules",
147
- "ignore",
148
- "output"
149
- ]) {
150
- const value = config[key];
151
- if (value !== void 0 && !isStringList(value)) problems.push(`${key} must be a list of strings`);
152
- }
153
- for (const [key, value] of Object.entries(config.runtimeModules ?? {})) if (!isStringList(value)) problems.push(`runtimeModules.${key} must be a list of strings`);
154
- for (const key of ["manifestEdgeFields", "shippedFields"]) {
155
- const value = config.imports?.[key];
156
- if (value !== void 0 && (!isStringList(value) || !value.every((field) => allManifestFields.includes(field)))) problems.push(`imports.${key} must be a list of ${allManifestFields.join(", ")}`);
157
- }
158
- for (const [index, fileClass] of (config.fileClasses ?? []).entries()) {
159
- problems.push(...unknownKeys(`file class ${index}`, fileClass, allowedKeys.fileClass));
160
- if (!isStringList(fileClass.files)) problems.push(`file class ${index}: files must be a list of globs`);
161
- if (![
162
- "shipped",
163
- "development",
164
- "unchecked"
165
- ].includes(fileClass.policy)) problems.push(`file class ${index} has the unknown policy ${String(fileClass.policy)}`);
166
- }
167
- if (config.runtimeModules !== void 0) problems.push(...unknownKeys("runtimeModules", config.runtimeModules, allowedKeys.runtimeModules));
168
- if (config.imports !== void 0) {
169
- problems.push(...unknownKeys("imports", config.imports, allowedKeys.imports));
170
- for (const [key, allowed] of Object.entries(choices)) {
171
- const value = config.imports[key];
172
- if (value !== void 0 && !allowed.includes(value)) problems.push(`imports.${key} is ${JSON.stringify(value)}; it must be one of ${allowed.join(", ")}`);
173
- }
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 [];
174
391
  }
175
- for (const entry of config.baseline ?? []) {
176
- problems.push(...unknownKeys(`the baseline entry for ${entry.file}`, entry, allowedKeys.baseline));
177
- if (!ruleNames.includes(entry.rule)) problems.push(`the baseline entry for ${entry.file} names the unknown rule ${String(entry.rule)}`);
178
- if (typeof entry.target !== "string" || entry.target === "") problems.push(`the baseline entry for ${entry.rule} in ${entry.file} names no target, so it would hide every future finding of that rule there`);
179
- if (typeof entry.reason !== "string" || entry.reason.trim() === "") problems.push(`the baseline entry for ${entry.rule} in ${entry.file} has no reason`);
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;
180
397
  }
181
- return problems;
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
+ });
182
415
  };
183
416
  const resolveConfig = (config) => {
184
417
  const problems = configProblems(config);
@@ -213,7 +446,8 @@ const resolveConfig = (config) => {
213
446
  "dependencies",
214
447
  "peerDependencies",
215
448
  "optionalDependencies"
216
- ]
449
+ ],
450
+ typeOnly: config.imports?.typeOnly ?? "relaxed"
217
451
  },
218
452
  baseline: config.baseline ?? [],
219
453
  ignore: config.ignore ?? [],
@@ -267,20 +501,36 @@ const globExpression = (pattern) => {
267
501
  const matchesGlob = (path, pattern) => globExpression(pattern).test(path);
268
502
  /** Whether `path` matches any of `patterns`. */
269
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);
270
523
  // src/workspace.ts
271
524
  /**
272
525
  * The workspace as `package.json` files declare it: the root's `workspaces` globs, and each package's real name (never
273
526
  * guessed from its directory) and dependency fields.
274
527
  */
528
+ /** The manifest at `path`, or why it cannot be read. */
275
529
  const readManifest = (path) => {
276
- let parsed;
277
- try {
278
- parsed = JSON.parse(readFileSync(path, "utf8"));
279
- } catch (cause) {
280
- throw new Error(`${path}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
281
- }
282
- if (typeof parsed !== "object" || parsed === null) throw new Error(`${path} is not a JSON object`);
283
- 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);
284
534
  };
285
535
  const namesOf = (field) => typeof field === "object" && field !== null && !Array.isArray(field) ? Object.keys(field).toSorted() : [];
286
536
  const fieldsOf = (manifest) => ({
@@ -308,12 +558,9 @@ const workspaceGlobs = (manifest) => {
308
558
  if (Array.isArray(field)) return field.filter((entry) => typeof entry === "string");
309
559
  if (typeof field === "object" && field !== null && "packages" in field && Array.isArray(field.packages)) return field.packages.filter((entry) => typeof entry === "string");
310
560
  };
311
- const workspacesOf = (manifest) => {
312
- try {
313
- return workspaceGlobs(readManifest(manifest)) ?? [];
314
- } catch {
315
- return [];
316
- }
561
+ const workspacesOf = (path) => {
562
+ const manifest = readManifest(path);
563
+ return manifest.ok ? workspaceGlobs(manifest.value) ?? [] : [];
317
564
  };
318
565
  /** Whether `globs` (from the manifest in `candidate`) hold `directory`: one of them matches it or a directory above it. */
319
566
  const holds = (candidate, globs, directory) => {
@@ -363,7 +610,9 @@ const directoriesMatching = (root, pattern) => {
363
610
  return found;
364
611
  };
365
612
  const loadWorkspace = (root) => {
366
- const rootManifest = readManifest(nodePath.join(root, "package.json"));
613
+ const rootRead = readManifest(nodePath.join(root, "package.json"));
614
+ if (!rootRead.ok) throw new Error(rootRead.error);
615
+ const rootManifest = rootRead.value;
367
616
  const globs = workspaceGlobs(rootManifest) ?? [];
368
617
  const directories = /* @__PURE__ */ new Set();
369
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);
@@ -372,16 +621,15 @@ const loadWorkspace = (root) => {
372
621
  const problems = [];
373
622
  const byName = /* @__PURE__ */ new Map();
374
623
  for (const directory of [...directories].toSorted()) {
375
- let manifest;
376
- try {
377
- manifest = readManifest(nodePath.join(root, directory, "package.json"));
378
- } catch (error) {
624
+ const read = readManifest(nodePath.join(root, directory, "package.json"));
625
+ if (!read.ok) {
379
626
  problems.push({
380
627
  directory,
381
- message: `package.json cannot be read: ${error instanceof Error ? error.message : String(error)}`
628
+ message: `package.json cannot be read: ${read.error}`
382
629
  });
383
630
  continue;
384
631
  }
632
+ const manifest = read.value;
385
633
  if (typeof manifest.name !== "string" || manifest.name === "") {
386
634
  problems.push({
387
635
  directory,
@@ -800,6 +1048,19 @@ const importForms = (path, source) => {
800
1048
  * it over every file and adds the whole-graph checks (`graph.ts`), so the two never disagree. Rules adapted from the
801
1049
  * module graph checks of phibkro/file-transfer, phibkro/vektorprogrammet-reimagined and reebeat/reebeat.
802
1050
  */
1051
+ const nodeModules = ["node:module", ...builtinModules];
1052
+ /**
1053
+ * Packages that hold a runtime's types, and the runtime modules they describe: the package counts as the runtime when
1054
+ * the configuration names any of them a runtime module (Node's by its `node:` prefix or any bare built-in).
1055
+ */
1056
+ const runtimeTypePackages = {
1057
+ node: nodeModules,
1058
+ "@types/node": nodeModules,
1059
+ bun: ["bun"],
1060
+ "bun-types": ["bun"],
1061
+ "@types/bun": ["bun"],
1062
+ "@cloudflare/workers-types": ["cloudflare:workers"]
1063
+ };
803
1064
  /** The source files the engine reads; anything else (`.vue`, `.json`) is no finding in either front end. */
804
1065
  const sourceFile = /\.[cm]?[jt]sx?$/u;
805
1066
  const createEngine = (root, config) => {
@@ -887,9 +1148,11 @@ const developmentFileBreaks = (engine, file) => {
887
1148
  if (known !== void 0 && [...known.read].every(([path, time]) => modifiedTime(nodePath.join(engine.workspace.root, path)) === time)) return known.breaks;
888
1149
  const read = /* @__PURE__ */ new Map();
889
1150
  const queue = [file];
1151
+ let head = 0;
890
1152
  let breaks;
891
- while (queue.length > 0 && breaks === void 0) {
892
- const next = queue.shift() ?? file;
1153
+ while (head < queue.length && breaks === void 0) {
1154
+ const next = queue[head] ?? file;
1155
+ head += 1;
893
1156
  const absolute = nodePath.join(engine.workspace.root, next);
894
1157
  const time = modifiedTime(absolute);
895
1158
  if (read.has(next) || time === void 0) continue;
@@ -963,10 +1226,22 @@ const findingsForFile = (engine, file, text, options = {}) => {
963
1226
  column: 0
964
1227
  }, `could not be parsed, so its imports cannot be checked: ${message}`, "parse");
965
1228
  for (const site of forms.computed) if (config.imports.computed === "error" || config.imports.computed === "error-in-shipped" && shipped) report("computed-import", site, `${site.text}: ${site.reason}; nothing can check what it loads`, site.text);
1229
+ /**
1230
+ * The packages that hold a runtime's own types, by the runtime module they describe: a type-only use of one
1231
+ * (`/// <reference types="node" />`, `import type {} from "bun-types"`) is a use of that runtime, judged by
1232
+ * `runtime-module` like `node:fs` and never as an outside package to declare.
1233
+ */
1234
+ const runtimeOf = (site) => {
1235
+ return (site.typeOnly ? runtimeTypePackages[packageNameOf(site.specifier)] : void 0)?.find((module) => isRuntimeModule(config, module));
1236
+ };
966
1237
  for (const site of forms.imports) {
967
- const { typeOnly } = site;
1238
+ const typeOnly = site.typeOnly && config.imports.typeOnly === "relaxed";
968
1239
  const specifier = withoutQuery(site.specifier);
969
- const target = engine.resolver.resolve(file, site.specifier);
1240
+ const runtime = runtimeOf(site);
1241
+ const target = runtime === void 0 ? engine.resolver.resolve(file, site.specifier) : {
1242
+ kind: "runtime",
1243
+ name: runtime
1244
+ };
970
1245
  const resolvedName = target.kind === "external" || target.kind === "workspace" ? target.name : void 0;
971
1246
  const subpathOf = (name) => specifier === name || specifier.startsWith(`${name}/`);
972
1247
  const names = resolvedName === void 0 || subpathOf(resolvedName) ? [specifier] : [specifier, resolvedName];
@@ -982,7 +1257,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
982
1257
  };
983
1258
  switch (target.kind) {
984
1259
  case "runtime":
985
- if (shipped && !typeOnly && kindRule?.runtimeModules === false) report("runtime-module", site, `imports the runtime module ${specifier}, which ${article(kind)} package may not use in shipped code`, specifier);
1260
+ if (shipped && !typeOnly && kindRule?.runtimeModules === false) report("runtime-module", site, `imports ${runtime === void 0 ? `the runtime module ${specifier},` : `the runtime types package ${specifier},`} which ${article(kind)} package may not use in shipped code`, specifier);
986
1261
  break;
987
1262
  case "virtual": break;
988
1263
  case "unknown-workspace":
@@ -1000,7 +1275,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
1000
1275
  if (config.imports.selfByName === "error") report("self-import-by-name", site, "imports its own package by its name; use a relative path", specifier);
1001
1276
  break;
1002
1277
  }
1003
- const refusal = edgeRefusal(engine, owner, to, typeOnly, specifier);
1278
+ const refusal = edgeRefusal(engine, owner, to, site.typeOnly, specifier);
1004
1279
  if (refusal !== void 0) report(refusal.rule, site, refusal.why, to.name);
1005
1280
  if (config.imports.requireDeclared && !declares(engine, owner, to.name, !shipped || typeOnly)) report("undeclared-import", site, `imports ${to.name}, which ${owner.directory}/package.json does not declare${shipped ? " for shipped code" : ""}`, to.name);
1006
1281
  break;
@@ -1024,7 +1299,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
1024
1299
  const permitted = (name) => allowed === "any" || allowed?.includes(name) === true || extra.includes(name);
1025
1300
  if (shipped && !typeOnly && !permitted(target.name)) report("forbidden-external", site, `${article(kind)} package may not import ${target.name} in shipped code`, target.name);
1026
1301
  const anywhere = !shipped || typeOnly;
1027
- const declared = declares(engine, owner, target.name, anywhere) || declares(engine, owner, target.written, anywhere) || typeOnly && declares(engine, owner, typesPackage(target.name), true);
1302
+ const declared = declares(engine, owner, target.name, anywhere) || declares(engine, owner, target.written, anywhere) || site.typeOnly && declares(engine, owner, typesPackage(target.name), anywhere);
1028
1303
  if (config.imports.requireDeclared && !declared) report("undeclared-import", site, `imports ${target.name}, which ${owner.directory}/package.json does not declare${shipped ? " for shipped code" : ""}`, target.name);
1029
1304
  break;
1030
1305
  }
@@ -1036,4 +1311,4 @@ const findingsForFile = (engine, file, text, options = {}) => {
1036
1311
  const baselineCovers = (entry, finding) => entry.file === finding.file && entry.rule === finding.rule && entry.target === finding.target;
1037
1312
  /** `findings` less those the baseline covers. */
1038
1313
  const withoutBaselined = (engine, findings) => findings.filter((finding) => !engine.config.baseline.some((entry) => baselineCovers(entry, finding)));
1039
- export { configProblems as _, findingsForFile as a, resolveConfig as b, withoutBaselined as c, findWorkspaceRoot as d, loadWorkspace as f, matchesGlob as g, matchesAny 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, defaultFileClasses as v, defineConfig as y };
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-CpuQhyZE.mjs";
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 configProblems, a as findingsForFile, b as resolveConfig, c as withoutBaselined, d as findWorkspaceRoot, f as loadWorkspace, g as matchesGlob, h as matchesAny, i as effectivePolicy, l as importForms, m as packageOfFile, n as createEngine, o as policyOf, p as packageNameOf, s as reads, v as defaultFileClasses, y as defineConfig } from "./engine-7N-4EtzS.mjs";
2
- import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-GClvJhK3.mjs";
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-7N-4EtzS.mjs";
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 instanceof Error ? [] : engine.workspace.packages.map((entry) => nodePath.join(root, entry.directory));
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
- try {
36
- return createEngine(root, config);
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 engine = engineFor(root, config);
94
- if (engine instanceof Error) {
90
+ const built = engineFor(root, config);
91
+ if (!built.ok) {
95
92
  context.report({
96
- message: engine.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.1.1",
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",
@@ -32,13 +32,19 @@
32
32
  "build": "vp pack",
33
33
  "check": "vp check --deny-warnings && vp test run",
34
34
  "test": "vp test run",
35
- "prepack": "vp pack"
35
+ "prepack": "vp pack",
36
+ "prepare": "vp config",
37
+ "changeset": "changeset"
36
38
  },
37
39
  "dependencies": {
38
40
  "oxc-parser": "0.153.0",
39
- "oxc-resolver": "11.24.2"
41
+ "oxc-resolver": "11.24.2",
42
+ "valibot": "1.5.0"
40
43
  },
41
44
  "devDependencies": {
45
+ "@changesets/cli": "3.0.3",
46
+ "@commitlint/cli": "21.2.3",
47
+ "@commitlint/config-conventional": "21.2.3",
42
48
  "@types/node": "24.9.1",
43
49
  "oxlint": "1.86.0",
44
50
  "typescript": "7.0.2",