oxlint-plugin-arch 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
1
  import type { OxlintConfig } from 'oxlint';
2
2
  import type { PresetOptions } from '../types/preset.types.ts';
3
+ /** The React modules preset with TanStack Start as the default web framework. */
3
4
  export default function tanstackStartReactModulesPreset(options?: PresetOptions): OxlintConfig;
@@ -418,6 +418,34 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
422
+ var presetTestMockingApis = {
423
+ modules: presetModuleMockingApis,
424
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
425
+ };
426
+ var presetTestMockingAllow = {
427
+ modules: [],
428
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
429
+ };
430
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
431
+ var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
432
+ var presetNextSegmentConfig = [
433
+ "generateStaticParams",
434
+ "dynamic",
435
+ "dynamicParams",
436
+ "revalidate",
437
+ "fetchCache",
438
+ "runtime",
439
+ "preferredRegion",
440
+ "maxDuration"
441
+ ];
442
+ var presetNextSegmentExports = [
443
+ "generateMetadata",
444
+ "metadata",
445
+ "generateViewport",
446
+ "viewport",
447
+ ...presetNextSegmentConfig
448
+ ];
421
449
  var presetExportPrefix = {
422
450
  stem: "before-first-dot",
423
451
  trailingRoles: ["constants", "utils", "handler", "service", "types"],
@@ -436,7 +464,7 @@ var presetGeneratedFiles = [
436
464
 
437
465
  // src/utils/helpers/preset.ts
438
466
  function presetScopes(roots, suffix = `**/${presetExtensions}`) {
439
- return roots.map((root) => `${root}/${suffix}`);
467
+ return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
440
468
  }
441
469
  function presetOverride(files, rules, excludeFiles = []) {
442
470
  return {
@@ -445,6 +473,18 @@ function presetOverride(files, rules, excludeFiles = []) {
445
473
  rules
446
474
  };
447
475
  }
476
+ function presetRuleSeverities(rules, severities) {
477
+ const result = structuredClone(rules);
478
+ for (const [name, level] of Object.entries(severities)) {
479
+ const value = result[name];
480
+ const current = Array.isArray(value) ? value[0] : value;
481
+ if (value === undefined || current === "off" || current === "allow" || current === 0) {
482
+ continue;
483
+ }
484
+ result[name] = Array.isArray(value) && level !== "off" ? [level, ...value.slice(1)] : level;
485
+ }
486
+ return result;
487
+ }
448
488
  function presetPackageName(root, directory) {
449
489
  try {
450
490
  const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
@@ -469,7 +509,14 @@ function presetSeverity(rules, level) {
469
509
  function presetSourceConfig(context) {
470
510
  const { options, web, scripts, app, appFiles, backendFiles, ui, safetyFiles, publicEntrypoints, baseImports } = context;
471
511
  const policies = {};
472
- policies.formatting = [presetOverride([`**/${presetExtensions}`], presetFormattingRules)];
512
+ const { semi = "always", quotes = "single" } = options.formatting ?? {};
513
+ policies.formatting = [
514
+ presetOverride([`**/${presetExtensions}`], {
515
+ ...presetFormattingRules,
516
+ "eslint-js/semi": ["error", semi],
517
+ "eslint-js/quotes": ["error", quotes, { avoidEscape: true }]
518
+ })
519
+ ];
473
520
  policies.statementLayout = [
474
521
  presetOverride([`**/${presetExtensions}`], {
475
522
  "arch/padding-between-statements": ["error", { multilineVariables: true }],
@@ -555,12 +602,18 @@ function presetTypesConfig(context) {
555
602
  safetyFiles,
556
603
  components,
557
604
  hooks,
558
- publicEntrypoints
605
+ publicEntrypoints,
606
+ tanstackWeb,
607
+ nextWeb
559
608
  } = context;
609
+ const schemaOwners = backend.length ? backend : nextWeb;
610
+ const runtimeFreeTypes = backend.length ? web : tanstackWeb;
611
+ const colocatedTypes = options.modules?.colocatedTypeFiles ?? [];
560
612
  const policies = {};
561
613
  policies.typePlacement = [
562
614
  presetOverride(appFiles, { "arch/no-type-declarations": "error" }, [
563
615
  ...types,
616
+ ...colocatedTypes,
564
617
  ...components,
565
618
  ...hooks,
566
619
  ...services,
@@ -583,16 +636,18 @@ function presetTypesConfig(context) {
583
636
  ...options.react?.componentProps === false ? [] : [presetOverride(components, { "arch/component-props": ["error", { filePrefix: true }] }, [...ui, ...testFiles])],
584
637
  presetOverride(presetScopes(app, "**/*.types.ts"), {
585
638
  "arch/no-restricted-files": ["error", { message: "Domain types belong in src/types." }]
586
- }, [...types, ...ui]),
639
+ }, [...types, ...colocatedTypes, ...ui]),
587
640
  presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
588
641
  "arch/no-type-declarations": "error"
589
642
  }, [...ui, ...testFiles]),
590
- presetOverride([...presetScopes(backend, "types/**/*.types.ts"), ...presetScopes(app, "**/*.constants.ts")], {
643
+ presetOverride([...presetScopes(schemaOwners, "types/**/*.types.ts"), ...colocatedTypes, ...presetScopes(app, "**/*.constants.ts")], {
591
644
  "arch/no-top-level-functions": ["error", { banReExports: false }]
592
645
  }),
593
- presetOverride(presetScopes(web, "types/**/*.types.ts"), {
594
- "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
595
- })
646
+ ...runtimeFreeTypes.length ? [
647
+ presetOverride(presetScopes(runtimeFreeTypes, "types/**/*.types.ts"), {
648
+ "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
649
+ })
650
+ ] : []
596
651
  ];
597
652
  policies.typeSafety = [
598
653
  presetOverride(safetyFiles, {
@@ -629,7 +684,7 @@ function presetTypesConfig(context) {
629
684
  policies.schemas = [
630
685
  presetOverride(appFiles, {
631
686
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
632
- }, [...presetScopes(backend, "types/**"), ...ui]),
687
+ }, [...presetScopes(schemaOwners, "types/**"), ...colocatedTypes, ...ui]),
633
688
  presetOverride(webFiles, {
634
689
  "arch/no-rederive-schema": [
635
690
  "error",
@@ -668,8 +723,9 @@ function presetTypesConfig(context) {
668
723
  }
669
724
 
670
725
  // src/configs/modules.ts
726
+ import { posix } from "node:path";
671
727
  function presetModulesConfig(context) {
672
- const { web, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
728
+ const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
673
729
  const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
674
730
  const migrations = options.modules?.migrationFiles ?? [];
675
731
  const frontendEntries = [
@@ -703,7 +759,7 @@ function presetModulesConfig(context) {
703
759
  })
704
760
  ] : []
705
761
  ]),
706
- presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), `components/*/*/**/${presetExtensions}`), {
762
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), `components/*/*/**/${presetExtensions}`), {
707
763
  "arch/no-restricted-files": ["error", { message: "Components allow one group folder." }]
708
764
  }, ui),
709
765
  presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], {
@@ -719,7 +775,13 @@ function presetModulesConfig(context) {
719
775
  message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
720
776
  }
721
777
  ]
722
- }, ["**/*.{client,server,rsc,utils,constants}.ts", "**/*.rsc.tsx", ...conceptFiles])
778
+ }, [
779
+ "**/*.{client,server,rsc,utils,constants}.ts",
780
+ "**/*.rsc.tsx",
781
+ ...presetScopes(nextWeb, "services/**/*.action.ts"),
782
+ ...conceptFiles,
783
+ ...options.modules?.colocatedTypeFiles ?? []
784
+ ])
723
785
  ];
724
786
  policies.serviceStructure = [
725
787
  presetOverride(services, {
@@ -837,9 +899,9 @@ function presetModulesConfig(context) {
837
899
  "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
838
900
  }),
839
901
  presetOverride(components, {
840
- "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true }]
902
+ "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
841
903
  }, [...ui, ...testFiles]),
842
- presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), "components/**/*.tsx"), {
904
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), "components/**/*.tsx"), {
843
905
  "arch/folder-prefix": ["error", { singularize: "trailing-s", separators: ["-"] }]
844
906
  }, [...ui, ...testFiles]),
845
907
  ...["action", "query"].map((role) => presetOverride([
@@ -897,9 +959,15 @@ function presetModulesConfig(context) {
897
959
  return policies;
898
960
  }
899
961
 
962
+ // src/utils/helpers/mocking.ts
963
+ function mockingApiPatternsAssert(patterns) {
964
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
965
+ throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
966
+ }
967
+
900
968
  // src/configs/tests.ts
901
969
  function presetTestsConfig(context) {
902
- const { backend, testFiles, baseImports, options } = context;
970
+ const { backendRoots, testFiles, baseImports, options } = context;
903
971
  const profile = options.tests?.profile ?? "fixtures";
904
972
  if (profile !== "standard" && profile !== "fixtures")
905
973
  throw new Error("Unknown test profile.");
@@ -909,11 +977,30 @@ function presetTestsConfig(context) {
909
977
  throw new Error("Additional test functions must be literal identifiers or dotted names.");
910
978
  if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
911
979
  throw new Error("Assertion functions must be rooted dotted names or patterns.");
980
+ const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
981
+ const mockingPreset = mocking.preset ?? "modules";
982
+ if (mockingPreset !== "modules" && mockingPreset !== "strict")
983
+ throw new Error("Unknown test mocking preset.");
984
+ const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
985
+ if (typeof entry !== "string")
986
+ return entry;
987
+ return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
988
+ });
989
+ const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
990
+ mockingApiPatternsAssert([
991
+ ...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
992
+ ...mockingAllow.flatMap((entry) => entry.apis)
993
+ ]);
994
+ const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
995
+ const mockingRule = (allow) => [
996
+ "error",
997
+ { deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
998
+ ];
912
999
  const policies = {};
913
1000
  policies.tests = [
914
1001
  presetOverride(testFiles, {
915
1002
  ...presetTestRules,
916
- "dillon-anti-slop/no-module-mocking": "error",
1003
+ "dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
917
1004
  "modules/test-modifiers": [
918
1005
  "error",
919
1006
  { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
@@ -930,7 +1017,7 @@ function presetTestsConfig(context) {
930
1017
  { callees: ["describe", ...testFunctions], forbid: "^should\\b", flags: "i" }
931
1018
  ]
932
1019
  }),
933
- presetOverride(presetScopes(backend.map((path) => path.replace(/\/src$/, "")), "tests/**/*.ts"), {
1020
+ presetOverride(presetScopes(backendRoots, "tests/**/*.ts"), {
934
1021
  "modules/concurrent-db": "error",
935
1022
  ...profile === "fixtures" ? {
936
1023
  "no-restricted-imports": [
@@ -946,14 +1033,17 @@ function presetTestsConfig(context) {
946
1033
  }
947
1034
  ]
948
1035
  } : {}
949
- })
1036
+ }),
1037
+ ...mockingAllow.map((entry) => presetOverride(entry.files, {
1038
+ "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
1039
+ }))
950
1040
  ];
951
1041
  return policies;
952
1042
  }
953
1043
 
954
1044
  // src/configs/react.ts
955
1045
  function presetReactConfig(context) {
956
- const { options, web, webFiles, ui, testFiles, components, css } = context;
1046
+ const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
957
1047
  const policies = {};
958
1048
  if (web.length) {
959
1049
  policies.react = [
@@ -975,9 +1065,10 @@ function presetReactConfig(context) {
975
1065
  if (web.length) {
976
1066
  policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
977
1067
  }
978
- if (web.length) {
979
- policies.routes = [
980
- presetOverride(presetScopes(web, "routes/**/*.{ts,tsx}"), {
1068
+ const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
1069
+ const routes = [
1070
+ ...tanstackWeb.length ? [
1071
+ presetOverride(presetScopes(tanstackWeb, "routes/**/*.{ts,tsx}"), {
981
1072
  "arch/route-surface": [
982
1073
  "error",
983
1074
  {
@@ -987,7 +1078,7 @@ function presetReactConfig(context) {
987
1078
  }
988
1079
  ]
989
1080
  }, testFiles),
990
- presetOverride(presetScopes(web, "routes/handlers/**/*.ts"), {
1081
+ presetOverride(presetScopes(tanstackWeb, "routes/handlers/**/*.ts"), {
991
1082
  "arch/no-file-level-helpers": [
992
1083
  "error",
993
1084
  {
@@ -996,7 +1087,43 @@ function presetReactConfig(context) {
996
1087
  }
997
1088
  ]
998
1089
  })
999
- ];
1090
+ ] : [],
1091
+ ...nextWeb.length ? [
1092
+ presetOverride(presetScopes(nextWeb, "app/**/{page,template,default,not-found,forbidden,unauthorized,loading}.tsx"), {
1093
+ "arch/route-surface": [
1094
+ "error",
1095
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: true, ...nextPage }
1096
+ ]
1097
+ }, [...testFiles, ...presetScopes(nextWeb, "app/not-found.tsx")]),
1098
+ presetOverride([...presetScopes(nextWeb, "app/**/{layout,global-not-found}.tsx"), ...presetScopes(nextWeb, "app/not-found.tsx")], {
1099
+ "arch/route-surface": [
1100
+ "error",
1101
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: false, ...nextPage }
1102
+ ]
1103
+ }, testFiles),
1104
+ presetOverride(presetScopes(nextWeb, "app/**/route.ts"), {
1105
+ "arch/route-surface": [
1106
+ "error",
1107
+ {
1108
+ exportName: "GET",
1109
+ allowedExports: [...presetNextHandlerMethods.slice(1), ...presetNextSegmentConfig],
1110
+ bannedHooks: [],
1111
+ banIntrinsicJsx: false
1112
+ }
1113
+ ],
1114
+ "arch/no-file-level-helpers": [
1115
+ "error",
1116
+ {
1117
+ detectComponents: false,
1118
+ allowPattern: `^(${[...presetNextHandlerMethods, ...presetNextSegmentConfig].join("|")})$`,
1119
+ message: "Nest this helper in its owning route handler, or move genuine domain logic to a service. Do not create a component, hook or public helper to bypass this rule."
1120
+ }
1121
+ ]
1122
+ })
1123
+ ] : []
1124
+ ];
1125
+ if (routes.length) {
1126
+ policies.routes = routes;
1000
1127
  }
1001
1128
  if (web.length) {
1002
1129
  policies.forms = [
@@ -1049,23 +1176,51 @@ function presetReactConfig(context) {
1049
1176
  `${group}-[env(safe-area-inset-${side})]`,
1050
1177
  ...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
1051
1178
  ]));
1179
+ const illustrationPalette = [
1180
+ "text-color",
1181
+ "bg-color",
1182
+ "gradient-from",
1183
+ "gradient-via",
1184
+ "gradient-to",
1185
+ "border-color",
1186
+ "divide-color",
1187
+ "outline-color",
1188
+ "ring-color",
1189
+ "ring-offset-color",
1190
+ "shadow",
1191
+ "shadow-color",
1192
+ "inset-shadow",
1193
+ "inset-shadow-color",
1194
+ "drop-shadow",
1195
+ "drop-shadow-color",
1196
+ "fill",
1197
+ "stroke",
1198
+ "bg-image",
1199
+ "bg-size",
1200
+ "bg-position",
1201
+ "mask-image",
1202
+ "blur",
1203
+ "saturate"
1204
+ ];
1205
+ const restyle = (base) => ({
1206
+ allow: base,
1207
+ contracts: [
1208
+ { pattern: "^(CardContent|PopoverContent)$", allow: [...base, ...padding] },
1209
+ { pattern: "^TabsContent$", allow: [...base, ...gap, ...padding] },
1210
+ { pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...base, ...gap] }
1211
+ ]
1212
+ });
1052
1213
  policies.shadcn = [
1053
1214
  presetOverride(webFiles, {
1054
- "shadcn/no-restyle": [
1055
- "error",
1056
- {
1057
- allow,
1058
- contracts: [
1059
- { pattern: "^(CardContent|PopoverContent)$", allow: [...allow, ...padding] },
1060
- { pattern: "^TabsContent$", allow: [...allow, ...gap, ...padding] },
1061
- { pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...allow, ...gap] }
1062
- ]
1063
- }
1064
- ],
1215
+ "shadcn/no-restyle": ["error", restyle(allow)],
1065
1216
  "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
1066
1217
  "shadcn/require-static-classes": "error",
1067
1218
  "modules/dynamic-classes": "error"
1068
1219
  }, ui),
1220
+ presetOverride(presetScopes(web, "components/**/*.illustration.tsx"), {
1221
+ "shadcn/no-restyle": ["error", restyle([...allow, ...illustrationPalette])],
1222
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea, ...illustrationPalette] }]
1223
+ }, ui),
1069
1224
  presetOverride(webFiles, {
1070
1225
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1071
1226
  }),
@@ -1080,7 +1235,16 @@ function presetReactConfig(context) {
1080
1235
  // src/configs/boundaries.ts
1081
1236
  import { resolve as resolve2 } from "node:path";
1082
1237
  function presetBoundariesConfig(context) {
1083
- const { options, root, architecture, packages, web, backend, appFiles, publicEntrypoints, baseImports } = context;
1238
+ const { options, root, architecture, packages, web, tanstackWeb, nextWeb, backend, appFiles, publicEntrypoints, baseImports } = context;
1239
+ const runtimeBoundary = {
1240
+ web: web.map((path) => resolve2(root, path)),
1241
+ backend: backend.map((path) => resolve2(root, path)),
1242
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve2(root, path)])),
1243
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1244
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1245
+ return name ? [name] : [];
1246
+ })
1247
+ };
1084
1248
  const policies = {};
1085
1249
  policies.boundaries = [
1086
1250
  presetOverride([...appFiles, ...presetScopes(packages, "**/" + presetExtensions)], {
@@ -1108,20 +1272,11 @@ function presetBoundariesConfig(context) {
1108
1272
  }
1109
1273
  ]
1110
1274
  }),
1111
- presetOverride([...presetScopes(web, "services/**/*.{ts,tsx}"), ...presetScopes(web, "lib/**/*.{ts,tsx}")], {
1275
+ presetOverride([...presetScopes(tanstackWeb, "services/**/*.{ts,tsx}"), ...presetScopes(tanstackWeb, "lib/**/*.{ts,tsx}")], {
1112
1276
  "modules/tanstack-runtime": [
1113
1277
  "error",
1114
1278
  {
1115
- web: web.map((path) => resolve2(root, path)),
1116
- backend: backend.map((path) => resolve2(root, path)),
1117
- aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [
1118
- alias.replace(/\/$/, ""),
1119
- resolve2(root, path)
1120
- ])),
1121
- backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1122
- const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1123
- return name ? [name] : [];
1124
- }),
1279
+ ...runtimeBoundary,
1125
1280
  rpcClients: ((options.orpc?.clientOwnerFile) ? [options.orpc?.clientOwnerFile] : context.rpcClients).map((path) => resolve2(root, path)),
1126
1281
  serverImports: options.tanstackStart?.additionalServerOnlyImports ?? [],
1127
1282
  clientImports: options.tanstackStart?.additionalClientOnlyImports ?? [],
@@ -1129,6 +1284,17 @@ function presetBoundariesConfig(context) {
1129
1284
  }
1130
1285
  ]
1131
1286
  }),
1287
+ presetOverride([...presetScopes(nextWeb, "services/**/*.{ts,tsx}"), ...presetScopes(nextWeb, "lib/**/*.{ts,tsx}")], {
1288
+ "modules/next-runtime": [
1289
+ "error",
1290
+ {
1291
+ ...runtimeBoundary,
1292
+ serverImports: options.next?.additionalServerOnlyImports ?? [],
1293
+ clientImports: options.next?.additionalClientOnlyImports ?? [],
1294
+ allowComputedImportsIn: options.next?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
1295
+ }
1296
+ ]
1297
+ }),
1132
1298
  presetOverride(presetScopes(backend, "rpc/**/*.ts"), {
1133
1299
  "modules/rpc-database": "error",
1134
1300
  "no-restricted-imports": ["error", { paths: ["kysely"], patterns: baseImports }]
@@ -1191,7 +1357,7 @@ function presetPluginsConfig(context) {
1191
1357
  }
1192
1358
 
1193
1359
  // src/services/preset-context.ts
1194
- import { resolve as resolve3, posix } from "node:path";
1360
+ import { resolve as resolve3, posix as posix2 } from "node:path";
1195
1361
  function presetResolveContext(options) {
1196
1362
  const generated = presetGeneratedFiles;
1197
1363
  const root = resolve3(options.root ?? process.cwd());
@@ -1205,13 +1371,24 @@ function presetResolveContext(options) {
1205
1371
  const entry = typeof configuredRoot === "string" ? { role: configuredRoot } : configuredRoot;
1206
1372
  if (!entry || typeof entry !== "object")
1207
1373
  throw new Error("Architecture entries must be roles or role/layout objects.");
1208
- if (Object.keys(entry).some((key) => !["role", "layout"].includes(key)))
1374
+ if (Object.keys(entry).some((key) => !["role", "layout", "framework", "sourceDir"].includes(key)))
1209
1375
  throw new Error("Unknown architecture entry option.");
1210
1376
  const role = entry.role;
1377
+ if ("framework" in entry && entry.framework !== undefined && role !== "web")
1378
+ throw new Error("Only web architecture roots accept a framework.");
1379
+ const framework = role === "web" ? entry.framework ?? options.framework ?? "tanstack-start" : undefined;
1380
+ if (framework !== undefined && framework !== "tanstack-start" && framework !== "next")
1381
+ throw new Error(`Unknown framework: ${framework}`);
1382
+ const sourced = role === "web" || role === "server" || role === "runner";
1383
+ if ("sourceDir" in entry && entry.sourceDir !== undefined && !sourced)
1384
+ throw new Error("Only web, server and runner architecture roots accept a sourceDir.");
1385
+ const sourceDir = entry.sourceDir ?? "src";
1386
+ if (typeof sourceDir !== "string" || sourceDir !== "." && sourceDir.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
1387
+ throw new Error("Source folders must be `.` or literal root-relative paths without escapes, globs or empty segments.");
1211
1388
  const portable = path.replaceAll("\\", "/");
1212
- if (!portable.trim() || posix.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1389
+ if (!portable.trim() || posix2.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1213
1390
  throw new Error("Architecture roots must be nonempty project-relative paths.");
1214
- const normalized = posix.normalize(portable).replace(/\/$/, "");
1391
+ const normalized = posix2.normalize(portable).replace(/\/$/, "");
1215
1392
  if (normalized === ".." || normalized.startsWith("../"))
1216
1393
  throw new Error("Architecture roots must stay inside the project.");
1217
1394
  if (!["web", "server", "runner", "scripts", "packages"].includes(role))
@@ -1239,7 +1416,15 @@ function presetResolveContext(options) {
1239
1416
  throw new Error("Layout folders must not overlap.");
1240
1417
  }
1241
1418
  }
1242
- return [normalized, { role, layout }];
1419
+ return [
1420
+ normalized,
1421
+ {
1422
+ role,
1423
+ layout,
1424
+ ...framework ? { framework } : {},
1425
+ ...entry.sourceDir === undefined ? {} : { sourceDir }
1426
+ }
1427
+ ];
1243
1428
  });
1244
1429
  for (const [index, [path]] of entries.entries()) {
1245
1430
  const overlaps = entries.slice(0, index).some(([other]) => other === path || other === "." || path === "." || other.startsWith(path + "/") || path.startsWith(other + "/"));
@@ -1265,17 +1450,24 @@ function presetResolveContext(options) {
1265
1450
  "spec"
1266
1451
  ].includes(role)))
1267
1452
  throw new Error("File roles must not replace built-in roles.");
1268
- const web = entries.filter(([, { role }]) => role === "web").map(([path]) => posix.join(path, "src"));
1269
- const backendRoots = entries.filter(([, { role }]) => role === "server" || role === "runner").map(([path]) => path);
1270
- const backend = backendRoots.map((path) => posix.join(path, "src"));
1453
+ if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
1454
+ throw new Error(`Unknown framework: ${options.framework}`);
1455
+ const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
1456
+ const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
1457
+ const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
1458
+ const tanstackWeb = webFor("tanstack-start");
1459
+ const nextWeb = webFor("next");
1460
+ const backendEntries = entries.filter(([, { role }]) => role === "server" || role === "runner");
1461
+ const backendRoots = backendEntries.map(([path]) => path);
1462
+ const backend = backendEntries.map(([path, entry]) => source(path, entry));
1271
1463
  const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1272
1464
  const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1273
1465
  const folders = entries.flatMap(([path, entry]) => Object.entries(entry.layout).map(([folder, mode]) => ({
1274
- path: posix.join(path, ["web", "server", "runner"].includes(entry.role) ? "src" : "", folder),
1466
+ path: posix2.join(["web", "server", "runner"].includes(entry.role) ? source(path, entry) : path, folder),
1275
1467
  folder,
1276
1468
  mode
1277
1469
  })));
1278
- const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => posix.join(path, "src/services", entry.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts"));
1470
+ const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => posix2.join(source(path, entry), "services", entry.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts"));
1279
1471
  const app = [...web, ...backend];
1280
1472
  const appFiles = presetScopes(app);
1281
1473
  const webFiles = presetScopes(web);
@@ -1301,13 +1493,17 @@ function presetResolveContext(options) {
1301
1493
  ];
1302
1494
  const tailwind = options.tailwind;
1303
1495
  const entryPoints = Object.fromEntries(Object.entries(tailwind === false ? {} : tailwind?.cssEntryPointsByRoot ?? {}).map(([path, entry]) => [
1304
- posix.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1496
+ posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1305
1497
  entry
1306
1498
  ]));
1307
- const css = tailwind === false ? {} : Object.fromEntries(web.map((path) => [
1308
- path,
1309
- resolve3(root, entryPoints[posix.dirname(path)] ?? tailwind?.cssEntryPoint ?? `${path}/application/styles.css`)
1310
- ]));
1499
+ const css = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
1500
+ const sourcePath = source(path, entry);
1501
+ const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1502
+ return [
1503
+ sourcePath,
1504
+ resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1505
+ ];
1506
+ }));
1311
1507
  return {
1312
1508
  options,
1313
1509
  root,
@@ -1317,7 +1513,10 @@ function presetResolveContext(options) {
1317
1513
  packages,
1318
1514
  scripts,
1319
1515
  web,
1516
+ tanstackWeb,
1517
+ nextWeb,
1320
1518
  backend,
1519
+ backendRoots,
1321
1520
  app,
1322
1521
  appFiles,
1323
1522
  webFiles,
@@ -1341,6 +1540,8 @@ function presetResolveContext(options) {
1341
1540
  // src/services/preset-compose.ts
1342
1541
  function presetCompose(context, policies, plugins) {
1343
1542
  const { options, level, safetyFiles, ui, generated } = context;
1543
+ const severities = options.ruleSeverities ?? {};
1544
+ const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1344
1545
  const overrideList = [];
1345
1546
  for (const name of presetPolicyOrder) {
1346
1547
  const setting = options.policies?.[name];
@@ -1352,14 +1553,16 @@ function presetCompose(context, policies, plugins) {
1352
1553
  for (const entry of entries) {
1353
1554
  overrideList.push({
1354
1555
  ...entry,
1355
- rules: entry.rules ? presetSeverity(entry.rules, level) : undefined
1556
+ rules: entry.rules ? levels(entry.rules) : undefined
1356
1557
  });
1357
1558
  }
1358
1559
  }
1359
- overrideList.push(presetOverride(safetyFiles, {
1360
- "max-lines": [level, { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1361
- complexity: [level, { max: options.limits?.maxFunctionComplexity ?? 32 }]
1362
- }, ui));
1560
+ overrideList.push(presetOverride(safetyFiles, levels({
1561
+ "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1562
+ complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1563
+ }), ui));
1564
+ const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1565
+ const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1363
1566
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
1364
1567
  overrideList.push({ files, rules: { [rule]: "off" } });
1365
1568
  }
@@ -1373,17 +1576,18 @@ function presetCompose(context, policies, plugins) {
1373
1576
  "**/build/**",
1374
1577
  "**/coverage/**",
1375
1578
  "**/.output/**",
1579
+ ...context.nextWeb.length ? ["**/.next/**", "**/next-env.d.ts"] : [],
1376
1580
  ...generated,
1377
1581
  ...options.ignorePatterns ?? []
1378
1582
  ],
1379
1583
  ...plugins,
1380
- rules: presetSeverity(presetCommonRules, level),
1584
+ rules: { ...levels(presetCommonRules), ...unnamedLevels },
1381
1585
  overrides: overrideList.filter((entry) => entry.files.length > 0)
1382
1586
  };
1383
1587
  }
1384
1588
 
1385
- // src/presets/tanstack-start-react-modules-preset.ts
1386
- function tanstackStartReactModulesPreset(options = {}) {
1589
+ // src/presets/react-modules-preset.ts
1590
+ function reactModulesPreset(options = {}) {
1387
1591
  const context = presetResolveContext(options);
1388
1592
  return presetCompose(context, {
1389
1593
  ...presetSourceConfig(context),
@@ -1394,6 +1598,11 @@ function tanstackStartReactModulesPreset(options = {}) {
1394
1598
  ...presetBoundariesConfig(context)
1395
1599
  }, presetPluginsConfig(context));
1396
1600
  }
1601
+
1602
+ // src/presets/tanstack-start-react-modules-preset.ts
1603
+ function tanstackStartReactModulesPreset(options = {}) {
1604
+ return reactModulesPreset({ ...options, framework: options.framework ?? "tanstack-start" });
1605
+ }
1397
1606
  export {
1398
1607
  tanstackStartReactModulesPreset as default
1399
1608
  };