oxlint-plugin-arch 0.10.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,19 @@
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
+
3
17
  ## 0.10.0
4
18
 
5
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.
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);
@@ -747,7 +747,6 @@ function presetTypesConfig(context) {
747
747
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
748
748
  }, [
749
749
  ...presetScopes([...schemaOwners, ...packageSources], "types/**"),
750
- ...presetScopes(packageSources, "**/*.types.ts"),
751
750
  ...colocatedTypes,
752
751
  ...ui
753
752
  ]),
@@ -957,8 +956,8 @@ function presetModulesConfig(context) {
957
956
  "arch/filename-match": [
958
957
  "error",
959
958
  {
960
- pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
961
- 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."
962
961
  }
963
962
  ],
964
963
  "arch/export-file-prefix": "error",
@@ -1109,9 +1108,8 @@ function presetTestsConfig(context) {
1109
1108
 
1110
1109
  // src/configs/react.ts
1111
1110
  function presetReactConfig(context) {
1112
- const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, uiRoots, ui, testFiles, components, css } = context;
1111
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, webUiRoots, ui, testFiles, components, css } = context;
1113
1112
  const policies = {};
1114
- const webUi = uiRoots.filter((path) => web.some((root) => root === "." || path === root || path.startsWith(root + "/")));
1115
1113
  if (reactFiles.length) {
1116
1114
  policies.react = [
1117
1115
  presetOverride(reactFiles, {
@@ -1221,7 +1219,7 @@ function presetReactConfig(context) {
1221
1219
  },
1222
1220
  {
1223
1221
  token: "createContext",
1224
- allowPathPatterns: [...presetScopes(web, "context/"), ...webUi.map((path) => `${path}/`)]
1222
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUiRoots.map((path) => `${path}/`)]
1225
1223
  }
1226
1224
  ]
1227
1225
  }
@@ -1291,7 +1289,7 @@ function presetReactConfig(context) {
1291
1289
  presetOverride(webFiles, {
1292
1290
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1293
1291
  }),
1294
- presetOverride(presetScopes(webUi, "sonner.tsx"), {
1292
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), {
1295
1293
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1296
1294
  })
1297
1295
  ];
@@ -1576,6 +1574,7 @@ function presetResolveContext(options) {
1576
1574
  throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1577
1575
  const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1578
1576
  const ui = presetScopes(uiRoots, "**");
1577
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(webRoot + "/")));
1579
1578
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1580
1579
  const safetyFiles = [...appFiles, ...testFiles];
1581
1580
  const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
@@ -1596,7 +1595,7 @@ function presetResolveContext(options) {
1596
1595
  posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1597
1596
  entry
1598
1597
  ]));
