@hydration-audit/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/index.mjs +240 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @hydration-audit/cli
|
|
2
|
+
|
|
3
|
+
> CLI tool for analyzing JavaScript hydration costs in island-architecture frameworks.
|
|
4
|
+
|
|
5
|
+
Part of the [Hydration Cost Visibility Platform](../../README.md).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @hydration-audit/cli
|
|
11
|
+
# or use directly with npx
|
|
12
|
+
npx @hydration-audit/cli analyze
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### `hydration-audit analyze [dir]`
|
|
18
|
+
|
|
19
|
+
Analyze hydration costs in a built project.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Analyze current directory
|
|
23
|
+
hydration-audit analyze
|
|
24
|
+
|
|
25
|
+
# Analyze a specific build output
|
|
26
|
+
hydration-audit analyze ./dist
|
|
27
|
+
|
|
28
|
+
# CI mode — exit code 1 on errors, GitHub annotations
|
|
29
|
+
hydration-audit analyze --ci
|
|
30
|
+
|
|
31
|
+
# JSON output for tooling
|
|
32
|
+
hydration-audit analyze --json
|
|
33
|
+
|
|
34
|
+
# Watch mode — re-analyze on file changes
|
|
35
|
+
hydration-audit analyze --watch
|
|
36
|
+
|
|
37
|
+
# Custom per-island budget
|
|
38
|
+
hydration-audit analyze --budget 30000
|
|
39
|
+
|
|
40
|
+
# Verbose logging
|
|
41
|
+
hydration-audit analyze --verbose
|
|
42
|
+
|
|
43
|
+
# Custom config file
|
|
44
|
+
hydration-audit analyze --config ./my-config.ts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Flags:**
|
|
48
|
+
|
|
49
|
+
| Flag | Description |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `--json` | Output JSON report to stdout (suppresses terminal table) |
|
|
52
|
+
| `--ci` | CI mode: exit code 1 on errors, GitHub Actions annotations |
|
|
53
|
+
| `--watch` | Watch for changes and re-analyze |
|
|
54
|
+
| `--no-terminal` | Suppress terminal output |
|
|
55
|
+
| `--verbose` | Enable debug logging |
|
|
56
|
+
| `--config <path>` | Path to config file |
|
|
57
|
+
| `--budget <bytes>` | Per-island gzip budget in bytes |
|
|
58
|
+
|
|
59
|
+
### `hydration-audit init`
|
|
60
|
+
|
|
61
|
+
Scaffold a `.hydration-audit.config.ts` configuration file.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
hydration-audit init
|
|
65
|
+
# Creates .hydration-audit.config.ts in the current directory
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `hydration-audit dashboard [dir]`
|
|
69
|
+
|
|
70
|
+
Launch the interactive web dashboard.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
hydration-audit dashboard
|
|
74
|
+
hydration-audit dashboard --port 3000
|
|
75
|
+
hydration-audit dashboard --report ./custom-report.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Flags:**
|
|
79
|
+
|
|
80
|
+
| Flag | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `--port <port>` | Port number (default: 4173) |
|
|
83
|
+
| `--report <path>` | Path to report JSON file |
|
|
84
|
+
|
|
85
|
+
## CI/CD Integration
|
|
86
|
+
|
|
87
|
+
### GitHub Actions
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
- name: Check hydration costs
|
|
91
|
+
run: npx @hydration-audit/cli analyze ./dist --ci
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
When `--ci` is used:
|
|
95
|
+
- **Exit code 1** if issues match the configured `failOn` severity
|
|
96
|
+
- **GitHub annotations** appear on PR files (`::error file=...,line=...::message`)
|
|
97
|
+
- **Step summary** table written to `$GITHUB_STEP_SUMMARY`
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { analyze, resolveConfig } from "@hydration-audit/core";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
var program = new Command();
|
|
9
|
+
program.name("hydration-audit").description("Analyze JavaScript hydration costs in island-architecture frameworks").version("0.1.0");
|
|
10
|
+
program.command("analyze").description("Analyze hydration costs in a built project").argument("[dir]", "Build output directory", ".").option("--json", "Output JSON report only").option("--ci", "CI mode \u2014 exit with code 1 on errors").option("--no-terminal", "Suppress terminal output").option("--verbose", "Enable verbose logging").option("--watch", "Watch for changes and re-analyze").option("--config <path>", "Path to config file").option("--budget <bytes>", "Per-island gzip budget in bytes", parseInt).action(async (dir, opts) => {
|
|
11
|
+
try {
|
|
12
|
+
const cwd = path.resolve(dir);
|
|
13
|
+
const configOverrides = {};
|
|
14
|
+
if (opts.json) {
|
|
15
|
+
configOverrides.output = { terminal: false, json: ".hydration-audit-report.json", dashboard: false };
|
|
16
|
+
}
|
|
17
|
+
if (opts.noTerminal) {
|
|
18
|
+
configOverrides.output = { ...configOverrides.output, terminal: false, json: ".hydration-audit-report.json", dashboard: false };
|
|
19
|
+
}
|
|
20
|
+
if (opts.budget) {
|
|
21
|
+
configOverrides.thresholds = { islandBudget: opts.budget };
|
|
22
|
+
}
|
|
23
|
+
const report = await analyze({
|
|
24
|
+
config: configOverrides,
|
|
25
|
+
configPath: opts.config,
|
|
26
|
+
cwd,
|
|
27
|
+
verbose: opts.verbose
|
|
28
|
+
});
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
console.log(JSON.stringify(report, null, 2));
|
|
31
|
+
}
|
|
32
|
+
if (opts.ci) {
|
|
33
|
+
const config = resolveConfig(configOverrides, cwd);
|
|
34
|
+
const failOn = config.ci.failOn;
|
|
35
|
+
if (failOn === "none") {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
const hasErrors = report.totals.issuesBySeverity.error > 0;
|
|
39
|
+
const hasWarnings = report.totals.issuesBySeverity.warning > 0;
|
|
40
|
+
if (failOn === "error" && hasErrors) {
|
|
41
|
+
if (process.env.CI) {
|
|
42
|
+
emitGitHubAnnotations(report);
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (failOn === "warning" && (hasErrors || hasWarnings)) {
|
|
47
|
+
if (process.env.CI) {
|
|
48
|
+
emitGitHubAnnotations(report);
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (opts.watch) {
|
|
54
|
+
console.log("\nWatching for changes in", cwd, "...\n");
|
|
55
|
+
const outDir = path.resolve(cwd, "dist");
|
|
56
|
+
let debounceTimer = null;
|
|
57
|
+
fs.watch(outDir, { recursive: true }, (eventType, filename) => {
|
|
58
|
+
if (!filename?.match(/\.(js|mjs|html)$/)) return;
|
|
59
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
60
|
+
debounceTimer = setTimeout(async () => {
|
|
61
|
+
console.log(`
|
|
62
|
+
Change detected: ${filename}. Re-analyzing...
|
|
63
|
+
`);
|
|
64
|
+
try {
|
|
65
|
+
await analyze({
|
|
66
|
+
config: configOverrides,
|
|
67
|
+
configPath: opts.config,
|
|
68
|
+
cwd,
|
|
69
|
+
verbose: opts.verbose
|
|
70
|
+
});
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`Re-analysis failed: ${e.message}`);
|
|
73
|
+
}
|
|
74
|
+
}, 500);
|
|
75
|
+
});
|
|
76
|
+
process.stdin.resume();
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`Error: ${err.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
program.command("init").description("Create a hydration-audit config file").action(async () => {
|
|
84
|
+
const configPath = path.resolve(".hydration-audit.config.ts");
|
|
85
|
+
if (fs.existsSync(configPath)) {
|
|
86
|
+
console.log("Config file already exists:", configPath);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const template = `import { defineConfig } from '@hydration-audit/core';
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
framework: 'auto',
|
|
93
|
+
thresholds: {
|
|
94
|
+
islandBudget: 50_000,
|
|
95
|
+
islandBudgetError: 100_000,
|
|
96
|
+
pageBudget: 150_000,
|
|
97
|
+
pageBudgetError: 300_000,
|
|
98
|
+
totalBudget: 500_000,
|
|
99
|
+
},
|
|
100
|
+
rules: {
|
|
101
|
+
'oversized-island': 'error',
|
|
102
|
+
'eager-below-fold': 'warn',
|
|
103
|
+
'duplicate-framework': 'warn',
|
|
104
|
+
'budget-exceeded': 'error',
|
|
105
|
+
},
|
|
106
|
+
belowFold: ['Footer', 'Comments', 'Newsletter'],
|
|
107
|
+
exclude: [],
|
|
108
|
+
output: {
|
|
109
|
+
json: '.hydration-audit-report.json',
|
|
110
|
+
terminal: true,
|
|
111
|
+
dashboard: false,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
`;
|
|
115
|
+
fs.writeFileSync(configPath, template);
|
|
116
|
+
console.log("Created config file:", configPath);
|
|
117
|
+
});
|
|
118
|
+
program.command("dashboard").description("Launch the hydration tax dashboard").argument("[dir]", "Project directory", ".").option("--port <port>", "Port number", "4173").option("--report <path>", "Path to report JSON file").action(async (dir, opts) => {
|
|
119
|
+
const cwd = path.resolve(dir);
|
|
120
|
+
const reportPath = opts.report ? path.resolve(opts.report) : path.resolve(cwd, ".hydration-audit-report.json");
|
|
121
|
+
if (!fs.existsSync(reportPath)) {
|
|
122
|
+
console.error(
|
|
123
|
+
`Report file not found: ${reportPath}
|
|
124
|
+
Run "hydration-audit analyze" first to generate the report.`
|
|
125
|
+
);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const { startDashboard } = await import("@hydration-audit/dashboard");
|
|
130
|
+
const { url } = await startDashboard(reportPath, parseInt(opts.port));
|
|
131
|
+
console.log(`Dashboard running at ${url}`);
|
|
132
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
133
|
+
process.stdin.resume();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Failed to start dashboard: ${err.message}`);
|
|
136
|
+
console.error("Make sure @hydration-audit/dashboard is installed.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
program.command("history").description("Show hydration cost history over time").argument("[dir]", "Project directory", ".").option("--limit <n>", "Number of entries to show", "10").action(async (dir, opts) => {
|
|
141
|
+
const cwd = path.resolve(dir);
|
|
142
|
+
const { readHistory } = await import("@hydration-audit/core");
|
|
143
|
+
const history = readHistory(cwd);
|
|
144
|
+
if (!history || history.entries.length === 0) {
|
|
145
|
+
console.log('No history data found. Run "hydration-audit analyze" at least once.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const limit = parseInt(opts.limit) || 10;
|
|
149
|
+
const entries = history.entries.slice(0, limit);
|
|
150
|
+
console.log(`
|
|
151
|
+
Hydration Tax History \u2014 ${history.projectName}
|
|
152
|
+
`);
|
|
153
|
+
console.log(" Date Islands Gzip Size Issues Commit");
|
|
154
|
+
console.log(" " + "\u2500".repeat(70));
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const date = new Date(entry.timestamp).toLocaleDateString("en-US", {
|
|
157
|
+
month: "short",
|
|
158
|
+
day: "2-digit",
|
|
159
|
+
year: "numeric",
|
|
160
|
+
hour: "2-digit",
|
|
161
|
+
minute: "2-digit"
|
|
162
|
+
});
|
|
163
|
+
const commit = entry.commitHash ? entry.commitHash.slice(0, 8) : "--------";
|
|
164
|
+
const issues = (entry.issuesBySeverity.error ?? 0) + (entry.issuesBySeverity.warning ?? 0);
|
|
165
|
+
console.log(
|
|
166
|
+
` ${date.padEnd(22)} ${String(entry.totalIslands).padStart(4)} ${formatBytes(entry.totalGzipSize).padStart(10)} ${String(issues).padStart(6)} ${commit}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
console.log();
|
|
170
|
+
});
|
|
171
|
+
program.command("lighthouse").description("Run Lighthouse and correlate with hydration costs").argument("<url>", "URL to test").option("--report <path>", "Path to hydration-audit report JSON").option("--runs <n>", "Number of Lighthouse runs to average", "1").option("--desktop", "Use desktop emulation instead of mobile").action(async (url, opts) => {
|
|
172
|
+
const {
|
|
173
|
+
runLighthouse,
|
|
174
|
+
correlateLighthouseWithReport,
|
|
175
|
+
formatLighthouseReport
|
|
176
|
+
} = await import("@hydration-audit/core");
|
|
177
|
+
const reportPath = opts.report ? path.resolve(opts.report) : path.resolve(".hydration-audit-report.json");
|
|
178
|
+
console.log(`
|
|
179
|
+
Running Lighthouse on ${url}...
|
|
180
|
+
`);
|
|
181
|
+
try {
|
|
182
|
+
const metrics = await runLighthouse({
|
|
183
|
+
url,
|
|
184
|
+
runs: parseInt(opts.runs) || 1,
|
|
185
|
+
emulatedFormFactor: opts.desktop ? "desktop" : "mobile"
|
|
186
|
+
});
|
|
187
|
+
if (fs.existsSync(reportPath)) {
|
|
188
|
+
const reportContent = fs.readFileSync(reportPath, "utf-8");
|
|
189
|
+
const report = JSON.parse(reportContent);
|
|
190
|
+
const lhReport = correlateLighthouseWithReport(metrics, report, url);
|
|
191
|
+
console.log(formatLighthouseReport(lhReport));
|
|
192
|
+
} else {
|
|
193
|
+
console.log(` Performance Score: ${metrics.performanceScore}/100`);
|
|
194
|
+
console.log(` FCP: ${metrics.fcp.toFixed(0)}ms LCP: ${metrics.lcp.toFixed(0)}ms`);
|
|
195
|
+
console.log(` TBT: ${metrics.tbt.toFixed(0)}ms TTI: ${metrics.tti.toFixed(0)}ms`);
|
|
196
|
+
console.log(` CLS: ${metrics.cls.toFixed(3)} SI: ${metrics.speedIndex.toFixed(0)}ms`);
|
|
197
|
+
console.log(`
|
|
198
|
+
Note: No hydration-audit report found at ${reportPath}`);
|
|
199
|
+
console.log(' Run "hydration-audit analyze" first for island correlation.\n');
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`Lighthouse failed: ${err.message}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
program.parse();
|
|
207
|
+
function emitGitHubAnnotations(report) {
|
|
208
|
+
for (const island of report.islands) {
|
|
209
|
+
for (const issue of island.issues) {
|
|
210
|
+
const level = issue.severity === "error" ? "error" : "warning";
|
|
211
|
+
const file = island.component.sourceFile;
|
|
212
|
+
const line = island.component.sourceLine ?? 1;
|
|
213
|
+
console.log(
|
|
214
|
+
`::${level} file=${file},line=${line}::${issue.message}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
219
|
+
const summary = [
|
|
220
|
+
"## Hydration Tax Report",
|
|
221
|
+
"",
|
|
222
|
+
`| Metric | Value |`,
|
|
223
|
+
`| --- | --- |`,
|
|
224
|
+
`| Islands | ${report.totals.totalIslands} |`,
|
|
225
|
+
`| Total JS (gzip) | ${formatBytes(report.totals.totalGzipSize)} |`,
|
|
226
|
+
`| Errors | ${report.totals.issuesBySeverity.error} |`,
|
|
227
|
+
`| Warnings | ${report.totals.issuesBySeverity.warning} |`,
|
|
228
|
+
""
|
|
229
|
+
].join("\n");
|
|
230
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function formatBytes(bytes) {
|
|
234
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
235
|
+
const kb = bytes / 1024;
|
|
236
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
237
|
+
const mb = kb / 1024;
|
|
238
|
+
return `${mb.toFixed(2)} MB`;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { analyze, resolveConfig, loadConfig } from '@hydration-audit/core';\nimport type { UserConfig } from '@hydration-audit/core';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst program = new Command();\n\nprogram\n .name('hydration-audit')\n .description('Analyze JavaScript hydration costs in island-architecture frameworks')\n .version('0.1.0');\n\n// ─── analyze command ─────────────────────────────────────────────\n\nprogram\n .command('analyze')\n .description('Analyze hydration costs in a built project')\n .argument('[dir]', 'Build output directory', '.')\n .option('--json', 'Output JSON report only')\n .option('--ci', 'CI mode — exit with code 1 on errors')\n .option('--no-terminal', 'Suppress terminal output')\n .option('--verbose', 'Enable verbose logging')\n .option('--watch', 'Watch for changes and re-analyze')\n .option('--config <path>', 'Path to config file')\n .option('--budget <bytes>', 'Per-island gzip budget in bytes', parseInt)\n .action(async (dir: string, opts: any) => {\n try {\n const cwd = path.resolve(dir);\n\n const configOverrides: UserConfig = {};\n\n if (opts.json) {\n configOverrides.output = { terminal: false, json: '.hydration-audit-report.json', dashboard: false };\n }\n\n if (opts.noTerminal) {\n configOverrides.output = { ...configOverrides.output, terminal: false, json: '.hydration-audit-report.json', dashboard: false };\n }\n\n if (opts.budget) {\n configOverrides.thresholds = { islandBudget: opts.budget };\n }\n\n const report = await analyze({\n config: configOverrides,\n configPath: opts.config,\n cwd,\n verbose: opts.verbose,\n });\n\n // In JSON mode, print the report to stdout\n if (opts.json) {\n console.log(JSON.stringify(report, null, 2));\n }\n\n // In CI mode, exit with code 1 if there are errors\n if (opts.ci) {\n const config = resolveConfig(configOverrides, cwd);\n const failOn = config.ci.failOn;\n\n if (failOn === 'none') {\n process.exit(0);\n }\n\n const hasErrors = report.totals.issuesBySeverity.error > 0;\n const hasWarnings = report.totals.issuesBySeverity.warning > 0;\n\n if (failOn === 'error' && hasErrors) {\n // Emit GitHub Actions annotations if in CI\n if (process.env.CI) {\n emitGitHubAnnotations(report);\n }\n process.exit(1);\n }\n\n if (failOn === 'warning' && (hasErrors || hasWarnings)) {\n if (process.env.CI) {\n emitGitHubAnnotations(report);\n }\n process.exit(1);\n }\n }\n // Watch mode — re-analyze when build output changes\n if (opts.watch) {\n console.log('\\nWatching for changes in', cwd, '...\\n');\n const outDir = path.resolve(cwd, 'dist');\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n fs.watch(outDir, { recursive: true }, (eventType, filename) => {\n if (!filename?.match(/\\.(js|mjs|html)$/)) return;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(async () => {\n console.log(`\\nChange detected: ${filename}. Re-analyzing...\\n`);\n try {\n await analyze({\n config: configOverrides,\n configPath: opts.config,\n cwd,\n verbose: opts.verbose,\n });\n } catch (e) {\n console.error(`Re-analysis failed: ${(e as Error).message}`);\n }\n }, 500);\n });\n\n // Keep process alive\n process.stdin.resume();\n }\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n });\n\n// ─── init command ────────────────────────────────────────────────\n\nprogram\n .command('init')\n .description('Create a hydration-audit config file')\n .action(async () => {\n const configPath = path.resolve('.hydration-audit.config.ts');\n\n if (fs.existsSync(configPath)) {\n console.log('Config file already exists:', configPath);\n return;\n }\n\n const template = `import { defineConfig } from '@hydration-audit/core';\n\nexport default defineConfig({\n framework: 'auto',\n thresholds: {\n islandBudget: 50_000,\n islandBudgetError: 100_000,\n pageBudget: 150_000,\n pageBudgetError: 300_000,\n totalBudget: 500_000,\n },\n rules: {\n 'oversized-island': 'error',\n 'eager-below-fold': 'warn',\n 'duplicate-framework': 'warn',\n 'budget-exceeded': 'error',\n },\n belowFold: ['Footer', 'Comments', 'Newsletter'],\n exclude: [],\n output: {\n json: '.hydration-audit-report.json',\n terminal: true,\n dashboard: false,\n },\n});\n`;\n\n fs.writeFileSync(configPath, template);\n console.log('Created config file:', configPath);\n });\n\n// ─── dashboard command ───────────────────────────────────────────\n\nprogram\n .command('dashboard')\n .description('Launch the hydration tax dashboard')\n .argument('[dir]', 'Project directory', '.')\n .option('--port <port>', 'Port number', '4173')\n .option('--report <path>', 'Path to report JSON file')\n .action(async (dir: string, opts: any) => {\n const cwd = path.resolve(dir);\n const reportPath = opts.report\n ? path.resolve(opts.report)\n : path.resolve(cwd, '.hydration-audit-report.json');\n\n if (!fs.existsSync(reportPath)) {\n console.error(\n `Report file not found: ${reportPath}\\n` +\n `Run \"hydration-audit analyze\" first to generate the report.`,\n );\n process.exit(1);\n }\n\n try {\n const { startDashboard } = await import('@hydration-audit/dashboard');\n const { url } = await startDashboard(reportPath, parseInt(opts.port));\n console.log(`Dashboard running at ${url}`);\n console.log('Press Ctrl+C to stop.\\n');\n\n // Keep process alive\n process.stdin.resume();\n } catch (err) {\n console.error(`Failed to start dashboard: ${(err as Error).message}`);\n console.error('Make sure @hydration-audit/dashboard is installed.');\n process.exit(1);\n }\n });\n\n// ─── history command ─────────────────────────────────────────────\n\nprogram\n .command('history')\n .description('Show hydration cost history over time')\n .argument('[dir]', 'Project directory', '.')\n .option('--limit <n>', 'Number of entries to show', '10')\n .action(async (dir: string, opts: any) => {\n const cwd = path.resolve(dir);\n const { readHistory } = await import('@hydration-audit/core');\n\n const history = readHistory(cwd);\n if (!history || history.entries.length === 0) {\n console.log('No history data found. Run \"hydration-audit analyze\" at least once.');\n return;\n }\n\n const limit = parseInt(opts.limit) || 10;\n const entries = history.entries.slice(0, limit);\n\n console.log(`\\n Hydration Tax History — ${history.projectName}\\n`);\n console.log(' Date Islands Gzip Size Issues Commit');\n console.log(' ' + '─'.repeat(70));\n\n for (const entry of entries) {\n const date = new Date(entry.timestamp).toLocaleDateString('en-US', {\n month: 'short', day: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit',\n });\n const commit = entry.commitHash ? entry.commitHash.slice(0, 8) : '--------';\n const issues = (entry.issuesBySeverity.error ?? 0) + (entry.issuesBySeverity.warning ?? 0);\n console.log(\n ` ${date.padEnd(22)} ${String(entry.totalIslands).padStart(4)} ${formatBytes(entry.totalGzipSize).padStart(10)} ${String(issues).padStart(6)} ${commit}`,\n );\n }\n\n console.log();\n });\n\n// ─── lighthouse command ──────────────────────────────────────────\n\nprogram\n .command('lighthouse')\n .description('Run Lighthouse and correlate with hydration costs')\n .argument('<url>', 'URL to test')\n .option('--report <path>', 'Path to hydration-audit report JSON')\n .option('--runs <n>', 'Number of Lighthouse runs to average', '1')\n .option('--desktop', 'Use desktop emulation instead of mobile')\n .action(async (url: string, opts: any) => {\n const {\n runLighthouse,\n correlateLighthouseWithReport,\n formatLighthouseReport,\n } = await import('@hydration-audit/core');\n\n const reportPath = opts.report\n ? path.resolve(opts.report)\n : path.resolve('.hydration-audit-report.json');\n\n console.log(`\\n Running Lighthouse on ${url}...\\n`);\n\n try {\n const metrics = await runLighthouse({\n url,\n runs: parseInt(opts.runs) || 1,\n emulatedFormFactor: opts.desktop ? 'desktop' : 'mobile',\n });\n\n // Try to correlate with hydration report\n if (fs.existsSync(reportPath)) {\n const reportContent = fs.readFileSync(reportPath, 'utf-8');\n const report = JSON.parse(reportContent);\n const lhReport = correlateLighthouseWithReport(metrics, report, url);\n console.log(formatLighthouseReport(lhReport));\n } else {\n // Just print raw Lighthouse metrics\n console.log(` Performance Score: ${metrics.performanceScore}/100`);\n console.log(` FCP: ${metrics.fcp.toFixed(0)}ms LCP: ${metrics.lcp.toFixed(0)}ms`);\n console.log(` TBT: ${metrics.tbt.toFixed(0)}ms TTI: ${metrics.tti.toFixed(0)}ms`);\n console.log(` CLS: ${metrics.cls.toFixed(3)} SI: ${metrics.speedIndex.toFixed(0)}ms`);\n console.log(`\\n Note: No hydration-audit report found at ${reportPath}`);\n console.log(' Run \"hydration-audit analyze\" first for island correlation.\\n');\n }\n } catch (err) {\n console.error(`Lighthouse failed: ${(err as Error).message}`);\n process.exit(1);\n }\n });\n\n// ─── Parse and run ───────────────────────────────────────────────\n\nprogram.parse();\n\n// ─── Helpers ─────────────────────────────────────────────────────\n\nfunction emitGitHubAnnotations(report: any): void {\n for (const island of report.islands) {\n for (const issue of island.issues) {\n const level = issue.severity === 'error' ? 'error' : 'warning';\n const file = island.component.sourceFile;\n const line = island.component.sourceLine ?? 1;\n console.log(\n `::${level} file=${file},line=${line}::${issue.message}`,\n );\n }\n }\n\n // Write GitHub step summary\n if (process.env.GITHUB_STEP_SUMMARY) {\n const summary = [\n '## Hydration Tax Report',\n '',\n `| Metric | Value |`,\n `| --- | --- |`,\n `| Islands | ${report.totals.totalIslands} |`,\n `| Total JS (gzip) | ${formatBytes(report.totals.totalGzipSize)} |`,\n `| Errors | ${report.totals.issuesBySeverity.error} |`,\n `| Warnings | ${report.totals.issuesBySeverity.warning} |`,\n '',\n ].join('\\n');\n\n fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);\n }\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n return `${mb.toFixed(2)} MB`;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydration-audit/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for analyzing JavaScript hydration costs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hydration-audit": "./dist/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@hydration-audit/core": "workspace:*",
|
|
25
|
+
"@hydration-audit/dashboard": "workspace:*",
|
|
26
|
+
"commander": "^12.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"tsup": "^8.3.0",
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"vitest": "^2.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|