oxlint-plugin-arch 0.9.0 → 0.11.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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - `modules/no-unknown` allows `unknown` on the narrowed parameter of a type guard (`function isAppError(error: unknown): error is AppError`) and on the error parameter of a catch callback (`catch: (error: unknown) => E`, `.catch((error: unknown) => …)`). Other uses still fail. This applies to every root.
6
+ - `arch/no-trivial-functions` no longer reports type guards with a predicate return type as pass-through wrappers. Banned names and structurally generic guards are still reported.
7
+ - Package schemas: construction is allowed only in `types/**`. Package `*.types.ts` files outside `types/` are reported again (fix: move them to `src/types/`), matching `arch/no-restricted-files`.
8
+ - Hook filenames accept a React Native platform suffix: `use-chat-draft.web.ts`, `.native.ts`, `.ios.ts` and `.android.ts`. This applies to web roots and packages.
9
+ - A UI kit outside every web root, resolved from `shadcn.ui`, gets the Tailwind checks. Its stylesheet is `tailwind.cssEntryPointsByRoot` for the kit's package folder or architecture root, otherwise the one stylesheet the web roots share. With several web stylesheets and no entry, the preset throws and names `cssEntryPointsByRoot`.
10
+
11
+ | Fix | Rules |
12
+ | --- | --- |
13
+ | Auto-fix (`--fix`) | `tailwindcss/*` sort, canonical, shorthand and whitespace rules on a kit package |
14
+ | Move | `arch/no-local-schema-construction` in package `*.types.ts` outside `types/` (to `src/types/`) |
15
+ | Reshape | `tailwindcss/no-conflicting-classes`, `tailwindcss/no-deprecated-classes` on a kit package |
16
+
17
+ ## 0.10.0
18
+
19
+ **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.
20
+
21
+ - 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.
22
+ - 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`.
23
+ - 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.
24
+ - 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.
25
+ - Package `types/**` and `*.types.ts` files may construct schemas, like backend `types/**`.
26
+ - 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.
27
+ - 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' }`.
28
+ - `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.
29
+
30
+ How to fix what the new rules report:
31
+
32
+ | Fix | Rules |
33
+ | --- | --- |
34
+ | Auto-fix (`--fix`) | `arch/no-comments`, `arch/jsx-attributes-multiline`, the `react-extra/jsx-*` layout rules |
35
+ | 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` |
36
+ | 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` |
37
+ | 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` |
38
+
39
+ To defer a package, turn single rules off for it: `ruleExclusions: { '<rule>': ['packages/foo/**'] }`.
40
+
3
41
  ## 0.9.0
4
42
 
5
43
  - 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`.
package/dist/index.js CHANGED
@@ -4089,7 +4089,7 @@ var noTrivialFunctions = defineRule32({
4089
4089
  }
4090
4090
  seen.add(key);
4091
4091
  checkHelper(item.node);
4092
- if (reported.has(item.node.start))
4092
+ if (reported.has(item.node.start) || item.node.returnType?.typeAnnotation.type === "TSTypePredicate")
4093
4093
  continue;
4094
4094
  const expression = trivialExpression(item.node);
4095
4095
  const trivial2 = mode === "legacy" ? declarationsIsTrivialFunction(item.node) : expression ? trivialIsForwarder(item.node, expression) : declarationsIsTrivialFunction(item.node);
@@ -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,12 @@ 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
+ ...colocatedTypes,
751
+ ...ui
752
+ ]),
753
+ presetOverride([...webFiles, ...presetScopes(packageSources)], {
689
754
  "arch/no-rederive-schema": [
690
755
  "error",
691
756
  {
@@ -695,7 +760,7 @@ function presetTypesConfig(context) {
695
760
  }
696
761
  ]
697
762
  }),
698
- presetOverride(presetScopes(backend, "services/**/*.ts"), {
763
+ presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), {
699
764
  "arch/no-unescaped-like": [
700
765
  "error",
701
766
  {
@@ -725,7 +790,7 @@ function presetTypesConfig(context) {
725
790
  // src/configs/modules.ts
726
791
  import { posix } from "node:path";
727
792
  function presetModulesConfig(context) {
728
- const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
793
+ const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, components, hooks, options } = context;
729
794
  const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
730
795
  const migrations = options.modules?.migrationFiles ?? [];
731
796
  const frontendEntries = [
@@ -822,7 +887,7 @@ function presetModulesConfig(context) {
822
887
  }
823
888
  ]
824
889
  }, testFiles),
825
- presetOverride(presetScopes(backend, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
890
+ presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
826
891
  ...utilities,
827
892
  ...testFiles
828
893
  ]),
@@ -832,11 +897,11 @@ function presetModulesConfig(context) {
832
897
  presetOverride(frontendEntries, {
833
898
  "modules/domain-constants": ["error", { includeData: true, allowServiceMethods: true }]
834
899
  }, testFiles),
835
- presetOverride([...presetScopes(backend, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
900
+ presetOverride([...presetScopes(serviceRoots, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
836
901
  presetOverride(frontendEntries, {
837
902
  "arch/require-object-params": ["error", { maxParams: 2, serviceMethods: true }]
838
903
  }, testFiles),
839
- presetOverride(presetScopes(backend, "services/**/*.service.ts"), {
904
+ presetOverride(presetScopes(serviceRoots, "services/**/*.service.ts"), {
840
905
  "arch/no-extra-exports": [
841
906
  "error",
842
907
  {
@@ -864,7 +929,7 @@ function presetModulesConfig(context) {
864
929
  }
865
930
  ]
866
931
  }, ["**/*.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")]),
932
+ presetOverride(presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/export-file-prefix": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]),
868
933
  presetOverride(frontendEntries, {
869
934
  "arch/export-file-prefix": [
870
935
  "error",
@@ -891,13 +956,13 @@ function presetModulesConfig(context) {
891
956
  "arch/filename-match": [
892
957
  "error",
893
958
  {
894
- pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
895
- message: "Hooks use use-domain.ts filenames."
959
+ pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:web|native|ios|android))?[.]ts$",
960
+ message: "Hooks use use-domain.ts filenames, with an optional .web, .native, .ios or .android platform suffix."
896
961
  }
897
962
  ],
898
963
  "arch/export-file-prefix": "error",
899
964
  "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
900
- }),
965
+ }, ui),
901
966
  presetOverride(components, {
902
967
  "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
903
968
  }, [...ui, ...testFiles]),
@@ -1043,11 +1108,11 @@ function presetTestsConfig(context) {
1043
1108
 
1044
1109
  // src/configs/react.ts
1045
1110
  function presetReactConfig(context) {
1046
- const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
1111
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, webUiRoots, ui, testFiles, components, css } = context;
1047
1112
  const policies = {};
1048
- if (web.length) {
1113
+ if (reactFiles.length) {
1049
1114
  policies.react = [
1050
- presetOverride(webFiles, {
1115
+ presetOverride(reactFiles, {
1051
1116
  ...presetReactRules,
1052
1117
  ...options.react?.compiler === false ? {
1053
1118
  "react-perf/jsx-no-new-object-as-prop": "error",
@@ -1062,8 +1127,8 @@ function presetReactConfig(context) {
1062
1127
  }, [...ui, ...testFiles])
1063
1128
  ];
1064
1129
  }
1065
- if (web.length) {
1066
- policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
1130
+ if (reactFiles.length) {
1131
+ policies.effects = [presetOverride(reactFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
1067
1132
  }
1068
1133
  const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
1069
1134
  const routes = [
@@ -1125,9 +1190,9 @@ function presetReactConfig(context) {
1125
1190
  if (routes.length) {
1126
1191
  policies.routes = routes;
1127
1192
  }
1128
- if (web.length) {
1193
+ if (reactFiles.length) {
1129
1194
  policies.forms = [
1130
- presetOverride(webFiles, {
1195
+ presetOverride(reactFiles, {
1131
1196
  "arch/require-paired-call": [
1132
1197
  "error",
1133
1198
  {
@@ -1138,8 +1203,8 @@ function presetReactConfig(context) {
1138
1203
  }, ui)
1139
1204
  ];
1140
1205
  }
1141
- if (web.length && options.react?.compiler !== false) {
1142
- policies.memoization = [presetOverride(webFiles, { "modules/memoization": "error" }, ui)];
1206
+ if (reactFiles.length && options.react?.compiler !== false) {
1207
+ policies.memoization = [presetOverride(reactFiles, { "modules/memoization": "error" }, ui)];
1143
1208
  }
1144
1209
  if (web.length) {
1145
1210
  policies.rpcClientOwnership = [
@@ -1154,7 +1219,7 @@ function presetReactConfig(context) {
1154
1219
  },
1155
1220
  {
1156
1221
  token: "createContext",
1157
- allowPathPatterns: [...presetScopes(web, "context/"), ...presetScopes(web, "components/ui/")]
1222
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUiRoots.map((path) => `${path}/`)]
1158
1223
  }
1159
1224
  ]
1160
1225
  }
@@ -1224,7 +1289,7 @@ function presetReactConfig(context) {
1224
1289
  presetOverride(webFiles, {
1225
1290
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1226
1291
  }),
1227
- presetOverride(presetScopes(web, "components/ui/sonner.tsx"), {
1292
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), {
1228
1293
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1229
1294
  })
1230
1295
  ];
@@ -1307,7 +1372,7 @@ function presetBoundariesConfig(context) {
1307
1372
  import { createRequire } from "node:module";
1308
1373
  var require2 = createRequire(import.meta.url);
1309
1374
  function presetPluginsConfig(context) {
1310
- const { options, web, css } = context;
1375
+ const { options, reactFiles, css } = context;
1311
1376
  const jsPlugins = [
1312
1377
  { name: "eslint-js", specifier: require2.resolve("oxlint-plugin-eslint") },
1313
1378
  {
@@ -1336,7 +1401,7 @@ function presetPluginsConfig(context) {
1336
1401
  react: { version: "19" },
1337
1402
  "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
1338
1403
  };
1339
- if (web.length) {
1404
+ if (reactFiles.length) {
1340
1405
  jsPlugins.push({
1341
1406
  name: "react-extra",
1342
1407
  specifier: require2.resolve("eslint-plugin-react")
@@ -1348,8 +1413,8 @@ function presetPluginsConfig(context) {
1348
1413
  if (options.shadcn !== false) {
1349
1414
  jsPlugins.push(require2.resolve("@shadcn/lint"));
1350
1415
  settings.shadcn = {
1351
- ui: options.shadcn?.uiImportPath ?? "@/components/ui",
1352
- componentImports: options.shadcn?.componentImportSources ?? ["^@/components/"]
1416
+ ui: options.shadcn?.ui ?? "@/components/ui",
1417
+ componentImports: options.shadcn?.components ?? ["^@/components/"]
1353
1418
  };
1354
1419
  }
1355
1420
  }
@@ -1357,6 +1422,7 @@ function presetPluginsConfig(context) {
1357
1422
  }
1358
1423
 
1359
1424
  // src/services/preset-context.ts
1425
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
1360
1426
  import { resolve as resolve3, posix as posix2 } from "node:path";
1361
1427
  function presetResolveContext(options) {
1362
1428
  const generated = presetGeneratedFiles;
@@ -1396,7 +1462,7 @@ function presetResolveContext(options) {
1396
1462
  if (entry.layout !== undefined && (!entry.layout || typeof entry.layout !== "object" || Array.isArray(entry.layout)))
1397
1463
  throw new Error("Folder layout must be a path-to-mode object.");
1398
1464
  const layout = {};
1399
- if (role === "web" || role === "server" || role === "runner") {
1465
+ if (role !== "scripts") {
1400
1466
  layout.services = "domain";
1401
1467
  }
1402
1468
  if (role === "web") {
@@ -1453,6 +1519,8 @@ function presetResolveContext(options) {
1453
1519
  if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
1454
1520
  throw new Error(`Unknown framework: ${options.framework}`);
1455
1521
  const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
1522
+ const isPackage = (path) => existsSync2(resolve3(root, path, "package.json"));
1523
+ const packageSource = (path) => posix2.join(path, isPackage(path) ? "src" : "*/src");
1456
1524
  const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
1457
1525
  const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
1458
1526
  const tanstackWeb = webFor("tanstack-start");
@@ -1461,25 +1529,56 @@ function presetResolveContext(options) {
1461
1529
  const backendRoots = backendEntries.map(([path]) => path);
1462
1530
  const backend = backendEntries.map(([path, entry]) => source(path, entry));
1463
1531
  const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1532
+ const packageSources = packages.map(packageSource);
1533
+ const serviceRoots = [...backend, ...packageSources];
1464
1534
  const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1465
1535
  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),
1536
+ path: posix2.join(entry.role === "scripts" ? path : entry.role === "packages" ? packageSource(path) : source(path, entry), folder),
1467
1537
  folder,
1468
1538
  mode
1469
1539
  })));
1470
1540
  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];
1541
+ const app = [...web, ...backend, ...packageSources];
1472
1542
  const appFiles = presetScopes(app);
1473
1543
  const webFiles = presetScopes(web);
1544
+ const reactFiles = [
1545
+ ...webFiles,
1546
+ ...presetScopes(packageSources, "**/*.tsx"),
1547
+ ...presetScopes(packageSources, `components/**/${presetExtensions}`),
1548
+ ...presetScopes(packageSources, `hooks/**/${presetExtensions}`)
1549
+ ];
1474
1550
  const backendFiles = presetScopes(backend);
1475
1551
  const services = presetScopes(app, `services/**/${presetExtensions}`);
1476
1552
  const utilities = ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"];
1477
1553
  const types = presetScopes(app, "types/**");
1478
- const ui = presetScopes(web, "components/ui/**");
1554
+ const resolveUi = (value) => {
1555
+ if (typeof value !== "string" || !value)
1556
+ throw new Error("shadcn.ui must be a nonempty import path.");
1557
+ const manifests = entries.flatMap(([path, entry]) => {
1558
+ 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") })) : [];
1559
+ const sources = ["web", "server", "runner"].includes(entry.role) ? source(path, entry) : posix2.join(path, "src");
1560
+ return [{ directory: path, sources }, ...children];
1561
+ });
1562
+ const named = manifests.filter(({ directory }) => presetPackageName(root, directory) === value);
1563
+ if (named.length)
1564
+ return named.map(({ sources }) => sources);
1565
+ const aliased = [...new Set([".", ...manifests.map(({ directory }) => directory)].flatMap((directory) => presetTsconfigAliasTargets(root, directory, value)))];
1566
+ if (aliased.length)
1567
+ return aliased;
1568
+ throw new Error(`shadcn.ui ${JSON.stringify(value)} matches no workspace package name or existing tsconfig paths alias target.`);
1569
+ };
1570
+ const shadcn = options.shadcn || {};
1571
+ if ("uiImportPath" in shadcn)
1572
+ throw new Error("shadcn.uiImportPath was renamed to shadcn.ui.");
1573
+ if ("componentImportSources" in shadcn)
1574
+ throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1575
+ const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1576
+ const ui = presetScopes(uiRoots, "**");
1577
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(webRoot + "/")));
1479
1578
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1480
1579
  const safetyFiles = [...appFiles, ...testFiles];
1481
- const components = presetScopes(web, "components/**/*.tsx");
1482
- const hooks = presetScopes(web, "hooks/**/*.ts");
1580
+ const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
1581
+ const hooks = presetScopes([...web, ...packageSources], "hooks/**/*.ts");
1483
1582
  const level = options.severity ?? "error";
1484
1583
  const publicEntrypoints = options.imports?.backendEntryPoints ?? backendRoots.flatMap((directory) => {
1485
1584
  const name = presetPackageName(root, directory);
@@ -1496,7 +1595,7 @@ function presetResolveContext(options) {
1496
1595
  posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1497
1596
  entry
1498
1597
  ]));
1499
- const css = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
1598
+ const webCss = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
1500
1599
  const sourcePath = source(path, entry);
1501
1600
  const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1502
1601
  return [
@@ -1504,6 +1603,22 @@ function presetResolveContext(options) {
1504
1603
  resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1505
1604
  ];
1506
1605
  }));
1606
+ const webStylesheets = [...new Set(Object.values(webCss))];
1607
+ const kitCss = Object.fromEntries((tailwind === false ? [] : uiRoots.filter((path) => !webUiRoots.includes(path))).map((path) => {
1608
+ const owners = packages.flatMap((packageRoot) => {
1609
+ if (path !== packageRoot && !path.startsWith(packageRoot + "/"))
1610
+ return [];
1611
+ const child = path.slice(packageRoot.length + 1).split("/")[0];
1612
+ return isPackage(packageRoot) || !child ? [packageRoot] : [posix2.join(packageRoot, child), packageRoot];
1613
+ });
1614
+ const entry = owners.map((owner) => entryPoints[owner]).find(Boolean);
1615
+ if (entry)
1616
+ return [path, resolve3(root, entry)];
1617
+ if (webStylesheets.length === 1)
1618
+ return [path, webStylesheets[0]];
1619
+ throw new Error(`The UI kit ${path} needs a Tailwind stylesheet: set tailwind.cssEntryPointsByRoot for ${owners[0] ?? path}.`);
1620
+ }));
1621
+ const css = { ...webCss, ...kitCss };
1507
1622
  return {
1508
1623
  options,
1509
1624
  root,
@@ -1517,13 +1632,18 @@ function presetResolveContext(options) {
1517
1632
  nextWeb,
1518
1633
  backend,
1519
1634
  backendRoots,
1635
+ packageSources,
1636
+ serviceRoots,
1520
1637
  app,
1521
1638
  appFiles,
1522
1639
  webFiles,
1640
+ reactFiles,
1523
1641
  backendFiles,
1524
1642
  services,
1525
1643
  utilities,
1526
1644
  types,
1645
+ uiRoots,
1646
+ webUiRoots,
1527
1647
  ui,
1528
1648
  generated,
1529
1649
  testFiles,