1599
- 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]) => {
1600
1599
  const sourcePath = source(path, entry);
1601
1600
  const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1602
1601
  return [
@@ -1604,6 +1603,22 @@ function presetResolveContext(options) {
1604
1603
  resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1605
1604
  ];
1606
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 };
1607
1622
  return {
1608
1623
  options,
1609
1624
  root,
@@ -1628,6 +1643,7 @@ function presetResolveContext(options) {
1628
1643
  utilities,
1629
1644
  types,
1630
1645
  uiRoots,
1646
+ webUiRoots,
1631
1647
  ui,
1632
1648
  generated,
1633
1649
  testFiles,
@@ -747,7 +747,6 @@ function presetTypesConfig(context) {
747
747
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
748
748
  }, [
749
749
  ...presetScopes([...schemaOwners, ...packageSources], "types/**"),
750
- ...presetScopes(packageSources, "**/*.types.ts"),
751
750
  ...colocatedTypes,
752
751
  ...ui
753
752
  ]),
@@ -957,8 +956,8 @@ function presetModulesConfig(context) {
957
956
  "arch/filename-match": [
958
957
  "error",
959
958
  {
960
- pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
961
- 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."
962
961
  }
963
962
  ],
964
963
  "arch/export-file-prefix": "error",
@@ -1109,9 +1108,8 @@ function presetTestsConfig(context) {
1109
1108
 
1110
1109
  // src/configs/react.ts
1111
1110
  function presetReactConfig(context) {
1112
- const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, uiRoots, ui, testFiles, components, css } = context;
1111
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, webUiRoots, ui, testFiles, components, css } = context;
1113
1112
  const policies = {};
1114
- const webUi = uiRoots.filter((path) => web.some((root) => root === "." || path === root || path.startsWith(root + "/")));
1115
1113
  if (reactFiles.length) {
1116
1114
  policies.react = [
1117
1115
  presetOverride(reactFiles, {
@@ -1221,7 +1219,7 @@ function presetReactConfig(context) {
1221
1219
  },
1222
1220
  {
1223
1221
  token: "createContext",
1224
- allowPathPatterns: [...presetScopes(web, "context/"), ...webUi.map((path) => `${path}/`)]
1222
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUiRoots.map((path) => `${path}/`)]
1225
1223
  }
1226
1224
  ]
1227
1225
  }
@@ -1291,7 +1289,7 @@ function presetReactConfig(context) {
1291
1289
  presetOverride(webFiles, {
1292
1290
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1293
1291
  }),
1294
- presetOverride(presetScopes(webUi, "sonner.tsx"), {
1292
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), {
1295
1293
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1296
1294
  })
1297
1295
  ];
@@ -1576,6 +1574,7 @@ function presetResolveContext(options) {
1576
1574
  throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1577
1575
  const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1578
1576
  const ui = presetScopes(uiRoots, "**");
1577
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(webRoot + "/")));
1579
1578
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1580
1579
  const safetyFiles = [...appFiles, ...testFiles];
1581
1580
  const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
@@ -1596,7 +1595,7 @@ function presetResolveContext(options) {
1596
1595
  posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1597
1596
  entry
1598
1597
  ]));
1599
- 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]) => {
1600
1599
  const sourcePath = source(path, entry);
1601
1600
  const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1602
1601
  return [
@@ -1604,6 +1603,22 @@ function presetResolveContext(options) {
1604
1603
  resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1605
1604
  ];
1606
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 };
1607
1622
  return {
1608
1623
  options,
1609
1624
  root,
@@ -1628,6 +1643,7 @@ function presetResolveContext(options) {
1628
1643
  utilities,
1629
1644
  types,
1630
1645
  uiRoots,
1646
+ webUiRoots,
1631
1647
  ui,
1632
1648
  generated,
1633
1649
  testFiles,
@@ -747,7 +747,6 @@ function presetTypesConfig(context) {
747
747
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
748
748
  }, [
749
749
  ...presetScopes([...schemaOwners, ...packageSources], "types/**"),
750
- ...presetScopes(packageSources, "**/*.types.ts"),
751
750
  ...colocatedTypes,
752
751
  ...ui
753
752
  ]),
@@ -957,8 +956,8 @@ function presetModulesConfig(context) {
957
956
  "arch/filename-match": [
958
957
  "error",
959
958
  {
960
- pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
961
- 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."
962
961
  }
963
962
  ],
964
963
  "arch/export-file-prefix": "error",
@@ -1109,9 +1108,8 @@ function presetTestsConfig(context) {
1109
1108
 
1110
1109
  // src/configs/react.ts
1111
1110
  function presetReactConfig(context) {
1112
- const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, uiRoots, ui, testFiles, components, css } = context;
1111
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, webUiRoots, ui, testFiles, components, css } = context;
1113
1112
  const policies = {};
1114
- const webUi = uiRoots.filter((path) => web.some((root) => root === "." || path === root || path.startsWith(root + "/")));
1115
1113
  if (reactFiles.length) {
1116
1114
  policies.react = [
1117
1115
  presetOverride(reactFiles, {
@@ -1221,7 +1219,7 @@ function presetReactConfig(context) {
1221
1219
  },
1222
1220
  {
1223
1221
  token: "createContext",
1224
- allowPathPatterns: [...presetScopes(web, "context/"), ...webUi.map((path) => `${path}/`)]
1222
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUiRoots.map((path) => `${path}/`)]
1225
1223
  }
1226
1224
  ]
1227
1225
  }
@@ -1291,7 +1289,7 @@ function presetReactConfig(context) {
1291
1289
  presetOverride(webFiles, {
1292
1290
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1293
1291
  }),
1294
- presetOverride(presetScopes(webUi, "sonner.tsx"), {
1292
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), {
1295
1293
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1296
1294
  })
1297
1295
  ];
@@ -1576,6 +1574,7 @@ function presetResolveContext(options) {
1576
1574
  throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1577
1575
  const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1578
1576
  const ui = presetScopes(uiRoots, "**");
1577
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(webRoot + "/")));
1579
1578
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1580
1579
  const safetyFiles = [...appFiles, ...testFiles];
1581
1580
  const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
@@ -1596,7 +1595,7 @@ function presetResolveContext(options) {
1596
1595
  posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1597
1596
  entry
1598
1597
  ]));
1599
- 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]) => {
1600
1599
  const sourcePath = source(path, entry);
1601
1600
  const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1602
1601
  return [
@@ -1604,6 +1603,22 @@ function presetResolveContext(options) {
1604
1603
  resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1605
1604
  ];
1606
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 };
1607
1622
  return {
1608
1623
  options,
1609
1624
  root,
@@ -1628,6 +1643,7 @@ function presetResolveContext(options) {
1628
1643
  utilities,
1629
1644
  types,
1630
1645
  uiRoots,
1646
+ webUiRoots,
1631
1647
  ui,
1632
1648
  generated,
1633
1649
  testFiles,
@@ -747,7 +747,6 @@ function presetTypesConfig(context) {
747
747
  "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
748
748
  }, [
749
749
  ...presetScopes([...schemaOwners, ...packageSources], "types/**"),
750
- ...presetScopes(packageSources, "**/*.types.ts"),
751
750
  ...colocatedTypes,
752
751
  ...ui
753
752
  ]),
@@ -957,8 +956,8 @@ function presetModulesConfig(context) {
957
956
  "arch/filename-match": [
958
957
  "error",
959
958
  {
960
- pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
961
- 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."
962
961
  }
963
962
  ],
964
963
  "arch/export-file-prefix": "error",
@@ -1109,9 +1108,8 @@ function presetTestsConfig(context) {
1109
1108
 
1110
1109
  // src/configs/react.ts
1111
1110
  function presetReactConfig(context) {
1112
- const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, uiRoots, ui, testFiles, components, css } = context;
1111
+ const { options, web, tanstackWeb, nextWeb, webFiles, reactFiles, webUiRoots, ui, testFiles, components, css } = context;
1113
1112
  const policies = {};
1114
- const webUi = uiRoots.filter((path) => web.some((root) => root === "." || path === root || path.startsWith(root + "/")));
1115
1113
  if (reactFiles.length) {
1116
1114
  policies.react = [
1117
1115
  presetOverride(reactFiles, {
@@ -1221,7 +1219,7 @@ function presetReactConfig(context) {
1221
1219
  },
1222
1220
  {
1223
1221
  token: "createContext",
1224
- allowPathPatterns: [...presetScopes(web, "context/"), ...webUi.map((path) => `${path}/`)]
1222
+ allowPathPatterns: [...presetScopes(web, "context/"), ...webUiRoots.map((path) => `${path}/`)]
1225
1223
  }
1226
1224
  ]
1227
1225
  }
@@ -1291,7 +1289,7 @@ function presetReactConfig(context) {
1291
1289
  presetOverride(webFiles, {
1292
1290
  "shadcn/no-unknown-classes": ["error", { allow: [] }]
1293
1291
  }),
1294
- presetOverride(presetScopes(webUi, "sonner.tsx"), {
1292
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), {
1295
1293
  "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1296
1294
  })
1297
1295
  ];
@@ -1576,6 +1574,7 @@ function presetResolveContext(options) {
1576
1574
  throw new Error("shadcn.componentImportSources was renamed to shadcn.components.");
1577
1575
  const uiRoots = shadcn.ui === undefined ? web.map((path) => posix2.join(path, "components/ui")) : resolveUi(shadcn.ui);
1578
1576
  const ui = presetScopes(uiRoots, "**");
1577
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(webRoot + "/")));
1579
1578
  const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1580
1579
  const safetyFiles = [...appFiles, ...testFiles];
1581
1580
  const components = presetScopes([...web, ...packageSources], "components/**/*.tsx");
@@ -1596,7 +1595,7 @@ function presetResolveContext(options) {
1596
1595
  posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1597
1596
  entry
1598
1597
  ]));
1599
- 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]) => {
1600
1599
  const sourcePath = source(path, entry);
1601
1600
  const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1602
1601
  return [
@@ -1604,6 +1603,22 @@ function presetResolveContext(options) {
1604
1603
  resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1605
1604
  ];
1606
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 };
1607
1622
  return {
1608
1623
  options,
1609
1624
  root,
@@ -1628,6 +1643,7 @@ function presetResolveContext(options) {
1628
1643
  utilities,
1629
1644
  types,
1630
1645
  uiRoots,
1646
+ webUiRoots,
1631
1647
  ui,
1632
1648
  generated,
1633
1649
  testFiles,
@@ -2195,6 +2195,46 @@ var nextRuntime = defineRule11({
2195
2195
  }
2196
2196
  });
2197
2197
 
2198
+ // src/rules/module-policy/no-unknown.ts
2199
+ import { defineRule as defineRule12 } from "@oxlint/plugins";
2200
+ var noUnknown = defineRule12({
2201
+ meta: { type: "problem", schema: [], messages: { forbidden: "Use an owned domain type rather than unknown." } },
2202
+ createOnce(context) {
2203
+ function catchName(node) {
2204
+ const key = "key" in node && !(("computed" in node) && node.computed) ? node.key : undefined;
2205
+ return key?.type === "Identifier" && key.name === "catch";
2206
+ }
2207
+ function allowed(node) {
2208
+ const annotation = node.parent;
2209
+ const parameter = annotation?.type === "TSTypeAnnotation" ? annotation.parent : undefined;
2210
+ if (parameter?.type !== "Identifier")
2211
+ return false;
2212
+ const owner = parameter.parent;
2213
+ if (!owner || !("params" in owner) || !owner.params.some((entry) => entry === parameter))
2214
+ return false;
2215
+ const predicate = "returnType" in owner ? owner.returnType?.typeAnnotation : undefined;
2216
+ if (predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier")
2217
+ return predicate.parameterName.name === parameter.name;
2218
+ if (owner.params[0] !== parameter)
2219
+ return false;
2220
+ const holder = owner.parent;
2221
+ if (!holder)
2222
+ return false;
2223
+ if (catchName(owner) || catchName(holder))
2224
+ return true;
2225
+ if (holder.type === "TSTypeAnnotation" && holder.parent && catchName(holder.parent))
2226
+ return true;
2227
+ return holder.type === "CallExpression" && holder.arguments.some((entry) => entry === owner) && holder.callee.type === "MemberExpression" && !holder.callee.computed && holder.callee.property.type === "Identifier" && holder.callee.property.name === "catch";
2228
+ }
2229
+ return {
2230
+ TSUnknownKeyword(node) {
2231
+ if (!allowed(node))
2232
+ context.report({ node, messageId: "forbidden" });
2233
+ }
2234
+ };
2235
+ }
2236
+ });
2237
+
2198
2238
  // src/rules/module-policy/index.ts
2199
2239
  var module_policy_default = eslintCompatPlugin({
2200
2240
  meta: { name: "modules" },
@@ -2209,7 +2249,7 @@ var module_policy_default = eslintCompatPlugin({
2209
2249
  "import-boundaries": importBoundaries,
2210
2250
  "tanstack-runtime": tanstackRuntime,
2211
2251
  "local-type-alias": syntaxRule("TSTypeAliasDeclaration", "Local component/hook contracts use interfaces named for their owner. Derived or reusable aliases belong in domain types; do not substitute an empty interface."),
2212
- "no-unknown": syntaxRule("TSUnknownKeyword", "Use an owned domain type rather than unknown."),
2252
+ "no-unknown": noUnknown,
2213
2253
  memoization,
2214
2254
  "next-runtime": nextRuntime,
2215
2255
  "pascal-interface": syntaxRule("TSInterfaceDeclaration:not([id.name=/^[A-Z][a-zA-Z0-9]*$/])", "Use PascalCase for interfaces."),
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Rejects the `unknown` type except on the narrowed parameter of a type guard or the error parameter of a catch callback.
3
+ *
4
+ * Example: `type Value = unknown` fails; `function isUser(value: unknown): value is User` passes.
5
+ */
6
+ export declare const noUnknown: import("@oxlint/plugins").Rule;
@@ -175,6 +175,8 @@ export interface PresetContext {
175
175
  types: string[];
176
176
  /** UI kit folders resolved from `shadcn.ui`. */
177
177
  uiRoots: string[];
178
+ /** UI kit folders inside a web source. */
179
+ webUiRoots: string[];
178
180
  ui: string[];
179
181
  generated: string[];
180
182
  testFiles: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-arch",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Configurable architecture rules for Oxlint JavaScript plugins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",