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 +38 -0
- package/dist/index.js +1 -1
- package/dist/presets/index.js +156 -36
- package/dist/presets/next-react-modules-preset.js +156 -36
- package/dist/presets/react-modules-preset.js +156 -36
- package/dist/presets/tanstack-start-react-modules-preset.js +156 -36
- package/dist/rules/module-policy/index.js +41 -1
- package/dist/rules/module-policy/no-unknown.d.ts +6 -0
- package/dist/types/preset.types.d.ts +14 -2
- package/dist/utils/helpers/preset.d.ts +2 -0
- package/package.json +1 -1
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);
|
package/dist/presets/index.js
CHANGED
|
@@ -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([
|
|
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
|
-
}, [
|
|
688
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
1113
|
+
if (reactFiles.length) {
|
|
1049
1114
|
policies.react = [
|
|
1050
|
-
presetOverride(
|
|
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 (
|
|
1066
|
-
policies.effects = [presetOverride(
|
|
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 (
|
|
1193
|
+
if (reactFiles.length) {
|
|
1129
1194
|
policies.forms = [
|
|
1130
|
-
presetOverride(
|
|
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 (
|
|
1142
|
-
policies.memoization = [presetOverride(
|
|
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/"), ...
|
|
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(
|
|
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,
|
|
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 (
|
|
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?.
|
|
1352
|
-
componentImports: options.shadcn?.
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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,
|