ox-dep-lint 0.0.0-stage → 0.1.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/package.json +54 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philip B. Krogh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # ox-dep-lint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Module dependency graph rules for monorepos, as an [oxlint](https://oxc.rs/docs/guide/usage/linter) JS plugin and a CLI that share one engine. It declares the kinds of package a workspace has (libraries, services, platforms, apps, tools) and checks every import against them.
4
+
5
+ It is meant to sit beside two tools it does not repeat:
6
+
7
+ - [knip](https://knip.dev) for unused and unlisted packages, unresolved imports and binaries;
8
+ - oxlint's `import/no-cycle` for cycles between files.
9
+
10
+ ox-dep-lint replaces the hand-written module graph checks of three repositories by the same author (file-transfer, vektorprogrammet-reimagined and reebeat). Each copy had a gap another one closed.
11
+
12
+ ## What it checks
13
+
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
+ | `package-cycle` | A cycle between packages through their runtime dependency fields. | CLI |
33
+ | `stale-baseline` | A baseline entry that no finding needs any more, so the baseline can only shrink. | CLI |
34
+ | `parse-error` | A file that does not parse, so its imports cannot be checked. | both |
35
+
36
+ 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
+
38
+ ## Files: shipped, development, unchecked
39
+
40
+ The first matching file class decides how a file is checked. Globs in a class are relative to the file's package. Files that no class matches are shipped.
41
+
42
+ - **shipped:** every rule applies.
43
+ - **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
+ - **unchecked:** the file is not read.
45
+
46
+ By default, `test/**`, `tests/**`, `e2e/**`, `scripts/**`, package-root `*.config.*`, `**/__tests__/**`, `**/fixtures/**` and `**/*.{test,spec,bench}.*` are development files. A package's `dist/**` and `coverage/**` are not read; `output` changes that list (`[]` reads everything). A package's `testSupportFiles` are development files too (a conformance suite's entry, for example).
47
+
48
+ ## Configuration
49
+
50
+ ```ts
51
+ // ox-dep-lint.config.ts
52
+ import { defineConfig } from "ox-dep-lint"
53
+
54
+ export default defineConfig({
55
+ workspaceScopes: ["@acme/"],
56
+ kinds: {
57
+ library: { packages: ["libraries/*"], mayUse: ["library"], externals: ["effect"], runtimeModules: false },
58
+ service: { packages: ["services/*"], mayUse: ["library"], externals: ["effect"], runtimeModules: false },
59
+ platform: { packages: ["platforms/*"], mayUse: ["service", "library"] },
60
+ app: { packages: ["apps/*"], mayUse: ["library"] },
61
+ tool: { packages: ["tools/*"], mayUse: "any" },
62
+ },
63
+ packages: {
64
+ "@acme/conformance": { testSupportFiles: ["src/index.ts"] },
65
+ "@acme/web": { typeOnlyUses: ["@acme/service-room"] },
66
+ },
67
+ testOnlySpecifiers: ["vite-plus/test", "fast-check", "@acme/conformance"],
68
+ baseline: [{ file: "apps/web/src/legacy.ts", rule: "forbidden-edge", target: "@acme/service-room", reason: "moves behind the client library in stage 2" }],
69
+ })
70
+ ```
71
+
72
+ A kind may use its own kind only when `mayUse` lists it. `imports` holds the policies where repositories differ:
73
+
74
+ | Policy | Default | Other values |
75
+ | -------------------- | ---------------------------------------------------------- | ---------------- |
76
+ | `computed` | `error-in-shipped` | `error`, `allow` |
77
+ | `selfByName` | `allow` | `error` |
78
+ | `unresolved` | `error` | `ignore` |
79
+ | `manifestEdgeFields` | all four fields | any subset |
80
+ | `requireDeclared` | `true` | `false` |
81
+ | `shippedFields` | `dependencies`, `peerDependencies`, `optionalDependencies` | any subset |
82
+
83
+ Misspelled keys, unknown policies and baseline entries without a `target` are configuration errors, never silent defaults.
84
+
85
+ ## Installing
86
+
87
+ ```sh
88
+ bun add --dev ox-dep-lint # or npm install --save-dev ox-dep-lint
89
+ ```
90
+
91
+ Releases are published from a `v*` tag by `.github/workflows/release.yml`, with npm provenance.
92
+
93
+ ## Running it
94
+
95
+ The CLI walks every workspace package's directory (skipping `node_modules`, dot directories and `output`), so generated and gitignored sources and nested repositories are checked too, and runs the whole-graph checks:
96
+
97
+ ```sh
98
+ ox-dep-lint # finds ox-dep-lint.config.{ts,mts,js,mjs,json} at the workspace root
99
+ ox-dep-lint --config path/to/config.ts --root path/to/workspace
100
+ ```
101
+
102
+ The workspace root for a file is the nearest directory whose `package.json` `workspaces` hold that file, else the nearest with any `workspaces`. A worktree kept inside another checkout is checked as itself, and a git submodule listed as a member still belongs to the workspace that lists it.
103
+
104
+ The oxlint plugin reports the per-file findings in the editor and in `oxlint` / `vp lint`. It takes the same configuration as the option of its one rule:
105
+
106
+ ```ts
107
+ // vite.config.ts (Vite+) or oxlint.config.ts
108
+ import depLint from "./ox-dep-lint.config.ts"
109
+
110
+ export default defineConfig({
111
+ lint: {
112
+ jsPlugins: ["ox-dep-lint/plugin"],
113
+ rules: { "dep-lint/imports": ["error", depLint] },
114
+ },
115
+ })
116
+ ```
117
+
118
+ 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.
119
+
120
+ 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.
121
+
122
+ ## Not yet
123
+
124
+ - `import.meta.glob` patterns are reported as computed rather than read. vektorprogrammet-reimagined's closed glob grammar is the next thing to lift.
125
+ - Modules and layers inside a package, and restricted specifier groups with per-file allowances, are not in this version yet. vektorprogrammet-reimagined needs both.
126
+ - pnpm's `pnpm-workspace.yaml` is not read; the root `package.json` `workspaces` is.
127
+ - `/// <reference path="..." />` and JSDoc `@import` types are not read.
128
+ - 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
+
130
+ ## Licence
131
+
132
+ 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.
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "ox-dep-lint",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Module dependency graph rules for oxlint: package kinds, outside packages, runtime and test-only modules, undeclared, escaping and computed imports",
5
+ "license": "MIT",
6
+ "author": "Philip B. Krogh",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/phibkro/ox-dep-lint.git"
10
+ },
11
+ "bin": {
12
+ "ox-dep-lint": "./dist/cli.mjs"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.mts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "./plugin": {
26
+ "types": "./dist/plugin.d.mts",
27
+ "default": "./dist/plugin.mjs"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "vp pack",
32
+ "check": "vp check --deny-warnings && vp test run",
33
+ "test": "vp test run"
34
+ },
35
+ "dependencies": {
36
+ "oxc-parser": "0.153.0",
37
+ "oxc-resolver": "11.24.2"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "24.9.1",
41
+ "oxlint": "1.86.0",
42
+ "typescript": "7.0.2",
43
+ "vite-plus": "1.4.0"
44
+ },
45
+ "peerDependencies": {
46
+ "oxlint": ">=1.86.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "oxlint": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "engines": {
54
+ "node": ">=22.18"
55
+ }
56
+ }