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.
@@ -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 posix3, resolve as resolve3 } from "node:path";
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 posix2, resolve as resolve2 } from "node:path";
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-recipes.ts
12
- var presetUi = { react: true, kit: true, tailwind: true };
13
- var presetRecipes = {
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 presetBuiltinFileRoles = [
348
- "service",
349
- "client",
350
- "server",
351
- "rsc",
352
- "utils",
353
- "action",
354
- "query",
355
- "types",
356
- "constants",
357
- "handler",
358
- "test",
359
- "spec"
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, suffixes: [], strict: false, operations: "flat", service: ["make{Domain}Service", "{Domain}Service"] },
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, writes: ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"] },
461
- orpc: { level: "error", public: [], composers: [], client: undefined },
447
+ backend: { level: "error", exports: undefined, registry: undefined },
448
+ orpc: { level: "error", public: [], client: undefined },
462
449
  schemas: { level: "error" },
463
- forms: { level: "error", resolver: "standardSchemaResolver" },
464
- sql: { level: "error", sanitizers: ["escapeLikeWildcards"] },
465
- effect: { level: "error", width: 160, exports: undefined, skip: undefined },
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", packages: presetExternals }
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(resolve2(root, base))) {
1029
+ if (!presetIsDirectory(resolve3(root, base))) {
661
1030
  return [];
662
1031
  }
663
- return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix2.join(base, name));
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 posix2.normalize(file);
1041
+ return posix3.normalize(file);
679
1042
  }
680
- return [posix2.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix2.join(source, path))].find((path) => existsSync(resolve2(root, path)));
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) => posix2.basename(path)).filter((name) => existsSync(resolve2(root, services, name, `${name}.service.ts`)));
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 presetRecipe(root, directory, app, folder) {
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 = resolve2(root, servicesFolder);
693
- const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
694
- const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
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 flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
1067
+ return { ...presetRecipes[framework], ...layout, ...style };
698
1068
  }
699
1069
  if (has(presetDetectDesktop)) {
700
1070
  return presetRecipes.desktop;
701
1071
  }
702
- if (presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
703
- return presetRecipes.backend;
1072
+ const reactLayout = frontend ? { ...reactRecipe, ...layout } : reactRecipe;
1073
+ if (has(["effect"]) && react) {
1074
+ return { ...reactLayout, ...style };
704
1075
  }
705
- if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
706
- return app ? presetRecipes.react : presetRecipes.ui;
1076
+ if (has(["effect"])) {
1077
+ return backend || presetIsDirectory(resolve3(root, source, "domains")) ? { ...presetRecipes.backend, ...effect } : effect;
707
1078
  }
708
- return presetRecipes.library;
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
- return merged;
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 presetMergeObject(base, top, 2);
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 = posix2.normalize(mapped);
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 = posix2.relative(key, source) || ".";
779
- const at = (path) => local === "." ? path : posix2.join(local, path);
780
- const exists = (path) => existsSync(resolve2(root, key, 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 = posix2.relative(key, servicesFolder);
1135
+ const servicesKey = posix3.relative(key, servicesFolder);
784
1136
  const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
785
- const registry = registryFile === undefined ? undefined : posix2.relative(key, registryFile);
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) => posix2.basename(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[posix2.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
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(posix2.join(tests, "kits"))) {
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[posix2.join(tests, "kits")] = { naming: { files: presetKitFiles } };
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, posix2.join(key, folder)).map((path) => posix2.relative(key, path))) {
823
- if (posix2.basename(child) === "migrations") {
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(posix2.basename(child))) {
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 = posix2.relative(key, presetSource(root, key, value.source)) || ".";
834
- const at = (path) => source === "." ? path : posix2.join(source, path);
835
- const exists = (path) => existsSync(resolve2(root, key, 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 = resolve2(root, "pnpm-workspace.yaml");
856
- if (!existsSync(file)) {
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) => posix2.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
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) && existsSync(resolve2(root, path, "package.json"))).toSorted();
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, presetRecipe(root, directory, !directory.startsWith("packages/"), configured(directory))]));
925
- const rootRecipe = presetRecipe(root, ".", true, configured("."));
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 (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
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("/*") && posix2.dirname(key) === user.slice(0, -2)));
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
- merged[key] = key in merged ? presetMerge(merged[key], value) : value;
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 : posix2.join(key, path), folder);
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 : posix2.join(key, path), folder);
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 : posix3.normalize(configured.file);
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: resolve3(project.root, registry.file),
1019
- services: resolve3(project.root, registry.unit.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 { imports, calls } = settings;
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
- for (const unit of project.units) {
1063
- const writes = unit.kind === "backend" ? unit.scope.effective.backend?.writes ?? [] : [];
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
- const reads = Object.fromEntries(writes.map((name) => [name, "Query files only read; move writes to an action."]));
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 = (_project, settings) => [
1147
- presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: settings.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, settings) => [
1152
- presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: settings.resolver }] }, project.ui)
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 resolve4 } from "node:path";
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) => resolve4(project.root, 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.suffixes,
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 posix4 } from "node:path";
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 roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
1222
- const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
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 === posix4.join(path, "components")));
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) => posix4.join(source, "services") === path);
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: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
1251
- message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
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
- }, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
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
- ...conceptFiles,
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, ...conceptFiles]),
1703
+ }, [...utilities, ...testFiles, ...kindFiles]),
1279
1704
  presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
