roto-rooter 0.0.1 → 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 +24 -17
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +24 -26
- package/dist/analyzer.js.map +1 -1
- package/dist/checks/form-check.d.ts +1 -1
- package/dist/checks/form-check.d.ts.map +1 -1
- package/dist/checks/form-check.js +79 -17
- package/dist/checks/form-check.js.map +1 -1
- package/dist/checks/hydration-check.d.ts +12 -0
- package/dist/checks/hydration-check.d.ts.map +1 -0
- package/dist/checks/hydration-check.js +80 -0
- package/dist/checks/hydration-check.js.map +1 -0
- package/dist/checks/link-check.d.ts +1 -1
- package/dist/checks/link-check.js +10 -10
- package/dist/checks/loader-check.d.ts +1 -1
- package/dist/checks/loader-check.d.ts.map +1 -1
- package/dist/checks/loader-check.js +12 -12
- package/dist/checks/loader-check.js.map +1 -1
- package/dist/checks/params-check.d.ts +1 -1
- package/dist/checks/params-check.js +6 -6
- package/dist/cli.js +24 -24
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/parsers/action-parser.d.ts +2 -0
- package/dist/parsers/action-parser.d.ts.map +1 -1
- package/dist/parsers/action-parser.js +91 -8
- package/dist/parsers/action-parser.js.map +1 -1
- package/dist/parsers/component-parser.d.ts +1 -1
- package/dist/parsers/component-parser.d.ts.map +1 -1
- package/dist/parsers/component-parser.js +241 -41
- package/dist/parsers/component-parser.js.map +1 -1
- package/dist/parsers/route-parser.d.ts +1 -1
- package/dist/parsers/route-parser.js +19 -19
- package/dist/parsers/route-parser.js.map +1 -1
- package/dist/types.d.ts +23 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ast-utils.d.ts +1 -1
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +10 -6
- package/dist/utils/ast-utils.js.map +1 -1
- package/dist/utils/suggestion.d.ts.map +1 -1
- package/dist/utils/suggestion.js +1 -1
- package/dist/utils/suggestion.js.map +1 -1
- package/package.json +23 -2
- package/dist/checks/a11y-check.d.ts +0 -6
- package/dist/checks/a11y-check.d.ts.map +0 -1
- package/dist/checks/a11y-check.js +0 -13
- package/dist/checks/a11y-check.js.map +0 -1
- package/dist/checks/interactive-check.d.ts +0 -6
- package/dist/checks/interactive-check.d.ts.map +0 -1
- package/dist/checks/interactive-check.js +0 -12
- package/dist/checks/interactive-check.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,52 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# roto-rooter
|
|
2
2
|
|
|
3
|
-
Static analysis tool for React Router
|
|
3
|
+
Static analysis and functional verifier tool for React Router applications.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install
|
|
8
|
+
npm install -g roto-rooter
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Check all files
|
|
15
|
-
|
|
15
|
+
rr
|
|
16
16
|
|
|
17
17
|
# Check specific file(s)
|
|
18
|
-
|
|
18
|
+
rr app/routes/employees.tsx
|
|
19
19
|
|
|
20
20
|
# Run specific checks only
|
|
21
|
-
|
|
21
|
+
rr --check links,forms
|
|
22
22
|
|
|
23
23
|
# Output as JSON
|
|
24
|
-
|
|
24
|
+
rr --format json
|
|
25
25
|
|
|
26
|
-
# Set
|
|
27
|
-
|
|
26
|
+
# Set app directory
|
|
27
|
+
rr --app ./my-app
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Checks
|
|
31
31
|
|
|
32
32
|
- **links**: Validates Link, redirect(), and navigate() targets exist as defined routes
|
|
33
|
-
- **forms**: Validates forms submit to routes with action exports
|
|
33
|
+
- **forms**: Validates forms submit to routes with action exports, and that form fields match what the action reads via formData.get()
|
|
34
34
|
- **loader**: Validates useLoaderData() is only used in routes with loaders
|
|
35
35
|
- **params**: Validates useParams() accesses only params defined in the route
|
|
36
|
-
- **
|
|
37
|
-
- **a11y**: Accessibility checks requiring cross-element analysis (coming soon)
|
|
36
|
+
- **hydration**: Detects SSR hydration mismatch risks (dates, locale formatting, random values, browser APIs)
|
|
38
37
|
|
|
39
38
|
## Programmatic API
|
|
40
39
|
|
|
41
40
|
```typescript
|
|
42
|
-
import { analyze } from
|
|
41
|
+
import { analyze } from 'roto-rooter';
|
|
43
42
|
|
|
44
43
|
const result = analyze({
|
|
45
|
-
root:
|
|
46
|
-
files: [],
|
|
47
|
-
checks: [],
|
|
48
|
-
format:
|
|
44
|
+
root: './my-app',
|
|
45
|
+
files: [], // empty = all files
|
|
46
|
+
checks: [], // empty = all checks
|
|
47
|
+
format: 'text',
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
console.log(result.issues);
|
|
52
51
|
```
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install # Install dependencies
|
|
57
|
+
npm test # Run tests
|
|
58
|
+
npm run build # Build for distribution
|
|
59
|
+
```
|
package/dist/analyzer.d.ts
CHANGED
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAId,UAAU,EACX,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAId,UAAU,EACX,MAAM,YAAY,CAAC;AASpB;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,CAmF3D"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import { parseRoutes } from
|
|
4
|
-
import { parseComponent } from
|
|
5
|
-
import { checkLinks } from
|
|
6
|
-
import { checkForms } from
|
|
7
|
-
import { checkLoaders } from
|
|
8
|
-
import { checkParams } from
|
|
9
|
-
import {
|
|
10
|
-
import { checkA11y } from "./checks/a11y-check.js";
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseRoutes } from './parsers/route-parser.js';
|
|
4
|
+
import { parseComponent } from './parsers/component-parser.js';
|
|
5
|
+
import { checkLinks } from './checks/link-check.js';
|
|
6
|
+
import { checkForms } from './checks/form-check.js';
|
|
7
|
+
import { checkLoaders } from './checks/loader-check.js';
|
|
8
|
+
import { checkParams } from './checks/params-check.js';
|
|
9
|
+
import { checkHydration } from './checks/hydration-check.js';
|
|
11
10
|
/**
|
|
12
11
|
* Main analyzer - orchestrates parsing and checking
|
|
13
12
|
*/
|
|
@@ -23,13 +22,13 @@ export function analyze(options) {
|
|
|
23
22
|
return {
|
|
24
23
|
issues: [
|
|
25
24
|
{
|
|
26
|
-
category:
|
|
27
|
-
severity:
|
|
25
|
+
category: 'links',
|
|
26
|
+
severity: 'error',
|
|
28
27
|
message: error instanceof Error
|
|
29
28
|
? error.message
|
|
30
|
-
:
|
|
29
|
+
: 'Failed to parse routes file',
|
|
31
30
|
location: {
|
|
32
|
-
file: path.join(root,
|
|
31
|
+
file: path.join(root, 'app', 'routes.ts'),
|
|
33
32
|
line: 1,
|
|
34
33
|
column: 1,
|
|
35
34
|
},
|
|
@@ -55,24 +54,23 @@ export function analyze(options) {
|
|
|
55
54
|
}
|
|
56
55
|
// Run checks
|
|
57
56
|
const issues = [];
|
|
58
|
-
const enabledChecks = new Set(checks.length > 0
|
|
59
|
-
|
|
57
|
+
const enabledChecks = new Set(checks.length > 0
|
|
58
|
+
? checks
|
|
59
|
+
: ['links', 'forms', 'loader', 'params', 'hydration']);
|
|
60
|
+
if (enabledChecks.has('links')) {
|
|
60
61
|
issues.push(...checkLinks(components, routes));
|
|
61
62
|
}
|
|
62
|
-
if (enabledChecks.has(
|
|
63
|
+
if (enabledChecks.has('forms')) {
|
|
63
64
|
issues.push(...checkForms(components, routes, root));
|
|
64
65
|
}
|
|
65
|
-
if (enabledChecks.has(
|
|
66
|
+
if (enabledChecks.has('loader')) {
|
|
66
67
|
issues.push(...checkLoaders(components));
|
|
67
68
|
}
|
|
68
|
-
if (enabledChecks.has(
|
|
69
|
+
if (enabledChecks.has('params')) {
|
|
69
70
|
issues.push(...checkParams(components, routes, root));
|
|
70
71
|
}
|
|
71
|
-
if (enabledChecks.has(
|
|
72
|
-
issues.push(...
|
|
73
|
-
}
|
|
74
|
-
if (enabledChecks.has("a11y")) {
|
|
75
|
-
issues.push(...checkA11y(components));
|
|
72
|
+
if (enabledChecks.has('hydration')) {
|
|
73
|
+
issues.push(...checkHydration(components));
|
|
76
74
|
}
|
|
77
75
|
// If specific files were provided, filter issues to only those files
|
|
78
76
|
if (files.length > 0) {
|
|
@@ -91,13 +89,13 @@ function findComponentFiles(root, specificFiles) {
|
|
|
91
89
|
return specificFiles.map((f) => path.resolve(f));
|
|
92
90
|
}
|
|
93
91
|
// Find all TSX files in app/routes
|
|
94
|
-
const routesDir = path.join(root,
|
|
92
|
+
const routesDir = path.join(root, 'app', 'routes');
|
|
95
93
|
if (!fs.existsSync(routesDir)) {
|
|
96
94
|
return [];
|
|
97
95
|
}
|
|
98
96
|
const files = [];
|
|
99
97
|
walkDir(routesDir, (filePath) => {
|
|
100
|
-
if (filePath.endsWith(
|
|
98
|
+
if (filePath.endsWith('.tsx')) {
|
|
101
99
|
files.push(filePath);
|
|
102
100
|
}
|
|
103
101
|
});
|
package/dist/analyzer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAQ7B,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-check.d.ts","sourceRoot":"","sources":["../../src/checks/form-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EAChB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"form-check.d.ts","sourceRoot":"","sources":["../../src/checks/form-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EAChB,MAAM,aAAa,CAAC;AAOrB;;GAEG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,MAAM,EAAE,eAAe,EAAE,EACzB,OAAO,EAAE,MAAM,GACd,aAAa,EAAE,CAWjB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as path from
|
|
2
|
-
import { matchRoute } from
|
|
3
|
-
import { parseRouteExports } from
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { matchRoute } from '../parsers/route-parser.js';
|
|
3
|
+
import { parseRouteExports, } from '../parsers/action-parser.js';
|
|
4
4
|
/**
|
|
5
5
|
* Check form-action wiring
|
|
6
6
|
*/
|
|
@@ -24,8 +24,8 @@ function validateForm(form, component, routes, rootDir) {
|
|
|
24
24
|
const targetRoute = matchRoute(form.action, routes);
|
|
25
25
|
if (!targetRoute) {
|
|
26
26
|
issues.push({
|
|
27
|
-
category:
|
|
28
|
-
severity:
|
|
27
|
+
category: 'forms',
|
|
28
|
+
severity: 'error',
|
|
29
29
|
message: `Form action targets non-existent route: ${form.action}`,
|
|
30
30
|
location: form.location,
|
|
31
31
|
code: `<Form action="${form.action}">`,
|
|
@@ -33,19 +33,24 @@ function validateForm(form, component, routes, rootDir) {
|
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
35
35
|
// Check if the target route file has an action export
|
|
36
|
-
const routeFilePath = path.join(rootDir,
|
|
36
|
+
const routeFilePath = path.join(rootDir, 'app', targetRoute.file);
|
|
37
37
|
try {
|
|
38
38
|
const exports = parseRouteExports(routeFilePath);
|
|
39
39
|
if (!exports.hasAction) {
|
|
40
40
|
issues.push({
|
|
41
|
-
category:
|
|
42
|
-
severity:
|
|
41
|
+
category: 'forms',
|
|
42
|
+
severity: 'error',
|
|
43
43
|
message: `Form action targets route without action export`,
|
|
44
44
|
location: form.location,
|
|
45
45
|
code: `<Form action="${form.action}">`,
|
|
46
46
|
suggestion: `Add an action export to ${targetRoute.file}`,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
else {
|
|
50
|
+
// Action exists - check field alignment
|
|
51
|
+
const fieldIssues = validateFormFields(form, exports, targetRoute.file);
|
|
52
|
+
issues.push(...fieldIssues);
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
catch {
|
|
51
56
|
// File doesn't exist or can't be parsed - route-parser should catch this
|
|
@@ -56,19 +61,76 @@ function validateForm(form, component, routes, rootDir) {
|
|
|
56
61
|
// Form submits to current route - check if current file has action
|
|
57
62
|
if (!component.hasAction) {
|
|
58
63
|
issues.push({
|
|
59
|
-
category:
|
|
60
|
-
severity:
|
|
61
|
-
message:
|
|
64
|
+
category: 'forms',
|
|
65
|
+
severity: 'error',
|
|
66
|
+
message: 'Form in route with no action export',
|
|
67
|
+
location: form.location,
|
|
68
|
+
code: '<Form>',
|
|
69
|
+
suggestion: 'Add an action export to handle form submission',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Action exists in current file - check field alignment
|
|
74
|
+
const routeFilePath = component.file;
|
|
75
|
+
try {
|
|
76
|
+
const exports = parseRouteExports(routeFilePath);
|
|
77
|
+
if (exports.actionFields) {
|
|
78
|
+
const fieldIssues = validateFormFields(form, exports);
|
|
79
|
+
issues.push(...fieldIssues);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Parsing failed - skip field validation
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return issues;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate that form fields align with what the action expects
|
|
91
|
+
*/
|
|
92
|
+
function validateFormFields(form, exports, targetFile) {
|
|
93
|
+
const issues = [];
|
|
94
|
+
const formFields = new Set(form.inputNames);
|
|
95
|
+
const actionFields = new Set(exports.actionFields ?? []);
|
|
96
|
+
// Skip validation if action doesn't read any fields (might use Object.fromEntries or similar)
|
|
97
|
+
if (actionFields.size === 0) {
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
// Check for fields the action expects but form doesn't provide
|
|
101
|
+
for (const actionField of actionFields) {
|
|
102
|
+
if (!formFields.has(actionField)) {
|
|
103
|
+
const formCode = form.action
|
|
104
|
+
? `<Form action="${form.action}">`
|
|
105
|
+
: '<Form>';
|
|
106
|
+
issues.push({
|
|
107
|
+
category: 'forms',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
message: `Action reads field '${actionField}' but form has no input with name="${actionField}"`,
|
|
110
|
+
location: form.location,
|
|
111
|
+
code: formCode,
|
|
112
|
+
suggestion: targetFile
|
|
113
|
+
? `Add <input name="${actionField}" /> to the form, or check the action in ${targetFile}`
|
|
114
|
+
: `Add <input name="${actionField}" /> to the form`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Check for form fields the action never reads (warning - might be intentional)
|
|
119
|
+
for (const formField of formFields) {
|
|
120
|
+
if (!actionFields.has(formField)) {
|
|
121
|
+
const formCode = form.action
|
|
122
|
+
? `<Form action="${form.action}">`
|
|
123
|
+
: '<Form>';
|
|
124
|
+
issues.push({
|
|
125
|
+
category: 'forms',
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
message: `Form field '${formField}' is never read by the action`,
|
|
62
128
|
location: form.location,
|
|
63
|
-
code:
|
|
64
|
-
suggestion:
|
|
129
|
+
code: formCode,
|
|
130
|
+
suggestion: `Remove unused input or add formData.get('${formField}') to the action`,
|
|
65
131
|
});
|
|
66
132
|
}
|
|
67
133
|
}
|
|
68
|
-
// Check for inputs without name attribute
|
|
69
|
-
// Note: We track inputNames, but we should also warn about inputs WITHOUT names
|
|
70
|
-
// This would require more sophisticated tracking in the component parser
|
|
71
|
-
// For now, we'll skip this check
|
|
72
134
|
return issues;
|
|
73
135
|
}
|
|
74
136
|
//# sourceMappingURL=form-check.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-check.js","sourceRoot":"","sources":["../../src/checks/form-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAM7B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"form-check.js","sourceRoot":"","sources":["../../src/checks/form-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AnalyzerIssue, ComponentAnalysis } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Check for SSR hydration mismatch risks
|
|
4
|
+
*
|
|
5
|
+
* Detects patterns that cause hydration mismatches between server and client:
|
|
6
|
+
* - Date/time operations without consistent timezone handling
|
|
7
|
+
* - Locale-dependent formatting without explicit timezone
|
|
8
|
+
* - Random value generation during render
|
|
9
|
+
* - Browser-only API access outside useEffect
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkHydration(components: ComponentAnalysis[]): AnalyzerIssue[];
|
|
12
|
+
//# sourceMappingURL=hydration-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-check.d.ts","sourceRoot":"","sources":["../../src/checks/hydration-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EAElB,MAAM,aAAa,CAAC;AAErB;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,iBAAiB,EAAE,GAC9B,aAAa,EAAE,CAkBjB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for SSR hydration mismatch risks
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns that cause hydration mismatches between server and client:
|
|
5
|
+
* - Date/time operations without consistent timezone handling
|
|
6
|
+
* - Locale-dependent formatting without explicit timezone
|
|
7
|
+
* - Random value generation during render
|
|
8
|
+
* - Browser-only API access outside useEffect
|
|
9
|
+
*/
|
|
10
|
+
export function checkHydration(components) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
for (const component of components) {
|
|
13
|
+
for (const risk of component.hydrationRisks) {
|
|
14
|
+
// Skip risks that are already mitigated
|
|
15
|
+
if (risk.inUseEffect || risk.hasSuppressWarning) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const issue = createIssueForRisk(risk);
|
|
19
|
+
if (issue) {
|
|
20
|
+
issues.push(issue);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return issues;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create an analyzer issue for a hydration risk
|
|
28
|
+
*/
|
|
29
|
+
function createIssueForRisk(risk) {
|
|
30
|
+
switch (risk.type) {
|
|
31
|
+
case 'date-render':
|
|
32
|
+
return {
|
|
33
|
+
category: 'hydration',
|
|
34
|
+
severity: 'error',
|
|
35
|
+
message: 'Date created during render causes hydration mismatch',
|
|
36
|
+
location: risk.location,
|
|
37
|
+
code: risk.code,
|
|
38
|
+
suggestion: 'Move to useEffect, use suppressHydrationWarning, or pass date from loader',
|
|
39
|
+
};
|
|
40
|
+
case 'locale-format':
|
|
41
|
+
return {
|
|
42
|
+
category: 'hydration',
|
|
43
|
+
severity: 'error',
|
|
44
|
+
message: 'Locale-dependent formatting without explicit timeZone causes hydration mismatch',
|
|
45
|
+
location: risk.location,
|
|
46
|
+
code: risk.code,
|
|
47
|
+
suggestion: 'Add { timeZone: "UTC" } option, use suppressHydrationWarning, or format in useEffect',
|
|
48
|
+
};
|
|
49
|
+
case 'random-value':
|
|
50
|
+
return {
|
|
51
|
+
category: 'hydration',
|
|
52
|
+
severity: 'error',
|
|
53
|
+
message: 'Random value in render will cause hydration mismatch',
|
|
54
|
+
location: risk.location,
|
|
55
|
+
code: risk.code,
|
|
56
|
+
suggestion: 'Use React.useId() for IDs, or generate in useEffect and store in state',
|
|
57
|
+
};
|
|
58
|
+
case 'browser-api':
|
|
59
|
+
return {
|
|
60
|
+
category: 'hydration',
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: 'Browser-only API accessed during render will fail on server and cause hydration mismatch',
|
|
63
|
+
location: risk.location,
|
|
64
|
+
code: risk.code,
|
|
65
|
+
suggestion: 'Move to useEffect or check typeof window !== "undefined" first',
|
|
66
|
+
};
|
|
67
|
+
case 'loader-date':
|
|
68
|
+
return {
|
|
69
|
+
category: 'hydration',
|
|
70
|
+
severity: 'warning',
|
|
71
|
+
message: 'Date from loader may cause hydration mismatch when formatted',
|
|
72
|
+
location: risk.location,
|
|
73
|
+
code: risk.code,
|
|
74
|
+
suggestion: 'Format with explicit timeZone, use suppressHydrationWarning, or format in useEffect',
|
|
75
|
+
};
|
|
76
|
+
default:
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=hydration-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-check.js","sourceRoot":"","sources":["../../src/checks/hydration-check.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,UAA+B;IAE/B,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,cAAc,EAAE,CAAC;YAC5C,wCAAwC;YACxC,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAChD,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,IAAmB;IAC7C,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,aAAa;YAChB,OAAO;gBACL,QAAQ,EAAE,WAAW;gBACrB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,sDAAsD;gBAC/D,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EACR,2EAA2E;aAC9E,CAAC;QAEJ,KAAK,eAAe;YAClB,OAAO;gBACL,QAAQ,EAAE,WAAW;gBACrB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EACL,iFAAiF;gBACnF,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EACR,sFAAsF;aACzF,CAAC;QAEJ,KAAK,cAAc;YACjB,OAAO;gBACL,QAAQ,EAAE,WAAW;gBACrB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,sDAAsD;gBAC/D,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EACR,wEAAwE;aAC3E,CAAC;QAEJ,KAAK,aAAa;YAChB,OAAO;gBACL,QAAQ,EAAE,WAAW;gBACrB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EACL,0FAA0F;gBAC5F,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EACR,gEAAgE;aACnE,CAAC;QAEJ,KAAK,aAAa;YAChB,OAAO;gBACL,QAAQ,EAAE,WAAW;gBACrB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,8DAA8D;gBACvE,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EACR,qFAAqF;aACxF,CAAC;QAEJ;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { matchRoute, matchDynamicPattern, getAllRoutePaths, } from
|
|
2
|
-
import { findBestMatch, formatSuggestion } from
|
|
1
|
+
import { matchRoute, matchDynamicPattern, getAllRoutePaths, } from '../parsers/route-parser.js';
|
|
2
|
+
import { findBestMatch, formatSuggestion } from '../utils/suggestion.js';
|
|
3
3
|
/**
|
|
4
4
|
* Check all links in components against defined routes
|
|
5
5
|
*/
|
|
@@ -27,11 +27,11 @@ function validateLink(link, routes, allPaths) {
|
|
|
27
27
|
if (!match) {
|
|
28
28
|
const suggestion = findBestMatch(pattern, allPaths);
|
|
29
29
|
return {
|
|
30
|
-
category:
|
|
31
|
-
severity:
|
|
32
|
-
message:
|
|
30
|
+
category: 'links',
|
|
31
|
+
severity: 'error',
|
|
32
|
+
message: 'No matching route for dynamic link pattern',
|
|
33
33
|
location: link.location,
|
|
34
|
-
code: `${link.type ===
|
|
34
|
+
code: `${link.type === 'link' ? 'href' : link.type}="${link.href}"`,
|
|
35
35
|
suggestion: formatSuggestion(suggestion),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -42,11 +42,11 @@ function validateLink(link, routes, allPaths) {
|
|
|
42
42
|
if (!match) {
|
|
43
43
|
const suggestion = findBestMatch(link.href, allPaths);
|
|
44
44
|
return {
|
|
45
|
-
category:
|
|
46
|
-
severity:
|
|
47
|
-
message:
|
|
45
|
+
category: 'links',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
message: 'No matching route',
|
|
48
48
|
location: link.location,
|
|
49
|
-
code: `${link.type ===
|
|
49
|
+
code: `${link.type === 'link' ? 'href' : link.type}="${link.href}"`,
|
|
50
50
|
suggestion: formatSuggestion(suggestion),
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader-check.d.ts","sourceRoot":"","sources":["../../src/checks/loader-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE;;GAEG;AACH,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"loader-check.d.ts","sourceRoot":"","sources":["../../src/checks/loader-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE;;GAEG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,aAAa,EAAE,CAS7E"}
|
|
@@ -15,24 +15,24 @@ export function checkLoaders(components) {
|
|
|
15
15
|
function validateLoaderUsage(component) {
|
|
16
16
|
const issues = [];
|
|
17
17
|
for (const hook of component.dataHooks) {
|
|
18
|
-
if (hook.hook ===
|
|
18
|
+
if (hook.hook === 'useLoaderData' && !component.hasLoader) {
|
|
19
19
|
issues.push({
|
|
20
|
-
category:
|
|
21
|
-
severity:
|
|
22
|
-
message:
|
|
20
|
+
category: 'loader',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
message: 'useLoaderData() called but route has no loader',
|
|
23
23
|
location: hook.location,
|
|
24
|
-
code:
|
|
25
|
-
suggestion:
|
|
24
|
+
code: 'useLoaderData()',
|
|
25
|
+
suggestion: 'Add a loader function or remove the hook',
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
if (hook.hook ===
|
|
28
|
+
if (hook.hook === 'useActionData' && !component.hasAction) {
|
|
29
29
|
issues.push({
|
|
30
|
-
category:
|
|
31
|
-
severity:
|
|
32
|
-
message:
|
|
30
|
+
category: 'loader',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: 'useActionData() called but route has no action',
|
|
33
33
|
location: hook.location,
|
|
34
|
-
code:
|
|
35
|
-
suggestion:
|
|
34
|
+
code: 'useActionData()',
|
|
35
|
+
suggestion: 'Add an action function or remove the hook',
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader-check.js","sourceRoot":"","sources":["../../src/checks/loader-check.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,YAAY,
|
|
1
|
+
{"version":3,"file":"loader-check.js","sourceRoot":"","sources":["../../src/checks/loader-check.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,UAA+B;IAC1D,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,eAAe,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,SAA4B;IACvD,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;YAC1D,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,QAAQ;gBAClB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,gDAAgD;gBACzD,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,0CAA0C;aACvD,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;YAC1D,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,QAAQ;gBAClB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,gDAAgD;gBACzD,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,2CAA2C;aACxD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as path from
|
|
1
|
+
import * as path from 'path';
|
|
2
2
|
/**
|
|
3
3
|
* Check route parameter consistency
|
|
4
4
|
*/
|
|
@@ -8,7 +8,7 @@ export function checkParams(components, routes, rootDir) {
|
|
|
8
8
|
const fileToRoute = new Map();
|
|
9
9
|
function mapRoutes(routeList) {
|
|
10
10
|
for (const route of routeList) {
|
|
11
|
-
const fullPath = path.join(rootDir,
|
|
11
|
+
const fullPath = path.join(rootDir, 'app', route.file);
|
|
12
12
|
fileToRoute.set(fullPath, route);
|
|
13
13
|
if (route.children) {
|
|
14
14
|
mapRoutes(route.children);
|
|
@@ -34,17 +34,17 @@ function validateParamUsage(component, route) {
|
|
|
34
34
|
const issues = [];
|
|
35
35
|
const routeParams = new Set(route.params);
|
|
36
36
|
for (const hook of component.dataHooks) {
|
|
37
|
-
if (hook.hook ===
|
|
37
|
+
if (hook.hook === 'useParams' && hook.accessedParams) {
|
|
38
38
|
for (const param of hook.accessedParams) {
|
|
39
39
|
if (!routeParams.has(param)) {
|
|
40
40
|
issues.push({
|
|
41
|
-
category:
|
|
42
|
-
severity:
|
|
41
|
+
category: 'params',
|
|
42
|
+
severity: 'error',
|
|
43
43
|
message: `useParams() accesses "${param}" but route has no :${param} parameter`,
|
|
44
44
|
location: hook.location,
|
|
45
45
|
code: `useParams().${param}`,
|
|
46
46
|
suggestion: route.params.length > 0
|
|
47
|
-
? `Available params: ${route.params.map((p) =>
|
|
47
|
+
? `Available params: ${route.params.map((p) => ':' + p).join(', ')}`
|
|
48
48
|
: `Route ${route.path} has no parameters`,
|
|
49
49
|
});
|
|
50
50
|
}
|