oxlint-plugin-arch 0.8.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ **Breaking:** `packages` roots now get the app rules. Until now they only got the shared baseline (formatting, imports, stock TypeScript/JavaScript rules and `modules/import-boundaries`), so expect new errors in package `src` folders.
6
+
7
+ - A packages root with its own `package.json` is one package. Without one, each child folder is a package, so `packages: 'packages'` covers every `packages/*/src`. Per-package entries keep working. Files outside `src` keep the baseline only.
8
+ - Every package file gets the app-wide rules: `arch/no-comments` (with the stricter `*.types.ts`/`*.constants.ts` variant), the `typeSafety` set (including the `dillon-anti-slop/*` rules), `arch/no-type-declarations`, `arch/no-local-schema-construction`, `arch/no-rederive-schema`, `arch/no-module-mutable-state`, `modules/double-negation`, `arch/no-trivial-functions`, `max-lines` and `complexity`.
9
+ - Services, types and file roles in packages follow the server-root rules: `modules/service-functions`, `modules/service-types`, `modules/domain-constants`, `arch/require-object-params`, `arch/no-extra-exports`, `arch/no-inline-types`, `arch/export-file-prefix`, `arch/no-restricted-files`, `arch/no-top-level-functions`, `arch/filename-match`, `arch/filename-export-name`, `arch/folder-prefix` and `arch/no-unescaped-like`. Packages default to the `services: 'domain'` layout, and `layout` keys on a packages root now resolve inside `src`. `entry.*.ts` files stay exempt from `arch/export-file-prefix`, as in server roots.
10
+ - React code in packages (`*.tsx`, `components/**`, `hooks/**`) follows the web-root rules: the React, effects, memoization and forms policies, `arch/jsx-attributes-multiline`, `arch/no-file-level-helpers`, `arch/component-props`, `arch/declaration-name`, `modules/local-type-alias`, `modules/pascal-interface`, `arch/only-export-components` and the hook naming rules. The React plugins now load when packages exist, even without a web root.
11
+ - Package `types/**` and `*.types.ts` files may construct schemas, like backend `types/**`.
12
+ - Never on packages: `modules/backend-switch`, `modules/rpc-database`, `arch/require-orpc-output`, `modules/tanstack-runtime`, `arch/no-restricted-token`, `arch/no-runtime-in-types` and `modules/dynamic-classes`. Routes, Tailwind and shadcn usage rules stay with web roots.
13
+ - Rename `shadcn.uiImportPath` to `shadcn.ui` and `shadcn.componentImportSources` to `shadcn.components`, matching the components.json alias names. The old names throw. Migration: `shadcn: { uiImportPath: '@ui' }` becomes `shadcn: { ui: '@ui' }`.
14
+ - `shadcn.ui` also selects the UI kit files, which keep the existing kit exceptions (type safety, formatting and layout are still checked). Use a workspace package name (`'@app/ui'` → that package's `src/**`) or a tsconfig `paths` alias (`'@/kit'` → `<root>/src/kit/**`). A value that resolves to nothing throws. When unset, the kit stays `<web source>/components/ui/**`. A UI kit in a package needs `shadcn: { ui: '<package name>' }`; without it, the kit gets the full package rules.
15
+
16
+ How to fix what the new rules report:
17
+
18
+ | Fix | Rules |
19
+ | --- | --- |
20
+ | Auto-fix (`--fix`) | `arch/no-comments`, `arch/jsx-attributes-multiline`, the `react-extra/jsx-*` layout rules |
21
+ | Rename | `arch/export-file-prefix`, `arch/filename-match`, `arch/filename-export-name`, `arch/folder-prefix`, `arch/declaration-name`, `arch/export-name-pattern`, `modules/pascal-interface`, `modules/shape-suffix` |
22
+ | Move | `arch/no-type-declarations` (to `types/`), `arch/no-local-schema-construction` (to `types/` or `*.types.ts`), `arch/no-restricted-files`, `arch/no-extra-exports`, `arch/only-export-components`, `arch/no-file-level-helpers`, `modules/domain-constants`, `modules/service-types`, `modules/local-type-alias`, `max-lines` |
23
+ | Reshape | `arch/require-object-params`, `arch/component-props`, `arch/no-inline-types`, `arch/no-trivial-functions`, `arch/no-module-mutable-state`, `arch/no-restricted-constructor`, `modules/service-functions`, `modules/no-unknown`, `modules/memoization`, `typescript/no-explicit-any`, the `dillon-anti-slop/*` rules, the React and effects rules, `complexity` |
24
+
25
+ To defer a package, turn single rules off for it: `ruleExclusions: { '<rule>': ['packages/foo/**'] }`.
26
+
27
+ ## 0.9.0
28
+
29
+ - Add `limits.maxTypeFileLines` (default `1000`): the preset applies this `max-lines` limit to `*.types.ts` files in app roots instead of `limits.maxFileLines`. Other files keep `maxFileLines`.
30
+
3
31
  ## 0.8.0
4
32
 
5
33
  - Add `tests.mocking` to choose which test-double APIs the preset bans in test files: `'modules'` (default, unchanged) or `'strict'`, which also bans `spyOn`, `stubGlobal` and `stubEnv` outside `*.driver.test.ts` and asks for a fake driver. The object form adds `deny` API patterns and per-file `allow` exceptions.
@@ -1,6 +1,6 @@
1
1
  // src/utils/helpers/preset.ts
2
- import { readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { relative, resolve } from "node:path";
4
4
 
5
5
  // src/constants/preset.constants.ts
6
6
  var presetExtensions = "*.{ts,tsx,mts,cts}";
@@ -504,6 +504,61 @@ function presetSeverity(rules, level) {
504
504
  }
505
505
  return result;
506
506
  }
507
+ function presetReadJsonc(path) {
508
+ let text;
509
+ try {
510
+ text = readFileSync(path, "utf8");
511
+ } catch {
512
+ return;
513
+ }
514
+ let output = "";
515
+ for (let index = 0;index < text.length; index++) {
516
+ const character = text[index];
517
+ if (character === '"') {
518
+ const end = /"(?:[^"\\]|\\.)*"/y;
519
+ end.lastIndex = index;
520
+ const literal = end.exec(text)?.[0] ?? text.slice(index);
521
+ output += literal;
522
+ index += literal.length - 1;
523
+ } else if (character === "/" && text[index + 1] === "/") {
524
+ index = text.indexOf(`
525
+ `, index) === -1 ? text.length : text.indexOf(`
526
+ `, index) - 1;
527
+ } else if (character === "/" && text[index + 1] === "*") {
528
+ index = text.indexOf("*/", index + 2) === -1 ? text.length : text.indexOf("*/", index + 2) + 1;
529
+ } else if (character === "}" || character === "]") {
530
+ output = output.replace(/,\s*$/, "") + character;
531
+ } else {
532
+ output += character;
533
+ }
534
+ }
535
+ try {
536
+ return JSON.parse(output);
537
+ } catch {
538
+ return;
539
+ }
540
+ }
541
+ function presetTsconfigAliasTargets(root, directory, specifier) {
542
+ const config = presetReadJsonc(resolve(root, directory, "tsconfig.json"));
543
+ if (!config || typeof config !== "object" || !("compilerOptions" in config))
544
+ return [];
545
+ const { compilerOptions } = config;
546
+ if (!compilerOptions || typeof compilerOptions !== "object" || !("paths" in compilerOptions))
547
+ return [];
548
+ const { paths } = compilerOptions;
549
+ const baseUrl = "baseUrl" in compilerOptions && typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : ".";
550
+ if (!paths || typeof paths !== "object")
551
+ return [];
552
+ return Object.entries(paths).flatMap(([pattern, targets]) => {
553
+ const [prefix, suffix = ""] = pattern.split("*");
554
+ const wildcard = pattern.includes("*");
555
+ const matches = wildcard ? specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length : specifier === pattern;
556
+ if (!matches || !Array.isArray(targets))
557
+ return [];
558
+ const captured = wildcard ? specifier.slice(prefix.length, specifier.length - suffix.length) : "";
559
+ return targets.filter((target) => typeof target === "string").map((target) => relative(root, resolve(root, directory, baseUrl, target.replace("*", captured))).replaceAll("\\", "/")).filter((target) => target && !target.startsWith("..") && existsSync(resolve(root, target)));
560
+ });
561
+ }
507
562
 