1280
- ...conceptFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
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", composers: project.orpc.composers }]
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, settings) => [
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: [{ name: "@tanstack/react-start/rsc", message: "Render server components only in services/*.rsc.tsx files." }],
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(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
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 existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
1766
- import { posix as posix7, resolve as resolve7 } from "node:path";
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 readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
1770
- import { posix as posix5, resolve as resolve5 } from "node:path";
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(resolve5(root, folder));
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 readdirSync2(resolve5(root, folder))) {
1787
- const path = posix5.join(folder, name);
1788
- const full = resolve5(root, path);
1789
- const depth = posix5.relative(directory, path).split("/").length;
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(posix5.basename(path, ".css"));
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 existsSync2, readdirSync as readdirSync3 } from "node:fs";
1808
- import { posix as posix6, resolve as resolve6 } from "node:path";
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 posix6.join(key, source);
2236
+ return posix7.join(key, source);
1817
2237
  }
1818
- return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix6.join(key, "src");
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 existsSync2(resolve6(root, key, "package.json"));
2267
+ return existsSync4(resolve7(root, key, "package.json"));
1848
2268
  }
1849
- const base = resolve6(root, key.slice(0, -2));
1850
- return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
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 readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix6.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
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 presetNormalizeKey(key) {
2324
+ function presetPortableKey(key) {
1905
2325
  const portable = key.replaceAll("\\", "/");
1906
- if (!portable.trim() || posix7.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
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 = posix7.normalize(portable).replace(/\/$/, "");
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 }).flatMap(([key, message]) => {
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 message === false ? [] : [[key, message]];
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 (!existsSync3(resolve7(root, base))) {
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 = resolve7(config.root ?? process.cwd());
2042
- const root = existsSync3(given) ? realpathSync2(given) : given;
2043
- const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
2044
- const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
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 folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
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 suffixes = [...new Set(effective.flatMap((groups) => groups.layout?.suffixes ?? []))];
2084
- if (suffixes.some((suffix) => !/^[a-z][a-z0-9-]*$/.test(suffix))) {
2085
- throw new PresetError("Layout suffixes must be lowercase kebab-case names.");
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) => posix7.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
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) => posix7.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
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 : resolve7(root, css);
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 readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
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
- suffixes,
2628
+ fileKinds,
2145
2629
  colocated: listOf(effective.map((groups) => groups.types?.colocated)),
2146
2630
  orpc: {
2147
- public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix7.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
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 : posix7.join(unit.source, folder), folder, mode }))),
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 = createRequire(import.meta.url);
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 presetEmit(project, name, builder) {
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 nearest = (owner) => breaks.filter((scope) => {
2251
- let current = scope.parent;
2252
- while (current && current !== owner && !breaks.includes(current)) {
2741
+ const region = (scope) => {
2742
+ let current = scope;
2743
+ while (current && !breaks.includes(current)) {
2253
2744
  current = current.parent;
2254
2745
  }
2255
- return scope !== owner && current === owner;
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 excluded = nearest(owner).map((scope) => `${scope.key}/**`);
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 ?? [], ...excluded],
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 overrides = [...severities, ...presetGroupOrder.flatMap(emitGroup)];
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,