ox-dep-lint 0.1.1 → 0.2.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 +8 -0
- package/README.md +9 -0
- package/dist/{check-GClvJhK3.mjs → check-BIxs2Gr0.mjs} +1 -1
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{config-CpuQhyZE.d.mts → config-ClNl5rro.d.mts} +8 -0
- package/dist/{engine-7N-4EtzS.mjs → engine-C1qSxYaL.mjs} +36 -8
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 89ab407: New policy `imports.typeOnly`. With `"as-values"`, shipped code's type-only imports (every form, `/// <reference types>` included) are judged by `runtime-module`, `forbidden-external`, `undeclared-import` and `shipped-imports-development-file`. So `import type { Socket } from "bun"` fails in a kind with `runtimeModules: false`, and a vendor's types fail where the kind's `externals` do not allow it. A shipped `@types/x` still declares `x` for a type-only import. The default, `"relaxed"`, keeps 0.1's behaviour: types do not load, so those rules skip them.
|
|
8
|
+
|
|
9
|
+
A runtime's own types package (`node`, `@types/node`, `bun-types`, `@types/bun`, `@cloudflare/workers-types`, or a subpath of one) used type-only now counts as that runtime, in shipped and development files alike. So it is never reported as an outside package or as undeclared; until now an undeclared one gave `undeclared-import`. Under `"as-values"`, `runtime-module` judges it where the kind allows no runtime modules; under `"relaxed"` it passes, like other type-only imports.
|
|
10
|
+
|
|
3
11
|
## 0.1.1
|
|
4
12
|
|
|
5
13
|
- 0.1.0 was published without its built `dist` files, so neither the CLI nor the plugin could load. A `prepack` script now builds before every pack and publish, so a package cannot go out without them. Use 0.1.1.
|
package/README.md
CHANGED
|
@@ -79,6 +79,9 @@ A kind may use its own kind only when `mayUse` lists it. `imports` holds the pol
|
|
|
79
79
|
| `manifestEdgeFields` | all four fields | any subset |
|
|
80
80
|
| `requireDeclared` | `true` | `false` |
|
|
81
81
|
| `shippedFields` | `dependencies`, `peerDependencies`, `optionalDependencies` | any subset |
|
|
82
|
+
| `typeOnly` | `relaxed` | `as-values` |
|
|
83
|
+
|
|
84
|
+
`typeOnly: "as-values"` judges shipped code's type-only imports (`import type`, `export type`, type-level `import()`, `declare module`, `import type X = require()`, `/// <reference types>`) by `runtime-module`, `forbidden-external`, `undeclared-import` and `shipped-imports-development-file`, so `import type { Socket } from "bun"` fails in a kind without runtime modules. Two differences from value imports remain. A shipped field that declares `@types/x` declares `x` for a type-only import, and such an `@types/x` is an outside package too, so `externals` lists both. A runtime's own types package (`node` or `@types/node`, `bun`, `bun-types` or `@types/bun`, `@cloudflare/workers-types`) counts as that runtime, so it is judged by `runtime-module` and never needs declaring. That includes a published package whose emitted declarations reference those types: declare `@types/node` or `bun-types` for its consumers yourself; this check does not ask for it. By default types do not load, so those rules skip type-only imports. Kind edges and `typeOnlyUses` always see the real form.
|
|
82
85
|
|
|
83
86
|
Misspelled keys, unknown policies and baseline entries without a `target` are configuration errors, never silent defaults.
|
|
84
87
|
|
|
@@ -127,6 +130,12 @@ Where they can still differ: oxlint chooses which files it lints (its own ignore
|
|
|
127
130
|
- `/// <reference path="..." />` and JSDoc `@import` types are not read.
|
|
128
131
|
- A `require` passed around as a value (`const load = require; load("x")`) is not followed. Telling the global loader from a local of the same name needs scope analysis; until then the check reads `require` only where it is called.
|
|
129
132
|
|
|
133
|
+
## Contributing and releases
|
|
134
|
+
|
|
135
|
+
- Commits and pull request titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, `chore:`); a `commit-msg` hook and CI check them with commitlint.
|
|
136
|
+
- Every pull request adds a changeset: `bunx changeset` and pick patch, minor or major, or `bunx changeset --empty` when nothing ships. Before 1.0, a breaking change is a minor bump.
|
|
137
|
+
- Versions follow [semantic versioning](https://semver.org/) and are never edited by hand. While changesets wait on main, the `release` workflow keeps a pull request `chore(release): vX.Y.Z` up to date with the bumped `package.json` and the new `CHANGELOG.md` entry. Merging it publishes that version to npm through trusted publishing (with provenance), tags it and makes a GitHub release. Empty changesets alone open no release; they are consumed with the next one.
|
|
138
|
+
|
|
130
139
|
## Licence
|
|
131
140
|
|
|
132
141
|
MIT. The specifier reader and resolver are adapted from phibkro/vektorprogrammet-reimagined, and the rules from that repository and phibkro/file-transfer, all by the same author. The escape-sequence rule closes a bypass found in reebeat/reebeat's tier check. No code from reebeat was copied.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findingsForFile, c as withoutBaselined, n as createEngine, r as edgeRefusal, s as reads, t as baselineCovers, u as directoriesMatching } from "./engine-
|
|
1
|
+
import { a as findingsForFile, c as withoutBaselined, n as createEngine, r as edgeRefusal, s as reads, t as baselineCovers, u as directoriesMatching } from "./engine-C1qSxYaL.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
4
|
// src/graph.ts
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Config } from "./config-
|
|
1
|
+
import { n as Config } from "./config-ClNl5rro.mjs";
|
|
2
2
|
export declare const loadConfig: (path: string) => Promise<Config>;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { d as findWorkspaceRoot } from "./engine-
|
|
3
|
-
import { n as formatFinding, t as checkWorkspace } from "./check-
|
|
2
|
+
import { d as findWorkspaceRoot } from "./engine-C1qSxYaL.mjs";
|
|
3
|
+
import { n as formatFinding, t as checkWorkspace } from "./check-BIxs2Gr0.mjs";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import nodePath from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
@@ -63,6 +63,14 @@ interface ImportPolicy {
|
|
|
63
63
|
readonly requireDeclared?: boolean;
|
|
64
64
|
/** The fields that satisfy a shipped import. Default `dependencies`, `peerDependencies`, `optionalDependencies`. */
|
|
65
65
|
readonly shippedFields?: readonly ManifestField[];
|
|
66
|
+
/**
|
|
67
|
+
* How shipped code's type-only imports (`import type`, `export type`, a type-level `import()`, `declare module`, a
|
|
68
|
+
* types reference) are judged by `runtime-module`, `forbidden-external`, `undeclared-import` and
|
|
69
|
+
* `shipped-imports-development-file`. `"relaxed"` (default): types do not load, so those rules skip them and
|
|
70
|
+
* devDependencies declare them. `"as-values"`: judged like value imports, for kinds whose public types must not name
|
|
71
|
+
* a runtime or vendor either. Kind edges and `typeOnlyUses` always see the real form.
|
|
72
|
+
*/
|
|
73
|
+
readonly typeOnly?: "relaxed" | "as-values";
|
|
66
74
|
}
|
|
67
75
|
interface Config {
|
|
68
76
|
/** Scopes whose specifiers must name a workspace package (`@file-transfer/`). */
|
|
@@ -85,7 +85,8 @@ const allowedKeys = {
|
|
|
85
85
|
"unresolved",
|
|
86
86
|
"manifestEdgeFields",
|
|
87
87
|
"requireDeclared",
|
|
88
|
-
"shippedFields"
|
|
88
|
+
"shippedFields",
|
|
89
|
+
"typeOnly"
|
|
89
90
|
],
|
|
90
91
|
baseline: [
|
|
91
92
|
"file",
|
|
@@ -102,7 +103,8 @@ const choices = {
|
|
|
102
103
|
],
|
|
103
104
|
selfByName: ["error", "allow"],
|
|
104
105
|
unresolved: ["error", "ignore"],
|
|
105
|
-
requireDeclared: [true, false]
|
|
106
|
+
requireDeclared: [true, false],
|
|
107
|
+
typeOnly: ["relaxed", "as-values"]
|
|
106
108
|
};
|
|
107
109
|
const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
108
110
|
const unknownKeys = (where, value, allowed) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value).filter((key) => !allowed.includes(key)).map((key) => `${where} has the unknown key ${key} (known: ${allowed.join(", ")})`) : [`${where} is not an object`];
|
|
@@ -213,7 +215,8 @@ const resolveConfig = (config) => {
|
|
|
213
215
|
"dependencies",
|
|
214
216
|
"peerDependencies",
|
|
215
217
|
"optionalDependencies"
|
|
216
|
-
]
|
|
218
|
+
],
|
|
219
|
+
typeOnly: config.imports?.typeOnly ?? "relaxed"
|
|
217
220
|
},
|
|
218
221
|
baseline: config.baseline ?? [],
|
|
219
222
|
ignore: config.ignore ?? [],
|
|
@@ -800,6 +803,19 @@ const importForms = (path, source) => {
|
|
|
800
803
|
* it over every file and adds the whole-graph checks (`graph.ts`), so the two never disagree. Rules adapted from the
|
|
801
804
|
* module graph checks of phibkro/file-transfer, phibkro/vektorprogrammet-reimagined and reebeat/reebeat.
|
|
802
805
|
*/
|
|
806
|
+
const nodeModules = ["node:module", ...builtinModules];
|
|
807
|
+
/**
|
|
808
|
+
* Packages that hold a runtime's types, and the runtime modules they describe: the package counts as the runtime when
|
|
809
|
+
* the configuration names any of them a runtime module (Node's by its `node:` prefix or any bare built-in).
|
|
810
|
+
*/
|
|
811
|
+
const runtimeTypePackages = {
|
|
812
|
+
node: nodeModules,
|
|
813
|
+
"@types/node": nodeModules,
|
|
814
|
+
bun: ["bun"],
|
|
815
|
+
"bun-types": ["bun"],
|
|
816
|
+
"@types/bun": ["bun"],
|
|
817
|
+
"@cloudflare/workers-types": ["cloudflare:workers"]
|
|
818
|
+
};
|
|
803
819
|
/** The source files the engine reads; anything else (`.vue`, `.json`) is no finding in either front end. */
|
|
804
820
|
const sourceFile = /\.[cm]?[jt]sx?$/u;
|
|
805
821
|
const createEngine = (root, config) => {
|
|
@@ -963,10 +979,22 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
963
979
|
column: 0
|
|
964
980
|
}, `could not be parsed, so its imports cannot be checked: ${message}`, "parse");
|
|
965
981
|
for (const site of forms.computed) if (config.imports.computed === "error" || config.imports.computed === "error-in-shipped" && shipped) report("computed-import", site, `${site.text}: ${site.reason}; nothing can check what it loads`, site.text);
|
|
982
|
+
/**
|
|
983
|
+
* The packages that hold a runtime's own types, by the runtime module they describe: a type-only use of one
|
|
984
|
+
* (`/// <reference types="node" />`, `import type {} from "bun-types"`) is a use of that runtime, judged by
|
|
985
|
+
* `runtime-module` like `node:fs` and never as an outside package to declare.
|
|
986
|
+
*/
|
|
987
|
+
const runtimeOf = (site) => {
|
|
988
|
+
return (site.typeOnly ? runtimeTypePackages[packageNameOf(site.specifier)] : void 0)?.find((module) => isRuntimeModule(config, module));
|
|
989
|
+
};
|
|
966
990
|
for (const site of forms.imports) {
|
|
967
|
-
const
|
|
991
|
+
const typeOnly = site.typeOnly && config.imports.typeOnly === "relaxed";
|
|
968
992
|
const specifier = withoutQuery(site.specifier);
|
|
969
|
-
const
|
|
993
|
+
const runtime = runtimeOf(site);
|
|
994
|
+
const target = runtime === void 0 ? engine.resolver.resolve(file, site.specifier) : {
|
|
995
|
+
kind: "runtime",
|
|
996
|
+
name: runtime
|
|
997
|
+
};
|
|
970
998
|
const resolvedName = target.kind === "external" || target.kind === "workspace" ? target.name : void 0;
|
|
971
999
|
const subpathOf = (name) => specifier === name || specifier.startsWith(`${name}/`);
|
|
972
1000
|
const names = resolvedName === void 0 || subpathOf(resolvedName) ? [specifier] : [specifier, resolvedName];
|
|
@@ -982,7 +1010,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
982
1010
|
};
|
|
983
1011
|
switch (target.kind) {
|
|
984
1012
|
case "runtime":
|
|
985
|
-
if (shipped && !typeOnly && kindRule?.runtimeModules === false) report("runtime-module", site, `imports the runtime module ${specifier}
|
|
1013
|
+
if (shipped && !typeOnly && kindRule?.runtimeModules === false) report("runtime-module", site, `imports ${runtime === void 0 ? `the runtime module ${specifier},` : `the runtime types package ${specifier},`} which ${article(kind)} package may not use in shipped code`, specifier);
|
|
986
1014
|
break;
|
|
987
1015
|
case "virtual": break;
|
|
988
1016
|
case "unknown-workspace":
|
|
@@ -1000,7 +1028,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
1000
1028
|
if (config.imports.selfByName === "error") report("self-import-by-name", site, "imports its own package by its name; use a relative path", specifier);
|
|
1001
1029
|
break;
|
|
1002
1030
|
}
|
|
1003
|
-
const refusal = edgeRefusal(engine, owner, to, typeOnly, specifier);
|
|
1031
|
+
const refusal = edgeRefusal(engine, owner, to, site.typeOnly, specifier);
|
|
1004
1032
|
if (refusal !== void 0) report(refusal.rule, site, refusal.why, to.name);
|
|
1005
1033
|
if (config.imports.requireDeclared && !declares(engine, owner, to.name, !shipped || typeOnly)) report("undeclared-import", site, `imports ${to.name}, which ${owner.directory}/package.json does not declare${shipped ? " for shipped code" : ""}`, to.name);
|
|
1006
1034
|
break;
|
|
@@ -1024,7 +1052,7 @@ const findingsForFile = (engine, file, text, options = {}) => {
|
|
|
1024
1052
|
const permitted = (name) => allowed === "any" || allowed?.includes(name) === true || extra.includes(name);
|
|
1025
1053
|
if (shipped && !typeOnly && !permitted(target.name)) report("forbidden-external", site, `${article(kind)} package may not import ${target.name} in shipped code`, target.name);
|
|
1026
1054
|
const anywhere = !shipped || typeOnly;
|
|
1027
|
-
const declared = declares(engine, owner, target.name, anywhere) || declares(engine, owner, target.written, anywhere) || typeOnly && declares(engine, owner, typesPackage(target.name),
|
|
1055
|
+
const declared = declares(engine, owner, target.name, anywhere) || declares(engine, owner, target.written, anywhere) || site.typeOnly && declares(engine, owner, typesPackage(target.name), anywhere);
|
|
1028
1056
|
if (config.imports.requireDeclared && !declared) report("undeclared-import", site, `imports ${target.name}, which ${owner.directory}/package.json does not declare${shipped ? " for shipped code" : ""}`, target.name);
|
|
1029
1057
|
break;
|
|
1030
1058
|
}
|
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-ClNl5rro.mjs";
|
|
2
2
|
interface WorkspacePackage {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
/** Repository-relative directory, `/`-separated (`libraries/parcel`). */
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as configProblems, a as findingsForFile, b as resolveConfig, c as withoutBaselined, d as findWorkspaceRoot, f as loadWorkspace, g as matchesGlob, h as matchesAny, i as effectivePolicy, l as importForms, m as packageOfFile, n as createEngine, o as policyOf, p as packageNameOf, s as reads, v as defaultFileClasses, y as defineConfig } from "./engine-
|
|
2
|
-
import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-
|
|
1
|
+
import { _ as configProblems, a as findingsForFile, b as resolveConfig, c as withoutBaselined, d as findWorkspaceRoot, f as loadWorkspace, g as matchesGlob, h as matchesAny, i as effectivePolicy, l as importForms, m as packageOfFile, n as createEngine, o as policyOf, p as packageNameOf, s as reads, v as defaultFileClasses, y as defineConfig } from "./engine-C1qSxYaL.mjs";
|
|
2
|
+
import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-BIxs2Gr0.mjs";
|
|
3
3
|
export { checkWorkspace, configProblems, createEngine, defaultFileClasses, defineConfig, effectivePolicy, findWorkspaceRoot, findingsForFile, formatFinding, graphFindings, importForms, listSourceFiles, loadWorkspace, matchesAny, matchesGlob, packageNameOf, packageOfFile, policyOf, reads, resolveConfig, staleBaseline, withoutBaselined };
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findingsForFile, c as withoutBaselined, d as findWorkspaceRoot, n as createEngine } from "./engine-
|
|
1
|
+
import { a as findingsForFile, c as withoutBaselined, d as findWorkspaceRoot, n as createEngine } from "./engine-C1qSxYaL.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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Module dependency graph rules for oxlint: package kinds, outside packages, runtime and test-only modules, undeclared, escaping and computed imports",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Philip B. Krogh",
|
|
@@ -32,13 +32,18 @@
|
|
|
32
32
|
"build": "vp pack",
|
|
33
33
|
"check": "vp check --deny-warnings && vp test run",
|
|
34
34
|
"test": "vp test run",
|
|
35
|
-
"prepack": "vp pack"
|
|
35
|
+
"prepack": "vp pack",
|
|
36
|
+
"prepare": "vp config",
|
|
37
|
+
"changeset": "changeset"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"oxc-parser": "0.153.0",
|
|
39
41
|
"oxc-resolver": "11.24.2"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
44
|
+
"@changesets/cli": "3.0.3",
|
|
45
|
+
"@commitlint/cli": "21.2.3",
|
|
46
|
+
"@commitlint/config-conventional": "21.2.3",
|
|
42
47
|
"@types/node": "24.9.1",
|
|
43
48
|
"oxlint": "1.86.0",
|
|
44
49
|
"typescript": "7.0.2",
|