ox-dep-lint 0.3.0 → 0.4.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 +14 -0
- package/README.md +31 -24
- package/dist/{check-F4cLmfYv.mjs → check-BbeuFBNh.mjs} +140 -22
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{config-DO_LRXM8.d.mts → config-CmWm5RyK.d.mts} +1 -1
- package/dist/{engine-Dc5tRuHT.mjs → engine-DmECSDnu.mjs} +162 -10
- package/dist/index.d.mts +17 -5
- package/dist/index.mjs +3 -3
- package/dist/plugin.mjs +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e75db50: Sources under a workspace glob no longer escape the check unreported: each one is read, or the reason it cannot be read is a finding. Plain files at a glob's level (`packages/x.ts`, outside every package directory) and entries that are neither files, directories nor links (FIFOs, sockets) are still not reported. This can add findings to a repository that passed before, so upgrade with the CLI once and baseline or fix what it reports.
|
|
8
|
+
|
|
9
|
+
- The walk now reads dot directories inside a package (`src/.internal/x.ts`), as oxlint already did; it skips only `node_modules` and `.git`.
|
|
10
|
+
- `output` now defaults to `dist/**`, `coverage/**` and the caches tools write at a package root: `.wrangler/**`, `.astro/**`, `.svelte-kit/**`, `.next/**`, `.turbo/**`, `.vercel/**`, `.nuxt/**`, `.output/**`. Setting `output` replaces the defaults; it does not add to them.
|
|
11
|
+
- `symbolic-link`: every symbolic link under a package directory, whatever it points at and whatever its name (`.d.ts`, `.md`, `.json`, extensionless); a link a `workspaces` or kind glob matches or names literally; and a link a glob passes through (`packages` itself a link). Links are never followed: 0.2 read a link to a file as if it were the file, skipped a link to a directory silently, and followed a linked `packages`. Only links inside directories the check skips on purpose are not reported: `node_modules`, `.git`, and directories that `ignore`, `output` or an unchecked file class cover through a trailing `/**`. A baseline entry names the link as `file` and its destination, as written, as `target`; baselined or not, what a link points at is never read. A Nix `result` link inside a package is a finding; `ignore: ["**/result/**"]` clears it. The oxlint plugin reports it on each file oxlint reaches through a link and reads nothing else in that file.
|
|
12
|
+
- `unreadable-path`: a directory the walk, a `workspaces` glob or a kind's glob cannot list, or a source file the check cannot read (`EACCES`), unless the check skips it on purpose. 0.2 stopped with exit code 2 and the error instead.
|
|
13
|
+
- `directory-without-package`: a directory a root `workspaces` glob matches that has no `package.json` and lies neither inside nor above a member, so nothing checks its files; and a dot directory such a glob would match (`packages/.template`), with or without a `package.json`, since npm and Bun workspaces never match dot directories. Only the topmost of nested ones is reported, and a negated glob (`!tools/legacy` or `!tools/legacy/**`) leaves it out. No new configuration key: add `dir/**` to `ignore`, or baseline it, to keep one.
|
|
14
|
+
|
|
15
|
+
A workspaces entry or kind glob that names or passes through a symbolic link no longer finds packages there: the link is reported instead of followed. `listSourceFiles` no longer lists a file reached through a link; the new `walkSources` returns the files with the walk's findings. `Workspace` has a new field, `globs`, the root's `workspaces` globs.
|
|
16
|
+
|
|
3
17
|
## 0.3.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -11,27 +11,30 @@ ox-dep-lint replaces the hand-written module graph checks of three repositories
|
|
|
11
11
|
|
|
12
12
|
## What it checks
|
|
13
13
|
|
|
14
|
-
| Rule | Finding
|
|
15
|
-
| ---------------------------------- |
|
|
16
|
-
| `forbidden-edge` | A load of a workspace package that its kind (or its `uses`) does not allow. The same check runs on edges declared in `package.json`: all four dependency fields by default, devDependencies included.
|
|
17
|
-
| `type-only-edge` | A value import of a package listed in `typeOnlyUses`. Only `import type` and `export type` declarations are type-only; an inline `import { type X }` still loads the module under `verbatimModuleSyntax`.
|
|
18
|
-
| `unknown-workspace-package` | A name under a workspace scope that no package has, in an import or a `package.json`.
|
|
19
|
-
| `package-path-escape` | A package specifier with `.` or `..` after the name (`@acme/a/../b/src/x.ts`).
|
|
20
|
-
| `relative-import-leaves-package` | A relative path, alias, root-absolute path or worker URL that resolves into another package, into `node_modules` or out of the repository. Import a package by its name instead.
|
|
21
|
-
| `shipped-imports-development-file` | Shipped code importing a development file (of its own package, or one another package exports), or a file under `output` or `ignore`, that would not pass as shipped code itself: a test helper using `fast-check`, a script using `node:fs`. A development file that would pass, such as a shared helper under `scripts/`, may be imported.
|
|
22
|
-
| `forbidden-external` | An outside package that the kind's `externals` does not allow, in shipped code or in `package.json`'s shipped fields. An outside package is named by the package it installs (the `name` in its installed `package.json`, or an `npm:` alias's target), so an alias cannot pass one package off as another; `externals` name real packages.
|
|
23
|
-
| `runtime-module` | `node:*`, a bare Node built-in (`fs`), `bun`, `bun:*` or `cloudflare:*`, also when a package import (`#crypto`) maps to one, in the shipped code of a kind with `runtimeModules: false`.
|
|
24
|
-
| `test-only-in-shipped` | A `testOnlySpecifiers` match (`fast-check`, `vite-plus/test`) in shipped code, with any `?query` stripped.
|
|
25
|
-
| `undeclared-import` | A package the file's own `package.json` does not declare. Shipped code needs `dependencies`, `peerDependencies` or `optionalDependencies` (`imports.shippedFields`); type-only imports and development files may also use devDependencies and the root's. This covers what knip misses: files no entry reaches, and shipped code leaning on a root devDependency.
|
|
26
|
-
| `computed-import` | A load whose module the check cannot read: `import(expression)`, `require(expression)`, `import.meta.glob`, a kept `createRequire` function, or a literal written with an escape sequence (which is also checked by the module it spells). Shipped code only, by default.
|
|
27
|
-
| `unresolved-import` | A relative path or alias (`#x`, `@/x`, `~/x`) that names no file.
|
|
28
|
-
| `self-import-by-name` | A package importing itself by its own name (off by default).
|
|
29
|
-
| `package-without-kind` | A workspace package that no kind's globs hold.
|
|
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.
|
|
31
|
-
| `invalid-package` | A workspace member whose `package.json` cannot be read, has no name, or has another member's name.
|
|
32
|
-
| `package
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
14
|
+
| Rule | Finding | Front end |
|
|
15
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
16
|
+
| `forbidden-edge` | A load of a workspace package that its kind (or its `uses`) does not allow. The same check runs on edges declared in `package.json`: all four dependency fields by default, devDependencies included. | both |
|
|
17
|
+
| `type-only-edge` | A value import of a package listed in `typeOnlyUses`. Only `import type` and `export type` declarations are type-only; an inline `import { type X }` still loads the module under `verbatimModuleSyntax`. | both |
|
|
18
|
+
| `unknown-workspace-package` | A name under a workspace scope that no package has, in an import or a `package.json`. | both |
|
|
19
|
+
| `package-path-escape` | A package specifier with `.` or `..` after the name (`@acme/a/../b/src/x.ts`). | both |
|
|
20
|
+
| `relative-import-leaves-package` | A relative path, alias, root-absolute path or worker URL that resolves into another package, into `node_modules` or out of the repository. Import a package by its name instead. | both |
|
|
21
|
+
| `shipped-imports-development-file` | Shipped code importing a development file (of its own package, or one another package exports), or a file under `output` or `ignore`, that would not pass as shipped code itself: a test helper using `fast-check`, a script using `node:fs`. A development file that would pass, such as a shared helper under `scripts/`, may be imported. | both |
|
|
22
|
+
| `forbidden-external` | An outside package that the kind's `externals` does not allow, in shipped code or in `package.json`'s shipped fields. An outside package is named by the package it installs (the `name` in its installed `package.json`, or an `npm:` alias's target), so an alias cannot pass one package off as another; `externals` name real packages. | both, `package.json` in the CLI |
|
|
23
|
+
| `runtime-module` | `node:*`, a bare Node built-in (`fs`), `bun`, `bun:*` or `cloudflare:*`, also when a package import (`#crypto`) maps to one, in the shipped code of a kind with `runtimeModules: false`. | both |
|
|
24
|
+
| `test-only-in-shipped` | A `testOnlySpecifiers` match (`fast-check`, `vite-plus/test`) in shipped code, with any `?query` stripped. | both |
|
|
25
|
+
| `undeclared-import` | A package the file's own `package.json` does not declare. Shipped code needs `dependencies`, `peerDependencies` or `optionalDependencies` (`imports.shippedFields`); type-only imports and development files may also use devDependencies and the root's. This covers what knip misses: files no entry reaches, and shipped code leaning on a root devDependency. | both |
|
|
26
|
+
| `computed-import` | A load whose module the check cannot read: `import(expression)`, `require(expression)`, `import.meta.glob`, a kept `createRequire` function, or a literal written with an escape sequence (which is also checked by the module it spells). Shipped code only, by default. | both |
|
|
27
|
+
| `unresolved-import` | A relative path or alias (`#x`, `@/x`, `~/x`) that names no file. | both |
|
|
28
|
+
| `self-import-by-name` | A package importing itself by its own name (off by default). | both |
|
|
29
|
+
| `package-without-kind` | A workspace package that no kind's globs hold. | both |
|
|
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 |
|
|
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
|
+
| `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 |
|
|
35
|
+
| `package-cycle` | A cycle between packages through their runtime dependency fields. | CLI |
|
|
36
|
+
| `stale-baseline` | A baseline entry that no finding needs any more, so the baseline can only shrink. | CLI |
|
|
37
|
+
| `parse-error` | A file that does not parse, so its imports cannot be checked. | both |
|
|
35
38
|
|
|
36
39
|
A load is any of: `import` and `export ... from` (static, `defer` and `source` phases), `import()`, a type-level `import("...")`, `import x = require("...")`, a string-named `declare module "..."`, `/// <reference types="..." />`, `require`, `require.resolve`, `module.require`, `import.meta.require`, `import.meta.resolve`, `process.getBuiltinModule`, `createRequire(...)("...")`, and a `new URL("...", import.meta.url)` passed to `new Worker`, `new SharedWorker` or `navigator.serviceWorker.register` (bundlers load it as a module, extension or not; any other `new URL` locates a file to read or an asset to serve, and is not a load). `require.resolve` and `import.meta.resolve` with a literal are edges; with an expression they load nothing.
|
|
37
40
|
|
|
@@ -43,7 +46,7 @@ The first matching file class decides how a file is checked. Globs in a class ar
|
|
|
43
46
|
- **development:** kind edges, relative imports and unresolved imports are still checked. The file may use devDependencies (its own and the root's), test-only specifiers, runtime modules, any outside package and computed `import()`.
|
|
44
47
|
- **unchecked:** the file is not read.
|
|
45
48
|
|
|
46
|
-
By default, `test/**`, `tests/**`, `e2e/**`, `scripts/**`, package-root `*.config.*`, `**/__tests__/**`, `**/fixtures/**` and `**/*.{test,spec,bench}.*` are development files. A package's
|
|
49
|
+
By default, `test/**`, `tests/**`, `e2e/**`, `scripts/**`, package-root `*.config.*`, `**/__tests__/**`, `**/fixtures/**` and `**/*.{test,spec,bench}.*` are development files. A package's build output and the caches tools write at its root are not read: `output` defaults to `dist/**`, `coverage/**`, `.wrangler/**`, `.astro/**`, `.svelte-kit/**`, `.next/**`, `.turbo/**`, `.vercel/**`, `.nuxt/**` and `.output/**`. Setting `output` replaces these defaults rather than adding to them, so list the ones you still want (`[]` reads everything). Shipped code that imports a file under `output` still has that file judged. A package's `testSupportFiles` are development files too (a conformance suite's entry, for example).
|
|
47
50
|
|
|
48
51
|
## Configuration
|
|
49
52
|
|
|
@@ -85,6 +88,10 @@ A kind may use its own kind only when `mayUse` lists it. `imports` holds the pol
|
|
|
85
88
|
|
|
86
89
|
Misspelled keys, unknown policies and baseline entries without a `target` are configuration errors, never silent defaults.
|
|
87
90
|
|
|
91
|
+
The root `workspaces` globs also say where packages live: a directory they match without a `package.json` is a `directory-without-package` finding, so no new key is needed. To keep such a directory (vendored sources that are not a package), add `dir/**` to `ignore` or baseline it with `file` and `target` both set to the directory.
|
|
92
|
+
|
|
93
|
+
A `symbolic-link` baseline entry names the link as `file` and its destination, as the link writes it, as `target`, so pointing the link elsewhere makes the entry stale. Baselining a link does not make the check read through it: what a link points at is never read, baselined or not. A Nix `result` link (from `nix build`) left inside a package is a finding too; build outside packages, or add `**/result/**` to `ignore`.
|
|
94
|
+
|
|
88
95
|
## Installing
|
|
89
96
|
|
|
90
97
|
```sh
|
|
@@ -95,7 +102,7 @@ Releases are published from a `v*` tag by `.github/workflows/release.yml`, with
|
|
|
95
102
|
|
|
96
103
|
## Running it
|
|
97
104
|
|
|
98
|
-
The CLI walks every workspace package's directory (skipping `node_modules
|
|
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.
|
|
99
106
|
|
|
100
107
|
```sh
|
|
101
108
|
ox-dep-lint # finds ox-dep-lint.config.{ts,mts,js,mjs,json} at the workspace root
|
|
@@ -120,7 +127,7 @@ export default defineConfig({
|
|
|
120
127
|
|
|
121
128
|
The plugin and the CLI call one function per file, and a test runs both under real oxlint and requires the same findings at the same places. In a long-lived process (an editor's language server) the plugin rebuilds its view of the workspace when a `package.json` changes, and looks again for a file it could not resolve.
|
|
122
129
|
|
|
123
|
-
Where they can still differ: oxlint chooses which files it lints (its own ignores), and the CLI reads the files of workspace packages. oxlint shows line-1 columns of a file that starts with a byte order mark three further on than the CLI does.
|
|
130
|
+
Where they can still differ: oxlint chooses which files it lints (its own ignores), and the CLI reads the files of workspace packages. oxlint follows symbolic links, so the plugin reports `symbolic-link` at line 1 of each file it is given through a link (and reads nothing else in it), where the CLI reports the link once; a link to a file oxlint does not lint (`README.md`) is reported by the CLI alone. Both read dot directories. oxlint shows line-1 columns of a file that starts with a byte order mark three further on than the CLI does.
|
|
124
131
|
|
|
125
132
|
## Not yet
|
|
126
133
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
// src/graph.ts
|
|
5
5
|
/**
|
|
6
6
|
* Checks that need the whole workspace rather than one file: edges declared only in `package.json`, cycles between
|
|
7
|
-
* packages, packages no kind holds,
|
|
8
|
-
* oxlint plugin sees one file at a time and never sees
|
|
7
|
+
* packages, packages no kind holds, directories the workspaces match that no package owns, and baseline entries no
|
|
8
|
+
* finding needs any more. Only the CLI runs these, because an oxlint plugin sees one file at a time and never sees
|
|
9
|
+
* `package.json`.
|
|
9
10
|
*/
|
|
10
11
|
const runtimeFields = [
|
|
11
12
|
"dependencies",
|
|
@@ -27,7 +28,12 @@ const graphFindings = (engine) => {
|
|
|
27
28
|
target: problem.directory
|
|
28
29
|
});
|
|
29
30
|
const members = /* @__PURE__ */ new Set([...workspace.packages.map((entry) => entry.directory), ...workspace.problems.map((problem) => problem.directory)]);
|
|
30
|
-
|
|
31
|
+
const unlistedByKinds = {
|
|
32
|
+
links: [],
|
|
33
|
+
unreadable: [],
|
|
34
|
+
dotDirectories: []
|
|
35
|
+
};
|
|
36
|
+
for (const kind of Object.values(config.kinds)) for (const pattern of kind.packages) for (const directory of directoriesMatching(workspace.root, pattern, unlistedByKinds)) if (!members.has(directory) && existsSync(nodePath.join(workspace.root, directory, "package.json"))) {
|
|
31
37
|
members.add(directory);
|
|
32
38
|
findings.push({
|
|
33
39
|
rule: "package-outside-workspace",
|
|
@@ -37,6 +43,7 @@ const graphFindings = (engine) => {
|
|
|
37
43
|
target: directory
|
|
38
44
|
});
|
|
39
45
|
}
|
|
46
|
+
findings.push(...directoriesWithoutPackage(engine, members, unlistedByKinds));
|
|
40
47
|
for (const entry of workspace.packages) {
|
|
41
48
|
const manifest = `${entry.directory}/package.json`;
|
|
42
49
|
if (engine.kindOf(entry) === void 0) {
|
|
@@ -87,6 +94,97 @@ const graphFindings = (engine) => {
|
|
|
87
94
|
findings.push(...packageCycles(engine));
|
|
88
95
|
return findings;
|
|
89
96
|
};
|
|
97
|
+
const isDirectory = (absolute) => {
|
|
98
|
+
try {
|
|
99
|
+
return statSync(absolute).isDirectory();
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/** Whether `absolute`, links followed, is something other than a directory; false when nothing is there. */
|
|
105
|
+
const isFile = (absolute) => {
|
|
106
|
+
try {
|
|
107
|
+
return !statSync(absolute).isDirectory();
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
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
|
+
/**
|
|
122
|
+
* What the root's `workspaces` globs (and a kind's globs) match that is no package: a directory without a
|
|
123
|
+
* `package.json`, whose files no package owns and so nothing checks (only the topmost of nested ones); a dot directory,
|
|
124
|
+
* which no workspaces glob matches; a directory the globs cannot list; and a symbolic link to a directory (or to
|
|
125
|
+
* nothing), which is never followed. Inside a member the walk reports these, so they are left to it. A directory above a member (under `packages/**`), one a negated glob or `ignore` leaves out, and
|
|
126
|
+
* anything under `node_modules` or `.git` are not reported.
|
|
127
|
+
*/
|
|
128
|
+
const directoriesWithoutPackage = (engine, members, unlistedByKinds) => {
|
|
129
|
+
const { workspace } = engine;
|
|
130
|
+
const negated = workspace.globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1).replace(/^\.\//u, "").replace(/\/$/u, ""));
|
|
131
|
+
const inside = (path) => [...members].some((member) => path === member || path.startsWith(`${member}/`));
|
|
132
|
+
const above = (path) => [...members].some((member) => member.startsWith(`${path}/`));
|
|
133
|
+
const leftOut = (path) => path.split("/").some(passedOver) || negated.some((pattern) => matchesGlob(path, pattern) || coversDirectory(pattern, path)) || ignoresDirectory(engine, path);
|
|
134
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
135
|
+
const unlisted = {
|
|
136
|
+
links: [...unlistedByKinds.links],
|
|
137
|
+
unreadable: [...unlistedByKinds.unreadable],
|
|
138
|
+
dotDirectories: []
|
|
139
|
+
};
|
|
140
|
+
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
|
+
const withoutPackage = (directory, glob) => {
|
|
142
|
+
if (above(directory) || !isDirectory(nodePath.join(workspace.root, directory))) return;
|
|
143
|
+
const code = unreadable(nodePath.join(workspace.root, directory));
|
|
144
|
+
return code === void 0 ? {
|
|
145
|
+
rule: "directory-without-package",
|
|
146
|
+
file: directory,
|
|
147
|
+
line: 1,
|
|
148
|
+
column: 0,
|
|
149
|
+
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
|
+
target: directory
|
|
151
|
+
} : unreadablePathFinding(directory, code);
|
|
152
|
+
};
|
|
153
|
+
const dotDirectory = (directory) => ({
|
|
154
|
+
rule: "directory-without-package",
|
|
155
|
+
file: directory,
|
|
156
|
+
line: 1,
|
|
157
|
+
column: 0,
|
|
158
|
+
message: `${directory} is a dot directory, which workspaces globs never match, so it is no package${existsSync(nodePath.join(workspace.root, directory, "package.json")) ? " though it has a package.json" : ""} and nothing in it is checked: rename it, list it in workspaces by its full path, remove it, or add ${directory}/** to ignore`,
|
|
159
|
+
target: directory
|
|
160
|
+
});
|
|
161
|
+
const pending = [
|
|
162
|
+
...unlisted.unreadable.map(({ directory, code }) => ({
|
|
163
|
+
path: directory,
|
|
164
|
+
finding: () => unreadablePathFinding(directory, code)
|
|
165
|
+
})),
|
|
166
|
+
...[...candidates].map(([directory, glob]) => ({
|
|
167
|
+
path: directory,
|
|
168
|
+
finding: () => withoutPackage(directory, glob)
|
|
169
|
+
})),
|
|
170
|
+
...unlisted.dotDirectories.map((directory) => ({
|
|
171
|
+
path: directory,
|
|
172
|
+
finding: () => dotDirectory(directory)
|
|
173
|
+
})),
|
|
174
|
+
...unlisted.links.map((link) => ({
|
|
175
|
+
path: link,
|
|
176
|
+
finding: () => isFile(nodePath.join(workspace.root, link)) ? void 0 : symbolicLinkFinding(link, linkDestination(nodePath.join(workspace.root, link)))
|
|
177
|
+
}))
|
|
178
|
+
].toSorted((left, right) => left.path.split("/").length - right.path.split("/").length || left.path.localeCompare(right.path));
|
|
179
|
+
const findings = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const { path, finding } of pending) {
|
|
181
|
+
const reportedAbove = [...findings.keys()].some((reported) => path.startsWith(`${reported}/`));
|
|
182
|
+
if (findings.has(path) || inside(path) || leftOut(path) || reportedAbove) continue;
|
|
183
|
+
const made = finding();
|
|
184
|
+
if (made !== void 0) findings.set(path, made);
|
|
185
|
+
}
|
|
186
|
+
return [...findings.values()].toSorted((left, right) => left.file.localeCompare(right.file));
|
|
187
|
+
};
|
|
90
188
|
/**
|
|
91
189
|
* Tarjan's strongly connected components, iterative so that a long chain of packages cannot exhaust the stack. The
|
|
92
190
|
* search starts from `roots` in order and follows each node's `successors` in order, so components and routes are
|
|
@@ -186,38 +284,58 @@ const staleBaseline = (engine, findings) => engine.config.baseline.filter((entry
|
|
|
186
284
|
}));
|
|
187
285
|
// src/check.ts
|
|
188
286
|
/** The whole check: every source file of the workspace through the engine, plus the whole-graph checks and the baseline. */
|
|
189
|
-
const isFile = (path) => {
|
|
190
|
-
try {
|
|
191
|
-
return statSync(path).isFile();
|
|
192
|
-
} catch {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
287
|
/**
|
|
197
|
-
* Every source file the engine reads, found by walking each package's directory (skipping `node_modules` and
|
|
198
|
-
* directories). A walk rather than `git ls-files`: generated and gitignored sources still
|
|
199
|
-
* or submodule inside a package is still part of it.
|
|
288
|
+
* Every source file the engine reads, found by walking each package's directory (skipping `node_modules` and `.git`;
|
|
289
|
+
* other dot directories hold sources too). A walk rather than `git ls-files`: generated and gitignored sources still
|
|
290
|
+
* ship, and a nested repository or submodule inside a package is still part of it. A symbolic link is never followed:
|
|
291
|
+
* 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.
|
|
200
293
|
*/
|
|
201
|
-
const
|
|
294
|
+
const walkSources = (engine) => {
|
|
202
295
|
const files = /* @__PURE__ */ new Set();
|
|
296
|
+
const findings = /* @__PURE__ */ new Map();
|
|
297
|
+
const { root } = engine.workspace;
|
|
203
298
|
const walk = (relative) => {
|
|
204
|
-
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = readdirSync(nodePath.join(root, relative), { withFileTypes: true });
|
|
302
|
+
} catch (cause) {
|
|
303
|
+
if (!skipsDirectory(engine, relative)) findings.set(relative, unreadablePathFinding(relative, codeOf(cause)));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const entry of entries) {
|
|
205
307
|
const path = `${relative}/${entry.name}`;
|
|
206
|
-
if (entry.isDirectory()
|
|
207
|
-
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
if (!passedOver(entry.name)) walk(path);
|
|
310
|
+
} else if (entry.isSymbolicLink()) {
|
|
311
|
+
if (reportsLink(engine, path)) findings.set(path, symbolicLinkFinding(path, linkDestination(nodePath.join(root, path))));
|
|
312
|
+
} else if (entry.isFile() && reads(engine, path)) files.add(path);
|
|
208
313
|
}
|
|
209
314
|
};
|
|
210
315
|
for (const entry of engine.workspace.packages) walk(entry.directory);
|
|
211
|
-
return
|
|
316
|
+
return {
|
|
317
|
+
files: [...files].toSorted(),
|
|
318
|
+
findings: [...findings.values()].toSorted((left, right) => left.file.localeCompare(right.file))
|
|
319
|
+
};
|
|
212
320
|
};
|
|
321
|
+
/** The source files `walkSources` finds. */
|
|
322
|
+
const listSourceFiles = (engine) => [...walkSources(engine).files];
|
|
213
323
|
const checkWorkspace = (root, config) => {
|
|
214
324
|
const engine = createEngine(root, config);
|
|
215
|
-
const files =
|
|
216
|
-
const
|
|
325
|
+
const { files, findings: walked } = walkSources(engine);
|
|
326
|
+
const perFile = files.flatMap((file) => {
|
|
327
|
+
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");
|
|
330
|
+
const all = [
|
|
331
|
+
...walked,
|
|
332
|
+
...perFile,
|
|
333
|
+
...graphFindings(engine)
|
|
334
|
+
];
|
|
217
335
|
return {
|
|
218
336
|
files: files.length,
|
|
219
337
|
findings: [...withoutBaselined(engine, all), ...staleBaseline(engine, all)]
|
|
220
338
|
};
|
|
221
339
|
};
|
|
222
340
|
const formatFinding = (finding) => `${finding.file}:${finding.line}:${finding.column + 1} ${finding.rule}: ${finding.message}`;
|
|
223
|
-
export {
|
|
341
|
+
export { graphFindings as a, walkSources as i, formatFinding as n, staleBaseline as o, listSourceFiles as r, checkWorkspace as t };
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Config } from "./config-
|
|
1
|
+
import { n as Config } from "./config-CmWm5RyK.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 {
|
|
3
|
-
import { n as formatFinding, t as checkWorkspace } from "./check-
|
|
2
|
+
import { y as findWorkspaceRoot } from "./engine-DmECSDnu.mjs";
|
|
3
|
+
import { n as formatFinding, t as checkWorkspace } from "./check-BbeuFBNh.mjs";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import nodePath from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
@@ -33,7 +33,7 @@ interface FileClass {
|
|
|
33
33
|
readonly files: readonly string[];
|
|
34
34
|
readonly policy: FilePolicy;
|
|
35
35
|
}
|
|
36
|
-
declare const ruleNames: readonly ["unknown-workspace-package", "package-without-kind", "package-outside-workspace", "invalid-package", "forbidden-edge", "type-only-edge", "relative-import-leaves-package", "package-path-escape", "shipped-imports-development-file", "self-import-by-name", "forbidden-external", "runtime-module", "test-only-in-shipped", "undeclared-import", "computed-import", "unresolved-import", "package-cycle", "stale-baseline", "parse-error"];
|
|
36
|
+
declare const ruleNames: readonly ["unknown-workspace-package", "package-without-kind", "package-outside-workspace", "directory-without-package", "invalid-package", "symbolic-link", "unreadable-path", "forbidden-edge", "type-only-edge", "relative-import-leaves-package", "package-path-escape", "shipped-imports-development-file", "self-import-by-name", "forbidden-external", "runtime-module", "test-only-in-shipped", "undeclared-import", "computed-import", "unresolved-import", "package-cycle", "stale-baseline", "parse-error"];
|
|
37
37
|
type RuleName = (typeof ruleNames)[number];
|
|
38
38
|
/** A finding the repository tolerates for now. The check fails when no finding needs it any more, so the list shrinks. */
|
|
39
39
|
interface BaselineEntry {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { builtinModules } from "node:module";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
import * as v from "valibot";
|
|
5
5
|
import { ResolverFactory } from "oxc-resolver";
|
|
@@ -9,7 +9,10 @@ const ruleNames = [
|
|
|
9
9
|
"unknown-workspace-package",
|
|
10
10
|
"package-without-kind",
|
|
11
11
|
"package-outside-workspace",
|
|
12
|
+
"directory-without-package",
|
|
12
13
|
"invalid-package",
|
|
14
|
+
"symbolic-link",
|
|
15
|
+
"unreadable-path",
|
|
13
16
|
"forbidden-edge",
|
|
14
17
|
"type-only-edge",
|
|
15
18
|
"relative-import-leaves-package",
|
|
@@ -41,8 +44,22 @@ const defaultFileClasses = [{
|
|
|
41
44
|
],
|
|
42
45
|
policy: "development"
|
|
43
46
|
}];
|
|
44
|
-
/**
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Build output, coverage reports and the caches tools write under a package root (generated, never written by hand),
|
|
49
|
+
* never read unless `output` says otherwise. Shipped code that imports from them is still judged.
|
|
50
|
+
*/
|
|
51
|
+
const defaultOutput = [
|
|
52
|
+
"dist/**",
|
|
53
|
+
"coverage/**",
|
|
54
|
+
".wrangler/**",
|
|
55
|
+
".astro/**",
|
|
56
|
+
".svelte-kit/**",
|
|
57
|
+
".next/**",
|
|
58
|
+
".turbo/**",
|
|
59
|
+
".vercel/**",
|
|
60
|
+
".nuxt/**",
|
|
61
|
+
".output/**"
|
|
62
|
+
];
|
|
46
63
|
const allManifestFields = [
|
|
47
64
|
"dependencies",
|
|
48
65
|
"devDependencies",
|
|
@@ -520,6 +537,8 @@ const attempt = (operation) => {
|
|
|
520
537
|
};
|
|
521
538
|
/** The message of something thrown, which need not be an Error. */
|
|
522
539
|
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
540
|
+
/** The error code of something thrown by `node:fs` (`EACCES`), or its message when it has none. */
|
|
541
|
+
const codeOf = (cause) => typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" ? cause.code : messageOf(cause);
|
|
523
542
|
// src/workspace.ts
|
|
524
543
|
/**
|
|
525
544
|
* The workspace as `package.json` files declare it: the root's `workspaces` globs, and each package's real name (never
|
|
@@ -589,7 +608,21 @@ const findWorkspaceRoot = (start) => {
|
|
|
589
608
|
if (nodePath.dirname(candidate) === candidate) return root ?? nearest;
|
|
590
609
|
}
|
|
591
610
|
};
|
|
592
|
-
|
|
611
|
+
/** The first of `path`'s directories (repository-relative), itself included, that is a symbolic link. */
|
|
612
|
+
const linkAlong = (root, path) => {
|
|
613
|
+
const parts = path === "" ? [] : path.split("/");
|
|
614
|
+
return parts.map((_, index) => parts.slice(0, index + 1).join("/")).find((prefix) => {
|
|
615
|
+
const status = attempt(() => lstatSync(nodePath.join(root, prefix)));
|
|
616
|
+
return status.ok && status.value.isSymbolicLink();
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
/**
|
|
620
|
+
* The directories under `root` that `pattern` matches, skipping `node_modules` and dot directories, as npm and Bun
|
|
621
|
+
* workspaces do. A symbolic link is never followed, not even one the pattern names literally or passes through (a
|
|
622
|
+
* `packages` that is a link), and a directory that cannot be listed is passed over; with `unlisted`, those and the dot
|
|
623
|
+
* directories the pattern would match are collected there, so the caller can report them.
|
|
624
|
+
*/
|
|
625
|
+
const directoriesMatching = (root, pattern, unlisted) => {
|
|
593
626
|
const clean = (pattern.startsWith("!") ? pattern.slice(1) : pattern).replace(/^\.\//u, "").replace(/\/$/u, "");
|
|
594
627
|
const fixed = [];
|
|
595
628
|
for (const segment of clean.split("/")) {
|
|
@@ -602,9 +635,26 @@ const directoriesMatching = (root, pattern) => {
|
|
|
602
635
|
const walk = (relative, depth) => {
|
|
603
636
|
if (matchesGlob(relative, clean)) found.push(relative);
|
|
604
637
|
if (depth >= depthLimit) return;
|
|
605
|
-
const
|
|
606
|
-
|
|
638
|
+
const listed = attempt(() => readdirSync(nodePath.join(root, relative), { withFileTypes: true }));
|
|
639
|
+
if (!listed.ok) {
|
|
640
|
+
unlisted?.unreadable.push({
|
|
641
|
+
directory: relative,
|
|
642
|
+
code: codeOf(listed.error)
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
for (const entry of listed.value) {
|
|
647
|
+
const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
648
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) walk(path, depth + 1);
|
|
649
|
+
else if (entry.isDirectory() && entry.name.startsWith(".") && entry.name !== ".git" && matchesGlob(path, clean)) unlisted?.dotDirectories.push(path);
|
|
650
|
+
else if (entry.isSymbolicLink() && matchesGlob(path, clean)) unlisted?.links.push(path);
|
|
651
|
+
}
|
|
607
652
|
};
|
|
653
|
+
const link = linkAlong(root, base);
|
|
654
|
+
if (link !== void 0) {
|
|
655
|
+
unlisted?.links.push(link);
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
608
658
|
if (base === clean) return existsSync(nodePath.join(root, base)) ? [base] : [];
|
|
609
659
|
if (existsSync(nodePath.join(root, base)) && statSync(nodePath.join(root, base)).isDirectory()) walk(base, base === "" ? 0 : base.split("/").length);
|
|
610
660
|
return found;
|
|
@@ -659,7 +709,8 @@ const loadWorkspace = (root) => {
|
|
|
659
709
|
packages,
|
|
660
710
|
byName,
|
|
661
711
|
rootManifest: fieldsOf(rootManifest),
|
|
662
|
-
problems
|
|
712
|
+
problems,
|
|
713
|
+
globs
|
|
663
714
|
};
|
|
664
715
|
};
|
|
665
716
|
/** The package whose directory holds the repository-relative `file`, the deepest one when packages nest. */
|
|
@@ -1120,6 +1171,99 @@ const reads = (engine, file) => {
|
|
|
1120
1171
|
const owner = packageOfFile(engine.workspace, file);
|
|
1121
1172
|
return owner !== void 0 && !matchesAny(file.slice(owner.directory.length + 1), engine.config.output);
|
|
1122
1173
|
};
|
|
1174
|
+
/** Whether `glob` matches every path under `directory`: it is `**`, or it or a directory above it matches what comes before a trailing `/**`. */
|
|
1175
|
+
const coversDirectory = (glob, directory) => {
|
|
1176
|
+
if (glob === "**") return true;
|
|
1177
|
+
if (!glob.endsWith("/**") || directory === "") return false;
|
|
1178
|
+
const parts = directory.split("/");
|
|
1179
|
+
return parts.some((_, index) => matchesGlob(parts.slice(0, index + 1).join("/"), glob.slice(0, -3)));
|
|
1180
|
+
};
|
|
1181
|
+
/**
|
|
1182
|
+
* Whether `glob` might match a path under `directory`: its leading segments agree with the directory's until a `**`,
|
|
1183
|
+
* and it is longer than the directory. A glob with braces is taken to match, so a doubt counts as a match.
|
|
1184
|
+
*/
|
|
1185
|
+
const mayMatchUnder = (glob, directory) => {
|
|
1186
|
+
if (glob.includes("{")) return true;
|
|
1187
|
+
const globParts = glob.split("/");
|
|
1188
|
+
const directoryParts = directory === "" ? [] : directory.split("/");
|
|
1189
|
+
for (const [index, part] of directoryParts.entries()) {
|
|
1190
|
+
const globPart = globParts[index];
|
|
1191
|
+
if (globPart === void 0) return false;
|
|
1192
|
+
if (globPart === "**") return true;
|
|
1193
|
+
if (!matchesGlob(part, globPart)) return false;
|
|
1194
|
+
}
|
|
1195
|
+
return globParts.length > directoryParts.length;
|
|
1196
|
+
};
|
|
1197
|
+
/** Whether an `ignore` glob covers every file under the repository-relative `directory`. */
|
|
1198
|
+
const ignoresDirectory = (engine, directory) => engine.config.ignore.some((glob) => coversDirectory(glob, directory));
|
|
1199
|
+
/**
|
|
1200
|
+
* Whether the check reads no file under the repository-relative `directory`, so a symbolic link or an unreadable
|
|
1201
|
+
* directory there hides nothing: an `ignore` glob covers it, or (relative to the package that owns it) an `output` glob
|
|
1202
|
+
* or an unchecked file class does, with no earlier class that might match a file in it. A glob covers a directory only
|
|
1203
|
+
* through a trailing `/**`; any doubt counts as read, so the answer errs towards a finding.
|
|
1204
|
+
*/
|
|
1205
|
+
const skipsDirectory = (engine, directory) => {
|
|
1206
|
+
const { config } = engine;
|
|
1207
|
+
if (ignoresDirectory(engine, directory)) return true;
|
|
1208
|
+
const owner = packageOfFile(engine.workspace, `${directory}/`);
|
|
1209
|
+
if (owner === void 0) return true;
|
|
1210
|
+
const relative = directory.slice(owner.directory.length + 1);
|
|
1211
|
+
if (config.output.some((glob) => coversDirectory(glob, relative))) return true;
|
|
1212
|
+
for (const fileClass of config.fileClasses) {
|
|
1213
|
+
if (fileClass.files.some((glob) => coversDirectory(glob, relative))) return fileClass.policy === "unchecked";
|
|
1214
|
+
if (fileClass.policy !== "unchecked" && fileClass.files.some((glob) => mayMatchUnder(glob, relative))) return false;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
};
|
|
1218
|
+
/** Directories the walk never enters, wherever they are: dependencies and git's own data. */
|
|
1219
|
+
const passedOver = (name) => name === "node_modules" || name === ".git";
|
|
1220
|
+
/**
|
|
1221
|
+
* Whether a symbolic link at the repository-relative `path` is a finding: always, whatever it points at, unless it lies
|
|
1222
|
+
* in (or is) a directory the check skips on purpose (`node_modules`, `.git`, or one `skipsDirectory` covers).
|
|
1223
|
+
*/
|
|
1224
|
+
const reportsLink = (engine, path) => !path.split("/").some(passedOver) && !skipsDirectory(engine, path);
|
|
1225
|
+
/** A symbolic link the check met and did not follow, at `path` (repository-relative), with the destination it names. */
|
|
1226
|
+
const symbolicLinkFinding = (path, destination) => ({
|
|
1227
|
+
rule: "symbolic-link",
|
|
1228
|
+
file: path,
|
|
1229
|
+
line: 1,
|
|
1230
|
+
column: 0,
|
|
1231
|
+
message: `${path} is a symbolic link to ${destination}, which the check never follows, so what it reaches goes unchecked: replace it with the files themselves, or baseline it`,
|
|
1232
|
+
target: destination
|
|
1233
|
+
});
|
|
1234
|
+
/** A directory the check needed to list, or a source file it needed to read, and could not, with the error code that said why. */
|
|
1235
|
+
const unreadablePathFinding = (path, code, kind = "directory") => ({
|
|
1236
|
+
rule: "unreadable-path",
|
|
1237
|
+
file: path,
|
|
1238
|
+
line: 1,
|
|
1239
|
+
column: 0,
|
|
1240
|
+
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
|
+
target: path
|
|
1242
|
+
});
|
|
1243
|
+
/** The destination a symbolic link names, as written. */
|
|
1244
|
+
const linkDestination = (absolute) => {
|
|
1245
|
+
try {
|
|
1246
|
+
return readlinkSync(absolute);
|
|
1247
|
+
} catch {
|
|
1248
|
+
return "an unreadable destination";
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
/**
|
|
1252
|
+
* The first symbolic link on the way from `owner`'s directory down to `file` (both repository-relative), the file
|
|
1253
|
+
* itself included. The CLI's walk never follows one, but oxlint does, and lints the files behind it under the link's
|
|
1254
|
+
* path.
|
|
1255
|
+
*/
|
|
1256
|
+
const linkOnPath = (engine, owner, file) => {
|
|
1257
|
+
const parts = file.slice(owner.directory.length + 1).split("/");
|
|
1258
|
+
for (const [index] of parts.entries()) {
|
|
1259
|
+
const path = [owner.directory, ...parts.slice(0, index + 1)].join("/");
|
|
1260
|
+
try {
|
|
1261
|
+
if (lstatSync(nodePath.join(engine.workspace.root, path)).isSymbolicLink()) return path;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1123
1267
|
/** The rules that hold for shipped code and not for development files. */
|
|
1124
1268
|
const shippedOnlyRules = /* @__PURE__ */ new Set([
|
|
1125
1269
|
"runtime-module",
|
|
@@ -1160,7 +1304,12 @@ const developmentFileBreaks = (engine, file) => {
|
|
|
1160
1304
|
const directory = nodePath.dirname(next);
|
|
1161
1305
|
read.set(directory, modifiedTime(nodePath.join(engine.workspace.root, directory)) ?? -1);
|
|
1162
1306
|
const reached = [];
|
|
1163
|
-
const
|
|
1307
|
+
const text = attempt(() => readFileSync(absolute, "utf8"));
|
|
1308
|
+
if (!text.ok) {
|
|
1309
|
+
breaks = `${next} cannot be read (${codeOf(text.error)}), so it cannot be judged`;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
const finding = findingsForFile(engine, next, text.value, { developmentImports: reached }).find((candidate) => shippedOnlyRules.has(candidate.rule));
|
|
1164
1313
|
if (finding !== void 0) breaks = `${next === file ? "" : `through ${next}, `}${finding.rule} at line ${finding.line}: ${finding.message}`;
|
|
1165
1314
|
queue.push(...reached);
|
|
1166
1315
|
}
|
|
@@ -1180,7 +1329,10 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
1180
1329
|
const asShipped = developmentImports !== void 0;
|
|
1181
1330
|
const { config, workspace } = engine;
|
|
1182
1331
|
const owner = packageOfFile(workspace, file);
|
|
1183
|
-
if (owner === void 0 || !sourceFile.test(file)
|
|
1332
|
+
if (owner === void 0 || !sourceFile.test(file)) return [];
|
|
1333
|
+
const link = linkOnPath(engine, owner, file);
|
|
1334
|
+
if (link !== void 0) return reportsLink(engine, link) ? [symbolicLinkFinding(link, linkDestination(nodePath.join(workspace.root, link)))] : [];
|
|
1335
|
+
if (!reads(engine, file) && developmentImports === void 0) return [];
|
|
1184
1336
|
const packageRelative = file.slice(owner.directory.length + 1);
|
|
1185
1337
|
const classified = effectivePolicy(engine, owner, packageRelative);
|
|
1186
1338
|
const policy = asShipped ? "shipped" : classified;
|
|
@@ -1311,4 +1463,4 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
1311
1463
|
const baselineCovers = (entry, finding) => entry.file === finding.file && entry.rule === finding.rule && entry.target === finding.target;
|
|
1312
1464
|
/** `findings` less those the baseline covers. */
|
|
1313
1465
|
const withoutBaselined = (engine, findings) => findings.filter((finding) => !engine.config.baseline.some((entry) => baselineCovers(entry, finding)));
|
|
1314
|
-
export {
|
|
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 };
|
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-CmWm5RyK.mjs";
|
|
2
2
|
interface WorkspacePackage {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
/** Repository-relative directory, `/`-separated (`libraries/parcel`). */
|
|
@@ -19,6 +19,8 @@ interface Workspace {
|
|
|
19
19
|
readonly directory: string;
|
|
20
20
|
readonly message: string;
|
|
21
21
|
}[];
|
|
22
|
+
/** The root package.json's `workspaces` globs, negations (`!apps/old`) included. */
|
|
23
|
+
readonly globs: readonly string[];
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* The workspace root for a file in `start`. The nearest directory whose `package.json` `workspaces` hold `start` is a
|
|
@@ -125,11 +127,21 @@ export declare const findingsForFile: (engine: Engine, file: string, text: strin
|
|
|
125
127
|
}) => Finding[];
|
|
126
128
|
/** `findings` less those the baseline covers. */
|
|
127
129
|
export declare const withoutBaselined: (engine: Engine, findings: readonly Finding[]) => Finding[];
|
|
130
|
+
/** The source files of the workspace's packages, and what kept the walk from seeing all of them. */
|
|
131
|
+
interface SourceWalk {
|
|
132
|
+
readonly files: readonly string[];
|
|
133
|
+
/** A `symbolic-link` finding for each link outside the directories it skips, an `unreadable-path` one for each directory it could not list. */
|
|
134
|
+
readonly findings: readonly Finding[];
|
|
135
|
+
}
|
|
128
136
|
/**
|
|
129
|
-
* Every source file the engine reads, found by walking each package's directory (skipping `node_modules` and
|
|
130
|
-
* directories). A walk rather than `git ls-files`: generated and gitignored sources still
|
|
131
|
-
* or submodule inside a package is still part of it.
|
|
137
|
+
* Every source file the engine reads, found by walking each package's directory (skipping `node_modules` and `.git`;
|
|
138
|
+
* other dot directories hold sources too). A walk rather than `git ls-files`: generated and gitignored sources still
|
|
139
|
+
* ship, and a nested repository or submodule inside a package is still part of it. A symbolic link is never followed:
|
|
140
|
+
* 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.
|
|
132
142
|
*/
|
|
143
|
+
export declare const walkSources: (engine: Engine) => SourceWalk;
|
|
144
|
+
/** The source files `walkSources` finds. */
|
|
133
145
|
export declare const listSourceFiles: (engine: Engine) => string[];
|
|
134
146
|
interface CheckResult {
|
|
135
147
|
readonly files: number;
|
|
@@ -167,4 +179,4 @@ interface ImportForms {
|
|
|
167
179
|
readonly parseErrors: readonly string[];
|
|
168
180
|
}
|
|
169
181
|
export declare const importForms: (path: string, source: string) => ImportForms;
|
|
170
|
-
export { type BaselineEntry, type CheckResult, type ComputedSite, type Config, type Engine, type FileClass, type FilePolicy, type Finding, type ImportForms, type ImportPolicy, type ImportSite, type KindRule, type ManifestField, type PackageRule, type ResolvedConfig, type RuleName, type Workspace, type WorkspacePackage, configProblems, defaultFileClasses, defineConfig, resolveConfig };
|
|
182
|
+
export { type BaselineEntry, type CheckResult, type ComputedSite, type Config, type Engine, type FileClass, type FilePolicy, type Finding, type ImportForms, type ImportPolicy, type ImportSite, type KindRule, type ManifestField, type PackageRule, type ResolvedConfig, type RuleName, type SourceWalk, type Workspace, type WorkspacePackage, configProblems, defaultFileClasses, defineConfig, resolveConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
export { checkWorkspace, configProblems, createEngine, defaultFileClasses, defineConfig, effectivePolicy, findWorkspaceRoot, findingsForFile, formatFinding, graphFindings, importForms, listSourceFiles, loadWorkspace, matchesAny, matchesGlob, packageNameOf, packageOfFile, policyOf, reads, resolveConfig, staleBaseline, withoutBaselined };
|
|
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";
|
|
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 {
|
|
1
|
+
import { C as attempt, T as failure, g as withoutBaselined, o as findingsForFile, r as createEngine, y as findWorkspaceRoot } from "./engine-DmECSDnu.mjs";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
// src/plugin.ts
|
|
@@ -9,7 +9,9 @@ import nodePath from "node:path";
|
|
|
9
9
|
* jsPlugins: ["ox-dep-lint/plugin"],
|
|
10
10
|
* rules: { "dep-lint/imports": ["error", depLintConfig] }
|
|
11
11
|
*
|
|
12
|
-
* Whole-graph checks (package.json edges and externals, package cycles,
|
|
12
|
+
* Whole-graph checks (package.json edges and externals, package cycles, directories no package owns, unreadable
|
|
13
|
+
* directories, stale baseline entries) need the CLI. oxlint follows symbolic links, so a file it reaches through one is
|
|
14
|
+
* reported as `symbolic-link` and not read further, as the CLI never reads it.
|
|
13
15
|
*/
|
|
14
16
|
/** How often (milliseconds) a long-lived process (an editor's language server) looks for changed package.json files. */
|
|
15
17
|
const recheckInterval = 1e3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ox-dep-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|