svelte-5-doctor 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/dist/cli.d.ts +2 -0
- package/dist/cli.js +194 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/package.json +49 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Svelte Doctor CLI — ported from react-doctor-source/packages/react-doctor/src/cli/index.ts
|
|
4
|
+
* Mirrors React Doctor CLI surface: [directory] [options], --json, --verbose, --category, rules, why, ci
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { runInspect } from "svelte-5-doctor-core";
|
|
12
|
+
import { SVELTE_DOCTOR_RULES, RULE_MAP } from "svelte-5-doctor-core";
|
|
13
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name("svelte-5-doctor")
|
|
17
|
+
.description("Svelte 5 Doctor — 0-100 health check for Svelte 5 codebases. Ported from React Doctor. (svelte-doctor alias kept for compat)")
|
|
18
|
+
.version(pkg.version, "-v, --version", "output the version number")
|
|
19
|
+
.argument("[directory]", "directory to scan", ".")
|
|
20
|
+
.option("--json", "output JSON report")
|
|
21
|
+
.option("--json-out <path>", "write JSON to file")
|
|
22
|
+
.option("--verbose", "verbose diagnostics")
|
|
23
|
+
.option("--category <category>", "filter by category (repeatable)", (v, prev) => [...prev, v], [])
|
|
24
|
+
.option("--score", "output score only")
|
|
25
|
+
.option("--no-score", "disable scoring output")
|
|
26
|
+
.option("--diff", "alias for --scope changed (deprecated)", false)
|
|
27
|
+
.option("--scope <scope>", "scope: full | changed | files", "full")
|
|
28
|
+
.option("--base <branch>", "diff base for changed scope", "main")
|
|
29
|
+
.option("--no-color", "disable color")
|
|
30
|
+
.action(async (directory, opts) => {
|
|
31
|
+
const dir = resolve(process.cwd(), directory);
|
|
32
|
+
const categories = opts.category ?? [];
|
|
33
|
+
// deprecated --diff alias
|
|
34
|
+
const scope = opts.diff ? "changed" : opts.scope;
|
|
35
|
+
const started = Date.now();
|
|
36
|
+
const report = await runInspect({ directory: dir, categories: categories.length ? categories : undefined, scope });
|
|
37
|
+
if (opts.score && !opts.json) {
|
|
38
|
+
console.log(String(report.score));
|
|
39
|
+
process.exit(report.diagnostics.some((d) => d.severity === "error") ? 1 : 0);
|
|
40
|
+
}
|
|
41
|
+
if (opts.json) {
|
|
42
|
+
const out = JSON.stringify(report, null, 2);
|
|
43
|
+
if (opts.jsonOut) {
|
|
44
|
+
const { writeFileSync } = await import("node:fs");
|
|
45
|
+
writeFileSync(resolve(process.cwd(), opts.jsonOut), out, "utf-8");
|
|
46
|
+
console.log(pc.green(`JSON written to ${opts.jsonOut}`));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(out);
|
|
50
|
+
}
|
|
51
|
+
const hasErrors = report.diagnostics.some((d) => d.severity === "error");
|
|
52
|
+
process.exit(hasErrors ? 1 : 0);
|
|
53
|
+
}
|
|
54
|
+
// human output — mirrors react-doctor score header
|
|
55
|
+
const labelColor = report.label === "Great" ? pc.green : report.label === "Needs work" ? pc.yellow : pc.red;
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(` ${pc.bold("Svelte Doctor")} ${pc.dim(`v${pkg.version}`)} ${labelColor(`Score: ${report.score} (${report.label})`)}`);
|
|
58
|
+
console.log(` ${pc.dim(`${report.summary.total} findings — ${report.summary.errors} errors, ${report.summary.warnings} warnings — ${report.meta.durationMs}ms`)}`);
|
|
59
|
+
console.log(` ${pc.dim(`Svelte ${report.meta.svelteVersion} · ${report.meta.directory}`)}`);
|
|
60
|
+
console.log("");
|
|
61
|
+
if (report.diagnostics.length === 0) {
|
|
62
|
+
console.log(pc.green(" ✓ No issues found. Your Svelte 5 codebase is healthy!"));
|
|
63
|
+
console.log("");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
// group by category like react-doctor
|
|
67
|
+
const byCat = {};
|
|
68
|
+
for (const d of report.diagnostics)
|
|
69
|
+
(byCat[d.category] ??= []).push(d);
|
|
70
|
+
const order = ["Security", "Correctness", "Performance", "Accessibility", "Maintainability", "Architecture"];
|
|
71
|
+
const cats = order.filter((c) => byCat[c]?.length);
|
|
72
|
+
for (const cat of cats) {
|
|
73
|
+
const list = byCat[cat] ?? [];
|
|
74
|
+
const icon = cat === "Security" ? "🔒" : cat === "Correctness" ? "🐛" : cat === "Performance" ? "⚡" : cat === "Accessibility" ? "♿" : "🏗️";
|
|
75
|
+
console.log(` ${icon} ${pc.bold(cat)} — ${list.length}`);
|
|
76
|
+
for (const d of list.slice(0, opts.verbose ? 100 : 10)) {
|
|
77
|
+
const sev = d.severity === "error" ? pc.red("error") : pc.yellow("warn");
|
|
78
|
+
const loc = pc.dim(`${d.filePath}:${d.line}:${d.column}`);
|
|
79
|
+
console.log(` ${sev} ${pc.bold(d.ruleId)} ${d.message}`);
|
|
80
|
+
console.log(` ${loc}`);
|
|
81
|
+
if (opts.verbose && d.fix)
|
|
82
|
+
console.log(` ${pc.cyan("fix:")} ${d.fix}`);
|
|
83
|
+
}
|
|
84
|
+
if (!opts.verbose && list.length > 10)
|
|
85
|
+
console.log(pc.dim(` ... and ${list.length - 10} more (use --verbose)`));
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
88
|
+
if (report.skippedCheckReasons?.length) {
|
|
89
|
+
console.log(pc.dim(` skipped: ${report.skippedCheckReasons.length} checks`));
|
|
90
|
+
}
|
|
91
|
+
console.log(pc.dim(` Run ${pc.bold("npx svelte-5-doctor --json --json-out report.json")} for machine-readable output.`));
|
|
92
|
+
console.log(pc.dim(` Run ${pc.bold("npx svelte-5-doctor rules list")} to see all rules.`));
|
|
93
|
+
console.log("");
|
|
94
|
+
const hasErrors = report.diagnostics.some((d) => d.severity === "error");
|
|
95
|
+
process.exit(hasErrors ? 1 : 0);
|
|
96
|
+
});
|
|
97
|
+
program
|
|
98
|
+
.command("rules")
|
|
99
|
+
.description("list and explain rules (ported from react-doctor rules)")
|
|
100
|
+
.argument("[subcommand]", "list | explain", "list")
|
|
101
|
+
.argument("[ruleId]", "rule id for explain")
|
|
102
|
+
.option("--category <cat>", "filter by category")
|
|
103
|
+
.option("--json", "json output")
|
|
104
|
+
.action((sub, ruleId, opts, cmd) => {
|
|
105
|
+
const parentOpts = program.opts();
|
|
106
|
+
// commander v14: subcommand args handling tricky; fallback
|
|
107
|
+
const subCmd = sub ?? "list";
|
|
108
|
+
const targetId = ruleId;
|
|
109
|
+
if (subCmd === "explain" && targetId) {
|
|
110
|
+
const rule = RULE_MAP.get(targetId) ?? RULE_MAP.get(`svelte-doctor/${targetId}`) ?? RULE_MAP.get(`svelte-5-doctor/${targetId}`);
|
|
111
|
+
if (!rule) {
|
|
112
|
+
console.error(pc.red(`Unknown rule: ${targetId}`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
console.log(`${pc.bold(rule.id)} [${rule.category}] ${rule.severity}`);
|
|
116
|
+
console.log(rule.description);
|
|
117
|
+
if (rule.tags?.length)
|
|
118
|
+
console.log(pc.dim(`tags: ${rule.tags.join(", ")}`));
|
|
119
|
+
if (rule.fix)
|
|
120
|
+
console.log(pc.cyan(`fix: ${rule.fix}`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let rules = [...SVELTE_DOCTOR_RULES];
|
|
124
|
+
if (opts.category)
|
|
125
|
+
rules = rules.filter((r) => r.category.toLowerCase() === String(opts.category).toLowerCase());
|
|
126
|
+
if (opts.json) {
|
|
127
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.log(pc.bold(`Svelte Doctor — ${rules.length} rules (ported from React Doctor's 287)`));
|
|
131
|
+
console.log(pc.dim("Categories: Security · Correctness · Performance · Accessibility · Maintainability"));
|
|
132
|
+
console.log("");
|
|
133
|
+
for (const r of rules) {
|
|
134
|
+
const sev = r.severity === "error" ? pc.red(r.severity) : r.severity === "warn" ? pc.yellow(r.severity) : pc.dim(r.severity);
|
|
135
|
+
console.log(` ${sev.padEnd(10)} ${pc.bold(r.id)} ${pc.dim(`[${r.category}]`)} ${r.description}`);
|
|
136
|
+
}
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(pc.dim(`Run ${pc.bold("npx svelte-5-doctor rules explain <ruleId>")} for details.`));
|
|
139
|
+
});
|
|
140
|
+
program
|
|
141
|
+
.command("why")
|
|
142
|
+
.description("explain why a diagnostic was reported at a location")
|
|
143
|
+
.argument("<location>", "file:line e.g. src/App.svelte:42")
|
|
144
|
+
.action(async (location) => {
|
|
145
|
+
const [file, lineStr] = location.split(":");
|
|
146
|
+
const line = Number.parseInt(lineStr ?? "1", 10);
|
|
147
|
+
const dir = process.cwd();
|
|
148
|
+
const report = await runInspect({ directory: dir });
|
|
149
|
+
const matches = report.diagnostics.filter((d) => d.filePath.endsWith(file ?? "") && Math.abs(d.line - line) <= 2);
|
|
150
|
+
if (!matches.length) {
|
|
151
|
+
console.log(pc.yellow(`No diagnostics near ${location}`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const d of matches) {
|
|
155
|
+
console.log(`${pc.bold(d.ruleId)} [${d.category}] ${d.severity} at ${d.filePath}:${d.line}:${d.column}`);
|
|
156
|
+
console.log(` ${d.message}`);
|
|
157
|
+
if (d.fix)
|
|
158
|
+
console.log(pc.cyan(` fix: ${d.fix}`));
|
|
159
|
+
const rule = RULE_MAP.get(d.ruleId);
|
|
160
|
+
if (rule)
|
|
161
|
+
console.log(pc.dim(` ${rule.description}`));
|
|
162
|
+
console.log("");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
program
|
|
166
|
+
.command("ci")
|
|
167
|
+
.description("CI helpers (ported from react-doctor ci)")
|
|
168
|
+
.argument("[sub]", "install | config")
|
|
169
|
+
.action((sub) => {
|
|
170
|
+
if (sub === "install" || !sub) {
|
|
171
|
+
console.log(pc.bold("Svelte Doctor CI install"));
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log("Add to .github/workflows/svelte-doctor.yml:");
|
|
174
|
+
console.log(pc.cyan(`
|
|
175
|
+
name: Svelte Doctor
|
|
176
|
+
on:
|
|
177
|
+
pull_request: [opened, synchronize, reopened]
|
|
178
|
+
push:
|
|
179
|
+
branches: [main]
|
|
180
|
+
jobs:
|
|
181
|
+
svelte-doctor:
|
|
182
|
+
runs-on: ubuntu-latest
|
|
183
|
+
steps:
|
|
184
|
+
- uses: actions/checkout@v4
|
|
185
|
+
- uses: pnpm/action-setup@v4
|
|
186
|
+
- run: pnpm install
|
|
187
|
+
- run: npx svelte-doctor --scope changed --base main
|
|
188
|
+
`));
|
|
189
|
+
console.log(pc.dim("See action.yml for full GitHub Action inputs (blocking, scope, comment)."));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log("Unknown ci subcommand:", sub);
|
|
193
|
+
});
|
|
194
|
+
program.parseAsync(process.argv);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svelte-5-doctor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Svelte 5 Doctor — 0-100 health check for Svelte 5. Ported from React Doctor (millionco/react-doctor). Renamed from svelte-doctor (taken on npm by pimatis/svelte-doctor).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"svelte-5-doctor": "./dist/cli.js",
|
|
8
|
+
"svelte-doctor": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^14.0.3",
|
|
21
|
+
"picocolors": "^1.1.1",
|
|
22
|
+
"tinyglobby": "^0.2.12",
|
|
23
|
+
"svelte-5-doctor-core": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"svelte": "^5.35.0",
|
|
27
|
+
"tsx": "^4.22.4",
|
|
28
|
+
"typescript": "^5.9.2",
|
|
29
|
+
"vitest": "^3.2.4"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.19.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"svelte",
|
|
36
|
+
"svelte5",
|
|
37
|
+
"linter",
|
|
38
|
+
"doctor",
|
|
39
|
+
"security",
|
|
40
|
+
"performance",
|
|
41
|
+
"a11y"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
|
+
"dev": "tsx src/cli.ts --help",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
}
|
|
49
|
+
}
|