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,4 +1,4 @@
|
|
|
1
|
-
import type { PresetRules, PresetPolicyName } from '../types/preset.types.ts';
|
|
1
|
+
import type { PresetRules, PresetPolicyName, PresetTestMockingException, PresetTestMockingPreset } from '../types/preset.types.ts';
|
|
2
2
|
export declare const presetExtensions = "*.{ts,tsx,mts,cts}";
|
|
3
3
|
export declare const presetPolicyOrder: PresetPolicyName[];
|
|
4
4
|
export declare const presetCommonRules: PresetRules;
|
|
@@ -18,6 +18,16 @@ export declare const presetTailwindRules: {
|
|
|
18
18
|
export declare const presetImportRules: PresetRules;
|
|
19
19
|
export declare const presetNamingRules: PresetRules;
|
|
20
20
|
export declare const presetTestRules: PresetRules;
|
|
21
|
+
/** Test-double APIs each `tests.mocking` preset bans, for `vi` and `jest`. */
|
|
22
|
+
export declare const presetTestMockingApis: Record<PresetTestMockingPreset, string[]>;
|
|
23
|
+
export declare const presetTestMockingAllow: Record<PresetTestMockingPreset, PresetTestMockingException[]>;
|
|
24
|
+
export declare const presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
|
|
25
|
+
/** HTTP methods a Next `route.ts` handler may export. */
|
|
26
|
+
export declare const presetNextHandlerMethods: string[];
|
|
27
|
+
/** Next route segment config exports, shared by pages, layouts and route handlers. */
|
|
28
|
+
export declare const presetNextSegmentConfig: string[];
|
|
29
|
+
/** Exports a Next page or layout may add next to its default component. */
|
|
30
|
+
export declare const presetNextSegmentExports: string[];
|
|
21
31
|
export declare const presetExportPrefix: {
|
|
22
32
|
stem: string;
|
|
23
33
|
trailingRoles: string[];
|
package/dist/index.js
CHANGED
|
@@ -749,6 +749,11 @@ function layoutNewline(text) {
|
|
|
749
749
|
` : `
|
|
750
750
|
`;
|
|
751
751
|
}
|
|
752
|
+
// src/utils/helpers/mocking.ts
|
|
753
|
+
function mockingApiPatternsAssert(patterns) {
|
|
754
|
+
if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
|
|
755
|
+
throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
|
|
756
|
+
}
|
|
752
757
|
// src/utils/helpers/options.ts
|
|
753
758
|
function optionsFirst(context, fallback) {
|
|
754
759
|
return context.options[0] ?? fallback;
|
|
@@ -894,6 +899,49 @@ function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
|
|
|
894
899
|
}
|
|
895
900
|
});
|
|
896
901
|
}
|
|
902
|
+
// src/utils/helpers/runtime.ts
|
|
903
|
+
import { builtinModules } from "node:module";
|
|
904
|
+
var runtimeServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
905
|
+
var runtimeBrowserGlobals = [
|
|
906
|
+
"window",
|
|
907
|
+
"document",
|
|
908
|
+
"navigator",
|
|
909
|
+
"localStorage",
|
|
910
|
+
"sessionStorage",
|
|
911
|
+
"location",
|
|
912
|
+
"history",
|
|
913
|
+
"screen",
|
|
914
|
+
"HTMLElement",
|
|
915
|
+
"Element",
|
|
916
|
+
"HTMLInputElement",
|
|
917
|
+
"ResizeObserver",
|
|
918
|
+
"MutationObserver",
|
|
919
|
+
"IntersectionObserver",
|
|
920
|
+
"FileReader",
|
|
921
|
+
"matchMedia",
|
|
922
|
+
"getComputedStyle",
|
|
923
|
+
"requestAnimationFrame",
|
|
924
|
+
"cancelAnimationFrame",
|
|
925
|
+
"alert",
|
|
926
|
+
"confirm",
|
|
927
|
+
"prompt"
|
|
928
|
+
];
|
|
929
|
+
function runtimeMatchesImport(values, source) {
|
|
930
|
+
return Boolean(values?.some((value) => source === value || source.startsWith(value + "/")));
|
|
931
|
+
}
|
|
932
|
+
function runtimeIsBackendImport(options, target, source) {
|
|
933
|
+
return options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
934
|
+
}
|
|
935
|
+
function runtimeIsServerPlatformImport(source) {
|
|
936
|
+
return builtinModules.includes(source) || source === "@orpc/server" || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source);
|
|
937
|
+
}
|
|
938
|
+
function runtimeMemberName(node) {
|
|
939
|
+
if (node?.type !== "MemberExpression")
|
|
940
|
+
return null;
|
|
941
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
942
|
+
return node.property.name;
|
|
943
|
+
return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
|
|
944
|
+
}
|
|
897
945
|
// src/utils/helpers/sanitizers.ts
|
|
898
946
|
function sanitizersCallsConfigured(node, names) {
|
|
899
947
|
let found = false;
|
|
@@ -2378,8 +2426,10 @@ var noFileLevelHelpers = defineRule16({
|
|
|
2378
2426
|
} = optionsFirst(context, {});
|
|
2379
2427
|
const allowed = optionsOptionalPatterns(allowPattern);
|
|
2380
2428
|
const hooks = optionsPatterns(hookPattern);
|
|
2429
|
+
const exported = exportsCollect(program);
|
|
2430
|
+
const allowedExport = (name) => exported.some((binding) => binding.localName === name && optionsPatternsTest(allowed, binding.exportedName));
|
|
2381
2431
|
for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
|
|
2382
|
-
if (detectComponents && reactComponentsIsLike(program, candidate) || optionsPatternsTest(hooks, candidate.name) || optionsPatternsTest(allowed, candidate.name)) {
|
|
2432
|
+
if (detectComponents && reactComponentsIsLike(program, candidate) || optionsPatternsTest(hooks, candidate.name) || optionsPatternsTest(allowed, candidate.name) || allowedExport(candidate.name)) {
|
|
2383
2433
|
continue;
|
|
2384
2434
|
}
|
|
2385
2435
|
context.report({
|
|
@@ -4271,6 +4321,7 @@ var onlyExportComponents = defineRule36({
|
|
|
4271
4321
|
additionalProperties: false,
|
|
4272
4322
|
properties: {
|
|
4273
4323
|
matchFileName: { type: "boolean" },
|
|
4324
|
+
trailingRoles: { type: "array", items: { type: "string" } },
|
|
4274
4325
|
allowTypeExports: { type: "boolean" },
|
|
4275
4326
|
denyTypePattern: optionsPatternSchema
|
|
4276
4327
|
}
|
|
@@ -4288,10 +4339,15 @@ var onlyExportComponents = defineRule36({
|
|
|
4288
4339
|
Program(program) {
|
|
4289
4340
|
const {
|
|
4290
4341
|
matchFileName = true,
|
|
4342
|
+
trailingRoles = [],
|
|
4291
4343
|
allowTypeExports = true,
|
|
4292
4344
|
denyTypePattern
|
|
4293
4345
|
} = optionsFirst(context, {});
|
|
4294
|
-
const
|
|
4346
|
+
const basename = namingFileBasename(context.filename);
|
|
4347
|
+
const extension = basename.slice(basename.lastIndexOf("."));
|
|
4348
|
+
const role = trailingRoles.find((candidate) => basename.endsWith(`.${candidate}${extension}`));
|
|
4349
|
+
const expected = reactComponentsPascalFromBasename(role ? basename.replace(`.${role}${extension}`, extension) : basename);
|
|
4350
|
+
const expectedKeys = [expected, ...role ? [reactComponentsPascalFromBasename(basename)] : []].map((name) => name.toLowerCase());
|
|
4295
4351
|
const denied = optionsOptionalPatterns(denyTypePattern);
|
|
4296
4352
|
const components = [];
|
|
4297
4353
|
for (const candidate of reactComponentsExportCandidates(program)) {
|
|
@@ -4324,10 +4380,9 @@ var onlyExportComponents = defineRule36({
|
|
|
4324
4380
|
if (components.length === 0 || !matchFileName) {
|
|
4325
4381
|
return;
|
|
4326
4382
|
}
|
|
4327
|
-
const
|
|
4328
|
-
const
|
|
4329
|
-
|
|
4330
|
-
if (!exact && !allPrefixed) {
|
|
4383
|
+
const names = components.map((candidate) => candidate.name.toLowerCase());
|
|
4384
|
+
const matches = expectedKeys.some((expectedKey) => names.includes(expectedKey) || names.every((name) => name.startsWith(expectedKey)));
|
|
4385
|
+
if (!matches) {
|
|
4331
4386
|
for (const candidate of components) {
|
|
4332
4387
|
context.report({
|
|
4333
4388
|
node: candidate.node,
|
|
@@ -4994,13 +5049,21 @@ var routeSurface = defineRule44({
|
|
|
4994
5049
|
additionalProperties: false,
|
|
4995
5050
|
properties: {
|
|
4996
5051
|
exportName: { type: "string" },
|
|
5052
|
+
allowedExports: { type: "array", items: { type: "string" } },
|
|
5053
|
+
inspectExports: { type: "boolean" },
|
|
4997
5054
|
bannedHooks: { type: "array", items: { type: "string" } },
|
|
4998
5055
|
banIntrinsicJsx: { type: "boolean" }
|
|
4999
5056
|
}
|
|
5000
5057
|
}
|
|
5001
5058
|
],
|
|
5002
5059
|
defaultOptions: [
|
|
5003
|
-
{
|
|
5060
|
+
{
|
|
5061
|
+
exportName: "Route",
|
|
5062
|
+
allowedExports: [],
|
|
5063
|
+
inspectExports: false,
|
|
5064
|
+
bannedHooks: ["useState", "useEffect", "useMutation"],
|
|
5065
|
+
banIntrinsicJsx: true
|
|
5066
|
+
}
|
|
5004
5067
|
],
|
|
5005
5068
|
messages: {
|
|
5006
5069
|
extraExport: "Route files may only export '{{expected}}'; found '{{actual}}'.",
|
|
@@ -5011,20 +5074,34 @@ var routeSurface = defineRule44({
|
|
|
5011
5074
|
createOnce(context) {
|
|
5012
5075
|
return {
|
|
5013
5076
|
Program(program) {
|
|
5014
|
-
const { exportName, bannedHooks, banIntrinsicJsx } = optionsFirst(context);
|
|
5015
|
-
|
|
5016
|
-
|
|
5077
|
+
const { exportName, allowedExports = [], inspectExports = false, bannedHooks, banIntrinsicJsx } = optionsFirst(context);
|
|
5078
|
+
const allowed = new Set([exportName, ...allowedExports]);
|
|
5079
|
+
const bindings = exportsCollect(program);
|
|
5080
|
+
for (const binding of bindings) {
|
|
5081
|
+
if (!allowed.has(binding.exportedName)) {
|
|
5017
5082
|
context.report({
|
|
5018
5083
|
node: binding.node,
|
|
5019
5084
|
messageId: "extraExport",
|
|
5020
|
-
data: { expected:
|
|
5085
|
+
data: { expected: [...allowed].join("', '"), actual: binding.exportedName }
|
|
5021
5086
|
});
|
|
5022
5087
|
}
|
|
5023
5088
|
}
|
|
5024
5089
|
const banned = new Set(bannedHooks);
|
|
5090
|
+
const inspected = new Set;
|
|
5025
5091
|
for (const item of declarationsTopLevelUnexportedFunctions(program)) {
|
|
5092
|
+
inspected.add(item.node.start);
|
|
5026
5093
|
routesInspectFunction(context, item, banned, banIntrinsicJsx);
|
|
5027
5094
|
}
|
|
5095
|
+
if (!inspectExports)
|
|
5096
|
+
return;
|
|
5097
|
+
const names = new Set(bindings.filter((binding) => binding.exportedName === exportName).map((binding) => binding.localName ?? binding.exportedName));
|
|
5098
|
+
const defaultExport = program.body.find((statement) => statement.type === "ExportDefaultDeclaration");
|
|
5099
|
+
const declaration = defaultExport?.declaration;
|
|
5100
|
+
const defaultFunctions = declaration?.type === "ArrowFunctionExpression" || declaration?.type === "FunctionExpression" ? [{ name: "default", node: declaration }] : [];
|
|
5101
|
+
for (const item of [...exportsCollectFunctions(program), ...defaultFunctions]) {
|
|
5102
|
+
if (names.has(item.name) && !inspected.has(item.node.start))
|
|
5103
|
+
routesInspectFunction(context, item, banned, banIntrinsicJsx);
|
|
5104
|
+
}
|
|
5028
5105
|
}
|
|
5029
5106
|
};
|
|
5030
5107
|
}
|
package/dist/presets/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
export { default as reactModulesPreset } from './react-modules-preset.ts';
|
|
2
|
+
export { default as nextReactModulesPreset } from './next-react-modules-preset.ts';
|
|
1
3
|
export { default as tanstackStartReactModulesPreset } from './tanstack-start-react-modules-preset.ts';
|
|
2
|
-
export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';
|
|
4
|
+
export type { PresetOptions, PresetArchitecture, PresetArchitectureEntry, PresetFolderLayout, PresetFormattingOptions, PresetFramework, PresetNextOptions, PresetTanstackStartOptions, PresetLimitsOptions, PresetModulesOptions, PresetImportsOptions, PresetOrpcOptions, PresetSqlOptions, PresetFormsOptions, PresetReactOptions, PresetTestsOptions, PresetTestMockingApi, PresetTestMockingException, PresetTestMockingOptions, PresetTestMockingPreset, PresetPolicyOptions, PresetPolicy, PresetPolicyName, PresetTailwindOptions, PresetShadcnOptions, } from '../types/preset.types.ts';
|