oxlint-plugin-arch 0.12.6 → 0.13.1
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 +121 -0
- package/README.md +4 -4
- package/dist/index.js +1059 -306
- package/dist/preset/preset-effect.d.ts +3 -0
- package/dist/preset/preset-recipes.d.ts +10 -0
- package/dist/preset/preset.constants.d.ts +21 -2
- package/dist/preset/preset.d.ts +11 -1
- package/dist/preset/preset.js +715 -223
- package/dist/preset/preset.utils.d.ts +6 -0
- package/dist/rules/code/code-delegate.d.ts +1 -0
- package/dist/rules/layout/layout-type-arguments.d.ts +1 -0
- package/dist/rules/react/react-query-props.d.ts +1 -0
- package/dist/types/backend.types.d.ts +2 -3
- package/dist/types/boundaries.types.d.ts +26 -1
- package/dist/types/code.types.d.ts +7 -0
- package/dist/types/comments.types.d.ts +1 -0
- package/dist/types/dillon-anti-slop.types.d.ts +6 -0
- package/dist/types/effect.types.d.ts +1 -0
- package/dist/types/files.types.d.ts +3 -2
- package/dist/types/layout.types.d.ts +16 -0
- package/dist/types/naming.types.d.ts +4 -0
- package/dist/types/preset.types.d.ts +26 -24
- package/dist/types/react.types.d.ts +17 -0
- package/dist/types/types.types.d.ts +8 -0
- package/dist/utils/ast.utils.d.ts +1 -2
- package/dist/utils/boundaries.utils.d.ts +72 -4
- package/dist/utils/options.utils.d.ts +1 -0
- package/dist/vendor/dillon-anti-slop/index.js +38 -16
- package/package.json +15 -1
- package/src/vendor/dillon-anti-slop/UPSTREAM.md +3 -0
package/dist/preset/preset.js
CHANGED
|
@@ -2,38 +2,20 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/preset/preset.ts
|
|
5
|
-
import {
|
|
5
|
+
import { globSync as globSync2 } from "node:fs";
|
|
6
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
7
|
+
import { matchesGlob as matchesGlob2, posix as posix9 } from "node:path";
|
|
6
8
|
|
|
7
9
|
// src/preset/groups/backend.ts
|
|
8
|
-
import { posix as
|
|
10
|
+
import { posix as posix4, resolve as resolve4 } from "node:path";
|
|
9
11
|
|
|
10
12
|
// src/preset/preset-detect.ts
|
|
11
|
-
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
12
|
-
import { matchesGlob, posix as
|
|
13
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
14
|
+
import { matchesGlob, posix as posix3, resolve as resolve3 } from "node:path";
|
|
13
15
|
|
|
14
|
-
// src/preset/preset-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
18
|
-
next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
19
|
-
expo: { react: true, expo: true, schemas: true, forms: true },
|
|
20
|
-
backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
|
|
21
|
-
react: { react: true, tailwind: true, schemas: true },
|
|
22
|
-
desktop: { console: true },
|
|
23
|
-
library: { schemas: true },
|
|
24
|
-
effect: { schemas: true, effect: true },
|
|
25
|
-
ui: presetUi,
|
|
26
|
-
kit: presetUi,
|
|
27
|
-
cli: { console: true }
|
|
28
|
-
};
|
|
29
|
-
var presetTemplates = {
|
|
30
|
-
monorepo: {
|
|
31
|
-
"apps/web": presetRecipes.tanstack,
|
|
32
|
-
"apps/server": presetRecipes.backend,
|
|
33
|
-
"packages/*": presetRecipes.library,
|
|
34
|
-
scripts: presetRecipes.cli
|
|
35
|
-
}
|
|
36
|
-
};
|
|
16
|
+
// src/preset/preset-effect.ts
|
|
17
|
+
import { existsSync as existsSync2, readdirSync } from "node:fs";
|
|
18
|
+
import { posix as posix2, resolve as resolve2 } from "node:path";
|
|
37
19
|
|
|
38
20
|
// src/preset/preset.constants.ts
|
|
39
21
|
var presetExtensions = "*.{ts,tsx,mts,cts}";
|
|
@@ -347,20 +329,22 @@ var presetGeneratedFiles = [
|
|
|
347
329
|
"**/routeTree.gen.ts",
|
|
348
330
|
"**/paraglide/**"
|
|
349
331
|
];
|
|
350
|
-
var
|
|
351
|
-
"service",
|
|
352
|
-
"client",
|
|
353
|
-
"server",
|
|
354
|
-
"rsc",
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
332
|
+
var presetLayoutFiles = [
|
|
333
|
+
"*.service.ts",
|
|
334
|
+
"*.client.ts",
|
|
335
|
+
"*.server.ts",
|
|
336
|
+
"*.rsc.ts",
|
|
337
|
+
"*.rsc.tsx",
|
|
338
|
+
"*.utils.ts",
|
|
339
|
+
"*.types.ts",
|
|
340
|
+
"*.constants.ts",
|
|
341
|
+
"*.handler.ts",
|
|
342
|
+
"*.test.ts",
|
|
343
|
+
"*.spec.ts",
|
|
344
|
+
"*-action.*.ts",
|
|
345
|
+
"*-query.*.ts"
|
|
363
346
|
];
|
|
347
|
+
var presetLayoutFileName = /^!?[\w.*-]+$/;
|
|
364
348
|
var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
|
|
365
349
|
var presetOutputFolders = ["dist", "build", "coverage", ".output"];
|
|
366
350
|
var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
|
|
@@ -444,7 +428,7 @@ var presetExternals = [
|
|
|
444
428
|
"playwright-core"
|
|
445
429
|
];
|
|
446
430
|
var presetGroupOptions = {
|
|
447
|
-
layout: { level: "error", services: undefined, hooks: undefined, components: undefined,
|
|
431
|
+
layout: { level: "error", services: undefined, hooks: undefined, components: undefined, files: presetLayoutFiles, strict: false, operations: "flat", service: ["make{Domain}Service", "{Domain}Service"] },
|
|
448
432
|
limits: { level: "error", lines: 400, types: 1000, complexity: 32 },
|
|
449
433
|
style: { level: "error", quotes: "single", semicolons: true, multiline: true, padding: true, dashes: true },
|
|
450
434
|
basics: { level: "error", trivial: true, state: true, switch: true, negation: true },
|
|
@@ -460,17 +444,20 @@ var presetGroupOptions = {
|
|
|
460
444
|
tailwind: { level: "error", css: undefined, rem: 16 },
|
|
461
445
|
shadcn: { level: "error", ui: "@/components/ui", components: ["^@/components/"] },
|
|
462
446
|
kit: { level: "error" },
|
|
463
|
-
backend: { level: "error", exports: undefined, registry: undefined
|
|
464
|
-
orpc: { level: "error", public: [],
|
|
447
|
+
backend: { level: "error", exports: undefined, registry: undefined },
|
|
448
|
+
orpc: { level: "error", public: [], client: undefined },
|
|
465
449
|
schemas: { level: "error" },
|
|
466
|
-
forms: { level: "error"
|
|
467
|
-
sql: { level: "error"
|
|
468
|
-
effect: { level: "error", width:
|
|
450
|
+
forms: { level: "error" },
|
|
451
|
+
sql: { level: "error" },
|
|
452
|
+
effect: { level: "error", code: true, width: 140, exports: undefined, skip: undefined, entries: [] },
|
|
469
453
|
console: { level: "error" },
|
|
470
454
|
deny: { level: "error", imports: {}, calls: {} },
|
|
471
|
-
externals: { level: "error"
|
|
455
|
+
externals: { level: "error" }
|
|
472
456
|
};
|
|
457
|
+
var presetQueryWrites = ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"];
|
|
473
458
|
var presetFolderKeys = ["severity", "source", "services", "rules"];
|
|
459
|
+
var presetFolderOnlyKeys = ["source", "services", "tanstack", "next", "expo", "backend", "console", "kit"];
|
|
460
|
+
var presetFileSegment = /^(?!\.{1,2}$)[\w.@$[\]*-]+$/;
|
|
474
461
|
var presetA11yRules = [
|
|
475
462
|
"alt-text",
|
|
476
463
|
"anchor-has-content",
|
|
@@ -510,6 +497,7 @@ var presetA11yRules = [
|
|
|
510
497
|
].map((rule) => `jsx-a11y/${rule}`);
|
|
511
498
|
var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateStaticParams"];
|
|
512
499
|
var presetTestFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
|
|
500
|
+
var presetTanstackRsc = { name: "@tanstack/react-start/rsc", message: "Render server components only in services/*.rsc.tsx files." };
|
|
513
501
|
var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
|
|
514
502
|
var presetDetectFrameworks = [
|
|
515
503
|
["next", ["next", "vinext"]],
|
|
@@ -517,6 +505,17 @@ var presetDetectFrameworks = [
|
|
|
517
505
|
["expo", ["expo"]]
|
|
518
506
|
];
|
|
519
507
|
var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-apps/cli"];
|
|
508
|
+
var presetEffectImports = {
|
|
509
|
+
effect: "Import the module subpath, such as effect/Effect.",
|
|
510
|
+
zod: "Use effect/Schema.",
|
|
511
|
+
"ts-pattern": "Use effect/Match.",
|
|
512
|
+
dayjs: "Use effect/DateTime."
|
|
513
|
+
};
|
|
514
|
+
var presetEffectServerImports = {
|
|
515
|
+
ai: "Use effect/ai.",
|
|
516
|
+
"@ai-sdk/**": "Use effect/ai."
|
|
517
|
+
};
|
|
518
|
+
var presetRecipeMark = Symbol("recipe");
|
|
520
519
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
521
520
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
522
521
|
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
@@ -528,12 +527,73 @@ var presetKitImports = ["**/kits", "**/kits/index", "**/kits/index.ts"];
|
|
|
528
527
|
var presetKitFiles = "^(?:kit|[a-z0-9]+(?:-[a-z0-9]+)*\\.kit)\\.ts$";
|
|
529
528
|
var presetCliFiles = "^cli(?:-[a-z0-9]+)*(?:\\.[a-z0-9-]+)*\\.tsx?$";
|
|
530
529
|
var presetMigrationFiles = "^(?:\\d{3,14}[-_][a-z0-9]+(?:[-_][a-z0-9]+)*|index)\\.ts$";
|
|
531
|
-
var presetMigrationNumber = "^(\\d{3,14})[-_]";
|
|
530
|
+
var presetMigrationNumber = "^(\\d{4}-\\d{2}-\\d{2}(?:-\\d{2})*|\\d{3,14})[-_]";
|
|
532
531
|
var presetRootImports = { "framer-motion": "Use motion/react." };
|
|
533
532
|
var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
|
|
534
533
|
|
|
534
|
+
// src/preset/preset-recipes.ts
|
|
535
|
+
var presetUi = { react: true, kit: true, tailwind: true };
|
|
536
|
+
function presetRecipe(name, recipe) {
|
|
537
|
+
return { ...recipe, [presetRecipeMark]: name };
|
|
538
|
+
}
|
|
539
|
+
var presetRecipes = {
|
|
540
|
+
tanstack: presetRecipe("tanstack", {
|
|
541
|
+
react: true,
|
|
542
|
+
tanstack: true,
|
|
543
|
+
tailwind: true,
|
|
544
|
+
shadcn: true,
|
|
545
|
+
schemas: true,
|
|
546
|
+
forms: true
|
|
547
|
+
}),
|
|
548
|
+
next: presetRecipe("next", {
|
|
549
|
+
react: true,
|
|
550
|
+
next: true,
|
|
551
|
+
tailwind: true,
|
|
552
|
+
shadcn: true,
|
|
553
|
+
schemas: true,
|
|
554
|
+
forms: true
|
|
555
|
+
}),
|
|
556
|
+
expo: presetRecipe("expo", {
|
|
557
|
+
react: true,
|
|
558
|
+
expo: true,
|
|
559
|
+
schemas: true,
|
|
560
|
+
forms: true
|
|
561
|
+
}),
|
|
562
|
+
backend: presetRecipe("backend", {
|
|
563
|
+
backend: true,
|
|
564
|
+
schemas: true,
|
|
565
|
+
sql: true,
|
|
566
|
+
orpc: true,
|
|
567
|
+
layout: { strict: true }
|
|
568
|
+
}),
|
|
569
|
+
react: presetRecipe("react", {
|
|
570
|
+
react: true,
|
|
571
|
+
tailwind: true,
|
|
572
|
+
schemas: true
|
|
573
|
+
}),
|
|
574
|
+
desktop: presetRecipe("desktop", { console: true }),
|
|
575
|
+
library: presetRecipe("library", { schemas: true }),
|
|
576
|
+
effect: presetRecipe("effect", {
|
|
577
|
+
schemas: true,
|
|
578
|
+
effect: true,
|
|
579
|
+
deny: { imports: presetEffectImports }
|
|
580
|
+
}),
|
|
581
|
+
ui: presetRecipe("ui", presetUi),
|
|
582
|
+
kit: presetRecipe("kit", presetUi),
|
|
583
|
+
cli: presetRecipe("cli", { console: true })
|
|
584
|
+
};
|
|
585
|
+
var presetTemplates = {
|
|
586
|
+
monorepo: {
|
|
587
|
+
"apps/web": presetRecipes.tanstack,
|
|
588
|
+
"apps/server": presetRecipes.backend,
|
|
589
|
+
"packages/*": presetRecipes.library,
|
|
590
|
+
scripts: presetRecipes.cli
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
535
594
|
// src/preset/preset.utils.ts
|
|
536
|
-
import { readFileSync, statSync } from "node:fs";
|
|
595
|
+
import { existsSync, globSync, readFileSync, statSync } from "node:fs";
|
|
596
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
537
597
|
import { posix, resolve } from "node:path";
|
|
538
598
|
class PresetError extends Error {
|
|
539
599
|
}
|
|
@@ -549,6 +609,33 @@ async function presetEffectImport(load) {
|
|
|
549
609
|
}
|
|
550
610
|
var presetEffectTsgo = await presetEffectImport(import("@effect/tsgo/oxlint-presets"));
|
|
551
611
|
var presetEffectLinteffect = await presetEffectImport(import("@opsydyn/oxlint-effect"));
|
|
612
|
+
var presetEffectRegexp = await presetEffectImport(import("eslint-plugin-regexp"));
|
|
613
|
+
var presetEffectDeMorgan = await presetEffectImport(import("eslint-plugin-de-morgan"));
|
|
614
|
+
var presetRequire = createRequire2(import.meta.url);
|
|
615
|
+
function presetResolve(name) {
|
|
616
|
+
try {
|
|
617
|
+
return presetRequire.resolve(name);
|
|
618
|
+
} catch {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function presetEffectStyle() {
|
|
623
|
+
const sonarjs = presetResolve("eslint-plugin-sonarjs");
|
|
624
|
+
const regexp = presetEffectRegexp ? presetResolve("eslint-plugin-regexp") : undefined;
|
|
625
|
+
const deMorgan = presetEffectDeMorgan ? presetResolve("eslint-plugin-de-morgan") : undefined;
|
|
626
|
+
const rules = { ...presetEffectRegexp?.configs["flat/recommended"].rules, ...presetEffectDeMorgan?.configs.recommended.rules };
|
|
627
|
+
if (sonarjs) {
|
|
628
|
+
rules["sonarjs/super-linear-regex"] = "error";
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
rules,
|
|
632
|
+
jsPlugins: [
|
|
633
|
+
...regexp ? [{ name: "regexp", specifier: regexp }] : [],
|
|
634
|
+
...deMorgan ? [{ name: "de-morgan", specifier: deMorgan }] : [],
|
|
635
|
+
...sonarjs ? [{ name: "sonarjs", specifier: sonarjs }] : []
|
|
636
|
+
]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
552
639
|
function presetEffectPackages() {
|
|
553
640
|
if (!presetEffectTsgo || !presetEffectLinteffect) {
|
|
554
641
|
throw new PresetError("The effect group needs @effect/tsgo and @opsydyn/oxlint-effect: bun add -d @effect/tsgo @opsydyn/oxlint-effect");
|
|
@@ -617,6 +704,26 @@ function presetIntersect(glob, scope) {
|
|
|
617
704
|
const matches = presetIntersectSegments(glob.split("/"), scope.split("/")).map((segments) => segments.join("/"));
|
|
618
705
|
return [...new Set(matches)];
|
|
619
706
|
}
|
|
707
|
+
function presetGlobEscape(path) {
|
|
708
|
+
return path.replaceAll(/[[\]]/g, (bracket) => `[${bracket}]`);
|
|
709
|
+
}
|
|
710
|
+
function presetFileOverlaps(root, build, keys) {
|
|
711
|
+
const matched = new Map;
|
|
712
|
+
for (const key of keys) {
|
|
713
|
+
for (const path of globSync(key.pattern, { cwd: root, exclude: (name) => posix.basename(name) === "node_modules" })) {
|
|
714
|
+
matched.set(path, [...matched.get(path) ?? [], key]);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const reach = new Map(keys.map((key) => [key, [...matched.values()].filter((found) => found.includes(key)).length]));
|
|
718
|
+
const merged = [...matched].flatMap(([path, found]) => {
|
|
719
|
+
if (found.length < 2) {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
const ordered = found.toSorted((left, right) => (reach.get(right) ?? 0) - (reach.get(left) ?? 0));
|
|
723
|
+
return [build(presetGlobEscape(path), ordered.reduce((folder, key) => Object(presetMerge(folder, key.folder)), {}))];
|
|
724
|
+
});
|
|
725
|
+
return [...keys, ...merged];
|
|
726
|
+
}
|
|
620
727
|
function presetAliases(root, aliases) {
|
|
621
728
|
return Object.fromEntries(Object.entries(aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve(root, path)]));
|
|
622
729
|
}
|
|
@@ -675,77 +782,329 @@ function presetIsFile(path) {
|
|
|
675
782
|
return false;
|
|
676
783
|
}
|
|
677
784
|
}
|
|
785
|
+
function presetSource(root, directory, configured) {
|
|
786
|
+
if (configured !== undefined) {
|
|
787
|
+
return posix.join(directory, configured);
|
|
788
|
+
}
|
|
789
|
+
return existsSync(resolve(root, directory, "src")) ? posix.join(directory, "src") : directory;
|
|
790
|
+
}
|
|
791
|
+
function presetFilled(conventions, exists) {
|
|
792
|
+
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
793
|
+
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
794
|
+
return exists(path) && !empty;
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
function presetMergeObject(base, top, depth) {
|
|
798
|
+
const merged = { ...base };
|
|
799
|
+
for (const [key, value] of Object.entries(top)) {
|
|
800
|
+
const previous = Reflect.get(merged, key);
|
|
801
|
+
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
802
|
+
if (nested && depth > 0) {
|
|
803
|
+
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
804
|
+
} else if (value !== true || !(previous instanceof Object)) {
|
|
805
|
+
Reflect.set(merged, key, value);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return merged;
|
|
809
|
+
}
|
|
810
|
+
function presetMerge(base, top) {
|
|
811
|
+
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
812
|
+
return top === true && base instanceof Object ? base : top;
|
|
813
|
+
}
|
|
814
|
+
const merged = presetMergeObject(base, top, 2);
|
|
815
|
+
const before = base.layout instanceof Object ? base.layout.files : undefined;
|
|
816
|
+
const after = top.layout instanceof Object ? top.layout.files : undefined;
|
|
817
|
+
return before && after && merged.layout instanceof Object ? { ...merged, layout: { ...merged.layout, files: [...before, ...after] } } : merged;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/preset/preset-effect.ts
|
|
821
|
+
var presetEffectQueryWrites = Object.fromEntries(presetQueryWrites.map((name) => [name, "A queries file only reads; write in an actions file."]));
|
|
822
|
+
var presetEffectWorkflowCalls = Object.fromEntries(["selectFrom", ...presetQueryWrites].map((name) => [name, "A workflow reaches data through its domain service, not the database."]));
|
|
823
|
+
var presetEffectFrameworks = Object.fromEntries(["@tanstack/*", "react-dom", "react-dom/**", "next", "next/**", "vite"].map((name) => [name, "The server imports no web framework."]));
|
|
824
|
+
var presetEffectRouters = ["@tanstack/react-router", "next/link", "next/navigation", "next/router", "expo-router"];
|
|
825
|
+
var presetEffectErrors = ["Schema.TaggedError", "Schema.ErrorClass", "Data.TaggedError", "Data.Error"];
|
|
826
|
+
var presetEffectContractNames = "^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.]schemas|[.]admin[.]schemas|[.]contract|[.]admin[.]contract|[.]errors|[.]layer[.]types)[.]ts$";
|
|
827
|
+
var presetEffectContractMessage = "Contract files use <d>.schemas.ts, <d>-<part>.schemas.ts, <d>.admin.schemas.ts, <d>.contract.ts, <d>.admin.contract.ts, <d>.errors.ts or <d>.layer.types.ts.";
|
|
828
|
+
var presetEffectSchemaUses = {
|
|
829
|
+
namespaces: ["Schema"],
|
|
830
|
+
methods: ["Array", "Union", "Record", "Tuple"],
|
|
831
|
+
structural: ["Struct", "Array", "Union", "Record", "Tuple", "NullOr", "UndefinedOr", "optional", "TaggedStruct"]
|
|
832
|
+
};
|
|
833
|
+
function presetEffectExists(root, key) {
|
|
834
|
+
return (path) => {
|
|
835
|
+
const segments = path.split("/");
|
|
836
|
+
const wildcard = segments.findIndex((segment) => segment.includes("*"));
|
|
837
|
+
return existsSync2(resolve2(root, key, ...wildcard === -1 ? segments : segments.slice(0, wildcard)));
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function presetEffectAt(root, key, value) {
|
|
841
|
+
const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
|
|
842
|
+
return (path) => source === "." ? path : posix2.join(source, path);
|
|
843
|
+
}
|
|
844
|
+
function presetEffectCode(value) {
|
|
845
|
+
return value instanceof Object && Boolean(value.effect) && !(value.effect instanceof Object && value.effect.code === false);
|
|
846
|
+
}
|
|
847
|
+
function presetEffectContract(root, key, value) {
|
|
848
|
+
if (!presetEffectCode(value) || presetEffectDomains(root, key, value)) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
const source = resolve2(root, presetSource(root, key, value.source));
|
|
852
|
+
return presetIsDirectory(source) && readdirSync(source).some((name) => presetIsFile(resolve2(source, name, `${name}.contract.ts`)));
|
|
853
|
+
}
|
|
854
|
+
function presetEffectDomains(root, key, value) {
|
|
855
|
+
return presetEffectCode(value) && presetIsDirectory(resolve2(root, presetSource(root, key, value.source), "domains"));
|
|
856
|
+
}
|
|
857
|
+
function presetEffectServers(root, merged, units) {
|
|
858
|
+
for (const unit of units) {
|
|
859
|
+
if (presetEffectDomains(root, unit[0], unit[1])) {
|
|
860
|
+
unit[1] = presetMerge({ ...presetRecipes.backend, services: "domains" }, unit[1]);
|
|
861
|
+
merged[unit[0]] = unit[1];
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function presetEffectServerKeys(at) {
|
|
866
|
+
return {
|
|
867
|
+
[at(".")]: {
|
|
868
|
+
deny: { imports: { ...presetEffectFrameworks, "effect/Config": "Read config only in application/app.config.ts." } },
|
|
869
|
+
rules: {
|
|
870
|
+
"arch/types-declarations": ["error", { modules: true }],
|
|
871
|
+
"arch/types-inline": ["error", { variables: true, assertions: true, arguments: true }],
|
|
872
|
+
"arch/comments-code": ["error", { why: false, jsdoc: true, allow: ["^\\s*SAFETY: "], steps: true }]
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
[at("domains")]: {
|
|
876
|
+
layout: {
|
|
877
|
+
files: [
|
|
878
|
+
"*.actions.ts",
|
|
879
|
+
"*.queries.ts",
|
|
880
|
+
"*.layer.ts",
|
|
881
|
+
"*.workflows.ts",
|
|
882
|
+
"*.prompts.ts",
|
|
883
|
+
"*-*.driver.ts",
|
|
884
|
+
"!*.service.ts",
|
|
885
|
+
"!*.client.ts",
|
|
886
|
+
"!*.server.ts",
|
|
887
|
+
"!*.rsc.ts",
|
|
888
|
+
"!*.rsc.tsx",
|
|
889
|
+
"!*.utils.ts",
|
|
890
|
+
"!*.types.ts",
|
|
891
|
+
"!*.handler.ts",
|
|
892
|
+
"!*-action.*.ts",
|
|
893
|
+
"!*-query.*.ts"
|
|
894
|
+
]
|
|
895
|
+
},
|
|
896
|
+
deny: { imports: { "../*/*.constants{,.ts}": "Another domain's constants stay in that domain." } }
|
|
897
|
+
},
|
|
898
|
+
[at("domains/*/*.actions.ts")]: {
|
|
899
|
+
limits: { lines: 1000 },
|
|
900
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Action[A-Z]\\w*"], types: true }] }
|
|
901
|
+
},
|
|
902
|
+
[at("domains/*/*.queries.ts")]: {
|
|
903
|
+
limits: { lines: 1000 },
|
|
904
|
+
deny: { calls: presetEffectQueryWrites, imports: { "./*.actions{,.ts}": "A queries file imports no actions." } },
|
|
905
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Query[A-Z]\\w*"], types: true }] }
|
|
906
|
+
},
|
|
907
|
+
[at("domains/*/*.constants.ts")]: { rules: { "arch/files-no-functions": ["error", { reexports: false }] } },
|
|
908
|
+
[at("domains/*/*.layer.ts")]: { rules: { "arch/effect-layer-file": ["error", { suffix: "Layer" }] } },
|
|
909
|
+
[at("domains/*/*.driver.ts")]: { rules: { "arch/effect-layer-file": ["error", { suffix: "Layer" }] } },
|
|
910
|
+
[at("domains/*/*.workflows.ts")]: {
|
|
911
|
+
deny: {
|
|
912
|
+
calls: presetEffectWorkflowCalls,
|
|
913
|
+
imports: {
|
|
914
|
+
"**/database": "A workflow reaches data through its domain service, not the database.",
|
|
915
|
+
"./*.{actions,queries}{,.ts}": "A workflow calls its domain service, not its own actions or queries."
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
[at("application/app.config.ts")]: { deny: { imports: { "effect/Config": false } } },
|
|
920
|
+
[at("application/app.ts")]: {
|
|
921
|
+
deny: { imports: { "**/domains/*/!(*.layer|*.layer.ts|*.workflows|*.workflows.ts)": "The application imports only layer and workflows files." } },
|
|
922
|
+
rules: { "arch/boundaries-imports": "off" }
|
|
923
|
+
},
|
|
924
|
+
[at("**/*.tsx")]: { rules: { "arch/files-restricted": ["error", { message: "Mail templates in mails/ are the only JSX on a server." }] } },
|
|
925
|
+
[at("mails/*.mail.tsx")]: { rules: { "arch/files-restricted": "off" } },
|
|
926
|
+
[at("rpc")]: { rules: { "arch/code-delegate": "error" } }
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function presetEffectContractKeys(at) {
|
|
930
|
+
const schemas = {
|
|
931
|
+
rules: {
|
|
932
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }],
|
|
933
|
+
"arch/files-no-functions": ["error", { reexports: false }],
|
|
934
|
+
"arch/types-imported-alias": "error",
|
|
935
|
+
"arch/schemas-single-use": ["error", presetEffectSchemaUses]
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const noFunctions = { rules: { "arch/files-no-functions": "error" } };
|
|
939
|
+
return {
|
|
940
|
+
[at("*/*.ts")]: {
|
|
941
|
+
rules: {
|
|
942
|
+
"arch/naming-file": ["error", { pattern: presetEffectContractNames, message: presetEffectContractMessage }],
|
|
943
|
+
"arch/naming-folder": ["error", { singularize: "none", separators: [".", "-"] }],
|
|
944
|
+
"arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["{Domain}\\w*"] }]
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
[at("*/*.admin.*.ts")]: { rules: { "arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["Admin{Domain}\\w*"] }] } },
|
|
948
|
+
[at("*/*.schemas.ts")]: schemas,
|
|
949
|
+
[at("*/*.contract.ts")]: schemas,
|
|
950
|
+
[at("*/*.errors.ts")]: noFunctions,
|
|
951
|
+
[at("*/*.layer.types.ts")]: noFunctions,
|
|
952
|
+
[at("shared/*.ts")]: {
|
|
953
|
+
rules: {
|
|
954
|
+
"arch/naming-file": "off",
|
|
955
|
+
"arch/naming-folder": "off",
|
|
956
|
+
"arch/files-exports": "off",
|
|
957
|
+
"arch/files-no-functions": "off",
|
|
958
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }]
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
[at("contract*.ts")]: {
|
|
962
|
+
rules: {
|
|
963
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }],
|
|
964
|
+
"arch/types-imported-alias": "error",
|
|
965
|
+
"arch/schemas-single-use": ["error", presetEffectSchemaUses]
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function presetEffectUnit(root, key, value) {
|
|
971
|
+
if (!value.effect) {
|
|
972
|
+
return {};
|
|
973
|
+
}
|
|
974
|
+
const at = presetEffectAt(root, key, value);
|
|
975
|
+
const contract = presetEffectContract(root, key, value);
|
|
976
|
+
const server = presetEffectDomains(root, key, value);
|
|
977
|
+
const keys = server || contract ? presetFilled(server ? presetEffectServerKeys(at) : presetEffectContractKeys(at), presetEffectExists(root, key)) : {};
|
|
978
|
+
const errors = Object.fromEntries(presetEffectErrors.map((name) => [name, "Tagged errors live in <d>.errors.ts."]));
|
|
979
|
+
const placement = server || contract ? { types: { placement: false } } : {};
|
|
980
|
+
const imports = server || contract ? { ...presetEffectImports, ...presetEffectServerImports } : presetEffectImports;
|
|
981
|
+
return {
|
|
982
|
+
...keys,
|
|
983
|
+
".": presetMerge({ ...placement, deny: { imports, calls: errors } }, keys["."] ?? {}),
|
|
984
|
+
[at("**/*.errors.ts")]: { deny: { calls: Object.fromEntries(presetEffectErrors.map((name) => [name, false])) } }
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function presetEffectFrontend(root, key, value) {
|
|
988
|
+
const at = presetEffectAt(root, key, value);
|
|
989
|
+
const exists = presetEffectExists(root, key);
|
|
990
|
+
const framework = [value.tanstack, value.next, value.expo].some(Boolean);
|
|
991
|
+
if (!value.effect || !value.react || framework || !["pages", "link.tsx", "link.ts"].some((path) => exists(at(path)))) {
|
|
992
|
+
return {};
|
|
993
|
+
}
|
|
994
|
+
const routers = presetEffectRouters.map((name) => ({ name, message: "Import the router through link.tsx." }));
|
|
995
|
+
const linkRule = { "no-restricted-imports": ["error", { patterns: presetBaseImports }] };
|
|
996
|
+
return presetFilled({
|
|
997
|
+
".": { forms: true },
|
|
998
|
+
[at(".")]: {
|
|
999
|
+
deny: { imports: { "@orpc/*": "A frontend package takes its data functions as props, not from an rpc client." } },
|
|
1000
|
+
rules: {
|
|
1001
|
+
"arch/react-query-props": "error",
|
|
1002
|
+
"no-restricted-imports": [
|
|
1003
|
+
"error",
|
|
1004
|
+
{ paths: [{ ...routers[0], allowImportNames: ["ClientOnly"] }, ...routers.slice(1)], patterns: presetBaseImports }
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
[at("link.tsx")]: { rules: linkRule },
|
|
1009
|
+
[at("link.ts")]: { rules: linkRule },
|
|
1010
|
+
[at("services/**/*.client.ts")]: { rules: { "arch/backend-service-functions": ["error", { frontend: true }] } }
|
|
1011
|
+
}, exists);
|
|
1012
|
+
}
|
|
1013
|
+
function presetEffectWeb(root, key, value, contracts) {
|
|
1014
|
+
const at = presetEffectAt(root, key, value);
|
|
1015
|
+
if (contracts.length === 0 || !(value.tanstack || value.next)) {
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
const types = contracts.map((name) => ({ name, allowTypeImports: true, message: "The web imports the contract package as types only." }));
|
|
1019
|
+
const rsc = { rules: { "no-restricted-imports": ["error", { paths: types, patterns: presetBaseImports }] } };
|
|
1020
|
+
if (!value.tanstack) {
|
|
1021
|
+
return { [at(".")]: rsc };
|
|
1022
|
+
}
|
|
1023
|
+
const services = value.services ?? "services";
|
|
1024
|
+
return {
|
|
1025
|
+
[at(".")]: { rules: { "no-restricted-imports": ["error", { paths: [...types, presetTanstackRsc], patterns: presetBaseImports }] } },
|
|
1026
|
+
[at(`${services}/**/*.rsc.ts`)]: rsc,
|
|
1027
|
+
[at(`${services}/**/*.rsc.tsx`)]: rsc
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function presetEffectKit(root, key, value, units) {
|
|
1031
|
+
if (!value.kit || !value.effect) {
|
|
1032
|
+
return {};
|
|
1033
|
+
}
|
|
1034
|
+
const apps = units.filter(([other, folder]) => other !== key && !(folder instanceof Object && folder.kit));
|
|
1035
|
+
const names = apps.flatMap(([other]) => presetManifest(root, other).name ?? []);
|
|
1036
|
+
const message = "A UI kit imports no app package.";
|
|
1037
|
+
return { ".": { deny: { imports: Object.fromEntries(names.flatMap((name) => [[name, message], [`${name}/**`, message]])) } } };
|
|
1038
|
+
}
|
|
1039
|
+
function presetEffectConventions(root, key, value, units) {
|
|
1040
|
+
const contracts = units.flatMap(([other, folder]) => presetEffectContract(root, other, folder) ? presetManifest(root, other).name ?? [] : []);
|
|
1041
|
+
return [
|
|
1042
|
+
...Object.entries(presetEffectUnit(root, key, value)),
|
|
1043
|
+
...Object.entries(presetEffectFrontend(root, key, value)),
|
|
1044
|
+
...Object.entries(presetEffectWeb(root, key, value, contracts)),
|
|
1045
|
+
...Object.entries(presetEffectKit(root, key, value, units))
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
678
1048
|
|
|
679
1049
|
// src/preset/preset-detect.ts
|
|
680
1050
|
function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
681
|
-
if (!presetIsDirectory(
|
|
1051
|
+
if (!presetIsDirectory(resolve3(root, base))) {
|
|
682
1052
|
return [];
|
|
683
1053
|
}
|
|
684
|
-
return
|
|
1054
|
+
return readdirSync2(resolve3(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve3(root, base, name))).map((name) => posix3.join(base, name));
|
|
685
1055
|
}
|
|
686
1056
|
var presetNamedSkip = ["node_modules"];
|
|
687
|
-
function presetSource(root, directory, configured) {
|
|
688
|
-
if (configured !== undefined) {
|
|
689
|
-
return posix2.join(directory, configured);
|
|
690
|
-
}
|
|
691
|
-
return existsSync(resolve2(root, directory, "src")) ? posix2.join(directory, "src") : directory;
|
|
692
|
-
}
|
|
693
1057
|
function presetRegistryFile(root, source, configured, services) {
|
|
694
1058
|
if (configured === false) {
|
|
695
1059
|
return;
|
|
696
1060
|
}
|
|
697
1061
|
const file = typeof configured === "string" ? configured : configured?.file;
|
|
698
1062
|
if (file !== undefined) {
|
|
699
|
-
return
|
|
1063
|
+
return posix3.normalize(file);
|
|
700
1064
|
}
|
|
701
|
-
return [
|
|
1065
|
+
return [posix3.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix3.join(source, path))].find((path) => existsSync3(resolve3(root, path)));
|
|
702
1066
|
}
|
|
703
1067
|
function presetDomains(root, services) {
|
|
704
|
-
return presetDirectories(root, services, presetNamedSkip).map((path) =>
|
|
1068
|
+
return presetDirectories(root, services, presetNamedSkip).map((path) => posix3.basename(path)).filter((name) => existsSync3(resolve3(root, services, name, `${name}.service.ts`)));
|
|
705
1069
|
}
|
|
706
|
-
function
|
|
1070
|
+
function presetRecipe2(root, directory, app, folder) {
|
|
707
1071
|
const manifest = presetManifest(root, directory);
|
|
708
1072
|
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
709
1073
|
const has = (names) => names.some((name) => dependencies.has(name));
|
|
710
1074
|
const source = presetSource(root, directory, folder.source);
|
|
711
1075
|
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
1076
|
+
const frontend = presetIsDirectory(resolve3(root, source, "pages")) || ["link.tsx", "link.ts"].some((name) => existsSync3(resolve3(root, source, name)));
|
|
1077
|
+
const react = has(["react"]) && (frontend || presetIsDirectory(resolve3(root, source, "components")));
|
|
1078
|
+
const reactRecipe = app || frontend ? presetRecipes.react : presetRecipes.ui;
|
|
712
1079
|
const servicesFolder = presetServicesFolder(root, source, folder.services, app);
|
|
713
|
-
const services =
|
|
714
|
-
const entries = presetIsDirectory(services) ?
|
|
715
|
-
const folders = entries.filter((entry) => presetIsDirectory(
|
|
1080
|
+
const services = resolve3(root, servicesFolder);
|
|
1081
|
+
const entries = presetIsDirectory(services) ? readdirSync2(services, { withFileTypes: true }) : [];
|
|
1082
|
+
const folders = entries.filter((entry) => presetIsDirectory(resolve3(services, entry.name))).length;
|
|
716
1083
|
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
|
|
1084
|
+
const backend = presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services));
|
|
1085
|
+
const layout = flat ? { layout: { services: "flat" } } : {};
|
|
1086
|
+
const style = has(["effect"]) ? { effect: { code: false } } : {};
|
|
1087
|
+
const { deny: _bans, ...effect } = presetRecipes.effect;
|
|
717
1088
|
if (framework) {
|
|
718
|
-
return
|
|
1089
|
+
return { ...presetRecipes[framework], ...layout, ...style };
|
|
719
1090
|
}
|
|
720
1091
|
if (has(presetDetectDesktop)) {
|
|
721
1092
|
return presetRecipes.desktop;
|
|
722
1093
|
}
|
|
723
|
-
|
|
724
|
-
|
|
1094
|
+
const reactLayout = frontend ? { ...reactRecipe, ...layout } : reactRecipe;
|
|
1095
|
+
if (has(["effect"]) && react) {
|
|
1096
|
+
return { ...reactLayout, ...style };
|
|
725
1097
|
}
|
|
726
|
-
if (has(["
|
|
727
|
-
return
|
|
1098
|
+
if (has(["effect"])) {
|
|
1099
|
+
return backend || presetIsDirectory(resolve3(root, source, "domains")) ? { ...presetRecipes.backend, ...effect } : effect;
|
|
728
1100
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
function presetMergeObject(base, top, depth) {
|
|
732
|
-
const merged = { ...base };
|
|
733
|
-
for (const [key, value] of Object.entries(top)) {
|
|
734
|
-
const previous = Reflect.get(merged, key);
|
|
735
|
-
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
736
|
-
if (nested && depth > 0) {
|
|
737
|
-
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
738
|
-
} else if (value !== true || !(previous instanceof Object)) {
|
|
739
|
-
Reflect.set(merged, key, value);
|
|
740
|
-
}
|
|
1101
|
+
if (backend) {
|
|
1102
|
+
return presetRecipes.backend;
|
|
741
1103
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
function presetMerge(base, top) {
|
|
745
|
-
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
746
|
-
return top === true && base instanceof Object ? base : top;
|
|
1104
|
+
if (react) {
|
|
1105
|
+
return reactLayout;
|
|
747
1106
|
}
|
|
748
|
-
return
|
|
1107
|
+
return presetRecipes.library;
|
|
749
1108
|
}
|
|
750
1109
|
function presetImportTargets(value) {
|
|
751
1110
|
if (typeof value === "string") {
|
|
@@ -758,7 +1117,7 @@ function presetImportTargets(value) {
|
|
|
758
1117
|
}
|
|
759
1118
|
function presetSpecifiers(manifest, target, file = false) {
|
|
760
1119
|
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
761
|
-
const path =
|
|
1120
|
+
const path = posix3.normalize(mapped);
|
|
762
1121
|
const parts = path.split("*");
|
|
763
1122
|
const keyParts = key.split("*");
|
|
764
1123
|
if (parts.length === 1 && keyParts.length === 1) {
|
|
@@ -787,23 +1146,17 @@ function presetOverlaps(left, right) {
|
|
|
787
1146
|
function presetDeny(imports) {
|
|
788
1147
|
return { deny: { imports } };
|
|
789
1148
|
}
|
|
790
|
-
function presetFilled(conventions, exists) {
|
|
791
|
-
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
792
|
-
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
793
|
-
return exists(path) && !empty;
|
|
794
|
-
}));
|
|
795
|
-
}
|
|
796
1149
|
function presetBackendConventions(root, key, value, denied) {
|
|
797
1150
|
const manifest = presetManifest(root, key);
|
|
798
1151
|
const source = presetSource(root, key, value.source);
|
|
799
|
-
const local =
|
|
800
|
-
const at = (path) => local === "." ? path :
|
|
801
|
-
const exists = (path) =>
|
|
1152
|
+
const local = posix3.relative(key, source) || ".";
|
|
1153
|
+
const at = (path) => local === "." ? path : posix3.join(local, path);
|
|
1154
|
+
const exists = (path) => existsSync3(resolve3(root, key, path));
|
|
802
1155
|
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
803
1156
|
const servicesFolder = presetServicesFolder(root, source, value.services);
|
|
804
|
-
const servicesKey =
|
|
1157
|
+
const servicesKey = posix3.relative(key, servicesFolder);
|
|
805
1158
|
const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
|
|
806
|
-
const registry = registryFile === undefined ? undefined :
|
|
1159
|
+
const registry = registryFile === undefined ? undefined : posix3.relative(key, registryFile);
|
|
807
1160
|
const services = presetSpecifiers(manifest, servicesKey);
|
|
808
1161
|
const message = "Use the service API, not another service’s operations.";
|
|
809
1162
|
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
@@ -823,27 +1176,27 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
823
1176
|
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
824
1177
|
}
|
|
825
1178
|
};
|
|
826
|
-
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) =>
|
|
1179
|
+
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix3.basename(path))) {
|
|
827
1180
|
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
828
1181
|
[`${prefix}/${pattern}`, false],
|
|
829
1182
|
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
830
1183
|
]));
|
|
831
|
-
conventions[
|
|
1184
|
+
conventions[posix3.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
|
|
832
1185
|
}
|
|
833
1186
|
const testFolders = ["tests", "test"];
|
|
834
1187
|
for (const tests of testFolders) {
|
|
835
|
-
if (exists(
|
|
1188
|
+
if (exists(posix3.join(tests, "kits"))) {
|
|
836
1189
|
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
837
|
-
conventions[
|
|
1190
|
+
conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
838
1191
|
}
|
|
839
1192
|
}
|
|
840
1193
|
const queue = ["."];
|
|
841
1194
|
while (queue.length > 0) {
|
|
842
1195
|
const folder = queue.shift() ?? ".";
|
|
843
|
-
for (const child of presetDirectories(root,
|
|
844
|
-
if (
|
|
1196
|
+
for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
|
|
1197
|
+
if (posix3.basename(child) === "migrations") {
|
|
845
1198
|
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
846
|
-
} else if (child.split("/").length < 8 && !testFolders.includes(
|
|
1199
|
+
} else if (child.split("/").length < 8 && !testFolders.includes(posix3.basename(child))) {
|
|
847
1200
|
queue.push(child);
|
|
848
1201
|
}
|
|
849
1202
|
}
|
|
@@ -851,9 +1204,9 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
851
1204
|
return presetFilled(conventions, exists);
|
|
852
1205
|
}
|
|
853
1206
|
function presetWebConventions(root, key, value, servers) {
|
|
854
|
-
const source =
|
|
855
|
-
const at = (path) => source === "." ? path :
|
|
856
|
-
const exists = (path) =>
|
|
1207
|
+
const source = posix3.relative(key, presetSource(root, key, value.source)) || ".";
|
|
1208
|
+
const at = (path) => source === "." ? path : posix3.join(source, path);
|
|
1209
|
+
const exists = (path) => existsSync3(resolve3(root, key, path));
|
|
857
1210
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
858
1211
|
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
859
1212
|
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
@@ -873,8 +1226,8 @@ function presetWebConventions(root, key, value, servers) {
|
|
|
873
1226
|
}
|
|
874
1227
|
function presetWorkspacePatterns(root, manifest) {
|
|
875
1228
|
const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
|
|
876
|
-
const file =
|
|
877
|
-
if (!
|
|
1229
|
+
const file = resolve3(root, "pnpm-workspace.yaml");
|
|
1230
|
+
if (!existsSync3(file)) {
|
|
878
1231
|
return listed;
|
|
879
1232
|
}
|
|
880
1233
|
const patterns = [...listed];
|
|
@@ -897,7 +1250,7 @@ function presetWorkspacePatterns(root, manifest) {
|
|
|
897
1250
|
return patterns;
|
|
898
1251
|
}
|
|
899
1252
|
function presetWorkspaces(root, patterns) {
|
|
900
|
-
const clean = (pattern) =>
|
|
1253
|
+
const clean = (pattern) => posix3.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
901
1254
|
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
902
1255
|
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
903
1256
|
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
@@ -916,7 +1269,7 @@ function presetWorkspaces(root, patterns) {
|
|
|
916
1269
|
level.forEach((folder) => candidates.add(folder));
|
|
917
1270
|
}
|
|
918
1271
|
}
|
|
919
|
-
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) &&
|
|
1272
|
+
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync3(resolve3(root, path, "package.json"))).toSorted();
|
|
920
1273
|
}
|
|
921
1274
|
function presetContains(key, path) {
|
|
922
1275
|
if (key === "." || key === path) {
|
|
@@ -942,18 +1295,22 @@ function presetDetect(root, folders) {
|
|
|
942
1295
|
};
|
|
943
1296
|
const patterns = presetWorkspacePatterns(root, manifest);
|
|
944
1297
|
const directories = presetWorkspaces(root, patterns);
|
|
945
|
-
const detected = Object.fromEntries(directories.map((directory) => [directory,
|
|
946
|
-
const rootRecipe =
|
|
1298
|
+
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe2(root, directory, !directory.startsWith("packages/"), configured(directory))]));
|
|
1299
|
+
const rootRecipe = presetRecipe2(root, ".", true, configured("."));
|
|
947
1300
|
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
948
1301
|
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
|
|
949
1302
|
if (patterns.length === 0 ? single : rootApp) {
|
|
950
1303
|
detected["."] = rootRecipe;
|
|
951
1304
|
}
|
|
952
|
-
if (
|
|
1305
|
+
if (existsSync3(resolve3(root, "scripts")) && !directories.includes("scripts")) {
|
|
953
1306
|
detected.scripts = presetRecipes.cli;
|
|
954
1307
|
}
|
|
1308
|
+
if (existsSync3(resolve3(root, ".agents"))) {
|
|
1309
|
+
const { quotes, semicolons } = presetGroupOptions.style;
|
|
1310
|
+
detected[".agents"] = { ...presetRecipes.cli, style: { quotes, semicolons } };
|
|
1311
|
+
}
|
|
955
1312
|
const keys = Object.keys(folders);
|
|
956
|
-
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") &&
|
|
1313
|
+
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix3.dirname(key) === user.slice(0, -2)));
|
|
957
1314
|
const merged = { ".": presetDeny(presetRootImports) };
|
|
958
1315
|
for (const [key, value] of Object.entries(detected)) {
|
|
959
1316
|
if (!covered(key)) {
|
|
@@ -961,7 +1318,9 @@ function presetDetect(root, folders) {
|
|
|
961
1318
|
}
|
|
962
1319
|
}
|
|
963
1320
|
for (const [key, value] of Object.entries(folders)) {
|
|
964
|
-
|
|
1321
|
+
const forced = value instanceof Object && presetRecipeMark in value;
|
|
1322
|
+
const kept = forced && key === "." ? presetDeny(presetRootImports) : merged[key];
|
|
1323
|
+
merged[key] = key in merged && (!forced || key === ".") ? presetMerge(kept, value) : value;
|
|
965
1324
|
}
|
|
966
1325
|
const length = (key) => key.split("/").length;
|
|
967
1326
|
const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
|
|
@@ -975,6 +1334,7 @@ function presetDetect(root, folders) {
|
|
|
975
1334
|
const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
|
|
976
1335
|
return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
|
|
977
1336
|
});
|
|
1337
|
+
presetEffectServers(root, merged, units);
|
|
978
1338
|
const denied = new Set(Object.values(folders).flatMap((folder) => folder instanceof Object && folder.deny instanceof Object ? Object.entries(folder.deny.imports ?? {}).flatMap(([pattern, message]) => typeof message === "string" ? [pattern] : []) : []));
|
|
979
1339
|
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
980
1340
|
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
@@ -994,11 +1354,11 @@ function presetDetect(root, folders) {
|
|
|
994
1354
|
}
|
|
995
1355
|
if (value.backend) {
|
|
996
1356
|
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
997
|
-
add(path === "." ? key :
|
|
1357
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
998
1358
|
}
|
|
999
1359
|
} else if (value.tanstack || value.next) {
|
|
1000
1360
|
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
1001
|
-
add(path === "." ? key :
|
|
1361
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1002
1362
|
}
|
|
1003
1363
|
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
1004
1364
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
@@ -1006,6 +1366,9 @@ function presetDetect(root, folders) {
|
|
|
1006
1366
|
const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
|
|
1007
1367
|
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
1008
1368
|
}
|
|
1369
|
+
for (const [path, folder] of presetEffectConventions(root, key, value, units)) {
|
|
1370
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1371
|
+
}
|
|
1009
1372
|
}
|
|
1010
1373
|
return merged;
|
|
1011
1374
|
}
|
|
@@ -1026,7 +1389,7 @@ var backendGroup = (project, settings) => {
|
|
|
1026
1389
|
}
|
|
1027
1390
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
1028
1391
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
1029
|
-
const file = configured.file === undefined ? undefined :
|
|
1392
|
+
const file = configured.file === undefined ? undefined : posix4.normalize(configured.file);
|
|
1030
1393
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
1031
1394
|
const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
|
|
1032
1395
|
return found ? [{ file: found, unit }] : [];
|
|
@@ -1036,8 +1399,8 @@ var backendGroup = (project, settings) => {
|
|
|
1036
1399
|
throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
|
|
1037
1400
|
}
|
|
1038
1401
|
const options = {
|
|
1039
|
-
registry:
|
|
1040
|
-
services:
|
|
1402
|
+
registry: resolve4(project.root, registry.file),
|
|
1403
|
+
services: resolve4(project.root, registry.unit.services),
|
|
1041
1404
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
1042
1405
|
};
|
|
1043
1406
|
if (configured.object !== undefined) {
|
|
@@ -1052,9 +1415,11 @@ var backendGroup = (project, settings) => {
|
|
|
1052
1415
|
var basicsGroup = (project, settings) => {
|
|
1053
1416
|
const trivial = { mode: "precise", async: false, callees: [] };
|
|
1054
1417
|
const { appFiles, safetyFiles, ui, web } = project;
|
|
1418
|
+
const nextEntries = presetScopes(project.nextWeb, "app/**/{layout,page,default}.tsx");
|
|
1055
1419
|
return [
|
|
1056
1420
|
...settings.trivial ? [
|
|
1057
|
-
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
|
|
1421
|
+
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, [...ui, ...nextEntries]),
|
|
1422
|
+
presetOverride(nextEntries, { "arch/code-trivial-functions": ["error", { ...trivial, defaults: false }] }, ui),
|
|
1058
1423
|
presetOverride([...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")], { "arch/code-trivial-functions": ["error", { ...trivial, services: true }] }, ui)
|
|
1059
1424
|
] : [],
|
|
1060
1425
|
...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
|
|
@@ -1075,22 +1440,20 @@ var consoleGroup = () => [presetOverride([`**/${presetExtensions}`], { "no-conso
|
|
|
1075
1440
|
|
|
1076
1441
|
// src/preset/groups/deny.ts
|
|
1077
1442
|
var denyGroup = (project, settings) => {
|
|
1078
|
-
const
|
|
1443
|
+
const kept = (entries) => Object.fromEntries(Object.entries(entries).filter((entry) => entry[1] !== false));
|
|
1444
|
+
const imports = kept(settings.imports);
|
|
1445
|
+
const calls = kept(settings.calls);
|
|
1079
1446
|
const overrides = [];
|
|
1080
1447
|
if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
|
|
1081
1448
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
|
|
1082
1449
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (writes.length === 0) {
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1450
|
+
const reads = Object.fromEntries(presetQueryWrites.map((name) => [name, "Query files only read; move writes to an action."]));
|
|
1451
|
+
for (const unit of project.units.filter((item) => item.kind === "backend")) {
|
|
1088
1452
|
const files = [
|
|
1089
1453
|
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1090
1454
|
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
1091
1455
|
];
|
|
1092
|
-
|
|
1093
|
-
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
|
|
1456
|
+
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: kept({ ...reads, ...settings.calls }) }] }));
|
|
1094
1457
|
}
|
|
1095
1458
|
return overrides;
|
|
1096
1459
|
};
|
|
@@ -1133,9 +1496,18 @@ var effectTestOff = {
|
|
|
1133
1496
|
var effectGroup = (project, settings) => {
|
|
1134
1497
|
const files = [`**/${presetExtensions}`];
|
|
1135
1498
|
const max = settings.width;
|
|
1499
|
+
const style = presetOverride(files, {
|
|
1500
|
+
"typescript/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1501
|
+
"eslint-js/max-len": ["error", { code: max, ignoreUrls: true, ignoreComments: true, ignoreRegExpLiterals: true }],
|
|
1502
|
+
...presetEffectStyle().rules
|
|
1503
|
+
});
|
|
1504
|
+
if (!settings.code) {
|
|
1505
|
+
return [style];
|
|
1506
|
+
}
|
|
1136
1507
|
const { tsgo, linteffect } = presetEffectPackages();
|
|
1137
1508
|
const tsgoRules = [tsgo.correctness, tsgo.antipattern, tsgo.effectNative, tsgo.style].flatMap((config) => Object.keys(config.rules ?? {}));
|
|
1138
1509
|
return [
|
|
1510
|
+
style,
|
|
1139
1511
|
presetOverride(files, {
|
|
1140
1512
|
...Object.fromEntries(tsgoRules.map((rule) => [rule, "error"])),
|
|
1141
1513
|
...linteffect.recommended.rules,
|
|
@@ -1146,18 +1518,23 @@ var effectGroup = (project, settings) => {
|
|
|
1146
1518
|
presetOverride(files, {
|
|
1147
1519
|
"arch/code-trivial-functions": "off",
|
|
1148
1520
|
"arch/code-branch-once": "error",
|
|
1521
|
+
"arch/code-object-params": ["error", { all: true, services: true }],
|
|
1522
|
+
"arch/code-switch": ["error", { message: "Branch with Match." }],
|
|
1523
|
+
"arch/naming-exports": "off",
|
|
1149
1524
|
"arch/code-function-nesting": ["error", { max: 4 }],
|
|
1150
|
-
"arch/code-inline": ["error", { max, nesting: 2, keep: ["^[A-Z]", "^layer"], allow: "^(?:decode|encode)[A-Z]", services: ["of"] }],
|
|
1525
|
+
"arch/code-inline": ["error", { max, nesting: 2, callbacks: true, keep: ["^[A-Z]", "^layer"], allow: "^(?:decode|encode)[A-Z]", services: ["of"] }],
|
|
1151
1526
|
"arch/code-yield-argument": "error",
|
|
1152
1527
|
"arch/effect-layers": "error",
|
|
1153
1528
|
"arch/effect-merge-pipe": "error",
|
|
1154
1529
|
"arch/effect-values-first": "error",
|
|
1155
1530
|
"arch/layout-arrow-body": ["error", { max }],
|
|
1156
|
-
"arch/layout-call-arguments": ["error", { split: false, hug: { max }, join: { max, nesting: 2, keep: ["Layer.mergeAll"] } }],
|
|
1157
|
-
"arch/layout-call-depth": ["error", { max: 2 }],
|
|
1531
|
+
"arch/layout-call-arguments": ["error", { split: false, pipe: { min: 2 }, hug: { max }, join: { max, nesting: 2, keep: ["Layer.mergeAll"] } }],
|
|
1532
|
+
"arch/layout-call-depth": ["error", { max: 2, callbacks: true }],
|
|
1158
1533
|
"arch/layout-items": "error",
|
|
1534
|
+
"arch/layout-object": ["error", { min: 3, scope: "call-args", params: true, nested: true, siblings: true, inner: 2 }],
|
|
1159
1535
|
"arch/layout-pipe": "error",
|
|
1160
|
-
"arch/layout-ternary": ["error", {
|
|
1536
|
+
"arch/layout-ternary": ["error", { max: 100, branch: { max: 3 } }],
|
|
1537
|
+
"arch/layout-type-arguments": ["error", { min: 3 }]
|
|
1161
1538
|
}),
|
|
1162
1539
|
presetOverride(files, {
|
|
1163
1540
|
"arch/code-justified-call": [
|
|
@@ -1210,17 +1587,17 @@ var expoGroup = (project, settings) => {
|
|
|
1210
1587
|
};
|
|
1211
1588
|
|
|
1212
1589
|
// src/preset/groups/externals.ts
|
|
1213
|
-
var externalsGroup = (
|
|
1214
|
-
presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages:
|
|
1590
|
+
var externalsGroup = () => [
|
|
1591
|
+
presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: presetExternals }] })
|
|
1215
1592
|
];
|
|
1216
1593
|
|
|
1217
1594
|
// src/preset/groups/forms.ts
|
|
1218
|
-
var formsGroup = (project
|
|
1219
|
-
presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require:
|
|
1595
|
+
var formsGroup = (project) => [
|
|
1596
|
+
presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: "standardSchemaResolver" }] }, project.ui)
|
|
1220
1597
|
];
|
|
1221
1598
|
|
|
1222
1599
|
// src/preset/groups/imports.ts
|
|
1223
|
-
import { resolve as
|
|
1600
|
+
import { resolve as resolve5 } from "node:path";
|
|
1224
1601
|
function importsPackageNames(project, kinds) {
|
|
1225
1602
|
return project.units.flatMap((unit) => {
|
|
1226
1603
|
const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
|
|
@@ -1258,7 +1635,7 @@ var importsGroup = (project, settings) => {
|
|
|
1258
1635
|
if (!settings.boundaries) {
|
|
1259
1636
|
return overrides;
|
|
1260
1637
|
}
|
|
1261
|
-
const absolute = (paths) => paths.map((path) =>
|
|
1638
|
+
const absolute = (paths) => paths.map((path) => resolve5(project.root, path));
|
|
1262
1639
|
overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
|
|
1263
1640
|
"arch/boundaries-imports": [
|
|
1264
1641
|
"error",
|
|
@@ -1266,7 +1643,7 @@ var importsGroup = (project, settings) => {
|
|
|
1266
1643
|
web: absolute(project.web),
|
|
1267
1644
|
backend: absolute(project.backend),
|
|
1268
1645
|
packages: absolute(project.packages),
|
|
1269
|
-
roles: project.
|
|
1646
|
+
roles: project.fileKinds,
|
|
1270
1647
|
services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
|
|
1271
1648
|
portable: true,
|
|
1272
1649
|
aliases: presetAliases(project.root, settings.aliases),
|
|
@@ -1280,18 +1657,18 @@ var importsGroup = (project, settings) => {
|
|
|
1280
1657
|
};
|
|
1281
1658
|
|
|
1282
1659
|
// src/preset/groups/layout.ts
|
|
1283
|
-
import { posix as
|
|
1660
|
+
import { posix as posix5 } from "node:path";
|
|
1284
1661
|
var layoutGroup = (project, settings) => {
|
|
1285
1662
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
1286
1663
|
const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
|
|
1287
1664
|
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.";
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1665
|
+
const names = settings.files.map((name) => name.split("*").map((part) => part.replaceAll(".", "[.]")).join("[a-z0-9]+(?:-[a-z0-9]+)*"));
|
|
1666
|
+
const kindFiles = project.fileKinds.map((kind) => `**/*.${kind}.ts`);
|
|
1290
1667
|
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
1291
|
-
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path ===
|
|
1668
|
+
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix5.join(path, "components")));
|
|
1292
1669
|
return [
|
|
1293
1670
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
1294
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1671
|
+
const folders = folder === "services" && operations.some((source) => posix5.join(source, "services") === path);
|
|
1295
1672
|
const registries = folder === "services" ? project.registries : [];
|
|
1296
1673
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
1297
1674
|
"arch/files-restricted": [
|
|
@@ -1314,11 +1691,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1314
1691
|
"arch/naming-file": [
|
|
1315
1692
|
"error",
|
|
1316
1693
|
{
|
|
1317
|
-
pattern: `^
|
|
1318
|
-
message: `Domain files use
|
|
1694
|
+
pattern: `^(?:${names.join("|")})$`,
|
|
1695
|
+
message: `Domain files use one of: ${settings.files.join(", ")}. Add a name to layout.files.`
|
|
1319
1696
|
}
|
|
1320
1697
|
]
|
|
1321
|
-
}, [
|
|
1698
|
+
}, [
|
|
1699
|
+
...settings.files.includes("*-action.*.ts") ? ["**/*-action.*.ts"] : [],
|
|
1700
|
+
...settings.files.includes("*-query.*.ts") ? ["**/*-query.*.ts"] : [],
|
|
1701
|
+
...testFiles,
|
|
1702
|
+
...registries
|
|
1703
|
+
]);
|
|
1322
1704
|
return [...domain, strict];
|
|
1323
1705
|
}),
|
|
1324
1706
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
@@ -1335,16 +1717,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1335
1717
|
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
1336
1718
|
"**/*.rsc.tsx",
|
|
1337
1719
|
...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
|
|
1338
|
-
...
|
|
1720
|
+
...kindFiles,
|
|
1339
1721
|
...project.colocated,
|
|
1340
1722
|
...testFiles
|
|
1341
1723
|
]),
|
|
1342
1724
|
presetOverride(services, {
|
|
1343
1725
|
"arch/backend-service-functions": "error",
|
|
1344
1726
|
"arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
|
|
1345
|
-
}, [...utilities, ...testFiles, ...
|
|
1727
|
+
}, [...utilities, ...testFiles, ...kindFiles]),
|
|
1346
1728
|
presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
|
|
1347
|
-
...
|
|
1729
|
+
...kindFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
|
|
1348
1730
|
"arch/backend-service-functions": "off",
|
|
1349
1731
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
1350
1732
|
}, testFiles)),
|
|
@@ -1522,7 +1904,7 @@ var nextGroup = (project, settings) => {
|
|
|
1522
1904
|
// src/preset/groups/orpc.ts
|
|
1523
1905
|
var orpcGroup = (project) => project.orpc.public.length > 0 ? [
|
|
1524
1906
|
presetOverride(project.orpc.public, {
|
|
1525
|
-
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output"
|
|
1907
|
+
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output" }]
|
|
1526
1908
|
})
|
|
1527
1909
|
] : [];
|
|
1528
1910
|
|
|
@@ -1619,12 +2001,10 @@ var shadcnGroup = (project) => {
|
|
|
1619
2001
|
};
|
|
1620
2002
|
|
|
1621
2003
|
// src/preset/groups/sql.ts
|
|
1622
|
-
var sqlGroup = (project
|
|
2004
|
+
var sqlGroup = (project) => [
|
|
1623
2005
|
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
|
|
1624
|
-
"arch/backend-sql-like":
|
|
1625
|
-
|
|
1626
|
-
{ methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
|
|
1627
|
-
]
|
|
2006
|
+
"arch/backend-sql-like": "error",
|
|
2007
|
+
"arch/layout-chain": ["error", { groups: [{ min: 3, methods: ["selectFrom", "insertInto", "updateTable", "deleteFrom", "mergeInto", "with"] }] }]
|
|
1628
2008
|
})
|
|
1629
2009
|
];
|
|
1630
2010
|
|
|
@@ -1677,7 +2057,7 @@ var tanstackGroup = (project, settings) => {
|
|
|
1677
2057
|
"no-restricted-imports": [
|
|
1678
2058
|
"error",
|
|
1679
2059
|
{
|
|
1680
|
-
paths: [
|
|
2060
|
+
paths: [presetTanstackRsc],
|
|
1681
2061
|
patterns: project.baseImports
|
|
1682
2062
|
}
|
|
1683
2063
|
]
|
|
@@ -1713,7 +2093,7 @@ function optionsPatternsTest(patterns, value) {
|
|
|
1713
2093
|
function optionsGlob(pattern) {
|
|
1714
2094
|
const negated = [];
|
|
1715
2095
|
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
1716
|
-
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]
|
|
2096
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\?{}]/g, "\\$&").replaceAll("*", "[^/]*")).join("|"));
|
|
1717
2097
|
return `\x02${negated.length - 1}\x03`;
|
|
1718
2098
|
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
1719
2099
|
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
@@ -1726,6 +2106,9 @@ function optionsAssertMockingApis(patterns) {
|
|
|
1726
2106
|
throw new OptionsError("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
|
|
1727
2107
|
}
|
|
1728
2108
|
}
|
|
2109
|
+
function optionsEscape(value) {
|
|
2110
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2111
|
+
}
|
|
1729
2112
|
|
|
1730
2113
|
// src/preset/groups/tests.ts
|
|
1731
2114
|
var testsGroup = (project, settings) => {
|
|
@@ -1829,12 +2212,12 @@ var typesGroup = (project, settings) => {
|
|
|
1829
2212
|
};
|
|
1830
2213
|
|
|
1831
2214
|
// src/preset/preset-folders.ts
|
|
1832
|
-
import { existsSync as
|
|
1833
|
-
import { posix as
|
|
2215
|
+
import { existsSync as existsSync5, readdirSync as readdirSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
2216
|
+
import { posix as posix8, resolve as resolve8 } from "node:path";
|
|
1834
2217
|
|
|
1835
2218
|
// src/preset/preset-stylesheet.ts
|
|
1836
|
-
import { readdirSync as
|
|
1837
|
-
import { posix as
|
|
2219
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
2220
|
+
import { posix as posix6, resolve as resolve6 } from "node:path";
|
|
1838
2221
|
function presetTailwindEntry(css) {
|
|
1839
2222
|
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1840
2223
|
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
@@ -1845,15 +2228,15 @@ function presetStylesheet(root, directory) {
|
|
|
1845
2228
|
const visited = new Set;
|
|
1846
2229
|
while (queue.length > 0) {
|
|
1847
2230
|
const folder = queue.shift() ?? directory;
|
|
1848
|
-
const real = realpathSync(
|
|
2231
|
+
const real = realpathSync(resolve6(root, folder));
|
|
1849
2232
|
if (visited.has(real)) {
|
|
1850
2233
|
continue;
|
|
1851
2234
|
}
|
|
1852
2235
|
visited.add(real);
|
|
1853
|
-
for (const name of
|
|
1854
|
-
const path =
|
|
1855
|
-
const full =
|
|
1856
|
-
const depth =
|
|
2236
|
+
for (const name of readdirSync3(resolve6(root, folder))) {
|
|
2237
|
+
const path = posix6.join(folder, name);
|
|
2238
|
+
const full = resolve6(root, path);
|
|
2239
|
+
const depth = posix6.relative(directory, path).split("/").length;
|
|
1857
2240
|
if (presetIsDirectory(full)) {
|
|
1858
2241
|
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1859
2242
|
queue.push(path);
|
|
@@ -1864,15 +2247,15 @@ function presetStylesheet(root, directory) {
|
|
|
1864
2247
|
}
|
|
1865
2248
|
}
|
|
1866
2249
|
const rank = (path) => {
|
|
1867
|
-
const index = presetStylesheetNames.indexOf(
|
|
2250
|
+
const index = presetStylesheetNames.indexOf(posix6.basename(path, ".css"));
|
|
1868
2251
|
return index === -1 ? presetStylesheetNames.length : index;
|
|
1869
2252
|
};
|
|
1870
2253
|
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1871
2254
|
}
|
|
1872
2255
|
|
|
1873
2256
|
// src/preset/preset-units.ts
|
|
1874
|
-
import { existsSync as
|
|
1875
|
-
import { posix as
|
|
2257
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
|
|
2258
|
+
import { posix as posix7, resolve as resolve7 } from "node:path";
|
|
1876
2259
|
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1877
2260
|
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1878
2261
|
function presetUnitSource(root, key, kind, source) {
|
|
@@ -1880,9 +2263,9 @@ function presetUnitSource(root, key, kind, source) {
|
|
|
1880
2263
|
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1881
2264
|
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1882
2265
|
}
|
|
1883
|
-
return
|
|
2266
|
+
return posix7.join(key, source);
|
|
1884
2267
|
}
|
|
1885
|
-
return kind === "console" || !
|
|
2268
|
+
return kind === "console" || !existsSync4(resolve7(root, key, "src")) ? key : posix7.join(key, "src");
|
|
1886
2269
|
}
|
|
1887
2270
|
function presetSets(folder, name) {
|
|
1888
2271
|
return folder[name] !== undefined && folder[name] !== false;
|
|
@@ -1911,10 +2294,10 @@ function presetHasManifest(root, key) {
|
|
|
1911
2294
|
return false;
|
|
1912
2295
|
}
|
|
1913
2296
|
if (!key.endsWith("/*")) {
|
|
1914
|
-
return
|
|
2297
|
+
return existsSync4(resolve7(root, key, "package.json"));
|
|
1915
2298
|
}
|
|
1916
|
-
const base =
|
|
1917
|
-
return presetIsDirectory(base) &&
|
|
2299
|
+
const base = resolve7(root, key.slice(0, -2));
|
|
2300
|
+
return presetIsDirectory(base) && readdirSync4(base).some((name) => existsSync4(resolve7(base, name, "package.json")));
|
|
1918
2301
|
}
|
|
1919
2302
|
function presetUnit(root, key, scope) {
|
|
1920
2303
|
const { effective } = scope;
|
|
@@ -1963,20 +2346,24 @@ function presetUnits(root, scopes, ignored) {
|
|
|
1963
2346
|
return [presetUnit(root, scope.key, scope)];
|
|
1964
2347
|
}
|
|
1965
2348
|
const base = scope.key.slice(0, -2);
|
|
1966
|
-
return
|
|
2349
|
+
return readdirSync4(resolve7(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
|
|
1967
2350
|
});
|
|
1968
2351
|
}
|
|
1969
2352
|
|
|
1970
2353
|
// src/preset/preset-folders.ts
|
|
1971
|
-
function
|
|
2354
|
+
function presetPortableKey(key) {
|
|
1972
2355
|
const portable = key.replaceAll("\\", "/");
|
|
1973
|
-
if (!portable.trim() ||
|
|
2356
|
+
if (!portable.trim() || posix8.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1974
2357
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1975
2358
|
}
|
|
1976
|
-
const normalized =
|
|
2359
|
+
const normalized = posix8.normalize(portable).replace(/\/$/, "");
|
|
1977
2360
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1978
2361
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1979
2362
|
}
|
|
2363
|
+
return normalized;
|
|
2364
|
+
}
|
|
2365
|
+
function presetNormalizeKey(key) {
|
|
2366
|
+
const normalized = presetPortableKey(key);
|
|
1980
2367
|
const segments = normalized.split("/");
|
|
1981
2368
|
if (normalized !== "." && segments.some((segment) => segment !== "*" && !presetLiteralSegment.test(segment))) {
|
|
1982
2369
|
throw new PresetError(`Folder keys must be literal paths; only a whole \`*\` segment may be a wildcard: ${key}`);
|
|
@@ -1986,6 +2373,25 @@ function presetNormalizeKey(key) {
|
|
|
1986
2373
|
}
|
|
1987
2374
|
return normalized;
|
|
1988
2375
|
}
|
|
2376
|
+
function presetBraces(key) {
|
|
2377
|
+
const match = /\{([^{}]*)\}/.exec(key);
|
|
2378
|
+
if (!match) {
|
|
2379
|
+
return [key];
|
|
2380
|
+
}
|
|
2381
|
+
return match[1].split(",").flatMap((option) => presetBraces(`${key.slice(0, match.index)}${option}${key.slice(match.index + match[0].length)}`));
|
|
2382
|
+
}
|
|
2383
|
+
function presetIsFileKey(root, key) {
|
|
2384
|
+
const name = key.split("/").at(-1) ?? "";
|
|
2385
|
+
const pattern = key.includes("*");
|
|
2386
|
+
if (!pattern && !existsSync5(resolve8(root, key)) && /[^.]\.[^.]+$/.test(name)) {
|
|
2387
|
+
throw new PresetError(`File \`${key}\` does not exist.`);
|
|
2388
|
+
}
|
|
2389
|
+
const file = pattern ? name !== "*" && name.includes(".") : presetIsFile(resolve8(root, key));
|
|
2390
|
+
if (file && !key.split("/").every((segment) => presetFileSegment.test(segment))) {
|
|
2391
|
+
throw new PresetError(`File keys hold letters, digits, \`._-@$[]\` and \`*\` wildcards: ${key}`);
|
|
2392
|
+
}
|
|
2393
|
+
return file;
|
|
2394
|
+
}
|
|
1989
2395
|
function presetMatches(pattern, path) {
|
|
1990
2396
|
return pattern.every((segment, index) => segment === "*" || segment === path[index]);
|
|
1991
2397
|
}
|
|
@@ -2021,13 +2427,28 @@ function presetGroupValue(name, value, inherited, severity) {
|
|
|
2021
2427
|
return { ...base, ...merged, level: severity ?? base.level };
|
|
2022
2428
|
}
|
|
2023
2429
|
function presetDenyEntries(inherited, changes) {
|
|
2024
|
-
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).
|
|
2430
|
+
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).map(([key, message]) => {
|
|
2025
2431
|
if (message !== false && typeof message !== "string") {
|
|
2026
2432
|
throw new PresetError(`Deny entry \`${key}\` takes a message string or false.`);
|
|
2027
2433
|
}
|
|
2028
|
-
return
|
|
2434
|
+
return [key, message];
|
|
2029
2435
|
}));
|
|
2030
2436
|
}
|
|
2437
|
+
function presetLayoutNames(inherited, entries) {
|
|
2438
|
+
if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string" && presetLayoutFileName.test(entry))) {
|
|
2439
|
+
throw new PresetError("layout.files takes file names such as `*.prompts.ts`; `!*.utils.ts` removes one.");
|
|
2440
|
+
}
|
|
2441
|
+
const names = [...inherited];
|
|
2442
|
+
for (const entry of entries) {
|
|
2443
|
+
const index = names.indexOf(entry.startsWith("!") ? entry.slice(1) : entry);
|
|
2444
|
+
if (entry.startsWith("!") && index !== -1) {
|
|
2445
|
+
names.splice(index, 1);
|
|
2446
|
+
} else if (!entry.startsWith("!") && index === -1) {
|
|
2447
|
+
names.push(entry);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
return names;
|
|
2451
|
+
}
|
|
2031
2452
|
function presetEffective(folder, parent) {
|
|
2032
2453
|
const resolveGroup = (name) => {
|
|
2033
2454
|
const defaults = presetDefaultGroups.includes(name) ? presetGroupOptions[name] : null;
|
|
@@ -2043,6 +2464,11 @@ function presetEffective(folder, parent) {
|
|
|
2043
2464
|
calls: presetDenyEntries(inherited.calls, deny.calls)
|
|
2044
2465
|
};
|
|
2045
2466
|
}
|
|
2467
|
+
const layout = folder.layout;
|
|
2468
|
+
if (effective.layout && layout instanceof Object && layout.files !== undefined) {
|
|
2469
|
+
const inherited = (parent ? parent.layout?.files : null) ?? presetGroupOptions.layout.files;
|
|
2470
|
+
effective.layout = { ...effective.layout, files: presetLayoutNames(inherited, layout.files) };
|
|
2471
|
+
}
|
|
2046
2472
|
return effective;
|
|
2047
2473
|
}
|
|
2048
2474
|
function presetFolder(key, value) {
|
|
@@ -2073,7 +2499,7 @@ function presetScopesTree(root, folders) {
|
|
|
2073
2499
|
}
|
|
2074
2500
|
for (const key of keys) {
|
|
2075
2501
|
const base = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
2076
|
-
if (!
|
|
2502
|
+
if (!existsSync5(resolve8(root, base))) {
|
|
2077
2503
|
throw new PresetError(`Folder \`${key}\` does not exist.`);
|
|
2078
2504
|
}
|
|
2079
2505
|
}
|
|
@@ -2105,14 +2531,26 @@ function presetScopesTree(root, folders) {
|
|
|
2105
2531
|
return scopes;
|
|
2106
2532
|
}
|
|
2107
2533
|
function presetProject(config) {
|
|
2108
|
-
const given =
|
|
2109
|
-
const root =
|
|
2110
|
-
const
|
|
2111
|
-
const
|
|
2534
|
+
const given = resolve8(config.root ?? process.cwd());
|
|
2535
|
+
const root = existsSync5(given) ? realpathSync2(given) : given;
|
|
2536
|
+
const expanded = Object.entries(config.folders ?? {}).flatMap(([key, value]) => presetBraces(key).map((path) => [presetPortableKey(path), value, key.includes("{")]));
|
|
2537
|
+
const files = expanded.filter(([key]) => presetIsFileKey(root, key));
|
|
2538
|
+
const listed = expanded.filter((entry) => !files.includes(entry)).map(([key, value, fromList]) => [presetNormalizeKey(key), value, fromList]);
|
|
2539
|
+
const twice = listed.find(([key, , fromList], index) => listed.some(([other, , otherFromList], at) => at < index && other === key && !fromList && !otherFromList));
|
|
2112
2540
|
if (twice) {
|
|
2113
2541
|
throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
|
|
2114
2542
|
}
|
|
2115
|
-
const
|
|
2543
|
+
const configured = {};
|
|
2544
|
+
for (const [key, value] of listed) {
|
|
2545
|
+
configured[key] = key in configured ? presetMerge(configured[key], value) : value;
|
|
2546
|
+
}
|
|
2547
|
+
const detected = config.detect === false ? configured : presetDetect(root, configured);
|
|
2548
|
+
const conventions = Object.entries(detected).filter(([key]) => !(key in configured) && presetIsFileKey(root, key));
|
|
2549
|
+
const folders = Object.fromEntries(Object.entries(detected).filter(([key]) => !conventions.some(([other]) => other === key)));
|
|
2550
|
+
const fileEntries = [
|
|
2551
|
+
...conventions.filter(([key]) => !files.some(([other]) => other === key)),
|
|
2552
|
+
...files.map(([key, value]) => [key, key in detected ? presetMerge(detected[key], value) : value])
|
|
2553
|
+
];
|
|
2116
2554
|
const scopes = presetScopesTree(root, folders);
|
|
2117
2555
|
const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
|
|
2118
2556
|
const live = scopes.filter((scope) => !ignored.has(scope.key));
|
|
@@ -2120,6 +2558,24 @@ function presetProject(config) {
|
|
|
2120
2558
|
if (dead) {
|
|
2121
2559
|
throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
|
|
2122
2560
|
}
|
|
2561
|
+
const keyFor = (pattern, folder) => {
|
|
2562
|
+
const reached = live.filter((scope) => presetIntersect(pattern, scope.key).length > 0).map((scope) => ({ key: scope.key, parent: scope, depth: scope.depth, folder, effective: presetEffective(folder, scope.effective) }));
|
|
2563
|
+
return { pattern, folder, scopes: reached };
|
|
2564
|
+
};
|
|
2565
|
+
const fileKeys = presetFileOverlaps(root, keyFor, fileEntries.flatMap(([key, value]) => {
|
|
2566
|
+
if (value === false) {
|
|
2567
|
+
return [];
|
|
2568
|
+
}
|
|
2569
|
+
const folder = presetFolder(key, value);
|
|
2570
|
+
const option = Object.keys(folder).find((name) => presetFolderOnlyKeys.includes(name));
|
|
2571
|
+
if (option) {
|
|
2572
|
+
throw new PresetError(`Set \`${option}\` on a folder, not on \`${key}\`.`);
|
|
2573
|
+
}
|
|
2574
|
+
if (Object(folder.layout).operations !== undefined) {
|
|
2575
|
+
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${key}\`.`);
|
|
2576
|
+
}
|
|
2577
|
+
return [keyFor(presetGlobEscape(key), folder)];
|
|
2578
|
+
}));
|
|
2123
2579
|
const units = presetUnits(root, scopes, ignored);
|
|
2124
2580
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
2125
2581
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
@@ -2130,10 +2586,6 @@ function presetProject(config) {
|
|
|
2130
2586
|
if (misplaced) {
|
|
2131
2587
|
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
|
|
2132
2588
|
}
|
|
2133
|
-
const writes = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.backend).writes !== undefined);
|
|
2134
|
-
if (writes) {
|
|
2135
|
-
throw new PresetError(`Set backend.writes on a backend folder, not on \`${writes.key}\`.`);
|
|
2136
|
-
}
|
|
2137
2589
|
const of = (kind) => units.filter((unit) => unit.kind === kind);
|
|
2138
2590
|
const webUnits = of("web");
|
|
2139
2591
|
const web = webUnits.map((unit) => unit.source);
|
|
@@ -2147,19 +2599,15 @@ function presetProject(config) {
|
|
|
2147
2599
|
const appFiles = presetScopes(app);
|
|
2148
2600
|
const webFiles = presetScopes(web);
|
|
2149
2601
|
const effective = live.map((scope) => scope.effective);
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
}
|
|
2154
|
-
if (suffixes.some((suffix) => presetBuiltinFileRoles.includes(suffix))) {
|
|
2155
|
-
throw new PresetError("Layout suffixes must not replace built-in file roles.");
|
|
2156
|
-
}
|
|
2602
|
+
const fileKinds = [
|
|
2603
|
+
...new Set([...effective, ...fileKeys.flatMap((fileKey) => fileKey.scopes.map((scope) => scope.effective))].flatMap((groups) => (groups.layout?.files ?? []).flatMap((name) => presetLayoutFiles.includes(name) ? [] : /^\*\.([a-z][a-z0-9-]*)\.ts$/.exec(name)?.[1] ?? [])))
|
|
2604
|
+
];
|
|
2157
2605
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
2158
2606
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
2159
2607
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
2160
|
-
const rpcClients = client === undefined ? webUnits.map((unit) =>
|
|
2608
|
+
const rpcClients = client === undefined ? webUnits.map((unit) => posix8.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
|
|
2161
2609
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
2162
|
-
const uiRoots = [...new Set([...web.map((path) =>
|
|
2610
|
+
const uiRoots = [...new Set([...web.map((path) => posix8.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
|
|
2163
2611
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
2164
2612
|
const stylesheet = (unit) => {
|
|
2165
2613
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -2167,7 +2615,7 @@ function presetProject(config) {
|
|
|
2167
2615
|
return;
|
|
2168
2616
|
}
|
|
2169
2617
|
const css = tailwind.css ?? presetStylesheet(root, unit.key);
|
|
2170
|
-
return css === undefined ? undefined :
|
|
2618
|
+
return css === undefined ? undefined : resolve8(root, css);
|
|
2171
2619
|
};
|
|
2172
2620
|
const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
|
|
2173
2621
|
const css = stylesheet(unit);
|
|
@@ -2201,21 +2649,21 @@ function presetProject(config) {
|
|
|
2201
2649
|
return [key];
|
|
2202
2650
|
}
|
|
2203
2651
|
const base = key.slice(0, -2);
|
|
2204
|
-
return
|
|
2652
|
+
return readdirSync5(resolve8(root, base)).filter((name) => presetIsDirectory(resolve8(root, base, name))).map((name) => posix8.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
|
|
2205
2653
|
});
|
|
2206
2654
|
return {
|
|
2207
2655
|
root,
|
|
2208
2656
|
serviceFolders,
|
|
2209
2657
|
scopes: live,
|
|
2658
|
+
fileKeys,
|
|
2210
2659
|
units,
|
|
2211
|
-
|
|
2660
|
+
fileKinds,
|
|
2212
2661
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
2213
2662
|
orpc: {
|
|
2214
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2215
|
-
composers: listOf(orpcScopes.map((settings) => settings.composers)),
|
|
2663
|
+
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix8.join(unit.source, "rpc/api")).filter((path) => existsSync5(resolve8(root, path))).map((path) => `${path}/**`),
|
|
2216
2664
|
client: client ?? ""
|
|
2217
2665
|
},
|
|
2218
|
-
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services :
|
|
2666
|
+
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix8.join(unit.source, folder), folder, mode }))),
|
|
2219
2667
|
rpcClients,
|
|
2220
2668
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
2221
2669
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -2253,7 +2701,7 @@ function presetProject(config) {
|
|
|
2253
2701
|
baseImports: presetBaseImports,
|
|
2254
2702
|
css: { ...webCss, ...kitCss },
|
|
2255
2703
|
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
|
|
2256
|
-
ignores: skipped.map((key) => `${key}/**`),
|
|
2704
|
+
ignores: [...skipped.map((key) => `${key}/**`), ...fileEntries.flatMap(([key, value]) => value === false ? [presetGlobEscape(key)] : [])],
|
|
2257
2705
|
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
2258
2706
|
};
|
|
2259
2707
|
}
|
|
@@ -2262,7 +2710,7 @@ function presetProject(config) {
|
|
|
2262
2710
|
var recipes = presetRecipes;
|
|
2263
2711
|
var templates = presetTemplates;
|
|
2264
2712
|
var externals = presetExternals;
|
|
2265
|
-
var require2 =
|
|
2713
|
+
var require2 = createRequire3(import.meta.url);
|
|
2266
2714
|
function presetOptional(name, group) {
|
|
2267
2715
|
try {
|
|
2268
2716
|
return require2.resolve(name);
|
|
@@ -2311,41 +2759,70 @@ function presetSettings(scope, name) {
|
|
|
2311
2759
|
}
|
|
2312
2760
|
return settings ? null : { level: presetLevel(scope) };
|
|
2313
2761
|
}
|
|
2314
|
-
function
|
|
2762
|
+
function presetKeyFiles(fileKey, scope, globs, expand) {
|
|
2763
|
+
const scoped = globs.flatMap((glob) => presetIntersect(glob, scope.key));
|
|
2764
|
+
const within = (glob) => {
|
|
2765
|
+
const prefix = glob.slice(0, -`**/${presetExtensions}`.length).replace(/\/$/, "");
|
|
2766
|
+
return glob.endsWith(`**/${presetExtensions}`) && !/[*?{[]/.test(prefix) && (prefix === "" || fileKey.pattern.startsWith(`${prefix}/`)) && /\.(?:ts|tsx|mts|cts)$/.test(fileKey.pattern);
|
|
2767
|
+
};
|
|
2768
|
+
return scoped.some(within) ? presetIntersect(fileKey.pattern, scope.key) : expand(fileKey).filter((path) => scoped.some((glob) => matchesGlob2(path, glob))).map(presetGlobEscape);
|
|
2769
|
+
}
|
|
2770
|
+
function presetEmit(project, name, builder, expand) {
|
|
2315
2771
|
const key = (scope) => JSON.stringify(presetSettings(scope, name));
|
|
2316
2772
|
const breaks = project.scopes.filter((scope) => scope.parent === null || key(scope) !== key(scope.parent));
|
|
2317
|
-
const
|
|
2318
|
-
let current = scope
|
|
2319
|
-
while (current &&
|
|
2773
|
+
const region = (scope) => {
|
|
2774
|
+
let current = scope;
|
|
2775
|
+
while (current && !breaks.includes(current)) {
|
|
2320
2776
|
current = current.parent;
|
|
2321
2777
|
}
|
|
2322
|
-
return
|
|
2323
|
-
}
|
|
2778
|
+
return current;
|
|
2779
|
+
};
|
|
2780
|
+
const keyed = project.fileKeys.flatMap((fileKey) => fileKey.scopes.filter((scope) => scope.parent && key(scope) !== key(scope.parent)).map((scope) => ({ owner: region(scope.parent), fileKey, scope })));
|
|
2324
2781
|
return breaks.flatMap((owner) => {
|
|
2325
2782
|
const settings = presetSettings(owner, name);
|
|
2326
2783
|
if (!settings) {
|
|
2327
2784
|
return [];
|
|
2328
2785
|
}
|
|
2329
|
-
const
|
|
2786
|
+
const nested = breaks.filter((scope) => scope !== owner && region(scope.parent) === owner).map((scope) => `${scope.key}/**`);
|
|
2787
|
+
const owned = keyed.filter((entry) => entry.owner === owner);
|
|
2330
2788
|
return builder(project, settings, owner).flatMap((override) => {
|
|
2331
2789
|
const files = override.files.flatMap((glob) => presetIntersect(glob, owner.key));
|
|
2332
2790
|
if (files.length === 0 || !override.rules) {
|
|
2333
2791
|
return [];
|
|
2334
2792
|
}
|
|
2793
|
+
const replaced = owned.flatMap((entry) => presetKeyFiles(entry.fileKey, entry.scope, override.files, expand));
|
|
2335
2794
|
return [
|
|
2336
2795
|
{
|
|
2337
2796
|
...override,
|
|
2338
2797
|
files,
|
|
2339
|
-
excludeFiles: [...override.excludeFiles ?? [], ...
|
|
2798
|
+
excludeFiles: [...override.excludeFiles ?? [], ...nested, ...replaced],
|
|
2340
2799
|
rules: presetSeverity(override.rules, settings.level)
|
|
2341
2800
|
}
|
|
2342
2801
|
];
|
|
2343
2802
|
});
|
|
2344
2803
|
});
|
|
2345
2804
|
}
|
|
2805
|
+
function presetFileEmit(project, name, builder, expand) {
|
|
2806
|
+
return project.fileKeys.flatMap((fileKey) => fileKey.scopes.flatMap((scope) => {
|
|
2807
|
+
const owner = scope.parent;
|
|
2808
|
+
const settings = presetSettings(scope, name);
|
|
2809
|
+
if (!owner || !settings || JSON.stringify(presetSettings(owner, name)) === JSON.stringify(settings)) {
|
|
2810
|
+
return [];
|
|
2811
|
+
}
|
|
2812
|
+
const nested = project.scopes.filter((other) => other.parent === owner).map((other) => `${other.key}/**`);
|
|
2813
|
+
return builder(project, settings, scope).flatMap((override) => {
|
|
2814
|
+
const files = presetKeyFiles(fileKey, scope, override.files, expand);
|
|
2815
|
+
if (files.length === 0 || !override.rules) {
|
|
2816
|
+
return [];
|
|
2817
|
+
}
|
|
2818
|
+
return [{ ...override, files, excludeFiles: [...override.excludeFiles ?? [], ...nested], rules: presetSeverity(override.rules, settings.level) }];
|
|
2819
|
+
});
|
|
2820
|
+
}));
|
|
2821
|
+
}
|
|
2346
2822
|
function presetPlugins(project) {
|
|
2347
2823
|
const react = project.scopes.some((scope) => scope.effective.react);
|
|
2348
2824
|
const effect = project.scopes.some((scope) => scope.effective.effect);
|
|
2825
|
+
const effectCode = project.scopes.some((scope) => scope.effective.effect?.code);
|
|
2349
2826
|
const tailwinds = project.scopes.flatMap((scope) => scope.effective.tailwind ? [scope.effective.tailwind] : []);
|
|
2350
2827
|
const shadcns = project.scopes.flatMap((scope) => scope.effective.shadcn ? [scope.effective.shadcn] : []);
|
|
2351
2828
|
const tailwind = tailwinds.at(0);
|
|
@@ -2368,11 +2845,14 @@ function presetPlugins(project) {
|
|
|
2368
2845
|
{ name: "dillon-anti-slop", specifier: require2.resolve("oxlint-plugin-arch/dillon-anti-slop") }
|
|
2369
2846
|
];
|
|
2370
2847
|
const plugins = ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"];
|
|
2371
|
-
if (
|
|
2848
|
+
if (effectCode) {
|
|
2372
2849
|
const { tsgo, linteffect } = presetEffectPackages();
|
|
2373
2850
|
plugins.push(...tsgo.correctness.plugins ?? []);
|
|
2374
2851
|
jsPlugins.push(...linteffect.recommended.jsPlugins);
|
|
2375
2852
|
}
|
|
2853
|
+
if (effect) {
|
|
2854
|
+
jsPlugins.push(...presetEffectStyle().jsPlugins);
|
|
2855
|
+
}
|
|
2376
2856
|
const settings = {
|
|
2377
2857
|
react: { version: "19" },
|
|
2378
2858
|
"import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
|
|
@@ -2395,17 +2875,29 @@ function preset(config) {
|
|
|
2395
2875
|
const project = presetProject(config);
|
|
2396
2876
|
const [root] = project.scopes;
|
|
2397
2877
|
const level = presetLevel(root);
|
|
2878
|
+
const expanded = new Map;
|
|
2879
|
+
const expand = (fileKey) => {
|
|
2880
|
+
const paths = expanded.get(fileKey.pattern) ?? globSync2(fileKey.pattern, { cwd: project.root, exclude: (path) => posix9.basename(path) === "node_modules" });
|
|
2881
|
+
expanded.set(fileKey.pattern, paths);
|
|
2882
|
+
return paths;
|
|
2883
|
+
};
|
|
2398
2884
|
const emitGroup = (name) => {
|
|
2399
2885
|
const builder = presetBuilders[name];
|
|
2400
|
-
return builder ? presetEmit(project, name, builder) : [];
|
|
2886
|
+
return builder ? [...presetEmit(project, name, builder, expand), ...presetFileEmit(project, name, builder, expand)] : [];
|
|
2401
2887
|
};
|
|
2402
2888
|
const severities = project.scopes.filter((scope) => scope.parent && presetLevel(scope) !== presetLevel(scope.parent)).toSorted((left, right) => left.depth - right.depth).map((scope) => ({ files: [`${scope.key}/**`], rules: presetSeverity(presetCommonRules, presetLevel(scope)) }));
|
|
2403
|
-
const
|
|
2889
|
+
const fileSeverities = project.fileKeys.flatMap((fileKey) => fileKey.folder.severity ? [{ files: [fileKey.pattern], rules: presetSeverity(presetCommonRules, fileKey.folder.severity) }] : []);
|
|
2890
|
+
const overrides = [...severities, ...fileSeverities, ...presetGroupOrder.flatMap(emitGroup)];
|
|
2404
2891
|
for (const scope of project.scopes.toSorted((left, right) => left.depth - right.depth)) {
|
|
2405
2892
|
if (scope.folder.rules) {
|
|
2406
2893
|
overrides.push({ files: [scope.key === "." ? "**" : `${scope.key}/**`], rules: scope.folder.rules });
|
|
2407
2894
|
}
|
|
2408
2895
|
}
|
|
2896
|
+
for (const fileKey of project.fileKeys) {
|
|
2897
|
+
if (fileKey.folder.rules) {
|
|
2898
|
+
overrides.push({ files: [fileKey.pattern], rules: fileKey.folder.rules });
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2409
2901
|
return {
|
|
2410
2902
|
options: { typeAware: true },
|
|
2411
2903
|
categories: { correctness: level },
|