oxlint-plugin-boundaries 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Cedrick Artigo
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 ADDED
@@ -0,0 +1,131 @@
1
+ # oxlint-plugin-boundaries
2
+
3
+ Config-driven **cross-package / element-type boundaries** for [oxlint](https://oxc.rs) — enforce an architectural dependency matrix (which parts of your codebase may import which) **without a module resolver**.
4
+
5
+ You declare your own element table and allow-matrix in `.oxlintrc.json`; the plugin classifies every import by file path and flags edges your matrix disallows. Works in any monorepo (Bun / npm / pnpm / Yarn workspaces) and in single-package repos.
6
+
7
+ > **Status: alpha.** oxlint's JS plugin system (`jsPlugins`) is itself alpha and not yet semver-stable. This plugin pins a tested `oxlint` range in `peerDependencies` and is exercised against an oxlint-version matrix in CI. Expect a new minor when oxlint changes the plugin API. See [Versioning & the alpha pin](#versioning--the-alpha-pin).
8
+
9
+ ## Why this exists
10
+
11
+ oxlint has no native cross-package boundaries rule, and the popular [`eslint-plugin-boundaries`](https://github.com/javierbrea/eslint-plugin-boundaries) **cannot run under oxlint's JS-plugin layer**: that layer intentionally exposes _no module resolver_ (no `context.resolve`, empty `parserServices`), and `eslint-plugin-boundaries` depends on one (`eslint-import-resolver-typescript`).
12
+
13
+ This plugin sidesteps the missing resolver by **classifying purely from the file path**. It walks up to your workspace root, reads each package's `package.json` `name` once to build a `name → directory` index, and resolves bare specifiers like `@scope/pkg` to a directory — then to an element type. No resolver required.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ bun add -D oxlint-plugin-boundaries
19
+ # or: npm i -D oxlint-plugin-boundaries / pnpm add -D oxlint-plugin-boundaries
20
+ ```
21
+
22
+ `oxlint` is a peer dependency — install it yourself and keep it within the supported range.
23
+
24
+ ## Usage
25
+
26
+ Reference the plugin in `jsPlugins`, describe your architecture under `settings.boundaries`, and turn the rules on:
27
+
28
+ ```jsonc
29
+ {
30
+ "jsPlugins": ["oxlint-plugin-boundaries"],
31
+ "settings": {
32
+ "boundaries": {
33
+ // Ordered: more-specific patterns BEFORE their parents. First match wins.
34
+ "elements": [
35
+ { "type": "core", "pattern": "packages/core/**" },
36
+ { "type": "db", "pattern": "packages/db/**" },
37
+ { "type": "schemas", "pattern": "packages/schemas/**" },
38
+ { "type": "api-client", "pattern": "packages/api-client/**" },
39
+ { "type": "app-web", "pattern": "apps/web/**" },
40
+ ],
41
+ "rules": [
42
+ { "from": "core", "allow": ["db", "schemas"] },
43
+ { "from": "db", "allow": ["schemas"] },
44
+ { "from": "app-web", "allow": ["api-client", "schemas"] },
45
+ {
46
+ "from": "api-client",
47
+ "allow": ["app-api"],
48
+ "importKind": "type",
49
+ "message": "api-client may import apps/api only as `import type`.",
50
+ },
51
+ ],
52
+ "default": "disallow",
53
+ "ignore": ["**/*.test.ts", "**/*.spec.ts"],
54
+ },
55
+ },
56
+ "rules": {
57
+ "boundaries/element-types": "error",
58
+ "boundaries/no-unknown": "error",
59
+ },
60
+ }
61
+ ```
62
+
63
+ > oxlint resolves `jsPlugins` paths **relative to the config file**. Using the package name (as above) is the normal case; a relative path would be resolved against the `.oxlintrc.json` location, not your shell's cwd.
64
+
65
+ ## Configuration (`settings.boundaries`)
66
+
67
+ This object is the plugin's public API.
68
+
69
+ | Field | Type | Required | Meaning |
70
+ | ---------------- | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
71
+ | `elements` | `Element[]` | yes | Ordered list mapping path patterns to element types. **First match wins**, so list specific patterns before their parents. |
72
+ | `rules` | `Rule[]` | yes | Directional allow-matrix. Each entry says what a `from` element may import. |
73
+ | `default` | `"disallow"` \| `"allow"` | no (default `"disallow"`) | Verdict for an edge not covered by any rule. |
74
+ | `ignore` | `string[]` | no | Glob-ish patterns for files to skip entirely. |
75
+ | `workspaceScope` | `string` | no (derived) | The package-name prefix marking workspace-internal imports (e.g. `"@acme/"`). A bare specifier starting with it is a candidate boundary edge; anything else is an external dependency and ignored. When omitted, it is derived from the common scope of your workspace packages; set it explicitly if your packages don't share one. |
76
+
77
+ **`Element`**
78
+
79
+ | Field | Type | Meaning |
80
+ | --------- | -------- | -------------------------------------------------------------- |
81
+ | `type` | `string` | Element type name, referenced by `rules`. |
82
+ | `pattern` | `string` | Path pattern (root-relative) classifying files into this type. |
83
+
84
+ **`Rule`**
85
+
86
+ | Field | Type | Meaning |
87
+ | ------------ | --------------------- | ------------------------------------------------------------------------------------------------ |
88
+ | `from` | `string` | The importing element type this rule governs. |
89
+ | `allow` | `string[]` | Element types `from` may import. |
90
+ | `importKind` | `"value"` \| `"type"` | Optional. `"type"` narrows the `allow` list to `import type` edges only (a type-only carve-out). |
91
+ | `message` | `string` | Optional. Custom message shown when this edge is violated. |
92
+
93
+ Self-imports (an element importing its own type) are always allowed. External dependencies (npm packages outside your workspace) are never boundary edges and are ignored.
94
+
95
+ ### Rules
96
+
97
+ - **`boundaries/element-types`** — the core rule. For every import, classifies both ends and reports edges your matrix disallows (honoring the type-only carve-out).
98
+ - **`boundaries/no-unknown`** — flags a workspace-style specifier that resolves to _no_ package (typically a typo or a deleted package). Closes the gap that `element-types` leaves when a target classifies to nothing.
99
+
100
+ ## How classification works
101
+
102
+ 1. **Find the workspace root** — walk up from the file to the nearest `package.json` declaring `workspaces` (falls back to oxlint's cwd). Keying off the file path keeps results identical no matter which directory you run oxlint from.
103
+ 2. **Index packages** — read each workspace package's `name` once; memoize a `name → dir` map.
104
+ 3. **Classify both ends of each import** — the importing file by its path; the target by resolving a relative specifier against the file's directory, or a bare workspace specifier (`@scope/pkg[/sub]`) to its package dir via longest-prefix match.
105
+ 4. **Evaluate** — `self` → allowed; in the value allow-list → allowed; `import type` and in the type-only allow-list → allowed; otherwise the `default` decides.
106
+
107
+ ## Versioning & the alpha pin
108
+
109
+ oxlint's `jsPlugins` API is **alpha** and not semver-stable. The policy here:
110
+
111
+ - `peerDependencies.oxlint` is pinned to a **tested range**; CI runs an oxlint-version matrix.
112
+ - When oxlint ships a breaking plugin-API change, this package cuts a new release with an updated range. The test suite is the tripwire — there are no defensive version guards in the runtime.
113
+ - Keep your `oxlint` within the supported range for predictable behavior.
114
+
115
+ ## Authoring / build (for contributors)
116
+
117
+ Authored in TypeScript, shipped as compiled **ESM `.js` + `.d.ts`** so the published artifact loads on any supported Node — including versions below the 22.18 floor that raw `.ts` oxlint plugins require. Built with [tsdown](https://tsdown.dev) (oxc / Rolldown).
118
+
119
+ ```sh
120
+ bun install
121
+ bun run build # tsdown -> dist/ (.js + .d.ts), runs publint + attw
122
+ bun run type-check
123
+ bun run lint # dogfoods this very plugin
124
+ bun test
125
+ ```
126
+
127
+ > tsdown requires Node ≥ 22.18 / ≥ 24 to _run the build_. This affects contributors and CI only — never consumers of the published package.
128
+
129
+ ## License
130
+
131
+ [MIT](./LICENSE) © Paul Cedrick Artigo
@@ -0,0 +1,5 @@
1
+ //#region src/index.d.ts
2
+ declare const plugin: import("@oxlint/plugins").Plugin;
3
+ //#endregion
4
+ export { plugin as default };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cA8EM,MAAA,4BAAM,MAuFV"}
package/dist/index.js ADDED
@@ -0,0 +1,471 @@
1
+ import { definePlugin, defineRule } from "@oxlint/plugins";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { dirname, dirname as dirname$1, join, resolve } from "node:path";
4
+ //#region src/discover.ts
5
+ /**
6
+ * Walk up from `startDir` to the monorepo root: the nearest ancestor whose
7
+ * `package.json` declares `"workspaces"`. Falls back to `fallback` (typically
8
+ * `context.cwd`) when no such ancestor exists.
9
+ *
10
+ * Keying off the file path (not cwd) is what makes classification
11
+ * cwd-independent — running oxlint from a workspace subdir resolves the same
12
+ * root as running from the repo root.
13
+ */
14
+ function findWorkspaceRoot(startDir, fallback) {
15
+ let dir = startDir;
16
+ for (;;) {
17
+ const pkgPath = join(dir, "package.json");
18
+ if (existsSync(pkgPath)) try {
19
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
20
+ if (pkg && pkg.workspaces !== void 0) return dir;
21
+ } catch {}
22
+ const parent = dirname$1(dir);
23
+ if (parent === dir) break;
24
+ dir = parent;
25
+ }
26
+ return fallback;
27
+ }
28
+ /**
29
+ * Read the `workspaces` field of a root `package.json` and return the raw glob
30
+ * patterns. Supports both the array form (`["apps/*", ...]`) and the Bun/Yarn
31
+ * object form (`{ packages: [...] }`). Returns `[]` on any problem.
32
+ */
33
+ function readWorkspaceGlobs(root) {
34
+ try {
35
+ const ws = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).workspaces;
36
+ if (Array.isArray(ws)) return ws;
37
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages;
38
+ } catch {}
39
+ return [];
40
+ }
41
+ /**
42
+ * Expand a single workspace glob into concrete package directories.
43
+ *
44
+ * Only the two shapes monorepos actually use are handled: a literal directory
45
+ * (`packages/core`) and a single-level wildcard (`packages/*`, `apps/*`). A
46
+ * `**` glob is treated as the directory before it. No glob engine needed.
47
+ */
48
+ function expandGlob(root, glob) {
49
+ const normalized = glob.replaceAll("\\", "/").replace(/\/+$/, "");
50
+ const starIdx = normalized.indexOf("*");
51
+ if (starIdx === -1) {
52
+ const dir = resolve(root, normalized);
53
+ return existsSync(dir) ? [dir] : [];
54
+ }
55
+ const parentDir = resolve(root, normalized.slice(0, starIdx).replace(/\/[^/]*$/, "").replace(/\/$/, ""));
56
+ if (!existsSync(parentDir)) return [];
57
+ const out = [];
58
+ for (const entry of readdirSync(parentDir)) {
59
+ if (entry.startsWith(".")) continue;
60
+ const child = join(parentDir, entry);
61
+ try {
62
+ if (statSync(child).isDirectory()) out.push(child);
63
+ } catch {}
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Discover every workspace package under `root`: read each candidate dir's
69
+ * `package.json` and pair its `name` with its absolute directory.
70
+ */
71
+ function discoverPackages(root) {
72
+ const packages = [];
73
+ const seen = /* @__PURE__ */ new Set();
74
+ for (const glob of readWorkspaceGlobs(root)) for (const dir of expandGlob(root, glob)) {
75
+ if (seen.has(dir)) continue;
76
+ seen.add(dir);
77
+ try {
78
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
79
+ if (pkg && typeof pkg.name === "string") packages.push({
80
+ name: pkg.name,
81
+ dir
82
+ });
83
+ } catch {}
84
+ }
85
+ return packages;
86
+ }
87
+ /**
88
+ * Build a `packageName -> absoluteDir` map, memoized per monorepo root so the
89
+ * filesystem scan runs once across all linted files (mirrors oxlint's
90
+ * once-per-process `createOnce` design).
91
+ */
92
+ const indexCache = /* @__PURE__ */ new Map();
93
+ function getPackageIndex(root) {
94
+ const cached = indexCache.get(root);
95
+ if (cached) return cached;
96
+ const index = /* @__PURE__ */ new Map();
97
+ for (const { name, dir } of discoverPackages(root)) index.set(name, dir);
98
+ indexCache.set(root, index);
99
+ return index;
100
+ }
101
+ /**
102
+ * Resolve a bare package specifier to its package directory via the index.
103
+ *
104
+ * Handles subpath specifiers (`@scope/pkg/sub`) by matching the longest
105
+ * package name that the specifier equals or starts with (`name + "/"`). The
106
+ * matched package's directory is returned; subpath refinement (mapping
107
+ * `@scope/pkg/sub` to a sub-element) is intentionally NOT done here — callers
108
+ * classify the returned dir. Today only bare names are used.
109
+ */
110
+ function resolveSpecifierDir(specifier, index) {
111
+ const exact = index.get(specifier);
112
+ if (exact) return exact;
113
+ let bestDir = null;
114
+ let bestLen = -1;
115
+ for (const [name, dir] of index) {
116
+ const prefix = name + "/";
117
+ if (specifier.startsWith(prefix) && name.length > bestLen) {
118
+ bestDir = dir;
119
+ bestLen = name.length;
120
+ }
121
+ }
122
+ return bestDir;
123
+ }
124
+ //#endregion
125
+ //#region src/engine.ts
126
+ /**
127
+ * Normalize an absolute path to forward slashes and strip the monorepo-root
128
+ * prefix, yielding a root-relative path the element tests match against
129
+ * (e.g. `apps/api/src/http/app.ts`). This mirrors the old
130
+ * `eslint-plugin-boundaries` `mode:"full"` behavior, which matched patterns
131
+ * against root-relative paths.
132
+ *
133
+ * @param absPath absolute path
134
+ * @param root absolute monorepo root
135
+ * @returns root-relative, forward-slashed path
136
+ */
137
+ function toRelative(absPath, root) {
138
+ const normAbs = absPath.replaceAll("\\", "/");
139
+ const normRoot = root.replaceAll("\\", "/").replace(/\/+$/, "");
140
+ if (normAbs === normRoot) return "";
141
+ if (normAbs.startsWith(normRoot + "/")) return normAbs.slice(normRoot.length + 1);
142
+ return normAbs;
143
+ }
144
+ /**
145
+ * Classify an absolute path to an element type, or null if it matches no
146
+ * element. The first matching element in `ELEMENTS` wins, so the table MUST be
147
+ * ordered specific-before-parent.
148
+ *
149
+ * @param absPath absolute path to classify
150
+ * @param root absolute monorepo root
151
+ * @param elements ordered element list
152
+ */
153
+ function classifyPath(absPath, root, elements) {
154
+ const rel = toRelative(absPath, root);
155
+ if (!rel) return null;
156
+ for (const element of elements) if (element.test(rel)) return element.type;
157
+ return null;
158
+ }
159
+ /**
160
+ * Classify a bare workspace specifier (`@scope/pkg[/sub]`) to an element type
161
+ * by resolving it to its package directory and classifying that dir.
162
+ *
163
+ * @param specifier import specifier as written
164
+ * @param root absolute monorepo root
165
+ * @param elements ordered element list
166
+ */
167
+ function classifySpecifier(specifier, root, elements) {
168
+ const dir = resolveSpecifierDir(specifier, getPackageIndex(root));
169
+ if (!dir) return null;
170
+ return classifyPath(dir, root, elements);
171
+ }
172
+ /**
173
+ * Resolve an import specifier (relative or bare workspace) to the element type
174
+ * of its target. Returns null for external deps (`zod`, `hono`, …) and for
175
+ * anything that classifies to no element.
176
+ *
177
+ * @param specifier import specifier as written (`node.source.value`)
178
+ * @param fromFile absolute path of the importing file
179
+ * @param root absolute monorepo root
180
+ * @param options elements + workspace scope
181
+ */
182
+ function classifyTarget(specifier, fromFile, root, { elements, workspaceScope }) {
183
+ if (specifier.startsWith(".")) return classifyPath(resolve(dirname$1(fromFile), specifier), root, elements);
184
+ if (specifier.startsWith(workspaceScope)) return classifySpecifier(specifier, root, elements);
185
+ return null;
186
+ }
187
+ /**
188
+ * Evaluate whether `fromType` may import `toType` given the import kind.
189
+ *
190
+ * Rules:
191
+ * - self (`toType === fromType`) -> allowed (intra-package).
192
+ * - `ALLOW[fromType]` contains `toType` -> allowed (value edge).
193
+ * - `typeOnly` AND `TYPE_ONLY_ALLOW[from]`
194
+ * contains `toType` -> allowed (type-only carve-out).
195
+ * - otherwise -> disallowed.
196
+ *
197
+ * @param fromType element type of the importing file
198
+ * @param toType element type of the imported target
199
+ * @param typeOnly whether the import is `import type` (declaration-level)
200
+ * @param table the repo-specific table (passed in)
201
+ */
202
+ function evaluate(fromType, toType, typeOnly, table) {
203
+ if (toType === fromType) return {
204
+ allowed: true,
205
+ reason: "self"
206
+ };
207
+ if (table.ALLOW[fromType]?.has(toType)) return {
208
+ allowed: true,
209
+ reason: "value-allow"
210
+ };
211
+ if (typeOnly && table.TYPE_ONLY_ALLOW[fromType]?.has(toType)) return {
212
+ allowed: true,
213
+ reason: "type-allow"
214
+ };
215
+ return {
216
+ allowed: false,
217
+ reason: "disallow"
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/config.ts
222
+ var BoundariesConfigError = class extends Error {
223
+ constructor(message) {
224
+ super(`Invalid settings.boundaries: ${message}`);
225
+ this.name = "BoundariesConfigError";
226
+ }
227
+ };
228
+ function fail(message) {
229
+ throw new BoundariesConfigError(message);
230
+ }
231
+ function isObject(value) {
232
+ return typeof value === "object" && value !== null && !Array.isArray(value);
233
+ }
234
+ function escapeLiteral(literal) {
235
+ return literal.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
236
+ }
237
+ function compileElementPattern(pattern, label) {
238
+ const dir = pattern.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/\*\*$/, "").replace(/\/+$/, "");
239
+ if (dir === "" || dir === "**") fail(`${label} pattern ${JSON.stringify(pattern)} is too broad - name a directory.`);
240
+ if (dir.includes("*")) fail(`${label} pattern ${JSON.stringify(pattern)} is unsupported - element patterns must be a directory ("dir") or a directory tree ("dir" + slash + globstar), with no wildcards inside the path.`);
241
+ const re = new RegExp(`^${escapeLiteral(dir)}(/|$)`);
242
+ return (relPath) => re.test(relPath);
243
+ }
244
+ function compileIgnorePattern(pattern) {
245
+ const normalized = pattern.replaceAll("\\", "/").replace(/^\.\//, "");
246
+ const trailingTree = normalized.endsWith("/**");
247
+ const body = trailingTree ? normalized.slice(0, -3) : normalized;
248
+ let source = "";
249
+ let i = 0;
250
+ while (i < body.length) if (body.startsWith("**/", i)) {
251
+ source += "(?:.*/)?";
252
+ i += 3;
253
+ } else if (body.startsWith("**", i)) {
254
+ source += ".*";
255
+ i += 2;
256
+ } else if (body[i] === "*") {
257
+ source += "[^/]*";
258
+ i += 1;
259
+ } else {
260
+ source += escapeLiteral(body[i]);
261
+ i += 1;
262
+ }
263
+ const re = new RegExp(`^${source}${trailingTree ? "(/|$)" : "$"}`);
264
+ return (relPath) => re.test(relPath);
265
+ }
266
+ function validateElements(raw) {
267
+ if (!Array.isArray(raw)) fail("`elements` is required and must be an array.");
268
+ if (raw.length === 0) fail("`elements` must be a non-empty array.");
269
+ const seen = /* @__PURE__ */ new Set();
270
+ const out = [];
271
+ raw.forEach((entry, idx) => {
272
+ if (!isObject(entry)) fail(`elements[${idx}] must be an object.`);
273
+ const { type, pattern } = entry;
274
+ if (typeof type !== "string" || type === "") fail(`elements[${idx}] is missing a string \`type\`.`);
275
+ if (typeof pattern !== "string" || pattern === "") fail(`elements[${idx}] (type ${JSON.stringify(type)}) is missing a string \`pattern\`.`);
276
+ if (seen.has(type)) fail(`duplicate element type ${JSON.stringify(type)}.`);
277
+ seen.add(type);
278
+ out.push({
279
+ type,
280
+ pattern
281
+ });
282
+ });
283
+ return out;
284
+ }
285
+ function validateRules(raw, knownTypes) {
286
+ if (!Array.isArray(raw)) fail("`rules` is required and must be an array.");
287
+ const out = [];
288
+ raw.forEach((entry, idx) => {
289
+ if (!isObject(entry)) fail(`rules[${idx}] must be an object.`);
290
+ const { from, allow, importKind, message } = entry;
291
+ if (typeof from !== "string" || from === "") fail(`rules[${idx}] is missing a string \`from\`.`);
292
+ if (!knownTypes.has(from)) fail(`rules[${idx}].from references unknown element type ${JSON.stringify(from)}.`);
293
+ if (!Array.isArray(allow)) fail(`rules[${idx}] (from ${JSON.stringify(from)}) is missing an \`allow\` array.`);
294
+ allow.forEach((to, j) => {
295
+ if (typeof to !== "string" || to === "") fail(`rules[${idx}].allow[${j}] must be a non-empty string.`);
296
+ if (!knownTypes.has(to)) fail(`rules[${idx}].allow references unknown element type ${JSON.stringify(to)}.`);
297
+ });
298
+ if (importKind !== void 0 && importKind !== "value" && importKind !== "type") fail(`rules[${idx}].importKind must be "value" or "type" (got ${JSON.stringify(importKind)}).`);
299
+ if (message !== void 0 && typeof message !== "string") fail(`rules[${idx}].message must be a string.`);
300
+ out.push({
301
+ from,
302
+ allow,
303
+ ...importKind !== void 0 ? { importKind } : {},
304
+ ...message !== void 0 ? { message } : {}
305
+ });
306
+ });
307
+ return out;
308
+ }
309
+ function deriveWorkspaceScope(packageNames) {
310
+ const scopes = /* @__PURE__ */ new Set();
311
+ for (const name of packageNames) {
312
+ const match = /^(@[^/]+\/)/.exec(name);
313
+ if (!match) return null;
314
+ scopes.add(match[1]);
315
+ }
316
+ if (scopes.size !== 1) return null;
317
+ return [...scopes][0];
318
+ }
319
+ function compileConfig(raw, options = {}) {
320
+ if (!isObject(raw)) fail("settings.boundaries must be an object.");
321
+ const elementConfigs = validateElements(raw.elements);
322
+ const knownTypes = new Set(elementConfigs.map((e) => e.type));
323
+ const ruleConfigs = validateRules(raw.rules, knownTypes);
324
+ let dflt = "disallow";
325
+ if (raw.default !== void 0) {
326
+ if (raw.default !== "allow" && raw.default !== "disallow") fail(`\`default\` must be "allow" or "disallow" (got ${JSON.stringify(raw.default)}).`);
327
+ dflt = raw.default;
328
+ }
329
+ let ignorePatterns = [];
330
+ if (raw.ignore !== void 0) {
331
+ if (!Array.isArray(raw.ignore)) fail("`ignore` must be an array of glob strings.");
332
+ raw.ignore.forEach((p, idx) => {
333
+ if (typeof p !== "string" || p === "") fail(`ignore[${idx}] must be a non-empty string.`);
334
+ });
335
+ ignorePatterns = raw.ignore;
336
+ }
337
+ let workspaceScope;
338
+ if (raw.workspaceScope !== void 0) {
339
+ if (typeof raw.workspaceScope !== "string" || raw.workspaceScope === "") fail("`workspaceScope` must be a non-empty string (e.g. \"@acme/\").");
340
+ workspaceScope = raw.workspaceScope;
341
+ } else {
342
+ const derived = deriveWorkspaceScope(options.packageNames ?? []);
343
+ if (derived === null) fail("`workspaceScope` is required and could not be derived from the workspace package names. Set settings.boundaries.workspaceScope to your packages' shared scope (e.g. \"@acme/\").");
344
+ workspaceScope = derived;
345
+ }
346
+ const elements = elementConfigs.map((e) => ({
347
+ type: e.type,
348
+ test: compileElementPattern(e.pattern, `elements[type ${JSON.stringify(e.type)}]`)
349
+ }));
350
+ const ignore = ignorePatterns.map((pattern, idx) => ({
351
+ type: `ignore[${idx}]`,
352
+ test: compileIgnorePattern(pattern)
353
+ }));
354
+ const ALLOW = {};
355
+ const TYPE_ONLY_ALLOW = {};
356
+ const messages = /* @__PURE__ */ new Map();
357
+ const edgeKey = (from, to) => `${from} ${to}`;
358
+ for (const rule of ruleConfigs) {
359
+ const target = rule.importKind === "type" ? TYPE_ONLY_ALLOW : ALLOW;
360
+ const set = target[rule.from] ??= /* @__PURE__ */ new Set();
361
+ for (const to of rule.allow) {
362
+ set.add(to);
363
+ if (rule.message !== void 0) messages.set(edgeKey(rule.from, to), rule.message);
364
+ }
365
+ }
366
+ return {
367
+ elements,
368
+ table: {
369
+ ELEMENTS: elements,
370
+ ALLOW,
371
+ TYPE_ONLY_ALLOW
372
+ },
373
+ default: dflt,
374
+ ignore,
375
+ workspaceScope,
376
+ messageFor: (fromType, toType) => messages.get(edgeKey(fromType, toType))
377
+ };
378
+ }
379
+ //#endregion
380
+ //#region src/index.ts
381
+ const rootCache = /* @__PURE__ */ new Map();
382
+ function rootFor(filename, cwd) {
383
+ const fileDir = dirname(filename);
384
+ const cached = rootCache.get(fileDir);
385
+ if (cached !== void 0) return cached;
386
+ const root = findWorkspaceRoot(fileDir, cwd);
387
+ rootCache.set(fileDir, root);
388
+ return root;
389
+ }
390
+ const compiledCache = /* @__PURE__ */ new Map();
391
+ function compiledFor(context, root) {
392
+ const cached = compiledCache.get(root);
393
+ if (cached !== void 0) return cached;
394
+ const settings = context.settings;
395
+ if (settings.boundaries === void 0 || settings.boundaries === null) {
396
+ compiledCache.set(root, null);
397
+ return null;
398
+ }
399
+ const packageNames = discoverPackages(root).map((p) => p.name);
400
+ const compiled = compileConfig(settings.boundaries, { packageNames });
401
+ compiledCache.set(root, compiled);
402
+ return compiled;
403
+ }
404
+ function isIgnored(compiled, filename, root) {
405
+ if (compiled.ignore.length === 0) return false;
406
+ const rel = toRelative(filename, root);
407
+ return compiled.ignore.some((matcher) => matcher.test(rel));
408
+ }
409
+ const plugin = definePlugin({
410
+ meta: { name: "boundaries" },
411
+ rules: {
412
+ "element-types": defineRule({
413
+ meta: {
414
+ type: "problem",
415
+ docs: { description: "Enforce the configured cross-package dependency matrix (which element types may import which)." }
416
+ },
417
+ createOnce(context) {
418
+ return { ImportDeclaration(node) {
419
+ const filename = context.filename;
420
+ const root = rootFor(filename, context.cwd);
421
+ const compiled = compiledFor(context, root);
422
+ if (!compiled) return;
423
+ if (isIgnored(compiled, filename, root)) return;
424
+ const fromType = classifyPath(filename, root, compiled.elements);
425
+ if (fromType === null) return;
426
+ const specifier = node.source.value;
427
+ const toType = classifyTarget(specifier, filename, root, {
428
+ elements: compiled.elements,
429
+ workspaceScope: compiled.workspaceScope
430
+ });
431
+ if (toType === null) return;
432
+ const typeOnly = node.importKind === "type";
433
+ if (evaluate(fromType, toType, typeOnly, compiled.table).allowed) return;
434
+ if (compiled.default === "allow") return;
435
+ const message = compiled.messageFor(fromType, toType) ?? `'${fromType}' is not allowed to import '${toType}'.` + (typeOnly ? "" : ` (If this should be type-only, use \`import type\`.)`);
436
+ context.report({
437
+ message,
438
+ node
439
+ });
440
+ } };
441
+ }
442
+ }),
443
+ "no-unknown": defineRule({
444
+ meta: {
445
+ type: "problem",
446
+ docs: { description: "Disallow importing a workspace-scope specifier that resolves to no package (typo / nonexistent)." }
447
+ },
448
+ createOnce(context) {
449
+ return { ImportDeclaration(node) {
450
+ const filename = context.filename;
451
+ const root = rootFor(filename, context.cwd);
452
+ const compiled = compiledFor(context, root);
453
+ if (!compiled) return;
454
+ if (isIgnored(compiled, filename, root)) return;
455
+ if (classifyPath(filename, root, compiled.elements) === null) return;
456
+ const specifier = node.source.value;
457
+ if (!specifier.startsWith(compiled.workspaceScope)) return;
458
+ if (resolveSpecifierDir(specifier, getPackageIndex(root)) !== null) return;
459
+ context.report({
460
+ message: `Unknown workspace import: '${specifier}' resolves to no ${compiled.workspaceScope}* package (typo?).`,
461
+ node
462
+ });
463
+ } };
464
+ }
465
+ })
466
+ }
467
+ });
468
+ //#endregion
469
+ export { plugin as default };
470
+
471
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["dirname","dirname"],"sources":["../src/discover.ts","../src/engine.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["// GENERIC engine module — package discovery.\n//\n// Zero repo-specific constants live here. This is part of the extraction seam\n// for the standalone package: `discover.ts` + `engine.ts` form the reusable\n// engine; the boundary table is passed in.\n//\n// oxlint exposes NO module resolver to JS plugins (verified against oxlint\n// 1.69.0): a rule sees one file's AST + that file's path + config `settings`,\n// nothing cross-file. So we classify by PATH. To turn a bare workspace\n// specifier (e.g. `@scope/core`) into a directory, we read every workspace\n// `package.json`'s `name` once and build a name -> dir index.\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\nexport interface DiscoveredPackage {\n name: string;\n dir: string;\n}\n\n// Minimal shape we read off a parsed package.json. `JSON.parse` yields `any`;\n// narrowing through this type keeps the loose runtime checks below honest.\ninterface PackageJson {\n name?: unknown;\n workspaces?: unknown;\n}\n\n/**\n * Walk up from `startDir` to the monorepo root: the nearest ancestor whose\n * `package.json` declares `\"workspaces\"`. Falls back to `fallback` (typically\n * `context.cwd`) when no such ancestor exists.\n *\n * Keying off the file path (not cwd) is what makes classification\n * cwd-independent — running oxlint from a workspace subdir resolves the same\n * root as running from the repo root.\n */\nexport function findWorkspaceRoot(startDir: string, fallback: string): string {\n let dir = startDir;\n // Guard against symlink / root loops: stop when `dirname` stops changing.\n for (;;) {\n const pkgPath = join(dir, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as PackageJson;\n if (pkg && pkg.workspaces !== undefined) return dir;\n } catch {\n // Unreadable/!JSON package.json — keep walking up.\n }\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return fallback;\n}\n\n/**\n * Read the `workspaces` field of a root `package.json` and return the raw glob\n * patterns. Supports both the array form (`[\"apps/*\", ...]`) and the Bun/Yarn\n * object form (`{ packages: [...] }`). Returns `[]` on any problem.\n */\nfunction readWorkspaceGlobs(root: string): string[] {\n try {\n const pkg = JSON.parse(readFileSync(join(root, \"package.json\"), \"utf8\")) as PackageJson;\n const ws = pkg.workspaces;\n if (Array.isArray(ws)) return ws as string[];\n if (ws && typeof ws === \"object\" && Array.isArray((ws as { packages?: unknown }).packages)) {\n return (ws as { packages: string[] }).packages;\n }\n } catch {\n // fall through\n }\n return [];\n}\n\n/**\n * Expand a single workspace glob into concrete package directories.\n *\n * Only the two shapes monorepos actually use are handled: a literal directory\n * (`packages/core`) and a single-level wildcard (`packages/*`, `apps/*`). A\n * `**` glob is treated as the directory before it. No glob engine needed.\n */\nfunction expandGlob(root: string, glob: string): string[] {\n const normalized = glob.replaceAll(\"\\\\\", \"/\").replace(/\\/+$/, \"\");\n const starIdx = normalized.indexOf(\"*\");\n if (starIdx === -1) {\n const dir = resolve(root, normalized);\n return existsSync(dir) ? [dir] : [];\n }\n // Parent of the first wildcard segment, e.g. \"packages/*\" -> \"packages\".\n const beforeStar = normalized.slice(0, starIdx);\n const parentRel = beforeStar.replace(/\\/[^/]*$/, \"\").replace(/\\/$/, \"\");\n const parentDir = resolve(root, parentRel);\n if (!existsSync(parentDir)) return [];\n const out: string[] = [];\n for (const entry of readdirSync(parentDir)) {\n if (entry.startsWith(\".\")) continue;\n const child = join(parentDir, entry);\n try {\n if (statSync(child).isDirectory()) out.push(child);\n } catch {\n // ignore unreadable entries\n }\n }\n return out;\n}\n\n/**\n * Discover every workspace package under `root`: read each candidate dir's\n * `package.json` and pair its `name` with its absolute directory.\n */\nexport function discoverPackages(root: string): DiscoveredPackage[] {\n const packages: DiscoveredPackage[] = [];\n const seen = new Set<string>();\n for (const glob of readWorkspaceGlobs(root)) {\n for (const dir of expandGlob(root, glob)) {\n if (seen.has(dir)) continue;\n seen.add(dir);\n try {\n const pkg = JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\")) as PackageJson;\n if (pkg && typeof pkg.name === \"string\") {\n packages.push({ name: pkg.name, dir });\n }\n } catch {\n // No/!JSON package.json in this dir — not a package; skip.\n }\n }\n }\n return packages;\n}\n\n/**\n * Build a `packageName -> absoluteDir` map, memoized per monorepo root so the\n * filesystem scan runs once across all linted files (mirrors oxlint's\n * once-per-process `createOnce` design).\n */\nconst indexCache = new Map<string, Map<string, string>>();\n\nexport function getPackageIndex(root: string): Map<string, string> {\n const cached = indexCache.get(root);\n if (cached) return cached;\n const index = new Map<string, string>();\n for (const { name, dir } of discoverPackages(root)) index.set(name, dir);\n indexCache.set(root, index);\n return index;\n}\n\n/**\n * Resolve a bare package specifier to its package directory via the index.\n *\n * Handles subpath specifiers (`@scope/pkg/sub`) by matching the longest\n * package name that the specifier equals or starts with (`name + \"/\"`). The\n * matched package's directory is returned; subpath refinement (mapping\n * `@scope/pkg/sub` to a sub-element) is intentionally NOT done here — callers\n * classify the returned dir. Today only bare names are used.\n */\nexport function resolveSpecifierDir(specifier: string, index: Map<string, string>): string | null {\n // Exact package name.\n const exact = index.get(specifier);\n if (exact) return exact;\n // Longest-prefix match for subpath specifiers (`@scope/pkg/sub`).\n let bestDir: string | null = null;\n let bestLen = -1;\n for (const [name, dir] of index) {\n const prefix = name + \"/\";\n if (specifier.startsWith(prefix) && name.length > bestLen) {\n bestDir = dir;\n bestLen = name.length;\n }\n }\n return bestDir;\n}\n\n// Re-export for callers that build paths relative to a file.\nexport { dirname, resolve, sep };\n","// GENERIC engine module — path classification + matrix evaluation.\n//\n// Zero repo-specific constants live here. Everything repo-specific (the element\n// list, the allow-matrix, the type-only carve-out) is passed in via a `table`\n// object. This is the extraction seam for the standalone package: `engine.ts` +\n// `discover.ts` are the reusable engine.\n//\n// Classification is by FILE PATH, because oxlint exposes no module resolver to\n// JS plugins (verified against oxlint 1.69.0). Both ends of an import are\n// classified to an element type from the path alone:\n// - importing file -> `context.filename` (absolute)\n// - imported target -> relative spec resolved against the file dir, OR a bare\n// workspace specifier mapped to its package dir.\n\nimport { dirname, getPackageIndex, resolve, resolveSpecifierDir } from \"./discover.js\";\n\nexport interface Element {\n type: string;\n test: (relPath: string) => boolean;\n}\n\nexport interface BoundaryTable {\n /** Ordered, specific-before-parent. First matching element wins. */\n ELEMENTS: Element[];\n /** `fromType -> Set(allowed toType)` for VALUE imports. */\n ALLOW: Record<string, Set<string>>;\n /** `fromType -> Set(allowed toType)` permitted ONLY for `import type` edges. */\n TYPE_ONLY_ALLOW: Record<string, Set<string>>;\n}\n\n/** Options for {@link classifyTarget}. */\nexport interface ClassifyTargetOptions {\n /** Ordered element list. */\n elements: Element[];\n /**\n * Scope prefix marking workspace packages (e.g. `\"@scope/\"`). Bare specifiers\n * outside this scope are treated as external and ignored.\n */\n workspaceScope: string;\n}\n\n/**\n * Normalize an absolute path to forward slashes and strip the monorepo-root\n * prefix, yielding a root-relative path the element tests match against\n * (e.g. `apps/api/src/http/app.ts`). This mirrors the old\n * `eslint-plugin-boundaries` `mode:\"full\"` behavior, which matched patterns\n * against root-relative paths.\n *\n * @param absPath absolute path\n * @param root absolute monorepo root\n * @returns root-relative, forward-slashed path\n */\nexport function toRelative(absPath: string, root: string): string {\n const normAbs = absPath.replaceAll(\"\\\\\", \"/\");\n const normRoot = root.replaceAll(\"\\\\\", \"/\").replace(/\\/+$/, \"\");\n if (normAbs === normRoot) return \"\";\n if (normAbs.startsWith(normRoot + \"/\")) return normAbs.slice(normRoot.length + 1);\n return normAbs; // outside the root — return as-is; element tests just won't match\n}\n\n/**\n * Classify an absolute path to an element type, or null if it matches no\n * element. The first matching element in `ELEMENTS` wins, so the table MUST be\n * ordered specific-before-parent.\n *\n * @param absPath absolute path to classify\n * @param root absolute monorepo root\n * @param elements ordered element list\n */\nexport function classifyPath(absPath: string, root: string, elements: Element[]): string | null {\n const rel = toRelative(absPath, root);\n if (!rel) return null;\n for (const element of elements) {\n if (element.test(rel)) return element.type;\n }\n return null;\n}\n\n/**\n * Classify a bare workspace specifier (`@scope/pkg[/sub]`) to an element type\n * by resolving it to its package directory and classifying that dir.\n *\n * @param specifier import specifier as written\n * @param root absolute monorepo root\n * @param elements ordered element list\n */\nexport function classifySpecifier(\n specifier: string,\n root: string,\n elements: Element[],\n): string | null {\n const dir = resolveSpecifierDir(specifier, getPackageIndex(root));\n if (!dir) return null;\n return classifyPath(dir, root, elements);\n}\n\n/**\n * Resolve an import specifier (relative or bare workspace) to the element type\n * of its target. Returns null for external deps (`zod`, `hono`, …) and for\n * anything that classifies to no element.\n *\n * @param specifier import specifier as written (`node.source.value`)\n * @param fromFile absolute path of the importing file\n * @param root absolute monorepo root\n * @param options elements + workspace scope\n */\nexport function classifyTarget(\n specifier: string,\n fromFile: string,\n root: string,\n { elements, workspaceScope }: ClassifyTargetOptions,\n): string | null {\n if (specifier.startsWith(\".\")) {\n const abs = resolve(dirname(fromFile), specifier);\n return classifyPath(abs, root, elements);\n }\n if (specifier.startsWith(workspaceScope)) {\n return classifySpecifier(specifier, root, elements);\n }\n return null; // external dependency — not a boundary edge\n}\n\nexport interface Verdict {\n allowed: boolean;\n reason: \"self\" | \"value-allow\" | \"type-allow\" | \"disallow\";\n}\n\n/**\n * Evaluate whether `fromType` may import `toType` given the import kind.\n *\n * Rules:\n * - self (`toType === fromType`) -> allowed (intra-package).\n * - `ALLOW[fromType]` contains `toType` -> allowed (value edge).\n * - `typeOnly` AND `TYPE_ONLY_ALLOW[from]`\n * contains `toType` -> allowed (type-only carve-out).\n * - otherwise -> disallowed.\n *\n * @param fromType element type of the importing file\n * @param toType element type of the imported target\n * @param typeOnly whether the import is `import type` (declaration-level)\n * @param table the repo-specific table (passed in)\n */\nexport function evaluate(\n fromType: string,\n toType: string,\n typeOnly: boolean,\n table: BoundaryTable,\n): Verdict {\n if (toType === fromType) return { allowed: true, reason: \"self\" };\n if (table.ALLOW[fromType]?.has(toType)) return { allowed: true, reason: \"value-allow\" };\n if (typeOnly && table.TYPE_ONLY_ALLOW[fromType]?.has(toType)) {\n return { allowed: true, reason: \"type-allow\" };\n }\n return { allowed: false, reason: \"disallow\" };\n}\n","// Config layer - translate the user's `settings.boundaries` into exactly the\n// structures the generic engine consumes.\n//\n// The engine (engine.ts) is table-free: it takes an ordered `Element[]`, an\n// `ALLOW` map and a `TYPE_ONLY_ALLOW` map and evaluates edges. It knows nothing\n// about the public config schema. THIS module is the only place that schema is\n// understood: it validates the raw config once (throwing actionable errors),\n// compiles element/ignore patterns to matchers, and normalizes the directional\n// rules into the engine's two allow-maps. Pure - no oxlint imports, no\n// filesystem; the Step-4 rule layer wires the compiled output into oxlint.\n\nimport type { BoundaryTable, Element } from \"./engine.js\";\n// Re-export the engine verdict type so Step 4 can import everything it needs\n// from one place.\nexport type { BoundaryTable, Element, Verdict } from \"./engine.js\";\n\n// A single `settings.boundaries.elements[]` entry, post-validation.\nexport interface ElementConfig {\n type: string;\n pattern: string;\n}\n\n// A single `settings.boundaries.rules[]` entry, post-validation.\nexport interface RuleConfig {\n from: string;\n allow: string[];\n importKind?: \"value\" | \"type\";\n message?: string;\n}\n\n// The compiled, validated config the Step-4 rules consume. `elements` and\n// `table.ELEMENTS` are the SAME array (the engine classifies against\n// `table.ELEMENTS`); both are exposed for call-site clarity.\nexport interface CompiledBoundaries {\n // Ordered, first-match-wins element matchers.\n elements: Element[];\n // The engine table: ELEMENTS + ALLOW + TYPE_ONLY_ALLOW.\n table: BoundaryTable;\n // Verdict for an edge no rule covers. Applied by the Step-4 rule, not the engine.\n default: \"allow\" | \"disallow\";\n // Compiled matchers for files to skip entirely (root-relative path tests).\n ignore: Element[];\n // Scope prefix marking workspace-internal bare specifiers, e.g. \"@scope/\".\n workspaceScope: string;\n // Per-edge custom message, or undefined when none was configured.\n messageFor: (fromType: string, toType: string) => string | undefined;\n}\n\n// Options for compileConfig.\nexport interface CompileConfigOptions {\n // Workspace package names (from `discoverPackages`) used to DERIVE\n // `workspaceScope` when the config omits it. Ignored when the config sets\n // `workspaceScope` explicitly.\n packageNames?: string[];\n}\n\n// Thrown for any invalid `settings.boundaries`. The message is actionable.\nexport class BoundariesConfigError extends Error {\n constructor(message: string) {\n super(`Invalid settings.boundaries: ${message}`);\n this.name = \"BoundariesConfigError\";\n }\n}\n\nfunction fail(message: string): never {\n throw new BoundariesConfigError(message);\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// --- pattern compilation ----------------------------------------------------\n\n// Escape every RegExp metacharacter in a literal path fragment.\nfunction escapeLiteral(literal: string): string {\n return literal.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// Compile an ELEMENT pattern (a root-relative path prefix) to a matcher.\n//\n// Supported shapes - the small set monorepos actually use:\n// - \"dir/<star><star>\" -> match the dir itself AND anything under it.\n// - \"dir\" -> identical (a bare directory).\n//\n// Both compile to `^<dir>(/|$)` (gotcha G6). The `(/|$)` boundary is the whole\n// point: a bare workspace specifier (`@scope/core`) resolves to a package dir\n// with NO trailing segment, so a \"must be followed by slash\" test would\n// silently classify it as null and no-op the rule. `(/|$)` matches the dir\n// itself while still preventing `apps/api` from matching `apps/api-client`.\n//\n// Anything else (a mid-segment wildcard, a `*.ext` tail, a leading globstar\n// segment) is rejected - element patterns are prefixes, not file globs.\nfunction compileElementPattern(pattern: string, label: string): (relPath: string) => boolean {\n const normalized = pattern.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//, \"\");\n // Strip an optional trailing globstar segment (or bare slash) for the prefix.\n const dir = normalized.replace(/\\/\\*\\*$/, \"\").replace(/\\/+$/, \"\");\n if (dir === \"\" || dir === \"**\") {\n fail(`${label} pattern ${JSON.stringify(pattern)} is too broad - name a directory.`);\n }\n if (dir.includes(\"*\")) {\n fail(\n `${label} pattern ${JSON.stringify(pattern)} is unsupported - element patterns must be a ` +\n `directory (\"dir\") or a directory tree (\"dir\" + slash + globstar), with no wildcards ` +\n `inside the path.`,\n );\n }\n const re = new RegExp(`^${escapeLiteral(dir)}(/|$)`);\n return (relPath) => re.test(relPath);\n}\n\n// Compile an IGNORE glob to a matcher. Ignore patterns are file globs (they\n// match a whole root-relative path), so they support a broader vocabulary than\n// element prefixes:\n// - globstar -> any characters, any depth (`.*`)\n// - `*` -> any characters within a single segment (`[^/]*`)\n// - a trailing \"dir/\" + globstar also matches the bare dir (`(/|$)`\n// boundary), matching element semantics so an ignore and an element written\n// the same way agree.\n//\n// Everything else is treated as a literal.\nfunction compileIgnorePattern(pattern: string): (relPath: string) => boolean {\n const normalized = pattern.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//, \"\");\n // Special-case a trailing globstar segment so `packages/core/<star><star>`\n // also matches the bare dir `packages/core` (same `(/|$)` boundary as\n // element patterns).\n const trailingTree = normalized.endsWith(\"/**\");\n const body = trailingTree ? normalized.slice(0, -3) : normalized;\n\n let source = \"\";\n let i = 0;\n while (i < body.length) {\n if (body.startsWith(\"**/\", i)) {\n // Leading globstar segment - any number of leading segments (incl. none).\n source += \"(?:.*/)?\";\n i += 3;\n } else if (body.startsWith(\"**\", i)) {\n source += \".*\";\n i += 2;\n } else if (body[i] === \"*\") {\n source += \"[^/]*\";\n i += 1;\n } else {\n source += escapeLiteral(body[i] as string);\n i += 1;\n }\n }\n const tail = trailingTree ? \"(/|$)\" : \"$\";\n const re = new RegExp(`^${source}${tail}`);\n return (relPath) => re.test(relPath);\n}\n\n// --- validation -------------------------------------------------------------\n\nfunction validateElements(raw: unknown): ElementConfig[] {\n if (!Array.isArray(raw)) fail(\"`elements` is required and must be an array.\");\n if (raw.length === 0) fail(\"`elements` must be a non-empty array.\");\n const seen = new Set<string>();\n const out: ElementConfig[] = [];\n raw.forEach((entry, idx) => {\n if (!isObject(entry)) fail(`elements[${idx}] must be an object.`);\n const { type, pattern } = entry;\n if (typeof type !== \"string\" || type === \"\") {\n fail(`elements[${idx}] is missing a string \\`type\\`.`);\n }\n if (typeof pattern !== \"string\" || pattern === \"\") {\n fail(`elements[${idx}] (type ${JSON.stringify(type)}) is missing a string \\`pattern\\`.`);\n }\n if (seen.has(type)) fail(`duplicate element type ${JSON.stringify(type)}.`);\n seen.add(type);\n out.push({ type, pattern });\n });\n return out;\n}\n\nfunction validateRules(raw: unknown, knownTypes: Set<string>): RuleConfig[] {\n if (!Array.isArray(raw)) fail(\"`rules` is required and must be an array.\");\n const out: RuleConfig[] = [];\n raw.forEach((entry, idx) => {\n if (!isObject(entry)) fail(`rules[${idx}] must be an object.`);\n const { from, allow, importKind, message } = entry;\n if (typeof from !== \"string\" || from === \"\") {\n fail(`rules[${idx}] is missing a string \\`from\\`.`);\n }\n if (!knownTypes.has(from)) {\n fail(`rules[${idx}].from references unknown element type ${JSON.stringify(from)}.`);\n }\n if (!Array.isArray(allow)) {\n fail(`rules[${idx}] (from ${JSON.stringify(from)}) is missing an \\`allow\\` array.`);\n }\n allow.forEach((to, j) => {\n if (typeof to !== \"string\" || to === \"\") {\n fail(`rules[${idx}].allow[${j}] must be a non-empty string.`);\n }\n if (!knownTypes.has(to)) {\n fail(`rules[${idx}].allow references unknown element type ${JSON.stringify(to)}.`);\n }\n });\n if (importKind !== undefined && importKind !== \"value\" && importKind !== \"type\") {\n fail(\n `rules[${idx}].importKind must be \"value\" or \"type\" (got ${JSON.stringify(importKind)}).`,\n );\n }\n if (message !== undefined && typeof message !== \"string\") {\n fail(`rules[${idx}].message must be a string.`);\n }\n out.push({\n from,\n allow: allow as string[],\n ...(importKind !== undefined ? { importKind: importKind as \"value\" | \"type\" } : {}),\n ...(message !== undefined ? { message: message as string } : {}),\n });\n });\n return out;\n}\n\n// --- workspaceScope ---------------------------------------------------------\n\n// Derive the workspace scope prefix shared by every package name, e.g.\n// `[\"@acme/core\", \"@acme/api\"]` -> `\"@acme/\"`. Returns null when the names do\n// not all share a single `@scope/` prefix (so the caller can fall back to the\n// explicit config field or fail loudly).\nexport function deriveWorkspaceScope(packageNames: string[]): string | null {\n const scopes = new Set<string>();\n for (const name of packageNames) {\n const match = /^(@[^/]+\\/)/.exec(name);\n if (!match) return null; // an unscoped package - no common scope\n scopes.add(match[1] as string);\n }\n if (scopes.size !== 1) return null;\n return [...scopes][0] as string;\n}\n\n// --- entry point ------------------------------------------------------------\n\n// Validate and compile a raw `settings.boundaries` object into the structures\n// the Step-4 rules consume. Throws BoundariesConfigError with an actionable\n// message on any invalid config.\nexport function compileConfig(\n raw: unknown,\n options: CompileConfigOptions = {},\n): CompiledBoundaries {\n if (!isObject(raw)) {\n fail(\"settings.boundaries must be an object.\");\n }\n\n const elementConfigs = validateElements(raw.elements);\n const knownTypes = new Set(elementConfigs.map((e) => e.type));\n const ruleConfigs = validateRules(raw.rules, knownTypes);\n\n // `default`.\n let dflt: \"allow\" | \"disallow\" = \"disallow\";\n if (raw.default !== undefined) {\n if (raw.default !== \"allow\" && raw.default !== \"disallow\") {\n fail(`\\`default\\` must be \"allow\" or \"disallow\" (got ${JSON.stringify(raw.default)}).`);\n }\n dflt = raw.default;\n }\n\n // `ignore`.\n let ignorePatterns: string[] = [];\n if (raw.ignore !== undefined) {\n if (!Array.isArray(raw.ignore)) fail(\"`ignore` must be an array of glob strings.\");\n raw.ignore.forEach((p, idx) => {\n if (typeof p !== \"string\" || p === \"\") fail(`ignore[${idx}] must be a non-empty string.`);\n });\n ignorePatterns = raw.ignore as string[];\n }\n\n // `workspaceScope`: explicit field wins; otherwise derive from package names.\n let workspaceScope: string;\n if (raw.workspaceScope !== undefined) {\n if (typeof raw.workspaceScope !== \"string\" || raw.workspaceScope === \"\") {\n fail('`workspaceScope` must be a non-empty string (e.g. \"@acme/\").');\n }\n workspaceScope = raw.workspaceScope;\n } else {\n const derived = deriveWorkspaceScope(options.packageNames ?? []);\n if (derived === null) {\n fail(\n \"`workspaceScope` is required and could not be derived from the workspace package \" +\n \"names. Set settings.boundaries.workspaceScope to your packages' shared scope \" +\n '(e.g. \"@acme/\").',\n );\n }\n workspaceScope = derived;\n }\n\n // Compile element matchers (ordered, first-match-wins preserved).\n const elements: Element[] = elementConfigs.map((e) => ({\n type: e.type,\n test: compileElementPattern(e.pattern, `elements[type ${JSON.stringify(e.type)}]`),\n }));\n\n // Compile ignore matchers - reuse the Element shape (type is a placeholder\n // label; only `test` is used by the rule).\n const ignore: Element[] = ignorePatterns.map((pattern, idx) => ({\n type: `ignore[${idx}]`,\n test: compileIgnorePattern(pattern),\n }));\n\n // Normalize rules into ALLOW / TYPE_ONLY_ALLOW (union same-`from`, never\n // overwrite). Keep a per-edge message lookup.\n const ALLOW: Record<string, Set<string>> = {};\n const TYPE_ONLY_ALLOW: Record<string, Set<string>> = {};\n const messages = new Map<string, string>();\n const edgeKey = (from: string, to: string) => `${from} ${to}`;\n\n for (const rule of ruleConfigs) {\n const target = rule.importKind === \"type\" ? TYPE_ONLY_ALLOW : ALLOW;\n const set = (target[rule.from] ??= new Set<string>());\n for (const to of rule.allow) {\n set.add(to);\n if (rule.message !== undefined) messages.set(edgeKey(rule.from, to), rule.message);\n }\n }\n\n const table: BoundaryTable = { ELEMENTS: elements, ALLOW, TYPE_ONLY_ALLOW };\n\n return {\n elements,\n table,\n default: dflt,\n ignore,\n workspaceScope,\n messageFor: (fromType, toType) => messages.get(edgeKey(fromType, toType)),\n };\n}\n","// Public entry point for oxlint-plugin-boundaries.\n//\n// Wires the generic engine (engine.ts) and the config layer (config.ts) into two\n// oxlint jsPlugins rules:\n// - boundaries/element-types — enforce the configured allow-matrix (with the\n// type-only carve-out and the `default` verdict).\n// - boundaries/no-unknown — flag a workspace specifier that resolves to no\n// package (typo / deleted package).\n//\n// oxlint's plugin layer exposes NO module resolver, so both ends of an import are\n// classified by FILE PATH (see engine.ts / discover.ts). The element table and\n// allow-matrix come entirely from `settings.boundaries` (config.ts), which is\n// what makes this package generic — no repo-specific constants live here.\n//\n// Honored alpha-API gotchas (verified against oxlint / @oxlint/plugins 1.69.0):\n// G1 — `createOnce` runs ONCE across all files. `context.filename` is read\n// INSIDE the visitor, never in the `createOnce` body.\n// G2 — `report()` needs a node carrying `range`; we report on the real\n// `ImportDeclaration` node.\n// G5 — plugin name `boundaries`; rule ids `boundaries/element-types`,\n// `boundaries/no-unknown`.\n\nimport { definePlugin, defineRule } from \"@oxlint/plugins\";\nimport type { Context, ESTree } from \"@oxlint/plugins\";\n\nimport { classifyPath, classifyTarget, evaluate, toRelative } from \"./engine.js\";\nimport {\n discoverPackages,\n findWorkspaceRoot,\n getPackageIndex,\n resolveSpecifierDir,\n} from \"./discover.js\";\nimport { dirname } from \"node:path\";\nimport { compileConfig, type CompiledBoundaries } from \"./config.js\";\n\n// oxlint exposes its AST node types under the `ESTree` namespace, not as\n// top-level exports. Alias the one node we visit.\ntype ImportDeclaration = ESTree.ImportDeclaration;\n\n// Resolve the monorepo root for a file, memoized by the file's directory. Keying\n// off the file path (not cwd) keeps classification cwd-independent.\nconst rootCache = new Map<string, string>();\nfunction rootFor(filename: string, cwd: string): string {\n const fileDir = dirname(filename);\n const cached = rootCache.get(fileDir);\n if (cached !== undefined) return cached;\n const root = findWorkspaceRoot(fileDir, cwd);\n rootCache.set(fileDir, root);\n return root;\n}\n\n// Compile `settings.boundaries` once per workspace root, memoized. Returns null\n// when no `boundaries` config is present (rule is then a no-op). A present but\n// INVALID config throws BoundariesConfigError (actionable) — not swallowed.\nconst compiledCache = new Map<string, CompiledBoundaries | null>();\nfunction compiledFor(context: Context, root: string): CompiledBoundaries | null {\n const cached = compiledCache.get(root);\n if (cached !== undefined) return cached;\n\n const settings = context.settings as { boundaries?: unknown };\n if (settings.boundaries === undefined || settings.boundaries === null) {\n compiledCache.set(root, null);\n return null;\n }\n const packageNames = discoverPackages(root).map((p) => p.name);\n const compiled = compileConfig(settings.boundaries, { packageNames });\n compiledCache.set(root, compiled);\n return compiled;\n}\n\n// True when the importing file should be skipped entirely (matches an `ignore`\n// pattern), computed against the file's root-relative path.\nfunction isIgnored(compiled: CompiledBoundaries, filename: string, root: string): boolean {\n if (compiled.ignore.length === 0) return false;\n const rel = toRelative(filename, root);\n return compiled.ignore.some((matcher) => matcher.test(rel));\n}\n\nconst plugin = definePlugin({\n meta: { name: \"boundaries\" },\n rules: {\n \"element-types\": defineRule({\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Enforce the configured cross-package dependency matrix (which element types may import which).\",\n },\n },\n createOnce(context: Context) {\n // G1: nothing that reads context.filename here — only inside the visitor.\n return {\n ImportDeclaration(node: ImportDeclaration) {\n const filename = context.filename;\n const root = rootFor(filename, context.cwd);\n const compiled = compiledFor(context, root);\n if (!compiled) return; // rule enabled but not configured -> no-op\n if (isIgnored(compiled, filename, root)) return;\n\n const fromType = classifyPath(filename, root, compiled.elements);\n if (fromType === null) return; // importing file is not a known element\n\n const specifier = node.source.value;\n const toType = classifyTarget(specifier, filename, root, {\n elements: compiled.elements,\n workspaceScope: compiled.workspaceScope,\n });\n if (toType === null) return; // external dep / unclassifiable — not an edge\n\n const typeOnly = node.importKind === \"type\";\n const verdict = evaluate(fromType, toType, typeOnly, compiled.table);\n // The engine never applies `default`; do it here. An uncovered edge\n // (verdict.allowed === false with reason \"disallow\") is permitted\n // when default is \"allow\".\n if (verdict.allowed) return;\n if (compiled.default === \"allow\") return;\n\n // G2: report on the real ImportDeclaration node (carries range).\n const custom = compiled.messageFor(fromType, toType);\n const message =\n custom ??\n `'${fromType}' is not allowed to import '${toType}'.` +\n (typeOnly ? \"\" : ` (If this should be type-only, use \\`import type\\`.)`);\n context.report({ message, node });\n },\n };\n },\n }),\n\n \"no-unknown\": defineRule({\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Disallow importing a workspace-scope specifier that resolves to no package (typo / nonexistent).\",\n },\n },\n createOnce(context: Context) {\n return {\n ImportDeclaration(node: ImportDeclaration) {\n const filename = context.filename;\n const root = rootFor(filename, context.cwd);\n const compiled = compiledFor(context, root);\n if (!compiled) return;\n if (isIgnored(compiled, filename, root)) return;\n\n // Only meaningful from a recognized element.\n if (classifyPath(filename, root, compiled.elements) === null) return;\n\n const specifier = node.source.value;\n if (!specifier.startsWith(compiled.workspaceScope)) return; // external/relative\n\n const dir = resolveSpecifierDir(specifier, getPackageIndex(root));\n if (dir !== null) return; // resolves to a real workspace package\n\n // G2: report on the real node.\n context.report({\n message: `Unknown workspace import: '${specifier}' resolves to no ${compiled.workspaceScope}* package (typo?).`,\n node,\n });\n },\n };\n },\n }),\n },\n});\n\nexport default plugin;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "oxlint-plugin-boundaries",
3
+ "version": "0.1.0",
4
+ "description": "Config-driven cross-package / element-type boundaries enforcement for oxlint — a resolver-less JS plugin. Each repo declares its own element table and allow-matrix in `.oxlintrc.json` settings.",
5
+ "keywords": [
6
+ "architecture",
7
+ "boundaries",
8
+ "dependency-rules",
9
+ "lint",
10
+ "monorepo",
11
+ "oxc",
12
+ "oxlint",
13
+ "oxlint-plugin"
14
+ ],
15
+ "homepage": "https://github.com/paulcedrick/oxlint-plugin-boundaries#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/paulcedrick/oxlint-plugin-boundaries/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Paul Cedrick Artigo",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/paulcedrick/oxlint-plugin-boundaries.git"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "type": "module",
31
+ "sideEffects": false,
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "default": "./dist/index.js"
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "type-check": "tsc --noEmit",
45
+ "lint": "oxlint",
46
+ "lint:fix": "oxlint --fix",
47
+ "fmt": "oxfmt",
48
+ "fmt:check": "oxfmt --check",
49
+ "test": "bun test",
50
+ "prepublishOnly": "bun run build"
51
+ },
52
+ "dependencies": {
53
+ "@oxlint/plugins": "^1.69.0"
54
+ },
55
+ "devDependencies": {
56
+ "@arethetypeswrong/core": "^0.18.3",
57
+ "@types/bun": "latest",
58
+ "oxfmt": "^0.54.0",
59
+ "oxlint": "1.69.0",
60
+ "publint": "^0.3.21",
61
+ "tsdown": "^0.22.2",
62
+ "typescript": "^6.0.0"
63
+ },
64
+ "peerDependencies": {
65
+ "oxlint": ">=1.69.0 <2"
66
+ },
67
+ "engines": {
68
+ "node": ">=20.19"
69
+ }
70
+ }