roto-rooter 0.1.4 → 0.1.5
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 +29 -9
- package/dist/cli.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g roto-rooter
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
# Check all files
|
|
14
|
+
# Check all files in current directory
|
|
15
15
|
rr
|
|
16
16
|
|
|
17
17
|
# Check specific file(s)
|
|
@@ -23,23 +23,39 @@ rr --check links,forms
|
|
|
23
23
|
# Output as JSON
|
|
24
24
|
rr --format json
|
|
25
25
|
|
|
26
|
-
# Set app
|
|
27
|
-
rr --
|
|
26
|
+
# Set project root (the directory containing the app/ folder)
|
|
27
|
+
rr --root ./my-app
|
|
28
|
+
|
|
29
|
+
# Automatically fix issues where possible
|
|
30
|
+
rr --fix
|
|
31
|
+
|
|
32
|
+
# Preview fixes without applying
|
|
33
|
+
rr --dry-run
|
|
34
|
+
|
|
35
|
+
# Fix specific file(s)
|
|
36
|
+
rr --fix app/routes/dashboard.tsx
|
|
28
37
|
```
|
|
29
38
|
|
|
30
39
|
## Checks
|
|
31
40
|
|
|
32
|
-
- **links**: Validates Link
|
|
33
|
-
- **forms**: Validates
|
|
34
|
-
- **loader**: Validates useLoaderData() is only used in routes
|
|
35
|
-
- **params**: Validates useParams() accesses only params defined in the route
|
|
36
|
-
- **hydration**: Detects SSR hydration mismatch risks
|
|
41
|
+
- **links**: Validates `<Link>`, `redirect()`, and `navigate()` targets exist as defined routes. Suggests closest matching route when a typo is detected. Auto-fixable when a close match exists.
|
|
42
|
+
- **forms**: Validates `<Form>` components submit to routes with action exports, and that form fields match what the action reads via `formData.get()`. Supports intent-based dispatch patterns. Auto-fixable when targeting a mistyped route.
|
|
43
|
+
- **loader**: Validates `useLoaderData()` is only used in routes that export a loader function.
|
|
44
|
+
- **params**: Validates `useParams()` accesses only params defined in the route path (e.g., `:id` in `/users/:id`).
|
|
45
|
+
- **hydration**: Detects SSR hydration mismatch risks:
|
|
46
|
+
- Date/time operations without consistent timezone handling
|
|
47
|
+
- Locale-dependent formatting (e.g., `toLocaleString()`) without explicit `timeZone` option
|
|
48
|
+
- Random value generation during render (`Math.random()`, `uuid()`, `nanoid()`)
|
|
49
|
+
- Browser-only API access outside `useEffect` (`window`, `document`, `localStorage`)
|
|
50
|
+
|
|
51
|
+
Some hydration issues are auto-fixable (e.g., adding `{ timeZone: "UTC" }` to locale methods, replacing `uuid()` with `useId()`).
|
|
37
52
|
|
|
38
53
|
## Programmatic API
|
|
39
54
|
|
|
40
55
|
```typescript
|
|
41
|
-
import { analyze } from 'roto-rooter';
|
|
56
|
+
import { analyze, applyFixes } from 'roto-rooter';
|
|
42
57
|
|
|
58
|
+
// Run analysis
|
|
43
59
|
const result = analyze({
|
|
44
60
|
root: './my-app',
|
|
45
61
|
files: [], // empty = all files
|
|
@@ -48,6 +64,10 @@ const result = analyze({
|
|
|
48
64
|
});
|
|
49
65
|
|
|
50
66
|
console.log(result.issues);
|
|
67
|
+
|
|
68
|
+
// Apply auto-fixes
|
|
69
|
+
const fixResult = applyFixes(result.issues);
|
|
70
|
+
console.log(`Fixed ${fixResult.fixesApplied} issues`);
|
|
51
71
|
```
|
|
52
72
|
|
|
53
73
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -435,7 +435,7 @@ Additional information: BADCLIENT: Bad error code, ${T} not found in range ${c}.
|
|
|
435
435
|
`).length,mr=tt.location.line+qt-1;return Mt.location.line>=tt.location.line&&Mt.location.line<=mr}function Fdt(tt){switch(tt.type){case"date-render":return{category:"hydration",severity:"error",message:"Date created during render causes hydration mismatch",location:tt.location,code:tt.code,suggestion:"Move to useEffect, use suppressHydrationWarning, or pass date from loader"};case"locale-format":{let Mt={category:"hydration",severity:"error",message:"Locale-dependent formatting without explicit timeZone causes hydration mismatch",location:tt.location,code:tt.code,suggestion:'Add { timeZone: "UTC" } option, use suppressHydrationWarning, or format in useEffect'};if(tt.callSpan&&tt.argCount!==void 0){let qt=Odt(tt);qt&&(Mt.fix=qt)}return Mt}case"random-value":{let Mt={category:"hydration",severity:"error",message:"Random value in render will cause hydration mismatch",location:tt.location,code:tt.code,suggestion:"Use React.useId() for IDs, or generate in useEffect and store in state"};return tt.callSpan&&Ldt(tt.code)&&(Mt.fix={description:"Replaced with useId()",edits:[{file:tt.callSpan.file,start:tt.callSpan.start,end:tt.callSpan.end,newText:"useId()"}]}),Mt}case"browser-api":return{category:"hydration",severity:"error",message:"Browser-only API accessed during render will fail on server and cause hydration mismatch",location:tt.location,code:tt.code,suggestion:'Move to useEffect or check typeof window !== "undefined" first'};case"loader-date":return{category:"hydration",severity:"warning",message:"Date from loader may cause hydration mismatch when formatted",location:tt.location,code:tt.code,suggestion:"Format with explicit timeZone, use suppressHydrationWarning, or format in useEffect"};default:return}}function Odt(tt){if(!tt.callSpan)return;let Mt=tt.argCount??0,qt=tt.code.match(/\.toLocale(String|DateString|TimeString)\(\)/);if(Mt===0&&qt){let mr=`toLocale${qt[1]}`,Tr=tt.code.replace(`.${mr}()`,`.${mr}(undefined, { timeZone: "UTC" })`);return{description:'Added { timeZone: "UTC" } option',edits:[{file:tt.callSpan.file,start:tt.callSpan.start,end:tt.callSpan.end,newText:Tr}]}}if(Mt===0&&tt.code.includes("Intl.DateTimeFormat")){let mr=tt.code.replace("Intl.DateTimeFormat()",'Intl.DateTimeFormat(undefined, { timeZone: "UTC" })');if(mr!==tt.code)return{description:'Added { timeZone: "UTC" } option',edits:[{file:tt.callSpan.file,start:tt.callSpan.start,end:tt.callSpan.end,newText:mr}]}}}function Ldt(tt){return/^(uuid|nanoid|uuidv4|generateId)\(\)$/.test(tt.trim())}function jFe(tt){let{root:Mt,files:qt,checks:mr}=tt,Tr=[];try{Tr=yFe(Mt)}catch(Aa){return{issues:[{category:"links",severity:"error",message:Aa instanceof Error?Aa.message:"Failed to parse routes file",location:{file:bD.join(Mt,"app","routes.ts"),line:1,column:1}}],routes:[],components:[]}}let Ur=Mdt(Mt,qt),kn=[];for(let Aa of Ur)try{let Ls=xFe(Aa);kn.push(Ls)}catch(Ls){console.error(`Warning: Could not parse ${Aa}:`,Ls)}let Nn=[],Fn=new Set(mr.length>0?mr:["links","forms","loader","params","hydration"]);if(Fn.has("links")&&Nn.push(...DFe(kn,Tr)),Fn.has("forms")&&Nn.push(...FFe(kn,Tr,Mt)),Fn.has("loader")&&Nn.push(...OFe(kn)),Fn.has("params")&&Nn.push(...MFe(kn,Tr,Mt)),Fn.has("hydration")&&Nn.push(...RFe(kn)),qt.length>0){let Aa=new Set(qt.map(xl=>bD.resolve(xl)));return{issues:Nn.filter(xl=>Aa.has(bD.resolve(xl.location.file))),routes:Tr,components:kn}}return{issues:Nn,routes:Tr,components:kn}}function Mdt(tt,Mt){if(Mt.length>0)return Mt.map(Tr=>bD.resolve(Tr));let qt=bD.join(tt,"app","routes");if(!DQ.existsSync(qt))return[];let mr=[];return BFe(qt,Tr=>{Tr.endsWith(".tsx")&&mr.push(Tr)}),mr}function BFe(tt,Mt){let qt=DQ.readdirSync(tt,{withFileTypes:!0});for(let mr of qt){let Tr=bD.join(tt,mr.name);mr.isDirectory()?BFe(Tr,Mt):mr.isFile()&&Mt(Tr)}}var PQ=Fg(require("fs"));function JFe(tt,Mt=!1){let qt=tt.filter(Fn=>Fn.fix),mr=tt.filter(Fn=>!Fn.fix),Tr=new Map;for(let Fn of qt)for(let Aa of Fn.fix.edits){let Ls=Tr.get(Aa.file)||[];Ls.push({edit:Aa,issue:Fn}),Tr.set(Aa.file,Ls)}let Ur=[],kn=[],Nn=[];for(let[Fn,Aa]of Tr)try{let Ls=Aa.map(Re=>Re.edit);if(Rdt(Fn,Ls,Mt).modified){Ur.push(Fn);for(let{issue:Re}of Aa)kn.includes(Re)||kn.push(Re)}}catch(Ls){Nn.push({file:Fn,error:Ls instanceof Error?Ls.message:String(Ls)})}return{filesModified:Ur,fixesApplied:kn.length,fixedIssues:kn,unfixableIssues:mr,errors:Nn}}function Rdt(tt,Mt,qt){let mr=PQ.readFileSync(tt,"utf-8"),Tr=[...Mt].sort((kn,Nn)=>Nn.start-kn.start);for(let kn=0;kn<Tr.length-1;kn++){let Nn=Tr[kn],Fn=Tr[kn+1];if(Nn.start<Fn.end)throw new Error(`Overlapping edits at positions ${Fn.start}-${Fn.end} and ${Nn.start}-${Nn.end}`)}let Ur=mr;for(let kn of Tr)Ur=Ur.slice(0,kn.start)+kn.newText+Ur.slice(kn.end);return!qt&&Ur!==mr?(PQ.writeFileSync(tt,Ur,"utf-8"),{modified:!0,newContent:Ur}):{modified:Ur!==mr,newContent:Ur}}var wQ=Fg(require("path"));function jdt(tt,Mt=process.cwd()){let qt=[],mr=wQ.relative(Mt,tt.location.file),Tr=wQ.basename(mr);return qt.push(`[${tt.severity}] ${Tr}:${tt.location.line}:${tt.location.column}`),tt.code&&qt.push(` ${tt.code}`),qt.push(` x ${tt.message}`),tt.suggestion&&qt.push(` -> ${tt.suggestion}`),qt.join(`
|
|
436
436
|
`)}function zFe(tt,Mt=process.cwd()){if(tt.length===0)return"No issues found.";let qt=[];qt.push(`
|
|
437
437
|
rr found ${tt.length} issue${tt.length===1?"":"s"}:`),qt.push("");for(let Ur of tt)qt.push(jdt(Ur,Mt)),qt.push("");let mr=tt.filter(Ur=>Ur.severity==="error").length,Tr=tt.filter(Ur=>Ur.severity==="warning").length;return qt.push(`Summary: ${mr} error${mr===1?"":"s"}, ${Tr} warning${Tr===1?"":"s"}`),qt.push("Run with --help for options."),qt.join(`
|
|
438
|
-
`)}var WFe=Fg(require("path"));function Bdt(tt,Mt=process.cwd()){let qt=WFe.relative(Mt,tt.location.file),mr={category:tt.category,severity:tt.severity,message:tt.message,file:qt,line:tt.location.line,column:tt.location.column};return tt.code&&(mr.code=tt.code),tt.suggestion&&(mr.suggestion=tt.suggestion),mr}function VFe(tt,Mt=process.cwd()){let qt=tt.issues.map(Ur=>Bdt(Ur,Mt)),mr=tt.issues.filter(Ur=>Ur.severity==="error").length,Tr=tt.issues.filter(Ur=>Ur.severity==="warning").length;return{issues:qt,summary:{total:tt.issues.length,errors:mr,warnings:Tr}}}function Jdt(){return"0.1.
|
|
438
|
+
`)}var WFe=Fg(require("path"));function Bdt(tt,Mt=process.cwd()){let qt=WFe.relative(Mt,tt.location.file),mr={category:tt.category,severity:tt.severity,message:tt.message,file:qt,line:tt.location.line,column:tt.location.column};return tt.code&&(mr.code=tt.code),tt.suggestion&&(mr.suggestion=tt.suggestion),mr}function VFe(tt,Mt=process.cwd()){let qt=tt.issues.map(Ur=>Bdt(Ur,Mt)),mr=tt.issues.filter(Ur=>Ur.severity==="error").length,Tr=tt.issues.filter(Ur=>Ur.severity==="warning").length;return{issues:qt,summary:{total:tt.issues.length,errors:mr,warnings:Tr}}}function Jdt(){return"0.1.5"}function zdt(){let tt=process.argv.slice(2),Mt=Wdt(tt);Mt.version&&(console.log(Jdt()),process.exit(0)),Mt.help&&(Vdt(),process.exit(0));let qt=jFe(Mt);if(Mt.fix||Mt.dryRun){let Tr=JFe(qt.issues,Mt.dryRun??!1);Mt.format==="json"?console.log(JSON.stringify({...qt,fixResult:Tr},null,2)):Udt(Tr,Mt.dryRun??!1),Tr.unfixableIssues.filter(kn=>kn.severity==="error").length>0&&process.exit(1);return}if(Mt.format==="json"){let Tr=VFe(qt);console.log(JSON.stringify(Tr,null,2))}else console.log(zFe(qt.issues));qt.issues.filter(Tr=>Tr.severity==="error").length>0&&process.exit(1)}function Wdt(tt){let Mt={files:[],checks:[],format:"text",root:process.cwd(),help:!1,version:!1,fix:!1,dryRun:!1},qt=0;for(;qt<tt.length;){let mr=tt[qt];if(mr==="--help"||mr==="-h"){Mt.help=!0,qt++;continue}if(mr==="--version"||mr==="-v"){Mt.version=!0,qt++;continue}if(mr==="--fix"){Mt.fix=!0,qt++;continue}if(mr==="--dry-run"){Mt.dryRun=!0,qt++;continue}if(mr==="--format"||mr==="-f"){let Tr=tt[qt+1];(Tr==="json"||Tr==="text")&&(Mt.format=Tr),qt+=2;continue}if(mr==="--check"||mr==="-c"){let Tr=tt[qt+1];Tr&&(Mt.checks=Tr.split(",").map(Ur=>Ur.trim())),qt+=2;continue}if(mr==="--root"||mr==="-r"){let Tr=tt[qt+1];Tr&&(Mt.root=S7.resolve(Tr)),qt+=2;continue}mr.startsWith("-")||Mt.files.push(mr),qt++}return Mt}function Vdt(){console.log(`
|
|
439
439
|
rr - Static analysis and functional verification for React Router applications
|
|
440
440
|
|
|
441
441
|
USAGE:
|
|
@@ -447,7 +447,7 @@ OPTIONS:
|
|
|
447
447
|
-f, --format <format> Output format: text (default) or json
|
|
448
448
|
-c, --check <checks> Comma-separated list of checks to run (default is all checks)
|
|
449
449
|
Available: links, forms, loader, params, hydration
|
|
450
|
-
-
|
|
450
|
+
-r, --root <path> Project root directory containing the app/ folder (default: cwd)
|
|
451
451
|
--fix Automatically fix issues where possible
|
|
452
452
|
--dry-run Show what would be fixed without modifying files
|
|
453
453
|
|
|
@@ -473,8 +473,8 @@ EXAMPLES:
|
|
|
473
473
|
# Fix specific file(s)
|
|
474
474
|
rr --fix app/routes/dashboard.tsx
|
|
475
475
|
|
|
476
|
-
# Analyze files in
|
|
477
|
-
rr --
|
|
476
|
+
# Analyze files in a different project directory
|
|
477
|
+
rr --root ./my-app ./my-app/app/routes/dashboard.tsx
|
|
478
478
|
`)}function Udt(tt,Mt){let{fixedIssues:qt,unfixableIssues:mr,filesModified:Tr,errors:Ur}=tt;if(qt.length===0&&mr.length===0){console.log("No issues found.");return}if(qt.length>0){console.log(`
|
|
479
479
|
${Mt?"Would fix":"Fixed"} ${qt.length} issue${qt.length===1?"":"s"} in ${Tr.length} file${Tr.length===1?"":"s"}:`),console.log();for(let Nn of qt){let Fn=Nn.severity==="error"?"[error]":"[warning]",Aa=S7.relative(process.cwd(),Nn.location.file),Ls=Mt?"[would fix]":"[fixed]";console.log(`[${Nn.category}] ${Aa}:${Nn.location.line}:${Nn.location.column}`),Nn.code&&console.log(` ${Nn.code}`),console.log(` ${Fn} ${Nn.message}`),Nn.fix&&console.log(` ${Ls} ${Nn.fix.description}`),console.log()}}if(Ur.length>0){console.log("Errors while applying fixes:");for(let{file:kn,error:Nn}of Ur){let Fn=S7.relative(process.cwd(),kn);console.log(` [!] ${Fn}: ${Nn}`)}console.log()}if(mr.length>0){let kn=mr.filter(Fn=>Fn.severity==="error").length,Nn=mr.filter(Fn=>Fn.severity==="warning").length;console.log(`${mr.length} issue${mr.length===1?"":"s"} could not be auto-fixed:`),console.log();for(let Fn of mr){let Aa=Fn.severity==="error"?"[error]":"[warning]",Ls=S7.relative(process.cwd(),Fn.location.file);console.log(`[${Fn.category}] ${Ls}:${Fn.location.line}:${Fn.location.column}`),Fn.code&&console.log(` ${Fn.code}`),console.log(` ${Aa} ${Fn.message}`),Fn.suggestion&&console.log(` -> ${Fn.suggestion}`),console.log()}console.log(`Unfixable: ${kn} error${kn===1?"":"s"}, ${Nn} warning${Nn===1?"":"s"}`)}Mt&&qt.length>0&&console.log("Run with --fix to apply changes.")}zdt();
|
|
480
480
|
/*! Bundled license information:
|