oxlint-plugin-arch 0.6.2 → 0.7.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 +21 -0
- package/README.md +101 -515
- package/dist/constants/preset.constants.d.ts +6 -0
- package/dist/index.js +354 -102
- package/dist/presets/index.d.ts +3 -1
- package/dist/presets/index.js +247 -69
- package/dist/presets/next-react-modules-preset.d.ts +4 -0
- package/dist/presets/next-react-modules-preset.js +1570 -0
- package/dist/presets/react-modules-preset.d.ts +4 -0
- package/dist/presets/react-modules-preset.js +1565 -0
- package/dist/presets/tanstack-start-react-modules-preset.d.ts +1 -0
- package/dist/presets/tanstack-start-react-modules-preset.js +239 -68
- package/dist/rules/module-policy/index.js +202 -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 +29 -0
- package/dist/utils/helpers/declarations.d.ts +1 -1
- package/dist/utils/helpers/index.d.ts +1 -0
- package/dist/utils/helpers/layout.d.ts +1 -1
- 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
|
@@ -935,6 +935,49 @@ function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
|
|
|
935
935
|
}
|
|
936
936
|
});
|
|
937
937
|
}
|
|
938
|
+
// src/utils/helpers/runtime.ts
|
|
939
|
+
import { builtinModules } from "node:module";
|
|
940
|
+
var runtimeServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
941
|
+
var runtimeBrowserGlobals = [
|
|
942
|
+
"window",
|
|
943
|
+
"document",
|
|
944
|
+
"navigator",
|
|
945
|
+
"localStorage",
|
|
946
|
+
"sessionStorage",
|
|
947
|
+
"location",
|
|
948
|
+
"history",
|
|
949
|
+
"screen",
|
|
950
|
+
"HTMLElement",
|
|
951
|
+
"Element",
|
|
952
|
+
"HTMLInputElement",
|
|
953
|
+
"ResizeObserver",
|
|
954
|
+
"MutationObserver",
|
|
955
|
+
"IntersectionObserver",
|
|
956
|
+
"FileReader",
|
|
957
|
+
"matchMedia",
|
|
958
|
+
"getComputedStyle",
|
|
959
|
+
"requestAnimationFrame",
|
|
960
|
+
"cancelAnimationFrame",
|
|
961
|
+
"alert",
|
|
962
|
+
"confirm",
|
|
963
|
+
"prompt"
|
|
964
|
+
];
|
|
965
|
+
function runtimeMatchesImport(values, source) {
|
|
966
|
+
return Boolean(values?.some((value) => source === value || source.startsWith(value + "/")));
|
|
967
|
+
}
|
|
968
|
+
function runtimeIsBackendImport(options, target, source) {
|
|
969
|
+
return options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
970
|
+
}
|
|
971
|
+
function runtimeIsServerPlatformImport(source) {
|
|
972
|
+
return builtinModules.includes(source) || source === "@orpc/server" || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source);
|
|
973
|
+
}
|
|
974
|
+
function runtimeMemberName(node) {
|
|
975
|
+
if (node?.type !== "MemberExpression")
|
|
976
|
+
return null;
|
|
977
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
978
|
+
return node.property.name;
|
|
979
|
+
return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
|
|
980
|
+
}
|
|
938
981
|
// src/utils/helpers/sanitizers.ts
|
|
939
982
|
function sanitizersCallsConfigured(node, names) {
|
|
940
983
|
let found = false;
|
|
@@ -1589,7 +1632,6 @@ var directives = defineRule7({
|
|
|
1589
1632
|
});
|
|
1590
1633
|
|
|
1591
1634
|
// src/rules/module-policy/tanstack-runtime.ts
|
|
1592
|
-
import { builtinModules } from "node:module";
|
|
1593
1635
|
import { defineRule as defineRule8 } from "@oxlint/plugins";
|
|
1594
1636
|
var tanstackRuntime = defineRule8({
|
|
1595
1637
|
meta: {
|
|
@@ -1667,10 +1709,8 @@ var tanstackRuntime = defineRule8({
|
|
|
1667
1709
|
if (typeOnly)
|
|
1668
1710
|
return;
|
|
1669
1711
|
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);
|
|
1712
|
+
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);
|
|
1713
|
+
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || runtimeMatchesImport(options2.clientImports, source);
|
|
1674
1714
|
if (client && (serverFile || rscFile || shared) && boundary(node.start)?.side !== "client") {
|
|
1675
1715
|
const references = node.type === "ImportDeclaration" ? context.sourceCode.getDeclaredVariables(node).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0]))) : [];
|
|
1676
1716
|
if (!shared && references.length && references.every((reference) => boundary(reference.identifier.range[0])?.side === "client"))
|
|
@@ -1699,34 +1739,9 @@ var tanstackRuntime = defineRule8({
|
|
|
1699
1739
|
report(node, source, "use an inline imported TanStack server boundary; static isomorphic server imports belong only to the configured RPC owner");
|
|
1700
1740
|
}
|
|
1701
1741
|
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
1742
|
const member = node.parent.type === "MemberExpression" && node.parent.object === node && node.name === "globalThis" ? node.parent : undefined;
|
|
1728
|
-
const name = member
|
|
1729
|
-
if (!
|
|
1743
|
+
const name = runtimeMemberName(member) ?? node.name;
|
|
1744
|
+
if (!runtimeServerGlobals.includes(name) && !runtimeBrowserGlobals.includes(name))
|
|
1730
1745
|
return;
|
|
1731
1746
|
let scope = context.sourceCode.getScope(node);
|
|
1732
1747
|
while (scope) {
|
|
@@ -1738,9 +1753,9 @@ var tanstackRuntime = defineRule8({
|
|
|
1738
1753
|
const references = context.sourceCode.getScope(node).references;
|
|
1739
1754
|
if (!references.some((reference) => reference.identifier.range[0] === node.start && runtimeIdentifiers.has(reference.identifier.range[0])))
|
|
1740
1755
|
return;
|
|
1741
|
-
if (
|
|
1756
|
+
if (runtimeServerGlobals.includes(name) && (shared || !serverPosition(node.start)))
|
|
1742
1757
|
report(node, name, "server globals require a server execution boundary");
|
|
1743
|
-
if (
|
|
1758
|
+
if (runtimeBrowserGlobals.includes(name) && (serverFile || rscFile) && boundary(node.start)?.side !== "client")
|
|
1744
1759
|
report(node, name, "browser globals are unavailable during server execution");
|
|
1745
1760
|
}
|
|
1746
1761
|
return {
|
|
@@ -2023,6 +2038,158 @@ var memoization = defineRule10({
|
|
|
2023
2038
|
}
|
|
2024
2039
|
});
|
|
2025
2040
|
|
|
2041
|
+
// src/rules/module-policy/next-runtime.ts
|
|
2042
|
+
import { defineRule as defineRule11 } from "@oxlint/plugins";
|
|
2043
|
+
var nextRuntime = defineRule11({
|
|
2044
|
+
meta: {
|
|
2045
|
+
type: "problem",
|
|
2046
|
+
schema: [
|
|
2047
|
+
{
|
|
2048
|
+
type: "object",
|
|
2049
|
+
additionalProperties: false,
|
|
2050
|
+
properties: {
|
|
2051
|
+
web: { type: "array", items: { type: "string" } },
|
|
2052
|
+
backend: { type: "array", items: { type: "string" } },
|
|
2053
|
+
aliases: { type: "object", additionalProperties: { type: "string" } },
|
|
2054
|
+
backendPackages: { type: "array", items: { type: "string" } },
|
|
2055
|
+
serverImports: { type: "array", items: { type: "string" } },
|
|
2056
|
+
clientImports: { type: "array", items: { type: "string" } },
|
|
2057
|
+
allowComputedImportsIn: { type: "array", items: { type: "string" } }
|
|
2058
|
+
},
|
|
2059
|
+
required: ["web", "backend", "aliases", "backendPackages"]
|
|
2060
|
+
}
|
|
2061
|
+
],
|
|
2062
|
+
messages: {
|
|
2063
|
+
runtime: "{{source}} is not valid here: {{reason}}.",
|
|
2064
|
+
directive: "'{{directive}}' does not match this {{side}} module; rename the file or remove the directive.",
|
|
2065
|
+
action: "Server actions modules must start with 'use server'.",
|
|
2066
|
+
computed: "Use a literal dynamic import so runtime ownership can be checked, or declare this exact file in allowComputedImportsIn after reviewing its targets."
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
createOnce(context) {
|
|
2070
|
+
let options2;
|
|
2071
|
+
let file = "";
|
|
2072
|
+
let server = false;
|
|
2073
|
+
let client = false;
|
|
2074
|
+
let shared = false;
|
|
2075
|
+
const reactNamespaces = new Set;
|
|
2076
|
+
function report(node, source, reason) {
|
|
2077
|
+
context.report({ node, messageId: "runtime", data: { source, reason } });
|
|
2078
|
+
}
|
|
2079
|
+
function check(node, source, typeOnly) {
|
|
2080
|
+
if (typeOnly)
|
|
2081
|
+
return;
|
|
2082
|
+
const target = boundaryResolveTarget(options2, file, source);
|
|
2083
|
+
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);
|
|
2084
|
+
const clientOnly = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:client-only|react-dom\/client)$/.test(source) || runtimeMatchesImport(options2.clientImports, source);
|
|
2085
|
+
if (clientOnly && (server || shared)) {
|
|
2086
|
+
report(node, source, "browser-only dependencies do not belong in server, RSC or shared modules");
|
|
2087
|
+
} else if (serverOnly && !server) {
|
|
2088
|
+
report(node, source, shared ? "shared utilities/constants/lib cannot import server-only dependencies" : "server-only dependencies belong in .server, .rsc or server action modules");
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function resolve3(node) {
|
|
2092
|
+
let scope = context.sourceCode.getScope(node);
|
|
2093
|
+
while (scope) {
|
|
2094
|
+
const variable = scope.set.get(node.name);
|
|
2095
|
+
if (variable?.defs.length)
|
|
2096
|
+
return variable;
|
|
2097
|
+
scope = scope.upper;
|
|
2098
|
+
}
|
|
2099
|
+
return null;
|
|
2100
|
+
}
|
|
2101
|
+
function clientHook(name) {
|
|
2102
|
+
return /^use[A-Z]/.test(name) && !["useId", "use"].includes(name);
|
|
2103
|
+
}
|
|
2104
|
+
function inType(node) {
|
|
2105
|
+
return ["TSTypeQuery", "TSQualifiedName", "TSTypeReference"].includes(node.parent?.type ?? "");
|
|
2106
|
+
}
|
|
2107
|
+
function globalReferences() {
|
|
2108
|
+
const scope = context.sourceCode.scopeManager.scopes[0];
|
|
2109
|
+
if (!scope)
|
|
2110
|
+
return [];
|
|
2111
|
+
const implicit = [...scope.set.values()].filter((variable) => !variable.defs.length);
|
|
2112
|
+
return [...scope.through, ...implicit.flatMap((variable) => variable.references)].map((reference) => reference.identifier).filter((node) => node.type === "Identifier" && !inType(node));
|
|
2113
|
+
}
|
|
2114
|
+
function global(reference) {
|
|
2115
|
+
const owned = reference.name === "globalThis" ? runtimeMemberName(reference.parent) : null;
|
|
2116
|
+
const name = owned ?? reference.name;
|
|
2117
|
+
const serverGlobal = runtimeServerGlobals.includes(name);
|
|
2118
|
+
const browserGlobal = runtimeBrowserGlobals.includes(name);
|
|
2119
|
+
if (!serverGlobal && !browserGlobal)
|
|
2120
|
+
return;
|
|
2121
|
+
const node = owned ? reference.parent : reference;
|
|
2122
|
+
const env = node.parent;
|
|
2123
|
+
const key = env?.parent;
|
|
2124
|
+
const publicEnv = name === "process" && runtimeMemberName(env) === "env" && env?.type === "MemberExpression" && env.object === node && key?.type === "MemberExpression" && key.object === env && (runtimeMemberName(key) ?? "").startsWith("NEXT_PUBLIC_");
|
|
2125
|
+
if (serverGlobal && !server && !publicEnv && (client || shared))
|
|
2126
|
+
report(node, name, "server globals are unavailable in browser and shared modules");
|
|
2127
|
+
if (browserGlobal && server)
|
|
2128
|
+
report(node, name, "browser globals are unavailable during server execution");
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
Program(program) {
|
|
2132
|
+
options2 = optionsFirst(context);
|
|
2133
|
+
reactNamespaces.clear();
|
|
2134
|
+
file = context.filename.replaceAll("\\", "/");
|
|
2135
|
+
const directives2 = program.body.filter((statement) => statement.type === "ExpressionStatement" && ("directive" in statement) && Boolean(statement.directive)).map((statement) => ({ node: statement, value: statement.directive ?? "" }));
|
|
2136
|
+
const useServer = directives2.find((entry) => entry.value === "use server");
|
|
2137
|
+
const useClient = directives2.find((entry) => entry.value === "use client");
|
|
2138
|
+
const serverName = /\.(?:server|rsc|action)\.[cm]?[jt]sx?$/.test(file);
|
|
2139
|
+
const clientName = /\.client\.[cm]?[jt]sx?$/.test(file);
|
|
2140
|
+
shared = /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) || options2.web.some((path) => file.startsWith(path + "/lib/"));
|
|
2141
|
+
server = serverName || Boolean(useServer);
|
|
2142
|
+
client = clientName || Boolean(useClient);
|
|
2143
|
+
if (useClient && serverName)
|
|
2144
|
+
context.report({ node: useClient.node, messageId: "directive", data: { directive: "use client", side: "server" } });
|
|
2145
|
+
if (useServer && clientName)
|
|
2146
|
+
context.report({ node: useServer.node, messageId: "directive", data: { directive: "use server", side: "client" } });
|
|
2147
|
+
if (/\.action\.[cm]?[jt]sx?$/.test(file) && !useServer)
|
|
2148
|
+
context.report({ node: program, messageId: "action" });
|
|
2149
|
+
if (useClient)
|
|
2150
|
+
server = false;
|
|
2151
|
+
},
|
|
2152
|
+
ImportDeclaration(node) {
|
|
2153
|
+
const typeOnly = node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
2154
|
+
check(node, node.source.value, typeOnly);
|
|
2155
|
+
if (typeOnly || !(server || shared) || node.source.value !== "react")
|
|
2156
|
+
return;
|
|
2157
|
+
for (const specifier of node.specifiers) {
|
|
2158
|
+
if (specifier.type !== "ImportSpecifier")
|
|
2159
|
+
reactNamespaces.add(specifier);
|
|
2160
|
+
else if (specifier.importKind !== "type" && specifier.imported.type === "Identifier" && clientHook(specifier.imported.name))
|
|
2161
|
+
report(specifier, specifier.imported.name, "client hooks do not belong in server, RSC or shared modules");
|
|
2162
|
+
}
|
|
2163
|
+
},
|
|
2164
|
+
MemberExpression(node) {
|
|
2165
|
+
const name = runtimeMemberName(node);
|
|
2166
|
+
if (!name || !clientHook(name) || node.object.type !== "Identifier")
|
|
2167
|
+
return;
|
|
2168
|
+
const binding = resolve3(node.object)?.defs[0]?.node;
|
|
2169
|
+
if (binding && reactNamespaces.has(binding))
|
|
2170
|
+
report(node, `${node.object.name}.${name}`, "client hooks do not belong in server, RSC or shared modules");
|
|
2171
|
+
},
|
|
2172
|
+
ExportNamedDeclaration(node) {
|
|
2173
|
+
if (node.source)
|
|
2174
|
+
check(node, node.source.value, node.exportKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.exportKind === "type"));
|
|
2175
|
+
},
|
|
2176
|
+
ExportAllDeclaration(node) {
|
|
2177
|
+
check(node, node.source.value, node.exportKind === "type");
|
|
2178
|
+
},
|
|
2179
|
+
ImportExpression(node) {
|
|
2180
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string")
|
|
2181
|
+
check(node, node.source.value, false);
|
|
2182
|
+
else if (!options2.allowComputedImportsIn?.includes(file))
|
|
2183
|
+
context.report({ node, messageId: "computed" });
|
|
2184
|
+
},
|
|
2185
|
+
"Program:exit"() {
|
|
2186
|
+
for (const reference of globalReferences())
|
|
2187
|
+
global(reference);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2026
2193
|
// src/rules/module-policy/index.ts
|
|
2027
2194
|
var module_policy_default = eslintCompatPlugin({
|
|
2028
2195
|
meta: { name: "modules" },
|
|
@@ -2039,6 +2206,7 @@ var module_policy_default = eslintCompatPlugin({
|
|
|
2039
2206
|
"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
2207
|
"no-unknown": syntaxRule("TSUnknownKeyword", "Use an owned domain type rather than unknown."),
|
|
2041
2208
|
memoization,
|
|
2209
|
+
"next-runtime": nextRuntime,
|
|
2042
2210
|
"pascal-interface": syntaxRule("TSInterfaceDeclaration:not([id.name=/^[A-Z][a-zA-Z0-9]*$/])", "Use PascalCase for interfaces."),
|
|
2043
2211
|
"reasoned-directives": directives,
|
|
2044
2212
|
"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,6 +63,8 @@ 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';
|
|
@@ -77,6 +96,11 @@ export interface PresetTanstackStartOptions {
|
|
|
77
96
|
additionalClientOnlyImports?: string[];
|
|
78
97
|
computedImportAllowedFiles?: string[];
|
|
79
98
|
}
|
|
99
|
+
export interface PresetNextOptions {
|
|
100
|
+
additionalServerOnlyImports?: string[];
|
|
101
|
+
additionalClientOnlyImports?: string[];
|
|
102
|
+
computedImportAllowedFiles?: string[];
|
|
103
|
+
}
|
|
80
104
|
export interface PresetTailwindOptions {
|
|
81
105
|
cssEntryPoint?: string;
|
|
82
106
|
cssEntryPointsByRoot?: Record<string, string>;
|
|
@@ -107,7 +131,12 @@ export interface PresetContext {
|
|
|
107
131
|
css: Record<string, string>;
|
|
108
132
|
baseImports: PresetImportRestriction[];
|
|
109
133
|
web: string[];
|
|
134
|
+
/** Web `src` roots per framework. */
|
|
135
|
+
tanstackWeb: string[];
|
|
136
|
+
nextWeb: string[];
|
|
110
137
|
backend: string[];
|
|
138
|
+
/** Server and runner architecture roots, without their source folder. */
|
|
139
|
+
backendRoots: string[];
|
|
111
140
|
app: string[];
|
|
112
141
|
appFiles: string[];
|
|
113
142
|
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;
|
|
@@ -10,6 +10,7 @@ export * from './options.ts';
|
|
|
10
10
|
export * from './orpc.ts';
|
|
11
11
|
export * from './react-components.ts';
|
|
12
12
|
export * from './routes.ts';
|
|
13
|
+
export * from './runtime.ts';
|
|
13
14
|
export * from './sanitizers.ts';
|
|
14
15
|
export * from './schemas.ts';
|
|
15
16
|
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.7.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",
|