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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public API — ported from react-doctor-source/packages/react-doctor/src/index.ts
3
+ * Exposes programmatic inspect() for Svelte Doctor.
4
+ */
5
+ export { runInspect } from "svelte-5-doctor-core";
6
+ export type { JsonReport, Diagnostic } from "svelte-5-doctor-core";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public API — ported from react-doctor-source/packages/react-doctor/src/index.ts
3
+ * Exposes programmatic inspect() for Svelte Doctor.
4
+ */
5
+ export { runInspect } from "svelte-5-doctor-core";
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
+ }