oxlint-plugin-arch 0.12.1 → 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 +10 -0
- package/dist/index.js +54 -13
- package/dist/preset/preset-detect.d.ts +1 -1
- package/dist/preset/preset.js +116 -91
- package/dist/preset/preset.utils.d.ts +2 -0
- package/dist/types/boundaries.types.d.ts +1 -0
- package/dist/types/preset.types.d.ts +3 -0
- package/dist/types/schemas.types.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.12.1
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -1111,15 +1111,19 @@ function boundariesContains(directory, candidate) {
|
|
|
1111
1111
|
function boundariesStripExtension(path) {
|
|
1112
1112
|
return path.replace(/\.[cm]?[jt]sx?$/, "");
|
|
1113
1113
|
}
|
|
1114
|
-
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) {
|
|
1115
1119
|
const relative = path.slice(appRoot.length + 1);
|
|
1116
1120
|
if (relative.startsWith("types/") && relative.endsWith(".types.ts")) {
|
|
1117
1121
|
return relative.slice("types/".length).split(".")[0];
|
|
1118
1122
|
}
|
|
1119
|
-
if (!
|
|
1123
|
+
if (!boundariesContains(services, path) || path === services) {
|
|
1120
1124
|
return;
|
|
1121
1125
|
}
|
|
1122
|
-
const parts =
|
|
1126
|
+
const parts = path.slice(services.length + 1).split("/");
|
|
1123
1127
|
return parts.length > 1 ? parts[0] : parts[0].split(".")[0].replace(/-(?:action|query|driver)$/, "");
|
|
1124
1128
|
}
|
|
1125
1129
|
function boundariesPrivateDomain({ options, file, target, reExport, roots, owner }) {
|
|
@@ -1128,8 +1132,8 @@ function boundariesPrivateDomain({ options, file, target, reExport, roots, owner
|
|
|
1128
1132
|
return;
|
|
1129
1133
|
}
|
|
1130
1134
|
const targetOwner = roots.find((path) => boundariesContains(path, target));
|
|
1131
|
-
const fileDomain = owner ? boundariesDomain(file, owner) : undefined;
|
|
1132
|
-
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;
|
|
1133
1137
|
const sameDomain = owner && owner === targetOwner && fileDomain && fileDomain === targetDomain;
|
|
1134
1138
|
const targetStem = boundariesStripExtension(target);
|
|
1135
1139
|
const publicSurface = boundariesPublicSurface.test(targetStem) && !boundariesActionOrQuery.test(targetStem);
|
|
@@ -1144,11 +1148,11 @@ function boundariesPrivateDomain({ options, file, target, reExport, roots, owner
|
|
|
1144
1148
|
}
|
|
1145
1149
|
function boundariesPortableLib(check) {
|
|
1146
1150
|
const { options, file, source, target, typeOnly, reExport, roots, web, backendImport } = check;
|
|
1147
|
-
if (!options.portable || !web || !boundariesContains(`${web}/lib`, file)) {
|
|
1151
|
+
if (!options.portable || !web || !boundariesContains(`${web}/lib`, file) || boundariesServiceFolder(options, web) === `${web}/lib`) {
|
|
1148
1152
|
return;
|
|
1149
1153
|
}
|
|
1150
1154
|
const portableTarget = target.startsWith("/") ? resolve2(target).replaceAll("\\", "/") : target;
|
|
1151
|
-
const appService = roots.some((path) => boundariesContains(
|
|
1155
|
+
const appService = roots.some((path) => boundariesContains(boundariesServiceFolder(options, path), portableTarget));
|
|
1152
1156
|
const backendTarget = options.backend.some((path) => boundariesContains(path, portableTarget));
|
|
1153
1157
|
const appPackage = boundariesMatchesImport(options.appPackages, source);
|
|
1154
1158
|
if (backendImport || backendTarget || appService || appPackage) {
|
|
@@ -1175,6 +1179,7 @@ var boundariesImports = defineRule7({
|
|
|
1175
1179
|
backend: { type: "array", items: { type: "string" } },
|
|
1176
1180
|
packages: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
|
|
1177
1181
|
roles: { type: "array", items: { type: "string" } },
|
|
1182
|
+
services: { type: "array", items: { type: "string" } },
|
|
1178
1183
|
portable: { type: "boolean" },
|
|
1179
1184
|
backendPackages: { type: "array", items: { type: "string" } },
|
|
1180
1185
|
entrypoints: { type: "array", items: { type: "string" } },
|
|
@@ -1200,11 +1205,14 @@ var boundariesImports = defineRule7({
|
|
|
1200
1205
|
const imported = { options, file, source, target, typeOnly, reExport, roots, owner, web, backendImport };
|
|
1201
1206
|
const inPackage = [options.packages].flat().some((path) => path && boundariesContains(path, file));
|
|
1202
1207
|
const appTarget = backendImport || options.web.some((path) => boundariesContains(path, target)) || boundariesMatchesImport(options.appPackages, source);
|
|
1203
|
-
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)));
|
|
1204
1212
|
const reason = [
|
|
1205
1213
|
web && backendImport && !options.entrypoints.includes(source) ? "frontend code uses only public backend entry points" : undefined,
|
|
1206
1214
|
inPackage && appTarget ? "shared packages do not import app internals" : undefined,
|
|
1207
|
-
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,
|
|
1208
1216
|
domainContract ? "domain types/constants use type-only imports from implementations" : undefined,
|
|
1209
1217
|
file.endsWith(".client.ts") && !typeOnly && boundariesMatchesImport(options.uiPackages ?? ["react", "react-dom"], source) ? "client services are React-free; use hooks for React behavior" : undefined,
|
|
1210
1218
|
boundariesPrivateDomain(imported),
|
|
@@ -1245,9 +1253,9 @@ var boundariesImports = defineRule7({
|
|
|
1245
1253
|
}
|
|
1246
1254
|
},
|
|
1247
1255
|
TSImportType(node) {
|
|
1248
|
-
const
|
|
1256
|
+
const options = { ...boundariesImportsDefaults, ...optionsFirst(context) };
|
|
1249
1257
|
const file = context.filename.replaceAll("\\", "/");
|
|
1250
|
-
if (
|
|
1258
|
+
if (options.portable && options.web.some((path) => file.startsWith(`${path}/lib/`) && boundariesServiceFolder(options, path) !== `${path}/lib`)) {
|
|
1251
1259
|
check(node, node.source.value, true);
|
|
1252
1260
|
}
|
|
1253
1261
|
}
|
|
@@ -5394,6 +5402,7 @@ var schemasInline = defineRule46({
|
|
|
5394
5402
|
import { defineRule as defineRule47 } from "@oxlint/plugins";
|
|
5395
5403
|
var schemasLocalImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
|
|
5396
5404
|
var schemasLocalConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
|
|
5405
|
+
var schemasLocalCalls = ["config", "prettifyError", "treeifyError", "flattenError", "formatError"];
|
|
5397
5406
|
var schemasLocal = defineRule47({
|
|
5398
5407
|
meta: {
|
|
5399
5408
|
type: "problem",
|
|
@@ -5404,6 +5413,7 @@ var schemasLocal = defineRule47({
|
|
|
5404
5413
|
properties: {
|
|
5405
5414
|
packages: { type: "array", items: { type: "string" } },
|
|
5406
5415
|
namespaces: { type: "array", items: { type: "string" } },
|
|
5416
|
+
calls: { type: "array", items: { type: "string" } },
|
|
5407
5417
|
allow: { type: "array", items: { type: "string" } },
|
|
5408
5418
|
patterns: { type: "array", items: { type: "string" } },
|
|
5409
5419
|
message: { type: "string" },
|
|
@@ -5421,8 +5431,15 @@ var schemasLocal = defineRule47({
|
|
|
5421
5431
|
createOnce(context) {
|
|
5422
5432
|
let constructors = new Set;
|
|
5423
5433
|
let packages = new Set;
|
|
5434
|
+
let calls = new Set;
|
|
5435
|
+
let pending = [];
|
|
5436
|
+
let schemaUses = new Set;
|
|
5437
|
+
let callUses = new Set;
|
|
5424
5438
|
let importMessage = schemasLocalImportMessage;
|
|
5425
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
|
+
}
|
|
5426
5443
|
function isPackage(source) {
|
|
5427
5444
|
return packages.has(source) || [...packages].some((name) => source.startsWith(`${name}/`));
|
|
5428
5445
|
}
|
|
@@ -5447,6 +5464,10 @@ var schemasLocal = defineRule47({
|
|
|
5447
5464
|
const filename = context.filename.replaceAll("\\", "/");
|
|
5448
5465
|
constructors = new Set(options.namespaces);
|
|
5449
5466
|
packages = new Set(options.packages);
|
|
5467
|
+
calls = new Set(options.calls ?? schemasLocalCalls);
|
|
5468
|
+
pending = [];
|
|
5469
|
+
schemaUses = new Set;
|
|
5470
|
+
callUses = new Set;
|
|
5450
5471
|
importMessage = options.importMessage ?? options.message ?? schemasLocalImportMessage;
|
|
5451
5472
|
constructionMessage = options.constructionMessage ?? options.message ?? schemasLocalConstructionMessage;
|
|
5452
5473
|
if ((options.allow ?? []).some((suffix) => filename.endsWith(suffix)) || (options.patterns ?? []).some((pattern) => new RegExp(pattern).test(filename))) {
|
|
@@ -5464,7 +5485,27 @@ var schemasLocal = defineRule47({
|
|
|
5464
5485
|
for (const name of locals) {
|
|
5465
5486
|
constructors.add(name);
|
|
5466
5487
|
}
|
|
5467
|
-
|
|
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
|
+
}
|
|
5468
5509
|
},
|
|
5469
5510
|
ExportNamedDeclaration: checkReexport,
|
|
5470
5511
|
ExportAllDeclaration: checkReexport,
|
|
@@ -5473,7 +5514,7 @@ var schemasLocal = defineRule47({
|
|
|
5473
5514
|
while (root.type === "MemberExpression") {
|
|
5474
5515
|
root = root.object;
|
|
5475
5516
|
}
|
|
5476
|
-
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)) {
|
|
5477
5518
|
return;
|
|
5478
5519
|
}
|
|
5479
5520
|
context.report({
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type * as PresetTypes from '../types/preset.types.ts';
|
|
2
|
-
export declare function presetRegistryFile(root: string, source: string, configured: PresetTypes.PresetBackendOptions['registry']): string | undefined;
|
|
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;
|
package/dist/preset/preset.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
4
|
// src/preset/groups/backend.ts
|
|
5
|
-
import { posix as
|
|
5
|
+
import { posix as posix3, resolve as resolve3 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/preset/preset-detect.ts
|
|
8
8
|
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
9
|
-
import { matchesGlob, posix, resolve as resolve2 } from "node:path";
|
|
9
|
+
import { matchesGlob, posix as posix2, resolve as resolve2 } from "node:path";
|
|
10
10
|
|
|
11
11
|
// src/preset/preset-recipes.ts
|
|
12
12
|
var presetUi = { react: true, kit: true, tailwind: true };
|
|
@@ -464,7 +464,7 @@ var presetGroupOptions = {
|
|
|
464
464
|
deny: { level: "error", imports: {}, calls: {} },
|
|
465
465
|
externals: { level: "error", packages: presetExternals }
|
|
466
466
|
};
|
|
467
|
-
var presetFolderKeys = ["severity", "source", "rules"];
|
|
467
|
+
var presetFolderKeys = ["severity", "source", "services", "rules"];
|
|
468
468
|
var presetA11yRules = [
|
|
469
469
|
"alt-text",
|
|
470
470
|
"anchor-has-content",
|
|
@@ -514,7 +514,7 @@ var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-
|
|
|
514
514
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
515
515
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
516
516
|
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
517
|
-
var presetRegistryFiles = ["
|
|
517
|
+
var presetRegistryFiles = ["core/core.services.ts", "service.ts"];
|
|
518
518
|
var presetRegistrySkip = ["brand", "database", "db", "math", "security", "tools"];
|
|
519
519
|
var presetServerEntries = ["rpc", "server", "security", "services"];
|
|
520
520
|
var presetOperationImports = ["*/*-action.*", "*/*-query.*", "*/actions/*", "*/queries/*"];
|
|
@@ -528,12 +528,15 @@ var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do no
|
|
|
528
528
|
|
|
529
529
|
// src/preset/preset.utils.ts
|
|
530
530
|
import { readFileSync, statSync } from "node:fs";
|
|
531
|
-
import { resolve } from "node:path";
|
|
531
|
+
import { posix, resolve } from "node:path";
|
|
532
532
|
class PresetError extends Error {
|
|
533
533
|
}
|
|
534
534
|
function presetScopes(roots, suffix = `**/${presetExtensions}`) {
|
|
535
535
|
return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
|
|
536
536
|
}
|
|
537
|
+
function presetServiceScopes(folders, sources, suffix = `**/${presetExtensions}`) {
|
|
538
|
+
return presetScopes(sources.map((source) => folders[source] ?? posix.join(source, "services")), suffix);
|
|
539
|
+
}
|
|
537
540
|
function presetOverride(files, rules, excludeFiles = []) {
|
|
538
541
|
return {
|
|
539
542
|
files,
|
|
@@ -611,7 +614,7 @@ function presetRuntime(project, settings, framework, rule) {
|
|
|
611
614
|
options.rpc = project.rpcClients.map((path) => resolve(project.root, path));
|
|
612
615
|
}
|
|
613
616
|
return [
|
|
614
|
-
presetOverride([...
|
|
617
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, [unit.source], "**/*.{ts,tsx}"), ...presetScopes([unit.source], "lib/**/*.{ts,tsx}")], {
|
|
615
618
|
[rule]: ["error", options]
|
|
616
619
|
})
|
|
617
620
|
];
|
|
@@ -631,6 +634,16 @@ function presetIsDirectory(path) {
|
|
|
631
634
|
return false;
|
|
632
635
|
}
|
|
633
636
|
}
|
|
637
|
+
function presetServicesFolder(root, source, configured, fallback = true) {
|
|
638
|
+
if (configured !== undefined) {
|
|
639
|
+
if (!configured.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
640
|
+
throw new PresetError(`services \`${configured}\` must be a literal relative path.`);
|
|
641
|
+
}
|
|
642
|
+
return posix.join(source, configured);
|
|
643
|
+
}
|
|
644
|
+
const lib = fallback && !presetIsDirectory(resolve(root, source, "services")) && presetIsDirectory(resolve(root, source, "lib"));
|
|
645
|
+
return posix.join(source, lib ? "lib" : "services");
|
|
646
|
+
}
|
|
634
647
|
function presetIsFile(path) {
|
|
635
648
|
try {
|
|
636
649
|
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
@@ -644,35 +657,36 @@ function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
|
644
657
|
if (!presetIsDirectory(resolve2(root, base))) {
|
|
645
658
|
return [];
|
|
646
659
|
}
|
|
647
|
-
return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) =>
|
|
660
|
+
return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix2.join(base, name));
|
|
648
661
|
}
|
|
649
662
|
var presetNamedSkip = ["node_modules"];
|
|
650
663
|
function presetSource(root, directory, configured) {
|
|
651
664
|
if (configured !== undefined) {
|
|
652
|
-
return
|
|
665
|
+
return posix2.join(directory, configured);
|
|
653
666
|
}
|
|
654
|
-
return existsSync(resolve2(root, directory, "src")) ?
|
|
667
|
+
return existsSync(resolve2(root, directory, "src")) ? posix2.join(directory, "src") : directory;
|
|
655
668
|
}
|
|
656
|
-
function presetRegistryFile(root, source, configured) {
|
|
669
|
+
function presetRegistryFile(root, source, configured, services) {
|
|
657
670
|
if (configured === false) {
|
|
658
671
|
return;
|
|
659
672
|
}
|
|
660
673
|
const file = typeof configured === "string" ? configured : configured?.file;
|
|
661
674
|
if (file !== undefined) {
|
|
662
|
-
return
|
|
675
|
+
return posix2.normalize(file);
|
|
663
676
|
}
|
|
664
|
-
return presetRegistryFiles.map((path) =>
|
|
677
|
+
return [posix2.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix2.join(source, path))].find((path) => existsSync(resolve2(root, path)));
|
|
665
678
|
}
|
|
666
|
-
function presetDomains(root,
|
|
667
|
-
return presetDirectories(root,
|
|
679
|
+
function presetDomains(root, services) {
|
|
680
|
+
return presetDirectories(root, services, presetNamedSkip).map((path) => posix2.basename(path)).filter((name) => existsSync(resolve2(root, services, name, `${name}.service.ts`)));
|
|
668
681
|
}
|
|
669
|
-
function presetRecipe(root, directory, app,
|
|
682
|
+
function presetRecipe(root, directory, app, folder) {
|
|
670
683
|
const manifest = presetManifest(root, directory);
|
|
671
684
|
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
672
685
|
const has = (names) => names.some((name) => dependencies.has(name));
|
|
673
|
-
const source = presetSource(root, directory,
|
|
686
|
+
const source = presetSource(root, directory, folder.source);
|
|
674
687
|
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
675
|
-
const
|
|
688
|
+
const servicesFolder = presetServicesFolder(root, source, folder.services, app);
|
|
689
|
+
const services = resolve2(root, servicesFolder);
|
|
676
690
|
const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
|
|
677
691
|
const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
|
|
678
692
|
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
|
|
@@ -682,7 +696,7 @@ function presetRecipe(root, directory, app, configured) {
|
|
|
682
696
|
if (has(presetDetectDesktop)) {
|
|
683
697
|
return presetRecipes.desktop;
|
|
684
698
|
}
|
|
685
|
-
if (presetDomains(root,
|
|
699
|
+
if (presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
|
|
686
700
|
return presetRecipes.backend;
|
|
687
701
|
}
|
|
688
702
|
if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
|
|
@@ -720,7 +734,7 @@ function presetImportTargets(value) {
|
|
|
720
734
|
}
|
|
721
735
|
function presetSpecifiers(manifest, target, file = false) {
|
|
722
736
|
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
723
|
-
const path =
|
|
737
|
+
const path = posix2.normalize(mapped);
|
|
724
738
|
const parts = path.split("*");
|
|
725
739
|
const keyParts = key.split("*");
|
|
726
740
|
if (parts.length === 1 && keyParts.length === 1) {
|
|
@@ -758,13 +772,15 @@ function presetFilled(conventions, exists) {
|
|
|
758
772
|
function presetBackendConventions(root, key, value, denied) {
|
|
759
773
|
const manifest = presetManifest(root, key);
|
|
760
774
|
const source = presetSource(root, key, value.source);
|
|
761
|
-
const local =
|
|
762
|
-
const at = (path) => local === "." ? path :
|
|
775
|
+
const local = posix2.relative(key, source) || ".";
|
|
776
|
+
const at = (path) => local === "." ? path : posix2.join(local, path);
|
|
763
777
|
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
764
778
|
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
const
|
|
779
|
+
const servicesFolder = presetServicesFolder(root, source, value.services);
|
|
780
|
+
const servicesKey = posix2.relative(key, servicesFolder);
|
|
781
|
+
const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
|
|
782
|
+
const registry = registryFile === undefined ? undefined : posix2.relative(key, registryFile);
|
|
783
|
+
const services = presetSpecifiers(manifest, servicesKey);
|
|
768
784
|
const message = "Use the service API, not another service’s operations.";
|
|
769
785
|
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
770
786
|
const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.[cm]?[jt]sx?$/, ""), true).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
|
|
@@ -774,7 +790,7 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
774
790
|
Object.assign(serviceImports, entries);
|
|
775
791
|
}
|
|
776
792
|
const conventions = {
|
|
777
|
-
[
|
|
793
|
+
[servicesKey]: presetDeny(serviceImports),
|
|
778
794
|
[at("core")]: presetDeny(operations),
|
|
779
795
|
[at("rpc")]: presetDeny(registryImports),
|
|
780
796
|
[at("routes")]: presetDeny(registryImports),
|
|
@@ -783,24 +799,24 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
783
799
|
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
784
800
|
}
|
|
785
801
|
};
|
|
786
|
-
for (const domain of presetDirectories(root,
|
|
802
|
+
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix2.basename(path))) {
|
|
787
803
|
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
788
804
|
[`${prefix}/${pattern}`, false],
|
|
789
805
|
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
790
806
|
]));
|
|
791
|
-
conventions[
|
|
807
|
+
conventions[posix2.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
|
|
792
808
|
}
|
|
793
809
|
for (const tests of ["tests", "test"]) {
|
|
794
|
-
if (exists(
|
|
810
|
+
if (exists(posix2.join(tests, "kits"))) {
|
|
795
811
|
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
796
|
-
conventions[
|
|
812
|
+
conventions[posix2.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
797
813
|
}
|
|
798
814
|
}
|
|
799
815
|
const queue = ["."];
|
|
800
816
|
while (queue.length > 0) {
|
|
801
817
|
const folder = queue.shift() ?? ".";
|
|
802
|
-
for (const child of presetDirectories(root,
|
|
803
|
-
if (
|
|
818
|
+
for (const child of presetDirectories(root, posix2.join(key, folder)).map((path) => posix2.relative(key, path))) {
|
|
819
|
+
if (posix2.basename(child) === "migrations") {
|
|
804
820
|
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
805
821
|
} else if (child.split("/").length < 8) {
|
|
806
822
|
queue.push(child);
|
|
@@ -810,8 +826,8 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
810
826
|
return presetFilled(conventions, exists);
|
|
811
827
|
}
|
|
812
828
|
function presetWebConventions(root, key, value, servers) {
|
|
813
|
-
const source =
|
|
814
|
-
const at = (path) => source === "." ? path :
|
|
829
|
+
const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
|
|
830
|
+
const at = (path) => source === "." ? path : posix2.join(source, path);
|
|
815
831
|
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
816
832
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
817
833
|
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
@@ -856,7 +872,7 @@ function presetWorkspacePatterns(root, manifest) {
|
|
|
856
872
|
return patterns;
|
|
857
873
|
}
|
|
858
874
|
function presetWorkspaces(root, patterns) {
|
|
859
|
-
const clean = (pattern) =>
|
|
875
|
+
const clean = (pattern) => posix2.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
860
876
|
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
861
877
|
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
862
878
|
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
@@ -895,16 +911,16 @@ function presetGroupOff(folders, path, group) {
|
|
|
895
911
|
}
|
|
896
912
|
function presetDetect(root, folders) {
|
|
897
913
|
const manifest = presetManifest(root, ".");
|
|
898
|
-
const
|
|
914
|
+
const configured = (key) => {
|
|
899
915
|
const value = folders[key];
|
|
900
|
-
return value instanceof Object ? value
|
|
916
|
+
return value instanceof Object ? value : {};
|
|
901
917
|
};
|
|
902
918
|
const patterns = presetWorkspacePatterns(root, manifest);
|
|
903
919
|
const directories = presetWorkspaces(root, patterns);
|
|
904
|
-
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"),
|
|
905
|
-
const rootRecipe = presetRecipe(root, ".", true,
|
|
920
|
+
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configured(directory))]));
|
|
921
|
+
const rootRecipe = presetRecipe(root, ".", true, configured("."));
|
|
906
922
|
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
907
|
-
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".",
|
|
923
|
+
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
|
|
908
924
|
if (patterns.length === 0 ? single : rootApp) {
|
|
909
925
|
detected["."] = rootRecipe;
|
|
910
926
|
}
|
|
@@ -912,7 +928,7 @@ function presetDetect(root, folders) {
|
|
|
912
928
|
detected.scripts = presetRecipes.cli;
|
|
913
929
|
}
|
|
914
930
|
const keys = Object.keys(folders);
|
|
915
|
-
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") &&
|
|
931
|
+
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix2.dirname(key) === user.slice(0, -2)));
|
|
916
932
|
const merged = { ".": presetDeny(presetRootImports) };
|
|
917
933
|
for (const [key, value] of Object.entries(detected)) {
|
|
918
934
|
if (!covered(key)) {
|
|
@@ -953,11 +969,11 @@ function presetDetect(root, folders) {
|
|
|
953
969
|
}
|
|
954
970
|
if (value.backend) {
|
|
955
971
|
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
956
|
-
add(path === "." ? key :
|
|
972
|
+
add(path === "." ? key : posix2.join(key, path), folder);
|
|
957
973
|
}
|
|
958
974
|
} else if (value.tanstack || value.next) {
|
|
959
975
|
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
960
|
-
add(path === "." ? key :
|
|
976
|
+
add(path === "." ? key : posix2.join(key, path), folder);
|
|
961
977
|
}
|
|
962
978
|
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
963
979
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
@@ -985,9 +1001,9 @@ var backendGroup = (project, settings) => {
|
|
|
985
1001
|
}
|
|
986
1002
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
987
1003
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
988
|
-
const file = configured.file === undefined ? undefined :
|
|
1004
|
+
const file = configured.file === undefined ? undefined : posix3.normalize(configured.file);
|
|
989
1005
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
990
|
-
const found = presetRegistryFile(project.root, unit.source, undefined);
|
|
1006
|
+
const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
|
|
991
1007
|
return found ? [{ file: found, unit }] : [];
|
|
992
1008
|
}) : [{ file, unit: backendUnits.find((unit) => unit.key === "." || file.startsWith(`${unit.key}/`)) }];
|
|
993
1009
|
for (const registry of registries) {
|
|
@@ -996,7 +1012,7 @@ var backendGroup = (project, settings) => {
|
|
|
996
1012
|
}
|
|
997
1013
|
const options = {
|
|
998
1014
|
registry: resolve3(project.root, registry.file),
|
|
999
|
-
services: resolve3(project.root, registry.unit.
|
|
1015
|
+
services: resolve3(project.root, registry.unit.services),
|
|
1000
1016
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
1001
1017
|
};
|
|
1002
1018
|
if (configured.object !== undefined) {
|
|
@@ -1014,7 +1030,7 @@ var basicsGroup = (project, settings) => {
|
|
|
1014
1030
|
return [
|
|
1015
1031
|
...settings.trivial ? [
|
|
1016
1032
|
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
|
|
1017
|
-
presetOverride([...
|
|
1033
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")], { "arch/code-trivial-functions": ["error", { ...trivial, services: true }] }, ui)
|
|
1018
1034
|
] : [],
|
|
1019
1035
|
...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
|
|
1020
1036
|
...settings.switch ? [presetOverride(project.backendFiles, { "arch/code-switch": ["error", { message: "Use ts-pattern in backend application code." }] })] : [],
|
|
@@ -1045,8 +1061,8 @@ var denyGroup = (project, settings) => {
|
|
|
1045
1061
|
continue;
|
|
1046
1062
|
}
|
|
1047
1063
|
const files = [
|
|
1048
|
-
...
|
|
1049
|
-
...
|
|
1064
|
+
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1065
|
+
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
1050
1066
|
];
|
|
1051
1067
|
const reads = Object.fromEntries(writes.map((name) => [name, "Query files only read; move writes to an action."]));
|
|
1052
1068
|
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
|
|
@@ -1151,6 +1167,7 @@ var importsGroup = (project, settings) => {
|
|
|
1151
1167
|
backend: absolute(project.backend),
|
|
1152
1168
|
packages: absolute(project.packages),
|
|
1153
1169
|
roles: project.suffixes,
|
|
1170
|
+
services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
|
|
1154
1171
|
portable: true,
|
|
1155
1172
|
aliases: presetAliases(project.root, settings.aliases),
|
|
1156
1173
|
appPackages: importsPackageNames(project, ["web", "backend"]),
|
|
@@ -1163,18 +1180,18 @@ var importsGroup = (project, settings) => {
|
|
|
1163
1180
|
};
|
|
1164
1181
|
|
|
1165
1182
|
// src/preset/groups/layout.ts
|
|
1166
|
-
import { posix as
|
|
1183
|
+
import { posix as posix4 } from "node:path";
|
|
1167
1184
|
var layoutGroup = (project, settings) => {
|
|
1168
1185
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
1169
|
-
const folderOperations =
|
|
1186
|
+
const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
|
|
1170
1187
|
const operationMessage = "Action/query files export exactly one named operation. Keep helpers used only by this operation private inside its function; do not export them. Move a helper to the module's .utils.ts only when it is genuinely shared, not merely to satisfy lint.";
|
|
1171
1188
|
const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
|
|
1172
1189
|
const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
|
|
1173
|
-
const frontendEntries = [...
|
|
1174
|
-
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path ===
|
|
1190
|
+
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
1191
|
+
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix4.join(path, "components")));
|
|
1175
1192
|
return [
|
|
1176
1193
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
1177
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1194
|
+
const folders = folder === "services" && operations.some((source) => posix4.join(source, "services") === path);
|
|
1178
1195
|
const registries = folder === "services" ? project.registries : [];
|
|
1179
1196
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
1180
1197
|
"arch/files-restricted": [
|
|
@@ -1207,7 +1224,7 @@ var layoutGroup = (project, settings) => {
|
|
|
1207
1224
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
1208
1225
|
presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], { "arch/files-restricted": ["error", { message: "Components use TSX; hooks use .ts files." }] }, ui),
|
|
1209
1226
|
presetOverride(presetScopes(web, "**/use-*.{ts,tsx}"), { "arch/files-restricted": ["error", { message: "Hooks belong in src/hooks." }] }, [...presetScopes(web, "hooks/**"), ...ui]),
|
|
1210
|
-
presetOverride(
|
|
1227
|
+
presetOverride(presetServiceScopes(project.serviceFolders, web, "**/*.{ts,tsx}"), {
|
|
1211
1228
|
"arch/files-restricted": [
|
|
1212
1229
|
"error",
|
|
1213
1230
|
{
|
|
@@ -1217,7 +1234,7 @@ var layoutGroup = (project, settings) => {
|
|
|
1217
1234
|
}, [
|
|
1218
1235
|
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
1219
1236
|
"**/*.rsc.tsx",
|
|
1220
|
-
...
|
|
1237
|
+
...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
|
|
1221
1238
|
...conceptFiles,
|
|
1222
1239
|
...project.colocated,
|
|
1223
1240
|
...testFiles
|
|
@@ -1227,22 +1244,22 @@ var layoutGroup = (project, settings) => {
|
|
|
1227
1244
|
"arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
|
|
1228
1245
|
}, [...utilities, ...testFiles, ...conceptFiles]),
|
|
1229
1246
|
presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
|
|
1230
|
-
...conceptFiles.map((pattern) => presetOverride(
|
|
1247
|
+
...conceptFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
|
|
1231
1248
|
"arch/backend-service-functions": "off",
|
|
1232
1249
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
1233
1250
|
}, testFiles)),
|
|
1234
|
-
presetOverride([...
|
|
1251
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, app, "**/*-{action,query}.*.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*-{action,query}.*.rsc.tsx")], { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
1235
1252
|
presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
1236
|
-
presetOverride(
|
|
1237
|
-
presetOverride(
|
|
1253
|
+
presetOverride(presetServiceScopes(project.serviceFolders, serviceRoots, "**/*.ts"), { "arch/files-domain-constants": "error" }, [...utilities, ...testFiles]),
|
|
1254
|
+
presetOverride(presetServiceScopes(project.serviceFolders, web, "**/*.{ts,tsx}"), { "arch/files-domain-constants": ["error", { data: true }] }, [
|
|
1238
1255
|
"**/*.constants.ts",
|
|
1239
1256
|
...testFiles
|
|
1240
1257
|
]),
|
|
1241
1258
|
presetOverride(frontendEntries, { "arch/files-domain-constants": ["error", { data: true, services: true }] }, testFiles),
|
|
1242
|
-
presetOverride([...
|
|
1259
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, serviceRoots, "**/*-{action,query}.*.ts"), ...folderOperations, ...frontendEntries], { "arch/code-object-params": ["error", { max: 2 }] }, testFiles),
|
|
1243
1260
|
presetOverride(frontendEntries, { "arch/code-object-params": ["error", { max: 2, services: true }] }, testFiles),
|
|
1244
|
-
presetOverride(
|
|
1245
|
-
presetOverride(
|
|
1261
|
+
presetOverride(presetServiceScopes(project.serviceFolders, serviceRoots, "**/*.service.ts"), { "arch/files-exports": ["error", { names: settings.service, stem: "before-first-dot", types: false }] }, testFiles),
|
|
1262
|
+
presetOverride(presetServiceScopes(project.serviceFolders, app.filter((path) => !operations.includes(path)), "**/{actions,queries}/**/*.ts"), {
|
|
1246
1263
|
"arch/files-restricted": ["error", { message: "Use domain-action.name.ts or domain-query.name.ts." }]
|
|
1247
1264
|
})
|
|
1248
1265
|
];
|
|
@@ -1270,7 +1287,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1270
1287
|
const { web, serviceRoots, app, ui, testFiles, components, hooks, operations } = project;
|
|
1271
1288
|
const flatApp = app.filter((path) => !operations.includes(path));
|
|
1272
1289
|
const folderApp = app.filter((path) => operations.includes(path));
|
|
1273
|
-
const frontendEntries = [...
|
|
1290
|
+
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
1274
1291
|
const ungroupedWeb = web.filter((path) => !project.layouts.some((scope) => scope.path === `${path}/components`));
|
|
1275
1292
|
const overrides = [];
|
|
1276
1293
|
const { files: filesPattern, exports: exportsPattern, unique } = settings;
|
|
@@ -1309,7 +1326,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1309
1326
|
add("files", presetScopes(app, "types/**/*.ts"), { "arch/naming-file": ["error", { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$", message: "Use domain.types.ts filenames." }] }, ["**/*.constants.ts"]);
|
|
1310
1327
|
add("exports", presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/naming-exports": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]);
|
|
1311
1328
|
add("exports", frontendEntries, { "arch/naming-exports": ["error", namingFrontendPrefix] }, testFiles);
|
|
1312
|
-
add("exports",
|
|
1329
|
+
add("exports", presetServiceScopes(project.serviceFolders, web, "**/*.constants.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "declarations", allow: "^\\*$" }] }, testFiles);
|
|
1313
1330
|
add("exports", presetScopes(app, "**/*.utils.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", allow: "^\\*$" }] }, ui);
|
|
1314
1331
|
add("files", hooks, {
|
|
1315
1332
|
"arch/naming-file": [
|
|
@@ -1324,7 +1341,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1324
1341
|
add("exports", components, { "arch/files-components-only": ["error", { filename: true, types: true, roles: ["illustration"] }] }, [...ui, ...testFiles]);
|
|
1325
1342
|
add("folders", presetScopes(ungroupedWeb, "components/**/*.tsx"), { "arch/naming-folder": ["error", { singularize: "trailing-s", separators: ["-"] }] }, [...ui, ...testFiles]);
|
|
1326
1343
|
for (const role of ["action", "query"]) {
|
|
1327
|
-
const files = [...
|
|
1344
|
+
const files = [...presetServiceScopes(project.serviceFolders, flatApp, `**/*-${role}.*.ts`), ...presetServiceScopes(project.serviceFolders, web, `**/*-${role}.*.rsc.tsx`)];
|
|
1328
1345
|
add("files", files, {
|
|
1329
1346
|
"arch/naming-file": ["error", { pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`, message: namingOperationMessage }]
|
|
1330
1347
|
}, testFiles);
|
|
@@ -1337,7 +1354,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1337
1354
|
}, testFiles);
|
|
1338
1355
|
}
|
|
1339
1356
|
for (const [role, folder, separator] of [["action", "actions", "."], ["query", "queries", "-"]]) {
|
|
1340
|
-
const files =
|
|
1357
|
+
const files = presetServiceScopes(project.serviceFolders, folderApp, `*/${folder}/*.ts`);
|
|
1341
1358
|
add("files", files, {
|
|
1342
1359
|
"arch/naming-file": [
|
|
1343
1360
|
"error",
|
|
@@ -1356,7 +1373,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1356
1373
|
}
|
|
1357
1374
|
for (const role of ["action", "query"]) {
|
|
1358
1375
|
for (const suffix of ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"]) {
|
|
1359
|
-
const files =
|
|
1376
|
+
const files = presetServiceScopes(project.serviceFolders, web, `**/*-${role}.*.${suffix}`);
|
|
1360
1377
|
add("files", files, {
|
|
1361
1378
|
"arch/naming-file": [
|
|
1362
1379
|
"error",
|
|
@@ -1369,7 +1386,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
1369
1386
|
add("exports", files, { "arch/naming-exports": ["error", namingOperationPrefix(role), namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] }, testFiles);
|
|
1370
1387
|
}
|
|
1371
1388
|
}
|
|
1372
|
-
add("exports",
|
|
1389
|
+
add("exports", presetServiceScopes(project.serviceFolders, web, "**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", namingFrontendPrefix, { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}", ...testFiles]);
|
|
1373
1390
|
if (typeof exportsPattern === "string") {
|
|
1374
1391
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
|
|
1375
1392
|
}
|
|
@@ -1503,7 +1520,7 @@ var shadcnGroup = (project) => {
|
|
|
1503
1520
|
|
|
1504
1521
|
// src/preset/groups/sql.ts
|
|
1505
1522
|
var sqlGroup = (project, settings) => [
|
|
1506
|
-
presetOverride(
|
|
1523
|
+
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
|
|
1507
1524
|
"arch/backend-sql-like": [
|
|
1508
1525
|
"error",
|
|
1509
1526
|
{ methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
|
|
@@ -1564,7 +1581,7 @@ var tanstackGroup = (project, settings) => {
|
|
|
1564
1581
|
patterns: project.baseImports
|
|
1565
1582
|
}
|
|
1566
1583
|
]
|
|
1567
|
-
},
|
|
1584
|
+
}, presetServiceScopes(project.serviceFolders, tanstackWeb, "**/*.rsc.{ts,tsx}")),
|
|
1568
1585
|
...presetRuntime(project, settings, "tanstack", "arch/boundaries-tanstack")
|
|
1569
1586
|
];
|
|
1570
1587
|
};
|
|
@@ -1713,11 +1730,11 @@ var typesGroup = (project, settings) => {
|
|
|
1713
1730
|
|
|
1714
1731
|
// src/preset/preset-folders.ts
|
|
1715
1732
|
import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
1716
|
-
import { posix as
|
|
1733
|
+
import { posix as posix7, resolve as resolve7 } from "node:path";
|
|
1717
1734
|
|
|
1718
1735
|
// src/preset/preset-stylesheet.ts
|
|
1719
1736
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
1720
|
-
import { posix as
|
|
1737
|
+
import { posix as posix5, resolve as resolve5 } from "node:path";
|
|
1721
1738
|
function presetTailwindEntry(css) {
|
|
1722
1739
|
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1723
1740
|
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
@@ -1734,9 +1751,9 @@ function presetStylesheet(root, directory) {
|
|
|
1734
1751
|
}
|
|
1735
1752
|
visited.add(real);
|
|
1736
1753
|
for (const name of readdirSync2(resolve5(root, folder))) {
|
|
1737
|
-
const path =
|
|
1754
|
+
const path = posix5.join(folder, name);
|
|
1738
1755
|
const full = resolve5(root, path);
|
|
1739
|
-
const depth =
|
|
1756
|
+
const depth = posix5.relative(directory, path).split("/").length;
|
|
1740
1757
|
if (presetIsDirectory(full)) {
|
|
1741
1758
|
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1742
1759
|
queue.push(path);
|
|
@@ -1747,7 +1764,7 @@ function presetStylesheet(root, directory) {
|
|
|
1747
1764
|
}
|
|
1748
1765
|
}
|
|
1749
1766
|
const rank = (path) => {
|
|
1750
|
-
const index = presetStylesheetNames.indexOf(
|
|
1767
|
+
const index = presetStylesheetNames.indexOf(posix5.basename(path, ".css"));
|
|
1751
1768
|
return index === -1 ? presetStylesheetNames.length : index;
|
|
1752
1769
|
};
|
|
1753
1770
|
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
@@ -1755,7 +1772,7 @@ function presetStylesheet(root, directory) {
|
|
|
1755
1772
|
|
|
1756
1773
|
// src/preset/preset-units.ts
|
|
1757
1774
|
import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
1758
|
-
import { posix as
|
|
1775
|
+
import { posix as posix6, resolve as resolve6 } from "node:path";
|
|
1759
1776
|
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1760
1777
|
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1761
1778
|
function presetUnitSource(root, key, kind, source) {
|
|
@@ -1763,9 +1780,9 @@ function presetUnitSource(root, key, kind, source) {
|
|
|
1763
1780
|
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1764
1781
|
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1765
1782
|
}
|
|
1766
|
-
return
|
|
1783
|
+
return posix6.join(key, source);
|
|
1767
1784
|
}
|
|
1768
|
-
return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key :
|
|
1785
|
+
return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix6.join(key, "src");
|
|
1769
1786
|
}
|
|
1770
1787
|
function presetSets(folder, name) {
|
|
1771
1788
|
return folder[name] !== undefined && folder[name] !== false;
|
|
@@ -1808,16 +1825,18 @@ function presetUnit(root, key, scope) {
|
|
|
1808
1825
|
const layout = {};
|
|
1809
1826
|
const settings = effective.layout;
|
|
1810
1827
|
if (settings) {
|
|
1811
|
-
const
|
|
1828
|
+
const services2 = settings.services ?? (kind === "console" ? false : "domain");
|
|
1812
1829
|
const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
|
|
1813
|
-
const modes = { services, hooks, components: settings.components ?? false };
|
|
1830
|
+
const modes = { services: services2, hooks, components: settings.components ?? false };
|
|
1814
1831
|
for (const [name, mode] of Object.entries(modes)) {
|
|
1815
1832
|
if (mode === "flat" || mode === "domain") {
|
|
1816
1833
|
layout[name] = mode;
|
|
1817
1834
|
}
|
|
1818
1835
|
}
|
|
1819
1836
|
}
|
|
1820
|
-
|
|
1837
|
+
const source = presetUnitSource(root, key, kind, folder.source);
|
|
1838
|
+
const services = presetServicesFolder(root, source, folder.services, kind === "web" || kind === "backend");
|
|
1839
|
+
return { key, scope, kind, framework, source, services, layout };
|
|
1821
1840
|
}
|
|
1822
1841
|
function presetUnits(root, scopes, ignored) {
|
|
1823
1842
|
const [rootScope] = scopes;
|
|
@@ -1844,17 +1863,17 @@ function presetUnits(root, scopes, ignored) {
|
|
|
1844
1863
|
return [presetUnit(root, scope.key, scope)];
|
|
1845
1864
|
}
|
|
1846
1865
|
const base = scope.key.slice(0, -2);
|
|
1847
|
-
return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) =>
|
|
1866
|
+
return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix6.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
|
|
1848
1867
|
});
|
|
1849
1868
|
}
|
|
1850
1869
|
|
|
1851
1870
|
// src/preset/preset-folders.ts
|
|
1852
1871
|
function presetNormalizeKey(key) {
|
|
1853
1872
|
const portable = key.replaceAll("\\", "/");
|
|
1854
|
-
if (!portable.trim() ||
|
|
1873
|
+
if (!portable.trim() || posix7.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1855
1874
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1856
1875
|
}
|
|
1857
|
-
const normalized =
|
|
1876
|
+
const normalized = posix7.normalize(portable).replace(/\/$/, "");
|
|
1858
1877
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1859
1878
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1860
1879
|
}
|
|
@@ -2004,6 +2023,10 @@ function presetProject(config) {
|
|
|
2004
2023
|
const units = presetUnits(root, scopes, ignored);
|
|
2005
2024
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
2006
2025
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
2026
|
+
const servicesKey = live.find((scope) => !unitKeys.has(scope.key) && scope.folder.services !== undefined);
|
|
2027
|
+
if (servicesKey) {
|
|
2028
|
+
throw new PresetError(`Set services on an app or package folder, not on \`${servicesKey.key}\`.`);
|
|
2029
|
+
}
|
|
2007
2030
|
if (misplaced) {
|
|
2008
2031
|
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
|
|
2009
2032
|
}
|
|
@@ -2020,6 +2043,7 @@ function presetProject(config) {
|
|
|
2020
2043
|
const libraryUnits = of("library");
|
|
2021
2044
|
const packageSources = libraryUnits.map((unit) => unit.source);
|
|
2022
2045
|
const app = [...web, ...backend, ...packageSources];
|
|
2046
|
+
const serviceFolders = Object.fromEntries(units.map((unit) => [unit.source, unit.services]));
|
|
2023
2047
|
const appFiles = presetScopes(app);
|
|
2024
2048
|
const webFiles = presetScopes(web);
|
|
2025
2049
|
const effective = live.map((scope) => scope.effective);
|
|
@@ -2033,9 +2057,9 @@ function presetProject(config) {
|
|
|
2033
2057
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
2034
2058
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
2035
2059
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
2036
|
-
const rpcClients = client === undefined ? webUnits.map((unit) =>
|
|
2060
|
+
const rpcClients = client === undefined ? webUnits.map((unit) => posix7.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
|
|
2037
2061
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
2038
|
-
const uiRoots = [...new Set([...web.map((path) =>
|
|
2062
|
+
const uiRoots = [...new Set([...web.map((path) => posix7.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
|
|
2039
2063
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
2040
2064
|
const stylesheet = (unit) => {
|
|
2041
2065
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -2077,20 +2101,21 @@ function presetProject(config) {
|
|
|
2077
2101
|
return [key];
|
|
2078
2102
|
}
|
|
2079
2103
|
const base = key.slice(0, -2);
|
|
2080
|
-
return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) =>
|
|
2104
|
+
return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
|
|
2081
2105
|
});
|
|
2082
2106
|
return {
|
|
2083
2107
|
root,
|
|
2108
|
+
serviceFolders,
|
|
2084
2109
|
scopes: live,
|
|
2085
2110
|
units,
|
|
2086
2111
|
suffixes,
|
|
2087
2112
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
2088
2113
|
orpc: {
|
|
2089
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2114
|
+
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix7.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
|
|
2090
2115
|
composers: listOf(orpcScopes.map((settings) => settings.composers)),
|
|
2091
2116
|
client: client ?? ""
|
|
2092
2117
|
},
|
|
2093
|
-
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path:
|
|
2118
|
+
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix7.join(unit.source, folder), folder, mode }))),
|
|
2094
2119
|
rpcClients,
|
|
2095
2120
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
2096
2121
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -2113,7 +2138,7 @@ function presetProject(config) {
|
|
|
2113
2138
|
...presetScopes(packageSources, `hooks/**/${presetExtensions}`)
|
|
2114
2139
|
],
|
|
2115
2140
|
backendFiles: presetScopes(backend),
|
|
2116
|
-
services:
|
|
2141
|
+
services: presetServiceScopes(serviceFolders, app),
|
|
2117
2142
|
utilities: ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"],
|
|
2118
2143
|
types: presetScopes(app, "types/**"),
|
|
2119
2144
|
uiRoots,
|
|
@@ -2127,7 +2152,7 @@ function presetProject(config) {
|
|
|
2127
2152
|
publicEntrypoints,
|
|
2128
2153
|
baseImports: presetBaseImports,
|
|
2129
2154
|
css: { ...webCss, ...kitCss },
|
|
2130
|
-
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry) ?? []),
|
|
2155
|
+
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
|
|
2131
2156
|
ignores: skipped.map((key) => `${key}/**`),
|
|
2132
2157
|
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
2133
2158
|
};
|
|
@@ -3,6 +3,7 @@ import type { OxlintOverride } from 'oxlint';
|
|
|
3
3
|
export declare class PresetError extends Error {
|
|
4
4
|
}
|
|
5
5
|
export declare function presetScopes(roots: string[], suffix?: string): string[];
|
|
6
|
+
export declare function presetServiceScopes(folders: Record<string, string>, sources: string[], suffix?: string): string[];
|
|
6
7
|
export declare function presetOverride(files: string[], rules: PresetTypes.PresetRules, excludeFiles?: string[]): OxlintOverride;
|
|
7
8
|
export declare function presetSeverity(rules: PresetTypes.PresetRules, level: PresetTypes.PresetLevel): PresetTypes.PresetRules;
|
|
8
9
|
export declare function presetPackageName(root: string, directory: string): string | undefined;
|
|
@@ -11,4 +12,5 @@ export declare function presetAliases(root: string, aliases: Record<string, stri
|
|
|
11
12
|
export declare function presetRuntime(project: PresetTypes.PresetProject, settings: Required<PresetTypes.PresetFrameworkOptions>, framework: PresetTypes.PresetFramework, rule: string): OxlintOverride[];
|
|
12
13
|
export declare function presetManifest(root: string, directory: string): PresetTypes.PresetManifest;
|
|
13
14
|
export declare function presetIsDirectory(path: string): boolean;
|
|
15
|
+
export declare function presetServicesFolder(root: string, source: string, configured?: string, fallback?: boolean): string;
|
|
14
16
|
export declare function presetIsFile(path: string): boolean;
|
|
@@ -153,6 +153,7 @@ export type PresetGroupName = keyof PresetGroups;
|
|
|
153
153
|
export interface PresetFolder extends PresetGroups {
|
|
154
154
|
severity?: PresetLevel;
|
|
155
155
|
source?: string;
|
|
156
|
+
services?: string;
|
|
156
157
|
rules?: PresetRules;
|
|
157
158
|
}
|
|
158
159
|
export type PresetFolders = Record<string, boolean | PresetFolder>;
|
|
@@ -224,6 +225,7 @@ export interface PresetUnit {
|
|
|
224
225
|
kind: PresetKind;
|
|
225
226
|
framework?: PresetFramework;
|
|
226
227
|
source: string;
|
|
228
|
+
services: string;
|
|
227
229
|
layout: Record<string, PresetLayoutMode>;
|
|
228
230
|
}
|
|
229
231
|
export interface PresetLayoutScope {
|
|
@@ -261,6 +263,7 @@ export interface PresetManifest {
|
|
|
261
263
|
}
|
|
262
264
|
export interface PresetProject {
|
|
263
265
|
root: string;
|
|
266
|
+
serviceFolders: Record<string, string>;
|
|
264
267
|
scopes: PresetScope[];
|
|
265
268
|
units: PresetUnit[];
|
|
266
269
|
suffixes: string[];
|