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 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallow Vitest and Jest module mocking, plus any other configured test-double APIs.
|
|
3
|
+
*
|
|
4
|
+
* Example: `vi.mock('./mailer')` fails; with `deny: ['*.spyOn']`,
|
|
5
|
+
* `vi.spyOn(mailer, 'send')` fails too. `checkDestructured` also catches
|
|
6
|
+
* `const { spyOn } = vi; spyOn(mailer, 'send')`.
|
|
7
|
+
*/
|
|
1
8
|
export declare const noModuleMockingRule: import("@oxlint/plugins").Rule;
|
|
@@ -790,6 +790,11 @@ function layoutNewline(text) {
|
|
|
790
790
|
` : `
|
|
791
791
|
`;
|
|
792
792
|
}
|
|
793
|
+
// src/utils/helpers/mocking.ts
|
|
794
|
+
function mockingApiPatternsAssert(patterns) {
|
|
795
|
+
if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
|
|
796
|
+
throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
|
|
797
|
+
}
|
|
793
798
|
// src/utils/helpers/options.ts
|
|
794
799
|
function optionsFirst(context, fallback) {
|
|
795
800
|
return context.options[0] ?? fallback;
|
|
@@ -935,6 +940,49 @@ function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
|
|
|
935
940
|
}
|
|
936
941
|
});
|
|
937
942
|
}
|
|
943
|
+
// src/utils/helpers/runtime.ts
|
|
944
|
+
import { builtinModules } from "node:module";
|
|
945
|
+
var runtimeServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
946
|
+
var runtimeBrowserGlobals = [
|
|
947
|
+
"window",
|
|
948
|
+
"document",
|
|
949
|
+
"navigator",
|
|
950
|
+
"localStorage",
|
|
951
|
+
"sessionStorage",
|
|
952
|
+
"location",
|
|
953
|
+
"history",
|
|
954
|
+
"screen",
|
|
955
|
+
"HTMLElement",
|
|
956
|
+
"Element",
|
|
957
|
+
"HTMLInputElement",
|
|
958
|
+
"ResizeObserver",
|
|
959
|
+
"MutationObserver",
|
|
960
|
+
"IntersectionObserver",
|
|
961
|
+
"FileReader",
|
|
962
|
+
"matchMedia",
|
|
963
|
+
"getComputedStyle",
|
|
964
|
+
"requestAnimationFrame",
|
|
965
|
+
"cancelAnimationFrame",
|
|
966
|
+
"alert",
|
|
967
|
+
"confirm",
|
|
968
|
+
"prompt"
|
|
969
|
+
];
|
|
970
|
+
function runtimeMatchesImport(values, source) {
|
|
971
|
+
return Boolean(values?.some((value) => source === value || source.startsWith(value + "/")));
|
|
972
|
+
}
|
|
973
|
+
function runtimeIsBackendImport(options, target, source) {
|
|
974
|
+
return options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
975
|
+
}
|
|
976
|
+
function runtimeIsServerPlatformImport(source) {
|
|
977
|
+
return builtinModules.includes(source) || source === "@orpc/server" || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source);
|
|
978
|
+
}
|
|
979
|
+
function runtimeMemberName(node) {
|
|
980
|
+
if (node?.type !== "MemberExpression")
|
|
981
|
+
return null;
|
|
982
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
983
|
+
return node.property.name;
|
|
984
|
+
return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
|
|
985
|
+
}
|
|
938
986
|
// src/utils/helpers/sanitizers.ts
|
|
939
987
|
function sanitizersCallsConfigured(node, names) {
|
|
940
988
|
let found = false;
|
|
@@ -1589,7 +1637,6 @@ var directives = defineRule7({
|
|
|
1589
1637
|
});
|
|
1590
1638
|
|
|
1591
1639
|
// src/rules/module-policy/tanstack-runtime.ts
|
|
1592
|
-
import { builtinModules } from "node:module";
|
|
1593
1640
|
import { defineRule as defineRule8 } from "@oxlint/plugins";
|
|
1594
1641
|
var tanstackRuntime = defineRule8({
|
|
1595
1642
|
meta: {
|
|
@@ -1667,10 +1714,8 @@ var tanstackRuntime = defineRule8({
|
|
|
1667
1714
|
if (typeOnly)
|
|
1668
1715
|
return;
|
|
1669
1716
|
const target = boundaryResolveTarget(options2, file, source);
|
|
1670
|
-
const
|
|
1671
|
-
const
|
|
1672
|
-
const server = Boolean(renderer) || builtinModules.includes(source) || source === "@orpc/server" || backend || /\.server(?:\.[cm]?[jt]sx?)?$/.test(target) || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^@tanstack\/(?:react|solid|vue)-start\/(?:server|server-only)(?:\/|$)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source) || matches(options2.serverImports);
|
|
1673
|
-
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || matches(options2.clientImports);
|
|
1717
|
+
const server = Boolean(renderer) || runtimeIsServerPlatformImport(source) || runtimeIsBackendImport(options2, target, source) || /\.server(?:\.[cm]?[jt]sx?)?$/.test(target) || /^@tanstack\/(?:react|solid|vue)-start\/(?:server|server-only)(?:\/|$)/.test(source) || runtimeMatchesImport(options2.serverImports, source);
|
|
1718
|
+
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || runtimeMatchesImport(options2.clientImports, source);
|
|
1674
1719
|
if (client && (serverFile || rscFile || shared) && boundary(node.start)?.side !== "client") {
|
|
1675
1720
|
const references = node.type === "ImportDeclaration" ? context.sourceCode.getDeclaredVariables(node).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0]))) : [];
|
|
1676
1721
|
if (!shared && references.length && references.every((reference) => boundary(reference.identifier.range[0])?.side === "client"))
|
|
@@ -1699,34 +1744,9 @@ var tanstackRuntime = defineRule8({
|
|
|
1699
1744
|
report(node, source, "use an inline imported TanStack server boundary; static isomorphic server imports belong only to the configured RPC owner");
|
|
1700
1745
|
}
|
|
1701
1746
|
function global(node) {
|
|
1702
|
-
const serverNames = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
1703
|
-
const browserNames = [
|
|
1704
|
-
"window",
|
|
1705
|
-
"document",
|
|
1706
|
-
"navigator",
|
|
1707
|
-
"localStorage",
|
|
1708
|
-
"sessionStorage",
|
|
1709
|
-
"location",
|
|
1710
|
-
"history",
|
|
1711
|
-
"screen",
|
|
1712
|
-
"HTMLElement",
|
|
1713
|
-
"Element",
|
|
1714
|
-
"HTMLInputElement",
|
|
1715
|
-
"ResizeObserver",
|
|
1716
|
-
"MutationObserver",
|
|
1717
|
-
"IntersectionObserver",
|
|
1718
|
-
"FileReader",
|
|
1719
|
-
"matchMedia",
|
|
1720
|
-
"getComputedStyle",
|
|
1721
|
-
"requestAnimationFrame",
|
|
1722
|
-
"cancelAnimationFrame",
|
|
1723
|
-
"alert",
|
|
1724
|
-
"confirm",
|
|
1725
|
-
"prompt"
|
|
1726
|
-
];
|
|
1727
1747
|
const member = node.parent.type === "MemberExpression" && node.parent.object === node && node.name === "globalThis" ? node.parent : undefined;
|
|
1728
|
-
const name = member
|
|
1729
|
-
if (!
|
|
1748
|
+
const name = runtimeMemberName(member) ?? node.name;
|
|
1749
|
+
if (!runtimeServerGlobals.includes(name) && !runtimeBrowserGlobals.includes(name))
|
|
1730
1750
|
return;
|
|
1731
1751
|
let scope = context.sourceCode.getScope(node);
|
|
1732
1752
|
while (scope) {
|
|
@@ -1738,9 +1758,9 @@ var tanstackRuntime = defineRule8({
|
|
|
1738
1758
|
const references = context.sourceCode.getScope(node).references;
|
|
1739
1759
|
if (!references.some((reference) => reference.identifier.range[0] === node.start && runtimeIdentifiers.has(reference.identifier.range[0])))
|
|
1740
1760
|
return;
|
|
1741
|
-
if (
|
|
1761
|
+
if (runtimeServerGlobals.includes(name) && (shared || !serverPosition(node.start)))
|
|
1742
1762
|
report(node, name, "server globals require a server execution boundary");
|
|
1743
|
-
if (
|
|
1763
|
+
if (runtimeBrowserGlobals.includes(name) && (serverFile || rscFile) && boundary(node.start)?.side !== "client")
|
|
1744
1764
|
report(node, name, "browser globals are unavailable during server execution");
|
|
1745
1765
|
}
|
|
1746
1766
|
return {
|
|
@@ -2023,6 +2043,158 @@ var memoization = defineRule10({
|
|
|
2023
2043
|
}
|
|
2024
2044
|
});
|
|
2025
2045
|
|
|
2046
|
+
// src/rules/module-policy/next-runtime.ts
|
|
2047
|
+
import { defineRule as defineRule11 } from "@oxlint/plugins";
|
|
2048
|
+
var nextRuntime = defineRule11({
|
|
2049
|
+
meta: {
|
|
2050
|
+
type: "problem",
|
|
2051
|
+
schema: [
|
|
2052
|
+
{
|
|
2053
|
+
type: "object",
|
|
2054
|
+
additionalProperties: false,
|
|
2055
|
+
properties: {
|
|
2056
|
+
web: { type: "array", items: { type: "string" } },
|
|
2057
|
+
backend: { type: "array", items: { type: "string" } },
|
|
2058
|
+
aliases: { type: "object", additionalProperties: { type: "string" } },
|
|
2059
|
+
backendPackages: { type: "array", items: { type: "string" } },
|
|
2060
|
+
serverImports: { type: "array", items: { type: "string" } },
|
|
2061
|
+
clientImports: { type: "array", items: { type: "string" } },
|
|
2062
|
+
allowComputedImportsIn: { type: "array", items: { type: "string" } }
|
|
2063
|
+
},
|
|
2064
|
+
required: ["web", "backend", "aliases", "backendPackages"]
|
|
2065
|
+
}
|
|
2066
|
+
],
|
|
2067
|
+
messages: {
|
|
2068
|
+
runtime: "{{source}} is not valid here: {{reason}}.",
|
|
2069
|
+
directive: "'{{directive}}' does not match this {{side}} module; rename the file or remove the directive.",
|
|
2070
|
+
action: "Server actions modules must start with 'use server'.",
|
|
2071
|
+
computed: "Use a literal dynamic import so runtime ownership can be checked, or declare this exact file in allowComputedImportsIn after reviewing its targets."
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
createOnce(context) {
|
|
2075
|
+
let options2;
|
|
2076
|
+
let file = "";
|
|
2077
|
+
let server = false;
|
|
2078
|
+
let client = false;
|
|
2079
|
+
let shared = false;
|
|
2080
|
+
const reactNamespaces = new Set;
|
|
2081
|
+
function report(node, source, reason) {
|
|
2082
|
+
context.report({ node, messageId: "runtime", data: { source, reason } });
|
|
2083
|
+
}
|
|
2084
|
+
function check(node, source, typeOnly) {
|
|
2085
|
+
if (typeOnly)
|
|
2086
|
+
return;
|
|
2087
|
+
const target = boundaryResolveTarget(options2, file, source);
|
|
2088
|
+
const serverOnly = runtimeIsServerPlatformImport(source) || runtimeIsBackendImport(options2, target, source) || /\.(?:server|rsc)(?:\.[cm]?[jt]sx?)?$/.test(target) || /^(?:server-only|next\/headers|next\/cache|next\/server)$/.test(source) || runtimeMatchesImport(options2.serverImports, source);
|
|
2089
|
+
const clientOnly = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:client-only|react-dom\/client)$/.test(source) || runtimeMatchesImport(options2.clientImports, source);
|
|
2090
|
+
if (clientOnly && (server || shared)) {
|
|
2091
|
+
report(node, source, "browser-only dependencies do not belong in server, RSC or shared modules");
|
|
2092
|
+
} else if (serverOnly && !server) {
|
|
2093
|
+
report(node, source, shared ? "shared utilities/constants/lib cannot import server-only dependencies" : "server-only dependencies belong in .server, .rsc or server action modules");
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function resolve3(node) {
|
|
2097
|
+
let scope = context.sourceCode.getScope(node);
|
|
2098
|
+
while (scope) {
|
|
2099
|
+
const variable = scope.set.get(node.name);
|
|
2100
|
+
if (variable?.defs.length)
|
|
2101
|
+
return variable;
|
|
2102
|
+
scope = scope.upper;
|
|
2103
|
+
}
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
function clientHook(name) {
|
|
2107
|
+
return /^use[A-Z]/.test(name) && !["useId", "use"].includes(name);
|
|
2108
|
+
}
|
|
2109
|
+
function inType(node) {
|
|
2110
|
+
return ["TSTypeQuery", "TSQualifiedName", "TSTypeReference"].includes(node.parent?.type ?? "");
|
|
2111
|
+
}
|
|
2112
|
+
function globalReferences() {
|
|
2113
|
+
const scope = context.sourceCode.scopeManager.scopes[0];
|
|
2114
|
+
if (!scope)
|
|
2115
|
+
return [];
|
|
2116
|
+
const implicit = [...scope.set.values()].filter((variable) => !variable.defs.length);
|
|
2117
|
+
return [...scope.through, ...implicit.flatMap((variable) => variable.references)].map((reference) => reference.identifier).filter((node) => node.type === "Identifier" && !inType(node));
|
|
2118
|
+
}
|
|
2119
|
+
function global(reference) {
|
|
2120
|
+
const owned = reference.name === "globalThis" ? runtimeMemberName(reference.parent) : null;
|
|
2121
|
+
const name = owned ?? reference.name;
|
|
2122
|
+
const serverGlobal = runtimeServerGlobals.includes(name);
|
|
2123
|
+
const browserGlobal = runtimeBrowserGlobals.includes(name);
|
|
2124
|
+
if (!serverGlobal && !browserGlobal)
|
|
2125
|
+
return;
|
|
2126
|
+
const node = owned ? reference.parent : reference;
|
|
2127
|
+
const env = node.parent;
|
|
2128
|
+
const key = env?.parent;
|
|
2129
|
+
const publicEnv = name === "process" && runtimeMemberName(env) === "env" && env?.type === "MemberExpression" && env.object === node && key?.type === "MemberExpression" && key.object === env && (runtimeMemberName(key) ?? "").startsWith("NEXT_PUBLIC_");
|
|
2130
|
+
if (serverGlobal && !server && !publicEnv && (client || shared))
|
|
2131
|
+
report(node, name, "server globals are unavailable in browser and shared modules");
|
|
2132
|
+
if (browserGlobal && server)
|
|
2133
|
+
report(node, name, "browser globals are unavailable during server execution");
|
|
2134
|
+
}
|
|
2135
|
+
return {
|
|
2136
|
+
Program(program) {
|
|
2137
|
+
options2 = optionsFirst(context);
|
|
2138
|
+
reactNamespaces.clear();
|
|
2139
|
+
file = context.filename.replaceAll("\\", "/");
|
|
2140
|
+
const directives2 = program.body.filter((statement) => statement.type === "ExpressionStatement" && ("directive" in statement) && Boolean(statement.directive)).map((statement) => ({ node: statement, value: statement.directive ?? "" }));
|
|
2141
|
+
const useServer = directives2.find((entry) => entry.value === "use server");
|
|
2142
|
+
const useClient = directives2.find((entry) => entry.value === "use client");
|
|
2143
|
+
const serverName = /\.(?:server|rsc|action)\.[cm]?[jt]sx?$/.test(file);
|
|
2144
|
+
const clientName = /\.client\.[cm]?[jt]sx?$/.test(file);
|
|
2145
|
+
shared = /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) || options2.web.some((path) => file.startsWith(path + "/lib/"));
|
|
2146
|
+
server = serverName || Boolean(useServer);
|
|
2147
|
+
client = clientName || Boolean(useClient);
|
|
2148
|
+
if (useClient && serverName)
|
|
2149
|
+
context.report({ node: useClient.node, messageId: "directive", data: { directive: "use client", side: "server" } });
|
|
2150
|
+
if (useServer && clientName)
|
|
2151
|
+
context.report({ node: useServer.node, messageId: "directive", data: { directive: "use server", side: "client" } });
|
|
2152
|
+
if (/\.action\.[cm]?[jt]sx?$/.test(file) && !useServer)
|
|
2153
|
+
context.report({ node: program, messageId: "action" });
|
|
2154
|
+
if (useClient)
|
|
2155
|
+
server = false;
|
|
2156
|
+
},
|
|
2157
|
+
ImportDeclaration(node) {
|
|
2158
|
+
const typeOnly = node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
2159
|
+
check(node, node.source.value, typeOnly);
|
|
2160
|
+
if (typeOnly || !(server || shared) || node.source.value !== "react")
|
|
2161
|
+
return;
|
|
2162
|
+
for (const specifier of node.specifiers) {
|
|
2163
|
+
if (specifier.type !== "ImportSpecifier")
|
|
2164
|
+
reactNamespaces.add(specifier);
|
|
2165
|
+
else if (specifier.importKind !== "type" && specifier.imported.type === "Identifier" && clientHook(specifier.imported.name))
|
|
2166
|
+
report(specifier, specifier.imported.name, "client hooks do not belong in server, RSC or shared modules");
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
MemberExpression(node) {
|
|
2170
|
+
const name = runtimeMemberName(node);
|
|
2171
|
+
if (!name || !clientHook(name) || node.object.type !== "Identifier")
|
|
2172
|
+
return;
|
|
2173
|
+
const binding = resolve3(node.object)?.defs[0]?.node;
|
|
2174
|
+
if (binding && reactNamespaces.has(binding))
|
|
2175
|
+
report(node, `${node.object.name}.${name}`, "client hooks do not belong in server, RSC or shared modules");
|
|
2176
|
+
},
|
|
2177
|
+
ExportNamedDeclaration(node) {
|
|
2178
|
+
if (node.source)
|
|
2179
|
+
check(node, node.source.value, node.exportKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.exportKind === "type"));
|
|
2180
|
+
},
|
|
2181
|
+
ExportAllDeclaration(node) {
|
|
2182
|
+
check(node, node.source.value, node.exportKind === "type");
|
|
2183
|
+
},
|
|
2184
|
+
ImportExpression(node) {
|
|
2185
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string")
|
|
2186
|
+
check(node, node.source.value, false);
|
|
2187
|
+
else if (!options2.allowComputedImportsIn?.includes(file))
|
|
2188
|
+
context.report({ node, messageId: "computed" });
|
|
2189
|
+
},
|
|
2190
|
+
"Program:exit"() {
|
|
2191
|
+
for (const reference of globalReferences())
|
|
2192
|
+
global(reference);
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2026
2198
|
// src/rules/module-policy/index.ts
|
|
2027
2199
|
var module_policy_default = eslintCompatPlugin({
|
|
2028
2200
|
meta: { name: "modules" },
|
|
@@ -2039,6 +2211,7 @@ var module_policy_default = eslintCompatPlugin({
|
|
|
2039
2211
|
"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."),
|
|
2040
2212
|
"no-unknown": syntaxRule("TSUnknownKeyword", "Use an owned domain type rather than unknown."),
|
|
2041
2213
|
memoization,
|
|
2214
|
+
"next-runtime": nextRuntime,
|
|
2042
2215
|
"pascal-interface": syntaxRule("TSInterfaceDeclaration:not([id.name=/^[A-Z][a-zA-Z0-9]*$/])", "Use PascalCase for interfaces."),
|
|
2043
2216
|
"reasoned-directives": directives,
|
|
2044
2217
|
"rpc-database": syntaxRule("CallExpression[callee.property.name=/^(selectFrom|insertInto|updateTable|deleteFrom|mergeInto|executeQuery)$/]", "RPC handlers delegate database work to services."),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks Next.js runtime ownership from file names and directives: `.server`/`.rsc` and `'use server'` modules stay off
|
|
3
|
+
* the browser, `.client` and `'use client'` modules stay off server-only APIs, and shared modules use neither.
|
|
4
|
+
*
|
|
5
|
+
* Example: `chat.client.ts` importing `next/headers` fails; `chat.server.ts` importing it passes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const nextRuntime: import("@oxlint/plugins").Rule;
|
|
@@ -26,6 +26,11 @@ export interface ModuleRuntimeOptions extends Pick<ModuleBoundaryOptions, 'web'
|
|
|
26
26
|
clientImports?: string[];
|
|
27
27
|
allowComputedImportsIn?: string[];
|
|
28
28
|
}
|
|
29
|
+
export interface ModuleNextRuntimeOptions extends Pick<ModuleBoundaryOptions, 'web' | 'backend' | 'aliases' | 'backendPackages'> {
|
|
30
|
+
serverImports?: string[];
|
|
31
|
+
clientImports?: string[];
|
|
32
|
+
allowComputedImportsIn?: string[];
|
|
33
|
+
}
|
|
29
34
|
export interface ModuleTestModifiersOptions {
|
|
30
35
|
additionalTestFunctions?: string[];
|
|
31
36
|
}
|
|
@@ -3,14 +3,21 @@ export type PresetPolicy = boolean | ((current: OxlintOverride[]) => OxlintOverr
|
|
|
3
3
|
export type PresetPolicyName = 'typePlacement' | 'typeSafety' | 'serviceStructure' | 'fileLayout' | 'naming' | 'comments' | 'formatting' | 'statementLayout' | 'imports' | 'react' | 'effects' | 'memoization' | 'routes' | 'forms' | 'schemas' | 'boundaries' | 'tests' | 'trivialFunctions' | 'mutableState' | 'backend' | 'tailwind' | 'shadcn' | 'rpcClientOwnership';
|
|
4
4
|
export type PresetRole = 'web' | 'server' | 'runner' | 'scripts' | 'packages';
|
|
5
5
|
export type PresetFolderLayout = 'flat' | 'domain';
|
|
6
|
+
export type PresetFramework = 'tanstack-start' | 'next';
|
|
6
7
|
export interface PresetArchitectureEntry {
|
|
7
8
|
role: PresetRole;
|
|
8
9
|
layout?: Record<string, PresetFolderLayout>;
|
|
10
|
+
/** Web roots only: selects the route and runtime-boundary policies. */
|
|
11
|
+
framework?: PresetFramework;
|
|
12
|
+
/** Web, server and runner roots only: the source folder inside the root; `src` when omitted, `.` for the root itself. */
|
|
13
|
+
sourceDir?: string;
|
|
9
14
|
}
|
|
10
15
|
export type PresetArchitecture = Record<string, PresetRole | PresetArchitectureEntry>;
|
|
11
16
|
export interface PresetResolvedRoot {
|
|
12
17
|
role: PresetRole;
|
|
13
18
|
layout: Record<string, PresetFolderLayout>;
|
|
19
|
+
framework?: PresetFramework;
|
|
20
|
+
sourceDir?: string;
|
|
14
21
|
}
|
|
15
22
|
export interface PresetFolderScope {
|
|
16
23
|
path: string;
|
|
@@ -19,8 +26,13 @@ export interface PresetFolderScope {
|
|
|
19
26
|
}
|
|
20
27
|
export interface PresetOptions {
|
|
21
28
|
root?: string;
|
|
29
|
+
/** Default framework for web roots that do not set their own; `tanstack-start` when omitted. */
|
|
30
|
+
framework?: PresetFramework;
|
|
22
31
|
architecture?: PresetArchitecture;
|
|
23
32
|
severity?: 'error' | 'warn';
|
|
33
|
+
/** Per-rule levels applied after `severity`, for rules the preset enables. */
|
|
34
|
+
ruleSeverities?: Record<string, 'error' | 'warn' | 'off'>;
|
|
35
|
+
formatting?: PresetFormattingOptions;
|
|
24
36
|
limits?: PresetLimitsOptions;
|
|
25
37
|
modules?: PresetModulesOptions;
|
|
26
38
|
imports?: PresetImportsOptions;
|
|
@@ -34,11 +46,16 @@ export interface PresetOptions {
|
|
|
34
46
|
ruleExclusions?: Record<string, string[]>;
|
|
35
47
|
cliFiles?: string[];
|
|
36
48
|
tanstackStart?: PresetTanstackStartOptions;
|
|
49
|
+
next?: PresetNextOptions;
|
|
37
50
|
tailwind?: false | PresetTailwindOptions;
|
|
38
51
|
shadcn?: false | PresetShadcnOptions;
|
|
39
52
|
}
|
|
40
53
|
export interface PresetPolicyOptions extends Partial<Record<PresetPolicyName, PresetPolicy>> {
|
|
41
54
|
}
|
|
55
|
+
export interface PresetFormattingOptions {
|
|
56
|
+
semi?: 'always' | 'never';
|
|
57
|
+
quotes?: 'single' | 'double';
|
|
58
|
+
}
|
|
42
59
|
export interface PresetLimitsOptions {
|
|
43
60
|
maxFileLines?: number;
|
|
44
61
|
maxFunctionComplexity?: number;
|
|
@@ -46,11 +63,32 @@ export interface PresetLimitsOptions {
|
|
|
46
63
|
export interface PresetModulesOptions {
|
|
47
64
|
customFileRoles?: string[];
|
|
48
65
|
migrationFiles?: string[];
|
|
66
|
+
/** Type files allowed outside `types/`; they follow the backend `types/` contract. */
|
|
67
|
+
colocatedTypeFiles?: string[];
|
|
49
68
|
}
|
|
50
69
|
export interface PresetTestsOptions {
|
|
51
70
|
profile?: 'standard' | 'fixtures';
|
|
52
71
|
additionalTestFunctions?: string[];
|
|
53
72
|
additionalAssertionFunctions?: string[];
|
|
73
|
+
/** Test-double APIs banned in test files; `modules` (module mocking only) when omitted. */
|
|
74
|
+
mocking?: PresetTestMockingPreset | PresetTestMockingOptions;
|
|
75
|
+
}
|
|
76
|
+
export type PresetTestMockingPreset = 'modules' | 'strict';
|
|
77
|
+
export interface PresetTestMockingApi {
|
|
78
|
+
api: string;
|
|
79
|
+
message?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface PresetTestMockingException {
|
|
82
|
+
files: string[];
|
|
83
|
+
/** API patterns allowed in these files; `*` allows every API. */
|
|
84
|
+
apis: string[];
|
|
85
|
+
}
|
|
86
|
+
export interface PresetTestMockingOptions {
|
|
87
|
+
preset?: PresetTestMockingPreset;
|
|
88
|
+
/** API patterns added to the preset list, such as `vi.*` or `jest.spyOn`. */
|
|
89
|
+
deny?: Array<string | PresetTestMockingApi>;
|
|
90
|
+
/** Per-file exceptions, added after the preset's own. */
|
|
91
|
+
allow?: PresetTestMockingException[];
|
|
54
92
|
}
|
|
55
93
|
export interface PresetImportsOptions {
|
|
56
94
|
backendEntryPoints?: string[];
|
|
@@ -77,6 +115,11 @@ export interface PresetTanstackStartOptions {
|
|
|
77
115
|
additionalClientOnlyImports?: string[];
|
|
78
116
|
computedImportAllowedFiles?: string[];
|
|
79
117
|
}
|
|
118
|
+
export interface PresetNextOptions {
|
|
119
|
+
additionalServerOnlyImports?: string[];
|
|
120
|
+
additionalClientOnlyImports?: string[];
|
|
121
|
+
computedImportAllowedFiles?: string[];
|
|
122
|
+
}
|
|
80
123
|
export interface PresetTailwindOptions {
|
|
81
124
|
cssEntryPoint?: string;
|
|
82
125
|
cssEntryPointsByRoot?: Record<string, string>;
|
|
@@ -107,7 +150,12 @@ export interface PresetContext {
|
|
|
107
150
|
css: Record<string, string>;
|
|
108
151
|
baseImports: PresetImportRestriction[];
|
|
109
152
|
web: string[];
|
|
153
|
+
/** Web `src` roots per framework. */
|
|
154
|
+
tanstackWeb: string[];
|
|
155
|
+
nextWeb: string[];
|
|
110
156
|
backend: string[];
|
|
157
|
+
/** Server and runner architecture roots, without their source folder. */
|
|
158
|
+
backendRoots: string[];
|
|
111
159
|
app: string[];
|
|
112
160
|
appFiles: string[];
|
|
113
161
|
webFiles: string[];
|
|
@@ -14,7 +14,7 @@ export declare function declarationsRuntimeName(node: DeclarationsRuntime): stri
|
|
|
14
14
|
export declare function declarationsRuntimeKind(node: DeclarationsRuntime): 'class' | 'enum' | 'function' | 'variable';
|
|
15
15
|
export declare function declarationsIsTrivialExpression(node: ESTree.Expression): boolean;
|
|
16
16
|
export declare function declarationsIsTrivialFunction(node: AstRuntimeFunction): boolean;
|
|
17
|
-
export declare const declarationsKinds: readonly [
|
|
17
|
+
export declare const declarationsKinds: readonly ['function', 'const', 'let', 'var', 'class', 'type', 'interface', 'enum'];
|
|
18
18
|
export type DeclarationsKind = (typeof declarationsKinds)[number];
|
|
19
19
|
export interface DeclarationsNamed {
|
|
20
20
|
name: string;
|
|
@@ -5,11 +5,13 @@ export * from './exports.ts';
|
|
|
5
5
|
export * from './factories.ts';
|
|
6
6
|
export * from './guards.ts';
|
|
7
7
|
export * from './layout.ts';
|
|
8
|
+
export * from './mocking.ts';
|
|
8
9
|
export * from './naming.ts';
|
|
9
10
|
export * from './options.ts';
|
|
10
11
|
export * from './orpc.ts';
|
|
11
12
|
export * from './react-components.ts';
|
|
12
13
|
export * from './routes.ts';
|
|
14
|
+
export * from './runtime.ts';
|
|
13
15
|
export * from './sanitizers.ts';
|
|
14
16
|
export * from './schemas.ts';
|
|
15
17
|
export * from './trivial.ts';
|
|
@@ -5,9 +5,9 @@ export declare const layoutIndentSchema: {
|
|
|
5
5
|
minimum: number;
|
|
6
6
|
enum?: undefined;
|
|
7
7
|
} | {
|
|
8
|
+
minimum?: undefined;
|
|
8
9
|
type: string;
|
|
9
10
|
enum: string[];
|
|
10
|
-
minimum?: undefined;
|
|
11
11
|
})[];
|
|
12
12
|
};
|
|
13
13
|
export declare function layoutLineIndent(text: string, offset: number): string;
|
|
@@ -6,7 +6,7 @@ export declare function namingFolderPrefixes(folders: string[], separator: strin
|
|
|
6
6
|
export declare function namingPascalCase(value: string): string;
|
|
7
7
|
export declare function namingCamelCase(value: string): string;
|
|
8
8
|
export declare function namingMatchTemplate(basename: string, template: string, placeholderPattern?: string): Record<string, string> | null;
|
|
9
|
-
export declare const namingStemModes: readonly [
|
|
9
|
+
export declare const namingStemModes: readonly ['before-first-dot', 'full-basename'];
|
|
10
10
|
export type NamingStemMode = (typeof namingStemModes)[number];
|
|
11
11
|
export declare function namingNormalized(value: string, normalize: 'remove-separators' | 'none'): string;
|
|
12
12
|
export declare function namingFileStem(basename: string, mode?: NamingStemMode, trailingRoles?: readonly string[], roleSeparators?: readonly string[]): string;
|
|
@@ -2,5 +2,6 @@ import type { OxlintOverride } from 'oxlint';
|
|
|
2
2
|
import type { PresetRules } from '../../types/preset.types.ts';
|
|
3
3
|
export declare function presetScopes(roots: string[], suffix?: string): string[];
|
|
4
4
|
export declare function presetOverride(files: string[], rules: PresetRules, excludeFiles?: string[]): OxlintOverride;
|
|
5
|
+
export declare function presetRuleSeverities(rules: PresetRules, severities: Record<string, 'error' | 'warn' | 'off'>): PresetRules;
|
|
5
6
|
export declare function presetPackageName(root: string, directory: string): string | undefined;
|
|
6
7
|
export declare function presetSeverity(rules: PresetRules, level: 'error' | 'warn'): PresetRules;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ESTree } from '@oxlint/plugins';
|
|
2
|
+
import type { ModuleBoundaryOptions } from '../../types/module-policy.types.ts';
|
|
3
|
+
/** Globals that only exist on the server runtime. */
|
|
4
|
+
export declare const runtimeServerGlobals: string[];
|
|
5
|
+
/** Globals that only exist in the browser. */
|
|
6
|
+
export declare const runtimeBrowserGlobals: string[];
|
|
7
|
+
/** Whether `source` is one of the configured module prefixes or a subpath of one. */
|
|
8
|
+
export declare function runtimeMatchesImport(values: string[] | undefined, source: string): boolean;
|
|
9
|
+
/** Whether an import reaches a configured backend root or a backend package's server/RPC entry. */
|
|
10
|
+
export declare function runtimeIsBackendImport(options: Pick<ModuleBoundaryOptions, 'backend' | 'backendPackages'>, target: string, source: string): boolean;
|
|
11
|
+
/** Whether an import is a server platform module: Node built-ins, Bun, Cloudflare, oRPC server or React DOM server. */
|
|
12
|
+
export declare function runtimeIsServerPlatformImport(source: string): boolean;
|
|
13
|
+
/** The static name of a member access: `a.b` and `a['b']` give `b`; anything else gives `null`. */
|
|
14
|
+
export declare function runtimeMemberName(node: ESTree.Node | null | undefined): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-arch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Configurable architecture rules for Oxlint JavaScript plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
"types": "./dist/presets/index.d.ts",
|
|
11
11
|
"default": "./dist/presets/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./presets/react-modules-preset": {
|
|
14
|
+
"types": "./dist/presets/react-modules-preset.d.ts",
|
|
15
|
+
"default": "./dist/presets/react-modules-preset.js"
|
|
16
|
+
},
|
|
17
|
+
"./presets/next-react-modules-preset": {
|
|
18
|
+
"types": "./dist/presets/next-react-modules-preset.d.ts",
|
|
19
|
+
"default": "./dist/presets/next-react-modules-preset.js"
|
|
20
|
+
},
|
|
13
21
|
"./presets/tanstack-start-react-modules-preset": {
|
|
14
22
|
"types": "./dist/presets/tanstack-start-react-modules-preset.d.ts",
|
|
15
23
|
"default": "./dist/presets/tanstack-start-react-modules-preset.js"
|
|
@@ -30,12 +38,12 @@
|
|
|
30
38
|
"dist"
|
|
31
39
|
],
|
|
32
40
|
"scripts": {
|
|
33
|
-
"build": "bun build src/index.ts src/presets/index.ts src/presets/tanstack-start-react-modules-preset.ts src/rules/dillon-anti-slop/index.ts src/rules/module-policy/index.ts --root src --outdir dist --target node --format esm --packages external && tsc --project tsconfig.build.json",
|
|
41
|
+
"build": "bun build src/index.ts src/presets/index.ts src/presets/react-modules-preset.ts src/presets/next-react-modules-preset.ts src/presets/tanstack-start-react-modules-preset.ts src/rules/dillon-anti-slop/index.ts src/rules/module-policy/index.ts --root src --outdir dist --target node --format esm --packages external && tsc --project tsconfig.build.json",
|
|
34
42
|
"check": "bun run lint && bun run build && bun run typecheck && bun run test && bun run check:examples",
|
|
35
43
|
"lint": "oxlint src examples scripts",
|
|
36
44
|
"test": "vitest run src/tests",
|
|
37
|
-
"typecheck": "
|
|
38
|
-
"check:examples": "oxlint examples/minimal.oxlint.config.ts -c examples/minimal.oxlint.config.ts --disable-nested-config && oxlint examples/full.oxlint.config.ts -c examples/full.oxlint.config.ts --disable-nested-config && oxlint examples/dist.oxlint.config.ts -c examples/dist.oxlint.config.ts --disable-nested-config && oxlint examples/monorepo.oxlint.config.ts -c examples/monorepo.oxlint.config.ts --disable-nested-config && oxlint --print-config -c examples/preset.oxlint.config.ts > /dev/null",
|
|
45
|
+
"typecheck": "tsrs --noEmit",
|
|
46
|
+
"check:examples": "oxlint examples/minimal.oxlint.config.ts -c examples/minimal.oxlint.config.ts --disable-nested-config && oxlint examples/full.oxlint.config.ts -c examples/full.oxlint.config.ts --disable-nested-config && oxlint examples/dist.oxlint.config.ts -c examples/dist.oxlint.config.ts --disable-nested-config && oxlint examples/monorepo.oxlint.config.ts -c examples/monorepo.oxlint.config.ts --disable-nested-config && oxlint --print-config -c examples/preset.oxlint.config.ts > /dev/null && oxlint --print-config -c examples/mixed-frameworks.oxlint.config.ts > /dev/null",
|
|
39
47
|
"prepublishOnly": "bun run check",
|
|
40
48
|
"release": "bash scripts/release.sh",
|
|
41
49
|
"release:patch": "bash scripts/release.sh patch --yes",
|
|
@@ -44,36 +52,37 @@
|
|
|
44
52
|
},
|
|
45
53
|
"dependencies": {
|
|
46
54
|
"@eslint-community/eslint-plugin-eslint-comments": "4.8.1",
|
|
47
|
-
"@oxlint/plugins": "1.
|
|
55
|
+
"@oxlint/plugins": "1.89.0",
|
|
48
56
|
"@regru/eslint-plugin-prefer-early-return": "1.0.0",
|
|
49
|
-
"@shadcn/lint": "0.
|
|
57
|
+
"@shadcn/lint": "0.2.3",
|
|
50
58
|
"eslint-plugin-import": "2.32.0",
|
|
51
|
-
"eslint-plugin-perfectionist": "5.
|
|
59
|
+
"eslint-plugin-perfectionist": "5.13.0",
|
|
52
60
|
"eslint-plugin-react": "7.37.5",
|
|
53
61
|
"eslint-plugin-react-you-might-not-need-an-effect": "1.0.2",
|
|
54
62
|
"eslint-plugin-svg-jsx": "1.3.0",
|
|
55
|
-
"eslint-plugin-tsdoc": "0.5.
|
|
63
|
+
"eslint-plugin-tsdoc": "0.5.4",
|
|
56
64
|
"eslint-plugin-unused-imports": "4.4.1",
|
|
57
|
-
"oxlint-plugin-eslint": "1.
|
|
58
|
-
"oxlint-tailwindcss": "1.
|
|
65
|
+
"oxlint-plugin-eslint": "1.89.0",
|
|
66
|
+
"oxlint-tailwindcss": "1.16.0"
|
|
59
67
|
},
|
|
60
68
|
"peerDependencies": {
|
|
61
69
|
"oxlint": ">=1.85.0",
|
|
62
70
|
"oxlint-tsgolint": ">=7.0.2003"
|
|
63
71
|
},
|
|
64
72
|
"devDependencies": {
|
|
65
|
-
"@orpc/server": "1.
|
|
66
|
-
"@tanstack/react-router": "1.170.
|
|
67
|
-
"@types/node": "26.
|
|
73
|
+
"@orpc/server": "1.16.1",
|
|
74
|
+
"@tanstack/react-router": "1.170.45",
|
|
75
|
+
"@types/node": "26.6.5",
|
|
68
76
|
"@types/react": "19.3.0",
|
|
69
|
-
"bumpp": "
|
|
70
|
-
"oxlint": "1.
|
|
71
|
-
"oxlint-tsgolint": "7.0.
|
|
72
|
-
"react": "19.3.
|
|
77
|
+
"bumpp": "12.3.0",
|
|
78
|
+
"oxlint": "1.89.0",
|
|
79
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
80
|
+
"react": "19.3.1",
|
|
73
81
|
"tailwindcss": "4.3.3",
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
82
|
+
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
|
|
83
|
+
"typescript": "7.0.2",
|
|
84
|
+
"vitest": "5.0.5",
|
|
85
|
+
"zod": "4.6.7"
|
|
77
86
|
},
|
|
78
87
|
"packageManager": "bun@1.3.14",
|
|
79
88
|
"license": "MIT",
|
|
@@ -17,3 +17,9 @@ vendoring and does not publish an official npm package.
|
|
|
17
17
|
Local behavior fixes recognize `SAFETY:` comments on exported declarations and
|
|
18
18
|
export-default assertions, while retaining statement boundaries for other
|
|
19
19
|
assertions. Covered by `src/tests/safety-comment.test.ts` and preset fixtures.
|
|
20
|
+
|
|
21
|
+
Local option changes let `no-module-mocking` take `deny` and `allow` API
|
|
22
|
+
patterns and `checkDestructured`, so the preset can also ban stubs such as
|
|
23
|
+
`vi.spyOn`. With no options
|
|
24
|
+
it reports the same calls as before. Covered by
|
|
25
|
+
`src/tests/no-module-mocking.test.ts` and preset tests.
|