oxlint-plugin-arch 0.6.3 → 0.7.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.
@@ -418,6 +418,24 @@ var presetTestRules = {
418
418
  "vitest/no-conditional-expect": "error",
419
419
  "vitest/require-mock-type-parameters": "error"
420
420
  };
421
+ var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
+ var presetNextSegmentConfig = [
423
+ "generateStaticParams",
424
+ "dynamic",
425
+ "dynamicParams",
426
+ "revalidate",
427
+ "fetchCache",
428
+ "runtime",
429
+ "preferredRegion",
430
+ "maxDuration"
431
+ ];
432
+ var presetNextSegmentExports = [
433
+ "generateMetadata",
434
+ "metadata",
435
+ "generateViewport",
436
+ "viewport",
437
+ ...presetNextSegmentConfig
438
+ ];
421
439
  var presetExportPrefix = {
422
440
  stem: "before-first-dot",
423
441
  trailingRoles: ["constants", "utils", "handler", "service", "types"],
@@ -436,7 +454,7 @@ var presetGeneratedFiles = [
436
454
 
437
455
  // src/utils/helpers/preset.ts
438
456
  function presetScopes(roots, suffix = `**/${presetExtensions}`) {
439
- return roots.map((root) => `${root}/${suffix}`);
457
+ return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
440
458
  }
441
459
  function presetOverride(files, rules, excludeFiles = []) {
442
460
  return {
@@ -445,6 +463,18 @@ function presetOverride(files, rules, excludeFiles = []) {
445
463
  rules
446
464
  };
447
465
  }
466
+ function presetRuleSeverities(rules, severities) {
467
+ const result = structuredClone(rules);
468
+ for (const [name, level] of Object.entries(severities)) {
469
+ const value = result[name];
470
+ const current = Array.isArray(value) ? value[0] : value;
471
+ if (value === undefined || current === "off" || current === "allow" || current === 0) {
472
+ continue;
473
+ }
474
+ result[name] = Array.isArray(value) && level !== "off" ? [level, ...value.slice(1)] : level;
475
+ }
476
+ return result;
477
+ }
448
478
  function presetPackageName(root, directory) {
449
479
  try {
450
480
  const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
@@ -469,7 +499,14 @@ function presetSeverity(rules, level) {
469
499
  function presetSourceConfig(context) {
470
500
  const { options, web, scripts, app, appFiles, backendFiles, ui, safetyFiles, publicEntrypoints, baseImports } = context;
471
501
  const policies = {};
472
- policies.formatting = [presetOverride([`**/${presetExtensions}`], presetFormattingRules)];
502
+ const { semi = "always", quotes = "single" } = options.formatting ?? {};
503
+ policies.formatting = [
504
+ presetOverride([`**/${presetExtensions}`], {
505
+ ...presetFormattingRules,
506
+ "eslint-js/semi": ["error", semi],
507
+ "eslint-js/quotes": ["error", quotes, { avoidEscape: true }]
508
+ })
509
+ ];
473
510
  policies.statementLayout = [
474
511
  presetOverride([`**/${presetExtensions}`], {
475
512
  "arch/padding-between-statements": ["error", { multilineVariables: true }],
@@ -555,12 +592,18 @@ function presetTypesConfig(context) {
555
592
  safetyFiles,
556
593
  components,
557
594
  hooks,
558
- publicEntrypoints
595
+ publicEntrypoints,
596
+ tanstackWeb,
597
+ nextWeb
559
598
  } = context;
599
+ const schemaOwners = backend.length ? backend : nextWeb;
600
+ const runtimeFreeTypes = backend.length ? web : tanstackWeb;
601
+ const colocatedTypes = options.modules?.colocatedTypeFiles ?? [];
560
602
  const policies = {};
561
603
  policies.typePlacement = [
562
604
  presetOverride(appFiles, { "arch/no-type-declarations": "error" }, [
563
605
  ...types,
606
+ ...colocatedTypes,
564
607
  ...components,
565
608
  ...hooks,
566
609
  ...services,
@@ -583,16 +626,18 @@ function presetTypesConfig(context) {
583
626
  ...options.react?.componentProps === false ? [] : [presetOverride(components, { "arch/component-props": ["error", { filePrefix: true }] }, [...ui, ...testFiles])],
584
627
  presetOverride(presetScopes(app, "**/*.types.ts"), {
585
628
  "arch/no-restricted-files": ["error", { message: "Domain types belong in src/types." }]
586
- }, [...types, ...ui]),
629
+ }, [...types, ...colocatedTypes, ...ui]),
587
630
  presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
588
631
  "arch/no-type-declarations": "error"
589
632
  }, [...ui, ...testFiles]),
590
- presetOverride([...presetScopes(backend, "types/**/*.types.ts"), ...presetScopes(app, "**/*.constants.ts")], {
633
+ presetOverride([...presetScopes(schemaOwners, "types/**/*.types.ts"), ...colocatedTypes, ...presetScopes(app, "**/*.constants.ts")], {
591
634
  "arch/no-top-level-functions": ["error", { banReExports: false }]
592
635
  }),
593
- presetOverride(presetScopes(web, "types/**/*.types.ts"), {
594
- "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
595
- })
636
+ ...runtimeFreeTypes.length ? [
637
+ presetOverride(presetScopes(runtimeFreeTypes, "types/**/*.types.ts"), {
638
+ "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
639
+ })
640
+ ] : []
596
641
  ];
597
642
  policies.typeSafety = [
598
643
  presetOverride(safetyFiles, {
@@ -629,7 +674,7 @@ function presetTypesConfig(context) {
629
674
  policies.schemas = [
630
675
  presetOverride(appFiles, {
631
676
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
632
- }, [...presetScopes(backend, "types/**"), ...ui]),
677
+ }, [...presetScopes(schemaOwners, "types/**"), ...colocatedTypes, ...ui]),
633
678
  presetOverride(webFiles, {
634
679
  "arch/no-rederive-schema": [
635
680
  "error",
@@ -668,8 +713,9 @@ function presetTypesConfig(context) {
668
713
  }
669
714
 
670
715
  // src/configs/modules.ts
716
+ import { posix } from "node:path";
671
717
  function presetModulesConfig(context) {
672
- const { web, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
718
+ const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
673
719
  const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
674
720
  const migrations = options.modules?.migrationFiles ?? [];
675
721
  const frontendEntries = [
@@ -703,7 +749,7 @@ function presetModulesConfig(context) {
703
749
  })
704
750
  ] : []
705
751
  ]),
706
- presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), `components/*/*/**/${presetExtensions}`), {
752
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), `components/*/*/**/${presetExtensions}`), {
707
753
  "arch/no-restricted-files": ["error", { message: "Components allow one group folder." }]
708
754
  }, ui),
709
755
  presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], {
@@ -719,7 +765,13 @@ function presetModulesConfig(context) {
719
765
  message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
720
766
  }
721
767
  ]
722
- }, ["**/*.{client,server,rsc,utils,constants}.ts", "**/*.rsc.tsx", ...conceptFiles])
768
+ }, [
769
+ "**/*.{client,server,rsc,utils,constants}.ts",
770
+ "**/*.rsc.tsx",
771
+ ...presetScopes(nextWeb, "services/**/*.action.ts"),
772
+ ...conceptFiles,
773
+ ...options.modules?.colocatedTypeFiles ?? []
774
+ ])
723
775
  ];
724
776
  policies.serviceStructure = [
725
777
  presetOverride(services, {
@@ -837,9 +889,9 @@ function presetModulesConfig(context) {
837
889
  "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
838
890
  }),
839
891
  presetOverride(components, {
840
- "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true }]
892
+ "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
841
893
  }, [...ui, ...testFiles]),
842
- presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), "components/**/*.tsx"), {
894
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), "components/**/*.tsx"), {
843
895
  "arch/folder-prefix": ["error", { singularize: "trailing-s", separators: ["-"] }]
844
896
  }, [...ui, ...testFiles]),
845
897
  ...["action", "query"].map((role) => presetOverride([
@@ -899,7 +951,7 @@ function presetModulesConfig(context) {
899
951
 
900
952
  // src/configs/tests.ts
901
953
  function presetTestsConfig(context) {
902
- const { backend, testFiles, baseImports, options } = context;
954
+ const { backendRoots, testFiles, baseImports, options } = context;
903
955
  const profile = options.tests?.profile ?? "fixtures";
904
956
  if (profile !== "standard" && profile !== "fixtures")
905
957
  throw new Error("Unknown test profile.");
@@ -930,7 +982,7 @@ function presetTestsConfig(context) {
930
982
  { callees: ["describe", ...testFunctions], forbid: "^should\\b", flags: "i" }
931
983
  ]
932
984
  }),
933
- presetOverride(presetScopes(backend.map((path) => path.replace(/\/src$/, "")), "tests/**/*.ts"), {
985
+ presetOverride(presetScopes(backendRoots, "tests/**/*.ts"), {
934
986
  "modules/concurrent-db": "error",
935
987
  ...profile === "fixtures" ? {
936
988
  "no-restricted-imports": [
@@ -953,7 +1005,7 @@ function presetTestsConfig(context) {
953
1005
 
954
1006
  // src/configs/react.ts
955
1007
  function presetReactConfig(context) {
956
- const { options, web, webFiles, ui, testFiles, components, css } = context;
1008
+ const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
957
1009
  const policies = {};
958
1010
  if (web.length) {
959
1011
  policies.react = [
@@ -975,9 +1027,10 @@ function presetReactConfig(context) {
975
1027
  if (web.length) {
976
1028
  policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
977
1029
  }
978
- if (web.length) {
979
- policies.routes = [
980
- presetOverride(presetScopes(web, "routes/**/*.{ts,tsx}"), {
1030
+ const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
1031
+ const routes = [
1032
+ ...tanstackWeb.length ? [
1033
+ presetOverride(presetScopes(tanstackWeb, "routes/**/*.{ts,tsx}"), {
981
1034
  "arch/route-surface": [
982
1035
  "error",
983
1036
  {
@@ -987,7 +1040,7 @@ function presetReactConfig(context) {
987
1040
  }
988
1041
  ]
989
1042
  }, testFiles),
990
- presetOverride(presetScopes(web, "routes/handlers/**/*.ts"), {
1043
+ presetOverride(presetScopes(tanstackWeb, "routes/handlers/**/*.ts"), {
991
1044
  "arch/no-file-level-helpers": [
992
1045
  "error",
993
1046
  {
@@ -996,7 +1049,43 @@ function presetReactConfig(context) {
996
1049
  }
997
1050
  ]
998
1051
  })
999
- ];
1052
+ ] : [],
1053
+ ...nextWeb.length ? [
1054
+ presetOverride(presetScopes(nextWeb, "app/**/{page,template,default,not-found,forbidden,unauthorized,loading}.tsx"), {
1055
+ "arch/route-surface": [
1056
+ "error",
1057
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: true, ...nextPage }
1058
+ ]
1059
+ }, [...testFiles, ...presetScopes(nextWeb, "app/not-found.tsx")]),
1060
+ presetOverride([...presetScopes(nextWeb, "app/**/{layout,global-not-found}.tsx"), ...presetScopes(nextWeb, "app/not-found.tsx")], {
1061
+ "arch/route-surface": [
1062
+ "error",
1063
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: false, ...nextPage }
1064
+ ]
1065
+ }, testFiles),
1066
+ presetOverride(presetScopes(nextWeb, "app/**/route.ts"), {
1067
+ "arch/route-surface": [
1068
+ "error",
1069
+ {
1070
+ exportName: "GET",
1071
+ allowedExports: [...presetNextHandlerMethods.slice(1), ...presetNextSegmentConfig],
1072
+ bannedHooks: [],
1073
+ banIntrinsicJsx: false
1074
+ }
1075
+ ],
1076
+ "arch/no-file-level-helpers": [
1077
+ "error",
1078
+ {
1079
+ detectComponents: false,
1080
+ allowPattern: `^(${[...presetNextHandlerMethods, ...presetNextSegmentConfig].join("|")})$`,
1081
+ 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."
1082
+ }
1083
+ ]
1084
+ })
1085
+ ] : []
1086
+ ];
1087
+ if (routes.length) {
1088
+ policies.routes = routes;
1000
1089
  }
1001
1090
  if (web.length) {
1002
1091
  policies.forms = [
@@ -1049,23 +1138,51 @@ function presetReactConfig(context) {
1049
1138
  `${group}-[env(safe-area-inset-${side})]`,
1050
1139
  ...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
1051
1140
  ]));
1141
+ const illustrationPalette = [
1142
+ "text-color",
1143
+ "bg-color",
1144
+ "gradient-from",
1145
+ "gradient-via",
1146
+ "gradient-to",
1147
+ "border-color",
1148
+ "divide-color",
1149
+ "outline-color",
1150
+ "ring-color",
1151
+ "ring-offset-color",
1152
+ "shadow",
1153
+ "shadow-color",
1154
+ "inset-shadow",
1155
+ "inset-shadow-color",
1156
+ "drop-shadow",
1157
+ "drop-shadow-color",
1158
+ "fill",
1159
+ "stroke",
1160
+ "bg-image",
1161
+ "bg-size",
1162
+ "bg-position",
1163
+ "mask-image",
1164
+ "blur",
1165
+ "saturate"
1166
+ ];
1167
+ const restyle = (base) => ({
1168
+ allow: base,
1169
+ contracts: [
1170
+ { pattern: "^(CardContent|PopoverContent)$", allow: [...base, ...padding] },
1171
+ { pattern: "^TabsContent$", allow: [...base, ...gap, ...padding] },
1172
+ { pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...base, ...gap] }
1173
+ ]
1174
+ });
1052
1175
  policies.shadcn = [
1053
1176
  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
- ],
1177
+ "shadcn/no-restyle": ["error", restyle(allow)],
1065
1178
  "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
1066
1179
  "shadcn/require-static-classes": "error",
1067
1180
  "modules/dynamic-classes": "error"
1068
1181
  }, ui),
1182
+ presetOverride(presetScopes(web, "components/**/*.illustration.tsx"), {
1183
+ "shadcn/no-restyle": ["error", restyle([...allow, ...illustrationPalette])],
1184
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea, ...illustrationPalette] }]
1185
+ }, ui),
1069
1186
  presetOverride(webFiles, {
1070
1187
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1071
1188
  }),
@@ -1080,7 +1197,16 @@ function presetReactConfig(context) {
1080
1197
  // src/configs/boundaries.ts
1081
1198
  import { resolve as resolve2 } from "node:path";
1082
1199
  function presetBoundariesConfig(context) {
1083
- const { options, root, architecture, packages, web, backend, appFiles, publicEntrypoints, baseImports } = context;
1200
+ const { options, root, architecture, packages, web, tanstackWeb, nextWeb, backend, appFiles, publicEntrypoints, baseImports } = context;
1201
+ const runtimeBoundary = {
1202
+ web: web.map((path) => resolve2(root, path)),
1203
+ backend: backend.map((path) => resolve2(root, path)),
1204
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve2(root, path)])),
1205
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1206
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1207
+ return name ? [name] : [];
1208
+ })
1209
+ };
1084
1210
  const policies = {};
1085
1211
  policies.boundaries = [
1086
1212
  presetOverride([...appFiles, ...presetScopes(packages, "**/" + presetExtensions)], {
@@ -1108,20 +1234,11 @@ function presetBoundariesConfig(context) {
1108
1234
  }
1109
1235
  ]
1110
1236
  }),
1111
- presetOverride([...presetScopes(web, "services/**/*.{ts,tsx}"), ...presetScopes(web, "lib/**/*.{ts,tsx}")], {
1237
+ presetOverride([...presetScopes(tanstackWeb, "services/**/*.{ts,tsx}"), ...presetScopes(tanstackWeb, "lib/**/*.{ts,tsx}")], {
1112
1238
  "modules/tanstack-runtime": [
1113
1239
  "error",
1114
1240
  {
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
- }),
1241
+ ...runtimeBoundary,
1125
1242
  rpcClients: ((options.orpc?.clientOwnerFile) ? [options.orpc?.clientOwnerFile] : context.rpcClients).map((path) => resolve2(root, path)),
1126
1243
  serverImports: options.tanstackStart?.additionalServerOnlyImports ?? [],
1127
1244
  clientImports: options.tanstackStart?.additionalClientOnlyImports ?? [],
@@ -1129,6 +1246,17 @@ function presetBoundariesConfig(context) {
1129
1246
  }
1130
1247
  ]
1131
1248
  }),
1249
+ presetOverride([...presetScopes(nextWeb, "services/**/*.{ts,tsx}"), ...presetScopes(nextWeb, "lib/**/*.{ts,tsx}")], {
1250
+ "modules/next-runtime": [
1251
+ "error",
1252
+ {
1253
+ ...runtimeBoundary,
1254
+ serverImports: options.next?.additionalServerOnlyImports ?? [],
1255
+ clientImports: options.next?.additionalClientOnlyImports ?? [],
1256
+ allowComputedImportsIn: options.next?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
1257
+ }
1258
+ ]
1259
+ }),
1132
1260
  presetOverride(presetScopes(backend, "rpc/**/*.ts"), {
1133
1261
  "modules/rpc-database": "error",
1134
1262
  "no-restricted-imports": ["error", { paths: ["kysely"], patterns: baseImports }]
@@ -1191,7 +1319,7 @@ function presetPluginsConfig(context) {
1191
1319
  }
1192
1320
 
1193
1321
  // src/services/preset-context.ts
1194
- import { resolve as resolve3, posix } from "node:path";
1322
+ import { resolve as resolve3, posix as posix2 } from "node:path";
1195
1323
  function presetResolveContext(options) {
1196
1324
  const generated = presetGeneratedFiles;
1197
1325
  const root = resolve3(options.root ?? process.cwd());
@@ -1205,13 +1333,24 @@ function presetResolveContext(options) {
1205
1333
  const entry = typeof configuredRoot === "string" ? { role: configuredRoot } : configuredRoot;
1206
1334
  if (!entry || typeof entry !== "object")
1207
1335
  throw new Error("Architecture entries must be roles or role/layout objects.");
1208
- if (Object.keys(entry).some((key) => !["role", "layout"].includes(key)))
1336
+ if (Object.keys(entry).some((key) => !["role", "layout", "framework", "sourceDir"].includes(key)))
1209
1337
  throw new Error("Unknown architecture entry option.");
1210
1338
  const role = entry.role;
1339
+ if ("framework" in entry && entry.framework !== undefined && role !== "web")
1340
+ throw new Error("Only web architecture roots accept a framework.");
1341
+ const framework = role === "web" ? entry.framework ?? options.framework ?? "tanstack-start" : undefined;
1342
+ if (framework !== undefined && framework !== "tanstack-start" && framework !== "next")
1343
+ throw new Error(`Unknown framework: ${framework}`);
1344
+ const sourced = role === "web" || role === "server" || role === "runner";
1345
+ if ("sourceDir" in entry && entry.sourceDir !== undefined && !sourced)
1346
+ throw new Error("Only web, server and runner architecture roots accept a sourceDir.");
1347
+ const sourceDir = entry.sourceDir ?? "src";
1348
+ if (typeof sourceDir !== "string" || sourceDir !== "." && sourceDir.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
1349
+ throw new Error("Source folders must be `.` or literal root-relative paths without escapes, globs or empty segments.");
1211
1350
  const portable = path.replaceAll("\\", "/");
1212
- if (!portable.trim() || posix.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1351
+ if (!portable.trim() || posix2.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1213
1352
  throw new Error("Architecture roots must be nonempty project-relative paths.");
1214
- const normalized = posix.normalize(portable).replace(/\/$/, "");
1353
+ const normalized = posix2.normalize(portable).replace(/\/$/, "");
1215
1354
  if (normalized === ".." || normalized.startsWith("../"))
1216
1355
  throw new Error("Architecture roots must stay inside the project.");
1217
1356
  if (!["web", "server", "runner", "scripts", "packages"].includes(role))
@@ -1239,7 +1378,15 @@ function presetResolveContext(options) {
1239
1378
  throw new Error("Layout folders must not overlap.");
1240
1379
  }
1241
1380
  }
1242
- return [normalized, { role, layout }];
1381
+ return [
1382
+ normalized,
1383
+ {
1384
+ role,
1385
+ layout,
1386
+ ...framework ? { framework } : {},
1387
+ ...entry.sourceDir === undefined ? {} : { sourceDir }
1388
+ }
1389
+ ];
1243
1390
  });
1244
1391
  for (const [index, [path]] of entries.entries()) {
1245
1392
  const overlaps = entries.slice(0, index).some(([other]) => other === path || other === "." || path === "." || other.startsWith(path + "/") || path.startsWith(other + "/"));
@@ -1265,17 +1412,24 @@ function presetResolveContext(options) {
1265
1412
  "spec"
1266
1413
  ].includes(role)))
1267
1414
  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"));
1415
+ if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
1416
+ throw new Error(`Unknown framework: ${options.framework}`);
1417
+ const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
1418
+ const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
1419
+ const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
1420
+ const tanstackWeb = webFor("tanstack-start");
1421
+ const nextWeb = webFor("next");
1422
+ const backendEntries = entries.filter(([, { role }]) => role === "server" || role === "runner");
1423
+ const backendRoots = backendEntries.map(([path]) => path);
1424
+ const backend = backendEntries.map(([path, entry]) => source(path, entry));
1271
1425
  const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1272
1426
  const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1273
1427
  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),
1428
+ path: posix2.join(["web", "server", "runner"].includes(entry.role) ? source(path, entry) : path, folder),
1275
1429
  folder,
1276
1430
  mode
1277
1431
  })));
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"));
1432
+ 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
1433
  const app = [...web, ...backend];
1280
1434
  const appFiles = presetScopes(app);
1281
1435
  const webFiles = presetScopes(web);
@@ -1301,13 +1455,17 @@ function presetResolveContext(options) {
1301
1455
  ];
1302
1456
  const tailwind = options.tailwind;
1303
1457
  const entryPoints = Object.fromEntries(Object.entries(tailwind === false ? {} : tailwind?.cssEntryPointsByRoot ?? {}).map(([path, entry]) => [
1304
- posix.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1458
+ posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1305
1459
  entry
1306
1460
  ]));
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
- ]));
1461
+ const css = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
1462
+ const sourcePath = source(path, entry);
1463
+ const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1464
+ return [
1465
+ sourcePath,
1466
+ resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1467
+ ];
1468
+ }));
1311
1469
  return {
1312
1470
  options,
1313
1471
  root,
@@ -1317,7 +1475,10 @@ function presetResolveContext(options) {
1317
1475
  packages,
1318
1476
  scripts,
1319
1477
  web,
1478
+ tanstackWeb,
1479
+ nextWeb,
1320
1480
  backend,
1481
+ backendRoots,
1321
1482
  app,
1322
1483
  appFiles,
1323
1484
  webFiles,
@@ -1341,6 +1502,8 @@ function presetResolveContext(options) {
1341
1502
  // src/services/preset-compose.ts
1342
1503
  function presetCompose(context, policies, plugins) {
1343
1504
  const { options, level, safetyFiles, ui, generated } = context;
1505
+ const severities = options.ruleSeverities ?? {};
1506
+ const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1344
1507
  const overrideList = [];
1345
1508
  for (const name of presetPolicyOrder) {
1346
1509
  const setting = options.policies?.[name];
@@ -1352,14 +1515,16 @@ function presetCompose(context, policies, plugins) {
1352
1515
  for (const entry of entries) {
1353
1516
  overrideList.push({
1354
1517
  ...entry,
1355
- rules: entry.rules ? presetSeverity(entry.rules, level) : undefined
1518
+ rules: entry.rules ? levels(entry.rules) : undefined
1356
1519
  });
1357
1520
  }
1358
1521
  }
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));
1522
+ overrideList.push(presetOverride(safetyFiles, levels({
1523
+ "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1524
+ complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
+ }), ui));
1526
+ const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
+ const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1363
1528
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
1364
1529
  overrideList.push({ files, rules: { [rule]: "off" } });
1365
1530
  }
@@ -1373,17 +1538,18 @@ function presetCompose(context, policies, plugins) {
1373
1538
  "**/build/**",
1374
1539
  "**/coverage/**",
1375
1540
  "**/.output/**",
1541
+ ...context.nextWeb.length ? ["**/.next/**", "**/next-env.d.ts"] : [],
1376
1542
  ...generated,
1377
1543
  ...options.ignorePatterns ?? []
1378
1544
  ],
1379
1545
  ...plugins,
1380
- rules: presetSeverity(presetCommonRules, level),
1546
+ rules: { ...levels(presetCommonRules), ...unnamedLevels },
1381
1547
  overrides: overrideList.filter((entry) => entry.files.length > 0)
1382
1548
  };
1383
1549
  }
1384
1550
 
1385
- // src/presets/tanstack-start-react-modules-preset.ts
1386
- function tanstackStartReactModulesPreset(options = {}) {
1551
+ // src/presets/react-modules-preset.ts
1552
+ function reactModulesPreset(options = {}) {
1387
1553
  const context = presetResolveContext(options);
1388
1554
  return presetCompose(context, {
1389
1555
  ...presetSourceConfig(context),
@@ -1394,6 +1560,18 @@ function tanstackStartReactModulesPreset(options = {}) {
1394
1560
  ...presetBoundariesConfig(context)
1395
1561
  }, presetPluginsConfig(context));
1396
1562
  }
1563
+
1564
+ // src/presets/next-react-modules-preset.ts
1565
+ function nextReactModulesPreset(options = {}) {
1566
+ return reactModulesPreset({ ...options, framework: options.framework ?? "next" });
1567
+ }
1568
+
1569
+ // src/presets/tanstack-start-react-modules-preset.ts
1570
+ function tanstackStartReactModulesPreset(options = {}) {
1571
+ return reactModulesPreset({ ...options, framework: options.framework ?? "tanstack-start" });
1572
+ }
1397
1573
  export {
1398
- tanstackStartReactModulesPreset
1574
+ tanstackStartReactModulesPreset,
1575
+ reactModulesPreset,
1576
+ nextReactModulesPreset
1399
1577
  };
@@ -0,0 +1,4 @@
1
+ import type { OxlintConfig } from 'oxlint';
2
+ import type { PresetOptions } from '../types/preset.types.ts';
3
+ /** The React modules preset with Next.js App Router as the default web framework. */
4
+ export default function nextReactModulesPreset(options?: PresetOptions): OxlintConfig;