oxlint-plugin-arch 0.12.5 → 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 +106 -0
- package/README.md +4 -4
- package/dist/index.js +1204 -170
- 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 +756 -223
- package/dist/preset/preset.utils.d.ts +6 -0
- package/dist/rules/code/code-delegate.d.ts +1 -0
- package/dist/rules/effect/effect-layer-file.d.ts +1 -0
- package/dist/rules/effect/effect-layers.d.ts +1 -0
- package/dist/rules/effect/effect-merge-pipe.d.ts +1 -0
- package/dist/rules/effect/effect-schema-first.d.ts +1 -0
- package/dist/rules/effect/effect-values-first.d.ts +1 -0
- package/dist/rules/react/react-query-props.d.ts +1 -0
- package/dist/rules/types/types-duplicate.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/effect.types.d.ts +46 -0
- package/dist/types/files.types.d.ts +3 -2
- package/dist/types/preset.types.d.ts +34 -23
- package/dist/types/react.types.d.ts +4 -0
- package/dist/types/schemas.types.d.ts +2 -0
- package/dist/types/types.types.d.ts +8 -0
- package/dist/utils/effect.utils.d.ts +5 -0
- package/dist/vendor/dillon-anti-slop/index.js +4 -1
- package/package.json +25 -1
package/dist/preset/preset.js
CHANGED
|
@@ -1,36 +1,21 @@
|
|
|
1
|
-
// src/preset/preset.ts
|
|
2
1
|
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/preset/preset.ts
|
|
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";
|
|
3
8
|
|
|
4
9
|
// src/preset/groups/backend.ts
|
|
5
|
-
import { posix as
|
|
10
|
+
import { posix as posix4, resolve as resolve4 } from "node:path";
|
|
6
11
|
|
|
7
12
|
// src/preset/preset-detect.ts
|
|
8
|
-
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
9
|
-
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";
|
|
10
15
|
|
|
11
|
-
// src/preset/preset-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
15
|
-
next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
16
|
-
expo: { react: true, expo: true, schemas: true, forms: true },
|
|
17
|
-
backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
|
|
18
|
-
react: { react: true, tailwind: true, schemas: true },
|
|
19
|
-
desktop: { console: true },
|
|
20
|
-
library: { schemas: true },
|
|
21
|
-
effect: { schemas: true, effect: true },
|
|
22
|
-
ui: presetUi,
|
|
23
|
-
kit: presetUi,
|
|
24
|
-
cli: { console: true }
|
|
25
|
-
};
|
|
26
|
-
var presetTemplates = {
|
|
27
|
-
monorepo: {
|
|
28
|
-
"apps/web": presetRecipes.tanstack,
|
|
29
|
-
"apps/server": presetRecipes.backend,
|
|
30
|
-
"packages/*": presetRecipes.library,
|
|
31
|
-
scripts: presetRecipes.cli
|
|
32
|
-
}
|
|
33
|
-
};
|
|
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";
|
|
34
19
|
|
|
35
20
|
// src/preset/preset.constants.ts
|
|
36
21
|
var presetExtensions = "*.{ts,tsx,mts,cts}";
|
|
@@ -344,20 +329,22 @@ var presetGeneratedFiles = [
|
|
|
344
329
|
"**/routeTree.gen.ts",
|
|
345
330
|
"**/paraglide/**"
|
|
346
331
|
];
|
|
347
|
-
var
|
|
348
|
-
"service",
|
|
349
|
-
"client",
|
|
350
|
-
"server",
|
|
351
|
-
"rsc",
|
|
352
|
-
"
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
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"
|
|
360
346
|
];
|
|
347
|
+
var presetLayoutFileName = /^!?[\w.*-]+$/;
|
|
361
348
|
var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
|
|
362
349
|
var presetOutputFolders = ["dist", "build", "coverage", ".output"];
|
|
363
350
|
var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
|
|
@@ -441,7 +428,7 @@ var presetExternals = [
|
|
|
441
428
|
"playwright-core"
|
|
442
429
|
];
|
|
443
430
|
var presetGroupOptions = {
|
|
444
|
-
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"] },
|
|
445
432
|
limits: { level: "error", lines: 400, types: 1000, complexity: 32 },
|
|
446
433
|
style: { level: "error", quotes: "single", semicolons: true, multiline: true, padding: true, dashes: true },
|
|
447
434
|
basics: { level: "error", trivial: true, state: true, switch: true, negation: true },
|
|
@@ -457,17 +444,20 @@ var presetGroupOptions = {
|
|
|
457
444
|
tailwind: { level: "error", css: undefined, rem: 16 },
|
|
458
445
|
shadcn: { level: "error", ui: "@/components/ui", components: ["^@/components/"] },
|
|
459
446
|
kit: { level: "error" },
|
|
460
|
-
backend: { level: "error", exports: undefined, registry: undefined
|
|
461
|
-
orpc: { level: "error", public: [],
|
|
447
|
+
backend: { level: "error", exports: undefined, registry: undefined },
|
|
448
|
+
orpc: { level: "error", public: [], client: undefined },
|
|
462
449
|
schemas: { level: "error" },
|
|
463
|
-
forms: { level: "error"
|
|
464
|
-
sql: { level: "error"
|
|
465
|
-
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: [] },
|
|
466
453
|
console: { level: "error" },
|
|
467
454
|
deny: { level: "error", imports: {}, calls: {} },
|
|
468
|
-
externals: { level: "error"
|
|
455
|
+
externals: { level: "error" }
|
|
469
456
|
};
|
|
457
|
+
var presetQueryWrites = ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"];
|
|
470
458
|
var presetFolderKeys = ["severity", "source", "services", "rules"];
|
|
459
|
+
var presetFolderOnlyKeys = ["source", "services", "tanstack", "next", "expo", "backend", "console", "kit"];
|
|
460
|
+
var presetFileSegment = /^(?!\.{1,2}$)[\w.@$[\]*-]+$/;
|
|
471
461
|
var presetA11yRules = [
|
|
472
462
|
"alt-text",
|
|
473
463
|
"anchor-has-content",
|
|
@@ -507,6 +497,7 @@ var presetA11yRules = [
|
|
|
507
497
|
].map((rule) => `jsx-a11y/${rule}`);
|
|
508
498
|
var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateStaticParams"];
|
|
509
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." };
|
|
510
501
|
var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
|
|
511
502
|
var presetDetectFrameworks = [
|
|
512
503
|
["next", ["next", "vinext"]],
|
|
@@ -514,6 +505,13 @@ var presetDetectFrameworks = [
|
|
|
514
505
|
["expo", ["expo"]]
|
|
515
506
|
];
|
|
516
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");
|
|
517
515
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
518
516
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
519
517
|
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
@@ -525,15 +523,121 @@ var presetKitImports = ["**/kits", "**/kits/index", "**/kits/index.ts"];
|
|
|
525
523
|
var presetKitFiles = "^(?:kit|[a-z0-9]+(?:-[a-z0-9]+)*\\.kit)\\.ts$";
|
|
526
524
|
var presetCliFiles = "^cli(?:-[a-z0-9]+)*(?:\\.[a-z0-9-]+)*\\.tsx?$";
|
|
527
525
|
var presetMigrationFiles = "^(?:\\d{3,14}[-_][a-z0-9]+(?:[-_][a-z0-9]+)*|index)\\.ts$";
|
|
528
|
-
var presetMigrationNumber = "^(\\d{3,14})[-_]";
|
|
526
|
+
var presetMigrationNumber = "^(\\d{4}-\\d{2}-\\d{2}(?:-\\d{2})*|\\d{3,14})[-_]";
|
|
529
527
|
var presetRootImports = { "framer-motion": "Use motion/react." };
|
|
530
528
|
var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
|
|
531
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
|
+
|
|
532
590
|
// src/preset/preset.utils.ts
|
|
533
|
-
import { readFileSync, statSync } from "node:fs";
|
|
591
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
592
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
534
593
|
import { posix, resolve } from "node:path";
|
|
535
594
|
class PresetError extends Error {
|
|
536
595
|
}
|
|
596
|
+
async function presetEffectImport(load) {
|
|
597
|
+
try {
|
|
598
|
+
return await load;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (error instanceof Error && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
var presetEffectTsgo = await presetEffectImport(import("@effect/tsgo/oxlint-presets"));
|
|
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
|
+
}
|
|
635
|
+
function presetEffectPackages() {
|
|
636
|
+
if (!presetEffectTsgo || !presetEffectLinteffect) {
|
|
637
|
+
throw new PresetError("The effect group needs @effect/tsgo and @opsydyn/oxlint-effect: bun add -d @effect/tsgo @opsydyn/oxlint-effect");
|
|
638
|
+
}
|
|
639
|
+
return { tsgo: presetEffectTsgo, linteffect: presetEffectLinteffect };
|
|
640
|
+
}
|
|
537
641
|
function presetScopes(roots, suffix = `**/${presetExtensions}`) {
|
|
538
642
|
return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
|
|
539
643
|
}
|
|
@@ -596,6 +700,9 @@ function presetIntersect(glob, scope) {
|
|
|
596
700
|
const matches = presetIntersectSegments(glob.split("/"), scope.split("/")).map((segments) => segments.join("/"));
|
|
597
701
|
return [...new Set(matches)];
|
|
598
702
|
}
|
|
703
|
+
function presetGlobEscape(path) {
|
|
704
|
+
return path.replaceAll(/[[\]]/g, (bracket) => `[${bracket}]`);
|
|
705
|
+
}
|
|
599
706
|
function presetAliases(root, aliases) {
|
|
600
707
|
return Object.fromEntries(Object.entries(aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve(root, path)]));
|
|
601
708
|
}
|
|
@@ -654,77 +761,328 @@ function presetIsFile(path) {
|
|
|
654
761
|
return false;
|
|
655
762
|
}
|
|
656
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
|
+
}
|
|
657
1026
|
|
|
658
1027
|
// src/preset/preset-detect.ts
|
|
659
1028
|
function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
660
|
-
if (!presetIsDirectory(
|
|
1029
|
+
if (!presetIsDirectory(resolve3(root, base))) {
|
|
661
1030
|
return [];
|
|
662
1031
|
}
|
|
663
|
-
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));
|
|
664
1033
|
}
|
|
665
1034
|
var presetNamedSkip = ["node_modules"];
|
|
666
|
-
function presetSource(root, directory, configured) {
|
|
667
|
-
if (configured !== undefined) {
|
|
668
|
-
return posix2.join(directory, configured);
|
|
669
|
-
}
|
|
670
|
-
return existsSync(resolve2(root, directory, "src")) ? posix2.join(directory, "src") : directory;
|
|
671
|
-
}
|
|
672
1035
|
function presetRegistryFile(root, source, configured, services) {
|
|
673
1036
|
if (configured === false) {
|
|
674
1037
|
return;
|
|
675
1038
|
}
|
|
676
1039
|
const file = typeof configured === "string" ? configured : configured?.file;
|
|
677
1040
|
if (file !== undefined) {
|
|
678
|
-
return
|
|
1041
|
+
return posix3.normalize(file);
|
|
679
1042
|
}
|
|
680
|
-
return [
|
|
1043
|
+
return [posix3.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix3.join(source, path))].find((path) => existsSync3(resolve3(root, path)));
|
|
681
1044
|
}
|
|
682
1045
|
function presetDomains(root, services) {
|
|
683
|
-
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`)));
|
|
684
1047
|
}
|
|
685
|
-
function
|
|
1048
|
+
function presetRecipe2(root, directory, app, folder) {
|
|
686
1049
|
const manifest = presetManifest(root, directory);
|
|
687
1050
|
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
688
1051
|
const has = (names) => names.some((name) => dependencies.has(name));
|
|
689
1052
|
const source = presetSource(root, directory, folder.source);
|
|
690
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;
|
|
691
1057
|
const servicesFolder = presetServicesFolder(root, source, folder.services, app);
|
|
692
|
-
const services =
|
|
693
|
-
const entries = presetIsDirectory(services) ?
|
|
694
|
-
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;
|
|
695
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;
|
|
696
1066
|
if (framework) {
|
|
697
|
-
return
|
|
1067
|
+
return { ...presetRecipes[framework], ...layout, ...style };
|
|
698
1068
|
}
|
|
699
1069
|
if (has(presetDetectDesktop)) {
|
|
700
1070
|
return presetRecipes.desktop;
|
|
701
1071
|
}
|
|
702
|
-
|
|
703
|
-
|
|
1072
|
+
const reactLayout = frontend ? { ...reactRecipe, ...layout } : reactRecipe;
|
|
1073
|
+
if (has(["effect"]) && react) {
|
|
1074
|
+
return { ...reactLayout, ...style };
|
|
704
1075
|
}
|
|
705
|
-
if (has(["
|
|
706
|
-
return
|
|
1076
|
+
if (has(["effect"])) {
|
|
1077
|
+
return backend || presetIsDirectory(resolve3(root, source, "domains")) ? { ...presetRecipes.backend, ...effect } : effect;
|
|
707
1078
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
function presetMergeObject(base, top, depth) {
|
|
711
|
-
const merged = { ...base };
|
|
712
|
-
for (const [key, value] of Object.entries(top)) {
|
|
713
|
-
const previous = Reflect.get(merged, key);
|
|
714
|
-
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
715
|
-
if (nested && depth > 0) {
|
|
716
|
-
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
717
|
-
} else if (value !== true || !(previous instanceof Object)) {
|
|
718
|
-
Reflect.set(merged, key, value);
|
|
719
|
-
}
|
|
1079
|
+
if (backend) {
|
|
1080
|
+
return presetRecipes.backend;
|
|
720
1081
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
function presetMerge(base, top) {
|
|
724
|
-
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
725
|
-
return top === true && base instanceof Object ? base : top;
|
|
1082
|
+
if (react) {
|
|
1083
|
+
return reactLayout;
|
|
726
1084
|
}
|
|
727
|
-
return
|
|
1085
|
+
return presetRecipes.library;
|
|
728
1086
|
}
|
|
729
1087
|
function presetImportTargets(value) {
|
|
730
1088
|
if (typeof value === "string") {
|
|
@@ -737,7 +1095,7 @@ function presetImportTargets(value) {
|
|
|
737
1095
|
}
|
|
738
1096
|
function presetSpecifiers(manifest, target, file = false) {
|
|
739
1097
|
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
740
|
-
const path =
|
|
1098
|
+
const path = posix3.normalize(mapped);
|
|
741
1099
|
const parts = path.split("*");
|
|
742
1100
|
const keyParts = key.split("*");
|
|
743
1101
|
if (parts.length === 1 && keyParts.length === 1) {
|
|
@@ -766,23 +1124,17 @@ function presetOverlaps(left, right) {
|
|
|
766
1124
|
function presetDeny(imports) {
|
|
767
1125
|
return { deny: { imports } };
|
|
768
1126
|
}
|
|
769
|
-
function presetFilled(conventions, exists) {
|
|
770
|
-
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
771
|
-
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
772
|
-
return exists(path) && !empty;
|
|
773
|
-
}));
|
|
774
|
-
}
|
|
775
1127
|
function presetBackendConventions(root, key, value, denied) {
|
|
776
1128
|
const manifest = presetManifest(root, key);
|
|
777
1129
|
const source = presetSource(root, key, value.source);
|
|
778
|
-
const local =
|
|
779
|
-
const at = (path) => local === "." ? path :
|
|
780
|
-
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));
|
|
781
1133
|
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
782
1134
|
const servicesFolder = presetServicesFolder(root, source, value.services);
|
|
783
|
-
const servicesKey =
|
|
1135
|
+
const servicesKey = posix3.relative(key, servicesFolder);
|
|
784
1136
|
const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
|
|
785
|
-
const registry = registryFile === undefined ? undefined :
|
|
1137
|
+
const registry = registryFile === undefined ? undefined : posix3.relative(key, registryFile);
|
|
786
1138
|
const services = presetSpecifiers(manifest, servicesKey);
|
|
787
1139
|
const message = "Use the service API, not another service’s operations.";
|
|
788
1140
|
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
@@ -802,27 +1154,27 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
802
1154
|
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
803
1155
|
}
|
|
804
1156
|
};
|
|
805
|
-
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) =>
|
|
1157
|
+
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix3.basename(path))) {
|
|
806
1158
|
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
807
1159
|
[`${prefix}/${pattern}`, false],
|
|
808
1160
|
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
809
1161
|
]));
|
|
810
|
-
conventions[
|
|
1162
|
+
conventions[posix3.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
|
|
811
1163
|
}
|
|
812
1164
|
const testFolders = ["tests", "test"];
|
|
813
1165
|
for (const tests of testFolders) {
|
|
814
|
-
if (exists(
|
|
1166
|
+
if (exists(posix3.join(tests, "kits"))) {
|
|
815
1167
|
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
816
|
-
conventions[
|
|
1168
|
+
conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
817
1169
|
}
|
|
818
1170
|
}
|
|
819
1171
|
const queue = ["."];
|
|
820
1172
|
while (queue.length > 0) {
|
|
821
1173
|
const folder = queue.shift() ?? ".";
|
|
822
|
-
for (const child of presetDirectories(root,
|
|
823
|
-
if (
|
|
1174
|
+
for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
|
|
1175
|
+
if (posix3.basename(child) === "migrations") {
|
|
824
1176
|
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
825
|
-
} else if (child.split("/").length < 8 && !testFolders.includes(
|
|
1177
|
+
} else if (child.split("/").length < 8 && !testFolders.includes(posix3.basename(child))) {
|
|
826
1178
|
queue.push(child);
|
|
827
1179
|
}
|
|
828
1180
|
}
|
|
@@ -830,9 +1182,9 @@ function presetBackendConventions(root, key, value, denied) {
|
|
|
830
1182
|
return presetFilled(conventions, exists);
|
|
831
1183
|
}
|
|
832
1184
|
function presetWebConventions(root, key, value, servers) {
|
|
833
|
-
const source =
|
|
834
|
-
const at = (path) => source === "." ? path :
|
|
835
|
-
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));
|
|
836
1188
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
837
1189
|
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
838
1190
|
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
@@ -852,8 +1204,8 @@ function presetWebConventions(root, key, value, servers) {
|
|
|
852
1204
|
}
|
|
853
1205
|
function presetWorkspacePatterns(root, manifest) {
|
|
854
1206
|
const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
|
|
855
|
-
const file =
|
|
856
|
-
if (!
|
|
1207
|
+
const file = resolve3(root, "pnpm-workspace.yaml");
|
|
1208
|
+
if (!existsSync3(file)) {
|
|
857
1209
|
return listed;
|
|
858
1210
|
}
|
|
859
1211
|
const patterns = [...listed];
|
|
@@ -876,7 +1228,7 @@ function presetWorkspacePatterns(root, manifest) {
|
|
|
876
1228
|
return patterns;
|
|
877
1229
|
}
|
|
878
1230
|
function presetWorkspaces(root, patterns) {
|
|
879
|
-
const clean = (pattern) =>
|
|
1231
|
+
const clean = (pattern) => posix3.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
880
1232
|
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
881
1233
|
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
882
1234
|
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
@@ -895,7 +1247,7 @@ function presetWorkspaces(root, patterns) {
|
|
|
895
1247
|
level.forEach((folder) => candidates.add(folder));
|
|
896
1248
|
}
|
|
897
1249
|
}
|
|
898
|
-
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();
|
|
899
1251
|
}
|
|
900
1252
|
function presetContains(key, path) {
|
|
901
1253
|
if (key === "." || key === path) {
|
|
@@ -921,18 +1273,22 @@ function presetDetect(root, folders) {
|
|
|
921
1273
|
};
|
|
922
1274
|
const patterns = presetWorkspacePatterns(root, manifest);
|
|
923
1275
|
const directories = presetWorkspaces(root, patterns);
|
|
924
|
-
const detected = Object.fromEntries(directories.map((directory) => [directory,
|
|
925
|
-
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("."));
|
|
926
1278
|
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
927
1279
|
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
|
|
928
1280
|
if (patterns.length === 0 ? single : rootApp) {
|
|
929
1281
|
detected["."] = rootRecipe;
|
|
930
1282
|
}
|
|
931
|
-
if (
|
|
1283
|
+
if (existsSync3(resolve3(root, "scripts")) && !directories.includes("scripts")) {
|
|
932
1284
|
detected.scripts = presetRecipes.cli;
|
|
933
1285
|
}
|
|
1286
|
+
if (existsSync3(resolve3(root, ".agents"))) {
|
|
1287
|
+
const { quotes, semicolons } = presetGroupOptions.style;
|
|
1288
|
+
detected[".agents"] = { ...presetRecipes.cli, style: { quotes, semicolons } };
|
|
1289
|
+
}
|
|
934
1290
|
const keys = Object.keys(folders);
|
|
935
|
-
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)));
|
|
936
1292
|
const merged = { ".": presetDeny(presetRootImports) };
|
|
937
1293
|
for (const [key, value] of Object.entries(detected)) {
|
|
938
1294
|
if (!covered(key)) {
|
|
@@ -940,7 +1296,9 @@ function presetDetect(root, folders) {
|
|
|
940
1296
|
}
|
|
941
1297
|
}
|
|
942
1298
|
for (const [key, value] of Object.entries(folders)) {
|
|
943
|
-
|
|
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;
|
|
944
1302
|
}
|
|
945
1303
|
const length = (key) => key.split("/").length;
|
|
946
1304
|
const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
|
|
@@ -954,6 +1312,7 @@ function presetDetect(root, folders) {
|
|
|
954
1312
|
const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
|
|
955
1313
|
return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
|
|
956
1314
|
});
|
|
1315
|
+
presetEffectServers(root, merged, units);
|
|
957
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] : []) : []));
|
|
958
1317
|
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
959
1318
|
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
@@ -973,11 +1332,11 @@ function presetDetect(root, folders) {
|
|
|
973
1332
|
}
|
|
974
1333
|
if (value.backend) {
|
|
975
1334
|
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
976
|
-
add(path === "." ? key :
|
|
1335
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
977
1336
|
}
|
|
978
1337
|
} else if (value.tanstack || value.next) {
|
|
979
1338
|
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
980
|
-
add(path === "." ? key :
|
|
1339
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
981
1340
|
}
|
|
982
1341
|
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
983
1342
|
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
@@ -985,6 +1344,9 @@ function presetDetect(root, folders) {
|
|
|
985
1344
|
const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
|
|
986
1345
|
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
987
1346
|
}
|
|
1347
|
+
for (const [path, folder] of presetEffectConventions(root, key, value, units)) {
|
|
1348
|
+
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1349
|
+
}
|
|
988
1350
|
}
|
|
989
1351
|
return merged;
|
|
990
1352
|
}
|
|
@@ -1005,7 +1367,7 @@ var backendGroup = (project, settings) => {
|
|
|
1005
1367
|
}
|
|
1006
1368
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
1007
1369
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
1008
|
-
const file = configured.file === undefined ? undefined :
|
|
1370
|
+
const file = configured.file === undefined ? undefined : posix4.normalize(configured.file);
|
|
1009
1371
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
1010
1372
|
const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
|
|
1011
1373
|
return found ? [{ file: found, unit }] : [];
|
|
@@ -1015,8 +1377,8 @@ var backendGroup = (project, settings) => {
|
|
|
1015
1377
|
throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
|
|
1016
1378
|
}
|
|
1017
1379
|
const options = {
|
|
1018
|
-
registry:
|
|
1019
|
-
services:
|
|
1380
|
+
registry: resolve4(project.root, registry.file),
|
|
1381
|
+
services: resolve4(project.root, registry.unit.services),
|
|
1020
1382
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
1021
1383
|
};
|
|
1022
1384
|
if (configured.object !== undefined) {
|
|
@@ -1031,9 +1393,11 @@ var backendGroup = (project, settings) => {
|
|
|
1031
1393
|
var basicsGroup = (project, settings) => {
|
|
1032
1394
|
const trivial = { mode: "precise", async: false, callees: [] };
|
|
1033
1395
|
const { appFiles, safetyFiles, ui, web } = project;
|
|
1396
|
+
const nextEntries = presetScopes(project.nextWeb, "app/**/{layout,page,default}.tsx");
|
|
1034
1397
|
return [
|
|
1035
1398
|
...settings.trivial ? [
|
|
1036
|
-
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),
|
|
1037
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)
|
|
1038
1402
|
] : [],
|
|
1039
1403
|
...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
|
|
@@ -1054,37 +1418,93 @@ var consoleGroup = () => [presetOverride([`**/${presetExtensions}`], { "no-conso
|
|
|
1054
1418
|
|
|
1055
1419
|
// src/preset/groups/deny.ts
|
|
1056
1420
|
var denyGroup = (project, settings) => {
|
|
1057
|
-
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);
|
|
1058
1424
|
const overrides = [];
|
|
1059
1425
|
if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
|
|
1060
1426
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
|
|
1061
1427
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (writes.length === 0) {
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
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")) {
|
|
1067
1430
|
const files = [
|
|
1068
1431
|
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1069
1432
|
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
1070
1433
|
];
|
|
1071
|
-
|
|
1072
|
-
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 }) }] }));
|
|
1073
1435
|
}
|
|
1074
1436
|
return overrides;
|
|
1075
1437
|
};
|
|
1076
1438
|
|
|
1077
1439
|
// src/preset/groups/effect.ts
|
|
1440
|
+
var effectOff = {
|
|
1441
|
+
"linteffect/no-effect-fn-generator": "off",
|
|
1442
|
+
"linteffect/prefer-extracted-concept": "off",
|
|
1443
|
+
"linteffect/no-clever-effect-expression": "off",
|
|
1444
|
+
"linteffect/no-mixed-pillar-function": "off",
|
|
1445
|
+
"linteffect/no-piped-yield-in-gen": "off",
|
|
1446
|
+
"linteffect/no-domain-logic-in-conditional": "off",
|
|
1447
|
+
"linteffect/prefer-pipe-for-behavior": "off",
|
|
1448
|
+
"linteffect/no-ternary": "off",
|
|
1449
|
+
"linteffect/no-effect-as": "off",
|
|
1450
|
+
"linteffect/no-namespace-effect-import": "off",
|
|
1451
|
+
"linteffect/no-manual-effect-channels": "off",
|
|
1452
|
+
"linteffect/no-or-die-outside-boundary": "off",
|
|
1453
|
+
"linteffect/no-call-tower": "off",
|
|
1454
|
+
"linteffect/no-pipe-ladder": "off",
|
|
1455
|
+
"linteffect/no-nested-effect-gen": "off",
|
|
1456
|
+
"linteffect/no-branch-in-object": "off",
|
|
1457
|
+
"linteffect/no-magic-domain-string": "off",
|
|
1458
|
+
"linteffect/no-return-in-arrow": "off",
|
|
1459
|
+
"linteffect/no-effect-succeed-variable": "off",
|
|
1460
|
+
"linteffect/no-match-effect-branch": "off"
|
|
1461
|
+
};
|
|
1462
|
+
var effectEntryOff = {
|
|
1463
|
+
"effecttsgo/strict-effect-provide": "off",
|
|
1464
|
+
"linteffect/no-inline-runtime-provide": "off",
|
|
1465
|
+
"linteffect/no-inline-layer-provide-in-program": "off"
|
|
1466
|
+
};
|
|
1467
|
+
var effectTestOff = {
|
|
1468
|
+
...effectEntryOff,
|
|
1469
|
+
"linteffect/no-string-sentinel-return": "off",
|
|
1470
|
+
"linteffect/no-effect-never": "off",
|
|
1471
|
+
"linteffect/no-gen-for-mapping": "off",
|
|
1472
|
+
"linteffect/no-adhoc-domain-error": "off"
|
|
1473
|
+
};
|
|
1078
1474
|
var effectGroup = (project, settings) => {
|
|
1079
1475
|
const files = [`**/${presetExtensions}`];
|
|
1080
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
|
+
}
|
|
1485
|
+
const { tsgo, linteffect } = presetEffectPackages();
|
|
1486
|
+
const tsgoRules = [tsgo.correctness, tsgo.antipattern, tsgo.effectNative, tsgo.style].flatMap((config) => Object.keys(config.rules ?? {}));
|
|
1081
1487
|
return [
|
|
1488
|
+
style,
|
|
1489
|
+
presetOverride(files, {
|
|
1490
|
+
...Object.fromEntries(tsgoRules.map((rule) => [rule, "error"])),
|
|
1491
|
+
...linteffect.recommended.rules,
|
|
1492
|
+
...effectOff
|
|
1493
|
+
}),
|
|
1494
|
+
presetOverride(project.testFiles, effectTestOff),
|
|
1495
|
+
...settings.entries.length > 0 ? [presetOverride(settings.entries, effectEntryOff)] : [],
|
|
1082
1496
|
presetOverride(files, {
|
|
1083
1497
|
"arch/code-trivial-functions": "off",
|
|
1084
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",
|
|
1085
1502
|
"arch/code-function-nesting": ["error", { max: 4 }],
|
|
1086
1503
|
"arch/code-inline": ["error", { max, nesting: 2, keep: ["^[A-Z]", "^layer"], allow: "^(?:decode|encode)[A-Z]", services: ["of"] }],
|
|
1087
1504
|
"arch/code-yield-argument": "error",
|
|
1505
|
+
"arch/effect-layers": "error",
|
|
1506
|
+
"arch/effect-merge-pipe": "error",
|
|
1507
|
+
"arch/effect-values-first": "error",
|
|
1088
1508
|
"arch/layout-arrow-body": ["error", { max }],
|
|
1089
1509
|
"arch/layout-call-arguments": ["error", { split: false, hug: { max }, join: { max, nesting: 2, keep: ["Layer.mergeAll"] } }],
|
|
1090
1510
|
"arch/layout-call-depth": ["error", { max: 2 }],
|
|
@@ -1143,17 +1563,17 @@ var expoGroup = (project, settings) => {
|
|
|
1143
1563
|
};
|
|
1144
1564
|
|
|
1145
1565
|
// src/preset/groups/externals.ts
|
|
1146
|
-
var externalsGroup = (
|
|
1147
|
-
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 }] })
|
|
1148
1568
|
];
|
|
1149
1569
|
|
|
1150
1570
|
// src/preset/groups/forms.ts
|
|
1151
|
-
var formsGroup = (project
|
|
1152
|
-
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)
|
|
1153
1573
|
];
|
|
1154
1574
|
|
|
1155
1575
|
// src/preset/groups/imports.ts
|
|
1156
|
-
import { resolve as
|
|
1576
|
+
import { resolve as resolve5 } from "node:path";
|
|
1157
1577
|
function importsPackageNames(project, kinds) {
|
|
1158
1578
|
return project.units.flatMap((unit) => {
|
|
1159
1579
|
const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
|
|
@@ -1191,7 +1611,7 @@ var importsGroup = (project, settings) => {
|
|
|
1191
1611
|
if (!settings.boundaries) {
|
|
1192
1612
|
return overrides;
|
|
1193
1613
|
}
|
|
1194
|
-
const absolute = (paths) => paths.map((path) =>
|
|
1614
|
+
const absolute = (paths) => paths.map((path) => resolve5(project.root, path));
|
|
1195
1615
|
overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
|
|
1196
1616
|
"arch/boundaries-imports": [
|
|
1197
1617
|
"error",
|
|
@@ -1199,7 +1619,7 @@ var importsGroup = (project, settings) => {
|
|
|
1199
1619
|
web: absolute(project.web),
|
|
1200
1620
|
backend: absolute(project.backend),
|
|
1201
1621
|
packages: absolute(project.packages),
|
|
1202
|
-
roles: project.
|
|
1622
|
+
roles: project.fileKinds,
|
|
1203
1623
|
services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
|
|
1204
1624
|
portable: true,
|
|
1205
1625
|
aliases: presetAliases(project.root, settings.aliases),
|
|
@@ -1213,18 +1633,18 @@ var importsGroup = (project, settings) => {
|
|
|
1213
1633
|
};
|
|
1214
1634
|
|
|
1215
1635
|
// src/preset/groups/layout.ts
|
|
1216
|
-
import { posix as
|
|
1636
|
+
import { posix as posix5 } from "node:path";
|
|
1217
1637
|
var layoutGroup = (project, settings) => {
|
|
1218
1638
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
1219
1639
|
const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
|
|
1220
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.";
|
|
1221
|
-
const
|
|
1222
|
-
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`);
|
|
1223
1643
|
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
1224
|
-
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")));
|
|
1225
1645
|
return [
|
|
1226
1646
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
1227
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1647
|
+
const folders = folder === "services" && operations.some((source) => posix5.join(source, "services") === path);
|
|
1228
1648
|
const registries = folder === "services" ? project.registries : [];
|
|
1229
1649
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
1230
1650
|
"arch/files-restricted": [
|
|
@@ -1247,11 +1667,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1247
1667
|
"arch/naming-file": [
|
|
1248
1668
|
"error",
|
|
1249
1669
|
{
|
|
1250
|
-
pattern: `^
|
|
1251
|
-
message: `Domain files use
|
|
1670
|
+
pattern: `^(?:${names.join("|")})$`,
|
|
1671
|
+
message: `Domain files use one of: ${settings.files.join(", ")}. Add a name to layout.files.`
|
|
1252
1672
|
}
|
|
1253
1673
|
]
|
|
1254
|
-
}, [
|
|
1674
|
+
}, [
|
|
1675
|
+
...settings.files.includes("*-action.*.ts") ? ["**/*-action.*.ts"] : [],
|
|
1676
|
+
...settings.files.includes("*-query.*.ts") ? ["**/*-query.*.ts"] : [],
|
|
1677
|
+
...testFiles,
|
|
1678
|
+
...registries
|
|
1679
|
+
]);
|
|
1255
1680
|
return [...domain, strict];
|
|
1256
1681
|
}),
|
|
1257
1682
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
@@ -1268,16 +1693,16 @@ var layoutGroup = (project, settings) => {
|
|
|
1268
1693
|
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
1269
1694
|
"**/*.rsc.tsx",
|
|
1270
1695
|
...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
|
|
1271
|
-
...
|
|
1696
|
+
...kindFiles,
|
|
1272
1697
|
...project.colocated,
|
|
1273
1698
|
...testFiles
|
|
1274
1699
|
]),
|
|
1275
1700
|
presetOverride(services, {
|
|
1276
1701
|
"arch/backend-service-functions": "error",
|
|
1277
1702
|
"arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
|
|
1278
|
-
}, [...utilities, ...testFiles, ...
|
|
1703
|
+
}, [...utilities, ...testFiles, ...kindFiles]),
|
|
1279
1704
|
presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
|
|
1280
|
-
...
|
|
1705
|
+
...kindFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
|
|
1281
1706
|
"arch/backend-service-functions": "off",
|
|
1282
1707
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
1283
1708
|
}, testFiles)),
|
|
@@ -1455,7 +1880,7 @@ var nextGroup = (project, settings) => {
|
|
|
1455
1880
|
// src/preset/groups/orpc.ts
|
|
1456
1881
|
var orpcGroup = (project) => project.orpc.public.length > 0 ? [
|
|
1457
1882
|
presetOverride(project.orpc.public, {
|
|
1458
|
-
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output"
|
|
1883
|
+
"arch/backend-orpc-output": ["error", { handler: "handler", output: "output" }]
|
|
1459
1884
|
})
|
|
1460
1885
|
] : [];
|
|
1461
1886
|
|
|
@@ -1552,13 +1977,8 @@ var shadcnGroup = (project) => {
|
|
|
1552
1977
|
};
|
|
1553
1978
|
|
|
1554
1979
|
// src/preset/groups/sql.ts
|
|
1555
|
-
var sqlGroup = (project
|
|
1556
|
-
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
|
|
1557
|
-
"arch/backend-sql-like": [
|
|
1558
|
-
"error",
|
|
1559
|
-
{ methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
|
|
1560
|
-
]
|
|
1561
|
-
})
|
|
1980
|
+
var sqlGroup = (project) => [
|
|
1981
|
+
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), { "arch/backend-sql-like": "error" })
|
|
1562
1982
|
];
|
|
1563
1983
|
|
|
1564
1984
|
// src/preset/groups/style.ts
|
|
@@ -1610,7 +2030,7 @@ var tanstackGroup = (project, settings) => {
|
|
|
1610
2030
|
"no-restricted-imports": [
|
|
1611
2031
|
"error",
|
|
1612
2032
|
{
|
|
1613
|
-
paths: [
|
|
2033
|
+
paths: [presetTanstackRsc],
|
|
1614
2034
|
patterns: project.baseImports
|
|
1615
2035
|
}
|
|
1616
2036
|
]
|
|
@@ -1646,7 +2066,7 @@ function optionsPatternsTest(patterns, value) {
|
|
|
1646
2066
|
function optionsGlob(pattern) {
|
|
1647
2067
|
const negated = [];
|
|
1648
2068
|
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
1649
|
-
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]
|
|
2069
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\?{}]/g, "\\$&").replaceAll("*", "[^/]*")).join("|"));
|
|
1650
2070
|
return `\x02${negated.length - 1}\x03`;
|
|
1651
2071
|
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
1652
2072
|
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
@@ -1762,12 +2182,12 @@ var typesGroup = (project, settings) => {
|
|
|
1762
2182
|
};
|
|
1763
2183
|
|
|
1764
2184
|
// src/preset/preset-folders.ts
|
|
1765
|
-
import { existsSync as
|
|
1766
|
-
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";
|
|
1767
2187
|
|
|
1768
2188
|
// src/preset/preset-stylesheet.ts
|
|
1769
|
-
import { readdirSync as
|
|
1770
|
-
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";
|
|
1771
2191
|
function presetTailwindEntry(css) {
|
|
1772
2192
|
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1773
2193
|
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
@@ -1778,15 +2198,15 @@ function presetStylesheet(root, directory) {
|
|
|
1778
2198
|
const visited = new Set;
|
|
1779
2199
|
while (queue.length > 0) {
|
|
1780
2200
|
const folder = queue.shift() ?? directory;
|
|
1781
|
-
const real = realpathSync(
|
|
2201
|
+
const real = realpathSync(resolve6(root, folder));
|
|
1782
2202
|
if (visited.has(real)) {
|
|
1783
2203
|
continue;
|
|
1784
2204
|
}
|
|
1785
2205
|
visited.add(real);
|
|
1786
|
-
for (const name of
|
|
1787
|
-
const path =
|
|
1788
|
-
const full =
|
|
1789
|
-
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;
|
|
1790
2210
|
if (presetIsDirectory(full)) {
|
|
1791
2211
|
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1792
2212
|
queue.push(path);
|
|
@@ -1797,15 +2217,15 @@ function presetStylesheet(root, directory) {
|
|
|
1797
2217
|
}
|
|
1798
2218
|
}
|
|
1799
2219
|
const rank = (path) => {
|
|
1800
|
-
const index = presetStylesheetNames.indexOf(
|
|
2220
|
+
const index = presetStylesheetNames.indexOf(posix6.basename(path, ".css"));
|
|
1801
2221
|
return index === -1 ? presetStylesheetNames.length : index;
|
|
1802
2222
|
};
|
|
1803
2223
|
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1804
2224
|
}
|
|
1805
2225
|
|
|
1806
2226
|
// src/preset/preset-units.ts
|
|
1807
|
-
import { existsSync as
|
|
1808
|
-
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";
|
|
1809
2229
|
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1810
2230
|
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1811
2231
|
function presetUnitSource(root, key, kind, source) {
|
|
@@ -1813,9 +2233,9 @@ function presetUnitSource(root, key, kind, source) {
|
|
|
1813
2233
|
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1814
2234
|
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1815
2235
|
}
|
|
1816
|
-
return
|
|
2236
|
+
return posix7.join(key, source);
|
|
1817
2237
|
}
|
|
1818
|
-
return kind === "console" || !
|
|
2238
|
+
return kind === "console" || !existsSync4(resolve7(root, key, "src")) ? key : posix7.join(key, "src");
|
|
1819
2239
|
}
|
|
1820
2240
|
function presetSets(folder, name) {
|
|
1821
2241
|
return folder[name] !== undefined && folder[name] !== false;
|
|
@@ -1844,10 +2264,10 @@ function presetHasManifest(root, key) {
|
|
|
1844
2264
|
return false;
|
|
1845
2265
|
}
|
|
1846
2266
|
if (!key.endsWith("/*")) {
|
|
1847
|
-
return
|
|
2267
|
+
return existsSync4(resolve7(root, key, "package.json"));
|
|
1848
2268
|
}
|
|
1849
|
-
const base =
|
|
1850
|
-
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")));
|
|
1851
2271
|
}
|
|
1852
2272
|
function presetUnit(root, key, scope) {
|
|
1853
2273
|
const { effective } = scope;
|
|
@@ -1896,20 +2316,24 @@ function presetUnits(root, scopes, ignored) {
|
|
|
1896
2316
|
return [presetUnit(root, scope.key, scope)];
|
|
1897
2317
|
}
|
|
1898
2318
|
const base = scope.key.slice(0, -2);
|
|
1899
|
-
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));
|
|
1900
2320
|
});
|
|
1901
2321
|
}
|
|
1902
2322
|
|
|
1903
2323
|
// src/preset/preset-folders.ts
|
|
1904
|
-
function
|
|
2324
|
+
function presetPortableKey(key) {
|
|
1905
2325
|
const portable = key.replaceAll("\\", "/");
|
|
1906
|
-
if (!portable.trim() ||
|
|
2326
|
+
if (!portable.trim() || posix8.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1907
2327
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1908
2328
|
}
|
|
1909
|
-
const normalized =
|
|
2329
|
+
const normalized = posix8.normalize(portable).replace(/\/$/, "");
|
|
1910
2330
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1911
2331
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1912
2332
|
}
|
|
2333
|
+
return normalized;
|
|
2334
|
+
}
|
|
2335
|
+
function presetNormalizeKey(key) {
|
|
2336
|
+
const normalized = presetPortableKey(key);
|
|
1913
2337
|
const segments = normalized.split("/");
|
|
1914
2338
|
if (normalized !== "." && segments.some((segment) => segment !== "*" && !presetLiteralSegment.test(segment))) {
|
|
1915
2339
|
throw new PresetError(`Folder keys must be literal paths; only a whole \`*\` segment may be a wildcard: ${key}`);
|
|
@@ -1919,6 +2343,25 @@ function presetNormalizeKey(key) {
|
|
|
1919
2343
|
}
|
|
1920
2344
|
return normalized;
|
|
1921
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
|
+
}
|
|
1922
2365
|
function presetMatches(pattern, path) {
|
|
1923
2366
|
return pattern.every((segment, index) => segment === "*" || segment === path[index]);
|
|
1924
2367
|
}
|
|
@@ -1954,13 +2397,28 @@ function presetGroupValue(name, value, inherited, severity) {
|
|
|
1954
2397
|
return { ...base, ...merged, level: severity ?? base.level };
|
|
1955
2398
|
}
|
|
1956
2399
|
function presetDenyEntries(inherited, changes) {
|
|
1957
|
-
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).
|
|
2400
|
+
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).map(([key, message]) => {
|
|
1958
2401
|
if (message !== false && typeof message !== "string") {
|
|
1959
2402
|
throw new PresetError(`Deny entry \`${key}\` takes a message string or false.`);
|
|
1960
2403
|
}
|
|
1961
|
-
return
|
|
2404
|
+
return [key, message];
|
|
1962
2405
|
}));
|
|
1963
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
|
+
}
|
|
1964
2422
|
function presetEffective(folder, parent) {
|
|
1965
2423
|
const resolveGroup = (name) => {
|
|
1966
2424
|
const defaults = presetDefaultGroups.includes(name) ? presetGroupOptions[name] : null;
|
|
@@ -1976,6 +2434,11 @@ function presetEffective(folder, parent) {
|
|
|
1976
2434
|
calls: presetDenyEntries(inherited.calls, deny.calls)
|
|
1977
2435
|
};
|
|
1978
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
|
+
}
|
|
1979
2442
|
return effective;
|
|
1980
2443
|
}
|
|
1981
2444
|
function presetFolder(key, value) {
|
|
@@ -2006,7 +2469,7 @@ function presetScopesTree(root, folders) {
|
|
|
2006
2469
|
}
|
|
2007
2470
|
for (const key of keys) {
|
|
2008
2471
|
const base = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
2009
|
-
if (!
|
|
2472
|
+
if (!existsSync5(resolve8(root, base))) {
|
|
2010
2473
|
throw new PresetError(`Folder \`${key}\` does not exist.`);
|
|
2011
2474
|
}
|
|
2012
2475
|
}
|
|
@@ -2038,14 +2501,26 @@ function presetScopesTree(root, folders) {
|
|
|
2038
2501
|
return scopes;
|
|
2039
2502
|
}
|
|
2040
2503
|
function presetProject(config) {
|
|
2041
|
-
const given =
|
|
2042
|
-
const root =
|
|
2043
|
-
const
|
|
2044
|
-
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));
|
|
2045
2510
|
if (twice) {
|
|
2046
2511
|
throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
|
|
2047
2512
|
}
|
|
2048
|
-
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
|
+
];
|
|
2049
2524
|
const scopes = presetScopesTree(root, folders);
|
|
2050
2525
|
const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
|
|
2051
2526
|
const live = scopes.filter((scope) => !ignored.has(scope.key));
|
|
@@ -2053,6 +2528,22 @@ function presetProject(config) {
|
|
|
2053
2528
|
if (dead) {
|
|
2054
2529
|
throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
|
|
2055
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
|
+
});
|
|
2056
2547
|
const units = presetUnits(root, scopes, ignored);
|
|
2057
2548
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
2058
2549
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
@@ -2063,10 +2554,6 @@ function presetProject(config) {
|
|
|
2063
2554
|
if (misplaced) {
|
|
2064
2555
|
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
|
|
2065
2556
|
}
|
|
2066
|
-
const writes = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.backend).writes !== undefined);
|
|
2067
|
-
if (writes) {
|
|
2068
|
-
throw new PresetError(`Set backend.writes on a backend folder, not on \`${writes.key}\`.`);
|
|
2069
|
-
}
|
|
2070
2557
|
const of = (kind) => units.filter((unit) => unit.kind === kind);
|
|
2071
2558
|
const webUnits = of("web");
|
|
2072
2559
|
const web = webUnits.map((unit) => unit.source);
|
|
@@ -2080,19 +2567,15 @@ function presetProject(config) {
|
|
|
2080
2567
|
const appFiles = presetScopes(app);
|
|
2081
2568
|
const webFiles = presetScopes(web);
|
|
2082
2569
|
const effective = live.map((scope) => scope.effective);
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
}
|
|
2087
|
-
if (suffixes.some((suffix) => presetBuiltinFileRoles.includes(suffix))) {
|
|
2088
|
-
throw new PresetError("Layout suffixes must not replace built-in file roles.");
|
|
2089
|
-
}
|
|
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
|
+
];
|
|
2090
2573
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
2091
2574
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
2092
2575
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
2093
|
-
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];
|
|
2094
2577
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
2095
|
-
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)])];
|
|
2096
2579
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
2097
2580
|
const stylesheet = (unit) => {
|
|
2098
2581
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -2100,7 +2583,7 @@ function presetProject(config) {
|
|
|
2100
2583
|
return;
|
|
2101
2584
|
}
|
|
2102
2585
|
const css = tailwind.css ?? presetStylesheet(root, unit.key);
|
|
2103
|
-
return css === undefined ? undefined :
|
|
2586
|
+
return css === undefined ? undefined : resolve8(root, css);
|
|
2104
2587
|
};
|
|
2105
2588
|
const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
|
|
2106
2589
|
const css = stylesheet(unit);
|
|
@@ -2134,21 +2617,21 @@ function presetProject(config) {
|
|
|
2134
2617
|
return [key];
|
|
2135
2618
|
}
|
|
2136
2619
|
const base = key.slice(0, -2);
|
|
2137
|
-
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));
|
|
2138
2621
|
});
|
|
2139
2622
|
return {
|
|
2140
2623
|
root,
|
|
2141
2624
|
serviceFolders,
|
|
2142
2625
|
scopes: live,
|
|
2626
|
+
fileKeys,
|
|
2143
2627
|
units,
|
|
2144
|
-
|
|
2628
|
+
fileKinds,
|
|
2145
2629
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
2146
2630
|
orpc: {
|
|
2147
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2148
|
-
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}/**`),
|
|
2149
2632
|
client: client ?? ""
|
|
2150
2633
|
},
|
|
2151
|
-
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 }))),
|
|
2152
2635
|
rpcClients,
|
|
2153
2636
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
2154
2637
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -2186,7 +2669,7 @@ function presetProject(config) {
|
|
|
2186
2669
|
baseImports: presetBaseImports,
|
|
2187
2670
|
css: { ...webCss, ...kitCss },
|
|
2188
2671
|
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
|
|
2189
|
-
ignores: skipped.map((key) => `${key}/**`),
|
|
2672
|
+
ignores: [...skipped.map((key) => `${key}/**`), ...fileEntries.flatMap(([key, value]) => value === false ? [presetGlobEscape(key)] : [])],
|
|
2190
2673
|
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
2191
2674
|
};
|
|
2192
2675
|
}
|
|
@@ -2195,7 +2678,7 @@ function presetProject(config) {
|
|
|
2195
2678
|
var recipes = presetRecipes;
|
|
2196
2679
|
var templates = presetTemplates;
|
|
2197
2680
|
var externals = presetExternals;
|
|
2198
|
-
var require2 =
|
|
2681
|
+
var require2 = createRequire3(import.meta.url);
|
|
2199
2682
|
function presetOptional(name, group) {
|
|
2200
2683
|
try {
|
|
2201
2684
|
return require2.resolve(name);
|
|
@@ -2244,40 +2727,70 @@ function presetSettings(scope, name) {
|
|
|
2244
2727
|
}
|
|
2245
2728
|
return settings ? null : { level: presetLevel(scope) };
|
|
2246
2729
|
}
|
|
2247
|
-
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) {
|
|
2248
2739
|
const key = (scope) => JSON.stringify(presetSettings(scope, name));
|
|
2249
2740
|
const breaks = project.scopes.filter((scope) => scope.parent === null || key(scope) !== key(scope.parent));
|
|
2250
|
-
const
|
|
2251
|
-
let current = scope
|
|
2252
|
-
while (current &&
|
|
2741
|
+
const region = (scope) => {
|
|
2742
|
+
let current = scope;
|
|
2743
|
+
while (current && !breaks.includes(current)) {
|
|
2253
2744
|
current = current.parent;
|
|
2254
2745
|
}
|
|
2255
|
-
return
|
|
2256
|
-
}
|
|
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 })));
|
|
2257
2749
|
return breaks.flatMap((owner) => {
|
|
2258
2750
|
const settings = presetSettings(owner, name);
|
|
2259
2751
|
if (!settings) {
|
|
2260
2752
|
return [];
|
|
2261
2753
|
}
|
|
2262
|
-
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);
|
|
2263
2756
|
return builder(project, settings, owner).flatMap((override) => {
|
|
2264
2757
|
const files = override.files.flatMap((glob) => presetIntersect(glob, owner.key));
|
|
2265
2758
|
if (files.length === 0 || !override.rules) {
|
|
2266
2759
|
return [];
|
|
2267
2760
|
}
|
|
2761
|
+
const replaced = owned.flatMap((entry) => presetKeyFiles(entry.fileKey, entry.scope, override.files, expand));
|
|
2268
2762
|
return [
|
|
2269
2763
|
{
|
|
2270
2764
|
...override,
|
|
2271
2765
|
files,
|
|
2272
|
-
excludeFiles: [...override.excludeFiles ?? [], ...
|
|
2766
|
+
excludeFiles: [...override.excludeFiles ?? [], ...nested, ...replaced],
|
|
2273
2767
|
rules: presetSeverity(override.rules, settings.level)
|
|
2274
2768
|
}
|
|
2275
2769
|
];
|
|
2276
2770
|
});
|
|
2277
2771
|
});
|
|
2278
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
|
+
}
|
|
2279
2790
|
function presetPlugins(project) {
|
|
2280
2791
|
const react = project.scopes.some((scope) => scope.effective.react);
|
|
2792
|
+
const effect = project.scopes.some((scope) => scope.effective.effect);
|
|
2793
|
+
const effectCode = project.scopes.some((scope) => scope.effective.effect?.code);
|
|
2281
2794
|
const tailwinds = project.scopes.flatMap((scope) => scope.effective.tailwind ? [scope.effective.tailwind] : []);
|
|
2282
2795
|
const shadcns = project.scopes.flatMap((scope) => scope.effective.shadcn ? [scope.effective.shadcn] : []);
|
|
2283
2796
|
const tailwind = tailwinds.at(0);
|
|
@@ -2299,6 +2812,15 @@ function presetPlugins(project) {
|
|
|
2299
2812
|
{ name: "arch", specifier: require2.resolve("oxlint-plugin-arch") },
|
|
2300
2813
|
{ name: "dillon-anti-slop", specifier: require2.resolve("oxlint-plugin-arch/dillon-anti-slop") }
|
|
2301
2814
|
];
|
|
2815
|
+
const plugins = ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"];
|
|
2816
|
+
if (effectCode) {
|
|
2817
|
+
const { tsgo, linteffect } = presetEffectPackages();
|
|
2818
|
+
plugins.push(...tsgo.correctness.plugins ?? []);
|
|
2819
|
+
jsPlugins.push(...linteffect.recommended.jsPlugins);
|
|
2820
|
+
}
|
|
2821
|
+
if (effect) {
|
|
2822
|
+
jsPlugins.push(...presetEffectStyle().jsPlugins);
|
|
2823
|
+
}
|
|
2302
2824
|
const settings = {
|
|
2303
2825
|
react: { version: "19" },
|
|
2304
2826
|
"import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
|
|
@@ -2307,7 +2829,7 @@ function presetPlugins(project) {
|
|
|
2307
2829
|
jsPlugins.push({ name: "react-extra", specifier: presetOptional("eslint-plugin-react", "react") }, presetOptional("eslint-plugin-react-you-might-not-need-an-effect", "react"), presetOptional("eslint-plugin-svg-jsx", "react"));
|
|
2308
2830
|
}
|
|
2309
2831
|
if (!tailwind || Object.keys(project.css).length === 0) {
|
|
2310
|
-
return { jsPlugins, settings };
|
|
2832
|
+
return { plugins, jsPlugins, settings };
|
|
2311
2833
|
}
|
|
2312
2834
|
jsPlugins.push(presetOptional("oxlint-tailwindcss", "tailwind"));
|
|
2313
2835
|
settings.tailwindcss = { rootFontSize: tailwind.rem };
|
|
@@ -2315,27 +2837,38 @@ function presetPlugins(project) {
|
|
|
2315
2837
|
jsPlugins.push(presetOptional("@shadcn/lint", "shadcn"));
|
|
2316
2838
|
settings.shadcn = { ui: shadcn.ui, componentImports: shadcn.components };
|
|
2317
2839
|
}
|
|
2318
|
-
return { jsPlugins, settings };
|
|
2840
|
+
return { plugins, jsPlugins, settings };
|
|
2319
2841
|
}
|
|
2320
2842
|
function preset(config) {
|
|
2321
2843
|
const project = presetProject(config);
|
|
2322
2844
|
const [root] = project.scopes;
|
|
2323
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
|
+
};
|
|
2324
2852
|
const emitGroup = (name) => {
|
|
2325
2853
|
const builder = presetBuilders[name];
|
|
2326
|
-
return builder ? presetEmit(project, name, builder) : [];
|
|
2854
|
+
return builder ? [...presetEmit(project, name, builder, expand), ...presetFileEmit(project, name, builder, expand)] : [];
|
|
2327
2855
|
};
|
|
2328
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)) }));
|
|
2329
|
-
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)];
|
|
2330
2859
|
for (const scope of project.scopes.toSorted((left, right) => left.depth - right.depth)) {
|
|
2331
2860
|
if (scope.folder.rules) {
|
|
2332
2861
|
overrides.push({ files: [scope.key === "." ? "**" : `${scope.key}/**`], rules: scope.folder.rules });
|
|
2333
2862
|
}
|
|
2334
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
|
+
}
|
|
2335
2869
|
return {
|
|
2336
2870
|
options: { typeAware: true },
|
|
2337
2871
|
categories: { correctness: level },
|
|
2338
|
-
plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
|
|
2339
2872
|
ignorePatterns: [
|
|
2340
2873
|
"**/node_modules/**",
|
|
2341
2874
|
...project.outputs,
|