508
563
  // src/configs/source.ts
509
564
  function presetSourceConfig(context) {
@@ -591,6 +646,8 @@ function presetTypesConfig(context) {
591
646
  options,
592
647
  web,
593
648
  backend,
649
+ packageSources,
650
+ serviceRoots,
594
651
  app,
595
652
  appFiles,
596
653
  webFiles,
@@ -640,7 +697,11 @@ function presetTypesConfig(context) {
640
697
  presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
641
698
  "arch/no-type-declarations": "error"
642
699
  }, [...ui, ...testFiles]),
643
- presetOverride([...presetScopes(schemaOwners, "types/**/*.types.ts"), ...colocatedTypes, ...presetScopes(app, "**/*.constants.ts")], {
700
+ presetOverride([
701
+ ...presetScopes([...schemaOwners, ...packageSources], "types/**/*.types.ts"),
702
+ ...colocatedTypes,
703
+ ...presetScopes(app, "**/*.constants.ts")
704
+ ], {
644
705
  "arch/no-top-level-functions": ["error", { banReExports: false }]
645
706
  }),
646
707
  ...runtimeFreeTypes.length ? [
@@ -684,8 +745,13 @@ function presetTypesConfig(context) {
684
745
  policies.schemas = [
685
746
  presetOverride(appFiles, {
686
747
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
687
- }, [...presetScopes(schemaOwners, "types/**"), ...colocatedTypes, ...ui]),
688
- presetOverride(webFiles, {
748
+ }, [
749
+ ...presetScopes([...schemaOwners, ...packageSources], "types/**"),
750
+ ...presetScopes(packageSources, "**/*.types.ts"),
751
+ ...colocatedTypes,
752
+ ...ui
753
+ ]),
754
+ presetOverride([...webFiles, ...presetScopes(packageSources)], {
689
755
  "arch/no-rederive-schema": [
690
756
  "error",
691
757
  {
@@ -695,7 +761,7 @@ function presetTypesConfig(context) {
695
761
  }
696
762
  ]
697
763
  }),
698
- presetOverride(presetScopes(backend, "services/**/*.ts"), {
764
+ presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), {
699
765
  "arch/no-unescaped-like": [
700
766
  "error",
701
767
  {
@@ -725,7 +791,7 @@ function presetTypesConfig(context) {
725
791
  // src/configs/modules.ts
726
792
  import { posix } from "node:path";
727
793
  function presetModulesConfig(context) {
728
- const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
794
+ const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, components, hooks, options } = context;
729
795
  const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
730
796
  const migrations = options.modules?.migrationFiles ?? [];
731
797
  const frontendEntries = [
@@ -822,7 +888,7 @@ function presetModulesConfig(context) {
822
888
  }
823
889
  ]
824
890
  }, testFiles),
825
- presetOverride(presetScopes(backend, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
891
+ presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
826
892
  ...utilities,
827
893
  ...testFiles
828
894
  ]),
@@ -832,11 +898,11 @@ function presetModulesConfig(context) {
832
898
  presetOverride(frontendEntries, {
833
899
  "modules/domain-constants": ["error", { includeData: true, allowServiceMethods: true }]
834
900
  }, testFiles),
835
- presetOverride([...presetScopes(backend, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
901
+ presetOverride([...presetScopes(serviceRoots, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
836
902
  presetOverride(frontendEntries, {
837
903
  "arch/require-object-params": ["error", { maxParams: 2, serviceMethods: true }]
838
904
  }, testFiles),
839
- presetOverride(presetScopes(backend, "services/**/*.service.ts"), {
905
+ presetOverride(presetScopes(serviceRoots, "services/**/*.service.ts"), {
840
906
  "arch/no-extra-exports": [
841
907
  "error",
842
908
  {
@@ -864,7 +930,7 @@ function presetModulesConfig(context) {
864
930
  }
865
931
  ]
866
932
  }, ["**/*.constants.ts"]),
867
- presetOverride(presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/export-file-prefix": ["error", presetExportPrefix] }, [...ui, ...presetScopes(backend, "entry.*.ts")]),
933
+ presetOverride(presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/export-file-prefix": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]),
868
934
  presetOverride(frontendEntries, {
869
935
  "arch/export-file-prefix": [
870
936
  "error",
@@ -897,7 +963,7 @@ function presetModulesConfig(context) {
897
963
  ],
898
964
  "arch/export-file-prefix": "error",
899
965
  "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
900
- }),
966
+ }, ui),
901
967
  presetOverride(components, {
902
968
  "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
903
969
  }, [...ui, ...testFiles]),
@@ -1043,11 +1109,12 @@ function presetTestsConfig(context) {
1043
1109
 
1044
1110
  // src/configs/react.ts
1045
1111
  function presetReactConfig(context) {
1046
- const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
1112
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, uiRoots, ui, testFiles, components, css } = context;
1047
1113
  const policies = {};
1048
- if (web.length) {
1114
+ const webUi = uiRoots.filter((path) => web.some((root) => root === "." || path === root || path.startsWith(root + "/")));
1115
+ if (reactFiles.length) {
1049
1116
  policies.react = [
1050
- presetOverride(webFiles, {
1117
+ presetOverride(reactFiles, {
1051
1118
  ...presetReactRules,
1052
1119
  ...options.react?.compiler === false ? {
1053
1120
  "react-perf/jsx-no-new-object-as-prop": "error",
@@ -1062,8 +1129,8 @@ function presetReactConfig(context) {
1062
1129
  }, [...ui, ...testFiles])
1063
1130
  ];
1064
1131
  }
1065
- if (web.length) {
1066
- policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
1132
+ if (reactFiles.length) {
1133
+ policies.effects = [presetOverride(reactFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
1067
1134
  }
1068
1135
  const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
1069
1136
  const routes = [
@@ -1125,9 +1192,9 @@ function presetReactConfig(context) {
1125
1192
  if (routes.length) {
1126
1193
  policies.routes = routes;
1127
1194
  }
1128
- if (web.length) {
1195
+ if (reactFiles.length) {
1129
1196
  policies.forms = [
1130
- presetOverride(webFiles, {
1197
+ presetOverride(reactFiles, {
1131
1198
  "arch/require-paired-call": [
1132
1199
  "error",
1133
1200
  {
@@ -1138,8 +1205,8 @@ function presetReactConfig(context) {
1138
1205
  }, ui)
1139
1206
  ];
1140
1207
  }
1141
- if (web.length && options.react?.compiler !== false) {
1142
- policies.memoization = [presetOverride(webFiles, { "modules/memoization": "error" }, ui)];
1208
+ if (reactFiles.length && options.react?.compiler !== false) {
1209
+ policies.memoization = [presetOverride(reactFiles, { "modules/memoization": "error" }, ui)];
1143
1210
  }
1144
1211
  if (web.length) {
1145
1212
  policies.rpcClientOwnership = [
@@ -1154,7 +1221,7 @@ function presetReactConfig(context) {
1154
1221
  },
1155
1222
  {
1156
1223
  token: "createContext",
1157
- allowPathPatterns: [...presetScopes(web, "context/"), ...presetScopes(web, "components/ui/")]
1224
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUi.map((path) => `${path}/`)]
1158
1225
  }
1159
1226
  ]
1160
1227
  }
@@ -1224,7 +1291,7 @@ function presetReactConfig(context) {
1224
1291
  presetOverride(webFiles, {
1225
1292
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1226
1293
  }),
1227
- presetOverride(presetScopes(web, "components/ui/sonner.tsx"), {
1294
+ presetOverride(presetScopes(webUi, "sonner.tsx"), {
1228
1295
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1229
1296
  })
1230
1297
  ];
@@ -1307,7 +1374,7 @@ function presetBoundariesConfig(context) {
1307
1374
  import { createRequire } from "node:module";
1308
1375
  var require2 = createRequire(import.meta.url);
1309
1376
  function presetPluginsConfig(context) {
1310
- const { options, web, css } = context;
1377
+ const { options, reactFiles, css } = context;
1311
1378
  const jsPlugins = [
1312
1379
  { name: "eslint-js", specifier: require2.resolve("oxlint-plugin-eslint") },
1313
1380
  {
@@ -1336,7 +1403,7 @@ function presetPluginsConfig(context) {
1336
1403
  react: { version: "19" },
1337
1404
  "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
1338
1405
  };
1339
- if (web.length) {
1406
+ if (reactFiles.length) {
1340
1407
  jsPlugins.push({
1341
1408
  name: "react-extra",
1342
1409
  specifier: require2.resolve("eslint-plugin-react")
@@ -1348,8 +1415,8 @@ function presetPluginsConfig(context) {
1348
1415
  if (options.shadcn !== false) {
1349
1416
  jsPlugins.push(require2.resolve("@shadcn/lint"));
1350
1417
  settings.shadcn = {
1351
- ui: options.shadcn?.uiImportPath ?? "@/components/ui",
1352
- componentImports: options.shadcn?.componentImportSources ?? ["^@/components/"]
1418
+ ui: options.shadcn?.ui ?? "@/components/ui",
1419
+ componentImports: options.shadcn?.components ?? ["^@/components/"]
1353
1420
  };
1354
1421
  }
1355
1422
  }
@@ -1357,6 +1424,7 @@ function presetPluginsConfig(context) {
1357
1424
  }
1358
1425
 
1359
1426
  // src/services/preset-context.ts
1427
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
1360
1428
  import { resolve as resolve3, posix as posix2 } from "node:path";
1361
1429
  function presetResolveContext(options) {
1362
1430
  const generated = presetGeneratedFiles;
@@ -1396,7 +1464,7 @@ function presetResolveContext(options) {
1396
1464
  if (entry.layout !== undefined && (!entry.layout || typeof entry.layout !== "object" || Array.isArray(entry.layout)))
1397
1465
  throw new Error("Folder layout must be a path-to-mode object.");
1398
1466
  const layout = {};
1399
- if (role === "web" || role === "server" || role === "runner") {
1467
+ if (role !== "scripts") {
1400
1468
  layout.services = "domain";
1401
1469
  }
1402
1470
  if (role === "web") {
@@ -1453,6 +1521,8 @@ function presetResolveContext(options) {
1453
1521
  if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
1454
1522
  throw new Error(`Unknown framework: ${options.framework}`);
1455
1523
  const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
1524
+ const isPackage = (path) => existsSync2(resolve3(root, path, "package.json"));
1525
+ const packageSource = (path) => posix2.join(path, isPackage(path) ? "src" : "*/src");
1456
1526
  const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
1457
1527
  const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
1458
1528
  const tanstackWeb = webFor("tanstack-start");
@@ -1461,25 +1531,55 @@ function presetResolveContext(options) {
1461
1531
  const backendRoots = backendEntries.map(([path]) => path);
1462
1532
  const backend = backendEntries.map(([path, entry]) => source(path, entry));
1463
1533
  const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1534
+ const packageSources = packages.map(packageSource);
1535
+ const serviceRoots = [...backend, ...packageSources];
1464
1536
  const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1465
1537
  const folders = entries.flatMap(([path, entry]) => Object.entries(entry.layout).map(([folder, mode]) => ({
1466
- path: posix2.join(["web", "server", "runner"].includes(entry.role) ? source(path, entry) : path, folder),
1538
+ path: posix2.join(entry.role === "scripts" ? path : entry.role === "packages" ? packageSource(path) : source(path, entry), folder),
1467
1539
  folder,
1468
1540
  mode
1469
1541
  })));
1470
1542
  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"));
1471
- const app = [...web, ...backend];
1543
+ const app = [...web, ...backend, ...packageSources];
1472
1544
  const appFiles = presetScopes(app);
1473
1545
  const webFiles = presetScopes(web);
1546
+ const reactFiles = [
1547
+ ...webFiles,
1548
+ ...presetScopes(packageSources, "**/*.tsx"),
1549
+ ...presetScopes(packageSources, `components/**/${presetExtensions}`),
1550
+ ...presetScopes(packageSources, `hooks/**/${presetExtensions}`)
1551
+ ];
1474
1552
  const backendFiles = presetScopes(backend);
1475
1553
  const services = presetScopes(app, `services/**/${presetExtensions}`);
1476
1554
  const utilities = ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"];
1477
1555
  const types = presetScopes(app, "types/**");
1478
- const ui = presetScopes(web, "components/ui/**");
1556
+ const resolveUi = (value) => {
1557
+ if (typeof value !== "string" || !value)
1558
+ throw new Error("shadcn.ui must be a nonempty import path.");
1559
+ const manifests = entries.flatMap(([path, entry]) => {
1560
+ const children = entry.role === "packages" && existsSync2(resolve3(root, path)) && !isPackage(path) ? readdirSync(resolve3(root, path), { withFileTypes: true }).filter((child) => child.isDirectory() && isPackage(posix2.join(path, child.name))).map((child) => ({ directory: posix2.join(path, child.name), sources: posix2.join(path, child.name, "src") })) : [];
1561
+ const sources = ["web", "server", "runner"].includes(entry.role) ? source(path, entry) : posix2.join(path, "src");
1562
+ return [{ directory: path, sources }, ...children];
1563
+ });
1564
+ const named = manifests.filter(({ directory }) => presetPackageName(root, directory) === value);
1565
+ if (named.length)
1566
+ return named.map(({ sources }) => sources);
1567
+ const aliased = [...new Set([".", ...manifests.map(({ directory }) => directory)].flatMap((directory) => presetTsconfigAliasTargets(root, directory, value)))];
1568
+ if (aliased.length)
1569
+ return aliased;
1570
+ throw new Error(`shadcn.ui ${JSON.stringify(value)} matches no workspace package name or existing tsconfig paths alias target.`);
1571
+ };
1572
+ const shadcn = options.shadcn || {};
1573
+ if ("uiImportPath" in shadcn)
1574
+ throw new Error("shadcn.uiImportPath was renamed to shadcn.ui.");
1575
+ if ("componentImportSources" in shadcn)
1576
+ throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1577
+ const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1578
+ const ui = presetScopes(uiRoots, "**");
1479
1579
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1480
1580
  const safetyFiles = [...appFiles, ...testFiles];
1481
- const components = presetScopes(web, "components/**/*.tsx");
1482
- const hooks = presetScopes(web, "hooks/**/*.ts");
1581
+ const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
1582
+ const hooks = presetScopes([...web, ...packageSources], "hooks/**/*.ts");
1483
1583
  const level = options.severity ?? "error";
1484
1584
  const publicEntrypoints = options.imports?.backendEntryPoints ?? backendRoots.flatMap((directory) => {
1485
1585
  const name = presetPackageName(root, directory);
@@ -1517,13 +1617,17 @@ function presetResolveContext(options) {
1517
1617
  nextWeb,
1518
1618
  backend,
1519
1619
  backendRoots,
1620
+ packageSources,
1621
+ serviceRoots,
1520
1622
  app,
1521
1623
  appFiles,
1522
1624
  webFiles,
1625
+ reactFiles,
1523
1626
  backendFiles,
1524
1627
  services,
1525
1628
  utilities,
1526
1629
  types,
1630
+ uiRoots,
1527
1631
  ui,
1528
1632
  generated,
1529
1633
  testFiles,
@@ -1539,9 +1643,10 @@ function presetResolveContext(options) {
1539
1643
 
1540
1644
  // src/services/preset-compose.ts
1541
1645
  function presetCompose(context, policies, plugins) {
1542
- const { options, level, safetyFiles, ui, generated } = context;
1646
+ const { options, level, app, safetyFiles, ui, generated } = context;
1543
1647
  const severities = options.ruleSeverities ?? {};
1544
1648
  const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1649
+ const maxLines = (max) => levels({ "max-lines": ["error", { max, skipBlankLines: true, skipComments: true }] });
1545
1650
  const overrideList = [];
1546
1651
  for (const name of presetPolicyOrder) {
1547
1652
  const setting = options.policies?.[name];
@@ -1558,9 +1663,9 @@ function presetCompose(context, policies, plugins) {
1558
1663
  }
1559
1664
  }
1560
1665
  overrideList.push(presetOverride(safetyFiles, levels({
1561
- "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1666
+ ...maxLines(options.limits?.maxFileLines ?? 400),
1562
1667
  complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1563
- }), ui));
1668
+ }), ui), presetOverride(presetScopes(app, "**/*.types.ts"), maxLines(options.limits?.maxTypeFileLines ?? 1000), ui));
1564
1669
  const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1565
1670
  const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1566
1671
  for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {