oxlint-plugin-arch 0.12.0 → 0.12.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,8 +2,34 @@
2
2
  import { createRequire } from "node:module";
3
3
 
4
4
  // src/preset/groups/backend.ts
5
- import { existsSync } from "node:fs";
6
- import { posix, resolve as resolve2 } from "node:path";
5
+ import { posix as posix2, resolve as resolve3 } from "node:path";
6
+
7
+ // src/preset/preset-detect.ts
8
+ import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
9
+ import { matchesGlob, posix, resolve as resolve2 } from "node:path";
10
+
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
+ ui: presetUi,
22
+ kit: presetUi,
23
+ cli: { console: true }
24
+ };
25
+ var presetTemplates = {
26
+ monorepo: {
27
+ "apps/web": presetRecipes.tanstack,
28
+ "apps/server": presetRecipes.backend,
29
+ "packages/*": presetRecipes.library,
30
+ scripts: presetRecipes.cli
31
+ }
32
+ };
7
33
 
8
34
  // src/preset/preset.constants.ts
9
35
  var presetExtensions = "*.{ts,tsx,mts,cts}";
@@ -331,7 +357,9 @@ var presetBuiltinFileRoles = [
331
357
  "test",
332
358
  "spec"
333
359
  ];
334
- var presetLiteralSegment = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
360
+ var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
361
+ var presetOutputFolders = ["dist", "build", "coverage", ".output"];
362
+ var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
335
363
  var presetIllustrationPalette = [
336
364
  "text-color",
337
365
  "bg-color",
@@ -478,8 +506,8 @@ var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateSta
478
506
  var presetTestFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
479
507
  var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
480
508
  var presetDetectFrameworks = [
481
- ["tanstack", ["@tanstack/react-start"]],
482
509
  ["next", ["next", "vinext"]],
510
+ ["tanstack", ["@tanstack/react-start"]],
483
511
  ["expo", ["expo"]]
484
512
  ];
485
513
  var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-apps/cli"];
@@ -499,7 +527,7 @@ var presetRootImports = { "framer-motion": "Use motion/react." };
499
527
  var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
500
528
 
501
529
  // src/preset/preset.utils.ts
502
- import { readFileSync } from "node:fs";
530
+ import { readFileSync, statSync } from "node:fs";
503
531
  import { resolve } from "node:path";
504
532
  class PresetError extends Error {
505
533
  }
@@ -589,6 +617,357 @@ function presetRuntime(project, settings, framework, rule) {
589
617
  ];
590
618
  });
591
619
  }
