eslint-plugin-route-intelligence 2.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.
@@ -0,0 +1,10 @@
1
+ import { ESLint, Rule } from 'eslint';
2
+
3
+ declare const noBrokenRoute: Rule.RuleModule;
4
+ declare const noInvalidRedirect: Rule.RuleModule;
5
+ declare const noDeadPage: Rule.RuleModule;
6
+ declare const preferRouteConstants: Rule.RuleModule;
7
+ declare const detectRouteCycles: Rule.RuleModule;
8
+ declare const plugin: ESLint.Plugin;
9
+
10
+ export { plugin as default, detectRouteCycles, noBrokenRoute, noDeadPage, noInvalidRedirect, preferRouteConstants };
package/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ // src/index.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ function loadGraph(cwd) {
5
+ const paths = [
6
+ join(cwd, "ri-output", "graph.json"),
7
+ join(cwd, ".route-intelligence", "graph.json")
8
+ ];
9
+ for (const p of paths) {
10
+ if (existsSync(p)) {
11
+ return JSON.parse(readFileSync(p, "utf-8"));
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ function getKnownPaths(graph) {
17
+ return new Set(
18
+ graph.nodes.filter((n) => n.attributes.type === "route").map((n) => n.attributes.path)
19
+ );
20
+ }
21
+ var noBrokenRoute = {
22
+ meta: {
23
+ type: "problem",
24
+ docs: { description: "Disallow links to non-existent routes" },
25
+ schema: [],
26
+ messages: { broken: 'Route "{{path}}" does not exist in the routing graph' }
27
+ },
28
+ create(context) {
29
+ const graph = loadGraph(context.cwd);
30
+ if (!graph) return {};
31
+ const paths = getKnownPaths(graph);
32
+ return {
33
+ JSXAttribute(node) {
34
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "href") return;
35
+ const value = node.value;
36
+ if (!value || value.type !== "Literal" || typeof value.value !== "string") return;
37
+ const path = value.value;
38
+ if (path.startsWith("http") || path.startsWith("#")) return;
39
+ if (!paths.has(path) && !paths.has(path.replace(/\/$/, ""))) {
40
+ context.report({ node, messageId: "broken", data: { path } });
41
+ }
42
+ }
43
+ };
44
+ }
45
+ };
46
+ var noInvalidRedirect = {
47
+ meta: {
48
+ type: "problem",
49
+ docs: { description: "Disallow redirects to non-existent routes" },
50
+ schema: [],
51
+ messages: { invalid: 'Redirect target "{{path}}" does not exist' }
52
+ },
53
+ create(context) {
54
+ const graph = loadGraph(context.cwd);
55
+ if (!graph) return {};
56
+ const paths = getKnownPaths(graph);
57
+ return {
58
+ CallExpression(node) {
59
+ if (node.callee.type !== "Identifier") return;
60
+ if (node.callee.name !== "redirect" && node.callee.name !== "permanentRedirect") return;
61
+ const arg = node.arguments[0];
62
+ if (!arg || arg.type !== "Literal" || typeof arg.value !== "string") return;
63
+ if (!paths.has(arg.value)) {
64
+ context.report({ node, messageId: "invalid", data: { path: arg.value } });
65
+ }
66
+ }
67
+ };
68
+ }
69
+ };
70
+ var noDeadPage = {
71
+ meta: {
72
+ type: "suggestion",
73
+ docs: { description: "Warn when current file is a dead route" },
74
+ schema: [],
75
+ messages: { dead: "This page appears to be unreachable (dead route)" }
76
+ },
77
+ create(context) {
78
+ const graph = loadGraph(context.cwd);
79
+ if (!graph) return {};
80
+ const filename = context.filename.replace(/\\/g, "/");
81
+ const deadNode = graph.nodes.find(
82
+ (n) => n.attributes.isDead && n.attributes.filePath.replace(/\\/g, "/").includes(filename)
83
+ );
84
+ if (deadNode) {
85
+ return {
86
+ Program(node) {
87
+ context.report({ node, messageId: "dead" });
88
+ }
89
+ };
90
+ }
91
+ return {};
92
+ }
93
+ };
94
+ var preferRouteConstants = {
95
+ meta: {
96
+ type: "suggestion",
97
+ docs: { description: "Prefer route constants over string literals" },
98
+ schema: [],
99
+ messages: { prefer: 'Use a route constant instead of inline string "{{path}}"' }
100
+ },
101
+ create(context) {
102
+ return {
103
+ JSXAttribute(node) {
104
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "href") return;
105
+ const value = node.value;
106
+ if (!value || value.type !== "Literal" || typeof value.value !== "string") return;
107
+ if (value.value.startsWith("/")) {
108
+ context.report({ node, messageId: "prefer", data: { path: value.value } });
109
+ }
110
+ }
111
+ };
112
+ }
113
+ };
114
+ var detectRouteCycles = {
115
+ meta: {
116
+ type: "suggestion",
117
+ docs: { description: "Detect navigation that creates cycles" },
118
+ schema: [],
119
+ messages: { cycle: "Navigation may create a route cycle" }
120
+ },
121
+ create(context) {
122
+ const graph = loadGraph(context.cwd);
123
+ if (!graph) return {};
124
+ const edges = /* @__PURE__ */ new Map();
125
+ for (const edge of graph.edges) {
126
+ if (edge.attributes.type === "navigation" || edge.attributes.type === "conditional-navigation") {
127
+ const set = edges.get(edge.source) ?? /* @__PURE__ */ new Set();
128
+ set.add(edge.target);
129
+ edges.set(edge.source, set);
130
+ }
131
+ }
132
+ return {
133
+ CallExpression(node) {
134
+ if (node.callee.type !== "Identifier" && node.callee.type !== "MemberExpression") return;
135
+ const text = node.callee.type === "Identifier" ? node.callee.name : node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" ? node.callee.property.name : "";
136
+ if (!["push", "navigate", "redirect"].includes(text)) return;
137
+ const filename = context.filename;
138
+ const sourceNode = graph.nodes.find(
139
+ (n) => n.attributes.filePath.includes(filename.split(/[/\\]/).pop() ?? "")
140
+ );
141
+ if (!sourceNode) return;
142
+ const targets = edges.get(sourceNode.id);
143
+ if (targets?.has(sourceNode.id)) {
144
+ context.report({ node, messageId: "cycle" });
145
+ }
146
+ }
147
+ };
148
+ }
149
+ };
150
+ var plugin = {
151
+ meta: { name: "eslint-plugin-route-intelligence", version: "0.1.0" },
152
+ rules: {
153
+ "no-broken-route": noBrokenRoute,
154
+ "no-invalid-redirect": noInvalidRedirect,
155
+ "no-dead-page": noDeadPage,
156
+ "prefer-route-constants": preferRouteConstants,
157
+ "detect-route-cycles": detectRouteCycles
158
+ }
159
+ };
160
+ var index_default = plugin;
161
+ export {
162
+ index_default as default,
163
+ detectRouteCycles,
164
+ noBrokenRoute,
165
+ noDeadPage,
166
+ noInvalidRedirect,
167
+ preferRouteConstants
168
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "eslint-plugin-route-intelligence",
3
+ "version": "2.1.0",
4
+ "description": "ESLint plugin for Route Intelligence",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "development": "./src/index.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm --dts --clean",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@route-intelligence/shared": "*"
24
+ },
25
+ "peerDependencies": {
26
+ "eslint": ">=9"
27
+ },
28
+ "devDependencies": {
29
+ "@route-intelligence/tsconfig": "*",
30
+ "@types/estree-jsx": "^1.0.5",
31
+ "@types/node": "^22.15.32",
32
+ "eslint": "^9.29.0",
33
+ "tsup": "^8.4.0",
34
+ "typescript": "^5.8.3"
35
+ },
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }