oxlint-plugin-arch 0.12.6 → 0.13.1

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