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 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-7N-4EtzS.mjs";
1
+ import { a as findingsForFile, c as withoutBaselined, n as createEngine, r as edgeRefusal, s as reads, t as baselineCovers, u as directoriesMatching } from "./engine-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-CpuQhyZE.mjs";
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-7N-4EtzS.mjs";
3
- import { n as formatFinding, t as checkWorkspace } from "./check-GClvJhK3.mjs";
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 { typeOnly } = site;
991
+ const typeOnly = site.typeOnly && config.imports.typeOnly === "relaxed";
968
992
  const specifier = withoutQuery(site.specifier);
969
- const target = engine.resolver.resolve(file, site.specifier);
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}, which ${article(kind)} package may not use in shipped code`, 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), true);
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-CpuQhyZE.mjs";
1
+ import { a as ImportPolicy, c as PackageRule, d as configProblems, f as defaultFileClasses, i as FilePolicy, l as ResolvedConfig, m as resolveConfig, n as Config, o as KindRule, p as defineConfig, r as FileClass, s as ManifestField, t as BaselineEntry, u as RuleName } from "./config-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-7N-4EtzS.mjs";
2
- import { a as staleBaseline, i as graphFindings, n as formatFinding, r as listSourceFiles, t as checkWorkspace } from "./check-GClvJhK3.mjs";
1
+ import { _ 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-7N-4EtzS.mjs";
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.1.1",
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",