ox-dep-lint 0.4.0 → 0.5.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 57698bb: `invalid-package` now reports a workspace member whose `dependencies`, `devDependencies`, `optionalDependencies` or `peerDependencies` is not an object: a string (`"dependencies": "some string"`), a list, a number, a boolean or `null`. 0.4 read no names from such a field and said nothing, so a forbidden or undeclared edge written there passed unseen. The finding names the field and what it holds, once per field; the member is still checked, as if that field declared nothing. The root `package.json` is not judged this way.
8
+
9
+ This is a minor release rather than a patch because it can fail a check that passed on 0.4 (the rule in `.changeset/README.md`: before 1.0, a change to what the rules report is a minor bump). Fix the field, or baseline the finding by its own target, the member's directory and the field joined by `#`:
10
+
11
+ ```ts
12
+ baseline: [
13
+ {
14
+ file: "tools/quality/package.json",
15
+ rule: "invalid-package",
16
+ target: "tools/quality#peerDependencies",
17
+ reason: "...",
18
+ },
19
+ ]
20
+ ```
21
+
22
+ Such an entry covers only that field. If the `package.json` later cannot be read or loses its name, which drops the member from the check, that is still reported, with the directory as its target.
23
+
24
+ ### Patch Changes
25
+
26
+ - 57698bb: A path that disappears while the check runs is skipped instead of reported as `unreadable-path`. A test that creates and removes a temporary tree inside a package while the CLI walks it (tsukuri's `tools/quality/.scan-x/...`) no longer fails the check with `ENOENT`. This covers every place the check lists, stats or reads: the walk of each package, the `workspaces` and kind globs, a member's `package.json`, a symbolic link's destination, and a development file that shipped code imports. `ENOTDIR` (a directory on the way replaced by a file) is skipped the same way. A path that exists but cannot be read (`EACCES` and every other error) is still a finding: `unreadable-path`, or `invalid-package` for a member's `package.json`, or `shipped-imports-development-file` for a development file that shipped code imports, or `symbolic-link` (to an unreadable destination) for a link, and the oxlint plugin, which shares these functions, behaves the same. The file count the CLI prints no longer includes files that disappeared.
27
+
3
28
  ## 0.4.0
4
29
 
5
30
  ### Minor Changes
package/README.md CHANGED
@@ -28,10 +28,10 @@ ox-dep-lint replaces the hand-written module graph checks of three repositories
28
28
  | `self-import-by-name` | A package importing itself by its own name (off by default). | both |
29
29
  | `package-without-kind` | A workspace package that no kind's globs hold. | both |
30
30
  | `package-outside-workspace` | A directory a kind names, with a `package.json`, that the root `workspaces` leave out, so nothing in it would be checked. | CLI |
31
- | `invalid-package` | A workspace member whose `package.json` cannot be read, has no name, or has another member's name. | CLI |
31
+ | `invalid-package` | A workspace member whose `package.json` cannot be read, has no name, or has another member's name; or one with a dependency field (`dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`) that is not an object (`"dependencies": "x"`, a list, a number, `null`), so no edge in it could be read. Such a member is still checked, as if that field declared nothing. A field's finding has the target `<directory>#<field>` (`tools/quality#peerDependencies`), so a baseline entry for it cannot cover the other causes, whose target is the directory. | CLI |
32
32
  | `directory-without-package` | A directory a root `workspaces` glob matches that has no `package.json` and lies neither inside nor above a member, so no package owns its files and nothing checks them (tsukuri's "directory in no tier"). Also a dot directory such a glob would match (`packages/.template`), with or without a `package.json`: npm and Bun workspaces never match dot directories, so it is no package. Only the topmost of nested ones is reported; a negated glob (`!tools/legacy` or `!tools/legacy/**`) leaves the directory out. Silence one with `ignore` (`tools/vendored/**`) or the baseline. | CLI |
33
33
  | `symbolic-link` | Every symbolic link under a package directory, whatever it points at (a file, a directory, nothing, a cycle) and whatever its name (`.d.ts`, `.md`, `.json`, extensionless); a link a `workspaces` or kind glob matches or names (`packages/x -> ../elsewhere`); and a link a glob passes through (`packages` itself a link). The check never follows one. It skips only links inside directories it skips on purpose: `node_modules`, `.git`, and directories `ignore`, `output` or an unchecked file class cover through a trailing `/**`. | both (the plugin on each file oxlint reaches through a link) |
34
- | `unreadable-path` | A directory the walk, a `workspaces` glob or a kind glob cannot list, or a source file the check cannot read (`EACCES`), unless the check skips it on purpose. It used to stop the check with exit code 2. | CLI |
34
+ | `unreadable-path` | A directory the walk, a `workspaces` glob or a kind glob cannot list, or a source file the check cannot read (`EACCES`), unless the check skips it on purpose. It used to stop the check with exit code 2. A path removed while the check runs (`ENOENT`, or `ENOTDIR` when a directory on the way became a file), such as a test's temporary tree, is skipped with no finding. | CLI |
35
35
  | `package-cycle` | A cycle between packages through their runtime dependency fields. | CLI |
36
36
  | `stale-baseline` | A baseline entry that no finding needs any more, so the baseline can only shrink. | CLI |
37
37
  | `parse-error` | A file that does not parse, so its imports cannot be checked. | both |
@@ -102,7 +102,7 @@ Releases are published from a `v*` tag by `.github/workflows/release.yml`, with
102
102
 
103
103
  ## Running it
104
104
 
105
- The CLI walks every workspace package's directory, dot directories included (`src/.internal/`), skipping only `node_modules` and `.git` and reading nothing under `output`, so generated and gitignored sources and nested repositories are checked too, and runs the whole-graph checks. Tool caches such as `.wrangler` and `.astro` are in the default `output`; for another cache, set `output` to the defaults plus its glob, since `output` replaces the defaults. The CLI never follows a symbolic link: every link is a `symbolic-link` finding, and a directory it cannot list or a file it cannot read is an `unreadable-path` finding. What it still does not report: plain files directly at a workspace glob's level (`packages/x.ts`), and FIFOs and sockets.
105
+ The CLI walks every workspace package's directory, dot directories included (`src/.internal/`), skipping only `node_modules` and `.git` and reading nothing under `output`, so generated and gitignored sources and nested repositories are checked too, and runs the whole-graph checks. Tool caches such as `.wrangler` and `.astro` are in the default `output`; for another cache, set `output` to the defaults plus its glob, since `output` replaces the defaults. The CLI never follows a symbolic link: every link is a `symbolic-link` finding, and a directory it cannot list or a file it cannot read is an `unreadable-path` finding; one that disappears between being listed and being read is skipped. What it still does not report: plain files directly at a workspace glob's level (`packages/x.ts`), and FIFOs and sockets.
106
106
 
107
107
  ```sh
108
108
  ox-dep-lint # finds ox-dep-lint.config.{ts,mts,js,mjs,json} at the workspace root
@@ -1,4 +1,4 @@
1
- import { C as attempt, D as matchesGlob, c as linkDestination, d as reads, f as reportsLink, g as withoutBaselined, h as unreadablePathFinding, i as edgeRefusal, l as passedOver, m as symbolicLinkFinding, n as coversDirectory, o as findingsForFile, p as skipsDirectory, r as createEngine, s as ignoresDirectory, t as baselineCovers, v as directoriesMatching, w as codeOf } from "./engine-DmECSDnu.mjs";
1
+ import { C as attempt, E as vanished, O as matchesGlob, c as linkDestination, d as reads, f as reportsLink, g as withoutBaselined, h as unreadablePathFinding, i as edgeRefusal, l as passedOver, m as symbolicLinkFinding, n as coversDirectory, o as findingsForFile, p as skipsDirectory, r as createEngine, s as ignoresDirectory, t as baselineCovers, v as directoriesMatching, w as codeOf } from "./engine-CwR59HVO.mjs";
2
2
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import nodePath from "node:path";
4
4
  // src/graph.ts
@@ -20,13 +20,16 @@ const graphFindings = (engine) => {
20
20
  line: 1,
21
21
  column: 0
22
22
  };
23
- for (const problem of workspace.problems) findings.push({
24
- rule: "invalid-package",
25
- file: `${problem.directory}/package.json`,
26
- ...at,
27
- message: problem.message,
28
- target: problem.directory
29
- });
23
+ for (const problem of workspace.problems) {
24
+ const target = problem.field === void 0 ? problem.directory : `${problem.directory}#${problem.field}`;
25
+ findings.push({
26
+ rule: "invalid-package",
27
+ file: `${problem.directory}/package.json`,
28
+ ...at,
29
+ message: problem.message,
30
+ target
31
+ });
32
+ }
30
33
  const members = /* @__PURE__ */ new Set([...workspace.packages.map((entry) => entry.directory), ...workspace.problems.map((problem) => problem.directory)]);
31
34
  const unlistedByKinds = {
32
35
  links: [],
@@ -109,15 +112,6 @@ const isFile = (absolute) => {
109
112
  return false;
110
113
  }
111
114
  };
112
- /** The error code of a directory that cannot be listed, or undefined when it can. */
113
- const unreadable = (absolute) => {
114
- try {
115
- readdirSync(absolute);
116
- return;
117
- } catch (cause) {
118
- return codeOf(cause);
119
- }
120
- };
121
115
  /**
122
116
  * What the root's `workspaces` globs (and a kind's globs) match that is no package: a directory without a
123
117
  * `package.json`, whose files no package owns and so nothing checks (only the topmost of nested ones); a dot directory,
@@ -140,15 +134,16 @@ const directoriesWithoutPackage = (engine, members, unlistedByKinds) => {
140
134
  for (const glob of workspace.globs.filter((pattern) => !pattern.startsWith("!"))) for (const directory of directoriesMatching(workspace.root, glob, unlisted)) if (!candidates.has(directory)) candidates.set(directory, glob);
141
135
  const withoutPackage = (directory, glob) => {
142
136
  if (above(directory) || !isDirectory(nodePath.join(workspace.root, directory))) return;
143
- const code = unreadable(nodePath.join(workspace.root, directory));
144
- return code === void 0 ? {
137
+ const listed = attempt(() => readdirSync(nodePath.join(workspace.root, directory)));
138
+ if (!listed.ok) return vanished(listed.error) ? void 0 : unreadablePathFinding(directory, codeOf(listed.error));
139
+ return {
145
140
  rule: "directory-without-package",
146
141
  file: directory,
147
142
  line: 1,
148
143
  column: 0,
149
144
  message: `${directory} matches the workspaces glob ${glob} but has no package.json, so no package owns what it holds and nothing in it is checked: make it a package, remove it, or add ${directory}/** to ignore`,
150
145
  target: directory
151
- } : unreadablePathFinding(directory, code);
146
+ };
152
147
  };
153
148
  const dotDirectory = (directory) => ({
154
149
  rule: "directory-without-package",
@@ -173,7 +168,10 @@ const directoriesWithoutPackage = (engine, members, unlistedByKinds) => {
173
168
  })),
174
169
  ...unlisted.links.map((link) => ({
175
170
  path: link,
176
- finding: () => isFile(nodePath.join(workspace.root, link)) ? void 0 : symbolicLinkFinding(link, linkDestination(nodePath.join(workspace.root, link)))
171
+ finding: () => {
172
+ const destination = isFile(nodePath.join(workspace.root, link)) ? void 0 : linkDestination(nodePath.join(workspace.root, link));
173
+ return destination === void 0 ? void 0 : symbolicLinkFinding(link, destination);
174
+ }
177
175
  }))
178
176
  ].toSorted((left, right) => left.path.split("/").length - right.path.split("/").length || left.path.localeCompare(right.path));
179
177
  const findings = /* @__PURE__ */ new Map();
@@ -289,7 +287,7 @@ const staleBaseline = (engine, findings) => engine.config.baseline.filter((entry
289
287
  * other dot directories hold sources too). A walk rather than `git ls-files`: generated and gitignored sources still
290
288
  * ship, and a nested repository or submodule inside a package is still part of it. A symbolic link is never followed:
291
289
  * every one is a finding, whatever it points at, unless it lies in a directory the check skips on purpose; and so is a
292
- * directory the walk cannot list.
290
+ * directory the walk cannot list. A path removed between being listed and being read is skipped, with no finding.
293
291
  */
294
292
  const walkSources = (engine) => {
295
293
  const files = /* @__PURE__ */ new Set();
@@ -300,7 +298,7 @@ const walkSources = (engine) => {
300
298
  try {
301
299
  entries = readdirSync(nodePath.join(root, relative), { withFileTypes: true });
302
300
  } catch (cause) {
303
- if (!skipsDirectory(engine, relative)) findings.set(relative, unreadablePathFinding(relative, codeOf(cause)));
301
+ if (!vanished(cause) && !skipsDirectory(engine, relative)) findings.set(relative, unreadablePathFinding(relative, codeOf(cause)));
304
302
  return;
305
303
  }
306
304
  for (const entry of entries) {
@@ -308,7 +306,8 @@ const walkSources = (engine) => {
308
306
  if (entry.isDirectory()) {
309
307
  if (!passedOver(entry.name)) walk(path);
310
308
  } else if (entry.isSymbolicLink()) {
311
- if (reportsLink(engine, path)) findings.set(path, symbolicLinkFinding(path, linkDestination(nodePath.join(root, path))));
309
+ const destination = reportsLink(engine, path) ? linkDestination(nodePath.join(root, path)) : void 0;
310
+ if (destination !== void 0) findings.set(path, symbolicLinkFinding(path, destination));
312
311
  } else if (entry.isFile() && reads(engine, path)) files.add(path);
313
312
  }
314
313
  };
@@ -323,17 +322,25 @@ const listSourceFiles = (engine) => [...walkSources(engine).files];
323
322
  const checkWorkspace = (root, config) => {
324
323
  const engine = createEngine(root, config);
325
324
  const { files, findings: walked } = walkSources(engine);
326
- const perFile = files.flatMap((file) => {
325
+ const perFile = [];
326
+ let checked = 0;
327
+ for (const file of files) {
327
328
  const text = attempt(() => readFileSync(nodePath.join(root, file), "utf8"));
328
- return text.ok ? findingsForFile(engine, file, text.value) : [unreadablePathFinding(file, codeOf(text.error), "file")];
329
- }).filter((finding) => finding.rule !== "package-without-kind");
329
+ if (text.ok) {
330
+ checked += 1;
331
+ perFile.push(...findingsForFile(engine, file, text.value));
332
+ } else if (!vanished(text.error)) {
333
+ checked += 1;
334
+ perFile.push(unreadablePathFinding(file, codeOf(text.error), "file"));
335
+ }
336
+ }
330
337
  const all = [
331
338
  ...walked,
332
- ...perFile,
339
+ ...perFile.filter((finding) => finding.rule !== "package-without-kind"),
333
340
  ...graphFindings(engine)
334
341
  ];
335
342
  return {
336
- files: files.length,
343
+ files: checked,
337
344
  findings: [...withoutBaselined(engine, all), ...staleBaseline(engine, all)]
338
345
  };
339
346
  };
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { y as findWorkspaceRoot } from "./engine-DmECSDnu.mjs";
3
- import { n as formatFinding, t as checkWorkspace } from "./check-BbeuFBNh.mjs";
2
+ import { y as findWorkspaceRoot } from "./engine-CwR59HVO.mjs";
3
+ import { n as formatFinding, t as checkWorkspace } from "./check-CWi8LUnB.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import nodePath from "node:path";
6
6
  import process from "node:process";
@@ -537,6 +537,15 @@ const attempt = (operation) => {
537
537
  };
538
538
  /** The message of something thrown, which need not be an Error. */
539
539
  const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
540
+ /**
541
+ * Whether something thrown by `node:fs` says the path is no longer there: `ENOENT`, or `ENOTDIR` when a directory on the
542
+ * way was replaced by a file. A path removed between being listed and being read (a test's temporary tree, an editor's
543
+ * swap file) fails this way, and is skipped rather than reported as unreadable.
544
+ */
545
+ const vanished = (cause) => {
546
+ const code = codeOf(cause);
547
+ return code === "ENOENT" || code === "ENOTDIR";
548
+ };
540
549
  /** The error code of something thrown by `node:fs` (`EACCES`), or its message when it has none. */
541
550
  const codeOf = (cause) => typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" ? cause.code : messageOf(cause);
542
551
  // src/workspace.ts
@@ -546,12 +555,36 @@ const codeOf = (cause) => typeof cause === "object" && cause !== null && "code"
546
555
  */
547
556
  /** The manifest at `path`, or why it cannot be read. */
548
557
  const readManifest = (path) => {
549
- const parsed = attempt(() => JSON.parse(readFileSync(path, "utf8")));
550
- if (!parsed.ok) return failure(`${path}: ${messageOf(parsed.error)}`);
551
- if (typeof parsed.value !== "object" || parsed.value === null) return failure(`${path} is not a JSON object`);
558
+ const text = attempt(() => readFileSync(path, "utf8"));
559
+ if (!text.ok) return failure({
560
+ message: `${path}: ${messageOf(text.error)}`,
561
+ vanished: vanished(text.error)
562
+ });
563
+ const parsed = attempt(() => JSON.parse(text.value));
564
+ if (!parsed.ok) return failure({
565
+ message: `${path}: ${messageOf(parsed.error)}`,
566
+ vanished: false
567
+ });
568
+ if (typeof parsed.value !== "object" || parsed.value === null) return failure({
569
+ message: `${path} is not a JSON object`,
570
+ vanished: false
571
+ });
552
572
  return success(parsed.value);
553
573
  };
554
- const namesOf = (field) => typeof field === "object" && field !== null && !Array.isArray(field) ? Object.keys(field).toSorted() : [];
574
+ /** Whether a dependency field has the shape npm and Bun require: an object of package names to version ranges. */
575
+ const isRecord = (field) => typeof field === "object" && field !== null && !Array.isArray(field);
576
+ const namesOf = (field) => isRecord(field) ? Object.keys(field).toSorted() : [];
577
+ /**
578
+ * A message for each dependency field of `manifest` that is present and not an object (`"dependencies": "x"`, a list, a
579
+ * number, null). Such a field declares nothing the check can read, so without a finding its edges would pass unseen.
580
+ */
581
+ const malformedFields = (manifest) => allManifestFields.filter((field) => manifest[field] !== void 0 && !isRecord(manifest[field])).map((field) => {
582
+ const value = manifest[field];
583
+ return {
584
+ field,
585
+ message: `${field} is ${value === null ? "null" : Array.isArray(value) ? "a list" : `a ${typeof value}`}, not an object of package names to version ranges, so no dependency in it is read`
586
+ };
587
+ });
555
588
  const fieldsOf = (manifest) => ({
556
589
  dependencies: namesOf(manifest.dependencies),
557
590
  devDependencies: namesOf(manifest.devDependencies),
@@ -566,7 +599,7 @@ const aliasesOf = (manifest) => {
566
599
  manifest.devDependencies,
567
600
  manifest.optionalDependencies,
568
601
  manifest.peerDependencies
569
- ]) if (typeof field === "object" && field !== null && !Array.isArray(field)) for (const [name, spec] of Object.entries(field)) {
602
+ ]) if (isRecord(field)) for (const [name, spec] of Object.entries(field)) {
570
603
  const target = typeof spec === "string" ? /^npm:(?<target>@?[^@]+)/u.exec(spec)?.groups?.target : void 0;
571
604
  if (target !== void 0) aliases.set(name, target);
572
605
  }
@@ -637,7 +670,7 @@ const directoriesMatching = (root, pattern, unlisted) => {
637
670
  if (depth >= depthLimit) return;
638
671
  const listed = attempt(() => readdirSync(nodePath.join(root, relative), { withFileTypes: true }));
639
672
  if (!listed.ok) {
640
- unlisted?.unreadable.push({
673
+ if (!vanished(listed.error)) unlisted?.unreadable.push({
641
674
  directory: relative,
642
675
  code: codeOf(listed.error)
643
676
  });
@@ -656,12 +689,13 @@ const directoriesMatching = (root, pattern, unlisted) => {
656
689
  return [];
657
690
  }
658
691
  if (base === clean) return existsSync(nodePath.join(root, base)) ? [base] : [];
659
- if (existsSync(nodePath.join(root, base)) && statSync(nodePath.join(root, base)).isDirectory()) walk(base, base === "" ? 0 : base.split("/").length);
692
+ const status = attempt(() => statSync(nodePath.join(root, base)));
693
+ if (status.ok && status.value.isDirectory()) walk(base, base === "" ? 0 : base.split("/").length);
660
694
  return found;
661
695
  };
662
696
  const loadWorkspace = (root) => {
663
697
  const rootRead = readManifest(nodePath.join(root, "package.json"));
664
- if (!rootRead.ok) throw new Error(rootRead.error);
698
+ if (!rootRead.ok) throw new Error(rootRead.error.message);
665
699
  const rootManifest = rootRead.value;
666
700
  const globs = workspaceGlobs(rootManifest) ?? [];
667
701
  const directories = /* @__PURE__ */ new Set();
@@ -672,10 +706,11 @@ const loadWorkspace = (root) => {
672
706
  const byName = /* @__PURE__ */ new Map();
673
707
  for (const directory of [...directories].toSorted()) {
674
708
  const read = readManifest(nodePath.join(root, directory, "package.json"));
709
+ if (!read.ok && read.error.vanished) continue;
675
710
  if (!read.ok) {
676
711
  problems.push({
677
712
  directory,
678
- message: `package.json cannot be read: ${read.error}`
713
+ message: `package.json cannot be read: ${read.error.message}`
679
714
  });
680
715
  continue;
681
716
  }
@@ -695,6 +730,11 @@ const loadWorkspace = (root) => {
695
730
  });
696
731
  continue;
697
732
  }
733
+ for (const { field, message } of malformedFields(manifest)) problems.push({
734
+ directory,
735
+ message,
736
+ field
737
+ });
698
738
  const entry = {
699
739
  name: manifest.name,
700
740
  directory,
@@ -1240,13 +1280,11 @@ const unreadablePathFinding = (path, code, kind = "directory") => ({
1240
1280
  message: kind === "directory" ? `${path} cannot be read (${code}), so the files in it cannot be checked: make it readable, or add ${path}/** to ignore` : `${path} cannot be read (${code}), so its imports cannot be checked: make it readable, or add it to ignore`,
1241
1281
  target: path
1242
1282
  });
1243
- /** The destination a symbolic link names, as written. */
1283
+ /** The destination a symbolic link names, as written, or undefined when the link has been removed since it was seen. */
1244
1284
  const linkDestination = (absolute) => {
1245
- try {
1246
- return readlinkSync(absolute);
1247
- } catch {
1248
- return "an unreadable destination";
1249
- }
1285
+ const destination = attempt(() => readlinkSync(absolute));
1286
+ if (destination.ok) return destination.value;
1287
+ return vanished(destination.error) ? void 0 : "an unreadable destination";
1250
1288
  };
1251
1289
  /**
1252
1290
  * The first symbolic link on the way from `owner`'s directory down to `file` (both repository-relative), the file
@@ -1305,6 +1343,7 @@ const developmentFileBreaks = (engine, file) => {
1305
1343
  read.set(directory, modifiedTime(nodePath.join(engine.workspace.root, directory)) ?? -1);
1306
1344
  const reached = [];
1307
1345
  const text = attempt(() => readFileSync(absolute, "utf8"));
1346
+ if (!text.ok && vanished(text.error)) continue;
1308
1347
  if (!text.ok) {
1309
1348
  breaks = `${next} cannot be read (${codeOf(text.error)}), so it cannot be judged`;
1310
1349
  break;
@@ -1331,7 +1370,10 @@ const findingsForFile = (engine, file, text, options = {}) => {
1331
1370
  const owner = packageOfFile(workspace, file);
1332
1371
  if (owner === void 0 || !sourceFile.test(file)) return [];
1333
1372
  const link = linkOnPath(engine, owner, file);
1334
- if (link !== void 0) return reportsLink(engine, link) ? [symbolicLinkFinding(link, linkDestination(nodePath.join(workspace.root, link)))] : [];
1373
+ if (link !== void 0) {
1374
+ const destination = reportsLink(engine, link) ? linkDestination(nodePath.join(workspace.root, link)) : void 0;
1375
+ return destination === void 0 ? [] : [symbolicLinkFinding(link, destination)];
1376
+ }
1335
1377
  if (!reads(engine, file) && developmentImports === void 0) return [];
1336
1378
  const packageRelative = file.slice(owner.directory.length + 1);
1337
1379
  const classified = effectivePolicy(engine, owner, packageRelative);
@@ -1463,4 +1505,4 @@ const findingsForFile = (engine, file, text, options = {}) => {
1463
1505
  const baselineCovers = (entry, finding) => entry.file === finding.file && entry.rule === finding.rule && entry.target === finding.target;
1464
1506
  /** `findings` less those the baseline covers. */
1465
1507
  const withoutBaselined = (engine, findings) => findings.filter((finding) => !engine.config.baseline.some((entry) => baselineCovers(entry, finding)));
1466
- export { defineConfig as A, attempt as C, matchesGlob as D, matchesAny as E, configProblems as O, packageOfFile as S, failure as T, importForms as _, effectivePolicy as a, loadWorkspace as b, linkDestination as c, reads as d, reportsLink as f, withoutBaselined as g, unreadablePathFinding as h, edgeRefusal as i, resolveConfig as j, defaultFileClasses as k, passedOver as l, symbolicLinkFinding as m, coversDirectory as n, findingsForFile as o, skipsDirectory as p, createEngine as r, ignoresDirectory as s, baselineCovers as t, policyOf as u, directoriesMatching as v, codeOf as w, packageNameOf as x, findWorkspaceRoot as y };
1508
+ export { defaultFileClasses as A, attempt as C, matchesAny as D, vanished as E, resolveConfig as M, matchesGlob as O, packageOfFile as S, failure as T, importForms as _, effectivePolicy as a, loadWorkspace as b, linkDestination as c, reads as d, reportsLink as f, withoutBaselined as g, unreadablePathFinding as h, edgeRefusal as i, defineConfig as j, configProblems as k, passedOver as l, symbolicLinkFinding as m, coversDirectory as n, findingsForFile as o, skipsDirectory as p, createEngine as r, ignoresDirectory as s, baselineCovers as t, policyOf as u, directoriesMatching as v, codeOf as w, packageNameOf as x, findWorkspaceRoot as y };
package/dist/index.d.mts CHANGED
@@ -14,10 +14,15 @@ interface Workspace {
14
14
  readonly byName: ReadonlyMap<string, WorkspacePackage>;
15
15
  /** The root package's own dependency fields. */
16
16
  readonly rootManifest: Readonly<Record<ManifestField, readonly string[]>>;
17
- /** Members that cannot be checked: no name, a name another member has, an unreadable package.json. */
17
+ /**
18
+ * Members whose package.json is wrong. With no name, a name another member has, or a package.json that cannot be read,
19
+ * the member cannot be checked and is not in `packages`; with a dependency field that is not an object, it is, as if
20
+ * that field declared nothing, and `field` names it.
21
+ */
18
22
  readonly problems: readonly {
19
23
  readonly directory: string;
20
24
  readonly message: string;
25
+ readonly field?: ManifestField;
21
26
  }[];
22
27
  /** The root package.json's `workspaces` globs, negations (`!apps/old`) included. */
23
28
  readonly globs: readonly string[];
@@ -138,7 +143,7 @@ interface SourceWalk {
138
143
  * other dot directories hold sources too). A walk rather than `git ls-files`: generated and gitignored sources still
139
144
  * ship, and a nested repository or submodule inside a package is still part of it. A symbolic link is never followed:
140
145
  * every one is a finding, whatever it points at, unless it lies in a directory the check skips on purpose; and so is a
141
- * directory the walk cannot list.
146
+ * directory the walk cannot list. A path removed between being listed and being read is skipped, with no finding.
142
147
  */
143
148
  export declare const walkSources: (engine: Engine) => SourceWalk;
144
149
  /** The source files `walkSources` finds. */
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as defineConfig, D as matchesGlob, E as matchesAny, O as configProblems, S as packageOfFile, _ as importForms, a as effectivePolicy, b as loadWorkspace, d as reads, g as withoutBaselined, j as resolveConfig, k as defaultFileClasses, o as findingsForFile, r as createEngine, u as policyOf, x as packageNameOf, y as findWorkspaceRoot } from "./engine-DmECSDnu.mjs";
2
- import { a as graphFindings, i as walkSources, n as formatFinding, o as staleBaseline, r as listSourceFiles, t as checkWorkspace } from "./check-BbeuFBNh.mjs";
1
+ import { A as defaultFileClasses, D as matchesAny, M as resolveConfig, O as matchesGlob, S as packageOfFile, _ as importForms, a as effectivePolicy, b as loadWorkspace, d as reads, g as withoutBaselined, j as defineConfig, k as configProblems, o as findingsForFile, r as createEngine, u as policyOf, x as packageNameOf, y as findWorkspaceRoot } from "./engine-CwR59HVO.mjs";
2
+ import { a as graphFindings, i as walkSources, n as formatFinding, o as staleBaseline, r as listSourceFiles, t as checkWorkspace } from "./check-CWi8LUnB.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, walkSources, withoutBaselined };
package/dist/plugin.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as attempt, T as failure, g as withoutBaselined, o as findingsForFile, r as createEngine, y as findWorkspaceRoot } from "./engine-DmECSDnu.mjs";
1
+ import { C as attempt, T as failure, g as withoutBaselined, o as findingsForFile, r as createEngine, y as findWorkspaceRoot } from "./engine-CwR59HVO.mjs";
2
2
  import { statSync } from "node:fs";
3
3
  import nodePath from "node:path";
4
4
  // src/plugin.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ox-dep-lint",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",