oxlint-plugin-arch 0.12.6 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +3 -3
- package/dist/index.js +396 -149
- 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 +17 -2
- package/dist/preset/preset.d.ts +11 -1
- package/dist/preset/preset.js +681 -221
- package/dist/preset/preset.utils.d.ts +5 -0
- package/dist/rules/code/code-delegate.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/code.types.d.ts +6 -0
- package/dist/types/files.types.d.ts +3 -2
- package/dist/types/preset.types.d.ts +26 -24
- package/dist/types/react.types.d.ts +4 -0
- package/dist/types/types.types.d.ts +4 -0
- package/dist/vendor/dillon-anti-slop/index.js +1 -1
- package/package.json +15 -1
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 } 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,13 @@ 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 presetRecipeMark = Symbol("recipe");
|
|
520
515
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
521
516
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
522
517
|
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
@@ -528,12 +523,73 @@ var presetKitImports = ["**/kits", "**/kits/index", "**/kits/index.ts"];
|
|
|
528
523
|
var presetKitFiles = "^(?:kit|[a-z0-9]+(?:-[a-z0-9]+)*\\.kit)\\.ts$";
|
|
529
524
|
var presetCliFiles = "^cli(?:-[a-z0-9]+)*(?:\\.[a-z0-9-]+)*\\.tsx?$";
|
|
530
525
|
var presetMigrationFiles = "^(?:\\d{3,14}[-_][a-z0-9]+(?:[-_][a-z0-9]+)*|index)\\.ts$";
|
|
531
|
-
var presetMigrationNumber = "^(\\d{3,14})[-_]";
|
|
526
|
+
var presetMigrationNumber = "^(\\d{4}-\\d{2}-\\d{2}(?:-\\d{2})*|\\d{3,14})[-_]";
|
|
532
527
|
var presetRootImports = { "framer-motion": "Use motion/react." };
|
|
533
528
|
var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
|
|
534
529
|
|
|
530
|
+
// src/preset/preset-recipes.ts
|
|
531
|
+
var presetUi = { react: true, kit: true, tailwind: true };
|
|
532
|
+
function presetRecipe(name, recipe) {
|
|
533
|
+
return { ...recipe, [presetRecipeMark]: name };
|
|
534
|
+
}
|
|
535
|
+
var presetRecipes = {
|
|
536
|
+
tanstack: presetRecipe("tanstack", {
|
|
537
|
+
react: true,
|
|
538
|
+
tanstack: true,
|
|
539
|
+
tailwind: true,
|
|
540
|
+
shadcn: true,
|
|
541
|
+
schemas: true,
|
|
542
|
+
forms: true
|
|
543
|
+
}),
|
|
544
|
+
next: presetRecipe("next", {
|
|
545
|
+
react: true,
|
|
546
|
+
next: true,
|
|
547
|
+
tailwind: true,
|
|
548
|
+
shadcn: true,
|
|
549
|
+
schemas: true,
|
|
550
|
+
forms: true
|
|
551
|
+
}),
|
|
552
|
+
expo: presetRecipe("expo", {
|
|
553
|
+
react: true,
|
|
554
|
+
expo: true,
|
|
555
|
+
schemas: true,
|
|
556
|
+
forms: true
|
|
557
|
+
}),
|
|
558
|
+
backend: presetRecipe("backend", {
|
|
559
|
+
backend: true,
|
|
560
|
+
schemas: true,
|
|
561
|
+
sql: true,
|
|
562
|
+
orpc: true,
|
|
563
|
+
layout: { strict: true }
|
|
564
|
+
}),
|
|
565
|
+
react: presetRecipe("react", {
|
|
566
|
+
react: true,
|
|
567
|
+
tailwind: true,
|
|
568
|
+
schemas: true
|
|
569
|
+
}),
|
|
570
|
+
desktop: presetRecipe("desktop", { console: true }),
|
|
571
|
+
library: presetRecipe("library", { schemas: true }),
|
|
572
|
+
effect: presetRecipe("effect", {
|
|
573
|
+
schemas: true,
|
|
574
|
+
effect: true,
|
|
575
|
+
deny: { imports: presetEffectImports }
|
|
576
|
+
}),
|
|
577
|
+
ui: presetRecipe("ui", presetUi),
|
|
578
|
+
kit: presetRecipe("kit", presetUi),
|
|
579
|
+
cli: presetRecipe("cli", { console: true })
|
|
580
|
+
};
|
|
581
|
+
var presetTemplates = {
|
|
582
|
+
monorepo: {
|
|
583
|
+
"apps/web": presetRecipes.tanstack,
|
|
584
|
+
"apps/server": presetRecipes.backend,
|
|
585
|
+
"packages/*": presetRecipes.library,
|
|
586
|
+
scripts: presetRecipes.cli
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
535
590
|
// src/preset/preset.utils.ts
|
|
536
|
-
import { readFileSync, statSync } from "node:fs";
|
|
591
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
592
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
537
593
|
import { posix, resolve } from "node:path";
|
|
538
594
|
class PresetError extends Error {
|
|
539
595
|
}
|
|
@@ -549,6 +605,33 @@ async function presetEffectImport(load) {
|
|
|
549
605
|
}
|
|
550
606
|
var presetEffectTsgo = await presetEffectImport(import("@effect/tsgo/oxlint-presets"));
|
|
551
607
|
var presetEffectLinteffect = await presetEffectImport(import("@opsydyn/oxlint-effect"));
|
|
608
|
+
var presetEffectRegexp = await presetEffectImport(import("eslint-plugin-regexp"));
|
|
609
|
+
var presetEffectDeMorgan = await presetEffectImport(import("eslint-plugin-de-morgan"));
|
|
610
|
+
var presetRequire = createRequire2(import.meta.url);
|
|
611
|
+
function presetResolve(name) {
|
|
612
|
+
try {
|
|
613
|
+
return presetRequire.resolve(name);
|
|
614
|
+
} catch {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function presetEffectStyle() {
|
|
619
|
+
const sonarjs = presetResolve("eslint-plugin-sonarjs");
|
|
620
|
+
const regexp = presetEffectRegexp ? presetResolve("eslint-plugin-regexp") : undefined;
|
|
621
|
+
const deMorgan = presetEffectDeMorgan ? presetResolve("eslint-plugin-de-morgan") : undefined;
|
|
622
|
+
const rules = { ...presetEffectRegexp?.configs["flat/recommended"].rules, ...presetEffectDeMorgan?.configs.recommended.rules };
|
|
623
|
+
if (sonarjs) {
|
|
624
|
+
rules["sonarjs/super-linear-regex"] = "error";
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
rules,
|
|
628
|
+
jsPlugins: [
|
|
629
|
+
...regexp ? [{ name: "regexp", specifier: regexp }] : [],
|
|
630
|
+
...deMorgan ? [{ name: "de-morgan", specifier: deMorgan }] : [],
|
|
631
|
+
...sonarjs ? [{ name: "sonarjs", specifier: sonarjs }] : []
|
|
632
|
+
]
|
|
633
|
+
};
|
|
634
|
+
}
|
|
552
635
|
function presetEffectPackages() {
|
|
553
636
|
if (!presetEffectTsgo || !presetEffectLinteffect) {
|
|
554
637
|
throw new PresetError("The effect group needs @effect/tsgo and @opsydyn/oxlint-effect: bun add -d @effect/tsgo @opsydyn/oxlint-effect");
|
|
@@ -617,6 +700,9 @@ function presetIntersect(glob, scope) {
|
|
|
617
700
|
const matches = presetIntersectSegments(glob.split("/"), scope.split("/")).map((segments) => segments.join("/"));
|
|
618
701
|
return [...new Set(matches)];
|
|
619
702
|
}
|
|
703
|
+
function presetGlobEscape(path) {
|
|
704
|
+
return path.replaceAll(/[[\]]/g, (bracket) => `[${bracket}]`);
|
|
705
|
+
}
|
|
620
706
|
function presetAliases(root, aliases) {
|
|
621
707
|
return Object.fromEntries(Object.entries(aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve(root, path)]));
|
|
622
708
|
}
|
|
@@ -675,77 +761,328 @@ function presetIsFile(path) {
|
|
|
675
761
|
return false;
|
|
676
762
|
}
|
|
677
763
|
}
|
|
764
|
+
function presetSource(root, directory, configured) {
|
|
765
|
+
if (configured !== undefined) {
|
|
766
|
+
return posix.join(directory, configured);
|
|
767
|
+
}
|
|
768
|
+
return existsSync(resolve(root, directory, "src")) ? posix.join(directory, "src") : directory;
|
|
769
|
+
}
|
|
770
|
+
function presetFilled(conventions, exists) {
|
|
771
|
+
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
772
|
+
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
773
|
+
return exists(path) && !empty;
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
function presetMergeObject(base, top, depth) {
|
|
777
|
+
const merged = { ...base };
|
|
778
|
+
for (const [key, value] of Object.entries(top)) {
|
|
779
|
+
const previous = Reflect.get(merged, key);
|
|
780
|
+
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
781
|
+
if (nested && depth > 0) {
|
|
782
|
+
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
783
|
+
} else if (value !== true || !(previous instanceof Object)) {
|
|
784
|
+
Reflect.set(merged, key, value);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return merged;
|
|
788
|
+
}
|
|
789
|
+
function presetMerge(base, top) {
|
|
790
|
+
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
791
|
+
return top === true && base instanceof Object ? base : top;
|
|
792
|
+
}
|
|
793
|
+
const merged = presetMergeObject(base, top, 2);
|
|
794
|
+
const before = base.layout instanceof Object ? base.layout.files : undefined;
|
|
795
|
+
const after = top.layout instanceof Object ? top.layout.files : undefined;
|
|
796
|
+
return before && after && merged.layout instanceof Object ? { ...merged, layout: { ...merged.layout, files: [...before, ...after] } } : merged;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/preset/preset-effect.ts
|
|
800
|
+
var presetEffectQueryWrites = Object.fromEntries(presetQueryWrites.map((name) => [name, "A queries file only reads; write in an actions file."]));
|
|
801
|
+
var presetEffectWorkflowCalls = Object.fromEntries(["selectFrom", ...presetQueryWrites].map((name) => [name, "A workflow reaches data through its domain service, not the database."]));
|
|
802
|
+
var presetEffectFrameworks = Object.fromEntries(["@tanstack/*", "react-dom", "react-dom/**", "next", "next/**", "vite"].map((name) => [name, "The server imports no web framework."]));
|
|
803
|
+
var presetEffectRouters = ["@tanstack/react-router", "next/link", "next/navigation", "next/router", "expo-router"];
|
|
804
|
+
var presetEffectErrors = ["Schema.TaggedError", "Schema.ErrorClass", "Data.TaggedError", "Data.Error"];
|
|
805
|
+
var presetEffectContractNames = "^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.]schemas|[.]admin[.]schemas|[.]contract|[.]admin[.]contract|[.]errors|[.]layer[.]types)[.]ts$";
|
|
806
|
+
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.";
|
|
807
|
+
var presetEffectSchemaUses = {
|
|
808
|
+
namespaces: ["Schema"],
|
|
809
|
+
methods: ["Array", "Union", "Record", "Tuple"],
|
|
810
|
+
structural: ["Struct", "Array", "Union", "Record", "Tuple", "NullOr", "UndefinedOr", "optional", "TaggedStruct"]
|
|
811
|
+
};
|
|
812
|
+
function presetEffectExists(root, key) {
|
|
813
|
+
return (path) => {
|
|
814
|
+
const segments = path.split("/");
|
|
815
|
+
const wildcard = segments.findIndex((segment) => segment.includes("*"));
|
|
816
|
+
return existsSync2(resolve2(root, key, ...wildcard === -1 ? segments : segments.slice(0, wildcard)));
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function presetEffectAt(root, key, value) {
|
|
820
|
+
const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
|
|
821
|
+
return (path) => source === "." ? path : posix2.join(source, path);
|
|
822
|
+
}
|
|
823
|
+
function presetEffectCode(value) {
|
|
824
|
+
return value instanceof Object && Boolean(value.effect) && !(value.effect instanceof Object && value.effect.code === false);
|
|
825
|
+
}
|
|
826
|
+
function presetEffectContract(root, key, value) {
|
|
827
|
+
if (!presetEffectCode(value) || presetEffectDomains(root, key, value)) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const source = resolve2(root, presetSource(root, key, value.source));
|
|
831
|
+
return presetIsDirectory(source) && readdirSync(source).some((name) => presetIsFile(resolve2(source, name, `${name}.contract.ts`)));
|
|
832
|
+
}
|
|
833
|
+
function presetEffectDomains(root, key, value) {
|
|
834
|
+
return presetEffectCode(value) && presetIsDirectory(resolve2(root, presetSource(root, key, value.source), "domains"));
|
|
835
|
+
}
|
|
836
|
+
function presetEffectServers(root, merged, units) {
|
|
837
|
+
for (const unit of units) {
|
|
838
|
+
if (presetEffectDomains(root, unit[0], unit[1])) {
|
|
839
|
+
unit[1] = presetMerge({ ...presetRecipes.backend, services: "domains" }, unit[1]);
|
|
840
|
+
merged[unit[0]] = unit[1];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function presetEffectServerKeys(at) {
|
|
845
|
+
return {
|
|
846
|
+
[at(".")]: {
|
|
847
|
+
deny: { imports: { ...presetEffectFrameworks, "effect/Config": "Read config only in application/app.config.ts." } },
|
|
848
|
+
rules: {
|
|
849
|
+
"arch/types-declarations": ["error", { modules: true }],
|
|
850
|
+
"arch/types-inline": ["error", { variables: true, assertions: true, arguments: true }],
|
|
851
|
+
"arch/comments-code": ["error", { why: false, jsdoc: true, allow: ["^\\s*SAFETY: "] }]
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
[at("domains")]: {
|
|
855
|
+
layout: {
|
|
856
|
+
files: [
|
|
857
|
+
"*.actions.ts",
|
|
858
|
+
"*.queries.ts",
|
|
859
|
+
"*.layer.ts",
|
|
860
|
+
"*.workflows.ts",
|
|
861
|
+
"*.prompts.ts",
|
|
862
|
+
"*-*.driver.ts",
|
|
863
|
+
"!*.service.ts",
|
|
864
|
+
"!*.client.ts",
|
|
865
|
+
"!*.server.ts",
|
|
866
|
+
"!*.rsc.ts",
|
|
867
|
+
"!*.rsc.tsx",
|
|
868
|
+
"!*.utils.ts",
|
|
869
|
+
"!*.types.ts",
|
|
870
|
+
"!*.handler.ts",
|
|
871
|
+
"!*-action.*.ts",
|
|
872
|
+
"!*-query.*.ts"
|
|
873
|
+
]
|
|
874
|
+
},
|
|
875
|
+
deny: { imports: { "../*/*.constants{,.ts}": "Another domain's constants stay in that domain." } }
|
|
876
|
+
},
|
|
877
|
+
[at("domains/*/*.actions.ts")]: {
|
|
878
|
+
limits: { lines: 1000 },
|
|
879
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Action[A-Z]\\w*"], types: true }] }
|
|
880
|
+
},
|
|
881
|
+
[at("domains/*/*.queries.ts")]: {
|
|
882
|
+
limits: { lines: 1000 },
|
|
883
|
+
deny: { calls: presetEffectQueryWrites, imports: { "./*.actions{,.ts}": "A queries file imports no actions." } },
|
|
884
|
+
rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Query[A-Z]\\w*"], types: true }] }
|
|
885
|
+
},
|
|
886
|
+
[at("domains/*/*.constants.ts")]: { rules: { "arch/files-no-functions": ["error", { reexports: false }] } },
|
|
887
|
+
[at("domains/*/*.layer.ts")]: { rules: { "arch/effect-layer-file": "error" } },
|
|
888
|
+
[at("domains/*/*.driver.ts")]: { rules: { "arch/effect-layer-file": "error" } },
|
|
889
|
+
[at("domains/*/*.workflows.ts")]: {
|
|
890
|
+
deny: {
|
|
891
|
+
calls: presetEffectWorkflowCalls,
|
|
892
|
+
imports: {
|
|
893
|
+
"**/database": "A workflow reaches data through its domain service, not the database.",
|
|
894
|
+
"./*.{actions,queries}{,.ts}": "A workflow calls its domain service, not its own actions or queries."
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
[at("application/app.config.ts")]: { deny: { imports: { "effect/Config": false } } },
|
|
899
|
+
[at("application/app.ts")]: {
|
|
900
|
+
deny: { imports: { "**/domains/*/!(*.layer|*.layer.ts|*.workflows|*.workflows.ts)": "The application imports only layer and workflows files." } },
|
|
901
|
+
rules: { "arch/boundaries-imports": "off" }
|
|
902
|
+
},
|
|
903
|
+
[at("**/*.tsx")]: { rules: { "arch/files-restricted": ["error", { message: "Mail templates in mails/ are the only JSX on a server." }] } },
|
|
904
|
+
[at("mails/*.mail.tsx")]: { rules: { "arch/files-restricted": "off" } },
|
|
905
|
+
[at("rpc")]: { rules: { "arch/code-delegate": "error" } }
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function presetEffectContractKeys(at) {
|
|
909
|
+
const schemas = {
|
|
910
|
+
rules: {
|
|
911
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }],
|
|
912
|
+
"arch/files-no-functions": ["error", { reexports: false }],
|
|
913
|
+
"arch/types-imported-alias": "error",
|
|
914
|
+
"arch/schemas-single-use": ["error", presetEffectSchemaUses]
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
const noFunctions = { rules: { "arch/files-no-functions": "error" } };
|
|
918
|
+
return {
|
|
919
|
+
[at("*/*.ts")]: {
|
|
920
|
+
rules: {
|
|
921
|
+
"arch/naming-file": ["error", { pattern: presetEffectContractNames, message: presetEffectContractMessage }],
|
|
922
|
+
"arch/naming-folder": ["error", { singularize: "none", separators: [".", "-"] }],
|
|
923
|
+
"arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["{Domain}\\w*"] }]
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
[at("*/*.admin.*.ts")]: { rules: { "arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["Admin{Domain}\\w*"] }] } },
|
|
927
|
+
[at("*/*.schemas.ts")]: schemas,
|
|
928
|
+
[at("*/*.contract.ts")]: schemas,
|
|
929
|
+
[at("*/*.errors.ts")]: noFunctions,
|
|
930
|
+
[at("*/*.layer.types.ts")]: noFunctions,
|
|
931
|
+
[at("shared/*.ts")]: {
|
|
932
|
+
rules: {
|
|
933
|
+
"arch/naming-file": "off",
|
|
934
|
+
"arch/naming-folder": "off",
|
|
935
|
+
"arch/files-exports": "off",
|
|
936
|
+
"arch/files-no-functions": "off",
|
|
937
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }]
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
[at("contract*.ts")]: {
|
|
941
|
+
rules: {
|
|
942
|
+
"arch/files-constants-only": ["error", { types: true, functions: true }],
|
|
943
|
+
"arch/types-imported-alias": "error",
|
|
944
|
+
"arch/schemas-single-use": ["error", presetEffectSchemaUses]
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function presetEffectUnit(root, key, value) {
|
|
950
|
+
if (!value.effect) {
|
|
951
|
+
return {};
|
|
952
|
+
}
|
|
953
|
+
const at = presetEffectAt(root, key, value);
|
|
954
|
+
const contract = presetEffectContract(root, key, value);
|
|
955
|
+
const server = presetEffectDomains(root, key, value);
|
|
956
|
+
const keys = server || contract ? presetFilled(server ? presetEffectServerKeys(at) : presetEffectContractKeys(at), presetEffectExists(root, key)) : {};
|
|
957
|
+
const errors = Object.fromEntries(presetEffectErrors.map((name) => [name, "Tagged errors live in <d>.errors.ts."]));
|
|
958
|
+
const placement = server || contract ? { types: { placement: false } } : {};
|
|
959
|
+
return {
|
|
960
|
+
...keys,
|
|
961
|
+
".": presetMerge({ ...placement, deny: { imports: presetEffectImports, calls: errors } }, keys["."] ?? {}),
|
|
962
|
+
[at("**/*.errors.ts")]: { deny: { calls: Object.fromEntries(presetEffectErrors.map((name) => [name, false])) } }
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function presetEffectFrontend(root, key, value) {
|
|
966
|
+
const at = presetEffectAt(root, key, value);
|
|
967
|
+
const exists = presetEffectExists(root, key);
|
|
968
|
+
const framework = [value.tanstack, value.next, value.expo].some(Boolean);
|
|
969
|
+
if (!value.effect || !value.react || framework || !["pages", "link.tsx", "link.ts"].some((path) => exists(at(path)))) {
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
const routers = presetEffectRouters.map((name) => ({ name, message: "Import the router through link.tsx." }));
|
|
973
|
+
const linkRule = { "no-restricted-imports": ["error", { patterns: presetBaseImports }] };
|
|
974
|
+
return presetFilled({
|
|
975
|
+
".": { forms: true },
|
|
976
|
+
[at(".")]: {
|
|
977
|
+
deny: { imports: { "@orpc/*": "A frontend package takes its data functions as props, not from an rpc client." } },
|
|
978
|
+
rules: {
|
|
979
|
+
"arch/react-query-props": "error",
|
|
980
|
+
"no-restricted-imports": [
|
|
981
|
+
"error",
|
|
982
|
+
{ paths: [{ ...routers[0], allowImportNames: ["ClientOnly"] }, ...routers.slice(1)], patterns: presetBaseImports }
|
|
983
|
+
]
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
[at("link.tsx")]: { rules: linkRule },
|
|
987
|
+
[at("link.ts")]: { rules: linkRule },
|
|
988
|
+
[at("services/**/*.client.ts")]: { rules: { "arch/backend-service-functions": ["error", { frontend: true }] } }
|
|
989
|
+
}, exists);
|
|
990
|
+
}
|
|
991
|
+
function presetEffectWeb(root, key, value, contracts) {
|
|
992
|
+
const at = presetEffectAt(root, key, value);
|
|
993
|
+
if (contracts.length === 0 || !(value.tanstack || value.next)) {
|
|
994
|
+
return {};
|
|
995
|
+
}
|
|
996
|
+
const types = contracts.map((name) => ({ name, allowTypeImports: true, message: "The web imports the contract package as types only." }));
|
|
997
|
+
const rsc = { rules: { "no-restricted-imports": ["error", { paths: types, patterns: presetBaseImports }] } };
|
|
998
|
+
if (!value.tanstack) {
|
|
999
|
+
return { [at(".")]: rsc };
|
|
1000
|
+
}
|
|
1001
|
+
const services = value.services ?? "services";
|
|
1002
|
+
return {
|
|
1003
|
+
[at(".")]: { rules: { "no-restricted-imports": ["error", { paths: [...types, presetTanstackRsc], patterns: presetBaseImports }] } },
|
|
1004
|
+
[at(`${services}/**/*.rsc.ts`)]: rsc,
|
|
1005
|
+
[at(`${services}/**/*.rsc.tsx`)]: rsc
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function presetEffectKit(root, key, value, units) {
|
|
1009
|
+
if (!value.kit || !value.effect) {
|
|
1010
|
+
return {};
|
|
1011
|
+
}
|
|
1012
|
+
const apps = units.filter(([other, folder]) => other !== key && !(folder instanceof Object && folder.kit));
|
|
1013
|
+
const names = apps.flatMap(([other]) => presetManifest(root, other).name ?? []);
|
|
1014
|
+
const message = "A UI kit imports no app package.";
|
|
1015
|
+
return { ".": { deny: { imports: Object.fromEntries(names.flatMap((name) => [[name, message], [`${name}/**`, message]])) } } };
|
|
1016
|
+
}
|
|
1017
|
+
function presetEffectConventions(root, key, value, units) {
|
|
1018
|
+
const contracts = units.flatMap(([other, folder]) => presetEffectContract(root, other, folder) ? presetManifest(root, other).name ?? [] : []);
|
|
1019
|
+
return [
|
|
1020
|
+
...Object.entries(presetEffectUnit(root, key, value)),
|
|
1021
|
+
...Object.entries(presetEffectFrontend(root, key, value)),
|
|
1022
|
+
...Object.entries(presetEffectWeb(root, key, value, contracts)),
|
|
1023
|
+
...Object.entries(presetEffectKit(root, key, value, units))
|
|
1024
|
+
];
|
|
1025
|
+
}
|
|
678
1026
|
|
|
679
1027
|
// src/preset/preset-detect.ts
|
|
680
1028
|
function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
681
|
-
if (!presetIsDirectory(
|
|
1029
|
+
if (!presetIsDirectory(resolve3(root, base))) {
|
|
682
1030
|
return [];
|
|
683
1031
|
}
|
|
684
|
-
return
|
|
1032
|
+
return readdirSync2(resolve3(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve3(root, base, name))).map((name) => posix3.join(base, name));
|
|
685
1033
|
}
|
|
686
1034
|
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
1035
|
function presetRegistryFile(root, source, configured, services) {
|
|
694
1036
|
if (configured === false) {
|
|
695
1037
|
return;
|
|
696
1038
|
}
|
|
697
1039
|
const file = typeof configured === "string" ? configured : configured?.file;
|
|
698
1040
|
if (file !== undefined) {
|
|
699
|
-
return
|
|
1041
|
+
return posix3.normalize(file);
|
|
700
1042
|
}
|
|
701
|
-
return [
|
|
1043
|
+
return [posix3.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix3.join(source, path))].find((path) => existsSync3(resolve3(root, path)));
|
|
702
1044
|
}
|
|
703
1045
|
function presetDomains(root, services) {
|
|
704
|
-
return presetDirectories(root, services, presetNamedSkip).map((path) =>
|
|
1046
|
+
return presetDirectories(root, services, presetNamedSkip).map((path) => posix3.basename(path)).filter((name) => existsSync3(resolve3(root, services, name, `${name}.service.ts`)));
|
|
705
1047
|
}
|
|
706
|
-
function
|
|
1048
|
+
function presetRecipe2(root, directory, app, folder) {
|
|
707
1049
|
const manifest = presetManifest(root, directory);
|
|
708
1050
|
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
709
1051
|
const has = (names) => names.some((name) => dependencies.has(name));
|
|
710
1052
|
const source = presetSource(root, directory, folder.source);
|
|
711
1053
|
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
1054
|
+
const frontend = presetIsDirectory(resolve3(root, source, "pages")) || ["link.tsx", "link.ts"].some((name) => existsSync3(resolve3(root, source, name)));
|
|
1055
|
+
const react = has(["react"]) && (frontend || presetIsDirectory(resolve3(root, source, "components")));
|
|
1056
|
+
const reactRecipe = app || frontend ? presetRecipes.react : presetRecipes.ui;
|
|
712
1057
|
const servicesFolder = presetServicesFolder(root, source, folder.services, app);
|
|
713
|
-
const services =
|
|
714
|
-
const entries = presetIsDirectory(services) ?
|
|
715
|
-
const folders = entries.filter((entry) => presetIsDirectory(
|
|
1058
|
+
const services = resolve3(root, servicesFolder);
|
|
1059
|
+
const entries = presetIsDirectory(services) ? readdirSync2(services, { withFileTypes: true }) : [];
|
|
1060
|
+
const folders = entries.filter((entry) => presetIsDirectory(resolve3(services, entry.name))).length;
|
|
716
1061
|
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
|
|
1062
|
+
const backend = presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services));
|
|
1063
|
+
const layout = flat ? { layout: { services: "flat" } } : {};
|
|
1064
|
+
const style = has(["effect"]) ? { effect: { code: false } } : {};
|
|
1065
|
+
const { deny: _bans, ...effect } = presetRecipes.effect;
|
|
717
1066
|
if (framework) {
|
|
718
|
-
return
|
|
1067
|
+
return { ...presetRecipes[framework], ...layout, ...style };
|
|
719
1068
|
}
|
|
720
1069
|
if (has(presetDetectDesktop)) {
|
|
721
1070
|
return presetRecipes.desktop;
|
|
722
1071
|
}
|
|
723
|
-
|
|
724
|
-
|
|
1072
|
+
const reactLayout = frontend ? { ...reactRecipe, ...layout } : reactRecipe;
|
|
1073
|
+
if (has(["effect"]) && react) {
|
|
1074
|
+
return { ...reactLayout, ...style };
|
|
725
1075
|
}
|
|
726
|
-
if (has(["
|
|
727
|
-
return
|
|
1076
|
+
if (has(["effect"])) {
|
|
1077
|
+
return backend || presetIsDirectory(resolve3(root, source, "domains")) ? { ...presetRecipes.backend, ...effect } : effect;
|
|
728
1078
|
}
|
|
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
|
-
}
|
|
1079
|
+
if (backend) {
|
|
1080
|
+
return presetRecipes.backend;
|
|
741
1081
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
function presetMerge(base, top) {
|
|
745
|
-
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
746
|
-
return top === true && base instanceof Object ? base : top;
|
|
1082
|
+
if (react) {
|
|
1083
|
+
return reactLayout;
|
|
747
1084
|
}
|
|
748
|
-
return
|
|
1085
|
+
return presetRecipes.library;
|
|
749
1086
|
}
|
|
750
1087
|
function presetImportTargets(value) {
|
|
751
1088
|
if (typeof value === "string") {
|
|
@@ -758,7 +1095,7 @@ function presetImportTargets(value) {
|
|
|
758
1095
|
}
|
|
759
1096
|
function presetSpecifiers(manifest, target, file = false) {
|
|
760
1097
|
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
761
|
-
const path =
|
|
1098
|
+
const path = posix3.normalize(mapped);
|
|
762
1099
|
const parts = path.split("*");
|
|
763
1100
|
const keyParts = key.split("*");
|
|
764
1101
|
if (parts.length === 1 && keyParts.length === 1) {
|
|
@@ -787,23 +1124,17 @@ function presetOverlaps(left, right) {
|
|
|
787
1124
|
function presetDeny(imports) {
|
|
788
1125
|
return { deny: { imports } };
|
|
789
1126
|
}
|
|
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
1127
|
function presetBackendConventions(root, key, value, denied) {
|
|
797
1128
|
const manifest = presetManifest(root, key);
|
|
798
1129
|
const source = presetSource(root, key, value.source);
|
|
799
|
-
const local =
|
|
800
|
-
const at = (path) => local === "." ? path :
|
|
801
|
-
const exists = (path) =>
|
|
1130
|
+
const local = posix3.relative(key, source) || ".";
|
|
1131
|
+
const at = (path) => local === "." ? path : posix3.join(local, path);
|
|
1132
|
+
const exists = (path) => existsSync3(resolve3(root, key, path));
|
|
802
1133
|
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
803
1134
|
const servicesFolder = presetServicesFolder(root, source, value.services);
|
|
804
|
-
const servicesKey =
|
|
1135
|
+
const servicesKey = posix3.relative(key, servicesFolder);
|
|
805
1136
|
const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
|
|
806
|
-
const registry = registryFile === undefined ? undefined :
|
|
1137
|
+
const registry = registryFile === undefined ? undefined : posix3.relative(key, registryFile);
|
|
807
1138
|
const services = presetSpecifiers(manifest, servicesKey);
|
|
808
1139
|
const message = "Use the service API, not another service’s operations.";
|
|
809
1140
|
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
@@ -823,27 +1154,27 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
823
1154
|
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
824
1155
|
}
|
|
825
1156
|
};
|
|
826
|
-
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) =>
|
|
1157
|
+
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix3.basename(path))) {
|
|
827
1158
|
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
828
1159
|
[`${prefix}/${pattern}`, false],
|
|
829
1160
|
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
830
1161
|
]));
|
|
831
|
-
conventions[
|
|
1162
|
+
conventions[posix3.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
|
|
832
1163
|
}
|
|
833
1164
|
const testFolders = ["tests", "test"];
|
|
834
1165
|
for (const tests of testFolders) {
|
|
835
|
-
if (exists(
|
|
1166
|
+
if (exists(posix3.join(tests, "kits"))) {
|
|
836
1167
|
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
837
|
-
conventions[
|
|
1168
|
+
conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
838
1169
|
}
|
|
839
1170
|
}
|
|
840
1171
|
const queue = ["."];
|
|
841
1172
|
while (queue.length > 0) {
|
|
842
1173
|
const folder = queue.shift() ?? ".";
|
|
843
|
-
for (const child of presetDirectories(root,
|
|
844
|
-
if (
|
|
1174
|
+
for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
|
|
1175
|
+
if (posix3.basename(child) === "migrations") {
|
|
845
1176
|
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
846
|
-
} else if (child.split("/").length < 8 && !testFolders.includes(
|
|
1177
|
+
} else if (child.split("/").length < 8 && !testFolders.includes(posix3.basename(child))) {
|
|
847
1178
|
queue.push(child);
|
|
848
1179
|
}
|
|
849
1180
|
}
|
|
@@ -851,9 +1182,9 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
851
1182
|
return presetFilled(conventions, exists);
|
|
852
1183
|
}
|
|
853
1184
|
function presetWebConventions(root, key, value, servers) {
|
|
854
|
-
const source =
|
|
855
|
-
const at = (path) => source === "." ? path :
|
|
856
|
-
const exists = (path) =>
|
|
1185
|
+
const source = posix3.relative(key, presetSource(root, key, value.source)) || ".";
|
|
1186
|
+
const at = (path) => source === "." ? path : posix3.join(source, path);
|
|
1187
|
+
const exists = (path) => existsSync3(resolve3(root, key, path));
|
|
857
1188
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
858
1189
|
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
859
1190
|
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
@@ -873,8 +1204,8 @@ function presetWebConventions(root, key, value, servers) {
|
|
|
873
1204
|
}
|
|
874
1205
|
function presetWorkspacePatterns(root, manifest) {
|
|
875
1206
|
const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
|
|
876
|
-
const file =
|
|
877
|
-
if (!
|
|
1207
|
+
const file = resolve3(root, "pnpm-workspace.yaml");
|
|
1208
|
+
if (!existsSync3(file)) {
|
|
878
1209
|
return listed;
|
|
879
1210
|
}
|
|
880
1211
|
const patterns = [...listed];
|
|
@@ -897,7 +1228,7 @@ function presetWorkspacePatterns(root, manifest) {
|
|
|
897
1228
|
return patterns;
|
|
898
1229
|
}
|
|
899
1230
|
function presetWorkspaces(root, patterns) {
|
|
900
|
-
const clean = (pattern) =>
|
|
1231
|
+
const clean = (pattern) => posix3.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
901
1232
|
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
902
1233
|
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
903
1234
|
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
@@ -916,7 +1247,7 @@ function presetWorkspaces(root, patterns) {
|
|
|
916
1247
|
level.forEach((folder) => candidates.add(folder));
|
|
917
1248
|
}
|
|
918
1249
|
}
|
|
919
|
-
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) &&
|
|
1250
|
+
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync3(resolve3(root, path, "package.json"))).toSorted();
|
|
920
1251
|
}
|
|
921
1252
|
function presetContains(key, path) {
|
|
922
1253
|
if (key === "." || key === path) {
|
|
@@ -942,18 +1273,22 @@ function presetDetect(root, folders) {
|
|
|
942
1273
|
};
|
|
943
1274
|
const patterns = presetWorkspacePatterns(root, manifest);
|
|
944
1275
|
const directories = presetWorkspaces(root, patterns);
|
|
945
|
-
const detected = Object.fromEntries(directories.map((directory) => [directory,
|
|
946
|
-
const rootRecipe =
|
|
1276
|
+
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe2(root, directory, !directory.startsWith("packages/"), configured(directory))]));
|
|
1277
|
+
const rootRecipe = presetRecipe2(root, ".", true, configured("."));
|
|
947
1278
|
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
948
1279
|
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
|
|
949
1280
|
if (patterns.length === 0 ? single : rootApp) {
|
|
950
1281
|
detected["."] = rootRecipe;
|
|
951
1282
|
}
|
|
952
|
-
if (
|
|
1283
|
+
if (existsSync3(resolve3(root, "scripts")) && !directories.includes("scripts")) {
|
|
953
1284
|
detected.scripts = presetRecipes.cli;
|
|
954
1285
|
}
|
|
1286
|
+
if (existsSync3(resolve3(root, ".agents"))) {
|
|
1287
|
+
const { quotes, semicolons } = presetGroupOptions.style;
|
|
1288
|
+
detected[".agents"] = { ...presetRecipes.cli, style: { quotes, semicolons } };
|
|
1289
|
+
}
|
|
955
1290
|
const keys = Object.keys(folders);
|
|
956
|
-
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") &&
|
|
1291
|
+
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix3.dirname(key) === user.slice(0, -2)));
|
|
957
1292
|
const merged = { ".": presetDeny(presetRootImports) };
|
|
958
1293
|
for (const [key, value] of Object.entries(detected)) {
|
|
959
1294
|
if (!covered(key)) {
|
|
@@ -961,7 +1296,9 @@ function presetDetect(root, folders) {
|
|
|
961
1296
|
}
|
|
962
1297
|
}
|
|
963
1298
|
for (const [key, value] of Object.entries(folders)) {
|
|
964
|
-
|
|
1299
|
+
const forced = value instanceof Object && presetRecipeMark in value;
|
|
1300
|
+
const kept = forced && key === "." ? presetDeny(presetRootImports) : merged[key];
|
|
1301
|
+
merged[key] = key in merged && (!forced || key === ".") ? presetMerge(kept, value) : value;
|
|
965
1302
|
}
|
|
966
1303
|
const length = (key) => key.split("/").length;
|
|
967
1304
|
const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
|
|
@@ -975,6 +1312,7 @@ function presetDetect(root, folders) {
|
|
|
975
1312
|
const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
|
|
976
1313
|
return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
|
|
977
1314
|
});
|
|
1315
|
+
presetEffectServers(root, merged, units);
|
|
978
1316
|
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
1317
|
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
980
1318
|
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
@@ -994,11 +1332,11 @@ function presetDetect(root, folders) {
|
|
|
994
1332
|
}
|
|
995
1333
|
if (value.backend) {
|
|
996
1334
|
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
997
|
-
add(path === "." ? key :
|
|
1335
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
998
1336
|
}
|
|
999
1337
|
} else if (value.tanstack || value.next) {
|
|
1000
1338
|
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
1001
|
-
add(path === "." ? key :
|
|
1339
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1002
1340
|
}
|
|
1003
1341
|
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
1004
1342
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
@@ -1006,6 +1344,9 @@ function presetDetect(root, folders) {
|
|
|
1006
1344
|
const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
|
|
1007
1345
|
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
1008
1346
|
}
|
|
1347
|
+
for (const [path, folder] of presetEffectConventions(root, key, value, units)) {
|
|
1348
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1349
|
+
}
|
|
1009
1350
|
}
|
|
1010
1351
|
return merged;
|
|
1011
1352
|
}
|
|
@@ -1026,7 +1367,7 @@ var backendGroup = (project, settings) => {
|
|
|
1026
1367
|
}
|
|
1027
1368
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
1028
1369
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
1029
|
-
const file = configured.file === undefined ? undefined :
|
|
1370
|
+
const file = configured.file === undefined ? undefined : posix4.normalize(configured.file);
|
|
1030
1371
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
1031
1372
|
const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
|
|
1032
1373
|
return found ? [{ file: found, unit }] : [];
|
|
@@ -1036,8 +1377,8 @@ var backendGroup = (project, settings) => {
|
|
|
1036
1377
|
throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
|
|
1037
1378
|
}
|
|
1038
1379
|
const options = {
|
|
1039
|
-
registry:
|
|
1040
|
-
services:
|
|
1380
|
+
registry: resolve4(project.root, registry.file),
|
|
1381
|
+
services: resolve4(project.root, registry.unit.services),
|
|
1041
1382
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
1042
1383
|
};
|
|
1043
1384
|
if (configured.object !== undefined) {
|
|
@@ -1052,9 +1393,11 @@ var backendGroup = (project, settings) => {
|
|
|
1052
1393
|
var basicsGroup = (project, settings) => {
|
|
1053
1394
|
const trivial = { mode: "precise", async: false, callees: [] };
|
|
1054
1395
|
const { appFiles, safetyFiles, ui, web } = project;
|
|
1396
|
+
const nextEntries = presetScopes(project.nextWeb, "app/**/{layout,page,default}.tsx");
|
|
1055
1397
|
return [
|
|
1056
1398
|
...settings.trivial ? [
|
|
1057
|
-
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
|
|
1399
|
+
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, [...ui, ...nextEntries]),
|
|
1400
|
+
presetOverride(nextEntries, { "arch/code-trivial-functions": ["error", { ...trivial, defaults: false }] }, ui),
|
|
1058
1401
|
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
1402
|
] : [],
|
|
1060
1403
|
...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
|
|
@@ -1075,22 +1418,20 @@ var consoleGroup = () => [presetOverride([`**/${presetExtensions}`], { "no-conso
|
|
|
1075
1418
|
|
|
1076
1419
|
// src/preset/groups/deny.ts
|
|
1077
1420
|
var denyGroup = (project, settings) => {
|
|
1078
|
-
const
|
|
1421
|
+
const kept = (entries) => Object.fromEntries(Object.entries(entries).filter((entry) => entry[1] !== false));
|
|
1422
|
+
const imports = kept(settings.imports);
|
|
1423
|
+
const calls = kept(settings.calls);
|
|
1079
1424
|
const overrides = [];
|
|
1080
1425
|
if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
|
|
1081
1426
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
|
|
1082
1427
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (writes.length === 0) {
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1428
|
+
const reads = Object.fromEntries(presetQueryWrites.map((name) => [name, "Query files only read; move writes to an action."]));
|
|
1429
|
+
for (const unit of project.units.filter((item) => item.kind === "backend")) {
|
|
1088
1430
|
const files = [
|
|
1089
1431
|
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1090
1432
|
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
1091
1433
|
];
|
|
1092
|
-
|
|
1093
|
-
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
|
|
1434
|
+
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: kept({ ...reads, ...settings.calls }) }] }));
|
|
1094
1435
|
}
|
|
1095
1436
|
return overrides;
|
|
1096
1437
|
};
|
|
@@ -1133,9 +1474,18 @@ var effectTestOff = {
|
|
|
1133
1474
|
var effectGroup = (project, settings) => {
|
|
1134
1475
|
const files = [`**/${presetExtensions}`];
|
|
1135
1476
|
const max = settings.width;
|
|
1477
|
+
const style = presetOverride(files, {
|
|
1478
|
+
"typescript/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1479
|
+
"eslint-js/max-len": ["error", { code: max, ignoreUrls: true, ignoreComments: true, ignoreRegExpLiterals: true }],
|
|
1480
|
+
...presetEffectStyle().rules
|
|
1481
|
+
});
|
|
1482
|
+
if (!settings.code) {
|
|
1483
|
+
return [style];
|
|
1484
|
+
}
|
|
1136
1485
|
const { tsgo, linteffect } = presetEffectPackages();
|
|
1137
1486
|
const tsgoRules = [tsgo.correctness, tsgo.antipattern, tsgo.effectNative, tsgo.style].flatMap((config) => Object.keys(config.rules ?? {}));
|
|
1138
1487
|
return [
|
|
1488
|
+
style,
|
|
1139
1489
|
presetOverride(files, {
|
|
1140
1490
|
...Object.fromEntries(tsgoRules.map((rule) => [rule, "error"])),
|
|
1141
1491
|
...linteffect.recommended.rules,
|
|
@@ -1146,6 +1496,9 @@ var effectGroup = (project, settings) => {
|
|
|
1146
1496
|
presetOverride(files, {
|
|
1147
1497
|
"arch/code-trivial-functions": "off",
|
|
1148
1498
|
"arch/code-branch-once": "error",
|
|
1499
|
+
"arch/code-object-params": ["error", { all: true, services: true }],
|
|
1500
|
+
"arch/code-switch": ["error", { message: "Branch with Match." }],
|
|
1501
|
+
"arch/naming-exports": "off",
|
|
1149
1502
|
"arch/code-function-nesting": ["error", { max: 4 }],
|
|
1150
1503
|
"arch/code-inline": ["error", { max, nesting: 2, keep: ["^[A-Z]", "^layer"], allow: "^(?:decode|encode)[A-Z]", services: ["of"] }],
|
|
1151
1504
|
"arch/code-yield-argument": "error",
|
|
@@ -1210,17 +1563,17 @@ var expoGroup = (project, settings) => {
|
|
|
1210
1563
|
};
|
|
1211
1564
|
|
|
1212
1565
|
// src/preset/groups/externals.ts
|
|
1213
|
-
var externalsGroup = (
|
|
1214
|
-
presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages:
|
|
1566
|
+
var externalsGroup = () => [
|
|
1567
|
+
presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: presetExternals }] })
|
|
1215
1568
|
];
|
|
1216
1569
|
|
|
1217
1570
|
// src/preset/groups/forms.ts
|
|
1218
|
-
var formsGroup = (project
|
|
1219
|
-
presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require:
|
|
1571
|
+
var formsGroup = (project) => [
|
|
1572
|
+
presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: "standardSchemaResolver" }] }, project.ui)
|
|
1220
1573
|
];
|
|
1221
1574
|
|
|
1222
1575
|
// src/preset/groups/imports.ts
|
|
1223
|
-
import { resolve as
|
|
1576
|
+
import { resolve as resolve5 } from "node:path";
|
|
1224
1577
|
function importsPackageNames(project, kinds) {
|
|
1225
1578
|
return project.units.flatMap((unit) => {
|
|
1226
1579
|
const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
|
|
@@ -1258,7 +1611,7 @@ var importsGroup = (project, settings) => {
|
|
|
1258
1611
|
if (!settings.boundaries) {
|
|
1259
1612
|
return overrides;
|
|
1260
1613
|
}
|
|
1261
|
-
const absolute = (paths) => paths.map((path) =>
|
|
1614
|
+
const absolute = (paths) => paths.map((path) => resolve5(project.root, path));
|
|
1262
1615
|
overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
|
|
1263
1616
|
"arch/boundaries-imports": [
|
|
1264
1617
|
"error",
|
|
@@ -1266,7 +1619,7 @@ var importsGroup = (project, settings) => {
|
|
|
1266
1619
|
web: absolute(project.web),
|
|
1267
1620
|
backend: absolute(project.backend),
|
|
1268
1621
|
packages: absolute(project.packages),
|
|
1269
|
-
roles: project.
|
|
1622
|
+
roles: project.fileKinds,
|
|
1270
1623
|
services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
|
|
1271
1624
|
portable: true,
|
|
1272
1625
|
aliases: presetAliases(project.root, settings.aliases),
|
|
@@ -1280,18 +1633,18 @@ var importsGroup = (project, settings) => {
|
|
|
1280
1633
|
};
|
|
1281
1634
|
|
|
1282
1635
|
// src/preset/groups/layout.ts
|
|
1283
|
-
import { posix as
|
|
1636
|
+
import { posix as posix5 } from "node:path";
|
|
1284
1637
|
var layoutGroup = (project, settings) => {
|
|
1285
1638
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
1286
1639
|
const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
|
|
1287
1640
|
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
|
|
1641
|
+
const names = settings.files.map((name) => name.split("*").map((part) => part.replaceAll(".", "[.]")).join("[a-z0-9]+(?:-[a-z0-9]+)*"));
|
|
1642
|
+
const kindFiles = project.fileKinds.map((kind) => `**/*.${kind}.ts`);
|
|
1290
1643
|
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 ===
|
|
1644
|
+
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix5.join(path, "components")));
|
|
1292
1645
|
return [
|
|
1293
1646
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
1294
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1647
|
+
const folders = folder === "services" && operations.some((source) => posix5.join(source, "services") === path);
|
|
1295
1648
|
const registries = folder === "services" ? project.registries : [];
|
|
1296
1649
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
1297
1650
|
"arch/files-restricted": [
|
|
@@ -1314,11 +1667,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1314
1667
|
"arch/naming-file": [
|
|
1315
1668
|
"error",
|
|
1316
1669
|
{
|
|
1317
|
-
pattern: `^
|
|
1318
|
-
message: `Domain files use
|
|
1670
|
+
pattern: `^(?:${names.join("|")})$`,
|
|
1671
|
+
message: `Domain files use one of: ${settings.files.join(", ")}. Add a name to layout.files.`
|
|
1319
1672
|
}
|
|
1320
1673
|
]
|
|
1321
|
-
}, [
|
|
1674
|
+
}, [
|
|
1675
|
+
...settings.files.includes("*-action.*.ts") ? ["**/*-action.*.ts"] : [],
|
|
1676
|
+
...settings.files.includes("*-query.*.ts") ? ["**/*-query.*.ts"] : [],
|
|
1677
|
+
...testFiles,
|
|
1678
|
+
...registries
|
|
1679
|
+
]);
|
|
1322
1680
|
return [...domain, strict];
|
|
1323
1681
|
}),
|
|
1324
1682
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
@@ -1335,16 +1693,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1335
1693
|
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
1336
1694
|
"**/*.rsc.tsx",
|
|
1337
1695
|
...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
|
|
1338
|
-
...
|
|
1696
|
+
...kindFiles,
|
|
1339
1697
|
...project.colocated,
|
|
1340
1698
|
...testFiles
|
|
1341
1699
|
]),
|
|
1342
1700
|
presetOverride(services, {
|
|
1343
1701
|
"arch/backend-service-functions": "error",
|
|
1344
1702
|
"arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
|
|
1345
|
-
}, [...utilities, ...testFiles, ...
|
|
1703
|
+
}, [...utilities, ...testFiles, ...kindFiles]),
|
|
1346
1704
|
presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
|
|
1347
|
-
...
|
|
1705
|
+
...kindFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
|
|
1348
1706
|
"arch/backend-service-functions": "off",
|
|
1349
1707
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
1350
1708
|
}, testFiles)),
|
|
@@ -1522,7 +1880,7 @@ var nextGroup = (project, settings) => {
|
|
|
1522
1880
|
// src/preset/groups/orpc.ts
|
|
1523
1881
|
var orpcGroup = (project) => project.orpc.public.length > 0 ? [
|
|
1524
1882
|
presetOverride(project.orpc.public, {
|
|
1525
|
-
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output"
|
|
1883
|
+
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output" }]
|
|
1526
1884
|
})
|
|
1527
1885
|
] : [];
|
|
1528
1886
|
|
|
@@ -1619,13 +1977,8 @@ var shadcnGroup = (project) => {
|
|
|
1619
1977
|
};
|
|
1620
1978
|
|
|
1621
1979
|
// src/preset/groups/sql.ts
|
|
1622
|
-
var sqlGroup = (project
|
|
1623
|
-
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
|
|
1624
|
-
"arch/backend-sql-like": [
|
|
1625
|
-
"error",
|
|
1626
|
-
{ methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
|
|
1627
|
-
]
|
|
1628
|
-
})
|
|
1980
|
+
var sqlGroup = (project) => [
|
|
1981
|
+
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), { "arch/backend-sql-like": "error" })
|
|
1629
1982
|
];
|
|
1630
1983
|
|
|
1631
1984
|
// src/preset/groups/style.ts
|
|
@@ -1677,7 +2030,7 @@ var tanstackGroup = (project, settings) => {
|
|
|
1677
2030
|
"no-restricted-imports": [
|
|
1678
2031
|
"error",
|
|
1679
2032
|
{
|
|
1680
|
-
paths: [
|
|
2033
|
+
paths: [presetTanstackRsc],
|
|
1681
2034
|
patterns: project.baseImports
|
|
1682
2035
|
}
|
|
1683
2036
|
]
|
|
@@ -1713,7 +2066,7 @@ function optionsPatternsTest(patterns, value) {
|
|
|
1713
2066
|
function optionsGlob(pattern) {
|
|
1714
2067
|
const negated = [];
|
|
1715
2068
|
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
1716
|
-
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]
|
|
2069
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\?{}]/g, "\\$&").replaceAll("*", "[^/]*")).join("|"));
|
|
1717
2070
|
return `\x02${negated.length - 1}\x03`;
|
|
1718
2071
|
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
1719
2072
|
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
@@ -1829,12 +2182,12 @@ var typesGroup = (project, settings) => {
|
|
|
1829
2182
|
};
|
|
1830
2183
|
|
|
1831
2184
|
// src/preset/preset-folders.ts
|
|
1832
|
-
import { existsSync as
|
|
1833
|
-
import { posix as
|
|
2185
|
+
import { existsSync as existsSync5, readdirSync as readdirSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
2186
|
+
import { posix as posix8, resolve as resolve8 } from "node:path";
|
|
1834
2187
|
|
|
1835
2188
|
// src/preset/preset-stylesheet.ts
|
|
1836
|
-
import { readdirSync as
|
|
1837
|
-
import { posix as
|
|
2189
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
2190
|
+
import { posix as posix6, resolve as resolve6 } from "node:path";
|
|
1838
2191
|
function presetTailwindEntry(css) {
|
|
1839
2192
|
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1840
2193
|
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
@@ -1845,15 +2198,15 @@ function presetStylesheet(root, directory) {
|
|
|
1845
2198
|
const visited = new Set;
|
|
1846
2199
|
while (queue.length > 0) {
|
|
1847
2200
|
const folder = queue.shift() ?? directory;
|
|
1848
|
-
const real = realpathSync(
|
|
2201
|
+
const real = realpathSync(resolve6(root, folder));
|
|
1849
2202
|
if (visited.has(real)) {
|
|
1850
2203
|
continue;
|
|
1851
2204
|
}
|
|
1852
2205
|
visited.add(real);
|
|
1853
|
-
for (const name of
|
|
1854
|
-
const path =
|
|
1855
|
-
const full =
|
|
1856
|
-
const depth =
|
|
2206
|
+
for (const name of readdirSync3(resolve6(root, folder))) {
|
|
2207
|
+
const path = posix6.join(folder, name);
|
|
2208
|
+
const full = resolve6(root, path);
|
|
2209
|
+
const depth = posix6.relative(directory, path).split("/").length;
|
|
1857
2210
|
if (presetIsDirectory(full)) {
|
|
1858
2211
|
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1859
2212
|
queue.push(path);
|
|
@@ -1864,15 +2217,15 @@ function presetStylesheet(root, directory) {
|
|
|
1864
2217
|
}
|
|
1865
2218
|
}
|
|
1866
2219
|
const rank = (path) => {
|
|
1867
|
-
const index = presetStylesheetNames.indexOf(
|
|
2220
|
+
const index = presetStylesheetNames.indexOf(posix6.basename(path, ".css"));
|
|
1868
2221
|
return index === -1 ? presetStylesheetNames.length : index;
|
|
1869
2222
|
};
|
|
1870
2223
|
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1871
2224
|
}
|
|
1872
2225
|
|
|
1873
2226
|
// src/preset/preset-units.ts
|
|
1874
|
-
import { existsSync as
|
|
1875
|
-
import { posix as
|
|
2227
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
|
|
2228
|
+
import { posix as posix7, resolve as resolve7 } from "node:path";
|
|
1876
2229
|
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1877
2230
|
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1878
2231
|
function presetUnitSource(root, key, kind, source) {
|
|
@@ -1880,9 +2233,9 @@ function presetUnitSource(root, key, kind, source) {
|
|
|
1880
2233
|
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1881
2234
|
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1882
2235
|
}
|
|
1883
|
-
return
|
|
2236
|
+
return posix7.join(key, source);
|
|
1884
2237
|
}
|
|
1885
|
-
return kind === "console" || !
|
|
2238
|
+
return kind === "console" || !existsSync4(resolve7(root, key, "src")) ? key : posix7.join(key, "src");
|
|
1886
2239
|
}
|
|
1887
2240
|
function presetSets(folder, name) {
|
|
1888
2241
|
return folder[name] !== undefined && folder[name] !== false;
|
|
@@ -1911,10 +2264,10 @@ function presetHasManifest(root, key) {
|
|
|
1911
2264
|
return false;
|
|
1912
2265
|
}
|
|
1913
2266
|
if (!key.endsWith("/*")) {
|
|
1914
|
-
return
|
|
2267
|
+
return existsSync4(resolve7(root, key, "package.json"));
|
|
1915
2268
|
}
|
|
1916
|
-
const base =
|
|
1917
|
-
return presetIsDirectory(base) &&
|
|
2269
|
+
const base = resolve7(root, key.slice(0, -2));
|
|
2270
|
+
return presetIsDirectory(base) && readdirSync4(base).some((name) => existsSync4(resolve7(base, name, "package.json")));
|
|
1918
2271
|
}
|
|
1919
2272
|
function presetUnit(root, key, scope) {
|
|
1920
2273
|
const { effective } = scope;
|
|
@@ -1963,20 +2316,24 @@ function presetUnits(root, scopes, ignored) {
|
|
|
1963
2316
|
return [presetUnit(root, scope.key, scope)];
|
|
1964
2317
|
}
|
|
1965
2318
|
const base = scope.key.slice(0, -2);
|
|
1966
|
-
return
|
|
2319
|
+
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
2320
|
});
|
|
1968
2321
|
}
|
|
1969
2322
|
|
|
1970
2323
|
// src/preset/preset-folders.ts
|
|
1971
|
-
function
|
|
2324
|
+
function presetPortableKey(key) {
|
|
1972
2325
|
const portable = key.replaceAll("\\", "/");
|
|
1973
|
-
if (!portable.trim() ||
|
|
2326
|
+
if (!portable.trim() || posix8.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1974
2327
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1975
2328
|
}
|
|
1976
|
-
const normalized =
|
|
2329
|
+
const normalized = posix8.normalize(portable).replace(/\/$/, "");
|
|
1977
2330
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1978
2331
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1979
2332
|
}
|
|
2333
|
+
return normalized;
|
|
2334
|
+
}
|
|
2335
|
+
function presetNormalizeKey(key) {
|
|
2336
|
+
const normalized = presetPortableKey(key);
|
|
1980
2337
|
const segments = normalized.split("/");
|
|
1981
2338
|
if (normalized !== "." && segments.some((segment) => segment !== "*" && !presetLiteralSegment.test(segment))) {
|
|
1982
2339
|
throw new PresetError(`Folder keys must be literal paths; only a whole \`*\` segment may be a wildcard: ${key}`);
|
|
@@ -1986,6 +2343,25 @@ function presetNormalizeKey(key) {
|
|
|
1986
2343
|
}
|
|
1987
2344
|
return normalized;
|
|
1988
2345
|
}
|
|
2346
|
+
function presetBraces(key) {
|
|
2347
|
+
const match = /\{([^{}]*)\}/.exec(key);
|
|
2348
|
+
if (!match) {
|
|
2349
|
+
return [key];
|
|
2350
|
+
}
|
|
2351
|
+
return match[1].split(",").flatMap((option) => presetBraces(`${key.slice(0, match.index)}${option}${key.slice(match.index + match[0].length)}`));
|
|
2352
|
+
}
|
|
2353
|
+
function presetIsFileKey(root, key) {
|
|
2354
|
+
const name = key.split("/").at(-1) ?? "";
|
|
2355
|
+
const pattern = key.includes("*");
|
|
2356
|
+
if (!pattern && !existsSync5(resolve8(root, key)) && /[^.]\.[^.]+$/.test(name)) {
|
|
2357
|
+
throw new PresetError(`File \`${key}\` does not exist.`);
|
|
2358
|
+
}
|
|
2359
|
+
const file = pattern ? name !== "*" && name.includes(".") : presetIsFile(resolve8(root, key));
|
|
2360
|
+
if (file && !key.split("/").every((segment) => presetFileSegment.test(segment))) {
|
|
2361
|
+
throw new PresetError(`File keys hold letters, digits, \`._-@$[]\` and \`*\` wildcards: ${key}`);
|
|
2362
|
+
}
|
|
2363
|
+
return file;
|
|
2364
|
+
}
|
|
1989
2365
|
function presetMatches(pattern, path) {
|
|
1990
2366
|
return pattern.every((segment, index) => segment === "*" || segment === path[index]);
|
|
1991
2367
|
}
|
|
@@ -2021,13 +2397,28 @@ function presetGroupValue(name, value, inherited, severity) {
|
|
|
2021
2397
|
return { ...base, ...merged, level: severity ?? base.level };
|
|
2022
2398
|
}
|
|
2023
2399
|
function presetDenyEntries(inherited, changes) {
|
|
2024
|
-
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).
|
|
2400
|
+
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).map(([key, message]) => {
|
|
2025
2401
|
if (message !== false && typeof message !== "string") {
|
|
2026
2402
|
throw new PresetError(`Deny entry \`${key}\` takes a message string or false.`);
|
|
2027
2403
|
}
|
|
2028
|
-
return
|
|
2404
|
+
return [key, message];
|
|
2029
2405
|
}));
|
|
2030
2406
|
}
|
|
2407
|
+
function presetLayoutNames(inherited, entries) {
|
|
2408
|
+
if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string" && presetLayoutFileName.test(entry))) {
|
|
2409
|
+
throw new PresetError("layout.files takes file names such as `*.prompts.ts`; `!*.utils.ts` removes one.");
|
|
2410
|
+
}
|
|
2411
|
+
const names = [...inherited];
|
|
2412
|
+
for (const entry of entries) {
|
|
2413
|
+
const index = names.indexOf(entry.startsWith("!") ? entry.slice(1) : entry);
|
|
2414
|
+
if (entry.startsWith("!") && index !== -1) {
|
|
2415
|
+
names.splice(index, 1);
|
|
2416
|
+
} else if (!entry.startsWith("!") && index === -1) {
|
|
2417
|
+
names.push(entry);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return names;
|
|
2421
|
+
}
|
|
2031
2422
|
function presetEffective(folder, parent) {
|
|
2032
2423
|
const resolveGroup = (name) => {
|
|
2033
2424
|
const defaults = presetDefaultGroups.includes(name) ? presetGroupOptions[name] : null;
|
|
@@ -2043,6 +2434,11 @@ function presetEffective(folder, parent) {
|
|
|
2043
2434
|
calls: presetDenyEntries(inherited.calls, deny.calls)
|
|
2044
2435
|
};
|
|
2045
2436
|
}
|
|
2437
|
+
const layout = folder.layout;
|
|
2438
|
+
if (effective.layout && layout instanceof Object && layout.files !== undefined) {
|
|
2439
|
+
const inherited = (parent ? parent.layout?.files : null) ?? presetGroupOptions.layout.files;
|
|
2440
|
+
effective.layout = { ...effective.layout, files: presetLayoutNames(inherited, layout.files) };
|
|
2441
|
+
}
|
|
2046
2442
|
return effective;
|
|
2047
2443
|
}
|
|
2048
2444
|
function presetFolder(key, value) {
|
|
@@ -2073,7 +2469,7 @@ function presetScopesTree(root, folders) {
|
|
|
2073
2469
|
}
|
|
2074
2470
|
for (const key of keys) {
|
|
2075
2471
|
const base = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
2076
|
-
if (!
|
|
2472
|
+
if (!existsSync5(resolve8(root, base))) {
|
|
2077
2473
|
throw new PresetError(`Folder \`${key}\` does not exist.`);
|
|
2078
2474
|
}
|
|
2079
2475
|
}
|
|
@@ -2105,14 +2501,26 @@ function presetScopesTree(root, folders) {
|
|
|
2105
2501
|
return scopes;
|
|
2106
2502
|
}
|
|
2107
2503
|
function presetProject(config) {
|
|
2108
|
-
const given =
|
|
2109
|
-
const root =
|
|
2110
|
-
const
|
|
2111
|
-
const
|
|
2504
|
+
const given = resolve8(config.root ?? process.cwd());
|
|
2505
|
+
const root = existsSync5(given) ? realpathSync2(given) : given;
|
|
2506
|
+
const expanded = Object.entries(config.folders ?? {}).flatMap(([key, value]) => presetBraces(key).map((path) => [presetPortableKey(path), value, key.includes("{")]));
|
|
2507
|
+
const files = expanded.filter(([key]) => presetIsFileKey(root, key));
|
|
2508
|
+
const listed = expanded.filter((entry) => !files.includes(entry)).map(([key, value, fromList]) => [presetNormalizeKey(key), value, fromList]);
|
|
2509
|
+
const twice = listed.find(([key, , fromList], index) => listed.some(([other, , otherFromList], at) => at < index && other === key && !fromList && !otherFromList));
|
|
2112
2510
|
if (twice) {
|
|
2113
2511
|
throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
|
|
2114
2512
|
}
|
|
2115
|
-
const
|
|
2513
|
+
const configured = {};
|
|
2514
|
+
for (const [key, value] of listed) {
|
|
2515
|
+
configured[key] = key in configured ? presetMerge(configured[key], value) : value;
|
|
2516
|
+
}
|
|
2517
|
+
const detected = config.detect === false ? configured : presetDetect(root, configured);
|
|
2518
|
+
const conventions = Object.entries(detected).filter(([key]) => !(key in configured) && presetIsFileKey(root, key));
|
|
2519
|
+
const folders = Object.fromEntries(Object.entries(detected).filter(([key]) => !conventions.some(([other]) => other === key)));
|
|
2520
|
+
const fileEntries = [
|
|
2521
|
+
...conventions.filter(([key]) => !files.some(([other]) => other === key)),
|
|
2522
|
+
...files.map(([key, value]) => [key, key in detected ? presetMerge(detected[key], value) : value])
|
|
2523
|
+
];
|
|
2116
2524
|
const scopes = presetScopesTree(root, folders);
|
|
2117
2525
|
const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
|
|
2118
2526
|
const live = scopes.filter((scope) => !ignored.has(scope.key));
|
|
@@ -2120,6 +2528,22 @@ function presetProject(config) {
|
|
|
2120
2528
|
if (dead) {
|
|
2121
2529
|
throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
|
|
2122
2530
|
}
|
|
2531
|
+
const fileKeys = fileEntries.flatMap(([key, value]) => {
|
|
2532
|
+
if (value === false) {
|
|
2533
|
+
return [];
|
|
2534
|
+
}
|
|
2535
|
+
const folder = presetFolder(key, value);
|
|
2536
|
+
const option = Object.keys(folder).find((name) => presetFolderOnlyKeys.includes(name));
|
|
2537
|
+
if (option) {
|
|
2538
|
+
throw new PresetError(`Set \`${option}\` on a folder, not on \`${key}\`.`);
|
|
2539
|
+
}
|
|
2540
|
+
if (Object(folder.layout).operations !== undefined) {
|
|
2541
|
+
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${key}\`.`);
|
|
2542
|
+
}
|
|
2543
|
+
const pattern = presetGlobEscape(key);
|
|
2544
|
+
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) }));
|
|
2545
|
+
return [{ pattern, folder, scopes: reached }];
|
|
2546
|
+
});
|
|
2123
2547
|
const units = presetUnits(root, scopes, ignored);
|
|
2124
2548
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
2125
2549
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
@@ -2130,10 +2554,6 @@ function presetProject(config) {
|
|
|
2130
2554
|
if (misplaced) {
|
|
2131
2555
|
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
|
|
2132
2556
|
}
|
|
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
2557
|
const of = (kind) => units.filter((unit) => unit.kind === kind);
|
|
2138
2558
|
const webUnits = of("web");
|
|
2139
2559
|
const web = webUnits.map((unit) => unit.source);
|
|
@@ -2147,19 +2567,15 @@ function presetProject(config) {
|
|
|
2147
2567
|
const appFiles = presetScopes(app);
|
|
2148
2568
|
const webFiles = presetScopes(web);
|
|
2149
2569
|
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
|
-
}
|
|
2570
|
+
const fileKinds = [
|
|
2571
|
+
...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] ?? [])))
|
|
2572
|
+
];
|
|
2157
2573
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
2158
2574
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
2159
2575
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
2160
|
-
const rpcClients = client === undefined ? webUnits.map((unit) =>
|
|
2576
|
+
const rpcClients = client === undefined ? webUnits.map((unit) => posix8.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
|
|
2161
2577
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
2162
|
-
const uiRoots = [...new Set([...web.map((path) =>
|
|
2578
|
+
const uiRoots = [...new Set([...web.map((path) => posix8.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
|
|
2163
2579
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
2164
2580
|
const stylesheet = (unit) => {
|
|
2165
2581
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -2167,7 +2583,7 @@ function presetProject(config) {
|
|
|
2167
2583
|
return;
|
|
2168
2584
|
}
|
|
2169
2585
|
const css = tailwind.css ?? presetStylesheet(root, unit.key);
|
|
2170
|
-
return css === undefined ? undefined :
|
|
2586
|
+
return css === undefined ? undefined : resolve8(root, css);
|
|
2171
2587
|
};
|
|
2172
2588
|
const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
|
|
2173
2589
|
const css = stylesheet(unit);
|
|
@@ -2201,21 +2617,21 @@ function presetProject(config) {
|
|
|
2201
2617
|
return [key];
|
|
2202
2618
|
}
|
|
2203
2619
|
const base = key.slice(0, -2);
|
|
2204
|
-
return
|
|
2620
|
+
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
2621
|
});
|
|
2206
2622
|
return {
|
|
2207
2623
|
root,
|
|
2208
2624
|
serviceFolders,
|
|
2209
2625
|
scopes: live,
|
|
2626
|
+
fileKeys,
|
|
2210
2627
|
units,
|
|
2211
|
-
|
|
2628
|
+
fileKinds,
|
|
2212
2629
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
2213
2630
|
orpc: {
|
|
2214
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2215
|
-
composers: listOf(orpcScopes.map((settings) => settings.composers)),
|
|
2631
|
+
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix8.join(unit.source, "rpc/api")).filter((path) => existsSync5(resolve8(root, path))).map((path) => `${path}/**`),
|
|
2216
2632
|
client: client ?? ""
|
|
2217
2633
|
},
|
|
2218
|
-
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services :
|
|
2634
|
+
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix8.join(unit.source, folder), folder, mode }))),
|
|
2219
2635
|
rpcClients,
|
|
2220
2636
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
2221
2637
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -2253,7 +2669,7 @@ function presetProject(config) {
|
|
|
2253
2669
|
baseImports: presetBaseImports,
|
|
2254
2670
|
css: { ...webCss, ...kitCss },
|
|
2255
2671
|
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
|
|
2256
|
-
ignores: skipped.map((key) => `${key}/**`),
|
|
2672
|
+
ignores: [...skipped.map((key) => `${key}/**`), ...fileEntries.flatMap(([key, value]) => value === false ? [presetGlobEscape(key)] : [])],
|
|
2257
2673
|
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
2258
2674
|
};
|
|
2259
2675
|
}
|
|
@@ -2262,7 +2678,7 @@ function presetProject(config) {
|
|
|
2262
2678
|
var recipes = presetRecipes;
|
|
2263
2679
|
var templates = presetTemplates;
|
|
2264
2680
|
var externals = presetExternals;
|
|
2265
|
-
var require2 =
|
|
2681
|
+
var require2 = createRequire3(import.meta.url);
|
|
2266
2682
|
function presetOptional(name, group) {
|
|
2267
2683
|
try {
|
|
2268
2684
|
return require2.resolve(name);
|
|
@@ -2311,41 +2727,70 @@ function presetSettings(scope, name) {
|
|
|
2311
2727
|
}
|
|
2312
2728
|
return settings ? null : { level: presetLevel(scope) };
|
|
2313
2729
|
}
|
|
2314
|
-
function
|
|
2730
|
+
function presetKeyFiles(fileKey, scope, globs, expand) {
|
|
2731
|
+
const scoped = globs.flatMap((glob) => presetIntersect(glob, scope.key));
|
|
2732
|
+
const within = (glob) => {
|
|
2733
|
+
const prefix = glob.slice(0, -`**/${presetExtensions}`.length).replace(/\/$/, "");
|
|
2734
|
+
return glob.endsWith(`**/${presetExtensions}`) && !/[*?{[]/.test(prefix) && (prefix === "" || fileKey.pattern.startsWith(`${prefix}/`)) && /\.(?:ts|tsx|mts|cts)$/.test(fileKey.pattern);
|
|
2735
|
+
};
|
|
2736
|
+
return scoped.some(within) ? presetIntersect(fileKey.pattern, scope.key) : expand(fileKey).filter((path) => scoped.some((glob) => matchesGlob2(path, glob))).map(presetGlobEscape);
|
|
2737
|
+
}
|
|
2738
|
+
function presetEmit(project, name, builder, expand) {
|
|
2315
2739
|
const key = (scope) => JSON.stringify(presetSettings(scope, name));
|
|
2316
2740
|
const breaks = project.scopes.filter((scope) => scope.parent === null || key(scope) !== key(scope.parent));
|
|
2317
|
-
const
|
|
2318
|
-
let current = scope
|
|
2319
|
-
while (current &&
|
|
2741
|
+
const region = (scope) => {
|
|
2742
|
+
let current = scope;
|
|
2743
|
+
while (current && !breaks.includes(current)) {
|
|
2320
2744
|
current = current.parent;
|
|
2321
2745
|
}
|
|
2322
|
-
return
|
|
2323
|
-
}
|
|
2746
|
+
return current;
|
|
2747
|
+
};
|
|
2748
|
+
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
2749
|
return breaks.flatMap((owner) => {
|
|
2325
2750
|
const settings = presetSettings(owner, name);
|
|
2326
2751
|
if (!settings) {
|
|
2327
2752
|
return [];
|
|
2328
2753
|
}
|
|
2329
|
-
const
|
|
2754
|
+
const nested = breaks.filter((scope) => scope !== owner && region(scope.parent) === owner).map((scope) => `${scope.key}/**`);
|
|
2755
|
+
const owned = keyed.filter((entry) => entry.owner === owner);
|
|
2330
2756
|
return builder(project, settings, owner).flatMap((override) => {
|
|
2331
2757
|
const files = override.files.flatMap((glob) => presetIntersect(glob, owner.key));
|
|
2332
2758
|
if (files.length === 0 || !override.rules) {
|
|
2333
2759
|
return [];
|
|
2334
2760
|
}
|
|
2761
|
+
const replaced = owned.flatMap((entry) => presetKeyFiles(entry.fileKey, entry.scope, override.files, expand));
|
|
2335
2762
|
return [
|
|
2336
2763
|
{
|
|
2337
2764
|
...override,
|
|
2338
2765
|
files,
|
|
2339
|
-
excludeFiles: [...override.excludeFiles ?? [], ...
|
|
2766
|
+
excludeFiles: [...override.excludeFiles ?? [], ...nested, ...replaced],
|
|
2340
2767
|
rules: presetSeverity(override.rules, settings.level)
|
|
2341
2768
|
}
|
|
2342
2769
|
];
|
|
2343
2770
|
});
|
|
2344
2771
|
});
|
|
2345
2772
|
}
|
|
2773
|
+
function presetFileEmit(project, name, builder, expand) {
|
|
2774
|
+
return project.fileKeys.flatMap((fileKey) => fileKey.scopes.flatMap((scope) => {
|
|
2775
|
+
const owner = scope.parent;
|
|
2776
|
+
const settings = presetSettings(scope, name);
|
|
2777
|
+
if (!owner || !settings || JSON.stringify(presetSettings(owner, name)) === JSON.stringify(settings)) {
|
|
2778
|
+
return [];
|
|
2779
|
+
}
|
|
2780
|
+
const nested = project.scopes.filter((other) => other.parent === owner).map((other) => `${other.key}/**`);
|
|
2781
|
+
return builder(project, settings, scope).flatMap((override) => {
|
|
2782
|
+
const files = presetKeyFiles(fileKey, scope, override.files, expand);
|
|
2783
|
+
if (files.length === 0 || !override.rules) {
|
|
2784
|
+
return [];
|
|
2785
|
+
}
|
|
2786
|
+
return [{ ...override, files, excludeFiles: [...override.excludeFiles ?? [], ...nested], rules: presetSeverity(override.rules, settings.level) }];
|
|
2787
|
+
});
|
|
2788
|
+
}));
|
|
2789
|
+
}
|
|
2346
2790
|
function presetPlugins(project) {
|
|
2347
2791
|
const react = project.scopes.some((scope) => scope.effective.react);
|
|
2348
2792
|
const effect = project.scopes.some((scope) => scope.effective.effect);
|
|
2793
|
+
const effectCode = project.scopes.some((scope) => scope.effective.effect?.code);
|
|
2349
2794
|
const tailwinds = project.scopes.flatMap((scope) => scope.effective.tailwind ? [scope.effective.tailwind] : []);
|
|
2350
2795
|
const shadcns = project.scopes.flatMap((scope) => scope.effective.shadcn ? [scope.effective.shadcn] : []);
|
|
2351
2796
|
const tailwind = tailwinds.at(0);
|
|
@@ -2368,11 +2813,14 @@ function presetPlugins(project) {
|
|
|
2368
2813
|
{ name: "dillon-anti-slop", specifier: require2.resolve("oxlint-plugin-arch/dillon-anti-slop") }
|
|
2369
2814
|
];
|
|
2370
2815
|
const plugins = ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"];
|
|
2371
|
-
if (
|
|
2816
|
+
if (effectCode) {
|
|
2372
2817
|
const { tsgo, linteffect } = presetEffectPackages();
|
|
2373
2818
|
plugins.push(...tsgo.correctness.plugins ?? []);
|
|
2374
2819
|
jsPlugins.push(...linteffect.recommended.jsPlugins);
|
|
2375
2820
|
}
|
|
2821
|
+
if (effect) {
|
|
2822
|
+
jsPlugins.push(...presetEffectStyle().jsPlugins);
|
|
2823
|
+
}
|
|
2376
2824
|
const settings = {
|
|
2377
2825
|
react: { version: "19" },
|
|
2378
2826
|
"import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
|
|
@@ -2395,17 +2843,29 @@ function preset(config) {
|
|
|
2395
2843
|
const project = presetProject(config);
|
|
2396
2844
|
const [root] = project.scopes;
|
|
2397
2845
|
const level = presetLevel(root);
|
|
2846
|
+
const expanded = new Map;
|
|
2847
|
+
const expand = (fileKey) => {
|
|
2848
|
+
const paths = expanded.get(fileKey.pattern) ?? globSync(fileKey.pattern, { cwd: project.root, exclude: (path) => posix9.basename(path) === "node_modules" });
|
|
2849
|
+
expanded.set(fileKey.pattern, paths);
|
|
2850
|
+
return paths;
|
|
2851
|
+
};
|
|
2398
2852
|
const emitGroup = (name) => {
|
|
2399
2853
|
const builder = presetBuilders[name];
|
|
2400
|
-
return builder ? presetEmit(project, name, builder) : [];
|
|
2854
|
+
return builder ? [...presetEmit(project, name, builder, expand), ...presetFileEmit(project, name, builder, expand)] : [];
|
|
2401
2855
|
};
|
|
2402
2856
|
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
|
|
2857
|
+
const fileSeverities = project.fileKeys.flatMap((fileKey) => fileKey.folder.severity ? [{ files: [fileKey.pattern], rules: presetSeverity(presetCommonRules, fileKey.folder.severity) }] : []);
|
|
2858
|
+
const overrides = [...severities, ...fileSeverities, ...presetGroupOrder.flatMap(emitGroup)];
|
|
2404
2859
|
for (const scope of project.scopes.toSorted((left, right) => left.depth - right.depth)) {
|
|
2405
2860
|
if (scope.folder.rules) {
|
|
2406
2861
|
overrides.push({ files: [scope.key === "." ? "**" : `${scope.key}/**`], rules: scope.folder.rules });
|
|
2407
2862
|
}
|
|
2408
2863
|
}
|
|
2864
|
+
for (const fileKey of project.fileKeys) {
|
|
2865
|
+
if (fileKey.folder.rules) {
|
|
2866
|
+
overrides.push({ files: [fileKey.pattern], rules: fileKey.folder.rules });
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2409
2869
|
return {
|
|
2410
2870
|
options: { typeAware: true },
|
|
2411
2871
|
categories: { correctness: level },
|