@spa-kit/eslint-plugin 0.0.0-stage → 0.1.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/README.md +93 -2
- package/dist/index.cjs +161 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -4
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @spa-kit/eslint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Opt-in ESLint rules for spa-kit applications. This package is development tooling;
|
|
4
|
+
it does not add lint dependencies to `@spa-kit/react-router`.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Install ESLint, a TypeScript parser, and the plugin as development dependencies:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install --save-dev eslint @typescript-eslint/parser @spa-kit/eslint-plugin
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
ESLint 9 and 10 are supported. Use a Node.js version supported by your ESLint release.
|
|
15
|
+
|
|
16
|
+
Add the rule to your flat configuration (`eslint.config.mjs`):
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import tsParser from "@typescript-eslint/parser";
|
|
20
|
+
import spaKit from "@spa-kit/eslint-plugin";
|
|
21
|
+
|
|
22
|
+
export default [
|
|
23
|
+
{
|
|
24
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
25
|
+
languageOptions: {
|
|
26
|
+
parser: tsParser,
|
|
27
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
28
|
+
},
|
|
29
|
+
plugins: { "@spa-kit": spaKit },
|
|
30
|
+
rules: {
|
|
31
|
+
"@spa-kit/require-preload-context": "error",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If your configuration already selects a TypeScript parser, add only the plugin
|
|
38
|
+
and rule entries. Installing or registering the plugin does not enable the rule.
|
|
39
|
+
No TypeScript project or parser services are required.
|
|
40
|
+
|
|
41
|
+
## `require-preload-context`
|
|
42
|
+
|
|
43
|
+
Require a parameter type annotation on `preload` callbacks in route configurations
|
|
44
|
+
passed to `createSpaRouter` from `@spa-kit/react-router`. This preserves inference
|
|
45
|
+
of the preload return type in the sibling `render` callback.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { createSpaRouter } from "@spa-kit/react-router";
|
|
49
|
+
import { WikiRoutes } from "./generated/WikiRoutes";
|
|
50
|
+
import type { ViewContext } from "./generated/WikiRoutes";
|
|
51
|
+
|
|
52
|
+
createSpaRouter(WikiRoutes, {
|
|
53
|
+
View: {
|
|
54
|
+
preload: ({ params }: ViewContext) => loadQuery(environment, Query, {
|
|
55
|
+
wikiId: params.wikiId,
|
|
56
|
+
}),
|
|
57
|
+
render: ({ preload }) => <ViewWikiPage queryRef={preload} />,
|
|
58
|
+
},
|
|
59
|
+
// Other generated routes omitted here.
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
These callbacks are reported:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
preload: ({ params }) => loadQuery(/* ... */)
|
|
67
|
+
preload: context => prepare(context)
|
|
68
|
+
preload({ params }) { return prepare(params); }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
These are accepted:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
preload: ({ params }: ViewContext) => prepare(params)
|
|
75
|
+
preload(context: ViewContext) { return prepare(context.params); }
|
|
76
|
+
preload: () => prepareWithoutInputs()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The rule recognizes renamed imports and namespace imports, and follows local
|
|
80
|
+
`const` bindings for router aliases, configurations, route entries, and preload
|
|
81
|
+
functions. It also checks local function declarations and statically known object
|
|
82
|
+
spreads. Shadowed imports and unrelated functions named `createSpaRouter` are
|
|
83
|
+
ignored. Shared callbacks are reported only once per file.
|
|
84
|
+
|
|
85
|
+
The rule checks annotation presence, not the annotation's meaning. TypeScript
|
|
86
|
+
checks compatibility with the generated route context and resource types. A type
|
|
87
|
+
assertion on the function or a variable type does not replace the required
|
|
88
|
+
annotation on its parameter.
|
|
89
|
+
|
|
90
|
+
Imported callbacks/configurations, mutable bindings, dynamic property keys,
|
|
91
|
+
factory calls, and cross-file re-exports are not resolved. Keep the configuration
|
|
92
|
+
and annotated preload callback in the same file for enforcement. There are no
|
|
93
|
+
options and no autofix: selecting and importing the generated context type is an
|
|
94
|
+
application decision.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var name = "@spa-kit/eslint-plugin";
|
|
5
|
+
var version = "0.1.0";
|
|
6
|
+
|
|
7
|
+
// src/rules/requirePreloadContext.ts
|
|
8
|
+
var wrappers = /* @__PURE__ */ new Set(["TSAsExpression", "TSSatisfiesExpression", "TSTypeAssertion", "TSNonNullExpression"]);
|
|
9
|
+
function unwrap(node) {
|
|
10
|
+
let current = node;
|
|
11
|
+
while (wrappers.has(current.type) && current.expression) {
|
|
12
|
+
current = current.expression;
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function propertyName(node, computed) {
|
|
17
|
+
if (!computed && node.type === "Identifier") {
|
|
18
|
+
return node.name;
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
21
|
+
return node.value;
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
var requirePreloadContext = {
|
|
26
|
+
meta: {
|
|
27
|
+
type: "problem",
|
|
28
|
+
docs: {
|
|
29
|
+
description: "Require an explicit parameter type on route preloads to preserve render inference"
|
|
30
|
+
},
|
|
31
|
+
schema: [],
|
|
32
|
+
messages: {
|
|
33
|
+
missingContext: "Annotate the preload parameter with its generated route context type to preserve render inference."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
const source = context.sourceCode;
|
|
38
|
+
const reported = /* @__PURE__ */ new Set();
|
|
39
|
+
function variableFor(node) {
|
|
40
|
+
if (node.type !== "Identifier") {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
let scope = source.getScope(node);
|
|
44
|
+
while (scope) {
|
|
45
|
+
const variable = scope.set.get(node.name);
|
|
46
|
+
if (variable) {
|
|
47
|
+
return variable;
|
|
48
|
+
}
|
|
49
|
+
scope = scope.upper;
|
|
50
|
+
}
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function resolve(node, seen = /* @__PURE__ */ new Set()) {
|
|
54
|
+
const value = unwrap(node);
|
|
55
|
+
if (seen.has(value) || value.type !== "Identifier") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
seen.add(value);
|
|
59
|
+
const variable = variableFor(value);
|
|
60
|
+
const definition = variable?.defs[0];
|
|
61
|
+
if (definition?.type === "FunctionName" && definition.node.type === "FunctionDeclaration") {
|
|
62
|
+
return definition.node;
|
|
63
|
+
}
|
|
64
|
+
if (definition?.type === "Variable" && definition.parent?.kind === "const" && definition.node.init) {
|
|
65
|
+
return resolve(definition.node.init, seen);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function isRouterImport(node, namespace) {
|
|
70
|
+
const definition = variableFor(node)?.defs[0];
|
|
71
|
+
if (definition?.type !== "ImportBinding" || definition.parent.source.value !== "@spa-kit/react-router") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const specifier = definition.node;
|
|
75
|
+
if (namespace) {
|
|
76
|
+
return specifier.type === "ImportNamespaceSpecifier";
|
|
77
|
+
}
|
|
78
|
+
return specifier.type === "ImportSpecifier" && propertyName(specifier.imported, false) === "createSpaRouter";
|
|
79
|
+
}
|
|
80
|
+
function isRouterCall(callee) {
|
|
81
|
+
const value = resolve(callee);
|
|
82
|
+
if (value.type === "Identifier") {
|
|
83
|
+
return isRouterImport(value, false);
|
|
84
|
+
}
|
|
85
|
+
return value.type === "MemberExpression" && propertyName(value.property, value.computed) === "createSpaRouter" && isRouterImport(resolve(value.object), true);
|
|
86
|
+
}
|
|
87
|
+
function checkPreload(node) {
|
|
88
|
+
const callback = resolve(node);
|
|
89
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression" && callback.type !== "FunctionDeclaration") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const parameter = callback.params.find((param) => param.type !== "Identifier" || param.name !== "this");
|
|
93
|
+
if (!parameter) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const input = parameter.type === "AssignmentPattern" ? parameter.left : parameter;
|
|
97
|
+
if (!input.typeAnnotation && !reported.has(input)) {
|
|
98
|
+
reported.add(input);
|
|
99
|
+
context.report({ node: input, messageId: "missingContext" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function properties(node, seen = /* @__PURE__ */ new Set()) {
|
|
103
|
+
const object = resolve(node);
|
|
104
|
+
if (object.type !== "ObjectExpression" || seen.has(object)) {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
const path = new Set(seen).add(object);
|
|
108
|
+
const result = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const property of object.properties) {
|
|
110
|
+
if (property.type === "SpreadElement") {
|
|
111
|
+
const spread = properties(property.argument, path);
|
|
112
|
+
if (spread) {
|
|
113
|
+
for (const [name2, value] of spread) result.set(name2, value);
|
|
114
|
+
} else {
|
|
115
|
+
result.clear();
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (property.type === "Property") {
|
|
120
|
+
const name2 = propertyName(property.key, property.computed);
|
|
121
|
+
if (name2 === void 0) {
|
|
122
|
+
result.clear();
|
|
123
|
+
} else if (property.kind === "init") {
|
|
124
|
+
result.set(name2, property.value);
|
|
125
|
+
} else {
|
|
126
|
+
result.delete(name2);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
CallExpression(node) {
|
|
134
|
+
if (!isRouterCall(node.callee)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const config = node.arguments[1];
|
|
138
|
+
if (!config || config.type === "SpreadElement") {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const routeConfig of properties(config)?.values() ?? []) {
|
|
142
|
+
const callback = properties(routeConfig)?.get("preload");
|
|
143
|
+
if (callback) checkPreload(callback);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/index.ts
|
|
151
|
+
var plugin = {
|
|
152
|
+
meta: { name, version },
|
|
153
|
+
rules: {
|
|
154
|
+
"require-preload-context": requirePreloadContext
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var index_default = plugin;
|
|
158
|
+
|
|
159
|
+
module.exports = index_default;
|
|
160
|
+
//# sourceMappingURL=index.cjs.map
|
|
161
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../package.json","../src/rules/requirePreloadContext.ts","../src/index.ts"],"names":["name"],"mappings":"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","file":"index.cjs","sourcesContent":["{\n \"name\": \"@spa-kit/eslint-plugin\",\n \"version\": \"0.1.0\",\n \"description\": \"Opt-in ESLint rules for spa-kit applications\",\n \"author\": \"Casey McGuire\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"typecheck:test\": \"tsc --project tsconfig.test.json\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^9.0.0 || ^10.0.0\"\n },\n \"devDependencies\": {\n \"eslint\": \"^10.12.0\",\n \"@typescript-eslint/parser\": \"^8.71.1\"\n },\n \"engines\": {\n \"node\": \">=18.18.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","import type { Rule } from \"eslint\";\n\n// ESLint's core node types describe JavaScript. These are the additional fields\n// used from the TypeScript ESTree parser; no type-checker services are needed.\ntype TypeScriptNode = Rule.Node & {\n expression?: Rule.Node;\n typeAnnotation?: unknown;\n};\n\nconst wrappers = new Set([\"TSAsExpression\", \"TSSatisfiesExpression\", \"TSTypeAssertion\", \"TSNonNullExpression\"]);\n\nfunction unwrap(node: Rule.Node): Rule.Node {\n let current = node as TypeScriptNode;\n while (wrappers.has(current.type) && current.expression) {\n current = current.expression as TypeScriptNode;\n }\n return current;\n}\n\nfunction propertyName(node: Rule.Node, computed: boolean): string | undefined {\n if (!computed && node.type === \"Identifier\") {\n return node.name;\n }\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n return node.value;\n }\n return undefined;\n}\n\n/** Require explicit generated context annotations on route preload inputs. */\nexport const requirePreloadContext: Rule.RuleModule = {\n meta: {\n type: \"problem\",\n docs: {\n description: \"Require an explicit parameter type on route preloads to preserve render inference\",\n },\n schema: [],\n messages: {\n missingContext: \"Annotate the preload parameter with its generated route context type to preserve render inference.\",\n },\n },\n create(context) {\n const source = context.sourceCode;\n const reported = new Set<Rule.Node>();\n\n function variableFor(node: Rule.Node) {\n if (node.type !== \"Identifier\") {\n return undefined;\n }\n let scope: ReturnType<typeof source.getScope> | null = source.getScope(node);\n while (scope) {\n const variable = scope.set.get(node.name);\n if (variable) {\n return variable;\n }\n scope = scope.upper;\n }\n return undefined;\n }\n\n // Follow immutable bindings in this file only. Imported values, mutable\n // bindings, and dynamically constructed configurations are intentionally opaque.\n function resolve(node: Rule.Node, seen = new Set<Rule.Node>()): Rule.Node {\n const value = unwrap(node);\n if (seen.has(value) || value.type !== \"Identifier\") {\n return value;\n }\n seen.add(value);\n const variable = variableFor(value);\n const definition = variable?.defs[0];\n if (definition?.type === \"FunctionName\" && definition.node.type === \"FunctionDeclaration\") {\n return definition.node as Rule.Node;\n }\n if (definition?.type === \"Variable\" && definition.parent?.kind === \"const\" && definition.node.init) {\n return resolve(definition.node.init as Rule.Node, seen);\n }\n return value;\n }\n\n function isRouterImport(node: Rule.Node, namespace: boolean): boolean {\n const definition = variableFor(node)?.defs[0];\n if (definition?.type !== \"ImportBinding\" || definition.parent.source.value !== \"@spa-kit/react-router\") {\n return false;\n }\n const specifier = definition.node;\n if (namespace) {\n return specifier.type === \"ImportNamespaceSpecifier\";\n }\n return specifier.type === \"ImportSpecifier\" &&\n propertyName(specifier.imported as Rule.Node, false) === \"createSpaRouter\";\n }\n\n function isRouterCall(callee: Rule.Node): boolean {\n const value = resolve(callee);\n if (value.type === \"Identifier\") {\n return isRouterImport(value, false);\n }\n return value.type === \"MemberExpression\" &&\n propertyName(value.property as Rule.Node, value.computed) === \"createSpaRouter\" &&\n isRouterImport(resolve(value.object as Rule.Node), true);\n }\n\n function checkPreload(node: Rule.Node) {\n const callback = resolve(node);\n if (callback.type !== \"ArrowFunctionExpression\" && callback.type !== \"FunctionExpression\" &&\n callback.type !== \"FunctionDeclaration\") {\n return;\n }\n // A TypeScript `this` parameter is erased and is not the route context.\n const parameter = callback.params.find((param) => param.type !== \"Identifier\" || param.name !== \"this\");\n if (!parameter) {\n return;\n }\n const input = (parameter.type === \"AssignmentPattern\" ? parameter.left : parameter) as TypeScriptNode;\n if (!input.typeAnnotation && !reported.has(input)) {\n reported.add(input);\n context.report({ node: input, messageId: \"missingContext\" });\n }\n }\n\n function properties(node: Rule.Node, seen = new Set<Rule.Node>()): Map<string, Rule.Node> | undefined {\n const object = resolve(node);\n if (object.type !== \"ObjectExpression\" || seen.has(object)) {\n return undefined;\n }\n const path = new Set(seen).add(object);\n const result = new Map<string, Rule.Node>();\n for (const property of object.properties) {\n if (property.type === \"SpreadElement\") {\n const spread = properties(property.argument as Rule.Node, path);\n if (spread) {\n for (const [name, value] of spread) result.set(name, value);\n } else {\n // An opaque spread may replace any preceding property.\n result.clear();\n }\n continue;\n }\n if (property.type === \"Property\") {\n const name = propertyName(property.key as Rule.Node, property.computed);\n if (name === undefined) {\n result.clear();\n } else if (property.kind === \"init\") {\n result.set(name, property.value as Rule.Node);\n } else {\n result.delete(name);\n }\n }\n }\n return result;\n }\n\n return {\n CallExpression(node) {\n if (!isRouterCall(node.callee as Rule.Node)) {\n return;\n }\n const config = node.arguments[1];\n if (!config || config.type === \"SpreadElement\") {\n return;\n }\n for (const routeConfig of properties(config as Rule.Node)?.values() ?? []) {\n const callback = properties(routeConfig)?.get(\"preload\");\n if (callback) checkPreload(callback);\n }\n },\n };\n },\n};\n","import type { ESLint } from \"eslint\";\nimport { name, version } from \"../package.json\";\nimport { requirePreloadContext } from \"./rules/requirePreloadContext.js\";\n\n/** Opt-in ESLint rules for spa-kit consumers. */\nconst plugin: ESLint.Plugin = {\n meta: { name, version },\n rules: {\n \"require-preload-context\": requirePreloadContext,\n },\n};\n\nexport default plugin;\n"]}
|
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var name = "@spa-kit/eslint-plugin";
|
|
3
|
+
var version = "0.1.0";
|
|
4
|
+
|
|
5
|
+
// src/rules/requirePreloadContext.ts
|
|
6
|
+
var wrappers = /* @__PURE__ */ new Set(["TSAsExpression", "TSSatisfiesExpression", "TSTypeAssertion", "TSNonNullExpression"]);
|
|
7
|
+
function unwrap(node) {
|
|
8
|
+
let current = node;
|
|
9
|
+
while (wrappers.has(current.type) && current.expression) {
|
|
10
|
+
current = current.expression;
|
|
11
|
+
}
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
function propertyName(node, computed) {
|
|
15
|
+
if (!computed && node.type === "Identifier") {
|
|
16
|
+
return node.name;
|
|
17
|
+
}
|
|
18
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
19
|
+
return node.value;
|
|
20
|
+
}
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
var requirePreloadContext = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: "problem",
|
|
26
|
+
docs: {
|
|
27
|
+
description: "Require an explicit parameter type on route preloads to preserve render inference"
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
messages: {
|
|
31
|
+
missingContext: "Annotate the preload parameter with its generated route context type to preserve render inference."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
const source = context.sourceCode;
|
|
36
|
+
const reported = /* @__PURE__ */ new Set();
|
|
37
|
+
function variableFor(node) {
|
|
38
|
+
if (node.type !== "Identifier") {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
let scope = source.getScope(node);
|
|
42
|
+
while (scope) {
|
|
43
|
+
const variable = scope.set.get(node.name);
|
|
44
|
+
if (variable) {
|
|
45
|
+
return variable;
|
|
46
|
+
}
|
|
47
|
+
scope = scope.upper;
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
function resolve(node, seen = /* @__PURE__ */ new Set()) {
|
|
52
|
+
const value = unwrap(node);
|
|
53
|
+
if (seen.has(value) || value.type !== "Identifier") {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
seen.add(value);
|
|
57
|
+
const variable = variableFor(value);
|
|
58
|
+
const definition = variable?.defs[0];
|
|
59
|
+
if (definition?.type === "FunctionName" && definition.node.type === "FunctionDeclaration") {
|
|
60
|
+
return definition.node;
|
|
61
|
+
}
|
|
62
|
+
if (definition?.type === "Variable" && definition.parent?.kind === "const" && definition.node.init) {
|
|
63
|
+
return resolve(definition.node.init, seen);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function isRouterImport(node, namespace) {
|
|
68
|
+
const definition = variableFor(node)?.defs[0];
|
|
69
|
+
if (definition?.type !== "ImportBinding" || definition.parent.source.value !== "@spa-kit/react-router") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const specifier = definition.node;
|
|
73
|
+
if (namespace) {
|
|
74
|
+
return specifier.type === "ImportNamespaceSpecifier";
|
|
75
|
+
}
|
|
76
|
+
return specifier.type === "ImportSpecifier" && propertyName(specifier.imported, false) === "createSpaRouter";
|
|
77
|
+
}
|
|
78
|
+
function isRouterCall(callee) {
|
|
79
|
+
const value = resolve(callee);
|
|
80
|
+
if (value.type === "Identifier") {
|
|
81
|
+
return isRouterImport(value, false);
|
|
82
|
+
}
|
|
83
|
+
return value.type === "MemberExpression" && propertyName(value.property, value.computed) === "createSpaRouter" && isRouterImport(resolve(value.object), true);
|
|
84
|
+
}
|
|
85
|
+
function checkPreload(node) {
|
|
86
|
+
const callback = resolve(node);
|
|
87
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression" && callback.type !== "FunctionDeclaration") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const parameter = callback.params.find((param) => param.type !== "Identifier" || param.name !== "this");
|
|
91
|
+
if (!parameter) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const input = parameter.type === "AssignmentPattern" ? parameter.left : parameter;
|
|
95
|
+
if (!input.typeAnnotation && !reported.has(input)) {
|
|
96
|
+
reported.add(input);
|
|
97
|
+
context.report({ node: input, messageId: "missingContext" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function properties(node, seen = /* @__PURE__ */ new Set()) {
|
|
101
|
+
const object = resolve(node);
|
|
102
|
+
if (object.type !== "ObjectExpression" || seen.has(object)) {
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
const path = new Set(seen).add(object);
|
|
106
|
+
const result = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const property of object.properties) {
|
|
108
|
+
if (property.type === "SpreadElement") {
|
|
109
|
+
const spread = properties(property.argument, path);
|
|
110
|
+
if (spread) {
|
|
111
|
+
for (const [name2, value] of spread) result.set(name2, value);
|
|
112
|
+
} else {
|
|
113
|
+
result.clear();
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (property.type === "Property") {
|
|
118
|
+
const name2 = propertyName(property.key, property.computed);
|
|
119
|
+
if (name2 === void 0) {
|
|
120
|
+
result.clear();
|
|
121
|
+
} else if (property.kind === "init") {
|
|
122
|
+
result.set(name2, property.value);
|
|
123
|
+
} else {
|
|
124
|
+
result.delete(name2);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
CallExpression(node) {
|
|
132
|
+
if (!isRouterCall(node.callee)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const config = node.arguments[1];
|
|
136
|
+
if (!config || config.type === "SpreadElement") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const routeConfig of properties(config)?.values() ?? []) {
|
|
140
|
+
const callback = properties(routeConfig)?.get("preload");
|
|
141
|
+
if (callback) checkPreload(callback);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/index.ts
|
|
149
|
+
var plugin = {
|
|
150
|
+
meta: { name, version },
|
|
151
|
+
rules: {
|
|
152
|
+
"require-preload-context": requirePreloadContext
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var index_default = plugin;
|
|
156
|
+
|
|
157
|
+
export { index_default as default };
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
159
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../package.json","../src/rules/requirePreloadContext.ts","../src/index.ts"],"names":["name"],"mappings":"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","file":"index.js","sourcesContent":["{\n \"name\": \"@spa-kit/eslint-plugin\",\n \"version\": \"0.1.0\",\n \"description\": \"Opt-in ESLint rules for spa-kit applications\",\n \"author\": \"Casey McGuire\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"typecheck:test\": \"tsc --project tsconfig.test.json\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^9.0.0 || ^10.0.0\"\n },\n \"devDependencies\": {\n \"eslint\": \"^10.12.0\",\n \"@typescript-eslint/parser\": \"^8.71.1\"\n },\n \"engines\": {\n \"node\": \">=18.18.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","import type { Rule } from \"eslint\";\n\n// ESLint's core node types describe JavaScript. These are the additional fields\n// used from the TypeScript ESTree parser; no type-checker services are needed.\ntype TypeScriptNode = Rule.Node & {\n expression?: Rule.Node;\n typeAnnotation?: unknown;\n};\n\nconst wrappers = new Set([\"TSAsExpression\", \"TSSatisfiesExpression\", \"TSTypeAssertion\", \"TSNonNullExpression\"]);\n\nfunction unwrap(node: Rule.Node): Rule.Node {\n let current = node as TypeScriptNode;\n while (wrappers.has(current.type) && current.expression) {\n current = current.expression as TypeScriptNode;\n }\n return current;\n}\n\nfunction propertyName(node: Rule.Node, computed: boolean): string | undefined {\n if (!computed && node.type === \"Identifier\") {\n return node.name;\n }\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n return node.value;\n }\n return undefined;\n}\n\n/** Require explicit generated context annotations on route preload inputs. */\nexport const requirePreloadContext: Rule.RuleModule = {\n meta: {\n type: \"problem\",\n docs: {\n description: \"Require an explicit parameter type on route preloads to preserve render inference\",\n },\n schema: [],\n messages: {\n missingContext: \"Annotate the preload parameter with its generated route context type to preserve render inference.\",\n },\n },\n create(context) {\n const source = context.sourceCode;\n const reported = new Set<Rule.Node>();\n\n function variableFor(node: Rule.Node) {\n if (node.type !== \"Identifier\") {\n return undefined;\n }\n let scope: ReturnType<typeof source.getScope> | null = source.getScope(node);\n while (scope) {\n const variable = scope.set.get(node.name);\n if (variable) {\n return variable;\n }\n scope = scope.upper;\n }\n return undefined;\n }\n\n // Follow immutable bindings in this file only. Imported values, mutable\n // bindings, and dynamically constructed configurations are intentionally opaque.\n function resolve(node: Rule.Node, seen = new Set<Rule.Node>()): Rule.Node {\n const value = unwrap(node);\n if (seen.has(value) || value.type !== \"Identifier\") {\n return value;\n }\n seen.add(value);\n const variable = variableFor(value);\n const definition = variable?.defs[0];\n if (definition?.type === \"FunctionName\" && definition.node.type === \"FunctionDeclaration\") {\n return definition.node as Rule.Node;\n }\n if (definition?.type === \"Variable\" && definition.parent?.kind === \"const\" && definition.node.init) {\n return resolve(definition.node.init as Rule.Node, seen);\n }\n return value;\n }\n\n function isRouterImport(node: Rule.Node, namespace: boolean): boolean {\n const definition = variableFor(node)?.defs[0];\n if (definition?.type !== \"ImportBinding\" || definition.parent.source.value !== \"@spa-kit/react-router\") {\n return false;\n }\n const specifier = definition.node;\n if (namespace) {\n return specifier.type === \"ImportNamespaceSpecifier\";\n }\n return specifier.type === \"ImportSpecifier\" &&\n propertyName(specifier.imported as Rule.Node, false) === \"createSpaRouter\";\n }\n\n function isRouterCall(callee: Rule.Node): boolean {\n const value = resolve(callee);\n if (value.type === \"Identifier\") {\n return isRouterImport(value, false);\n }\n return value.type === \"MemberExpression\" &&\n propertyName(value.property as Rule.Node, value.computed) === \"createSpaRouter\" &&\n isRouterImport(resolve(value.object as Rule.Node), true);\n }\n\n function checkPreload(node: Rule.Node) {\n const callback = resolve(node);\n if (callback.type !== \"ArrowFunctionExpression\" && callback.type !== \"FunctionExpression\" &&\n callback.type !== \"FunctionDeclaration\") {\n return;\n }\n // A TypeScript `this` parameter is erased and is not the route context.\n const parameter = callback.params.find((param) => param.type !== \"Identifier\" || param.name !== \"this\");\n if (!parameter) {\n return;\n }\n const input = (parameter.type === \"AssignmentPattern\" ? parameter.left : parameter) as TypeScriptNode;\n if (!input.typeAnnotation && !reported.has(input)) {\n reported.add(input);\n context.report({ node: input, messageId: \"missingContext\" });\n }\n }\n\n function properties(node: Rule.Node, seen = new Set<Rule.Node>()): Map<string, Rule.Node> | undefined {\n const object = resolve(node);\n if (object.type !== \"ObjectExpression\" || seen.has(object)) {\n return undefined;\n }\n const path = new Set(seen).add(object);\n const result = new Map<string, Rule.Node>();\n for (const property of object.properties) {\n if (property.type === \"SpreadElement\") {\n const spread = properties(property.argument as Rule.Node, path);\n if (spread) {\n for (const [name, value] of spread) result.set(name, value);\n } else {\n // An opaque spread may replace any preceding property.\n result.clear();\n }\n continue;\n }\n if (property.type === \"Property\") {\n const name = propertyName(property.key as Rule.Node, property.computed);\n if (name === undefined) {\n result.clear();\n } else if (property.kind === \"init\") {\n result.set(name, property.value as Rule.Node);\n } else {\n result.delete(name);\n }\n }\n }\n return result;\n }\n\n return {\n CallExpression(node) {\n if (!isRouterCall(node.callee as Rule.Node)) {\n return;\n }\n const config = node.arguments[1];\n if (!config || config.type === \"SpreadElement\") {\n return;\n }\n for (const routeConfig of properties(config as Rule.Node)?.values() ?? []) {\n const callback = properties(routeConfig)?.get(\"preload\");\n if (callback) checkPreload(callback);\n }\n },\n };\n },\n};\n","import type { ESLint } from \"eslint\";\nimport { name, version } from \"../package.json\";\nimport { requirePreloadContext } from \"./rules/requirePreloadContext.js\";\n\n/** Opt-in ESLint rules for spa-kit consumers. */\nconst plugin: ESLint.Plugin = {\n meta: { name, version },\n rules: {\n \"require-preload-context\": requirePreloadContext,\n },\n};\n\nexport default plugin;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spa-kit/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Opt-in ESLint rules for spa-kit applications",
|
|
5
|
+
"author": "Casey McGuire",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"typecheck:test": "tsc --project tsconfig.test.json"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"eslint": "^9.0.0 || ^10.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"eslint": "^10.12.0",
|
|
32
|
+
"@typescript-eslint/parser": "^8.71.1"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.18.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|