@pyreon/cli 0.11.5 → 0.11.7
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 -20
- package/lib/index.js.map +1 -1
- package/package.json +13 -13
- package/src/context.ts +11 -11
- package/src/doctor.ts +27 -27
- package/src/index.ts +13 -13
- package/src/tests/context.test.ts +79 -79
- package/src/tests/doctor.test.ts +62 -62
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ pyreon doctor --ci # exit code 1 on any error (for CI)
|
|
|
25
25
|
|
|
26
26
|
```tsx
|
|
27
27
|
// BEFORE: React patterns detected by doctor
|
|
28
|
-
import React from
|
|
29
|
-
import { useState, useEffect, useMemo, useCallback } from
|
|
28
|
+
import React from 'react'
|
|
29
|
+
import { useState, useEffect, useMemo, useCallback } from 'react'
|
|
30
30
|
|
|
31
31
|
function Counter() {
|
|
32
32
|
const [count, setCount] = useState(0)
|
|
@@ -36,7 +36,7 @@ function Counter() {
|
|
|
36
36
|
document.title = `Count: ${count}`
|
|
37
37
|
}, [count])
|
|
38
38
|
|
|
39
|
-
const increment = useCallback(() => setCount(c => c + 1), [])
|
|
39
|
+
const increment = useCallback(() => setCount((c) => c + 1), [])
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<div className="counter">
|
|
@@ -50,7 +50,7 @@ function Counter() {
|
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
// AFTER: Pyreon equivalents
|
|
53
|
-
import { signal, computed, effect } from
|
|
53
|
+
import { signal, computed, effect } from '@pyreon/reactivity'
|
|
54
54
|
|
|
55
55
|
function Counter() {
|
|
56
56
|
const count = signal(0)
|
|
@@ -64,7 +64,7 @@ function Counter() {
|
|
|
64
64
|
<div class="counter">
|
|
65
65
|
<label for="display">Count</label>
|
|
66
66
|
<span id="display">{doubled()}</span>
|
|
67
|
-
<button onClick={() => count.update(c => c + 1)}>+1</button>
|
|
67
|
+
<button onClick={() => count.update((c) => c + 1)}>+1</button>
|
|
68
68
|
</div>
|
|
69
69
|
)
|
|
70
70
|
}
|
|
@@ -72,15 +72,15 @@ function Counter() {
|
|
|
72
72
|
|
|
73
73
|
#### Detection table
|
|
74
74
|
|
|
75
|
-
| React Pattern
|
|
76
|
-
|
|
77
|
-
| `import React from "react"` | `import { h } from "@pyreon/core"` | No
|
|
78
|
-
| `useState(initial)`
|
|
79
|
-
| `useEffect(fn, deps)`
|
|
80
|
-
| `useMemo(fn, deps)`
|
|
81
|
-
| `useCallback(fn, deps)`
|
|
82
|
-
| `className="..."`
|
|
83
|
-
| `htmlFor="..."`
|
|
75
|
+
| React Pattern | Pyreon Equivalent | Auto-fixable |
|
|
76
|
+
| --------------------------- | ---------------------------------- | ------------ |
|
|
77
|
+
| `import React from "react"` | `import { h } from "@pyreon/core"` | No |
|
|
78
|
+
| `useState(initial)` | `signal(initial)` | No |
|
|
79
|
+
| `useEffect(fn, deps)` | `effect(fn)` | No |
|
|
80
|
+
| `useMemo(fn, deps)` | `computed(fn)` | No |
|
|
81
|
+
| `useCallback(fn, deps)` | Use function directly | No |
|
|
82
|
+
| `className="..."` | `class="..."` | Yes |
|
|
83
|
+
| `htmlFor="..."` | `for="..."` | Yes |
|
|
84
84
|
|
|
85
85
|
#### CI integration
|
|
86
86
|
|
|
@@ -139,7 +139,13 @@ pyreon context --out ./ai.json # custom output path
|
|
|
139
139
|
"generatedAt": "2026-03-19T12:00:00.000Z",
|
|
140
140
|
"routes": [
|
|
141
141
|
{ "path": "/", "name": "home", "params": [], "hasLoader": false, "hasGuard": false },
|
|
142
|
-
{
|
|
142
|
+
{
|
|
143
|
+
"path": "/users/:id",
|
|
144
|
+
"name": "user",
|
|
145
|
+
"params": ["id"],
|
|
146
|
+
"hasLoader": true,
|
|
147
|
+
"hasGuard": false
|
|
148
|
+
},
|
|
143
149
|
{ "path": "/admin", "params": [], "hasLoader": false, "hasGuard": true }
|
|
144
150
|
],
|
|
145
151
|
"components": [
|
|
@@ -151,20 +157,18 @@ pyreon context --out ./ai.json # custom output path
|
|
|
151
157
|
"signalNames": ["isExpanded"]
|
|
152
158
|
}
|
|
153
159
|
],
|
|
154
|
-
"islands": [
|
|
155
|
-
{ "name": "SearchBar", "file": "src/islands/SearchBar.tsx", "hydrate": "idle" }
|
|
156
|
-
]
|
|
160
|
+
"islands": [{ "name": "SearchBar", "file": "src/islands/SearchBar.tsx", "hydrate": "idle" }]
|
|
157
161
|
}
|
|
158
162
|
```
|
|
159
163
|
|
|
160
164
|
## Programmatic API
|
|
161
165
|
|
|
162
166
|
```ts
|
|
163
|
-
import { doctor, generateContext } from
|
|
167
|
+
import { doctor, generateContext } from '@pyreon/cli'
|
|
164
168
|
|
|
165
169
|
// Run doctor programmatically
|
|
166
170
|
const errorCount = await doctor({ fix: false, json: false, ci: false, cwd: process.cwd() })
|
|
167
171
|
|
|
168
172
|
// Generate context
|
|
169
|
-
const result = generateContext({ cwd: process.cwd(), out:
|
|
173
|
+
const result = generateContext({ cwd: process.cwd(), out: '.pyreon/context.json' })
|
|
170
174
|
```
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["scanProject"],"sources":["../src/context.ts","../src/doctor.ts","../src/index.ts"],"sourcesContent":["/**\n * pyreon context — generates .pyreon/context.json for AI tool consumption\n *\n * Delegates scanning to @pyreon/compiler's unified project scanner,\n * then writes the result to disk and ensures .pyreon/ is gitignored.\n */\n\nimport * as fs from \"node:fs\"\nimport * as path from \"node:path\"\nimport { type ProjectContext, generateContext as scanProject } from \"@pyreon/compiler\"\n\nexport type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from \"@pyreon/compiler\"\n\nexport interface ContextOptions {\n cwd: string\n outPath?: string | undefined\n}\n\nexport async function generateContext(options: ContextOptions): Promise<ProjectContext> {\n const context = scanProject(options.cwd)\n\n // Write to .pyreon/context.json\n const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, \".pyreon\")\n const outFile = options.outPath ?? path.join(outDir, \"context.json\")\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true })\n }\n fs.writeFileSync(outFile, JSON.stringify(context, null, 2), \"utf-8\")\n\n // Ensure .pyreon/ is in .gitignore\n ensureGitignore(options.cwd)\n\n const relOut = path.relative(options.cwd, outFile)\n console.log(\n ` ✓ Generated ${relOut} (${context.components.length} components, ${context.routes.length} routes, ${context.islands.length} islands)`,\n )\n\n return context\n}\n\nfunction ensureGitignore(cwd: string): void {\n const gitignorePath = path.join(cwd, \".gitignore\")\n try {\n const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, \"utf-8\") : \"\"\n\n if (!content.includes(\".pyreon/\") && !content.includes(\".pyreon\\n\")) {\n const addition = content.endsWith(\"\\n\") ? \".pyreon/\\n\" : \"\\n.pyreon/\\n\"\n fs.appendFileSync(gitignorePath, addition)\n }\n } catch {\n // Ignore errors with .gitignore\n }\n}\n","/**\n * pyreon doctor — project-wide health check for AI-friendly development\n *\n * Runs a pipeline of checks:\n * 1. React pattern detection (imports, hooks, JSX attributes)\n * 2. Import source validation (@pyreon/* vs react/vue)\n * 3. Common Pyreon mistakes (signal without call, key vs by, etc.)\n *\n * Output modes:\n * - Human-readable (default): colored terminal output\n * - JSON (--json): structured output for AI agent consumption\n * - CI (--ci): exits with code 1 on any error\n *\n * Fix mode (--fix): auto-applies safe transforms via migrateReactCode\n */\n\nimport * as fs from \"node:fs\"\nimport * as path from \"node:path\"\nimport {\n detectReactPatterns,\n hasReactPatterns,\n migrateReactCode,\n type ReactDiagnostic,\n} from \"@pyreon/compiler\"\n\nexport interface DoctorOptions {\n fix: boolean\n json: boolean\n ci: boolean\n cwd: string\n}\n\ninterface FileResult {\n file: string\n diagnostics: ReactDiagnostic[]\n fixed: boolean\n}\n\ninterface DoctorResult {\n passed: boolean\n files: FileResult[]\n summary: {\n filesScanned: number\n filesWithIssues: number\n totalErrors: number\n totalFixable: number\n totalFixed: number\n }\n}\n\nexport async function doctor(options: DoctorOptions): Promise<number> {\n const startTime = performance.now()\n const files = collectSourceFiles(options.cwd)\n const result = runChecks(files, options)\n const elapsed = Math.round(performance.now() - startTime)\n\n if (options.json) {\n printJson(result)\n } else {\n printHuman(result, elapsed)\n }\n\n return result.summary.totalErrors\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// File collection\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst sourceExtensions = new Set([\".tsx\", \".jsx\", \".ts\", \".js\"])\nconst sourceIgnoreDirs = new Set([\n \"node_modules\",\n \"dist\",\n \"lib\",\n \".pyreon\",\n \".git\",\n \".next\",\n \"build\",\n])\n\nfunction shouldSkipDirEntry(entry: fs.Dirent): boolean {\n if (!entry.isDirectory()) return false\n return entry.name.startsWith(\".\") || sourceIgnoreDirs.has(entry.name)\n}\n\nfunction walkSourceFiles(dir: string, results: string[]): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (shouldSkipDirEntry(entry)) continue\n\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSourceFiles(fullPath, results)\n } else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name))) {\n results.push(fullPath)\n }\n }\n}\n\nfunction collectSourceFiles(cwd: string): string[] {\n const results: string[] = []\n walkSourceFiles(cwd, results)\n return results\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Check pipeline\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction checkFileWithFix(\n file: string,\n relPath: string,\n): { result: FileResult | null; fixCount: number } {\n let code: string\n try {\n code = fs.readFileSync(file, \"utf-8\")\n } catch {\n return { result: null, fixCount: 0 }\n }\n\n if (!hasReactPatterns(code)) return { result: null, fixCount: 0 }\n\n const migrated = migrateReactCode(code, relPath)\n if (migrated.changes.length > 0) {\n fs.writeFileSync(file, migrated.code, \"utf-8\")\n }\n const remaining = detectReactPatterns(migrated.code, relPath)\n if (remaining.length > 0 || migrated.changes.length > 0) {\n return {\n result: { file: relPath, diagnostics: remaining, fixed: migrated.changes.length > 0 },\n fixCount: migrated.changes.length,\n }\n }\n return { result: null, fixCount: 0 }\n}\n\nfunction checkFileDetectOnly(file: string, relPath: string): FileResult | null {\n let code: string\n try {\n code = fs.readFileSync(file, \"utf-8\")\n } catch {\n return null\n }\n\n if (!hasReactPatterns(code)) return null\n\n const diagnostics = detectReactPatterns(code, relPath)\n if (diagnostics.length > 0) {\n return { file: relPath, diagnostics, fixed: false }\n }\n return null\n}\n\nfunction runChecks(files: string[], options: DoctorOptions): DoctorResult {\n const fileResults: FileResult[] = []\n let totalFixed = 0\n\n for (const file of files) {\n const relPath = path.relative(options.cwd, file)\n\n if (options.fix) {\n const { result, fixCount } = checkFileWithFix(file, relPath)\n totalFixed += fixCount\n if (result) fileResults.push(result)\n } else {\n const result = checkFileDetectOnly(file, relPath)\n if (result) fileResults.push(result)\n }\n }\n\n const totalErrors = fileResults.reduce((sum, f) => sum + f.diagnostics.length, 0)\n const totalFixable = fileResults.reduce(\n (sum, f) => sum + f.diagnostics.filter((d) => d.fixable).length,\n 0,\n )\n\n return {\n passed: totalErrors === 0,\n files: fileResults,\n summary: {\n filesScanned: files.length,\n filesWithIssues: fileResults.length,\n totalErrors,\n totalFixable,\n totalFixed,\n },\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Output formatters\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction printJson(result: DoctorResult): void {\n console.log(JSON.stringify(result, null, 2))\n}\n\nfunction printFileResult(fileResult: FileResult): void {\n if (fileResult.diagnostics.length === 0) return\n\n console.log(` ${fileResult.file}${fileResult.fixed ? \" (partially fixed)\" : \"\"}`)\n\n for (const diag of fileResult.diagnostics) {\n const fixTag = diag.fixable ? \" [fixable]\" : \"\"\n console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)\n console.log(` Current: ${diag.current}`)\n console.log(` Suggested: ${diag.suggested}`)\n console.log(\"\")\n }\n}\n\nfunction printSummary(summary: DoctorResult[\"summary\"]): void {\n console.log(\n ` ${summary.totalErrors} issue${summary.totalErrors === 1 ? \"\" : \"s\"} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? \"\" : \"s\"}`,\n )\n if (summary.totalFixable > 0) {\n console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)\n }\n console.log(\"\")\n}\n\nfunction printHuman(result: DoctorResult, elapsed: number): void {\n const { summary } = result\n\n console.log(\"\")\n console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)\n console.log(\"\")\n\n if (result.passed && summary.totalFixed === 0) {\n console.log(\" ✓ No issues found. Your code is Pyreon-native!\")\n console.log(\"\")\n return\n }\n\n if (summary.totalFixed > 0) {\n console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? \"\" : \"s\"}`)\n console.log(\"\")\n }\n\n for (const fileResult of result.files) {\n printFileResult(fileResult)\n }\n\n printSummary(summary)\n}\n","#!/usr/bin/env node\n\n/**\n * @pyreon/cli — Developer tools for Pyreon\n *\n * Commands:\n * pyreon doctor [--fix] [--json] — Scan project for React patterns, bad imports, etc.\n * pyreon context — Generate .pyreon/context.json for AI tools\n */\n\nimport { generateContext } from \"./context\"\nimport { type DoctorOptions, doctor } from \"./doctor\"\n\nconst args = process.argv.slice(2)\nconst command = args[0]\n\nfunction printUsage(): void {\n console.log(`\n pyreon <command> [options]\n\n Commands:\n doctor [--fix] [--json] [--ci] Scan for React patterns, bad imports, and common mistakes\n context [--out <path>] Generate .pyreon/context.json for AI tools\n\n Options:\n --help Show this help message\n --version Show version\n`)\n}\n\nasync function main(): Promise<void> {\n if (!command || command === \"--help\" || command === \"-h\") {\n printUsage()\n return\n }\n\n if (command === \"--version\" || command === \"-v\") {\n console.log(\"0.4.0\")\n return\n }\n\n if (command === \"doctor\") {\n const options: DoctorOptions = {\n fix: args.includes(\"--fix\"),\n json: args.includes(\"--json\"),\n ci: args.includes(\"--ci\"),\n cwd: process.cwd(),\n }\n const exitCode = await doctor(options)\n if (options.ci && exitCode > 0) {\n process.exit(1)\n }\n return\n }\n\n if (command === \"context\") {\n const outIdx = args.indexOf(\"--out\")\n const outPath = outIdx >= 0 ? args[outIdx + 1] : undefined\n await generateContext({ cwd: process.cwd(), outPath })\n return\n }\n\n console.error(`Unknown command: ${command}`)\n printUsage()\n process.exit(1)\n}\n\nmain().catch((err) => {\n console.error(err)\n process.exit(1)\n})\n\nexport type { ContextOptions, ProjectContext } from \"./context\"\nexport type { DoctorOptions } from \"./doctor\"\nexport { doctor, generateContext }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["scanProject"],"sources":["../src/context.ts","../src/doctor.ts","../src/index.ts"],"sourcesContent":["/**\n * pyreon context — generates .pyreon/context.json for AI tool consumption\n *\n * Delegates scanning to @pyreon/compiler's unified project scanner,\n * then writes the result to disk and ensures .pyreon/ is gitignored.\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { type ProjectContext, generateContext as scanProject } from '@pyreon/compiler'\n\nexport type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from '@pyreon/compiler'\n\nexport interface ContextOptions {\n cwd: string\n outPath?: string | undefined\n}\n\nexport async function generateContext(options: ContextOptions): Promise<ProjectContext> {\n const context = scanProject(options.cwd)\n\n // Write to .pyreon/context.json\n const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, '.pyreon')\n const outFile = options.outPath ?? path.join(outDir, 'context.json')\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true })\n }\n fs.writeFileSync(outFile, JSON.stringify(context, null, 2), 'utf-8')\n\n // Ensure .pyreon/ is in .gitignore\n ensureGitignore(options.cwd)\n\n const relOut = path.relative(options.cwd, outFile)\n console.log(\n ` ✓ Generated ${relOut} (${context.components.length} components, ${context.routes.length} routes, ${context.islands.length} islands)`,\n )\n\n return context\n}\n\nfunction ensureGitignore(cwd: string): void {\n const gitignorePath = path.join(cwd, '.gitignore')\n try {\n const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : ''\n\n if (!content.includes('.pyreon/') && !content.includes('.pyreon\\n')) {\n const addition = content.endsWith('\\n') ? '.pyreon/\\n' : '\\n.pyreon/\\n'\n fs.appendFileSync(gitignorePath, addition)\n }\n } catch {\n // Ignore errors with .gitignore\n }\n}\n","/**\n * pyreon doctor — project-wide health check for AI-friendly development\n *\n * Runs a pipeline of checks:\n * 1. React pattern detection (imports, hooks, JSX attributes)\n * 2. Import source validation (@pyreon/* vs react/vue)\n * 3. Common Pyreon mistakes (signal without call, key vs by, etc.)\n *\n * Output modes:\n * - Human-readable (default): colored terminal output\n * - JSON (--json): structured output for AI agent consumption\n * - CI (--ci): exits with code 1 on any error\n *\n * Fix mode (--fix): auto-applies safe transforms via migrateReactCode\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport {\n detectReactPatterns,\n hasReactPatterns,\n migrateReactCode,\n type ReactDiagnostic,\n} from '@pyreon/compiler'\n\nexport interface DoctorOptions {\n fix: boolean\n json: boolean\n ci: boolean\n cwd: string\n}\n\ninterface FileResult {\n file: string\n diagnostics: ReactDiagnostic[]\n fixed: boolean\n}\n\ninterface DoctorResult {\n passed: boolean\n files: FileResult[]\n summary: {\n filesScanned: number\n filesWithIssues: number\n totalErrors: number\n totalFixable: number\n totalFixed: number\n }\n}\n\nexport async function doctor(options: DoctorOptions): Promise<number> {\n const startTime = performance.now()\n const files = collectSourceFiles(options.cwd)\n const result = runChecks(files, options)\n const elapsed = Math.round(performance.now() - startTime)\n\n if (options.json) {\n printJson(result)\n } else {\n printHuman(result, elapsed)\n }\n\n return result.summary.totalErrors\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// File collection\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst sourceExtensions = new Set(['.tsx', '.jsx', '.ts', '.js'])\nconst sourceIgnoreDirs = new Set([\n 'node_modules',\n 'dist',\n 'lib',\n '.pyreon',\n '.git',\n '.next',\n 'build',\n])\n\nfunction shouldSkipDirEntry(entry: fs.Dirent): boolean {\n if (!entry.isDirectory()) return false\n return entry.name.startsWith('.') || sourceIgnoreDirs.has(entry.name)\n}\n\nfunction walkSourceFiles(dir: string, results: string[]): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (shouldSkipDirEntry(entry)) continue\n\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSourceFiles(fullPath, results)\n } else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name))) {\n results.push(fullPath)\n }\n }\n}\n\nfunction collectSourceFiles(cwd: string): string[] {\n const results: string[] = []\n walkSourceFiles(cwd, results)\n return results\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Check pipeline\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction checkFileWithFix(\n file: string,\n relPath: string,\n): { result: FileResult | null; fixCount: number } {\n let code: string\n try {\n code = fs.readFileSync(file, 'utf-8')\n } catch {\n return { result: null, fixCount: 0 }\n }\n\n if (!hasReactPatterns(code)) return { result: null, fixCount: 0 }\n\n const migrated = migrateReactCode(code, relPath)\n if (migrated.changes.length > 0) {\n fs.writeFileSync(file, migrated.code, 'utf-8')\n }\n const remaining = detectReactPatterns(migrated.code, relPath)\n if (remaining.length > 0 || migrated.changes.length > 0) {\n return {\n result: { file: relPath, diagnostics: remaining, fixed: migrated.changes.length > 0 },\n fixCount: migrated.changes.length,\n }\n }\n return { result: null, fixCount: 0 }\n}\n\nfunction checkFileDetectOnly(file: string, relPath: string): FileResult | null {\n let code: string\n try {\n code = fs.readFileSync(file, 'utf-8')\n } catch {\n return null\n }\n\n if (!hasReactPatterns(code)) return null\n\n const diagnostics = detectReactPatterns(code, relPath)\n if (diagnostics.length > 0) {\n return { file: relPath, diagnostics, fixed: false }\n }\n return null\n}\n\nfunction runChecks(files: string[], options: DoctorOptions): DoctorResult {\n const fileResults: FileResult[] = []\n let totalFixed = 0\n\n for (const file of files) {\n const relPath = path.relative(options.cwd, file)\n\n if (options.fix) {\n const { result, fixCount } = checkFileWithFix(file, relPath)\n totalFixed += fixCount\n if (result) fileResults.push(result)\n } else {\n const result = checkFileDetectOnly(file, relPath)\n if (result) fileResults.push(result)\n }\n }\n\n const totalErrors = fileResults.reduce((sum, f) => sum + f.diagnostics.length, 0)\n const totalFixable = fileResults.reduce(\n (sum, f) => sum + f.diagnostics.filter((d) => d.fixable).length,\n 0,\n )\n\n return {\n passed: totalErrors === 0,\n files: fileResults,\n summary: {\n filesScanned: files.length,\n filesWithIssues: fileResults.length,\n totalErrors,\n totalFixable,\n totalFixed,\n },\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Output formatters\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction printJson(result: DoctorResult): void {\n console.log(JSON.stringify(result, null, 2))\n}\n\nfunction printFileResult(fileResult: FileResult): void {\n if (fileResult.diagnostics.length === 0) return\n\n console.log(` ${fileResult.file}${fileResult.fixed ? ' (partially fixed)' : ''}`)\n\n for (const diag of fileResult.diagnostics) {\n const fixTag = diag.fixable ? ' [fixable]' : ''\n console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)\n console.log(` Current: ${diag.current}`)\n console.log(` Suggested: ${diag.suggested}`)\n console.log('')\n }\n}\n\nfunction printSummary(summary: DoctorResult['summary']): void {\n console.log(\n ` ${summary.totalErrors} issue${summary.totalErrors === 1 ? '' : 's'} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? '' : 's'}`,\n )\n if (summary.totalFixable > 0) {\n console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)\n }\n console.log('')\n}\n\nfunction printHuman(result: DoctorResult, elapsed: number): void {\n const { summary } = result\n\n console.log('')\n console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)\n console.log('')\n\n if (result.passed && summary.totalFixed === 0) {\n console.log(' ✓ No issues found. Your code is Pyreon-native!')\n console.log('')\n return\n }\n\n if (summary.totalFixed > 0) {\n console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? '' : 's'}`)\n console.log('')\n }\n\n for (const fileResult of result.files) {\n printFileResult(fileResult)\n }\n\n printSummary(summary)\n}\n","#!/usr/bin/env node\n\n/**\n * @pyreon/cli — Developer tools for Pyreon\n *\n * Commands:\n * pyreon doctor [--fix] [--json] — Scan project for React patterns, bad imports, etc.\n * pyreon context — Generate .pyreon/context.json for AI tools\n */\n\nimport { generateContext } from './context'\nimport { type DoctorOptions, doctor } from './doctor'\n\nconst args = process.argv.slice(2)\nconst command = args[0]\n\nfunction printUsage(): void {\n console.log(`\n pyreon <command> [options]\n\n Commands:\n doctor [--fix] [--json] [--ci] Scan for React patterns, bad imports, and common mistakes\n context [--out <path>] Generate .pyreon/context.json for AI tools\n\n Options:\n --help Show this help message\n --version Show version\n`)\n}\n\nasync function main(): Promise<void> {\n if (!command || command === '--help' || command === '-h') {\n printUsage()\n return\n }\n\n if (command === '--version' || command === '-v') {\n console.log('0.4.0')\n return\n }\n\n if (command === 'doctor') {\n const options: DoctorOptions = {\n fix: args.includes('--fix'),\n json: args.includes('--json'),\n ci: args.includes('--ci'),\n cwd: process.cwd(),\n }\n const exitCode = await doctor(options)\n if (options.ci && exitCode > 0) {\n process.exit(1)\n }\n return\n }\n\n if (command === 'context') {\n const outIdx = args.indexOf('--out')\n const outPath = outIdx >= 0 ? args[outIdx + 1] : undefined\n await generateContext({ cwd: process.cwd(), outPath })\n return\n }\n\n console.error(`Unknown command: ${command}`)\n printUsage()\n process.exit(1)\n}\n\nmain().catch((err) => {\n console.error(err)\n process.exit(1)\n})\n\nexport type { ContextOptions, ProjectContext } from './context'\nexport type { DoctorOptions } from './doctor'\nexport { doctor, generateContext }\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "CLI tools for Pyreon — doctor, generate, context",
|
|
5
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/cli#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
13
|
"directory": "packages/tools/cli"
|
|
10
14
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
"url": "https://github.com/pyreon/pyreon/issues"
|
|
15
|
+
"bin": {
|
|
16
|
+
"pyreon": "./lib/index.js"
|
|
14
17
|
},
|
|
15
18
|
"files": [
|
|
16
19
|
"lib",
|
|
@@ -18,14 +21,11 @@
|
|
|
18
21
|
"README.md",
|
|
19
22
|
"LICENSE"
|
|
20
23
|
],
|
|
21
|
-
"sideEffects": false,
|
|
22
24
|
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
23
26
|
"main": "./lib/index.js",
|
|
24
27
|
"module": "./lib/index.js",
|
|
25
28
|
"types": "./lib/types/index.d.ts",
|
|
26
|
-
"bin": {
|
|
27
|
-
"pyreon": "./lib/index.js"
|
|
28
|
-
},
|
|
29
29
|
"exports": {
|
|
30
30
|
".": {
|
|
31
31
|
"bun": "./src/index.ts",
|
|
@@ -33,21 +33,21 @@
|
|
|
33
33
|
"types": "./lib/types/index.d.ts"
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
36
39
|
"scripts": {
|
|
37
40
|
"build": "vl_rolldown_build",
|
|
38
41
|
"dev": "vl_rolldown_build-watch",
|
|
39
42
|
"test": "vitest run",
|
|
40
43
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"lint": "
|
|
44
|
+
"lint": "oxlint .",
|
|
42
45
|
"prepublishOnly": "bun run build"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"@pyreon/compiler": "^0.11.
|
|
48
|
+
"@pyreon/compiler": "^0.11.7"
|
|
46
49
|
},
|
|
47
50
|
"peerDependencies": {
|
|
48
51
|
"typescript": ">=5.0.0"
|
|
49
|
-
},
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"access": "public"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/context.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* then writes the result to disk and ensures .pyreon/ is gitignored.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import * as fs from
|
|
9
|
-
import * as path from
|
|
10
|
-
import { type ProjectContext, generateContext as scanProject } from
|
|
8
|
+
import * as fs from 'node:fs'
|
|
9
|
+
import * as path from 'node:path'
|
|
10
|
+
import { type ProjectContext, generateContext as scanProject } from '@pyreon/compiler'
|
|
11
11
|
|
|
12
|
-
export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from
|
|
12
|
+
export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from '@pyreon/compiler'
|
|
13
13
|
|
|
14
14
|
export interface ContextOptions {
|
|
15
15
|
cwd: string
|
|
@@ -20,13 +20,13 @@ export async function generateContext(options: ContextOptions): Promise<ProjectC
|
|
|
20
20
|
const context = scanProject(options.cwd)
|
|
21
21
|
|
|
22
22
|
// Write to .pyreon/context.json
|
|
23
|
-
const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd,
|
|
24
|
-
const outFile = options.outPath ?? path.join(outDir,
|
|
23
|
+
const outDir = options.outPath ? path.dirname(options.outPath) : path.join(options.cwd, '.pyreon')
|
|
24
|
+
const outFile = options.outPath ?? path.join(outDir, 'context.json')
|
|
25
25
|
|
|
26
26
|
if (!fs.existsSync(outDir)) {
|
|
27
27
|
fs.mkdirSync(outDir, { recursive: true })
|
|
28
28
|
}
|
|
29
|
-
fs.writeFileSync(outFile, JSON.stringify(context, null, 2),
|
|
29
|
+
fs.writeFileSync(outFile, JSON.stringify(context, null, 2), 'utf-8')
|
|
30
30
|
|
|
31
31
|
// Ensure .pyreon/ is in .gitignore
|
|
32
32
|
ensureGitignore(options.cwd)
|
|
@@ -40,12 +40,12 @@ export async function generateContext(options: ContextOptions): Promise<ProjectC
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function ensureGitignore(cwd: string): void {
|
|
43
|
-
const gitignorePath = path.join(cwd,
|
|
43
|
+
const gitignorePath = path.join(cwd, '.gitignore')
|
|
44
44
|
try {
|
|
45
|
-
const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath,
|
|
45
|
+
const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : ''
|
|
46
46
|
|
|
47
|
-
if (!content.includes(
|
|
48
|
-
const addition = content.endsWith(
|
|
47
|
+
if (!content.includes('.pyreon/') && !content.includes('.pyreon\n')) {
|
|
48
|
+
const addition = content.endsWith('\n') ? '.pyreon/\n' : '\n.pyreon/\n'
|
|
49
49
|
fs.appendFileSync(gitignorePath, addition)
|
|
50
50
|
}
|
|
51
51
|
} catch {
|
package/src/doctor.ts
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* Fix mode (--fix): auto-applies safe transforms via migrateReactCode
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import * as fs from
|
|
18
|
-
import * as path from
|
|
17
|
+
import * as fs from 'node:fs'
|
|
18
|
+
import * as path from 'node:path'
|
|
19
19
|
import {
|
|
20
20
|
detectReactPatterns,
|
|
21
21
|
hasReactPatterns,
|
|
22
22
|
migrateReactCode,
|
|
23
23
|
type ReactDiagnostic,
|
|
24
|
-
} from
|
|
24
|
+
} from '@pyreon/compiler'
|
|
25
25
|
|
|
26
26
|
export interface DoctorOptions {
|
|
27
27
|
fix: boolean
|
|
@@ -67,20 +67,20 @@ export async function doctor(options: DoctorOptions): Promise<number> {
|
|
|
67
67
|
// File collection
|
|
68
68
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
69
69
|
|
|
70
|
-
const sourceExtensions = new Set([
|
|
70
|
+
const sourceExtensions = new Set(['.tsx', '.jsx', '.ts', '.js'])
|
|
71
71
|
const sourceIgnoreDirs = new Set([
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
'node_modules',
|
|
73
|
+
'dist',
|
|
74
|
+
'lib',
|
|
75
|
+
'.pyreon',
|
|
76
|
+
'.git',
|
|
77
|
+
'.next',
|
|
78
|
+
'build',
|
|
79
79
|
])
|
|
80
80
|
|
|
81
81
|
function shouldSkipDirEntry(entry: fs.Dirent): boolean {
|
|
82
82
|
if (!entry.isDirectory()) return false
|
|
83
|
-
return entry.name.startsWith(
|
|
83
|
+
return entry.name.startsWith('.') || sourceIgnoreDirs.has(entry.name)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function walkSourceFiles(dir: string, results: string[]): void {
|
|
@@ -119,7 +119,7 @@ function checkFileWithFix(
|
|
|
119
119
|
): { result: FileResult | null; fixCount: number } {
|
|
120
120
|
let code: string
|
|
121
121
|
try {
|
|
122
|
-
code = fs.readFileSync(file,
|
|
122
|
+
code = fs.readFileSync(file, 'utf-8')
|
|
123
123
|
} catch {
|
|
124
124
|
return { result: null, fixCount: 0 }
|
|
125
125
|
}
|
|
@@ -128,7 +128,7 @@ function checkFileWithFix(
|
|
|
128
128
|
|
|
129
129
|
const migrated = migrateReactCode(code, relPath)
|
|
130
130
|
if (migrated.changes.length > 0) {
|
|
131
|
-
fs.writeFileSync(file, migrated.code,
|
|
131
|
+
fs.writeFileSync(file, migrated.code, 'utf-8')
|
|
132
132
|
}
|
|
133
133
|
const remaining = detectReactPatterns(migrated.code, relPath)
|
|
134
134
|
if (remaining.length > 0 || migrated.changes.length > 0) {
|
|
@@ -143,7 +143,7 @@ function checkFileWithFix(
|
|
|
143
143
|
function checkFileDetectOnly(file: string, relPath: string): FileResult | null {
|
|
144
144
|
let code: string
|
|
145
145
|
try {
|
|
146
|
-
code = fs.readFileSync(file,
|
|
146
|
+
code = fs.readFileSync(file, 'utf-8')
|
|
147
147
|
} catch {
|
|
148
148
|
return null
|
|
149
149
|
}
|
|
@@ -204,43 +204,43 @@ function printJson(result: DoctorResult): void {
|
|
|
204
204
|
function printFileResult(fileResult: FileResult): void {
|
|
205
205
|
if (fileResult.diagnostics.length === 0) return
|
|
206
206
|
|
|
207
|
-
console.log(` ${fileResult.file}${fileResult.fixed ?
|
|
207
|
+
console.log(` ${fileResult.file}${fileResult.fixed ? ' (partially fixed)' : ''}`)
|
|
208
208
|
|
|
209
209
|
for (const diag of fileResult.diagnostics) {
|
|
210
|
-
const fixTag = diag.fixable ?
|
|
210
|
+
const fixTag = diag.fixable ? ' [fixable]' : ''
|
|
211
211
|
console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)
|
|
212
212
|
console.log(` Current: ${diag.current}`)
|
|
213
213
|
console.log(` Suggested: ${diag.suggested}`)
|
|
214
|
-
console.log(
|
|
214
|
+
console.log('')
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function printSummary(summary: DoctorResult[
|
|
218
|
+
function printSummary(summary: DoctorResult['summary']): void {
|
|
219
219
|
console.log(
|
|
220
|
-
` ${summary.totalErrors} issue${summary.totalErrors === 1 ?
|
|
220
|
+
` ${summary.totalErrors} issue${summary.totalErrors === 1 ? '' : 's'} in ${summary.filesWithIssues} file${summary.filesWithIssues === 1 ? '' : 's'}`,
|
|
221
221
|
)
|
|
222
222
|
if (summary.totalFixable > 0) {
|
|
223
223
|
console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)
|
|
224
224
|
}
|
|
225
|
-
console.log(
|
|
225
|
+
console.log('')
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
function printHuman(result: DoctorResult, elapsed: number): void {
|
|
229
229
|
const { summary } = result
|
|
230
230
|
|
|
231
|
-
console.log(
|
|
231
|
+
console.log('')
|
|
232
232
|
console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)
|
|
233
|
-
console.log(
|
|
233
|
+
console.log('')
|
|
234
234
|
|
|
235
235
|
if (result.passed && summary.totalFixed === 0) {
|
|
236
|
-
console.log(
|
|
237
|
-
console.log(
|
|
236
|
+
console.log(' ✓ No issues found. Your code is Pyreon-native!')
|
|
237
|
+
console.log('')
|
|
238
238
|
return
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
if (summary.totalFixed > 0) {
|
|
242
|
-
console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ?
|
|
243
|
-
console.log(
|
|
242
|
+
console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? '' : 's'}`)
|
|
243
|
+
console.log('')
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
for (const fileResult of result.files) {
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* pyreon context — Generate .pyreon/context.json for AI tools
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { generateContext } from
|
|
12
|
-
import { type DoctorOptions, doctor } from
|
|
11
|
+
import { generateContext } from './context'
|
|
12
|
+
import { type DoctorOptions, doctor } from './doctor'
|
|
13
13
|
|
|
14
14
|
const args = process.argv.slice(2)
|
|
15
15
|
const command = args[0]
|
|
@@ -29,21 +29,21 @@ function printUsage(): void {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function main(): Promise<void> {
|
|
32
|
-
if (!command || command ===
|
|
32
|
+
if (!command || command === '--help' || command === '-h') {
|
|
33
33
|
printUsage()
|
|
34
34
|
return
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
if (command ===
|
|
38
|
-
console.log(
|
|
37
|
+
if (command === '--version' || command === '-v') {
|
|
38
|
+
console.log('0.4.0')
|
|
39
39
|
return
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
if (command ===
|
|
42
|
+
if (command === 'doctor') {
|
|
43
43
|
const options: DoctorOptions = {
|
|
44
|
-
fix: args.includes(
|
|
45
|
-
json: args.includes(
|
|
46
|
-
ci: args.includes(
|
|
44
|
+
fix: args.includes('--fix'),
|
|
45
|
+
json: args.includes('--json'),
|
|
46
|
+
ci: args.includes('--ci'),
|
|
47
47
|
cwd: process.cwd(),
|
|
48
48
|
}
|
|
49
49
|
const exitCode = await doctor(options)
|
|
@@ -53,8 +53,8 @@ async function main(): Promise<void> {
|
|
|
53
53
|
return
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
if (command ===
|
|
57
|
-
const outIdx = args.indexOf(
|
|
56
|
+
if (command === 'context') {
|
|
57
|
+
const outIdx = args.indexOf('--out')
|
|
58
58
|
const outPath = outIdx >= 0 ? args[outIdx + 1] : undefined
|
|
59
59
|
await generateContext({ cwd: process.cwd(), outPath })
|
|
60
60
|
return
|
|
@@ -70,6 +70,6 @@ main().catch((err) => {
|
|
|
70
70
|
process.exit(1)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
export type { ContextOptions, ProjectContext } from
|
|
74
|
-
export type { DoctorOptions } from
|
|
73
|
+
export type { ContextOptions, ProjectContext } from './context'
|
|
74
|
+
export type { DoctorOptions } from './doctor'
|
|
75
75
|
export { doctor, generateContext }
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
4
|
-
import { generateContext } from
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { generateContext } from '../context'
|
|
5
5
|
|
|
6
6
|
function makeTmpDir(): string {
|
|
7
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
7
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-ctx-'))
|
|
8
8
|
return dir
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function writeFile(dir: string, relPath: string, content: string): void {
|
|
12
12
|
const full = path.join(dir, relPath)
|
|
13
13
|
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
14
|
-
fs.writeFileSync(full, content,
|
|
14
|
+
fs.writeFileSync(full, content, 'utf-8')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
describe(
|
|
17
|
+
describe('generateContext', () => {
|
|
18
18
|
let tmpDir: string
|
|
19
19
|
|
|
20
20
|
beforeEach(() => {
|
|
@@ -22,11 +22,11 @@ describe("generateContext", () => {
|
|
|
22
22
|
// Every project needs a package.json
|
|
23
23
|
writeFile(
|
|
24
24
|
tmpDir,
|
|
25
|
-
|
|
25
|
+
'package.json',
|
|
26
26
|
JSON.stringify({
|
|
27
|
-
name:
|
|
28
|
-
version:
|
|
29
|
-
dependencies: {
|
|
27
|
+
name: 'test-app',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
dependencies: { '@pyreon/core': '^0.4.0' },
|
|
30
30
|
}),
|
|
31
31
|
)
|
|
32
32
|
})
|
|
@@ -35,10 +35,10 @@ describe("generateContext", () => {
|
|
|
35
35
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
38
|
+
it('extracts routes from createRouter([...]) calls', async () => {
|
|
39
39
|
writeFile(
|
|
40
40
|
tmpDir,
|
|
41
|
-
|
|
41
|
+
'src/router.ts',
|
|
42
42
|
`
|
|
43
43
|
import { createRouter } from "@pyreon/router"
|
|
44
44
|
|
|
@@ -51,14 +51,14 @@ export default createRouter([
|
|
|
51
51
|
|
|
52
52
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
53
53
|
expect(ctx.routes).toHaveLength(2)
|
|
54
|
-
expect(ctx.routes[0]?.path).toBe(
|
|
55
|
-
expect(ctx.routes[1]?.path).toBe(
|
|
54
|
+
expect(ctx.routes[0]?.path).toBe('/')
|
|
55
|
+
expect(ctx.routes[1]?.path).toBe('/about')
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
it(
|
|
58
|
+
it('extracts route params (:id, :slug)', async () => {
|
|
59
59
|
writeFile(
|
|
60
60
|
tmpDir,
|
|
61
|
-
|
|
61
|
+
'src/router.ts',
|
|
62
62
|
`
|
|
63
63
|
const routes = [
|
|
64
64
|
{ path: "/users/:id", component: User },
|
|
@@ -69,14 +69,14 @@ const routes = [
|
|
|
69
69
|
|
|
70
70
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
71
71
|
expect(ctx.routes).toHaveLength(2)
|
|
72
|
-
expect(ctx.routes[0]?.params).toEqual([
|
|
73
|
-
expect(ctx.routes[1]?.params).toEqual([
|
|
72
|
+
expect(ctx.routes[0]?.params).toEqual(['id'])
|
|
73
|
+
expect(ctx.routes[1]?.params).toEqual(['slug', 'commentId'])
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
it(
|
|
76
|
+
it('detects loader and guard presence on a route', async () => {
|
|
77
77
|
writeFile(
|
|
78
78
|
tmpDir,
|
|
79
|
-
|
|
79
|
+
'src/router.ts',
|
|
80
80
|
`
|
|
81
81
|
const routes = [
|
|
82
82
|
{ path: "/dashboard", component: Dashboard, loader: fetchDashboard, beforeEnter: authGuard },
|
|
@@ -92,10 +92,10 @@ const routes = [
|
|
|
92
92
|
expect(dashboard?.hasGuard).toBe(true)
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
it(
|
|
95
|
+
it('reports no loader/guard when absent', async () => {
|
|
96
96
|
writeFile(
|
|
97
97
|
tmpDir,
|
|
98
|
-
|
|
98
|
+
'src/router.ts',
|
|
99
99
|
`
|
|
100
100
|
const routes = [
|
|
101
101
|
{ path: "/public", component: Public },
|
|
@@ -111,10 +111,10 @@ const routes = [
|
|
|
111
111
|
expect(publicRoute?.hasGuard).toBe(false)
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
it(
|
|
114
|
+
it('extracts component names and props', async () => {
|
|
115
115
|
writeFile(
|
|
116
116
|
tmpDir,
|
|
117
|
-
|
|
117
|
+
'src/components/Button.tsx',
|
|
118
118
|
`
|
|
119
119
|
export function Button({ label, onClick, disabled }) {
|
|
120
120
|
return <button disabled={disabled} onClick={onClick}>{label}</button>
|
|
@@ -123,17 +123,17 @@ export function Button({ label, onClick, disabled }) {
|
|
|
123
123
|
)
|
|
124
124
|
|
|
125
125
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
126
|
-
const button = ctx.components.find((c) => c.name ===
|
|
126
|
+
const button = ctx.components.find((c) => c.name === 'Button')
|
|
127
127
|
expect(button).toBeDefined()
|
|
128
|
-
expect(button?.props).toContain(
|
|
129
|
-
expect(button?.props).toContain(
|
|
130
|
-
expect(button?.props).toContain(
|
|
128
|
+
expect(button?.props).toContain('label')
|
|
129
|
+
expect(button?.props).toContain('onClick')
|
|
130
|
+
expect(button?.props).toContain('disabled')
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
-
it(
|
|
133
|
+
it('detects signal usage in components', async () => {
|
|
134
134
|
writeFile(
|
|
135
135
|
tmpDir,
|
|
136
|
-
|
|
136
|
+
'src/components/Counter.tsx',
|
|
137
137
|
`
|
|
138
138
|
export function Counter() {
|
|
139
139
|
const count = signal<number>(0)
|
|
@@ -144,17 +144,17 @@ export function Counter() {
|
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
147
|
-
const counter = ctx.components.find((c) => c.name ===
|
|
147
|
+
const counter = ctx.components.find((c) => c.name === 'Counter')
|
|
148
148
|
expect(counter).toBeDefined()
|
|
149
149
|
expect(counter?.hasSignals).toBe(true)
|
|
150
|
-
expect(counter?.signalNames).toContain(
|
|
151
|
-
expect(counter?.signalNames).toContain(
|
|
150
|
+
expect(counter?.signalNames).toContain('count')
|
|
151
|
+
expect(counter?.signalNames).toContain('doubled')
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it(
|
|
154
|
+
it('extracts island declarations', async () => {
|
|
155
155
|
writeFile(
|
|
156
156
|
tmpDir,
|
|
157
|
-
|
|
157
|
+
'src/islands/Search.tsx',
|
|
158
158
|
`
|
|
159
159
|
import { island } from "@pyreon/server"
|
|
160
160
|
|
|
@@ -164,14 +164,14 @@ export const SearchIsland = island(() => import("./SearchWidget"), { name: "Sear
|
|
|
164
164
|
|
|
165
165
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
166
166
|
expect(ctx.islands).toHaveLength(1)
|
|
167
|
-
expect(ctx.islands[0]?.name).toBe(
|
|
168
|
-
expect(ctx.islands[0]?.file).toBe(path.join(
|
|
167
|
+
expect(ctx.islands[0]?.name).toBe('Search')
|
|
168
|
+
expect(ctx.islands[0]?.file).toBe(path.join('src', 'islands', 'Search.tsx'))
|
|
169
169
|
})
|
|
170
170
|
|
|
171
|
-
it(
|
|
171
|
+
it('writes context.json to .pyreon/ directory', async () => {
|
|
172
172
|
writeFile(
|
|
173
173
|
tmpDir,
|
|
174
|
-
|
|
174
|
+
'src/App.tsx',
|
|
175
175
|
`
|
|
176
176
|
export function App() {
|
|
177
177
|
return <div>Hello</div>
|
|
@@ -181,32 +181,32 @@ export function App() {
|
|
|
181
181
|
|
|
182
182
|
await generateContext({ cwd: tmpDir })
|
|
183
183
|
|
|
184
|
-
const outPath = path.join(tmpDir,
|
|
184
|
+
const outPath = path.join(tmpDir, '.pyreon', 'context.json')
|
|
185
185
|
expect(fs.existsSync(outPath)).toBe(true)
|
|
186
186
|
|
|
187
|
-
const written = JSON.parse(fs.readFileSync(outPath,
|
|
188
|
-
expect(written.framework).toBe(
|
|
189
|
-
expect(written.version).toBe(
|
|
187
|
+
const written = JSON.parse(fs.readFileSync(outPath, 'utf-8'))
|
|
188
|
+
expect(written.framework).toBe('pyreon')
|
|
189
|
+
expect(written.version).toBe('0.4.0')
|
|
190
190
|
expect(written.generatedAt).toBeDefined()
|
|
191
191
|
expect(Array.isArray(written.routes)).toBe(true)
|
|
192
192
|
expect(Array.isArray(written.components)).toBe(true)
|
|
193
193
|
expect(Array.isArray(written.islands)).toBe(true)
|
|
194
194
|
})
|
|
195
195
|
|
|
196
|
-
it(
|
|
196
|
+
it('handles empty project (no routes/components)', async () => {
|
|
197
197
|
// No .tsx files at all, just the package.json
|
|
198
198
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
199
199
|
|
|
200
200
|
expect(ctx.routes).toEqual([])
|
|
201
201
|
expect(ctx.components).toEqual([])
|
|
202
202
|
expect(ctx.islands).toEqual([])
|
|
203
|
-
expect(ctx.framework).toBe(
|
|
203
|
+
expect(ctx.framework).toBe('pyreon')
|
|
204
204
|
})
|
|
205
205
|
|
|
206
|
-
it(
|
|
206
|
+
it('skips node_modules and dist directories', async () => {
|
|
207
207
|
writeFile(
|
|
208
208
|
tmpDir,
|
|
209
|
-
|
|
209
|
+
'node_modules/@pyreon/core/src/index.tsx',
|
|
210
210
|
`
|
|
211
211
|
export function Internal() {
|
|
212
212
|
return <div>internal</div>
|
|
@@ -215,7 +215,7 @@ export function Internal() {
|
|
|
215
215
|
)
|
|
216
216
|
writeFile(
|
|
217
217
|
tmpDir,
|
|
218
|
-
|
|
218
|
+
'dist/App.tsx',
|
|
219
219
|
`
|
|
220
220
|
export function DistApp() {
|
|
221
221
|
return <div>dist</div>
|
|
@@ -224,7 +224,7 @@ export function DistApp() {
|
|
|
224
224
|
)
|
|
225
225
|
writeFile(
|
|
226
226
|
tmpDir,
|
|
227
|
-
|
|
227
|
+
'src/App.tsx',
|
|
228
228
|
`
|
|
229
229
|
export function RealApp() {
|
|
230
230
|
return <div>real</div>
|
|
@@ -234,15 +234,15 @@ export function RealApp() {
|
|
|
234
234
|
|
|
235
235
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
236
236
|
const names = ctx.components.map((c) => c.name)
|
|
237
|
-
expect(names).toContain(
|
|
238
|
-
expect(names).not.toContain(
|
|
239
|
-
expect(names).not.toContain(
|
|
237
|
+
expect(names).toContain('RealApp')
|
|
238
|
+
expect(names).not.toContain('Internal')
|
|
239
|
+
expect(names).not.toContain('DistApp')
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
it(
|
|
242
|
+
it('extracts routes from const routes = [...] syntax', async () => {
|
|
243
243
|
writeFile(
|
|
244
244
|
tmpDir,
|
|
245
|
-
|
|
245
|
+
'src/router.ts',
|
|
246
246
|
`
|
|
247
247
|
const routes: RouteRecord[] = [
|
|
248
248
|
{ path: "/home", name: "home", component: Home },
|
|
@@ -252,14 +252,14 @@ const routes: RouteRecord[] = [
|
|
|
252
252
|
|
|
253
253
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
254
254
|
expect(ctx.routes).toHaveLength(1)
|
|
255
|
-
expect(ctx.routes[0]?.path).toBe(
|
|
256
|
-
expect(ctx.routes[0]?.name).toBe(
|
|
255
|
+
expect(ctx.routes[0]?.path).toBe('/home')
|
|
256
|
+
expect(ctx.routes[0]?.name).toBe('home')
|
|
257
257
|
})
|
|
258
258
|
|
|
259
259
|
it("defaults island hydrate to 'load' when not specified", async () => {
|
|
260
260
|
writeFile(
|
|
261
261
|
tmpDir,
|
|
262
|
-
|
|
262
|
+
'src/islands/Nav.tsx',
|
|
263
263
|
`
|
|
264
264
|
export const NavIsland = island(
|
|
265
265
|
() => import("./NavWidget"),
|
|
@@ -270,60 +270,60 @@ export const NavIsland = island(
|
|
|
270
270
|
|
|
271
271
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
272
272
|
expect(ctx.islands).toHaveLength(1)
|
|
273
|
-
expect(ctx.islands[0]?.hydrate).toBe(
|
|
273
|
+
expect(ctx.islands[0]?.hydrate).toBe('load')
|
|
274
274
|
})
|
|
275
275
|
|
|
276
|
-
it(
|
|
277
|
-
writeFile(tmpDir,
|
|
276
|
+
it('ensures .pyreon/ is added to .gitignore', async () => {
|
|
277
|
+
writeFile(tmpDir, '.gitignore', 'node_modules/\n')
|
|
278
278
|
|
|
279
279
|
await generateContext({ cwd: tmpDir })
|
|
280
280
|
|
|
281
|
-
const gitignore = fs.readFileSync(path.join(tmpDir,
|
|
282
|
-
expect(gitignore).toContain(
|
|
281
|
+
const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8')
|
|
282
|
+
expect(gitignore).toContain('.pyreon/')
|
|
283
283
|
})
|
|
284
284
|
|
|
285
|
-
it(
|
|
286
|
-
writeFile(tmpDir,
|
|
285
|
+
it('does not duplicate .pyreon/ in .gitignore', async () => {
|
|
286
|
+
writeFile(tmpDir, '.gitignore', 'node_modules/\n.pyreon/\n')
|
|
287
287
|
|
|
288
288
|
await generateContext({ cwd: tmpDir })
|
|
289
289
|
|
|
290
|
-
const gitignore = fs.readFileSync(path.join(tmpDir,
|
|
291
|
-
const occurrences = gitignore.split(
|
|
290
|
+
const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8')
|
|
291
|
+
const occurrences = gitignore.split('.pyreon/').length - 1
|
|
292
292
|
expect(occurrences).toBe(1)
|
|
293
293
|
})
|
|
294
294
|
|
|
295
|
-
it(
|
|
296
|
-
writeFile(tmpDir,
|
|
295
|
+
it('reads version from @pyreon/* dependency', async () => {
|
|
296
|
+
writeFile(tmpDir, 'src/App.tsx', `export function App() { return null }`)
|
|
297
297
|
|
|
298
298
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
299
|
-
expect(ctx.version).toBe(
|
|
299
|
+
expect(ctx.version).toBe('0.4.0')
|
|
300
300
|
})
|
|
301
301
|
|
|
302
302
|
it("returns 'unknown' version when no package.json exists", async () => {
|
|
303
303
|
const emptyDir = makeTmpDir()
|
|
304
304
|
try {
|
|
305
305
|
const ctx = await generateContext({ cwd: emptyDir })
|
|
306
|
-
expect(ctx.version).toBe(
|
|
306
|
+
expect(ctx.version).toBe('unknown')
|
|
307
307
|
} finally {
|
|
308
308
|
fs.rmSync(emptyDir, { recursive: true, force: true })
|
|
309
309
|
}
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
it(
|
|
313
|
-
writeFile(tmpDir,
|
|
314
|
-
const customOut = path.join(tmpDir,
|
|
312
|
+
it('writes to custom outPath when specified', async () => {
|
|
313
|
+
writeFile(tmpDir, 'src/App.tsx', `export function App() { return null }`)
|
|
314
|
+
const customOut = path.join(tmpDir, 'custom', 'output.json')
|
|
315
315
|
|
|
316
316
|
await generateContext({ cwd: tmpDir, outPath: customOut })
|
|
317
317
|
|
|
318
318
|
expect(fs.existsSync(customOut)).toBe(true)
|
|
319
|
-
const written = JSON.parse(fs.readFileSync(customOut,
|
|
320
|
-
expect(written.framework).toBe(
|
|
319
|
+
const written = JSON.parse(fs.readFileSync(customOut, 'utf-8'))
|
|
320
|
+
expect(written.framework).toBe('pyreon')
|
|
321
321
|
})
|
|
322
322
|
|
|
323
|
-
it(
|
|
323
|
+
it('detects component without signals', async () => {
|
|
324
324
|
writeFile(
|
|
325
325
|
tmpDir,
|
|
326
|
-
|
|
326
|
+
'src/Static.tsx',
|
|
327
327
|
`
|
|
328
328
|
export function Static({ title }) {
|
|
329
329
|
return <h1>{title}</h1>
|
|
@@ -332,7 +332,7 @@ export function Static({ title }) {
|
|
|
332
332
|
)
|
|
333
333
|
|
|
334
334
|
const ctx = await generateContext({ cwd: tmpDir })
|
|
335
|
-
const comp = ctx.components.find((c) => c.name ===
|
|
335
|
+
const comp = ctx.components.find((c) => c.name === 'Static')
|
|
336
336
|
expect(comp).toBeDefined()
|
|
337
337
|
expect(comp?.hasSignals).toBe(false)
|
|
338
338
|
expect(comp?.signalNames).toEqual([])
|
package/src/tests/doctor.test.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import { type DoctorOptions, doctor } from
|
|
5
|
+
import { type DoctorOptions, doctor } from '../doctor'
|
|
6
6
|
|
|
7
7
|
function makeTmpDir(): string {
|
|
8
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
8
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-doctor-'))
|
|
9
9
|
return dir
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function writeFile(dir: string, relPath: string, content: string): void {
|
|
13
13
|
const full = path.join(dir, relPath)
|
|
14
14
|
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
15
|
-
fs.writeFileSync(full, content,
|
|
15
|
+
fs.writeFileSync(full, content, 'utf-8')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function readFile(dir: string, relPath: string): string {
|
|
19
|
-
return fs.readFileSync(path.join(dir, relPath),
|
|
19
|
+
return fs.readFileSync(path.join(dir, relPath), 'utf-8')
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function defaultOptions(cwd: string): DoctorOptions {
|
|
@@ -42,13 +42,13 @@ export function Counter() {
|
|
|
42
42
|
}
|
|
43
43
|
`
|
|
44
44
|
|
|
45
|
-
describe(
|
|
45
|
+
describe('doctor', () => {
|
|
46
46
|
let tmpDir: string
|
|
47
47
|
let logSpy: ReturnType<typeof vi.spyOn>
|
|
48
48
|
|
|
49
49
|
beforeEach(() => {
|
|
50
50
|
tmpDir = makeTmpDir()
|
|
51
|
-
logSpy = vi.spyOn(console,
|
|
51
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
afterEach(() => {
|
|
@@ -58,59 +58,59 @@ describe("doctor", () => {
|
|
|
58
58
|
|
|
59
59
|
// ─── detect-only mode ──────────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
-
it(
|
|
62
|
-
writeFile(tmpDir,
|
|
61
|
+
it('detects React patterns in files (no --fix)', async () => {
|
|
62
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
63
63
|
|
|
64
64
|
const errorCount = await doctor(defaultOptions(tmpDir))
|
|
65
65
|
|
|
66
66
|
expect(errorCount).toBeGreaterThan(0)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it(
|
|
70
|
-
writeFile(tmpDir,
|
|
69
|
+
it('reports correct file paths and diagnostic counts', async () => {
|
|
70
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
71
71
|
|
|
72
72
|
const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
|
|
73
73
|
await doctor(opts)
|
|
74
74
|
|
|
75
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
75
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
76
76
|
const result = JSON.parse(output)
|
|
77
77
|
|
|
78
78
|
expect(result.passed).toBe(false)
|
|
79
79
|
expect(result.files.length).toBe(1)
|
|
80
|
-
expect(result.files[0].file).toBe(path.join(
|
|
80
|
+
expect(result.files[0].file).toBe(path.join('src', 'App.tsx'))
|
|
81
81
|
expect(result.summary.filesWithIssues).toBe(1)
|
|
82
82
|
expect(result.summary.totalErrors).toBeGreaterThan(0)
|
|
83
83
|
// Should detect: react-import, use-state, use-effect-deps, class-name-prop
|
|
84
84
|
const codes = result.files[0].diagnostics.map((d: { code: string }) => d.code)
|
|
85
|
-
expect(codes).toContain(
|
|
86
|
-
expect(codes).toContain(
|
|
87
|
-
expect(codes).toContain(
|
|
85
|
+
expect(codes).toContain('react-import')
|
|
86
|
+
expect(codes).toContain('use-state')
|
|
87
|
+
expect(codes).toContain('class-name-prop')
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
// ─── --fix mode ────────────────────────────────────────────────────────
|
|
91
91
|
|
|
92
|
-
it(
|
|
93
|
-
writeFile(tmpDir,
|
|
92
|
+
it('--fix mode rewrites files with migrations', async () => {
|
|
93
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
94
94
|
|
|
95
95
|
const opts: DoctorOptions = { fix: true, json: false, ci: false, cwd: tmpDir }
|
|
96
96
|
await doctor(opts)
|
|
97
97
|
|
|
98
|
-
const updated = readFile(tmpDir,
|
|
98
|
+
const updated = readFile(tmpDir, 'src/App.tsx')
|
|
99
99
|
// React import should be removed or rewritten
|
|
100
100
|
expect(updated).not.toContain('from "react"')
|
|
101
101
|
// useState should be migrated to signal
|
|
102
|
-
expect(updated).toContain(
|
|
102
|
+
expect(updated).toContain('signal')
|
|
103
103
|
// className should be migrated to class
|
|
104
|
-
expect(updated).toContain(
|
|
104
|
+
expect(updated).toContain('class=')
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
-
it(
|
|
108
|
-
writeFile(tmpDir,
|
|
107
|
+
it('--fix mode reports totalFixed in JSON output', async () => {
|
|
108
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
109
109
|
|
|
110
110
|
const opts: DoctorOptions = { fix: true, json: true, ci: false, cwd: tmpDir }
|
|
111
111
|
await doctor(opts)
|
|
112
112
|
|
|
113
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
113
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
114
114
|
const result = JSON.parse(output)
|
|
115
115
|
|
|
116
116
|
expect(result.summary.totalFixed).toBeGreaterThan(0)
|
|
@@ -118,30 +118,30 @@ describe("doctor", () => {
|
|
|
118
118
|
|
|
119
119
|
// ─── --json mode ───────────────────────────────────────────────────────
|
|
120
120
|
|
|
121
|
-
it(
|
|
122
|
-
writeFile(tmpDir,
|
|
121
|
+
it('--json mode returns structured JSON output', async () => {
|
|
122
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
123
123
|
|
|
124
124
|
const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
|
|
125
125
|
await doctor(opts)
|
|
126
126
|
|
|
127
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
127
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
128
128
|
const result = JSON.parse(output)
|
|
129
129
|
|
|
130
|
-
expect(result).toHaveProperty(
|
|
131
|
-
expect(result).toHaveProperty(
|
|
132
|
-
expect(result).toHaveProperty(
|
|
133
|
-
expect(result.summary).toHaveProperty(
|
|
134
|
-
expect(result.summary).toHaveProperty(
|
|
135
|
-
expect(result.summary).toHaveProperty(
|
|
136
|
-
expect(result.summary).toHaveProperty(
|
|
137
|
-
expect(result.summary).toHaveProperty(
|
|
130
|
+
expect(result).toHaveProperty('passed')
|
|
131
|
+
expect(result).toHaveProperty('files')
|
|
132
|
+
expect(result).toHaveProperty('summary')
|
|
133
|
+
expect(result.summary).toHaveProperty('filesScanned')
|
|
134
|
+
expect(result.summary).toHaveProperty('filesWithIssues')
|
|
135
|
+
expect(result.summary).toHaveProperty('totalErrors')
|
|
136
|
+
expect(result.summary).toHaveProperty('totalFixable')
|
|
137
|
+
expect(result.summary).toHaveProperty('totalFixed')
|
|
138
138
|
expect(Array.isArray(result.files)).toBe(true)
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
// ─── --ci mode ─────────────────────────────────────────────────────────
|
|
142
142
|
|
|
143
|
-
it(
|
|
144
|
-
writeFile(tmpDir,
|
|
143
|
+
it('--ci mode returns non-zero error count when issues found', async () => {
|
|
144
|
+
writeFile(tmpDir, 'src/App.tsx', REACT_TSX)
|
|
145
145
|
|
|
146
146
|
const opts: DoctorOptions = { fix: false, json: false, ci: true, cwd: tmpDir }
|
|
147
147
|
const errorCount = await doctor(opts)
|
|
@@ -149,8 +149,8 @@ describe("doctor", () => {
|
|
|
149
149
|
expect(errorCount).toBeGreaterThan(0)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
it(
|
|
153
|
-
writeFile(tmpDir,
|
|
152
|
+
it('--ci mode returns 0 when no issues found', async () => {
|
|
153
|
+
writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
|
|
154
154
|
|
|
155
155
|
const opts: DoctorOptions = { fix: false, json: false, ci: true, cwd: tmpDir }
|
|
156
156
|
const errorCount = await doctor(opts)
|
|
@@ -160,16 +160,16 @@ describe("doctor", () => {
|
|
|
160
160
|
|
|
161
161
|
// ─── skipping ──────────────────────────────────────────────────────────
|
|
162
162
|
|
|
163
|
-
it(
|
|
164
|
-
writeFile(tmpDir,
|
|
165
|
-
writeFile(tmpDir,
|
|
166
|
-
writeFile(tmpDir,
|
|
167
|
-
writeFile(tmpDir,
|
|
163
|
+
it('skips node_modules and non-source files', async () => {
|
|
164
|
+
writeFile(tmpDir, 'node_modules/some-pkg/index.tsx', REACT_TSX)
|
|
165
|
+
writeFile(tmpDir, 'dist/bundle.tsx', REACT_TSX)
|
|
166
|
+
writeFile(tmpDir, 'assets/readme.md', '# className something useState')
|
|
167
|
+
writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
|
|
168
168
|
|
|
169
169
|
const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
|
|
170
170
|
await doctor(opts)
|
|
171
171
|
|
|
172
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
172
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
173
173
|
const result = JSON.parse(output)
|
|
174
174
|
|
|
175
175
|
expect(result.passed).toBe(true)
|
|
@@ -180,26 +180,26 @@ describe("doctor", () => {
|
|
|
180
180
|
|
|
181
181
|
// ─── clean project ─────────────────────────────────────────────────────
|
|
182
182
|
|
|
183
|
-
it(
|
|
184
|
-
writeFile(tmpDir,
|
|
183
|
+
it('clean project returns no issues', async () => {
|
|
184
|
+
writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
|
|
185
185
|
|
|
186
186
|
const errorCount = await doctor(defaultOptions(tmpDir))
|
|
187
187
|
|
|
188
188
|
expect(errorCount).toBe(0)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it(
|
|
192
|
-
writeFile(tmpDir,
|
|
191
|
+
it('clean project prints success message in human mode', async () => {
|
|
192
|
+
writeFile(tmpDir, 'src/App.tsx', CLEAN_TSX)
|
|
193
193
|
|
|
194
194
|
await doctor(defaultOptions(tmpDir))
|
|
195
195
|
|
|
196
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
197
|
-
expect(output).toContain(
|
|
196
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n')
|
|
197
|
+
expect(output).toContain('No issues found')
|
|
198
198
|
})
|
|
199
199
|
|
|
200
200
|
// ─── hasReactPatterns pre-filter ────────────────────────────────────────
|
|
201
201
|
|
|
202
|
-
it(
|
|
202
|
+
it('hasReactPatterns pre-filter skips non-React files efficiently', async () => {
|
|
203
203
|
// A file with Pyreon-only code should not produce diagnostics
|
|
204
204
|
const pyreonOnly = `import { signal, computed, effect } from "@pyreon/reactivity"
|
|
205
205
|
import { onMount } from "@pyreon/core"
|
|
@@ -212,12 +212,12 @@ export function App() {
|
|
|
212
212
|
return <div class="app">{count()}</div>
|
|
213
213
|
}
|
|
214
214
|
`
|
|
215
|
-
writeFile(tmpDir,
|
|
215
|
+
writeFile(tmpDir, 'src/App.tsx', pyreonOnly)
|
|
216
216
|
|
|
217
217
|
const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
|
|
218
218
|
await doctor(opts)
|
|
219
219
|
|
|
220
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
220
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
221
221
|
const result = JSON.parse(output)
|
|
222
222
|
|
|
223
223
|
expect(result.passed).toBe(true)
|
|
@@ -226,7 +226,7 @@ export function App() {
|
|
|
226
226
|
|
|
227
227
|
// ─── empty directory ────────────────────────────────────────────────────
|
|
228
228
|
|
|
229
|
-
it(
|
|
229
|
+
it('handles empty directory with no source files', async () => {
|
|
230
230
|
const errorCount = await doctor(defaultOptions(tmpDir))
|
|
231
231
|
|
|
232
232
|
expect(errorCount).toBe(0)
|
|
@@ -234,21 +234,21 @@ export function App() {
|
|
|
234
234
|
|
|
235
235
|
// ─── multiple files ─────────────────────────────────────────────────────
|
|
236
236
|
|
|
237
|
-
it(
|
|
238
|
-
writeFile(tmpDir,
|
|
237
|
+
it('scans multiple files and aggregates results', async () => {
|
|
238
|
+
writeFile(tmpDir, 'src/A.tsx', REACT_TSX)
|
|
239
239
|
writeFile(
|
|
240
240
|
tmpDir,
|
|
241
|
-
|
|
241
|
+
'src/B.tsx',
|
|
242
242
|
`import { useState } from "react"
|
|
243
243
|
export function B() { const [x, setX] = useState(0); return <div>{x}</div> }
|
|
244
244
|
`,
|
|
245
245
|
)
|
|
246
|
-
writeFile(tmpDir,
|
|
246
|
+
writeFile(tmpDir, 'src/C.tsx', CLEAN_TSX)
|
|
247
247
|
|
|
248
248
|
const opts: DoctorOptions = { fix: false, json: true, ci: false, cwd: tmpDir }
|
|
249
249
|
await doctor(opts)
|
|
250
250
|
|
|
251
|
-
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join(
|
|
251
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('')
|
|
252
252
|
const result = JSON.parse(output)
|
|
253
253
|
|
|
254
254
|
expect(result.summary.filesScanned).toBe(3)
|