oxlint-plugin-arch 0.12.0 → 0.12.2
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 +36 -0
- package/dist/index.js +60 -15
- package/dist/preset/preset-detect.d.ts +1 -2
- package/dist/preset/preset-stylesheet.d.ts +1 -0
- package/dist/preset/preset-units.d.ts +2 -0
- package/dist/preset/preset.constants.d.ts +2 -0
- package/dist/preset/preset.js +617 -364
- package/dist/preset/preset.utils.d.ts +5 -0
- package/dist/types/boundaries.types.d.ts +1 -0
- package/dist/types/preset.types.d.ts +10 -1
- package/dist/types/schemas.types.d.ts +1 -0
- package/dist/vendor/dillon-anti-slop/index.js +32 -3
- package/package.json +3 -3
- package/src/vendor/dillon-anti-slop/UPSTREAM.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Preset: folder key `services` names an app's or package's service folder inside `source` (default `services`). An app or backend without `services/` uses `lib/` as its service folder, so a flat `lib/` gets the same layout, naming, import-boundary and registry checks; next to `services/`, `lib/` stays portable helpers. `arch/boundaries-imports` gets the matching `services` option.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `arch/schemas-local` no longer reports `z.config(...)` and zod's error formatters (`prettifyError`, `treeifyError`, `flattenError`, `formatError`), or an import used only for them; they build no schema. The new `calls` option sets that list.
|
|
12
|
+
|
|
13
|
+
## 0.12.1
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Preset: tests live in a `tests/` (or `test/`) folder. A `*.test.*` or `*.spec.*` file inside a unit's source is reported by `arch/files-restricted`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Preset detection reads `pnpm-workspace.yaml` (block or flow lists), workspace globs with `*` anywhere, `**`, `{a,b}` and `!exclude`, and peer dependencies (a UI package with React as a peer is `ui`). Packages and service domains named like output folders (`build`, `public`) are no longer skipped.
|
|
22
|
+
- Preset detection keeps a root app's or backend's recipe next to its workspace packages (when the root has its own source folder), gives a single root package its recipe (`library` included), and keeps the root unit next to a detected `scripts/` CLI. A folder with its own `package.json` is a unit with its own kind, also when nested in another workspace or listed through a `*` key, instead of inheriting the root app's kind.
|
|
23
|
+
- Preset: a unit without `src/` uses its own folder as source (a workspace Next app with `app/` at the package root).
|
|
24
|
+
- Preset detection honors a configured `source`, `backend.registry` file or `false`, expands `*` keys (with exact keys beside them, which also inherit the wildcard's `source`) before adding conventions, and understands import maps such as `"#/*": "./src/*.ts"`, `"#/*.ts": "./src/*.ts"`, exact and conditional entries.
|
|
25
|
+
- Preset detection: a service may import its own domain's operations; other domains stay banned through one `!(domain)` pattern per domain, and a ban you configure yourself is never lifted. A web app's or package's own aliases that overlap the backend's (`#/*`, `#/lib/*`) are no longer banned.
|
|
26
|
+
- Preset detection skips broken symlinks, follows symlinked folders, and drops conventions inside a `false` folder or for a group a parent sets to `false` (an exact key beats a `*` key at the same depth, and an exact key that revives a unit under a `false` wildcard keeps its conventions), instead of throwing.
|
|
27
|
+
- Preset: folder keys accept dots (`packages/my.lib`, `.agents`), and `apps/web/` or `./apps` match the detected `apps/web` and `apps`.
|
|
28
|
+
- Preset: nested group options merge with the inherited ones (`tests.mocking: { allow: [] }` keeps the parent's `preset` and `deny`, also when the parent used the `'strict'` shorthand).
|
|
29
|
+
- Preset: the registry file is exempt from the domain layout and its file and folder name checks; test files skip the domain, operation and frontend runtime file name checks (the `tests/` check below reports them once); operation files in `actions/` and `queries/` keep the single-export check; `layout.strict` accepts `.rsc.tsx`.
|
|
30
|
+
- Preset: Tailwind stylesheet detection ignores comments, strings and plugins such as `tailwindcss-animate`, accepts `@import url("tailwindcss")` with or without quotes, and follows symlinked folders.
|
|
31
|
+
- Preset: `dist`, `build`, `coverage` and `.output` are ignored at the project root and at each unit's root, not at any depth, so a `services/build/` domain is linted.
|
|
32
|
+
- `arch/boundaries-deny` import globs accept `!(a|b)`: one path segment that is none of the names.
|
|
33
|
+
- Preset: `layout.strict` checks domain file names at any depth under `services/<domain>/`, so a nested or misnamed file (`order/queries/order-query-list.ts`) is reported for its name, not only its folder. `layout.operations: 'folders'` keeps its own `actions/` and `queries/` names.
|
|
34
|
+
- Preset detection: a `next` or `vinext` dependency wins over a leftover `@tanstack/react-start` one.
|
|
35
|
+
- Preset detection: a package with `services/<domain>/<domain>.service.ts` gets the `backend` recipe without a backend framework dependency.
|
|
36
|
+
- Preset: `services/**/*.rsc.{ts,tsx}` files and frontend operation files (`*-action.*.client.ts` and the other runtime suffixes) keep the export prefix check next to their name pattern or template. Since 0.12.0 the later override replaced it; 0.11 ran both.
|
|
37
|
+
- `dillon-anti-slop/no-shape-in-symbol-names` reports only names you declare: not another API's property (`schema.shape`, `record.shape = 1`), a renamed destructured property or import, a qualified type read, a JSX member or attribute, or a renamed re-export from another module. A shorthand property reports once.
|
|
38
|
+
|
|
3
39
|
## 0.12.0
|
|
4
40
|
|
|
5
41
|
### Breaking: rebuilt package
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,12 @@ function optionsPatternsTest(patterns, value) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
function optionsGlob(pattern) {
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const negated = [];
|
|
36
|
+
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
37
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
|
|
38
|
+
return `\x02${negated.length - 1}\x03`;
|
|
39
|
+
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
40
|
+
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
37
41
|
}
|
|
38
42
|
function optionsPatternLabel(pattern) {
|
|
39
43
|
return pattern === undefined ? "" : [pattern].flat().filter((source) => source !== "").join(" | ");
|
|
@@ -1107,15 +1111,19 @@ function boundariesContains(directory, candidate) {
|
|
|
1107
1111
|
function boundariesStripExtension(path) {
|
|
1108
1112
|
return path.replace(/\.[cm]?[jt]sx?$/, "");
|
|
1109
1113
|
}
|
|
1110
|
-
function
|
|
1114
|
+
function boundariesServiceFolder(options, root) {
|
|
1115
|
+
const nested = [...options.web, ...options.backend].filter((other) => other.length > root.length && boundariesContains(root, other));
|
|
1116
|
+
return options.services?.find((folder) => folder.startsWith(`${root}/`) && !nested.some((other) => boundariesContains(other, folder))) ?? `${root}/services`;
|
|
1117
|
+
}
|
|
1118
|
+
function boundariesDomain(path, appRoot, services) {
|
|
1111
1119
|
const relative = path.slice(appRoot.length + 1);
|
|
1112
1120
|
if (relative.startsWith("types/") && relative.endsWith(".types.ts")) {
|
|
1113
1121
|
return relative.slice("types/".length).split(".")[0];
|
|
1114
1122
|
}
|
|
1115
|
-
if (!
|
|
1123
|
+
if (!boundariesContains(services, path) || path === services) {
|
|
1116
1124
|
return;
|
|
1117
1125
|
}
|
|
1118
|
-
const parts =
|
|
1126
|
+
const parts = path.slice(services.length + 1).split("/");
|
|
1119
1127
|
return parts.length > 1 ? parts[0] : parts[0].split(".")[0].replace(/-(?:action|query|driver)$/, "");
|
|
1120
1128
|
}
|
|
1121
1129
|
function boundariesPrivateDomain({ options, file, target, reExport, roots, owner }) {
|
|
@@ -1124,8 +1132,8 @@ function boundariesPrivateDomain({ options, file, target, reExport, roots, owner
|
|
|
1124
1132
|
return;
|
|
1125
1133
|
}
|
|
1126
1134
|
const targetOwner = roots.find((path) => boundariesContains(path, target));
|
|
1127
|
-
const fileDomain = owner ? boundariesDomain(file, owner) : undefined;
|
|
1128
|
-
const targetDomain = targetOwner ? boundariesDomain(target, targetOwner) : undefined;
|
|
1135
|
+
const fileDomain = owner ? boundariesDomain(file, owner, boundariesServiceFolder(options, owner)) : undefined;
|
|
1136
|
+
const targetDomain = targetOwner ? boundariesDomain(target, targetOwner, boundariesServiceFolder(options, targetOwner)) : undefined;
|
|
1129
1137
|
const sameDomain = owner && owner === targetOwner && fileDomain && fileDomain === targetDomain;
|
|
1130
1138
|
const targetStem = boundariesStripExtension(target);
|
|
1131
1139
|
const publicSurface = boundariesPublicSurface.test(targetStem) && !boundariesActionOrQuery.test(targetStem);
|
|
@@ -1140,11 +1148,11 @@ function boundariesPrivateDomain({ options, file, target, reExport, roots, owner
|
|
|
1140
1148
|
}
|
|
1141
1149
|
function boundariesPortableLib(check) {
|
|
1142
1150
|
const { options, file, source, target, typeOnly, reExport, roots, web, backendImport } = check;
|
|
1143
|
-
if (!options.portable || !web || !boundariesContains(`${web}/lib`, file)) {
|
|
1151
|
+
if (!options.portable || !web || !boundariesContains(`${web}/lib`, file) || boundariesServiceFolder(options, web) === `${web}/lib`) {
|
|
1144
1152
|
return;
|
|
1145
1153
|
}
|
|
1146
1154
|
const portableTarget = target.startsWith("/") ? resolve2(target).replaceAll("\\", "/") : target;
|
|
1147
|
-
const appService = roots.some((path) => boundariesContains(
|
|
1155
|
+
const appService = roots.some((path) => boundariesContains(boundariesServiceFolder(options, path), portableTarget));
|
|
1148
1156
|
const backendTarget = options.backend.some((path) => boundariesContains(path, portableTarget));
|
|
1149
1157
|
const appPackage = boundariesMatchesImport(options.appPackages, source);
|
|
1150
1158
|
if (backendImport || backendTarget || appService || appPackage) {
|
|
@@ -1171,6 +1179,7 @@ var boundariesImports = defineRule7({
|
|
|
1171
1179
|
backend: { type: "array", items: { type: "string" } },
|
|
1172
1180
|
packages: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
|
|
1173
1181
|
roles: { type: "array", items: { type: "string" } },
|
|
1182
|
+
services: { type: "array", items: { type: "string" } },
|
|
1174
1183
|
portable: { type: "boolean" },
|
|
1175
1184
|
backendPackages: { type: "array", items: { type: "string" } },
|
|
1176
1185
|
entrypoints: { type: "array", items: { type: "string" } },
|
|
@@ -1196,11 +1205,14 @@ var boundariesImports = defineRule7({
|
|
|
1196
1205
|
const imported = { options, file, source, target, typeOnly, reExport, roots, owner, web, backendImport };
|
|
1197
1206
|
const inPackage = [options.packages].flat().some((path) => path && boundariesContains(path, file));
|
|
1198
1207
|
const appTarget = backendImport || options.web.some((path) => boundariesContains(path, target)) || boundariesMatchesImport(options.appPackages, source);
|
|
1199
|
-
const
|
|
1208
|
+
const serviceFolders = roots.map((path) => boundariesServiceFolder(options, path));
|
|
1209
|
+
const serviceTarget = serviceFolders.find((folder) => boundariesContains(folder, target));
|
|
1210
|
+
const serviceFile = serviceFolders.find((folder) => boundariesContains(folder, file));
|
|
1211
|
+
const domainContract = /\.(?:types|constants)\.ts$/.test(file) && !typeOnly && (/\/(?:core\/|services\/.*(?:\.(?:service|utils)|-(?:action|query)\.|\/(?:actions|queries)\/))/.test(target) || serviceTarget !== undefined && /\/.*(?:\.(?:service|utils)|-(?:action|query)\.|\/(?:actions|queries)\/)/.test(target.slice(serviceTarget.length)));
|
|
1200
1212
|
const reason = [
|
|
1201
1213
|
web && backendImport && !options.entrypoints.includes(source) ? "frontend code uses only public backend entry points" : undefined,
|
|
1202
1214
|
inPackage && appTarget ? "shared packages do not import app internals" : undefined,
|
|
1203
|
-
file.includes("/services/") && /\/services\/service(?:\.ts)?$/.test(target) && !typeOnly ? "service dependencies are injected rather than read from the registry" : undefined,
|
|
1215
|
+
(file.includes("/services/") && /\/services\/service(?:\.ts)?$/.test(target) || serviceFile !== undefined && /^\/service(?:\.ts)?$/.test(target.slice(serviceFile.length))) && !typeOnly ? "service dependencies are injected rather than read from the registry" : undefined,
|
|
1204
1216
|
domainContract ? "domain types/constants use type-only imports from implementations" : undefined,
|
|
1205
1217
|
file.endsWith(".client.ts") && !typeOnly && boundariesMatchesImport(options.uiPackages ?? ["react", "react-dom"], source) ? "client services are React-free; use hooks for React behavior" : undefined,
|
|
1206
1218
|
boundariesPrivateDomain(imported),
|
|
@@ -1241,9 +1253,9 @@ var boundariesImports = defineRule7({
|
|
|
1241
1253
|
}
|
|
1242
1254
|
},
|
|
1243
1255
|
TSImportType(node) {
|
|
1244
|
-
const
|
|
1256
|
+
const options = { ...boundariesImportsDefaults, ...optionsFirst(context) };
|
|
1245
1257
|
const file = context.filename.replaceAll("\\", "/");
|
|
1246
|
-
if (
|
|
1258
|
+
if (options.portable && options.web.some((path) => file.startsWith(`${path}/lib/`) && boundariesServiceFolder(options, path) !== `${path}/lib`)) {
|
|
1247
1259
|
check(node, node.source.value, true);
|
|
1248
1260
|
}
|
|
1249
1261
|
}
|
|
@@ -5390,6 +5402,7 @@ var schemasInline = defineRule46({
|
|
|
5390
5402
|
import { defineRule as defineRule47 } from "@oxlint/plugins";
|
|
5391
5403
|
var schemasLocalImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
|
|
5392
5404
|
var schemasLocalConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
|
|
5405
|
+
var schemasLocalCalls = ["config", "prettifyError", "treeifyError", "flattenError", "formatError"];
|
|
5393
5406
|
var schemasLocal = defineRule47({
|
|
5394
5407
|
meta: {
|
|
5395
5408
|
type: "problem",
|
|
@@ -5400,6 +5413,7 @@ var schemasLocal = defineRule47({
|
|
|
5400
5413
|
properties: {
|
|
5401
5414
|
packages: { type: "array", items: { type: "string" } },
|
|
5402
5415
|
namespaces: { type: "array", items: { type: "string" } },
|
|
5416
|
+
calls: { type: "array", items: { type: "string" } },
|
|
5403
5417
|
allow: { type: "array", items: { type: "string" } },
|
|
5404
5418
|
patterns: { type: "array", items: { type: "string" } },
|
|
5405
5419
|
message: { type: "string" },
|
|
@@ -5417,8 +5431,15 @@ var schemasLocal = defineRule47({
|
|
|
5417
5431
|
createOnce(context) {
|
|
5418
5432
|
let constructors = new Set;
|
|
5419
5433
|
let packages = new Set;
|
|
5434
|
+
let calls = new Set;
|
|
5435
|
+
let pending = [];
|
|
5436
|
+
let schemaUses = new Set;
|
|
5437
|
+
let callUses = new Set;
|
|
5420
5438
|
let importMessage = schemasLocalImportMessage;
|
|
5421
5439
|
let constructionMessage = schemasLocalConstructionMessage;
|
|
5440
|
+
function isAllowedCall(node, member) {
|
|
5441
|
+
return member?.type === "MemberExpression" && member.object === node && !member.computed && member.property.type === "Identifier" && calls.has(member.property.name) && member.parent.type === "CallExpression" && member.parent.callee === member;
|
|
5442
|
+
}
|
|
5422
5443
|
function isPackage(source) {
|
|
5423
5444
|
return packages.has(source) || [...packages].some((name) => source.startsWith(`${name}/`));
|
|
5424
5445
|
}
|
|
@@ -5443,6 +5464,10 @@ var schemasLocal = defineRule47({
|
|
|
5443
5464
|
const filename = context.filename.replaceAll("\\", "/");
|
|
5444
5465
|
constructors = new Set(options.namespaces);
|
|
5445
5466
|
packages = new Set(options.packages);
|
|
5467
|
+
calls = new Set(options.calls ?? schemasLocalCalls);
|
|
5468
|
+
pending = [];
|
|
5469
|
+
schemaUses = new Set;
|
|
5470
|
+
callUses = new Set;
|
|
5446
5471
|
importMessage = options.importMessage ?? options.message ?? schemasLocalImportMessage;
|
|
5447
5472
|
constructionMessage = options.constructionMessage ?? options.message ?? schemasLocalConstructionMessage;
|
|
5448
5473
|
if ((options.allow ?? []).some((suffix) => filename.endsWith(suffix)) || (options.patterns ?? []).some((pattern) => new RegExp(pattern).test(filename))) {
|
|
@@ -5460,7 +5485,27 @@ var schemasLocal = defineRule47({
|
|
|
5460
5485
|
for (const name of locals) {
|
|
5461
5486
|
constructors.add(name);
|
|
5462
5487
|
}
|
|
5463
|
-
|
|
5488
|
+
if (locals.length === 0) {
|
|
5489
|
+
reportRuntimeImport(node, node.source.value);
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
pending.push({ node, locals });
|
|
5493
|
+
},
|
|
5494
|
+
Identifier(node) {
|
|
5495
|
+
if (!constructors.has(node.name) || node.parent.type.startsWith("Import")) {
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5498
|
+
if (isAllowedCall(node, node.parent)) {
|
|
5499
|
+
callUses.add(node.name);
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
schemaUses.add(node.name);
|
|
5503
|
+
},
|
|
5504
|
+
"Program:exit"() {
|
|
5505
|
+
const runtime = pending.filter(({ locals }) => locals.some((name) => schemaUses.has(name) || !callUses.has(name)));
|
|
5506
|
+
for (const { node } of runtime) {
|
|
5507
|
+
reportRuntimeImport(node, node.source.value);
|
|
5508
|
+
}
|
|
5464
5509
|
},
|
|
5465
5510
|
ExportNamedDeclaration: checkReexport,
|
|
5466
5511
|
ExportAllDeclaration: checkReexport,
|
|
@@ -5469,7 +5514,7 @@ var schemasLocal = defineRule47({
|
|
|
5469
5514
|
while (root.type === "MemberExpression") {
|
|
5470
5515
|
root = root.object;
|
|
5471
5516
|
}
|
|
5472
|
-
if (root.type !== "Identifier" || !constructors.has(root.name)) {
|
|
5517
|
+
if (root.type !== "Identifier" || !constructors.has(root.name) || node.callee.type === "MemberExpression" && node.callee.object === root && isAllowedCall(root, root.parent)) {
|
|
5473
5518
|
return;
|
|
5474
5519
|
}
|
|
5475
5520
|
context.report({
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type * as PresetTypes from '../types/preset.types.ts';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function presetRegistryFile(root: string, source: string, configured: PresetTypes.PresetBackendOptions['registry'], services: string): string | undefined;
|
|
3
3
|
export declare function presetDetect(root: string, folders: PresetTypes.PresetFolders): PresetTypes.PresetFolders;
|
|
4
|
-
export declare function presetStylesheet(root: string, directory: string): string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function presetStylesheet(root: string, directory: string): string | undefined;
|
|
@@ -41,6 +41,8 @@ export declare const presetExportPrefix: {
|
|
|
41
41
|
};
|
|
42
42
|
export declare const presetGeneratedFiles: string[];
|
|
43
43
|
export declare const presetBuiltinFileRoles: string[];
|
|
44
|
+
export declare const presetShorthandOptions: readonly [readonly ['tests', 'mocking', 'preset'], readonly ['backend', 'registry', 'file']];
|
|
45
|
+
export declare const presetOutputFolders: string[];
|
|
44
46
|
export declare const presetLiteralSegment: RegExp;
|
|
45
47
|
export declare const presetIllustrationPalette: string[];
|
|
46
48
|
export declare const presetGroupOrder: PresetTypes.PresetGroupName[];
|