oxlint-plugin-arch 0.6.3 → 0.8.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 +20 -0
- package/README.md +102 -526
- package/dist/constants/preset.constants.d.ts +11 -1
- package/dist/index.js +88 -11
- package/dist/presets/index.d.ts +3 -1
- package/dist/presets/index.js +287 -71
- package/dist/presets/next-react-modules-preset.d.ts +4 -0
- package/dist/presets/next-react-modules-preset.js +1608 -0
- package/dist/presets/react-modules-preset.d.ts +4 -0
- package/dist/presets/react-modules-preset.js +1603 -0
- package/dist/presets/tanstack-start-react-modules-preset.d.ts +1 -0
- package/dist/presets/tanstack-start-react-modules-preset.js +279 -70
- package/dist/rules/dillon-anti-slop/index.js +1155 -24
- package/dist/rules/dillon-anti-slop/rules/no-module-mocking.d.ts +7 -0
- package/dist/rules/module-policy/index.js +207 -34
- package/dist/rules/module-policy/next-runtime.d.ts +7 -0
- package/dist/types/module-policy.types.d.ts +5 -0
- package/dist/types/preset.types.d.ts +48 -0
- package/dist/utils/helpers/declarations.d.ts +1 -1
- package/dist/utils/helpers/index.d.ts +2 -0
- package/dist/utils/helpers/layout.d.ts +1 -1
- package/dist/utils/helpers/mocking.d.ts +2 -0
- package/dist/utils/helpers/naming.d.ts +1 -1
- package/dist/utils/helpers/preset.d.ts +1 -0
- package/dist/utils/helpers/runtime.d.ts +14 -0
- package/package.json +29 -20
- package/src/rules/dillon-anti-slop/UPSTREAM.md +6 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { OxlintConfig } from 'oxlint';
|
|
2
2
|
import type { PresetOptions } from '../types/preset.types.ts';
|
|
3
|
+
/** The React modules preset with TanStack Start as the default web framework. */
|
|
3
4
|
export default function tanstackStartReactModulesPreset(options?: PresetOptions): OxlintConfig;
|
|
@@ -418,6 +418,34 @@ var presetTestRules = {
|
|
|
418
418
|
"vitest/no-conditional-expect": "error",
|
|
419
419
|
"vitest/require-mock-type-parameters": "error"
|
|
420
420
|
};
|
|
421
|
+
var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
|
|
422
|
+
var presetTestMockingApis = {
|
|
423
|
+
modules: presetModuleMockingApis,
|
|
424
|
+
strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
|
|
425
|
+
};
|
|
426
|
+
var presetTestMockingAllow = {
|
|
427
|
+
modules: [],
|
|
428
|
+
strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
|
|
429
|
+
};
|
|
430
|
+
var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
|
|
431
|
+
var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
432
|
+
var presetNextSegmentConfig = [
|
|
433
|
+
"generateStaticParams",
|
|
434
|
+
"dynamic",
|
|
435
|
+
"dynamicParams",
|
|
436
|
+
"revalidate",
|
|
437
|
+
"fetchCache",
|
|
438
|
+
"runtime",
|
|
439
|
+
"preferredRegion",
|
|
440
|
+
"maxDuration"
|
|
441
|
+
];
|
|
442
|
+
var presetNextSegmentExports = [
|
|
443
|
+
"generateMetadata",
|
|
444
|
+
"metadata",
|
|
445
|
+
"generateViewport",
|
|
446
|
+
"viewport",
|
|
447
|
+
...presetNextSegmentConfig
|
|
448
|
+
];
|
|
421
449
|
var presetExportPrefix = {
|
|
422
450
|
stem: "before-first-dot",
|
|
423
451
|
trailingRoles: ["constants", "utils", "handler", "service", "types"],
|
|
@@ -436,7 +464,7 @@ var presetGeneratedFiles = [
|
|
|
436
464
|
|
|
437
465
|
// src/utils/helpers/preset.ts
|
|
438
466
|
function presetScopes(roots, suffix = `**/${presetExtensions}`) {
|
|
439
|
-
return roots.map((root) => `${root}/${suffix}`);
|
|
467
|
+
return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
|
|
440
468
|
}
|
|
441
469
|
function presetOverride(files, rules, excludeFiles = []) {
|
|
442
470
|
return {
|
|
@@ -445,6 +473,18 @@ function presetOverride(files, rules, excludeFiles = []) {
|
|
|
445
473
|
rules
|
|
446
474
|
};
|
|
447
475
|
}
|
|
476
|
+
function presetRuleSeverities(rules, severities) {
|
|
477
|
+
const result = structuredClone(rules);
|
|
478
|
+
for (const [name, level] of Object.entries(severities)) {
|
|
479
|
+
const value = result[name];
|
|
480
|
+
const current = Array.isArray(value) ? value[0] : value;
|
|
481
|
+
if (value === undefined || current === "off" || current === "allow" || current === 0) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
result[name] = Array.isArray(value) && level !== "off" ? [level, ...value.slice(1)] : level;
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
448
488
|
function presetPackageName(root, directory) {
|
|
449
489
|
try {
|
|
450
490
|
const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
|
|
@@ -469,7 +509,14 @@ function presetSeverity(rules, level) {
|
|
|
469
509
|
function presetSourceConfig(context) {
|
|
470
510
|
const { options, web, scripts, app, appFiles, backendFiles, ui, safetyFiles, publicEntrypoints, baseImports } = context;
|
|
471
511
|
const policies = {};
|
|
472
|
-
|
|
512
|
+
const { semi = "always", quotes = "single" } = options.formatting ?? {};
|
|
513
|
+
policies.formatting = [
|
|
514
|
+
presetOverride([`**/${presetExtensions}`], {
|
|
515
|
+
...presetFormattingRules,
|
|
516
|
+
"eslint-js/semi": ["error", semi],
|
|
517
|
+
"eslint-js/quotes": ["error", quotes, { avoidEscape: true }]
|
|
518
|
+
})
|
|
519
|
+
];
|
|
473
520
|
policies.statementLayout = [
|
|
474
521
|
presetOverride([`**/${presetExtensions}`], {
|
|
475
522
|
"arch/padding-between-statements": ["error", { multilineVariables: true }],
|
|
@@ -555,12 +602,18 @@ function presetTypesConfig(context) {
|
|
|
555
602
|
safetyFiles,
|
|
556
603
|
components,
|
|
557
604
|
hooks,
|
|
558
|
-
publicEntrypoints
|
|
605
|
+
publicEntrypoints,
|
|
606
|
+
tanstackWeb,
|
|
607
|
+
nextWeb
|
|
559
608
|
} = context;
|
|
609
|
+
const schemaOwners = backend.length ? backend : nextWeb;
|
|
610
|
+
const runtimeFreeTypes = backend.length ? web : tanstackWeb;
|
|
611
|
+
const colocatedTypes = options.modules?.colocatedTypeFiles ?? [];
|
|
560
612
|
const policies = {};
|
|
561
613
|
policies.typePlacement = [
|
|
562
614
|
presetOverride(appFiles, { "arch/no-type-declarations": "error" }, [
|
|
563
615
|
...types,
|
|
616
|
+
...colocatedTypes,
|
|
564
617
|
...components,
|
|
565
618
|
...hooks,
|
|
566
619
|
...services,
|
|
@@ -583,16 +636,18 @@ function presetTypesConfig(context) {
|
|
|
583
636
|
...options.react?.componentProps === false ? [] : [presetOverride(components, { "arch/component-props": ["error", { filePrefix: true }] }, [...ui, ...testFiles])],
|
|
584
637
|
presetOverride(presetScopes(app, "**/*.types.ts"), {
|
|
585
638
|
"arch/no-restricted-files": ["error", { message: "Domain types belong in src/types." }]
|
|
586
|
-
}, [...types, ...ui]),
|
|
639
|
+
}, [...types, ...colocatedTypes, ...ui]),
|
|
587
640
|
presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
|
|
588
641
|
"arch/no-type-declarations": "error"
|
|
589
642
|
}, [...ui, ...testFiles]),
|
|
590
|
-
presetOverride([...presetScopes(
|
|
643
|
+
presetOverride([...presetScopes(schemaOwners, "types/**/*.types.ts"), ...colocatedTypes, ...presetScopes(app, "**/*.constants.ts")], {
|
|
591
644
|
"arch/no-top-level-functions": ["error", { banReExports: false }]
|
|
592
645
|
}),
|
|
593
|
-
|
|
594
|
-
"
|
|
595
|
-
|
|
646
|
+
...runtimeFreeTypes.length ? [
|
|
647
|
+
presetOverride(presetScopes(runtimeFreeTypes, "types/**/*.types.ts"), {
|
|
648
|
+
"arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
|
|
649
|
+
})
|
|
650
|
+
] : []
|
|
596
651
|
];
|
|
597
652
|
policies.typeSafety = [
|
|
598
653
|
presetOverride(safetyFiles, {
|
|
@@ -629,7 +684,7 @@ function presetTypesConfig(context) {
|
|
|
629
684
|
policies.schemas = [
|
|
630
685
|
presetOverride(appFiles, {
|
|
631
686
|
"arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
|
|
632
|
-
}, [...presetScopes(
|
|
687
|
+
}, [...presetScopes(schemaOwners, "types/**"), ...colocatedTypes, ...ui]),
|
|
633
688
|
presetOverride(webFiles, {
|
|
634
689
|
"arch/no-rederive-schema": [
|
|
635
690
|
"error",
|
|
@@ -668,8 +723,9 @@ function presetTypesConfig(context) {
|
|
|
668
723
|
}
|
|
669
724
|
|
|
670
725
|
// src/configs/modules.ts
|
|
726
|
+
import { posix } from "node:path";
|
|
671
727
|
function presetModulesConfig(context) {
|
|
672
|
-
const { web, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
|
|
728
|
+
const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
|
|
673
729
|
const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
|
|
674
730
|
const migrations = options.modules?.migrationFiles ?? [];
|
|
675
731
|
const frontendEntries = [
|
|
@@ -703,7 +759,7 @@ function presetModulesConfig(context) {
|
|
|
703
759
|
})
|
|
704
760
|
] : []
|
|
705
761
|
]),
|
|
706
|
-
presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path
|
|
762
|
+
presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), `components/*/*/**/${presetExtensions}`), {
|
|
707
763
|
"arch/no-restricted-files": ["error", { message: "Components allow one group folder." }]
|
|
708
764
|
}, ui),
|
|
709
765
|
presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], {
|
|
@@ -719,7 +775,13 @@ function presetModulesConfig(context) {
|
|
|
719
775
|
message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
|
|
720
776
|
}
|
|
721
777
|
]
|
|
722
|
-
}, [
|
|
778
|
+
}, [
|
|
779
|
+
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
780
|
+
"**/*.rsc.tsx",
|
|
781
|
+
...presetScopes(nextWeb, "services/**/*.action.ts"),
|
|
782
|
+
...conceptFiles,
|
|
783
|
+
...options.modules?.colocatedTypeFiles ?? []
|
|
784
|
+
])
|
|
723
785
|
];
|
|
724
786
|
policies.serviceStructure = [
|
|
725
787
|
presetOverride(services, {
|
|
@@ -837,9 +899,9 @@ function presetModulesConfig(context) {
|
|
|
837
899
|
"arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
|
|
838
900
|
}),
|
|
839
901
|
presetOverride(components, {
|
|
840
|
-
"arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true }]
|
|
902
|
+
"arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
|
|
841
903
|
}, [...ui, ...testFiles]),
|
|
842
|
-
presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path
|
|
904
|
+
presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), "components/**/*.tsx"), {
|
|
843
905
|
"arch/folder-prefix": ["error", { singularize: "trailing-s", separators: ["-"] }]
|
|
844
906
|
}, [...ui, ...testFiles]),
|
|
845
907
|
...["action", "query"].map((role) => presetOverride([
|
|
@@ -897,9 +959,15 @@ function presetModulesConfig(context) {
|
|
|
897
959
|
return policies;
|
|
898
960
|
}
|
|
899
961
|
|
|
962
|
+
// src/utils/helpers/mocking.ts
|
|
963
|
+
function mockingApiPatternsAssert(patterns) {
|
|
964
|
+
if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
|
|
965
|
+
throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
|
|
966
|
+
}
|
|
967
|
+
|
|
900
968
|
// src/configs/tests.ts
|
|
901
969
|
function presetTestsConfig(context) {
|
|
902
|
-
const {
|
|
970
|
+
const { backendRoots, testFiles, baseImports, options } = context;
|
|
903
971
|
const profile = options.tests?.profile ?? "fixtures";
|
|
904
972
|
if (profile !== "standard" && profile !== "fixtures")
|
|
905
973
|
throw new Error("Unknown test profile.");
|
|
@@ -909,11 +977,30 @@ function presetTestsConfig(context) {
|
|
|
909
977
|
throw new Error("Additional test functions must be literal identifiers or dotted names.");
|
|
910
978
|
if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
|
|
911
979
|
throw new Error("Assertion functions must be rooted dotted names or patterns.");
|
|
980
|
+
const mocking = typeof options.tests?.mocking === "string" ? { preset: options.tests.mocking } : options.tests?.mocking ?? {};
|
|
981
|
+
const mockingPreset = mocking.preset ?? "modules";
|
|
982
|
+
if (mockingPreset !== "modules" && mockingPreset !== "strict")
|
|
983
|
+
throw new Error("Unknown test mocking preset.");
|
|
984
|
+
const mockingDeny = [...mocking.deny ?? [], ...presetTestMockingApis[mockingPreset]].map((entry) => {
|
|
985
|
+
if (typeof entry !== "string")
|
|
986
|
+
return entry;
|
|
987
|
+
return mockingPreset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry;
|
|
988
|
+
});
|
|
989
|
+
const mockingAllow = [...presetTestMockingAllow[mockingPreset], ...mocking.allow ?? []].sort((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
|
|
990
|
+
mockingApiPatternsAssert([
|
|
991
|
+
...mockingDeny.map((entry) => typeof entry === "string" ? entry : entry.api),
|
|
992
|
+
...mockingAllow.flatMap((entry) => entry.apis)
|
|
993
|
+
]);
|
|
994
|
+
const mockingCustomized = mockingPreset !== "modules" || (mocking.deny ?? []).length > 0;
|
|
995
|
+
const mockingRule = (allow) => [
|
|
996
|
+
"error",
|
|
997
|
+
{ deny: mockingDeny, allow: ["*.fn", ...allow], checkDestructured: mockingCustomized }
|
|
998
|
+
];
|
|
912
999
|
const policies = {};
|
|
913
1000
|
policies.tests = [
|
|
914
1001
|
presetOverride(testFiles, {
|
|
915
1002
|
...presetTestRules,
|
|
916
|
-
"dillon-anti-slop/no-module-mocking": "error",
|
|
1003
|
+
"dillon-anti-slop/no-module-mocking": mockingCustomized ? mockingRule([]) : "error",
|
|
917
1004
|
"modules/test-modifiers": [
|
|
918
1005
|
"error",
|
|
919
1006
|
{ additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
|
|
@@ -930,7 +1017,7 @@ function presetTestsConfig(context) {
|
|
|
930
1017
|
{ callees: ["describe", ...testFunctions], forbid: "^should\\b", flags: "i" }
|
|
931
1018
|
]
|
|
932
1019
|
}),
|
|
933
|
-
presetOverride(presetScopes(
|
|
1020
|
+
presetOverride(presetScopes(backendRoots, "tests/**/*.ts"), {
|
|
934
1021
|
"modules/concurrent-db": "error",
|
|
935
1022
|
...profile === "fixtures" ? {
|
|
936
1023
|
"no-restricted-imports": [
|
|
@@ -946,14 +1033,17 @@ function presetTestsConfig(context) {
|
|
|
946
1033
|
}
|
|
947
1034
|
]
|
|
948
1035
|
} : {}
|
|
949
|
-
})
|
|
1036
|
+
}),
|
|
1037
|
+
...mockingAllow.map((entry) => presetOverride(entry.files, {
|
|
1038
|
+
"dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis)
|
|
1039
|
+
}))
|
|
950
1040
|
];
|
|
951
1041
|
return policies;
|
|
952
1042
|
}
|
|
953
1043
|
|
|
954
1044
|
// src/configs/react.ts
|
|
955
1045
|
function presetReactConfig(context) {
|
|
956
|
-
const { options, web, webFiles, ui, testFiles, components, css } = context;
|
|
1046
|
+
const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
|
|
957
1047
|
const policies = {};
|
|
958
1048
|
if (web.length) {
|
|
959
1049
|
policies.react = [
|
|
@@ -975,9 +1065,10 @@ function presetReactConfig(context) {
|
|
|
975
1065
|
if (web.length) {
|
|
976
1066
|
policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
|
|
977
1067
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1068
|
+
const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
|
|
1069
|
+
const routes = [
|
|
1070
|
+
...tanstackWeb.length ? [
|
|
1071
|
+
presetOverride(presetScopes(tanstackWeb, "routes/**/*.{ts,tsx}"), {
|
|
981
1072
|
"arch/route-surface": [
|
|
982
1073
|
"error",
|
|
983
1074
|
{
|
|
@@ -987,7 +1078,7 @@ function presetReactConfig(context) {
|
|
|
987
1078
|
}
|
|
988
1079
|
]
|
|
989
1080
|
}, testFiles),
|
|
990
|
-
presetOverride(presetScopes(
|
|
1081
|
+
presetOverride(presetScopes(tanstackWeb, "routes/handlers/**/*.ts"), {
|
|
991
1082
|
"arch/no-file-level-helpers": [
|
|
992
1083
|
"error",
|
|
993
1084
|
{
|
|
@@ -996,7 +1087,43 @@ function presetReactConfig(context) {
|
|
|
996
1087
|
}
|
|
997
1088
|
]
|
|
998
1089
|
})
|
|
999
|
-
]
|
|
1090
|
+
] : [],
|
|
1091
|
+
...nextWeb.length ? [
|
|
1092
|
+
presetOverride(presetScopes(nextWeb, "app/**/{page,template,default,not-found,forbidden,unauthorized,loading}.tsx"), {
|
|
1093
|
+
"arch/route-surface": [
|
|
1094
|
+
"error",
|
|
1095
|
+
{ exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: true, ...nextPage }
|
|
1096
|
+
]
|
|
1097
|
+
}, [...testFiles, ...presetScopes(nextWeb, "app/not-found.tsx")]),
|
|
1098
|
+
presetOverride([...presetScopes(nextWeb, "app/**/{layout,global-not-found}.tsx"), ...presetScopes(nextWeb, "app/not-found.tsx")], {
|
|
1099
|
+
"arch/route-surface": [
|
|
1100
|
+
"error",
|
|
1101
|
+
{ exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: false, ...nextPage }
|
|
1102
|
+
]
|
|
1103
|
+
}, testFiles),
|
|
1104
|
+
presetOverride(presetScopes(nextWeb, "app/**/route.ts"), {
|
|
1105
|
+
"arch/route-surface": [
|
|
1106
|
+
"error",
|
|
1107
|
+
{
|
|
1108
|
+
exportName: "GET",
|
|
1109
|
+
allowedExports: [...presetNextHandlerMethods.slice(1), ...presetNextSegmentConfig],
|
|
1110
|
+
bannedHooks: [],
|
|
1111
|
+
banIntrinsicJsx: false
|
|
1112
|
+
}
|
|
1113
|
+
],
|
|
1114
|
+
"arch/no-file-level-helpers": [
|
|
1115
|
+
"error",
|
|
1116
|
+
{
|
|
1117
|
+
detectComponents: false,
|
|
1118
|
+
allowPattern: `^(${[...presetNextHandlerMethods, ...presetNextSegmentConfig].join("|")})$`,
|
|
1119
|
+
message: "Nest this helper in its owning route handler, or move genuine domain logic to a service. Do not create a component, hook or public helper to bypass this rule."
|
|
1120
|
+
}
|
|
1121
|
+
]
|
|
1122
|
+
})
|
|
1123
|
+
] : []
|
|
1124
|
+
];
|
|
1125
|
+
if (routes.length) {
|
|
1126
|
+
policies.routes = routes;
|
|
1000
1127
|
}
|
|
1001
1128
|
if (web.length) {
|
|
1002
1129
|
policies.forms = [
|
|
@@ -1049,23 +1176,51 @@ function presetReactConfig(context) {
|
|
|
1049
1176
|
`${group}-[env(safe-area-inset-${side})]`,
|
|
1050
1177
|
...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
|
|
1051
1178
|
]));
|
|
1179
|
+
const illustrationPalette = [
|
|
1180
|
+
"text-color",
|
|
1181
|
+
"bg-color",
|
|
1182
|
+
"gradient-from",
|
|
1183
|
+
"gradient-via",
|
|
1184
|
+
"gradient-to",
|
|
1185
|
+
"border-color",
|
|
1186
|
+
"divide-color",
|
|
1187
|
+
"outline-color",
|
|
1188
|
+
"ring-color",
|
|
1189
|
+
"ring-offset-color",
|
|
1190
|
+
"shadow",
|
|
1191
|
+
"shadow-color",
|
|
1192
|
+
"inset-shadow",
|
|
1193
|
+
"inset-shadow-color",
|
|
1194
|
+
"drop-shadow",
|
|
1195
|
+
"drop-shadow-color",
|
|
1196
|
+
"fill",
|
|
1197
|
+
"stroke",
|
|
1198
|
+
"bg-image",
|
|
1199
|
+
"bg-size",
|
|
1200
|
+
"bg-position",
|
|
1201
|
+
"mask-image",
|
|
1202
|
+
"blur",
|
|
1203
|
+
"saturate"
|
|
1204
|
+
];
|
|
1205
|
+
const restyle = (base) => ({
|
|
1206
|
+
allow: base,
|
|
1207
|
+
contracts: [
|
|
1208
|
+
{ pattern: "^(CardContent|PopoverContent)$", allow: [...base, ...padding] },
|
|
1209
|
+
{ pattern: "^TabsContent$", allow: [...base, ...gap, ...padding] },
|
|
1210
|
+
{ pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...base, ...gap] }
|
|
1211
|
+
]
|
|
1212
|
+
});
|
|
1052
1213
|
policies.shadcn = [
|
|
1053
1214
|
presetOverride(webFiles, {
|
|
1054
|
-
"shadcn/no-restyle": [
|
|
1055
|
-
"error",
|
|
1056
|
-
{
|
|
1057
|
-
allow,
|
|
1058
|
-
contracts: [
|
|
1059
|
-
{ pattern: "^(CardContent|PopoverContent)$", allow: [...allow, ...padding] },
|
|
1060
|
-
{ pattern: "^TabsContent$", allow: [...allow, ...gap, ...padding] },
|
|
1061
|
-
{ pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...allow, ...gap] }
|
|
1062
|
-
]
|
|
1063
|
-
}
|
|
1064
|
-
],
|
|
1215
|
+
"shadcn/no-restyle": ["error", restyle(allow)],
|
|
1065
1216
|
"shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
|
|
1066
1217
|
"shadcn/require-static-classes": "error",
|
|
1067
1218
|
"modules/dynamic-classes": "error"
|
|
1068
1219
|
}, ui),
|
|
1220
|
+
presetOverride(presetScopes(web, "components/**/*.illustration.tsx"), {
|
|
1221
|
+
"shadcn/no-restyle": ["error", restyle([...allow, ...illustrationPalette])],
|
|
1222
|
+
"shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea, ...illustrationPalette] }]
|
|
1223
|
+
}, ui),
|
|
1069
1224
|
presetOverride(webFiles, {
|
|
1070
1225
|
"shadcn/no-unknown-classes": ["error", { allow: [] }]
|
|
1071
1226
|
}),
|
|
@@ -1080,7 +1235,16 @@ function presetReactConfig(context) {
|
|
|
1080
1235
|
// src/configs/boundaries.ts
|
|
1081
1236
|
import { resolve as resolve2 } from "node:path";
|
|
1082
1237
|
function presetBoundariesConfig(context) {
|
|
1083
|
-
const { options, root, architecture, packages, web, backend, appFiles, publicEntrypoints, baseImports } = context;
|
|
1238
|
+
const { options, root, architecture, packages, web, tanstackWeb, nextWeb, backend, appFiles, publicEntrypoints, baseImports } = context;
|
|
1239
|
+
const runtimeBoundary = {
|
|
1240
|
+
web: web.map((path) => resolve2(root, path)),
|
|
1241
|
+
backend: backend.map((path) => resolve2(root, path)),
|
|
1242
|
+
aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve2(root, path)])),
|
|
1243
|
+
backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
|
|
1244
|
+
const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
|
|
1245
|
+
return name ? [name] : [];
|
|
1246
|
+
})
|
|
1247
|
+
};
|
|
1084
1248
|
const policies = {};
|
|
1085
1249
|
policies.boundaries = [
|
|
1086
1250
|
presetOverride([...appFiles, ...presetScopes(packages, "**/" + presetExtensions)], {
|
|
@@ -1108,20 +1272,11 @@ function presetBoundariesConfig(context) {
|
|
|
1108
1272
|
}
|
|
1109
1273
|
]
|
|
1110
1274
|
}),
|
|
1111
|
-
presetOverride([...presetScopes(
|
|
1275
|
+
presetOverride([...presetScopes(tanstackWeb, "services/**/*.{ts,tsx}"), ...presetScopes(tanstackWeb, "lib/**/*.{ts,tsx}")], {
|
|
1112
1276
|
"modules/tanstack-runtime": [
|
|
1113
1277
|
"error",
|
|
1114
1278
|
{
|
|
1115
|
-
|
|
1116
|
-
backend: backend.map((path) => resolve2(root, path)),
|
|
1117
|
-
aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [
|
|
1118
|
-
alias.replace(/\/$/, ""),
|
|
1119
|
-
resolve2(root, path)
|
|
1120
|
-
])),
|
|
1121
|
-
backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
|
|
1122
|
-
const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
|
|
1123
|
-
return name ? [name] : [];
|
|
1124
|
-
}),
|
|
1279
|
+
...runtimeBoundary,
|
|
1125
1280
|
rpcClients: ((options.orpc?.clientOwnerFile) ? [options.orpc?.clientOwnerFile] : context.rpcClients).map((path) => resolve2(root, path)),
|
|
1126
1281
|
serverImports: options.tanstackStart?.additionalServerOnlyImports ?? [],
|
|
1127
1282
|
clientImports: options.tanstackStart?.additionalClientOnlyImports ?? [],
|
|
@@ -1129,6 +1284,17 @@ function presetBoundariesConfig(context) {
|
|
|
1129
1284
|
}
|
|
1130
1285
|
]
|
|
1131
1286
|
}),
|
|
1287
|
+
presetOverride([...presetScopes(nextWeb, "services/**/*.{ts,tsx}"), ...presetScopes(nextWeb, "lib/**/*.{ts,tsx}")], {
|
|
1288
|
+
"modules/next-runtime": [
|
|
1289
|
+
"error",
|
|
1290
|
+
{
|
|
1291
|
+
...runtimeBoundary,
|
|
1292
|
+
serverImports: options.next?.additionalServerOnlyImports ?? [],
|
|
1293
|
+
clientImports: options.next?.additionalClientOnlyImports ?? [],
|
|
1294
|
+
allowComputedImportsIn: options.next?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
|
|
1295
|
+
}
|
|
1296
|
+
]
|
|
1297
|
+
}),
|
|
1132
1298
|
presetOverride(presetScopes(backend, "rpc/**/*.ts"), {
|
|
1133
1299
|
"modules/rpc-database": "error",
|
|
1134
1300
|
"no-restricted-imports": ["error", { paths: ["kysely"], patterns: baseImports }]
|
|
@@ -1191,7 +1357,7 @@ function presetPluginsConfig(context) {
|
|
|
1191
1357
|
}
|
|
1192
1358
|
|
|
1193
1359
|
// src/services/preset-context.ts
|
|
1194
|
-
import { resolve as resolve3, posix } from "node:path";
|
|
1360
|
+
import { resolve as resolve3, posix as posix2 } from "node:path";
|
|
1195
1361
|
function presetResolveContext(options) {
|
|
1196
1362
|
const generated = presetGeneratedFiles;
|
|
1197
1363
|
const root = resolve3(options.root ?? process.cwd());
|
|
@@ -1205,13 +1371,24 @@ function presetResolveContext(options) {
|
|
|
1205
1371
|
const entry = typeof configuredRoot === "string" ? { role: configuredRoot } : configuredRoot;
|
|
1206
1372
|
if (!entry || typeof entry !== "object")
|
|
1207
1373
|
throw new Error("Architecture entries must be roles or role/layout objects.");
|
|
1208
|
-
if (Object.keys(entry).some((key) => !["role", "layout"].includes(key)))
|
|
1374
|
+
if (Object.keys(entry).some((key) => !["role", "layout", "framework", "sourceDir"].includes(key)))
|
|
1209
1375
|
throw new Error("Unknown architecture entry option.");
|
|
1210
1376
|
const role = entry.role;
|
|
1377
|
+
if ("framework" in entry && entry.framework !== undefined && role !== "web")
|
|
1378
|
+
throw new Error("Only web architecture roots accept a framework.");
|
|
1379
|
+
const framework = role === "web" ? entry.framework ?? options.framework ?? "tanstack-start" : undefined;
|
|
1380
|
+
if (framework !== undefined && framework !== "tanstack-start" && framework !== "next")
|
|
1381
|
+
throw new Error(`Unknown framework: ${framework}`);
|
|
1382
|
+
const sourced = role === "web" || role === "server" || role === "runner";
|
|
1383
|
+
if ("sourceDir" in entry && entry.sourceDir !== undefined && !sourced)
|
|
1384
|
+
throw new Error("Only web, server and runner architecture roots accept a sourceDir.");
|
|
1385
|
+
const sourceDir = entry.sourceDir ?? "src";
|
|
1386
|
+
if (typeof sourceDir !== "string" || sourceDir !== "." && sourceDir.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
|
|
1387
|
+
throw new Error("Source folders must be `.` or literal root-relative paths without escapes, globs or empty segments.");
|
|
1211
1388
|
const portable = path.replaceAll("\\", "/");
|
|
1212
|
-
if (!portable.trim() ||
|
|
1389
|
+
if (!portable.trim() || posix2.isAbsolute(portable) || /^[a-z]:/i.test(portable))
|
|
1213
1390
|
throw new Error("Architecture roots must be nonempty project-relative paths.");
|
|
1214
|
-
const normalized =
|
|
1391
|
+
const normalized = posix2.normalize(portable).replace(/\/$/, "");
|
|
1215
1392
|
if (normalized === ".." || normalized.startsWith("../"))
|
|
1216
1393
|
throw new Error("Architecture roots must stay inside the project.");
|
|
1217
1394
|
if (!["web", "server", "runner", "scripts", "packages"].includes(role))
|
|
@@ -1239,7 +1416,15 @@ function presetResolveContext(options) {
|
|
|
1239
1416
|
throw new Error("Layout folders must not overlap.");
|
|
1240
1417
|
}
|
|
1241
1418
|
}
|
|
1242
|
-
return [
|
|
1419
|
+
return [
|
|
1420
|
+
normalized,
|
|
1421
|
+
{
|
|
1422
|
+
role,
|
|
1423
|
+
layout,
|
|
1424
|
+
...framework ? { framework } : {},
|
|
1425
|
+
...entry.sourceDir === undefined ? {} : { sourceDir }
|
|
1426
|
+
}
|
|
1427
|
+
];
|
|
1243
1428
|
});
|
|
1244
1429
|
for (const [index, [path]] of entries.entries()) {
|
|
1245
1430
|
const overlaps = entries.slice(0, index).some(([other]) => other === path || other === "." || path === "." || other.startsWith(path + "/") || path.startsWith(other + "/"));
|
|
@@ -1265,17 +1450,24 @@ function presetResolveContext(options) {
|
|
|
1265
1450
|
"spec"
|
|
1266
1451
|
].includes(role)))
|
|
1267
1452
|
throw new Error("File roles must not replace built-in roles.");
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
const
|
|
1453
|
+
if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
|
|
1454
|
+
throw new Error(`Unknown framework: ${options.framework}`);
|
|
1455
|
+
const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
|
|
1456
|
+
const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
|
|
1457
|
+
const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
|
|
1458
|
+
const tanstackWeb = webFor("tanstack-start");
|
|
1459
|
+
const nextWeb = webFor("next");
|
|
1460
|
+
const backendEntries = entries.filter(([, { role }]) => role === "server" || role === "runner");
|
|
1461
|
+
const backendRoots = backendEntries.map(([path]) => path);
|
|
1462
|
+
const backend = backendEntries.map(([path, entry]) => source(path, entry));
|
|
1271
1463
|
const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
|
|
1272
1464
|
const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
|
|
1273
1465
|
const folders = entries.flatMap(([path, entry]) => Object.entries(entry.layout).map(([folder, mode]) => ({
|
|
1274
|
-
path:
|
|
1466
|
+
path: posix2.join(["web", "server", "runner"].includes(entry.role) ? source(path, entry) : path, folder),
|
|
1275
1467
|
folder,
|
|
1276
1468
|
mode
|
|
1277
1469
|
})));
|
|
1278
|
-
const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) =>
|
|
1470
|
+
const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => posix2.join(source(path, entry), "services", entry.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts"));
|
|
1279
1471
|
const app = [...web, ...backend];
|
|
1280
1472
|
const appFiles = presetScopes(app);
|
|
1281
1473
|
const webFiles = presetScopes(web);
|
|
@@ -1301,13 +1493,17 @@ function presetResolveContext(options) {
|
|
|
1301
1493
|
];
|
|
1302
1494
|
const tailwind = options.tailwind;
|
|
1303
1495
|
const entryPoints = Object.fromEntries(Object.entries(tailwind === false ? {} : tailwind?.cssEntryPointsByRoot ?? {}).map(([path, entry]) => [
|
|
1304
|
-
|
|
1496
|
+
posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
|
|
1305
1497
|
entry
|
|
1306
1498
|
]));
|
|
1307
|
-
const css = tailwind === false ? {} : Object.fromEntries(web.map((path) =>
|
|
1308
|
-
path,
|
|
1309
|
-
|
|
1310
|
-
|
|
1499
|
+
const css = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
|
|
1500
|
+
const sourcePath = source(path, entry);
|
|
1501
|
+
const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
|
|
1502
|
+
return [
|
|
1503
|
+
sourcePath,
|
|
1504
|
+
resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
|
|
1505
|
+
];
|
|
1506
|
+
}));
|
|
1311
1507
|
return {
|
|
1312
1508
|
options,
|
|
1313
1509
|
root,
|
|
@@ -1317,7 +1513,10 @@ function presetResolveContext(options) {
|
|
|
1317
1513
|
packages,
|
|
1318
1514
|
scripts,
|
|
1319
1515
|
web,
|
|
1516
|
+
tanstackWeb,
|
|
1517
|
+
nextWeb,
|
|
1320
1518
|
backend,
|
|
1519
|
+
backendRoots,
|
|
1321
1520
|
app,
|
|
1322
1521
|
appFiles,
|
|
1323
1522
|
webFiles,
|
|
@@ -1341,6 +1540,8 @@ function presetResolveContext(options) {
|
|
|
1341
1540
|
// src/services/preset-compose.ts
|
|
1342
1541
|
function presetCompose(context, policies, plugins) {
|
|
1343
1542
|
const { options, level, safetyFiles, ui, generated } = context;
|
|
1543
|
+
const severities = options.ruleSeverities ?? {};
|
|
1544
|
+
const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
|
|
1344
1545
|
const overrideList = [];
|
|
1345
1546
|
for (const name of presetPolicyOrder) {
|
|
1346
1547
|
const setting = options.policies?.[name];
|
|
@@ -1352,14 +1553,16 @@ function presetCompose(context, policies, plugins) {
|
|
|
1352
1553
|
for (const entry of entries) {
|
|
1353
1554
|
overrideList.push({
|
|
1354
1555
|
...entry,
|
|
1355
|
-
rules: entry.rules ?
|
|
1556
|
+
rules: entry.rules ? levels(entry.rules) : undefined
|
|
1356
1557
|
});
|
|
1357
1558
|
}
|
|
1358
1559
|
}
|
|
1359
|
-
overrideList.push(presetOverride(safetyFiles, {
|
|
1360
|
-
"max-lines": [
|
|
1361
|
-
complexity: [
|
|
1362
|
-
}, ui));
|
|
1560
|
+
overrideList.push(presetOverride(safetyFiles, levels({
|
|
1561
|
+
"max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
|
|
1562
|
+
complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
|
|
1563
|
+
}), ui));
|
|
1564
|
+
const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
|
|
1565
|
+
const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
|
|
1363
1566
|
for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
|
|
1364
1567
|
overrideList.push({ files, rules: { [rule]: "off" } });
|
|
1365
1568
|
}
|
|
@@ -1373,17 +1576,18 @@ function presetCompose(context, policies, plugins) {
|
|
|
1373
1576
|
"**/build/**",
|
|
1374
1577
|
"**/coverage/**",
|
|
1375
1578
|
"**/.output/**",
|
|
1579
|
+
...context.nextWeb.length ? ["**/.next/**", "**/next-env.d.ts"] : [],
|
|
1376
1580
|
...generated,
|
|
1377
1581
|
...options.ignorePatterns ?? []
|
|
1378
1582
|
],
|
|
1379
1583
|
...plugins,
|
|
1380
|
-
rules:
|
|
1584
|
+
rules: { ...levels(presetCommonRules), ...unnamedLevels },
|
|
1381
1585
|
overrides: overrideList.filter((entry) => entry.files.length > 0)
|
|
1382
1586
|
};
|
|
1383
1587
|
}
|
|
1384
1588
|
|
|
1385
|
-
// src/presets/
|
|
1386
|
-
function
|
|
1589
|
+
// src/presets/react-modules-preset.ts
|
|
1590
|
+
function reactModulesPreset(options = {}) {
|
|
1387
1591
|
const context = presetResolveContext(options);
|
|
1388
1592
|
return presetCompose(context, {
|
|
1389
1593
|
...presetSourceConfig(context),
|
|
@@ -1394,6 +1598,11 @@ function tanstackStartReactModulesPreset(options = {}) {
|
|
|
1394
1598
|
...presetBoundariesConfig(context)
|
|
1395
1599
|
}, presetPluginsConfig(context));
|
|
1396
1600
|
}
|
|
1601
|
+
|
|
1602
|
+
// src/presets/tanstack-start-react-modules-preset.ts
|
|
1603
|
+
function tanstackStartReactModulesPreset(options = {}) {
|
|
1604
|
+
return reactModulesPreset({ ...options, framework: options.framework ?? "tanstack-start" });
|
|
1605
|
+
}
|
|
1397
1606
|
export {
|
|
1398
1607
|
tanstackStartReactModulesPreset as default
|
|
1399
1608
|
};
|