@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 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":";;;AAAA,SAAS,eAAe;AACxB,SAAS,SAAS,qBAAiC;AAEnD,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,iBAAiB,EACtB,YAAY,sEAAsE,EAClF,QAAQ,OAAO;AAIlB,QACG,QAAQ,SAAS,EACjB,YAAY,4CAA4C,EACxD,SAAS,SAAS,0BAA0B,GAAG,EAC/C,OAAO,UAAU,yBAAyB,EAC1C,OAAO,QAAQ,2CAAsC,EACrD,OAAO,iBAAiB,0BAA0B,EAClD,OAAO,aAAa,wBAAwB,EAC5C,OAAO,WAAW,kCAAkC,EACpD,OAAO,mBAAmB,qBAAqB,EAC/C,OAAO,oBAAoB,mCAAmC,QAAQ,EACtE,OAAO,OAAO,KAAa,SAAc;AACxC,MAAI;AACF,UAAM,MAAM,KAAK,QAAQ,GAAG;AAE5B,UAAM,kBAA8B,CAAC;AAErC,QAAI,KAAK,MAAM;AACb,sBAAgB,SAAS,EAAE,UAAU,OAAO,MAAM,gCAAgC,WAAW,MAAM;AAAA,IACrG;AAEA,QAAI,KAAK,YAAY;AACnB,sBAAgB,SAAS,EAAE,GAAG,gBAAgB,QAAQ,UAAU,OAAO,MAAM,gCAAgC,WAAW,MAAM;AAAA,IAChI;AAEA,QAAI,KAAK,QAAQ;AACf,sBAAgB,aAAa,EAAE,cAAc,KAAK,OAAO;AAAA,IAC3D;AAEA,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,QAAQ;AAAA,MACR,YAAY,KAAK;AAAA,MACjB;AAAA,MACA,SAAS,KAAK;AAAA,IAChB,CAAC;AAGD,QAAI,KAAK,MAAM;AACb,cAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC7C;AAGA,QAAI,KAAK,IAAI;AACX,YAAM,SAAS,cAAc,iBAAiB,GAAG;AACjD,YAAM,SAAS,OAAO,GAAG;AAEzB,UAAI,WAAW,QAAQ;AACrB,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAEA,YAAM,YAAY,OAAO,OAAO,iBAAiB,QAAQ;AACzD,YAAM,cAAc,OAAO,OAAO,iBAAiB,UAAU;AAE7D,UAAI,WAAW,WAAW,WAAW;AAEnC,YAAI,QAAQ,IAAI,IAAI;AAClB,gCAAsB,MAAM;AAAA,QAC9B;AACA,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAEA,UAAI,WAAW,cAAc,aAAa,cAAc;AACtD,YAAI,QAAQ,IAAI,IAAI;AAClB,gCAAsB,MAAM;AAAA,QAC9B;AACA,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,KAAK,OAAO;AACd,cAAQ,IAAI,6BAA6B,KAAK,OAAO;AACrD,YAAM,SAAS,KAAK,QAAQ,KAAK,MAAM;AACvC,UAAI,gBAAsD;AAE1D,SAAG,MAAM,QAAQ,EAAE,WAAW,KAAK,GAAG,CAAC,WAAW,aAAa;AAC7D,YAAI,CAAC,UAAU,MAAM,kBAAkB,EAAG;AAC1C,YAAI,cAAe,cAAa,aAAa;AAC7C,wBAAgB,WAAW,YAAY;AACrC,kBAAQ,IAAI;AAAA,mBAAsB,QAAQ;AAAA,CAAqB;AAC/D,cAAI;AACF,kBAAM,QAAQ;AAAA,cACZ,QAAQ;AAAA,cACR,YAAY,KAAK;AAAA,cACjB;AAAA,cACA,SAAS,KAAK;AAAA,YAChB,CAAC;AAAA,UACH,SAAS,GAAG;AACV,oBAAQ,MAAM,uBAAwB,EAAY,OAAO,EAAE;AAAA,UAC7D;AAAA,QACF,GAAG,GAAG;AAAA,MACR,CAAC;AAGD,cAAQ,MAAM,OAAO;AAAA,IACvB;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,UAAW,IAAc,OAAO,EAAE;AAChD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAIH,QACG,QAAQ,MAAM,EACd,YAAY,sCAAsC,EAClD,OAAO,YAAY;AAClB,QAAM,aAAa,KAAK,QAAQ,4BAA4B;AAE5D,MAAI,GAAG,WAAW,UAAU,GAAG;AAC7B,YAAQ,IAAI,+BAA+B,UAAU;AACrD;AAAA,EACF;AAEA,QAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BjB,KAAG,cAAc,YAAY,QAAQ;AACrC,UAAQ,IAAI,wBAAwB,UAAU;AAChD,CAAC;AAIH,QACG,QAAQ,WAAW,EACnB,YAAY,oCAAoC,EAChD,SAAS,SAAS,qBAAqB,GAAG,EAC1C,OAAO,iBAAiB,eAAe,MAAM,EAC7C,OAAO,mBAAmB,0BAA0B,EACpD,OAAO,OAAO,KAAa,SAAc;AACxC,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAM,aAAa,KAAK,SACpB,KAAK,QAAQ,KAAK,MAAM,IACxB,KAAK,QAAQ,KAAK,8BAA8B;AAEpD,MAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC9B,YAAQ;AAAA,MACN,0BAA0B,UAAU;AAAA;AAAA,IAEtC;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI;AACF,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,4BAA4B;AACpE,UAAM,EAAE,IAAI,IAAI,MAAM,eAAe,YAAY,SAAS,KAAK,IAAI,CAAC;AACpE,YAAQ,IAAI,wBAAwB,GAAG,EAAE;AACzC,YAAQ,IAAI,yBAAyB;AAGrC,YAAQ,MAAM,OAAO;AAAA,EACvB,SAAS,KAAK;AACZ,YAAQ,MAAM,8BAA+B,IAAc,OAAO,EAAE;AACpE,YAAQ,MAAM,oDAAoD;AAClE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAIH,QACG,QAAQ,SAAS,EACjB,YAAY,uCAAuC,EACnD,SAAS,SAAS,qBAAqB,GAAG,EAC1C,OAAO,eAAe,6BAA6B,IAAI,EACvD,OAAO,OAAO,KAAa,SAAc;AACxC,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,uBAAuB;AAE5D,QAAM,UAAU,YAAY,GAAG;AAC/B,MAAI,CAAC,WAAW,QAAQ,QAAQ,WAAW,GAAG;AAC5C,YAAQ,IAAI,qEAAqE;AACjF;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS,KAAK,KAAK,KAAK;AACtC,QAAM,UAAU,QAAQ,QAAQ,MAAM,GAAG,KAAK;AAE9C,UAAQ,IAAI;AAAA,iCAA+B,QAAQ,WAAW;AAAA,CAAI;AAClE,UAAQ,IAAI,8DAA8D;AAC1E,UAAQ,IAAI,OAAO,SAAI,OAAO,EAAE,CAAC;AAEjC,aAAW,SAAS,SAAS;AAC3B,UAAM,OAAO,IAAI,KAAK,MAAM,SAAS,EAAE,mBAAmB,SAAS;AAAA,MACjE,OAAO;AAAA,MAAS,KAAK;AAAA,MAAW,MAAM;AAAA,MAAW,MAAM;AAAA,MAAW,QAAQ;AAAA,IAC5E,CAAC;AACD,UAAM,SAAS,MAAM,aAAa,MAAM,WAAW,MAAM,GAAG,CAAC,IAAI;AACjE,UAAM,UAAU,MAAM,iBAAiB,SAAS,MAAM,MAAM,iBAAiB,WAAW;AACxF,YAAQ;AAAA,MACN,KAAK,KAAK,OAAO,EAAE,CAAC,IAAI,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,CAAC,OAAO,YAAY,MAAM,aAAa,EAAE,SAAS,EAAE,CAAC,KAAK,OAAO,MAAM,EAAE,SAAS,CAAC,CAAC,KAAK,MAAM;AAAA,IAC9J;AAAA,EACF;AAEA,UAAQ,IAAI;AACd,CAAC;AAIH,QACG,QAAQ,YAAY,EACpB,YAAY,mDAAmD,EAC/D,SAAS,SAAS,aAAa,EAC/B,OAAO,mBAAmB,qCAAqC,EAC/D,OAAO,cAAc,wCAAwC,GAAG,EAChE,OAAO,aAAa,yCAAyC,EAC7D,OAAO,OAAO,KAAa,SAAc;AACxC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,OAAO,uBAAuB;AAExC,QAAM,aAAa,KAAK,SACpB,KAAK,QAAQ,KAAK,MAAM,IACxB,KAAK,QAAQ,8BAA8B;AAE/C,UAAQ,IAAI;AAAA,0BAA6B,GAAG;AAAA,CAAO;AAEnD,MAAI;AACF,UAAM,UAAU,MAAM,cAAc;AAAA,MAClC;AAAA,MACA,MAAM,SAAS,KAAK,IAAI,KAAK;AAAA,MAC7B,oBAAoB,KAAK,UAAU,YAAY;AAAA,IACjD,CAAC;AAGD,QAAI,GAAG,WAAW,UAAU,GAAG;AAC7B,YAAM,gBAAgB,GAAG,aAAa,YAAY,OAAO;AACzD,YAAM,SAAS,KAAK,MAAM,aAAa;AACvC,YAAM,WAAW,8BAA8B,SAAS,QAAQ,GAAG;AACnE,cAAQ,IAAI,uBAAuB,QAAQ,CAAC;AAAA,IAC9C,OAAO;AAEL,cAAQ,IAAI,wBAAwB,QAAQ,gBAAgB,MAAM;AAClE,cAAQ,IAAI,WAAW,QAAQ,IAAI,QAAQ,CAAC,CAAC,cAAc,QAAQ,IAAI,QAAQ,CAAC,CAAC,IAAI;AACrF,cAAQ,IAAI,WAAW,QAAQ,IAAI,QAAQ,CAAC,CAAC,cAAc,QAAQ,IAAI,QAAQ,CAAC,CAAC,IAAI;AACrF,cAAQ,IAAI,WAAW,QAAQ,IAAI,QAAQ,CAAC,CAAC,aAAa,QAAQ,WAAW,QAAQ,CAAC,CAAC,IAAI;AAC3F,cAAQ,IAAI;AAAA,6CAAgD,UAAU,EAAE;AACxE,cAAQ,IAAI,iEAAiE;AAAA,IAC/E;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,sBAAuB,IAAc,OAAO,EAAE;AAC5D,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAIH,QAAQ,MAAM;AAId,SAAS,sBAAsB,QAAmB;AAChD,aAAW,UAAU,OAAO,SAAS;AACnC,eAAW,SAAS,OAAO,QAAQ;AACjC,YAAM,QAAQ,MAAM,aAAa,UAAU,UAAU;AACrD,YAAM,OAAO,OAAO,UAAU;AAC9B,YAAM,OAAO,OAAO,UAAU,cAAc;AAC5C,cAAQ;AAAA,QACN,KAAK,KAAK,SAAS,IAAI,SAAS,IAAI,KAAK,MAAM,OAAO;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAGA,MAAI,QAAQ,IAAI,qBAAqB;AACnC,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe,OAAO,OAAO,YAAY;AAAA,MACzC,uBAAuB,YAAY,OAAO,OAAO,aAAa,CAAC;AAAA,MAC/D,cAAc,OAAO,OAAO,iBAAiB,KAAK;AAAA,MAClD,gBAAgB,OAAO,OAAO,iBAAiB,OAAO;AAAA,MACtD;AAAA,IACF,EAAE,KAAK,IAAI;AAEX,OAAG,eAAe,QAAQ,IAAI,qBAAqB,OAAO;AAAA,EAC5D;AACF;AAEA,SAAS,YAAY,OAAuB;AAC1C,MAAI,QAAQ,KAAM,QAAO,GAAG,KAAK;AACjC,QAAM,KAAK,QAAQ;AACnB,MAAI,KAAK,KAAM,QAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AACtC,QAAM,KAAK,KAAK;AAChB,SAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AACzB;","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
+ }