620
+ function presetManifest(root, directory) {
621
+ try {
622
+ return JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
623
+ } catch {
624
+ return {};
625
+ }
626
+ }
627
+ function presetIsDirectory(path) {
628
+ try {
629
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+ function presetIsFile(path) {
635
+ try {
636
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
637
+ } catch {
638
+ return false;
639
+ }
640
+ }
641
+
642
+ // src/preset/preset-detect.ts
643
+ function presetDirectories(root, base, skip = presetDetectSkip) {
644
+ if (!presetIsDirectory(resolve2(root, base))) {
645
+ return [];
646
+ }
647
+ return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix.join(base, name));
648
+ }
649
+ var presetNamedSkip = ["node_modules"];
650
+ function presetSource(root, directory, configured) {
651
+ if (configured !== undefined) {
652
+ return posix.join(directory, configured);
653
+ }
654
+ return existsSync(resolve2(root, directory, "src")) ? posix.join(directory, "src") : directory;
655
+ }
656
+ function presetRegistryFile(root, source, configured) {
657
+ if (configured === false) {
658
+ return;
659
+ }
660
+ const file = typeof configured === "string" ? configured : configured?.file;
661
+ if (file !== undefined) {
662
+ return posix.normalize(file);
663
+ }
664
+ return presetRegistryFiles.map((path) => posix.join(source, path)).find((path) => existsSync(resolve2(root, path)));
665
+ }
666
+ function presetDomains(root, source) {
667
+ return presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path)).filter((name) => existsSync(resolve2(root, source, "services", name, `${name}.service.ts`)));
668
+ }
669
+ function presetRecipe(root, directory, app, configured) {
670
+ const manifest = presetManifest(root, directory);
671
+ const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
672
+ const has = (names) => names.some((name) => dependencies.has(name));
673
+ const source = presetSource(root, directory, configured);
674
+ const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
675
+ const services = resolve2(root, source, "services");
676
+ const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
677
+ const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
678
+ const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
679
+ if (framework) {
680
+ return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
681
+ }
682
+ if (has(presetDetectDesktop)) {
683
+ return presetRecipes.desktop;
684
+ }
685
+ if (presetDomains(root, source).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
686
+ return presetRecipes.backend;
687
+ }
688
+ if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
689
+ return app ? presetRecipes.react : presetRecipes.ui;
690
+ }
691
+ return presetRecipes.library;
692
+ }
693
+ function presetMergeObject(base, top, depth) {
694
+ const merged = { ...base };
695
+ for (const [key, value] of Object.entries(top)) {
696
+ const previous = Reflect.get(merged, key);
697
+ const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
698
+ if (nested && depth > 0) {
699
+ Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
700
+ } else if (value !== true || !(previous instanceof Object)) {
701
+ Reflect.set(merged, key, value);
702
+ }
703
+ }
704
+ return merged;
705
+ }
706
+ function presetMerge(base, top) {
707
+ if (!(base instanceof Object) || !(top instanceof Object)) {
708
+ return top === true && base instanceof Object ? base : top;
709
+ }
710
+ return presetMergeObject(base, top, 2);
711
+ }
712
+ function presetImportTargets(value) {
713
+ if (typeof value === "string") {
714
+ return [value];
715
+ }
716
+ if (Array.isArray(value)) {
717
+ return value.flatMap(presetImportTargets);
718
+ }
719
+ return value instanceof Object ? Object.values(value).flatMap(presetImportTargets) : [];
720
+ }
721
+ function presetSpecifiers(manifest, target, file = false) {
722
+ const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
723
+ const path = posix.normalize(mapped);
724
+ const parts = path.split("*");
725
+ const keyParts = key.split("*");
726
+ if (parts.length === 1 && keyParts.length === 1) {
727
+ return path.replace(/\.[cm]?[jt]sx?$/, "") === target ? [key] : [];
728
+ }
729
+ if (parts.length !== 2 || keyParts.length !== 2) {
730
+ return [];
731
+ }
732
+ const [before, after] = parts;
733
+ const [head, suffix] = keyParts;
734
+ const prefix = before.replace(/\/$/, "");
735
+ if (target !== prefix && !target.startsWith(`${prefix}/`)) {
736
+ return [];
737
+ }
738
+ const rest = target.slice(prefix.length).replace(/^\//, "");
739
+ const tail = after && rest.endsWith(after) ? rest.slice(0, -after.length) : rest;
740
+ return [`${head}${tail}${file ? suffix : ""}`.replace(/\/$/, "")];
741
+ }));
742
+ return [...new Set(specifiers)];
743
+ }
744
+ function presetOverlaps(left, right) {
745
+ const [leftBase] = left.split("*");
746
+ const [rightBase] = right.split("*");
747
+ return leftBase.startsWith(rightBase) || rightBase.startsWith(leftBase);
748
+ }
749
+ function presetDeny(imports) {
750
+ return { deny: { imports } };
751
+ }
752
+ function presetFilled(conventions, exists) {
753
+ return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
754
+ const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
755
+ return exists(path) && !empty;
756
+ }));
757
+ }
758
+ function presetBackendConventions(root, key, value, denied) {
759
+ const manifest = presetManifest(root, key);
760
+ const source = presetSource(root, key, value.source);
761
+ const local = posix.relative(key, source) || ".";
762
+ const at = (path) => local === "." ? path : posix.join(local, path);
763
+ const exists = (path) => existsSync(resolve2(root, key, path));
764
+ const configured = value.backend instanceof Object ? value.backend.registry : undefined;
765
+ const registryFile = presetRegistryFile(root, source, configured);
766
+ const registry = registryFile === undefined ? undefined : posix.relative(key, registryFile);
767
+ const services = presetSpecifiers(manifest, at("services"));
768
+ const message = "Use the service API, not another service’s operations.";
769
+ const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
770
+ const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.[cm]?[jt]sx?$/, ""), true).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
771
+ const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
772
+ const serviceImports = { ...registryImports, ...operations };
773
+ if (manifest.name) {
774
+ Object.assign(serviceImports, entries);
775
+ }
776
+ const conventions = {
777
+ [at("services")]: presetDeny(serviceImports),
778
+ [at("core")]: presetDeny(operations),
779
+ [at("rpc")]: presetDeny(registryImports),
780
+ [at("routes")]: presetDeny(registryImports),
781
+ [at("cli")]: {
782
+ naming: { files: presetCliFiles },
783
+ ...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
784
+ }
785
+ };
786
+ for (const domain of presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path))) {
787
+ const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
788
+ [`${prefix}/${pattern}`, false],
789
+ [`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
790
+ ]));
791
+ conventions[at(`services/${domain}`)] = presetDeny(Object.fromEntries(imports));
792
+ }
793
+ for (const tests of ["tests", "test"]) {
794
+ if (exists(posix.join(tests, "kits"))) {
795
+ conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
796
+ conventions[posix.join(tests, "kits")] = { naming: { files: presetKitFiles } };
797
+ }
798
+ }
799
+ const queue = ["."];
800
+ while (queue.length > 0) {
801
+ const folder = queue.shift() ?? ".";
802
+ for (const child of presetDirectories(root, posix.join(key, folder)).map((path) => posix.relative(key, path))) {
803
+ if (posix.basename(child) === "migrations") {
804
+ conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
805
+ } else if (child.split("/").length < 8) {
806
+ queue.push(child);
807
+ }
808
+ }
809
+ }
810
+ return presetFilled(conventions, exists);
811
+ }
812
+ function presetWebConventions(root, key, value, servers) {
813
+ const source = posix.relative(key, presetSource(root, key, value.source)) || ".";
814
+ const at = (path) => source === "." ? path : posix.join(source, path);
815
+ const exists = (path) => existsSync(resolve2(root, key, path));
816
+ const own = Object.keys(presetManifest(root, key).imports ?? {});
817
+ const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
818
+ const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
819
+ const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
820
+ const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
821
+ const appImports = Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."]));
822
+ if (servers.length > 0) {
823
+ appImports["**/services/*.service"] = "Import the client or rsc service file, not a server service.";
824
+ }
825
+ const conventions = {
826
+ ".": presetDeny(appImports),
827
+ [at("components")]: presetDeny(entries),
828
+ [at("routes")]: presetDeny(entries),
829
+ [at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
830
+ };
831
+ return presetFilled(conventions, exists);
832
+ }
833
+ function presetWorkspacePatterns(root, manifest) {
834
+ const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
835
+ const file = resolve2(root, "pnpm-workspace.yaml");
836
+ if (!existsSync(file)) {
837
+ return listed;
838
+ }
839
+ const patterns = [...listed];
840
+ let inside = false;
841
+ for (const line of readFileSync2(file, "utf8").split(`
842
+ `)) {
843
+ const flow = /^packages\s*:\s*\[(.*)\]/.exec(line);
844
+ const item = /^\s*-\s*(["']?)([^"'#\s]+)\1\s*(?:#.*)?$/.exec(line);
845
+ if (flow) {
846
+ patterns.push(...flow[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean));
847
+ inside = false;
848
+ } else if (/^packages\s*:/.test(line)) {
849
+ inside = true;
850
+ } else if (inside && item) {
851
+ patterns.push(item[2]);
852
+ } else if (/^\S/.test(line)) {
853
+ inside = false;
854
+ }
855
+ }
856
+ return patterns;
857
+ }
858
+ function presetWorkspaces(root, patterns) {
859
+ const clean = (pattern) => posix.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
860
+ const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
861
+ const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
862
+ const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
863
+ const candidates = new Set;
864
+ for (const pattern of include) {
865
+ const segments = pattern.split("/");
866
+ const literal = segments.findIndex((segment) => /[*?{[]/.test(segment));
867
+ if (literal === -1) {
868
+ candidates.add(pattern);
869
+ continue;
870
+ }
871
+ const depth = segments.includes("**") ? 8 : segments.length - literal;
872
+ let level = [segments.slice(0, literal).join("/") || "."];
873
+ for (let step = 0;step < depth; step++) {
874
+ level = level.flatMap((folder) => presetDirectories(root, folder, presetNamedSkip));
875
+ level.forEach((folder) => candidates.add(folder));
876
+ }
877
+ }
878
+ return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync(resolve2(root, path, "package.json"))).toSorted();
879
+ }
880
+ function presetContains(key, path) {
881
+ if (key === "." || key === path) {
882
+ return true;
883
+ }
884
+ const pattern = key.split("/");
885
+ const segments = path.split("/");
886
+ return pattern.length <= segments.length && pattern.every((segment, index) => segment === "*" || segment === segments[index]);
887
+ }
888
+ function presetOwner(folders, path, owns) {
889
+ const depth = (key) => key === "." ? 0 : key.split("/").length;
890
+ return Object.entries(folders).filter(([key, value]) => presetContains(key, path) && owns(value)).toSorted(([left], [right]) => depth(left) - depth(right) || Number(right.includes("*")) - Number(left.includes("*"))).at(-1)?.[1];
891
+ }
892
+ function presetGroupOff(folders, path, group) {
893
+ const owner = presetOwner(folders, path, (value) => value instanceof Object && (group in value));
894
+ return owner instanceof Object && Object(owner)[group] === false;
895
+ }
896
+ function presetDetect(root, folders) {
897
+ const manifest = presetManifest(root, ".");
898
+ const configuredSource = (key) => {
899
+ const value = folders[key];
900
+ return value instanceof Object ? value.source : undefined;
901
+ };
902
+ const patterns = presetWorkspacePatterns(root, manifest);
903
+ const directories = presetWorkspaces(root, patterns);
904
+ const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configuredSource(directory))]));
905
+ const rootRecipe = presetRecipe(root, ".", true, configuredSource("."));
906
+ const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
907
+ const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configuredSource(".")) !== ".";
908
+ if (patterns.length === 0 ? single : rootApp) {
909
+ detected["."] = rootRecipe;
910
+ }
911
+ if (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
912
+ detected.scripts = presetRecipes.cli;
913
+ }
914
+ const keys = Object.keys(folders);
915
+ const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix.dirname(key) === user.slice(0, -2)));
916
+ const merged = { ".": presetDeny(presetRootImports) };
917
+ for (const [key, value] of Object.entries(detected)) {
918
+ if (!covered(key)) {
919
+ merged[key] = presetMerge(merged[key], value);
920
+ }
921
+ }
922
+ for (const [key, value] of Object.entries(folders)) {
923
+ merged[key] = key in merged ? presetMerge(merged[key], value) : value;
924
+ }
925
+ const length = (key) => key.split("/").length;
926
+ const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
927
+ const units = Object.entries(merged).flatMap(([key, value]) => {
928
+ if (key.endsWith("/*")) {
929
+ return presetDirectories(root, key.slice(0, -2), presetNamedSkip).filter((directory) => !(directory in merged)).map((directory) => [directory, value]);
930
+ }
931
+ if (key.includes("*") || nested(key)) {
932
+ return [];
933
+ }
934
+ const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
935
+ return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
936
+ });
937
+ 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] : []) : []));
938
+ const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
939
+ const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
940
+ const add = (key, folder) => {
941
+ if (presetOwner(folders, key, () => true) === false) {
942
+ return;
943
+ }
944
+ const kept = folder instanceof Object ? Object.fromEntries(Object.entries(folder).filter(([group]) => !presetGroupOff(folders, key, group))) : folder;
945
+ if (kept instanceof Object && Object.keys(kept).length === 0) {
946
+ return;
947
+ }
948
+ merged[key] = key in merged ? presetMerge(kept, merged[key]) : kept;
949
+ };
950
+ for (const [key, value] of units) {
951
+ if (!(value instanceof Object)) {
952
+ continue;
953
+ }
954
+ if (value.backend) {
955
+ for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
956
+ add(path === "." ? key : posix.join(key, path), folder);
957
+ }
958
+ } else if (value.tanstack || value.next) {
959
+ for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
960
+ add(path === "." ? key : posix.join(key, path), folder);
961
+ }
962
+ } else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
963
+ const own = Object.keys(presetManifest(root, key).imports ?? {});
964
+ const message = presetPackageImports["../../apps/**"];
965
+ const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
966
+ add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
967
+ }
968
+ }
969
+ return merged;
970
+ }
592
971
 
593
972
  // src/preset/groups/backend.ts
594
973
  var backendGroup = (project, settings) => {
@@ -606,9 +985,9 @@ var backendGroup = (project, settings) => {
606
985
  }
607
986
  const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
608
987
  const backendUnits = project.units.filter((unit) => unit.kind === "backend");
609
- const file = configured.file === undefined ? undefined : posix.normalize(configured.file);
988
+ const file = configured.file === undefined ? undefined : posix2.normalize(configured.file);
610
989
  const registries = file === undefined ? backendUnits.flatMap((unit) => {
611
- const found = presetRegistryFiles.map((path) => posix.join(unit.source, path)).find((path) => existsSync(resolve2(project.root, path)));
990
+ const found = presetRegistryFile(project.root, unit.source, undefined);
612
991
  return found ? [{ file: found, unit }] : [];
613
992
  }) : [{ file, unit: backendUnits.find((unit) => unit.key === "." || file.startsWith(`${unit.key}/`)) }];
614
993
  for (const registry of registries) {
@@ -616,8 +995,8 @@ var backendGroup = (project, settings) => {
616
995
  throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
617
996
  }
618
997
  const options = {
619
- registry: resolve2(project.root, registry.file),
620
- services: resolve2(project.root, registry.unit.source, "services"),
998
+ registry: resolve3(project.root, registry.file),
999
+ services: resolve3(project.root, registry.unit.source, "services"),
621
1000
  skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
622
1001
  };
623
1002
  if (configured.object !== undefined) {
@@ -725,7 +1104,7 @@ var formsGroup = (project, settings) => [
725
1104
  ];
726
1105
 
727
1106
  // src/preset/groups/imports.ts
728
- import { resolve as resolve3 } from "node:path";
1107
+ import { resolve as resolve4 } from "node:path";
729
1108
  function importsPackageNames(project, kinds) {
730
1109
  return project.units.flatMap((unit) => {
731
1110
  const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
@@ -763,7 +1142,7 @@ var importsGroup = (project, settings) => {
763
1142
  if (!settings.boundaries) {
764
1143
  return overrides;
765
1144
  }
766
- const absolute = (paths) => paths.map((path) => resolve3(project.root, path));
1145
+ const absolute = (paths) => paths.map((path) => resolve4(project.root, path));
767
1146
  overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
768
1147
  "arch/boundaries-imports": [
769
1148
  "error",
@@ -784,7 +1163,7 @@ var importsGroup = (project, settings) => {
784
1163
  };
785
1164
 
786
1165
  // src/preset/groups/layout.ts
787
- import { posix as posix2 } from "node:path";
1166
+ import { posix as posix3 } from "node:path";
788
1167
  var layoutGroup = (project, settings) => {
789
1168
  const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
790
1169
  const folderOperations = presetScopes(operations, "services/*/{actions,queries}/*.ts");
@@ -792,10 +1171,11 @@ var layoutGroup = (project, settings) => {
792
1171
  const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
793
1172
  const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
794
1173
  const frontendEntries = [...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")];
795
- const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix2.join(path, "components")));
1174
+ const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix3.join(path, "components")));
796
1175
  return [
797
1176
  ...layouts.flatMap(({ path, folder, mode }) => {
798
- const folders = folder === "services" && operations.some((source) => posix2.join(source, "services") === path);
1177
+ const folders = folder === "services" && operations.some((source) => posix3.join(source, "services") === path);
1178
+ const registries = folder === "services" ? project.registries : [];
799
1179
  const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
800
1180
  "arch/files-restricted": [
801
1181
  "error",
@@ -803,25 +1183,25 @@ var layoutGroup = (project, settings) => {
803
1183
  message: mode === "flat" ? "This folder uses a flat layout; subfolders are forbidden." : "This folder uses exactly one owning domain directory; loose and deeper files are forbidden."
804
1184
  }
805
1185
  ]
806
- }, folders ? [`${path}/*/{actions,queries}/*.ts`] : []);
1186
+ }, [...folders ? [`${path}/*/{actions,queries}/*.ts`] : [], ...registries]);
807
1187
  if (mode !== "domain") {
808
1188
  return [restricted];
809
1189
  }
810
1190
  const prefix = { singularize: folder === "components" ? "trailing-s" : "none", separators: [".", "-"] };
811
1191
  const naming = folder === "hooks" ? { ...prefix, prefixes: ["use-"] } : prefix;
812
- const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] })];
1192
+ const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] }, registries)];
813
1193
  if (!settings.strict || folder !== "services") {
814
1194
  return domain;
815
1195
  }
816
- const strict = presetOverride([`${path}/*/${presetExtensions}`], {
1196
+ const strict = presetOverride([`${path}/*/**/${presetExtensions}`], {
817
1197
  "arch/naming-file": [
818
1198
  "error",
819
1199
  {
820
- pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*[.](?:${roles.join("|")})[.]ts$`,
1200
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
821
1201
  message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
822
1202
  }
823
1203
  ]
