@stll/oxlint-plugin 0.1.0 → 0.2.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/dist/index.cjs +11 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +8 -1
- package/dist/index.mjs.map +1 -1
- package/dist/no-partial-record-satisfies.mjs +1 -1
- package/dist/no-raw-route-query-client.cjs +203 -0
- package/dist/no-raw-route-query-client.cjs.map +1 -0
- package/dist/no-raw-route-query-client.d.cts +4 -0
- package/dist/no-raw-route-query-client.d.mts +5 -0
- package/dist/no-raw-route-query-client.mjs +199 -0
- package/dist/no-raw-route-query-client.mjs.map +1 -0
- package/dist/no-unsafe-inner-html.mjs +1 -1
- package/dist/require-loader-prefetch.cjs +187 -0
- package/dist/require-loader-prefetch.cjs.map +1 -0
- package/dist/require-loader-prefetch.d.cts +4 -0
- package/dist/require-loader-prefetch.d.mts +5 -0
- package/dist/require-loader-prefetch.mjs +160 -0
- package/dist/require-loader-prefetch.mjs.map +1 -0
- package/dist/utils.cjs +19 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.mjs +8 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +25 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
const require_utils = require("./utils.cjs");
|
|
28
|
+
let _oxlint_plugins = require("@oxlint/plugins");
|
|
29
|
+
let node_fs = require("node:fs");
|
|
30
|
+
let node_path = require("node:path");
|
|
31
|
+
node_path = __toESM(node_path, 1);
|
|
32
|
+
//#region src/require-loader-prefetch.ts
|
|
33
|
+
const CREATE_FILE_ROUTE = "createFileRoute";
|
|
34
|
+
const SUSPENSE_HOOK = "useSuspenseQuery";
|
|
35
|
+
const LOADER_KEY = "loader";
|
|
36
|
+
const isCreateFileRouteCall = (callee) => require_utils.isIdentifier(callee, CREATE_FILE_ROUTE);
|
|
37
|
+
const isCreateFileRouteOptionsCall = (callee) => callee?.type === "CallExpression" && isCreateFileRouteCall(callee.callee);
|
|
38
|
+
const isSuspenseCallee = (callee) => {
|
|
39
|
+
if (require_utils.isIdentifier(callee, SUSPENSE_HOOK)) return true;
|
|
40
|
+
return callee?.type === "MemberExpression" && callee.computed === false && require_utils.isIdentifier(callee.property, SUSPENSE_HOOK);
|
|
41
|
+
};
|
|
42
|
+
const suspenseFactoryName = (node) => {
|
|
43
|
+
if (!isSuspenseCallee(node.callee)) return null;
|
|
44
|
+
const arg = require_utils.unwrapExpression(node.arguments?.[0]);
|
|
45
|
+
if (arg?.type === "CallExpression" && require_utils.isIdentifier(arg.callee)) return arg.callee.name;
|
|
46
|
+
if (require_utils.isIdentifier(arg)) return arg.name;
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
const isInsideLoader = (node) => {
|
|
50
|
+
let current = node.parent;
|
|
51
|
+
while (current) {
|
|
52
|
+
if (current.type === "Property" && require_utils.getPropertyName(current.key) === LOADER_KEY) return true;
|
|
53
|
+
current = current.parent;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
const ROUTES_SEGMENT = "/routes/";
|
|
58
|
+
const COLOCATED_DIR_RE = /\/-[^/]+\//;
|
|
59
|
+
const isColocatedRouteImport = (source) => source.startsWith("@/routes/") && COLOCATED_DIR_RE.test(source);
|
|
60
|
+
const resolveColocatedImportBase = (filename, source) => {
|
|
61
|
+
const normalizedFilename = filename.replaceAll("\\", "/");
|
|
62
|
+
const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);
|
|
63
|
+
if (routesIndex === -1) return null;
|
|
64
|
+
const aliasRoot = normalizedFilename.slice(0, routesIndex);
|
|
65
|
+
return node_path.default.join(aliasRoot, source.slice(2));
|
|
66
|
+
};
|
|
67
|
+
const readColocatedFile = (basePath) => {
|
|
68
|
+
for (const extension of [".tsx", ".ts"]) try {
|
|
69
|
+
return (0, node_fs.readFileSync)(`${basePath}${extension}`, "utf-8");
|
|
70
|
+
} catch {}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
const CHILD_SUSPENSE_RE = /useSuspenseQuery\(\s*([A-Za-z_$][\w$]*)\s*([(),])/g;
|
|
74
|
+
const suspenseFactoriesInSource = (sourceText) => {
|
|
75
|
+
const factories = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {
|
|
77
|
+
const factory = match[1];
|
|
78
|
+
if (factory !== void 0) factories.add(factory);
|
|
79
|
+
}
|
|
80
|
+
return factories;
|
|
81
|
+
};
|
|
82
|
+
var require_loader_prefetch_default = (0, _oxlint_plugins.eslintCompatPlugin)({
|
|
83
|
+
meta: { name: "require-loader-prefetch" },
|
|
84
|
+
rules: { "require-loader-prefetch": {
|
|
85
|
+
meta: {
|
|
86
|
+
type: "problem",
|
|
87
|
+
messages: {
|
|
88
|
+
missingLoaderPrefetch: "Route component calls useSuspenseQuery({{factory}}(...)) but the route defines no `loader`, so this query fetches only after the component mounts and suspends — an extra sequential network round on cold navigation. Prefetch the same query options in the route `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query, so the fetch starts during navigation and useSuspenseQuery consumes the warm cache.",
|
|
89
|
+
factoryNotPrefetched: "Route component calls useSuspenseQuery({{factory}}(...)) but the route `loader` never references {{factory}}, so this query still fetches on mount — an extra sequential network round on cold navigation. Prefetch {{factory}}(...) in the `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query; useSuspenseQuery in the component then consumes the warm cache.",
|
|
90
|
+
childMissingLoaderPrefetch: "Route imports {{importPath}}, whose component calls useSuspenseQuery({{factory}}(...)), but the route defines no `loader`, so this query fetches only after the child component mounts and suspends — an extra sequential network round on cold navigation. Prefetch the same query options in the route `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query, so the fetch starts during navigation and useSuspenseQuery in the child component consumes the warm cache.",
|
|
91
|
+
childFactoryNotPrefetched: "Route imports {{importPath}}, whose component calls useSuspenseQuery({{factory}}(...)), but the route `loader` never references {{factory}}, so this query still fetches on mount — an extra sequential network round on cold navigation. Prefetch {{factory}}(...) in the `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query; useSuspenseQuery in the child component then consumes the warm cache."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
createOnce(context) {
|
|
95
|
+
let isRouteFile = false;
|
|
96
|
+
let routeOptions = null;
|
|
97
|
+
const suspenseCalls = [];
|
|
98
|
+
const referencedInLoader = /* @__PURE__ */ new Set();
|
|
99
|
+
const colocatedImports = [];
|
|
100
|
+
return {
|
|
101
|
+
before() {
|
|
102
|
+
isRouteFile = false;
|
|
103
|
+
routeOptions = null;
|
|
104
|
+
suspenseCalls.length = 0;
|
|
105
|
+
referencedInLoader.clear();
|
|
106
|
+
colocatedImports.length = 0;
|
|
107
|
+
},
|
|
108
|
+
CallExpression(node) {
|
|
109
|
+
const callee = node.callee;
|
|
110
|
+
if (isCreateFileRouteCall(callee)) isRouteFile = true;
|
|
111
|
+
if (isCreateFileRouteOptionsCall(callee)) {
|
|
112
|
+
isRouteFile = true;
|
|
113
|
+
const optionsArg = node.arguments?.[0];
|
|
114
|
+
if (routeOptions === null && optionsArg?.type === "ObjectExpression") routeOptions = optionsArg;
|
|
115
|
+
}
|
|
116
|
+
const factory = suspenseFactoryName(node);
|
|
117
|
+
if (factory !== null) suspenseCalls.push({
|
|
118
|
+
node,
|
|
119
|
+
factory
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
Identifier(node) {
|
|
123
|
+
if (isInsideLoader(node)) referencedInLoader.add(node.name);
|
|
124
|
+
},
|
|
125
|
+
ImportDeclaration(node) {
|
|
126
|
+
if (require_utils.isStringLiteral(node.source) && isColocatedRouteImport(node.source.value)) colocatedImports.push({
|
|
127
|
+
node,
|
|
128
|
+
source: node.source.value
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
"Program:exit"() {
|
|
132
|
+
if (!isRouteFile || routeOptions === null) return;
|
|
133
|
+
const hasLoader = (Array.isArray(routeOptions.properties) ? routeOptions.properties : []).some((prop) => prop.type === "Property" && require_utils.getPropertyName(prop.key) === LOADER_KEY);
|
|
134
|
+
for (const { node, factory } of suspenseCalls) {
|
|
135
|
+
if (!hasLoader) {
|
|
136
|
+
context.report({
|
|
137
|
+
node,
|
|
138
|
+
messageId: "missingLoaderPrefetch",
|
|
139
|
+
data: { factory }
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!referencedInLoader.has(factory)) context.report({
|
|
144
|
+
node,
|
|
145
|
+
messageId: "factoryNotPrefetched",
|
|
146
|
+
data: { factory }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (colocatedImports.length === 0) return;
|
|
150
|
+
const filename = context.filename ?? context.getFilename?.();
|
|
151
|
+
if (typeof filename !== "string") return;
|
|
152
|
+
for (const { node, source } of colocatedImports) {
|
|
153
|
+
const basePath = resolveColocatedImportBase(filename, source);
|
|
154
|
+
if (basePath === null) continue;
|
|
155
|
+
const childSource = readColocatedFile(basePath);
|
|
156
|
+
if (childSource === null) continue;
|
|
157
|
+
for (const factory of suspenseFactoriesInSource(childSource)) {
|
|
158
|
+
if (!hasLoader) {
|
|
159
|
+
context.report({
|
|
160
|
+
node,
|
|
161
|
+
messageId: "childMissingLoaderPrefetch",
|
|
162
|
+
data: {
|
|
163
|
+
factory,
|
|
164
|
+
importPath: source
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!referencedInLoader.has(factory)) context.report({
|
|
170
|
+
node,
|
|
171
|
+
messageId: "childFactoryNotPrefetched",
|
|
172
|
+
data: {
|
|
173
|
+
factory,
|
|
174
|
+
importPath: source
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
} }
|
|
183
|
+
});
|
|
184
|
+
//#endregion
|
|
185
|
+
exports.default = require_loader_prefetch_default;
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=require-loader-prefetch.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-loader-prefetch.cjs","names":["isIdentifier","unwrapExpression","getPropertyName","path","isStringLiteral"],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call living in\n// a sibling file imported by the route (the codebase convention for colocated,\n// non-route-generating files is a dash-prefixed directory next to the route,\n// e.g. `-components/`, `-hooks/`, `-queries/`, imported via the `@/routes/...`\n// alias — TanStack Router excludes `-`-prefixed paths from route generation).\n// A real instance of this shipped on `/settings/account/connections` before\n// being caught by the e2e depth guard: the page component had no\n// `useSuspenseQuery` of its own, it all lived in a child card component.\n//\n// To close that gap without a real module resolver: once a file is confirmed\n// to be a route file with an inline options object (the same precondition the\n// single-file check already requires), each `@/routes/.../-<dir>/...` import\n// it declares is resolved to an absolute path by reusing the *linted file's\n// own* path up to its `/routes/` segment as the alias root (mirrors the\n// `\"@/*\": [\"./src/*\"]` tsconfig path). The resolved file is read once — bounded\n// to that one hop, its own imports are never followed — and scanned with a\n// regex for the same two `useSuspenseQuery(...)` shapes the AST check\n// recognizes. Any factory it finds is checked against the route's `loader`\n// exactly like an in-file suspense call would be.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n getPropertyName,\n isIdentifier,\n isStringLiteral,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const arg = unwrapExpression(node.arguments?.[0]);\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match[1];\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const optionsArg = node.arguments?.[0];\n if (\n routeOptions === null &&\n optionsArg?.type === \"ObjectExpression\"\n ) {\n routeOptions = optionsArg;\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = context.filename ?? context.getFilename?.();\n if (typeof filename !== \"string\") {\n return;\n }\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { a as isStringLiteral, i as isIdentifier, o as unwrapExpression, r as getPropertyName } from "./utils.mjs";
|
|
2
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/require-loader-prefetch.ts
|
|
6
|
+
const CREATE_FILE_ROUTE = "createFileRoute";
|
|
7
|
+
const SUSPENSE_HOOK = "useSuspenseQuery";
|
|
8
|
+
const LOADER_KEY = "loader";
|
|
9
|
+
const isCreateFileRouteCall = (callee) => isIdentifier(callee, CREATE_FILE_ROUTE);
|
|
10
|
+
const isCreateFileRouteOptionsCall = (callee) => callee?.type === "CallExpression" && isCreateFileRouteCall(callee.callee);
|
|
11
|
+
const isSuspenseCallee = (callee) => {
|
|
12
|
+
if (isIdentifier(callee, SUSPENSE_HOOK)) return true;
|
|
13
|
+
return callee?.type === "MemberExpression" && callee.computed === false && isIdentifier(callee.property, SUSPENSE_HOOK);
|
|
14
|
+
};
|
|
15
|
+
const suspenseFactoryName = (node) => {
|
|
16
|
+
if (!isSuspenseCallee(node.callee)) return null;
|
|
17
|
+
const arg = unwrapExpression(node.arguments?.[0]);
|
|
18
|
+
if (arg?.type === "CallExpression" && isIdentifier(arg.callee)) return arg.callee.name;
|
|
19
|
+
if (isIdentifier(arg)) return arg.name;
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const isInsideLoader = (node) => {
|
|
23
|
+
let current = node.parent;
|
|
24
|
+
while (current) {
|
|
25
|
+
if (current.type === "Property" && getPropertyName(current.key) === LOADER_KEY) return true;
|
|
26
|
+
current = current.parent;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
const ROUTES_SEGMENT = "/routes/";
|
|
31
|
+
const COLOCATED_DIR_RE = /\/-[^/]+\//;
|
|
32
|
+
const isColocatedRouteImport = (source) => source.startsWith("@/routes/") && COLOCATED_DIR_RE.test(source);
|
|
33
|
+
const resolveColocatedImportBase = (filename, source) => {
|
|
34
|
+
const normalizedFilename = filename.replaceAll("\\", "/");
|
|
35
|
+
const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);
|
|
36
|
+
if (routesIndex === -1) return null;
|
|
37
|
+
const aliasRoot = normalizedFilename.slice(0, routesIndex);
|
|
38
|
+
return path.join(aliasRoot, source.slice(2));
|
|
39
|
+
};
|
|
40
|
+
const readColocatedFile = (basePath) => {
|
|
41
|
+
for (const extension of [".tsx", ".ts"]) try {
|
|
42
|
+
return readFileSync(`${basePath}${extension}`, "utf-8");
|
|
43
|
+
} catch {}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
const CHILD_SUSPENSE_RE = /useSuspenseQuery\(\s*([A-Za-z_$][\w$]*)\s*([(),])/g;
|
|
47
|
+
const suspenseFactoriesInSource = (sourceText) => {
|
|
48
|
+
const factories = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {
|
|
50
|
+
const factory = match[1];
|
|
51
|
+
if (factory !== void 0) factories.add(factory);
|
|
52
|
+
}
|
|
53
|
+
return factories;
|
|
54
|
+
};
|
|
55
|
+
var require_loader_prefetch_default = eslintCompatPlugin({
|
|
56
|
+
meta: { name: "require-loader-prefetch" },
|
|
57
|
+
rules: { "require-loader-prefetch": {
|
|
58
|
+
meta: {
|
|
59
|
+
type: "problem",
|
|
60
|
+
messages: {
|
|
61
|
+
missingLoaderPrefetch: "Route component calls useSuspenseQuery({{factory}}(...)) but the route defines no `loader`, so this query fetches only after the component mounts and suspends — an extra sequential network round on cold navigation. Prefetch the same query options in the route `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query, so the fetch starts during navigation and useSuspenseQuery consumes the warm cache.",
|
|
62
|
+
factoryNotPrefetched: "Route component calls useSuspenseQuery({{factory}}(...)) but the route `loader` never references {{factory}}, so this query still fetches on mount — an extra sequential network round on cold navigation. Prefetch {{factory}}(...) in the `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query; useSuspenseQuery in the component then consumes the warm cache.",
|
|
63
|
+
childMissingLoaderPrefetch: "Route imports {{importPath}}, whose component calls useSuspenseQuery({{factory}}(...)), but the route defines no `loader`, so this query fetches only after the child component mounts and suspends — an extra sequential network round on cold navigation. Prefetch the same query options in the route `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query, so the fetch starts during navigation and useSuspenseQuery in the child component consumes the warm cache.",
|
|
64
|
+
childFactoryNotPrefetched: "Route imports {{importPath}}, whose component calls useSuspenseQuery({{factory}}(...)), but the route `loader` never references {{factory}}, so this query still fetches on mount — an extra sequential network round on cold navigation. Prefetch {{factory}}(...) in the `loader` via ensureRouteQueryData (blocking, critical data) or prefetchRouteQuery (non-blocking) from @/lib/react-query; useSuspenseQuery in the child component then consumes the warm cache."
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
createOnce(context) {
|
|
68
|
+
let isRouteFile = false;
|
|
69
|
+
let routeOptions = null;
|
|
70
|
+
const suspenseCalls = [];
|
|
71
|
+
const referencedInLoader = /* @__PURE__ */ new Set();
|
|
72
|
+
const colocatedImports = [];
|
|
73
|
+
return {
|
|
74
|
+
before() {
|
|
75
|
+
isRouteFile = false;
|
|
76
|
+
routeOptions = null;
|
|
77
|
+
suspenseCalls.length = 0;
|
|
78
|
+
referencedInLoader.clear();
|
|
79
|
+
colocatedImports.length = 0;
|
|
80
|
+
},
|
|
81
|
+
CallExpression(node) {
|
|
82
|
+
const callee = node.callee;
|
|
83
|
+
if (isCreateFileRouteCall(callee)) isRouteFile = true;
|
|
84
|
+
if (isCreateFileRouteOptionsCall(callee)) {
|
|
85
|
+
isRouteFile = true;
|
|
86
|
+
const optionsArg = node.arguments?.[0];
|
|
87
|
+
if (routeOptions === null && optionsArg?.type === "ObjectExpression") routeOptions = optionsArg;
|
|
88
|
+
}
|
|
89
|
+
const factory = suspenseFactoryName(node);
|
|
90
|
+
if (factory !== null) suspenseCalls.push({
|
|
91
|
+
node,
|
|
92
|
+
factory
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
Identifier(node) {
|
|
96
|
+
if (isInsideLoader(node)) referencedInLoader.add(node.name);
|
|
97
|
+
},
|
|
98
|
+
ImportDeclaration(node) {
|
|
99
|
+
if (isStringLiteral(node.source) && isColocatedRouteImport(node.source.value)) colocatedImports.push({
|
|
100
|
+
node,
|
|
101
|
+
source: node.source.value
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
"Program:exit"() {
|
|
105
|
+
if (!isRouteFile || routeOptions === null) return;
|
|
106
|
+
const hasLoader = (Array.isArray(routeOptions.properties) ? routeOptions.properties : []).some((prop) => prop.type === "Property" && getPropertyName(prop.key) === LOADER_KEY);
|
|
107
|
+
for (const { node, factory } of suspenseCalls) {
|
|
108
|
+
if (!hasLoader) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "missingLoaderPrefetch",
|
|
112
|
+
data: { factory }
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!referencedInLoader.has(factory)) context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: "factoryNotPrefetched",
|
|
119
|
+
data: { factory }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (colocatedImports.length === 0) return;
|
|
123
|
+
const filename = context.filename ?? context.getFilename?.();
|
|
124
|
+
if (typeof filename !== "string") return;
|
|
125
|
+
for (const { node, source } of colocatedImports) {
|
|
126
|
+
const basePath = resolveColocatedImportBase(filename, source);
|
|
127
|
+
if (basePath === null) continue;
|
|
128
|
+
const childSource = readColocatedFile(basePath);
|
|
129
|
+
if (childSource === null) continue;
|
|
130
|
+
for (const factory of suspenseFactoriesInSource(childSource)) {
|
|
131
|
+
if (!hasLoader) {
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: "childMissingLoaderPrefetch",
|
|
135
|
+
data: {
|
|
136
|
+
factory,
|
|
137
|
+
importPath: source
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!referencedInLoader.has(factory)) context.report({
|
|
143
|
+
node,
|
|
144
|
+
messageId: "childFactoryNotPrefetched",
|
|
145
|
+
data: {
|
|
146
|
+
factory,
|
|
147
|
+
importPath: source
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
} }
|
|
156
|
+
});
|
|
157
|
+
//#endregion
|
|
158
|
+
export { require_loader_prefetch_default as default };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=require-loader-prefetch.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-loader-prefetch.mjs","names":[],"sources":["../src/require-loader-prefetch.ts"],"sourcesContent":["// Require route loaders to prefetch the queries their component suspends on.\n//\n// A route component that calls `useSuspenseQuery(someOptions(...))` without the\n// route loader prefetching that same query forces a render-fetch waterfall: on\n// a cold visit the router resolves the route, the component mounts, suspends,\n// and only THEN starts the fetch — an extra sequential network round on every\n// cold navigation. TanStack Router loaders (`loader:` in `createFileRoute`\n// options) start the fetch during navigation, in parallel with code-split\n// chunk loading, via the helpers in `apps/web/src/lib/react-query.ts`\n// (`ensureRouteQueryData` for critical/blocking data, `prefetchRouteQuery` for\n// non-blocking warmup). With the loader priming the cache, `useSuspenseQuery`\n// in the component consumes a warm cache instead of opening a new round.\n//\n// Single-file AST analysis, scoped to route files (`createFileRoute(...)`):\n// - Flags `useSuspenseQuery(factory(...))` / `useSuspenseQuery(factory)` when\n// the route has no `loader`.\n// - Flags the same when a `loader` exists but never references the factory\n// identifier (any reference inside the loader body counts — typically\n// inside `ensureRouteQueryData` / `prefetchRouteQuery`).\n//\n// Cross-file, one-hop check for colocated route components: the single-file\n// analysis above is blind to a `useSuspenseQuery(factory(...))` call living in\n// a sibling file imported by the route (the codebase convention for colocated,\n// non-route-generating files is a dash-prefixed directory next to the route,\n// e.g. `-components/`, `-hooks/`, `-queries/`, imported via the `@/routes/...`\n// alias — TanStack Router excludes `-`-prefixed paths from route generation).\n// A real instance of this shipped on `/settings/account/connections` before\n// being caught by the e2e depth guard: the page component had no\n// `useSuspenseQuery` of its own, it all lived in a child card component.\n//\n// To close that gap without a real module resolver: once a file is confirmed\n// to be a route file with an inline options object (the same precondition the\n// single-file check already requires), each `@/routes/.../-<dir>/...` import\n// it declares is resolved to an absolute path by reusing the *linted file's\n// own* path up to its `/routes/` segment as the alias root (mirrors the\n// `\"@/*\": [\"./src/*\"]` tsconfig path). The resolved file is read once — bounded\n// to that one hop, its own imports are never followed — and scanned with a\n// regex for the same two `useSuspenseQuery(...)` shapes the AST check\n// recognizes. Any factory it finds is checked against the route's `loader`\n// exactly like an in-file suspense call would be.\n//\n// Safe (not flagged):\n// - `useSuspenseQuery` whose argument is a member expression or other shape\n// the rule can't statically attribute to a factory identifier (in-file or\n// in a colocated import).\n// - `useSuspenseQuery` in a non-route file (no `createFileRoute`) — including\n// when that file is itself a route's colocated component and is linted\n// directly rather than through the importing route.\n// - A route whose loader references the factory identifier.\n// - A colocated import that isn't under a dash-prefixed directory, or that\n// can't be read as a sibling `.tsx`/`.ts` file (no fs read is attempted\n// unless the file is already a confirmed route file with such an import).\n//\n// Known limitation: the child-file scan is a raw-text regex, not a second AST\n// parse, so it does not strip comments or string literals. A comment or\n// string in the colocated file that happens to spell out\n// `useSuspenseQuery(identifier(` / `useSuspenseQuery(identifier)` verbatim\n// would false-positive. Judged an acceptable, rare edge case for the\n// bounded/cheap scan this rule relies on; do not write that literal pattern\n// in prose comments inside `-components`/`-hooks`/`-queries` files.\n\nimport { eslintCompatPlugin, type Ranged } from \"@oxlint/plugins\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n getPropertyName,\n isIdentifier,\n isStringLiteral,\n unwrapExpression,\n} from \"./utils.ts\";\n\nconst CREATE_FILE_ROUTE = \"createFileRoute\";\nconst SUSPENSE_HOOK = \"useSuspenseQuery\";\nconst LOADER_KEY = \"loader\";\n\nconst isCreateFileRouteCall = (callee) =>\n isIdentifier(callee, CREATE_FILE_ROUTE);\n\n// `createFileRoute(\"...\")({ ... })` — the outer call whose callee is itself the\n// `createFileRoute(\"...\")` call. The first argument is the route options object.\nconst isCreateFileRouteOptionsCall = (callee) =>\n callee?.type === \"CallExpression\" && isCreateFileRouteCall(callee.callee);\n\nconst isSuspenseCallee = (callee) => {\n if (isIdentifier(callee, SUSPENSE_HOOK)) {\n return true;\n }\n return (\n callee?.type === \"MemberExpression\" &&\n callee.computed === false &&\n isIdentifier(callee.property, SUSPENSE_HOOK)\n );\n};\n\n// Resolve the factory identifier a `useSuspenseQuery` call depends on:\n// useSuspenseQuery(factory(...)) -> \"factory\"\n// useSuspenseQuery(factory) -> \"factory\"\n// Returns null for any other argument shape (member expressions, object\n// literals, etc.) so the rule never guesses.\nconst suspenseFactoryName = (node) => {\n if (!isSuspenseCallee(node.callee)) {\n return null;\n }\n const arg = unwrapExpression(node.arguments?.[0]);\n if (arg?.type === \"CallExpression\" && isIdentifier(arg.callee)) {\n return arg.callee.name;\n }\n if (isIdentifier(arg)) {\n return arg.name;\n }\n return null;\n};\n\nconst isInsideLoader = (node) => {\n let current = node.parent;\n while (current) {\n if (\n current.type === \"Property\" &&\n getPropertyName(current.key) === LOADER_KEY\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n};\n\nconst ROUTES_SEGMENT = \"/routes/\";\nconst COLOCATED_DIR_RE = /\\/-[^/]+\\//;\n\n// A colocated, non-route-generating import: under `@/routes/...` and through\n// a dash-prefixed directory (`-components/`, `-hooks/`, `-queries/`, ...).\nconst isColocatedRouteImport = (source) =>\n source.startsWith(\"@/routes/\") && COLOCATED_DIR_RE.test(source);\n\n// Resolve a `@/routes/...` specifier to an absolute path by reusing the\n// currently-linted file's own path up to its `/routes/` segment as the alias\n// root — mirrors the `\"@/*\": [\"./src/*\"]` tsconfig path without a real module\n// resolver. Returns null if the linted file isn't itself under a `routes`\n// directory (shouldn't happen for a confirmed route file, but fails closed).\nconst resolveColocatedImportBase = (filename, source) => {\n // Windows gives `context.filename` with backslashes, which would make\n // `lastIndexOf(ROUTES_SEGMENT)` miss; normalize to forward slashes first\n // (valid path input on Windows, and `join` below normalizes the output).\n const normalizedFilename = filename.replaceAll(\"\\\\\", \"/\");\n const routesIndex = normalizedFilename.lastIndexOf(ROUTES_SEGMENT);\n if (routesIndex === -1) {\n return null;\n }\n const aliasRoot = normalizedFilename.slice(0, routesIndex);\n return path.join(aliasRoot, source.slice(\"@/\".length));\n};\n\n// Bounded, one-hop read: try the two extensions a colocated TS/TSX import can\n// resolve to. No further imports of the child file are followed.\nconst readColocatedFile = (basePath) => {\n for (const extension of [\".tsx\", \".ts\"]) {\n try {\n return readFileSync(`${basePath}${extension}`, \"utf-8\");\n } catch {\n // Try the next extension; if neither exists this is a shape the\n // simplified alias resolution can't handle (e.g. a directory index) —\n // acceptable since this is a best-effort bounded check, not a resolver.\n }\n }\n return null;\n};\n\n// Regex counterpart of `suspenseFactoryName`, scanning raw source text for\n// the same two argument shapes: `useSuspenseQuery(factory(...))` and\n// `useSuspenseQuery(factory)`. The character right after the identifier\n// disambiguates them from a member-expression argument the AST check would\n// also decline to attribute (e.g. `useSuspenseQuery(factory.options())`).\nconst CHILD_SUSPENSE_RE = /useSuspenseQuery\\(\\s*([A-Za-z_$][\\w$]*)\\s*([(),])/g;\n\nconst suspenseFactoriesInSource = (sourceText: string): Set<string> => {\n const factories = new Set<string>();\n for (const match of sourceText.matchAll(CHILD_SUSPENSE_RE)) {\n const factory = match[1];\n if (factory !== undefined) {\n factories.add(factory);\n }\n }\n return factories;\n};\n\nexport default eslintCompatPlugin({\n meta: { name: \"require-loader-prefetch\" },\n rules: {\n \"require-loader-prefetch\": {\n meta: {\n type: \"problem\",\n messages: {\n missingLoaderPrefetch:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route defines no `loader`, so this query fetches only after the \" +\n \"component mounts and suspends — an extra sequential network round \" +\n \"on cold navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery consumes the \" +\n \"warm cache.\",\n factoryNotPrefetched:\n \"Route component calls useSuspenseQuery({{factory}}(...)) but the \" +\n \"route `loader` never references {{factory}}, so this query still \" +\n \"fetches on mount — an extra sequential network round on cold \" +\n \"navigation. Prefetch {{factory}}(...) in the `loader` via \" +\n \"ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query; \" +\n \"useSuspenseQuery in the component then consumes the warm cache.\",\n childMissingLoaderPrefetch:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route defines no \" +\n \"`loader`, so this query fetches only after the child component \" +\n \"mounts and suspends — an extra sequential network round on cold \" +\n \"navigation. Prefetch the same query options in the route \" +\n \"`loader` via ensureRouteQueryData (blocking, critical data) or \" +\n \"prefetchRouteQuery (non-blocking) from @/lib/react-query, so the \" +\n \"fetch starts during navigation and useSuspenseQuery in the child \" +\n \"component consumes the warm cache.\",\n childFactoryNotPrefetched:\n \"Route imports {{importPath}}, whose component calls \" +\n \"useSuspenseQuery({{factory}}(...)), but the route `loader` never \" +\n \"references {{factory}}, so this query still fetches on mount — \" +\n \"an extra sequential network round on cold navigation. Prefetch \" +\n \"{{factory}}(...) in the `loader` via ensureRouteQueryData \" +\n \"(blocking, critical data) or prefetchRouteQuery (non-blocking) \" +\n \"from @/lib/react-query; useSuspenseQuery in the child component \" +\n \"then consumes the warm cache.\",\n },\n },\n createOnce(context) {\n let isRouteFile = false;\n let routeOptions: { properties: unknown } | null = null;\n const suspenseCalls: { factory: string; node: Ranged }[] = [];\n const referencedInLoader = new Set();\n const colocatedImports: { node: Ranged; source: string }[] = [];\n\n return {\n before() {\n isRouteFile = false;\n routeOptions = null;\n suspenseCalls.length = 0;\n referencedInLoader.clear();\n colocatedImports.length = 0;\n },\n CallExpression(node) {\n const callee = node.callee;\n\n if (isCreateFileRouteCall(callee)) {\n isRouteFile = true;\n }\n\n if (isCreateFileRouteOptionsCall(callee)) {\n isRouteFile = true;\n const optionsArg = node.arguments?.[0];\n if (\n routeOptions === null &&\n optionsArg?.type === \"ObjectExpression\"\n ) {\n routeOptions = optionsArg;\n }\n }\n\n const factory = suspenseFactoryName(node);\n if (factory !== null) {\n suspenseCalls.push({ node, factory });\n }\n },\n\n Identifier(node) {\n if (isInsideLoader(node)) {\n referencedInLoader.add(node.name);\n }\n },\n\n ImportDeclaration(node) {\n if (\n isStringLiteral(node.source) &&\n isColocatedRouteImport(node.source.value)\n ) {\n colocatedImports.push({ node, source: node.source.value });\n }\n },\n\n \"Program:exit\"() {\n // Only route files with an inline options object are analyzable;\n // a non-inline options argument can't be statically inspected.\n if (!isRouteFile || routeOptions === null) {\n return;\n }\n\n const properties = Array.isArray(routeOptions.properties)\n ? routeOptions.properties\n : [];\n const hasLoader = properties.some(\n (prop) =>\n prop.type === \"Property\" &&\n getPropertyName(prop.key) === LOADER_KEY,\n );\n\n for (const { node, factory } of suspenseCalls) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"missingLoaderPrefetch\",\n data: { factory },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"factoryNotPrefetched\",\n data: { factory },\n });\n }\n }\n\n if (colocatedImports.length === 0) {\n return;\n }\n\n const filename = context.filename ?? context.getFilename?.();\n if (typeof filename !== \"string\") {\n return;\n }\n\n for (const { node, source } of colocatedImports) {\n const basePath = resolveColocatedImportBase(filename, source);\n if (basePath === null) {\n continue;\n }\n\n const childSource = readColocatedFile(basePath);\n if (childSource === null) {\n continue;\n }\n\n for (const factory of suspenseFactoriesInSource(childSource)) {\n if (!hasLoader) {\n context.report({\n node,\n messageId: \"childMissingLoaderPrefetch\",\n data: { factory, importPath: source },\n });\n continue;\n }\n\n if (!referencedInLoader.has(factory)) {\n context.report({\n node,\n messageId: \"childFactoryNotPrefetched\",\n data: { factory, importPath: source },\n });\n }\n }\n }\n },\n };\n },\n },\n },\n});\n"],"mappings":"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"}
|
package/dist/utils.cjs
CHANGED
|
@@ -25,6 +25,13 @@ const unwrapExpression = (node) => {
|
|
|
25
25
|
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
|
|
26
26
|
return node;
|
|
27
27
|
};
|
|
28
|
+
const getImportedName = (specifier) => {
|
|
29
|
+
if (!isAstNode(specifier) || specifier.type !== "ImportSpecifier") return null;
|
|
30
|
+
const imported = specifier.imported;
|
|
31
|
+
if (isIdentifier(imported)) return imported.name;
|
|
32
|
+
if (isStringLiteral(imported)) return imported.value;
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
28
35
|
//#endregion
|
|
29
36
|
Object.defineProperty(exports, "getCalleeName", {
|
|
30
37
|
enumerable: true,
|
|
@@ -32,6 +39,12 @@ Object.defineProperty(exports, "getCalleeName", {
|
|
|
32
39
|
return getCalleeName;
|
|
33
40
|
}
|
|
34
41
|
});
|
|
42
|
+
Object.defineProperty(exports, "getImportedName", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
get: function() {
|
|
45
|
+
return getImportedName;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
35
48
|
Object.defineProperty(exports, "getPropertyName", {
|
|
36
49
|
enumerable: true,
|
|
37
50
|
get: function() {
|
|
@@ -44,6 +57,12 @@ Object.defineProperty(exports, "isIdentifier", {
|
|
|
44
57
|
return isIdentifier;
|
|
45
58
|
}
|
|
46
59
|
});
|
|
60
|
+
Object.defineProperty(exports, "isStringLiteral", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function() {
|
|
63
|
+
return isStringLiteral;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
47
66
|
Object.defineProperty(exports, "unwrapExpression", {
|
|
48
67
|
enumerable: true,
|
|
49
68
|
get: function() {
|
package/dist/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT;AAIA,MAAa,mBAAmB,cAAsC;CACpE,IAAI,CAAC,UAAU,SAAS,KAAK,UAAU,SAAS,mBAC9C,OAAO;CAET,MAAM,WAAW,UAAU;CAC3B,IAAI,aAAa,QAAQ,GACvB,OAAO,SAAS;CAElB,IAAI,gBAAgB,QAAQ,GAC1B,OAAO,SAAS;CAElB,OAAO;AACT"}
|
package/dist/utils.mjs
CHANGED
|
@@ -25,7 +25,14 @@ const unwrapExpression = (node) => {
|
|
|
25
25
|
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
|
|
26
26
|
return node;
|
|
27
27
|
};
|
|
28
|
+
const getImportedName = (specifier) => {
|
|
29
|
+
if (!isAstNode(specifier) || specifier.type !== "ImportSpecifier") return null;
|
|
30
|
+
const imported = specifier.imported;
|
|
31
|
+
if (isIdentifier(imported)) return imported.name;
|
|
32
|
+
if (isStringLiteral(imported)) return imported.value;
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
28
35
|
//#endregion
|
|
29
|
-
export {
|
|
36
|
+
export { isStringLiteral as a, isIdentifier as i, getImportedName as n, unwrapExpression as o, getPropertyName as r, getCalleeName as t };
|
|
30
37
|
|
|
31
38
|
//# sourceMappingURL=utils.mjs.map
|
package/dist/utils.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":";AAkBA,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AA8BA,MAAa,iBAAiB,WAAmC;CAC/D,IAAI,aAAa,MAAM,GACrB,OAAO,OAAO;CAEhB,IAAI,CAAC,UAAU,MAAM,KAAK,OAAO,SAAS,oBACxC,OAAO;CAET,IAAI,OAAO,aAAa,OACtB,OAAO;CAET,MAAM,aAAa,cAAc,OAAO,MAAM;CAC9C,MAAM,eAAe,gBAAgB,OAAO,QAAQ;CACpD,IAAI,iBAAiB,MACnB,OAAO;CAET,OAAO,eAAe,OAAO,eAAe,GAAG,WAAW,GAAG;AAC/D;AAIA,MAAa,oBAAoB,SAAkC;CACjE,IAAI,CAAC,UAAU,IAAI,GACjB,OAAO;CAET,IACE,KAAK,SAAS,oBACd,KAAK,SAAS,2BACd,KAAK,SAAS,mBAEd,OAAO,iBAAiB,KAAK,UAAU;CAEzC,OAAO;AACT;AAIA,MAAa,mBAAmB,cAAsC;CACpE,IAAI,CAAC,UAAU,SAAS,KAAK,UAAU,SAAS,mBAC9C,OAAO;CAET,MAAM,WAAW,UAAU;CAC3B,IAAI,aAAa,QAAQ,GACvB,OAAO,SAAS;CAElB,IAAI,gBAAgB,QAAQ,GAC1B,OAAO,SAAS;CAElB,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/oxlint-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Portable static-safety rules for Oxlint consumers.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Portable static-safety and route-query rules for Oxlint consumers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -69,6 +69,28 @@
|
|
|
69
69
|
"default": "./dist/no-unsafe-inner-html.cjs"
|
|
70
70
|
},
|
|
71
71
|
"default": "./dist/no-unsafe-inner-html.mjs"
|
|
72
|
+
},
|
|
73
|
+
"./no-raw-route-query-client": {
|
|
74
|
+
"import": {
|
|
75
|
+
"types": "./dist/no-raw-route-query-client.d.mts",
|
|
76
|
+
"default": "./dist/no-raw-route-query-client.mjs"
|
|
77
|
+
},
|
|
78
|
+
"require": {
|
|
79
|
+
"types": "./dist/no-raw-route-query-client.d.cts",
|
|
80
|
+
"default": "./dist/no-raw-route-query-client.cjs"
|
|
81
|
+
},
|
|
82
|
+
"default": "./dist/no-raw-route-query-client.mjs"
|
|
83
|
+
},
|
|
84
|
+
"./require-loader-prefetch": {
|
|
85
|
+
"import": {
|
|
86
|
+
"types": "./dist/require-loader-prefetch.d.mts",
|
|
87
|
+
"default": "./dist/require-loader-prefetch.mjs"
|
|
88
|
+
},
|
|
89
|
+
"require": {
|
|
90
|
+
"types": "./dist/require-loader-prefetch.d.cts",
|
|
91
|
+
"default": "./dist/require-loader-prefetch.cjs"
|
|
92
|
+
},
|
|
93
|
+
"default": "./dist/require-loader-prefetch.mjs"
|
|
72
94
|
}
|
|
73
95
|
},
|
|
74
96
|
"publishConfig": {
|
|
@@ -82,6 +104,7 @@
|
|
|
82
104
|
"devDependencies": {
|
|
83
105
|
"@oxlint/plugins": "1.75.0",
|
|
84
106
|
"@stll/typescript-config": "workspace:*",
|
|
107
|
+
"@types/node": "25.9.1",
|
|
85
108
|
"oxfmt": "0.48.0",
|
|
86
109
|
"typescript": "7.0.2"
|
|
87
110
|
},
|