824
- }, ["**/*-{action,query}.*.ts"]);
1204
+ }, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
825
1205
  return [...domain, strict];
826
1206
  }),
827
1207
  presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
@@ -839,7 +1219,8 @@ var layoutGroup = (project, settings) => {
839
1219
  "**/*.rsc.tsx",
840
1220
  ...presetScopes(nextWeb, "services/**/*.action.ts"),
841
1221
  ...conceptFiles,
842
- ...project.colocated
1222
+ ...project.colocated,
1223
+ ...testFiles
843
1224
  ]),
844
1225
  presetOverride(services, {
845
1226
  "arch/backend-service-functions": "error",
@@ -851,7 +1232,7 @@ var layoutGroup = (project, settings) => {
851
1232
  "arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
852
1233
  }, testFiles)),
853
1234
  presetOverride([...presetScopes(app, "services/**/*-{action,query}.*.ts"), ...presetScopes(web, "services/**/*-{action,query}.*.rsc.tsx")], { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
854
- presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, message: operationMessage }] }, testFiles),
1235
+ presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
855
1236
  presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), { "arch/files-domain-constants": "error" }, [...utilities, ...testFiles]),
856
1237
  presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), { "arch/files-domain-constants": ["error", { data: true }] }, [
857
1238
  "**/*.constants.ts",
@@ -878,6 +1259,10 @@ var limitsGroup = (project, settings) => [
878
1259
 
879
1260
  // src/preset/groups/naming.ts
880
1261
  var namingOperationMessage = "Use domain-action.name.ts or domain-query.name.ts.";
1262
+ var namingFrontendPrefix = { ...presetExportPrefix, scope: "functions", services: true, allow: "^\\*$" };
1263
+ function namingOperationPrefix(role) {
1264
+ return { ...presetExportPrefix, roles: [role], scope: "functions", allow: "^\\*$" };
1265
+ }
881
1266
  function namingOperation(role, file) {
882
1267
  return { mode: "template", file, export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+", camelCase: true };
883
1268
  }
@@ -923,7 +1308,7 @@ var namingGroup = (project, settings, owner) => {
923
1308
  add("files", [`**/${presetExtensions}`], presetNamingRules);
924
1309
  add("files", presetScopes(app, "types/**/*.ts"), { "arch/naming-file": ["error", { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$", message: "Use domain.types.ts filenames." }] }, ["**/*.constants.ts"]);
925
1310
  add("exports", presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/naming-exports": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]);
926
- add("exports", frontendEntries, { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", services: true, allow: "^\\*$" }] }, testFiles);
1311
+ add("exports", frontendEntries, { "arch/naming-exports": ["error", namingFrontendPrefix] }, testFiles);
927
1312
  add("exports", presetScopes(web, "services/**/*.constants.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "declarations", allow: "^\\*$" }] }, testFiles);
928
1313
  add("exports", presetScopes(app, "**/*.utils.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", allow: "^\\*$" }] }, ui);
929
1314
  add("files", hooks, {
@@ -942,14 +1327,14 @@ var namingGroup = (project, settings, owner) => {
942
1327
  const files = [...presetScopes(flatApp, `services/**/*-${role}.*.ts`), ...presetScopes(web, `services/**/*-${role}.*.rsc.tsx`)];
943
1328
  add("files", files, {
944
1329
  "arch/naming-file": ["error", { pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`, message: namingOperationMessage }]
945
- });
1330
+ }, testFiles);
946
1331
  add("exports", files, {
947
1332
  "arch/naming-exports": [
948
1333
  "error",
949
- { ...presetExportPrefix, roles: [role], scope: "functions", allow: "^\\*$" },
1334
+ namingOperationPrefix(role),
950
1335
  namingOperation(role, `{domain}-${role}.{name}.ts`)
951
1336
  ]
952
- });
1337
+ }, testFiles);
953
1338
  }
954
1339
  for (const [role, folder, separator] of [["action", "actions", "."], ["query", "queries", "-"]]) {
955
1340
  const files = presetScopes(folderApp, `services/*/${folder}/*.ts`);
@@ -961,13 +1346,13 @@ var namingGroup = (project, settings, owner) => {
961
1346
  message: "Use domain-action.name.ts in actions/ and domain-query-name.ts in queries/."
962
1347
  }
963
1348
  ]
964
- });
1349
+ }, testFiles);
965
1350
  add("exports", files, {
966
1351
  "arch/naming-exports": [
967
1352
  "error",
968
1353
  { mode: "template", file: `{domain}-${role}${separator}{name}.ts`, export: `make{Domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+" }
969
1354
  ]
970
- });
1355
+ }, testFiles);
971
1356
  }
972
1357
  for (const role of ["action", "query"]) {
973
1358
  for (const suffix of ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"]) {
@@ -980,11 +1365,11 @@ var namingGroup = (project, settings, owner) => {
980
1365
  message: "Frontend operations retain a client, server or rsc runtime suffix."
981
1366
  }
982
1367
  ]
983
- });
984
- add("exports", files, { "arch/naming-exports": ["error", namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] });
1368
+ }, testFiles);
1369
+ add("exports", files, { "arch/naming-exports": ["error", namingOperationPrefix(role), namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] }, testFiles);
985
1370
  }
986
1371
  }
987
- add("exports", presetScopes(web, "services/**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}"]);
1372
+ add("exports", presetScopes(web, "services/**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", namingFrontendPrefix, { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}", ...testFiles]);
988
1373
  if (typeof exportsPattern === "string") {
989
1374
  overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
990
1375
  }
@@ -1209,8 +1594,12 @@ function optionsPatternsTest(patterns, value) {
1209
1594
  });
1210
1595
  }
1211
1596
  function optionsGlob(pattern) {
1212
- const source = pattern.replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
1213
- return new RegExp(`^${source}$`);
1597
+ const negated = [];
1598
+ const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
1599
+ negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
1600
+ return `\x02${negated.length - 1}\x03`;
1601
+ }).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
1602
+ return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
1214
1603
  }
1215
1604
  function optionsPatternLabel(pattern) {
1216
1605
  return pattern === undefined ? "" : [pattern].flat().filter((source) => source !== "").join(" | ");
@@ -1260,6 +1649,7 @@ var testsGroup = (project, settings) => {
1260
1649
  }
1261
1650
  return [
1262
1651
  presetOverride(project.testFiles, rules),
1652
+ presetOverride(presetScopes(project.units.map((unit) => unit.source), "**/*.{test,spec}.{ts,tsx,mts,cts}"), { "arch/files-restricted": ["error", { message: "Tests live in a tests/ folder, not next to the code." }] }, ["**/tests/**", "**/test/**"]),
1263
1653
  presetOverride(presetScopes(project.backendRoots, "tests/**/*.ts"), backendRules),
1264
1654
  ...allow.map((entry) => presetOverride(entry.files, { "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis) }))
1265
1655
  ];
@@ -1322,254 +1712,149 @@ var typesGroup = (project, settings) => {
1322
1712
  };
1323
1713
 
1324
1714
  // src/preset/preset-folders.ts
1325
- import { existsSync as existsSync3, readdirSync as readdirSync2, realpathSync, statSync as statSync2 } from "node:fs";
1326
- import { posix as posix4, resolve as resolve5 } from "node:path";
1327
-
1328
- // src/preset/preset-detect.ts
1329
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
1330
- import { posix as posix3, resolve as resolve4 } from "node:path";
1331
-
1332
- // src/preset/preset-recipes.ts
1333
- var presetUi = { react: true, kit: true, tailwind: true };
1334
- var presetRecipes = {
1335
- tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
1336
- next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
1337
- expo: { react: true, expo: true, schemas: true, forms: true },
1338
- backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
1339
- react: { react: true, tailwind: true, schemas: true },
1340
- desktop: { console: true },
1341
- library: { schemas: true },
1342
- ui: presetUi,
1343
- kit: presetUi,
1344
- cli: { console: true }
1345
- };
1346
- var presetTemplates = {
1347
- monorepo: {
1348
- "apps/web": presetRecipes.tanstack,
1349
- "apps/server": presetRecipes.backend,
1350
- "packages/*": presetRecipes.library,
1351
- scripts: presetRecipes.cli
1352
- }
1353
- };
1715
+ import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
1716
+ import { posix as posix6, resolve as resolve7 } from "node:path";
1354
1717
 
1355
- // src/preset/preset-detect.ts
1356
- function presetManifest(root, directory) {
1357
- try {
1358
- return JSON.parse(readFileSync2(resolve4(root, directory, "package.json"), "utf8"));
1359
- } catch {
1360
- return {};
1718
+ // src/preset/preset-stylesheet.ts
1719
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
1720
+ import { posix as posix4, resolve as resolve5 } from "node:path";
1721
+ function presetTailwindEntry(css) {
1722
+ const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
1723
+ return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
1724
+ }
1725
+ function presetStylesheet(root, directory) {
1726
+ const found = [];
1727
+ const queue = [directory];
1728
+ const visited = new Set;
1729
+ while (queue.length > 0) {
1730
+ const folder = queue.shift() ?? directory;
1731
+ const real = realpathSync(resolve5(root, folder));
1732
+ if (visited.has(real)) {
1733
+ continue;
1734
+ }
1735
+ visited.add(real);
1736
+ for (const name of readdirSync2(resolve5(root, folder))) {
1737
+ const path = posix4.join(folder, name);
1738
+ const full = resolve5(root, path);
1739
+ const depth = posix4.relative(directory, path).split("/").length;
1740
+ if (presetIsDirectory(full)) {
1741
+ if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
1742
+ queue.push(path);
1743
+ }
1744
+ } else if (name.endsWith(".css") && presetIsFile(full) && presetTailwindEntry(readFileSync3(full, "utf8"))) {
1745
+ found.push(path);
1746
+ }
1747
+ }
1361
1748
  }
1749
+ const rank = (path) => {
1750
+ const index = presetStylesheetNames.indexOf(posix4.basename(path, ".css"));
1751
+ return index === -1 ? presetStylesheetNames.length : index;
1752
+ };
1753
+ return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
1362
1754
  }
1363
- function presetDirectories(root, base) {
1364
- if (!existsSync2(resolve4(root, base))) {
1365
- return [];
1755
+
1756
+ // src/preset/preset-units.ts
1757
+ import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
1758
+ import { posix as posix5, resolve as resolve6 } from "node:path";
1759
+ var presetFrameworks = ["tanstack", "next", "expo"];
1760
+ var presetKindGroups = [...presetFrameworks, "backend", "console"];
1761
+ function presetUnitSource(root, key, kind, source) {
1762
+ if (source !== undefined) {
1763
+ if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
1764
+ throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
1765
+ }
1766
+ return posix5.join(key, source);
1366
1767
  }
1367
- return readdirSync(resolve4(root, base)).filter((name) => !name.startsWith(".") && statSync(resolve4(root, base, name)).isDirectory()).map((name) => posix3.join(base, name));
1768
+ return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix5.join(key, "src");
1368
1769
  }
1369
- function presetSource(root, directory) {
1370
- return existsSync2(resolve4(root, directory, "src")) ? posix3.join(directory, "src") : directory;
1770
+ function presetSets(folder, name) {
1771
+ return folder[name] !== undefined && folder[name] !== false;
1371
1772
  }
1372
- function presetRecipe(root, directory, app) {
1373
- const manifest = presetManifest(root, directory);
1374
- const dependencies = new Set(Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }));
1375
- const has = (names) => names.some((name) => dependencies.has(name));
1376
- const source = presetSource(root, directory);
1377
- const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
1378
- const services = resolve4(root, source, "services");
1379
- const entries = existsSync2(services) ? readdirSync(services, { withFileTypes: true }) : [];
1380
- const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > entries.filter((entry) => entry.isDirectory()).length;
1381
- if (framework) {
1382
- return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
1383
- }
1384
- if (has(presetDetectDesktop)) {
1385
- return presetRecipes.desktop;
1773
+ function presetKind(key, enabled) {
1774
+ const frameworks = presetFrameworks.filter(enabled);
1775
+ if (frameworks.length > 1) {
1776
+ throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
1386
1777
  }
1387
- if (has(presetDetectBackend) && (app || existsSync2(services))) {
1388
- return presetRecipes.backend;
1778
+ const [framework] = frameworks;
1779
+ if (frameworks.length > 0 && enabled("backend")) {
1780
+ throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
1389
1781
  }
1390
- if (has(["react"]) && existsSync2(resolve4(root, source, "components"))) {
1391
- return app ? presetRecipes.react : presetRecipes.ui;
1782
+ let kind = "library";
1783
+ if (frameworks.length > 0) {
1784
+ kind = "web";
1785
+ } else if (enabled("backend")) {
1786
+ kind = "backend";
1787
+ } else if (enabled("console")) {
1788
+ kind = "console";
1392
1789
  }
1393
- return presetRecipes.library;
1790
+ return { kind, framework };
1394
1791
  }
1395
- function presetMergeObject(base, top, depth) {
1396
- const merged = { ...base };
1397
- for (const [key, value] of Object.entries(top)) {
1398
- const previous = merged[key];
1399
- const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
1400
- if (nested && depth > 0) {
1401
- merged[key] = presetMergeObject(previous, value, depth - 1);
1402
- } else if (value !== true || !(previous instanceof Object)) {
1403
- merged[key] = value;
1404
- }
1792
+ function presetHasManifest(root, key) {
1793
+ if (key === ".") {
1794
+ return false;
1405
1795
  }
1406
- return merged;
1407
- }
1408
- function presetMerge(base, top) {
1409
- if (!(base instanceof Object) || !(top instanceof Object)) {
1410
- return top === true && base instanceof Object ? base : top;
1796
+ if (!key.endsWith("/*")) {
1797
+ return existsSync2(resolve6(root, key, "package.json"));
1411
1798
  }
1412
- return presetMergeObject(base, top, 2);
1413
- }
1414
- function presetSpecifiers(manifest, target) {
1415
- return Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => {
1416
- const prefix = typeof value === "string" && value.endsWith("/*") && key.endsWith("/*") ? posix3.normalize(value.slice(0, -2)) : undefined;
1417
- if (prefix === undefined || target !== prefix && !target.startsWith(`${prefix}/`)) {
1418
- return [];
1419
- }
1420
- return [`${key.slice(0, -2)}${target.slice(prefix.length)}`];
1421
- });
1799
+ const base = resolve6(root, key.slice(0, -2));
1800
+ return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
1422
1801
  }
1423
- function presetDeny(imports) {
1424
- return { deny: { imports } };
1425
- }
1426
- function presetBackendConventions(root, key, manifest) {
1427
- const source = presetSource(root, key);
1428
- const local = posix3.relative(key, source) || ".";
1429
- const at = (path) => local === "." ? path : posix3.join(local, path);
1430
- const exists = (path) => existsSync2(resolve4(root, key, path));
1431
- const registry = presetRegistryFiles.map(at).find(exists);
1432
- const services = presetSpecifiers(manifest, at("services"));
1433
- const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, "Use the service API, not another service’s operations."])));
1434
- const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.ts$/, "")).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
1435
- const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
1436
- const conventions = {
1437
- [at("services")]: presetDeny({
1438
- ...registryImports,
1439
- ...operations,
1440
- ...manifest.name ? entries : {}
1441
- }),
1442
- [at("core")]: presetDeny(operations),
1443
- [at("rpc")]: presetDeny(registryImports),
1444
- [at("routes")]: presetDeny(registryImports),
1445
- [at("cli")]: {
1446
- naming: { files: presetCliFiles },
1447
- ...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
1448
- }
1449
- };
1450
- for (const tests of ["tests", "test"]) {
1451
- if (exists(posix3.join(tests, "kits"))) {
1452
- conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
1453
- conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
1454
- }
1455
- }
1456
- const queue = ["."];
1457
- while (queue.length > 0) {
1458
- const folder = queue.shift() ?? ".";
1459
- for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
1460
- if (posix3.basename(child) === "migrations") {
1461
- conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
1462
- } else if (child.split("/").length < 6 && !presetDetectSkip.includes(posix3.basename(child))) {
1463
- queue.push(child);
1802
+ function presetUnit(root, key, scope) {
1803
+ const { effective } = scope;
1804
+ const wildcard = scope.parent && scope.parent.depth === scope.depth && scope.parent.key.includes("*") ? scope.parent.folder : {};
1805
+ const folder = { ...wildcard, ...scope.folder };
1806
+ const own = presetKindGroups.some((name) => presetSets(folder, name)) || presetHasManifest(root, key);
1807
+ const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
1808
+ const layout = {};
1809
+ const settings = effective.layout;
1810
+ if (settings) {
1811
+ const services = settings.services ?? (kind === "console" ? false : "domain");
1812
+ const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
1813
+ const modes = { services, hooks, components: settings.components ?? false };
1814
+ for (const [name, mode] of Object.entries(modes)) {
1815
+ if (mode === "flat" || mode === "domain") {
1816
+ layout[name] = mode;
1464
1817
  }
1465
1818
  }
1466
1819
  }
1467
- return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
1468
- const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
1469
- return exists(path) && !empty;
1470
- }));
1471
- }
1472
- function presetWebConventions(root, key, servers) {
1473
- const source = posix3.relative(key, presetSource(root, key)) || ".";
1474
- const at = (path) => source === "." ? path : posix3.join(source, path);
1475
- const exists = (path) => existsSync2(resolve4(root, key, path));
1476
- const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
1477
- const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
1478
- const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
1479
- const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
1480
- const conventions = {
1481
- ".": presetDeny({
1482
- ...Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."])),
1483
- ...servers.length > 0 ? { "**/services/*.service": "Import the client or rsc service file, not a server service." } : {}
1484
- }),
1485
- [at("components")]: presetDeny(entries),
1486
- [at("routes")]: presetDeny(entries),
1487
- [at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
1488
- };
1489
- return Object.fromEntries(Object.entries(conventions).filter(([path]) => path === "." || exists(path)));
1820
+ return { key, scope, kind, framework, source: presetUnitSource(root, key, kind, folder.source), layout };
1490
1821
  }
1491
- function presetDetect(root, folders) {
1492
- const manifest = presetManifest(root, ".");
1493
- const workspaces = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
1494
- const directories = workspaces.flatMap((pattern) => pattern.endsWith("/*") ? presetDirectories(root, pattern.slice(0, -2)) : [pattern]).filter((directory) => existsSync2(resolve4(root, directory, "package.json")));
1495
- const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"))]));
1496
- if (directories.length === 0) {
1497
- const recipe = presetRecipe(root, ".", true);
1498
- if (recipe !== presetRecipes.library) {
1499
- detected["."] = recipe;
1500
- }
1501
- }
1502
- if (existsSync2(resolve4(root, "scripts")) && !directories.includes("scripts")) {
1503
- detected.scripts = presetRecipes.cli;
1504
- }
1505
- const keys = Object.keys(folders);
1506
- const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix3.dirname(key) === user.slice(0, -2)));
1507
- const merged = { ".": presetDeny(presetRootImports) };
1508
- for (const [key, value] of Object.entries(detected)) {
1509
- if (!covered(key)) {
1510
- merged[key] = presetMerge(merged[key], value);
1511
- }
1512
- }
1513
- for (const [key, value] of Object.entries(folders)) {
1514
- merged[key] = key in merged ? presetMerge(merged[key], value) : value;
1515
- }
1516
- const units = Object.entries(merged).filter(([key]) => !key.includes("*"));
1517
- const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
1518
- const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
1519
- const add = (key, folder) => {
1520
- merged[key] = key in merged ? presetMerge(folder, merged[key]) : folder;
1521
- };
1522
- for (const [key, value] of units) {
1523
- if (!(value instanceof Object)) {
1524
- continue;
1525
- }
1526
- if (value.backend) {
1527
- for (const [path, folder] of Object.entries(presetBackendConventions(root, key, presetManifest(root, key)))) {
1528
- add(path === "." ? key : posix3.join(key, path), folder);
1529
- }
1530
- } else if (value.tanstack || value.next) {
1531
- for (const [path, folder] of Object.entries(presetWebConventions(root, key, servers))) {
1532
- add(path === "." ? key : posix3.join(key, path), folder);
1533
- }
1534
- } else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
1535
- const own = Object.keys(presetManifest(root, key).imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**"));
1536
- const message = presetPackageImports["../../apps/**"];
1537
- const appImports = internals.filter((specifier) => !own.includes(specifier)).map((specifier) => [specifier, message]);
1538
- add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
1822
+ function presetUnits(root, scopes, ignored) {
1823
+ const [rootScope] = scopes;
1824
+ const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
1825
+ const candidates = [];
1826
+ for (const scope of scopes) {
1827
+ const manifest = presetHasManifest(root, scope.key);
1828
+ const own = presetKindGroups.some((name) => presetSets(scope.folder, name)) || manifest;
1829
+ const top = scope.parent?.key === "." && (rootKind === "library" || own);
1830
+ const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
1831
+ const workspace = scope.parent !== null && scope.parent.key !== "." && manifest;
1832
+ if (top || sibling || workspace) {
1833
+ candidates.push(scope);
1539
1834
  }
1540
1835
  }
1541
- return merged;
1542
- }
1543
- function presetStylesheet(root, directory) {
1544
- const found = [];
1545
- const queue = [directory];
1546
- while (queue.length > 0) {
1547
- const folder = queue.shift() ?? directory;
1548
- for (const child of readdirSync(resolve4(root, folder), { withFileTypes: true })) {
1549
- const path = posix3.join(folder, child.name);
1550
- if (child.isDirectory() && !child.name.startsWith(".") && !presetDetectSkip.includes(child.name) && path.split("/").length < 8) {
1551
- queue.push(path);
1552
- } else if (child.isFile() && child.name.endsWith(".css") && /@import\s+["']tailwindcss|@tailwind\s/.test(readFileSync2(resolve4(root, path), "utf8"))) {
1553
- found.push(path);
1554
- }
1836
+ const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
1837
+ const explicit = new Set(unitScopes.map((scope) => scope.key));
1838
+ const rootSource = presetUnitSource(root, ".", rootKind, rootScope.folder.source);
1839
+ const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
1840
+ const rootOwnsSource = rootSource !== "." && !unitScopes.some((scope) => overlaps(scope.key.replace(/\/\*$/, ""), rootSource));
1841
+ const rootUnit = rootKind !== "library" || rootOwnsSource || unitScopes.every((scope) => presetUnit(root, scope.key, scope).kind === "console");
1842
+ return [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
1843
+ if (!scope.key.endsWith("/*")) {
1844
+ return [presetUnit(root, scope.key, scope)];
1555
1845
  }
1556
- }
1557
- const rank = (path) => {
1558
- const index = presetStylesheetNames.indexOf(posix3.basename(path, ".css"));
1559
- return index === -1 ? presetStylesheetNames.length : index;
1560
- };
1561
- return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
1846
+ const base = scope.key.slice(0, -2);
1847
+ return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix5.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
1848
+ });
1562
1849
  }
1563
1850
 
1564
1851
  // src/preset/preset-folders.ts
1565
- var presetFrameworks = ["tanstack", "next", "expo"];
1566
- var presetKindGroups = [...presetFrameworks, "backend", "console"];
1567
1852
  function presetNormalizeKey(key) {
1568
1853
  const portable = key.replaceAll("\\", "/");
1569
- if (!portable.trim() || posix4.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
1854
+ if (!portable.trim() || posix6.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
1570
1855
  throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
1571
1856
  }
1572
- const normalized = posix4.normalize(portable).replace(/\/$/, "");
1857
+ const normalized = posix6.normalize(portable).replace(/\/$/, "");
1573
1858
  if (normalized === ".." || normalized.startsWith("../")) {
1574
1859
  throw new PresetError(`Folder keys must stay inside the project: ${key}`);
1575
1860
  }
@@ -1607,7 +1892,14 @@ function presetGroupValue(name, value, inherited, severity) {
1607
1892
  if (unknown) {
1608
1893
  throw new PresetError(`Unknown option \`${name}.${unknown}\`.`);
1609
1894
  }
1610
- return { ...base, ...changes, level: severity ?? base.level };
1895
+ const plain = (option) => option instanceof Object && !Array.isArray(option);
1896
+ const merged = Object.fromEntries(Object.entries(changes).map(([key, option]) => {
1897
+ const stored = Object(base)[key];
1898
+ const field = presetShorthandOptions.find(([group, setting]) => group === name && setting === key)?.[2];
1899
+ const previous = typeof stored === "string" && field !== undefined && plain(option) ? { [field]: stored } : stored;
1900
+ return [key, plain(previous) && plain(option) ? { ...Object(previous), ...Object(option) } : option];
1901
+ }));
1902
+ return { ...base, ...merged, level: severity ?? base.level };
1611
1903
  }
1612
1904
  function presetDenyEntries(inherited, changes) {
1613
1905
  return Object.fromEntries(Object.entries({ ...inherited, ...changes }).flatMap(([key, message]) => {
@@ -1653,55 +1945,6 @@ function presetFolder(key, value) {
1653
1945
  }
1654
1946
  return folder;
1655
1947
  }
1656
- function presetUnitSource(root, key, kind, source) {
1657
- if (source !== undefined) {
1658
- if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
1659
- throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
1660
- }
1661
- return posix4.join(key, source);
1662
- }
1663
- return kind === "console" || key === "." && !existsSync3(resolve5(root, "src")) ? key : posix4.join(key, "src");
1664
- }
1665
- function presetSets(folder, name) {
1666
- return folder[name] !== undefined && folder[name] !== false;
1667
- }
1668
- function presetKind(key, enabled) {
1669
- const frameworks = presetFrameworks.filter(enabled);
1670
- if (frameworks.length > 1) {
1671
- throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
1672
- }
1673
- const [framework] = frameworks;
1674
- if (frameworks.length > 0 && enabled("backend")) {
1675
- throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
1676
- }
1677
- let kind = "library";
1678
- if (frameworks.length > 0) {
1679
- kind = "web";
1680
- } else if (enabled("backend")) {
1681
- kind = "backend";
1682
- } else if (enabled("console")) {
1683
- kind = "console";
1684
- }
1685
- return { kind, framework };
1686
- }
1687
- function presetUnit(root, key, scope) {
1688
- const { folder, effective } = scope;
1689
- const own = presetKindGroups.some((name) => presetSets(folder, name));
1690
- const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
1691
- const layout = {};
1692
- const settings = effective.layout;
1693
- if (settings) {
1694
- const services = settings.services ?? (kind === "console" ? false : "domain");
1695
- const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
1696
- const modes = { services, hooks, components: settings.components ?? false };
1697
- for (const [name, mode] of Object.entries(modes)) {
1698
- if (mode === "flat" || mode === "domain") {
1699
- layout[name] = mode;
1700
- }
1701
- }
1702
- }
1703
- return { key, scope, kind, framework, source: presetUnitSource(root, key, kind, scope.folder.source), layout };
1704
- }
1705
1948
  function presetScopesTree(root, folders) {
1706
1949
  const entries = Object.entries(folders).map(([key, value]) => [presetNormalizeKey(key), value]);
1707
1950
  const keys = entries.map(([key]) => key);
@@ -1711,7 +1954,7 @@ function presetScopesTree(root, folders) {
1711
1954
  }
1712
1955
  for (const key of keys) {
1713
1956
  const base = key.endsWith("/*") ? key.slice(0, -2) : key;
1714
- if (!existsSync3(resolve5(root, base))) {
1957
+ if (!existsSync3(resolve7(root, base))) {
1715
1958
  throw new PresetError(`Folder \`${key}\` does not exist.`);
1716
1959
  }
1717
1960
  }
@@ -1743,36 +1986,22 @@ function presetScopesTree(root, folders) {
1743
1986
  return scopes;
1744
1987
  }
1745
1988
  function presetProject(config) {
1746
- const given = resolve5(config.root ?? process.cwd());
1747
- const root = existsSync3(given) ? realpathSync(given) : given;
1748
- const folders = config.detect === false ? config.folders ?? {} : presetDetect(root, config.folders ?? {});
1989
+ const given = resolve7(config.root ?? process.cwd());
1990
+ const root = existsSync3(given) ? realpathSync2(given) : given;
1991
+ const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
1992
+ const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
1993
+ if (twice) {
1994
+ throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
1995
+ }
1996
+ const folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
1749
1997
  const scopes = presetScopesTree(root, folders);
1750
1998
  const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
1751
1999
  const live = scopes.filter((scope) => !ignored.has(scope.key));
1752
- const [rootScope] = scopes;
1753
- const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
1754
2000
  const dead = live.find((scope) => scope.parent && ignored.has(scope.parent.key) && scope.parent.depth < scope.depth);
1755
2001
  if (dead) {
1756
2002
  throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
1757
2003
  }
1758
- const candidates = [];
1759
- for (const scope of scopes) {
1760
- const top = scope.parent?.key === "." && (rootKind === "library" || presetKindGroups.some((name) => presetSets(scope.folder, name)));
1761
- const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
1762
- if (top || sibling) {
1763
- candidates.push(scope);
1764
- }
1765
- }
1766
- const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
1767
- const explicit = new Set(unitScopes.map((scope) => scope.key));
1768
- const rootUnit = rootKind !== "library" || unitScopes.length === 0;
1769
- const units = [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
1770
- if (!scope.key.endsWith("/*")) {
1771
- return [presetUnit(root, scope.key, scope)];
1772
- }
1773
- const base = scope.key.slice(0, -2);
1774
- return readdirSync2(resolve5(root, base)).filter((name) => !name.startsWith(".") && statSync2(resolve5(root, base, name)).isDirectory()).map((name) => posix4.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
1775
- });
2004
+ const units = presetUnits(root, scopes, ignored);
1776
2005
  const unitKeys = new Set(units.map((unit) => unit.scope.key));
1777
2006
  const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
1778
2007
  if (misplaced) {
@@ -1804,9 +2033,9 @@ function presetProject(config) {
1804
2033
  const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
1805
2034
  const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
1806
2035
  const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
1807
- const rpcClients = client === undefined ? webUnits.map((unit) => posix4.join(unit.source, "services", unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
2036
+ const rpcClients = client === undefined ? webUnits.map((unit) => posix6.join(unit.source, "services", unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
1808
2037
  const kitUnits = units.filter((unit) => unit.scope.effective.kit);
1809
- const uiRoots = [...new Set([...web.map((path) => posix4.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
2038
+ const uiRoots = [...new Set([...web.map((path) => posix6.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
1810
2039
  const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
1811
2040
  const stylesheet = (unit) => {
1812
2041
  const tailwind = unit.scope.effective.tailwind;
@@ -1814,7 +2043,7 @@ function presetProject(config) {
1814
2043
  return;
1815
2044
  }
1816
2045
  const css = tailwind.css ?? presetStylesheet(root, unit.key);
1817
- return css === undefined ? undefined : resolve5(root, css);
2046
+ return css === undefined ? undefined : resolve7(root, css);
1818
2047
  };
1819
2048
  const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
1820
2049
  const css = stylesheet(unit);
@@ -1848,7 +2077,7 @@ function presetProject(config) {
1848
2077
  return [key];
1849
2078
  }
1850
2079
  const base = key.slice(0, -2);
1851
- return readdirSync2(resolve5(root, base)).filter((name) => statSync2(resolve5(root, base, name)).isDirectory()).map((name) => posix4.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
2080
+ return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix6.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
1852
2081
  });
1853
2082
  return {
1854
2083
  root,
@@ -1857,11 +2086,11 @@ function presetProject(config) {
1857
2086
  suffixes,
1858
2087
  colocated: listOf(effective.map((groups) => groups.types?.colocated)),
1859
2088
  orpc: {
1860
- public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix4.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve5(root, path))).map((path) => `${path}/**`),
2089
+ public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix6.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
1861
2090
  composers: listOf(orpcScopes.map((settings) => settings.composers)),
1862
2091
  client: client ?? ""
1863
2092
  },
1864
- layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: posix4.join(unit.source, folder), folder, mode }))),
2093
+ layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: posix6.join(unit.source, folder), folder, mode }))),
1865
2094
  rpcClients,
1866
2095
  operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
1867
2096
  packages: libraryUnits.map((unit) => unit.key),
@@ -1898,7 +2127,9 @@ function presetProject(config) {
1898
2127
  publicEntrypoints,
1899
2128
  baseImports: presetBaseImports,
1900
2129
  css: { ...webCss, ...kitCss },
1901
- ignores: skipped.map((key) => `${key}/**`)
2130
+ registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry) ?? []),
2131
+ ignores: skipped.map((key) => `${key}/**`),
2132
+ outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
1902
2133
  };
1903
2134
  }
1904
2135
 
@@ -2048,10 +2279,7 @@ function preset(config) {
2048
2279
  plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
2049
2280
  ignorePatterns: [
2050
2281
  "**/node_modules/**",
2051
- "**/dist/**",
2052
- "**/build/**",
2053
- "**/coverage/**",
2054
- "**/.output/**",
2282
+ ...project.outputs,
2055
2283
  ...project.nextWeb.length > 0 ? ["**/.next/**", "**/next-env.d.ts"] : [],
2056
2284
  ...project.expoWeb.length > 0 ? ["**/.expo/**", "**/expo-env.d.ts"] : [],
2057
2285
  ...project.generated,