@askrjs/cli 0.0.15 → 0.0.16

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.
@@ -1,4 +1,3 @@
1
- import { For } from '@askrjs/askr/control';
2
1
  import { Link } from '@askrjs/askr/router';
3
2
  import {
4
3
  LayoutDashboardIcon,
@@ -68,31 +67,27 @@ export default function AppSidebar() {
68
67
  </NavBrand>
69
68
 
70
69
  <NavGroup id="workspace-nav-group" label="Workspace">
71
- <For each={primaryNav} by={(item) => item.href}>
72
- {(item) => {
73
- const Icon = item.icon;
74
- return (
75
- <NavLink href={item.href}>
76
- <Icon size={16} aria-hidden={true} />
77
- <span>{item.label}</span>
78
- </NavLink>
79
- );
80
- }}
81
- </For>
70
+ {primaryNav.map((item) => {
71
+ const Icon = item.icon;
72
+ return (
73
+ <NavLink href={item.href}>
74
+ <Icon size={16} aria-hidden={true} />
75
+ <span>{item.label}</span>
76
+ </NavLink>
77
+ );
78
+ })}
82
79
  </NavGroup>
83
80
 
84
81
  <NavGroup id="other-nav-group" label="Other" placement="bottom">
85
- <For each={secondaryNav} by={(item) => item.href}>
86
- {(item) => {
87
- const Icon = item.icon;
88
- return (
89
- <NavLink href={item.href}>
90
- <Icon size={16} aria-hidden={true} />
91
- <span>{item.label}</span>
92
- </NavLink>
93
- );
94
- }}
95
- </For>
82
+ {secondaryNav.map((item) => {
83
+ const Icon = item.icon;
84
+ return (
85
+ <NavLink href={item.href}>
86
+ <Icon size={16} aria-hidden={true} />
87
+ <span>{item.label}</span>
88
+ </NavLink>
89
+ );
90
+ })}
96
91
  </NavGroup>
97
92
  </Navbar>
98
93
  </aside>
@@ -1,4 +1,4 @@
1
- import { Case, For, Match } from '@askrjs/askr/control';
1
+ import { For } from '@askrjs/askr/control';
2
2
  import { Skeleton } from '@askrjs/themes/components';
3
3
  import EmptyState from './empty-state';
4
4
  import { joinClasses } from '../utils/join-classes';
@@ -22,26 +22,54 @@ export default function DataTable<Row>(props: {
22
22
  emptyTitle?: string;
23
23
  emptyDescription?: string;
24
24
  }) {
25
- const rows = props.rows();
26
- const table = (
25
+ if (props.errorText) {
26
+ return (
27
+ <EmptyState title="Could not load table" description={props.errorText} />
28
+ );
29
+ }
30
+
31
+ if (props.isLoading) {
32
+ return (
33
+ <div
34
+ class={joinClasses('panel stack-sm', props.class)}
35
+ aria-hidden="true"
36
+ >
37
+ <Skeleton class="skeleton-line" />
38
+ <Skeleton class="skeleton-line" />
39
+ <Skeleton class="skeleton-line" />
40
+ </div>
41
+ );
42
+ }
43
+
44
+ if (props.rows().length === 0) {
45
+ return (
46
+ <EmptyState
47
+ title={props.emptyTitle ?? 'No rows found'}
48
+ description={
49
+ props.emptyDescription ??
50
+ 'Try changing filters or adding new records.'
51
+ }
52
+ />
53
+ );
54
+ }
55
+
56
+ return (
27
57
  <div class={joinClasses('table-wrap', props.class)}>
28
58
  <table class={props.tableClass}>
29
59
  <thead>
30
60
  <tr>
31
- <For each={props.columns} by={(column) => column.key}>
32
- {(column) => <th class={column.class}>{column.header}</th>}
33
- </For>
61
+ {props.columns.map((column) => (
62
+ <th class={column.class}>{column.header}</th>
63
+ ))}
34
64
  </tr>
35
65
  </thead>
36
66
  <tbody>
37
67
  <For each={props.rows} by={props.rowKey}>
38
68
  {(row: Row) => (
39
69
  <tr class={props.rowClass?.(row)}>
40
- <For each={props.columns} by={(column) => column.key}>
41
- {(column) => (
42
- <td class={column.class}>{column.render(row)}</td>
43
- )}
44
- </For>
70
+ {props.columns.map((column) => (
71
+ <td class={column.class}>{column.render(row)}</td>
72
+ ))}
45
73
  </tr>
46
74
  )}
47
75
  </For>
@@ -49,34 +77,4 @@ export default function DataTable<Row>(props: {
49
77
  </table>
50
78
  </div>
51
79
  );
52
-
53
- return (
54
- <Case fallback={table}>
55
- <Match when={props.errorText}>
56
- <EmptyState
57
- title="Could not load table"
58
- description={props.errorText ?? 'The table could not be loaded.'}
59
- />
60
- </Match>
61
- <Match when={props.isLoading}>
62
- <div
63
- class={joinClasses('panel stack-sm', props.class)}
64
- aria-hidden="true"
65
- >
66
- <Skeleton class="skeleton-line" />
67
- <Skeleton class="skeleton-line" />
68
- <Skeleton class="skeleton-line" />
69
- </div>
70
- </Match>
71
- <Match when={rows.length === 0}>
72
- <EmptyState
73
- title={props.emptyTitle ?? 'No rows found'}
74
- description={
75
- props.emptyDescription ??
76
- 'Try changing filters or adding new records.'
77
- }
78
- />
79
- </Match>
80
- </Case>
81
- );
82
80
  }
@@ -1,5 +1,4 @@
1
1
  import { state } from '@askrjs/askr';
2
- import { For } from '@askrjs/askr/control';
3
2
  import { resource } from '@askrjs/askr/resources';
4
3
  import { Button } from '@askrjs/ui/button';
5
4
  import {
@@ -62,21 +61,19 @@ export default function DashboardPage() {
62
61
  />
63
62
 
64
63
  <div class="stat-grid">
65
- <For each={stats} by={(stat) => stat.key}>
66
- {(stat) => {
67
- const Icon =
68
- iconByStatKey[stat.key as keyof typeof iconByStatKey] ??
69
- BarChart3Icon;
70
- return (
71
- <StatCard
72
- label={stat.label}
73
- value={stat.value}
74
- trend={stat.trend}
75
- icon={Icon}
76
- />
77
- );
78
- }}
79
- </For>
64
+ {stats().map((stat) => {
65
+ const Icon =
66
+ iconByStatKey[stat.key as keyof typeof iconByStatKey] ??
67
+ BarChart3Icon;
68
+ return (
69
+ <StatCard
70
+ label={stat.label}
71
+ value={stat.value}
72
+ trend={stat.trend}
73
+ icon={Icon}
74
+ />
75
+ );
76
+ })}
80
77
  </div>
81
78
 
82
79
  <section class="panel stack-md">
package/dist/update.js CHANGED
@@ -217,7 +217,7 @@ async function runDependencyCli(command, args, io = console, runtime = {}) {
217
217
  let root = null;
218
218
  let selectedWorkspaces = [];
219
219
  try {
220
- const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-DUDrZCIC.js"), import("./planner-BEfWRd0x.js")]);
220
+ const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-DUDrZCIC.js"), import("./planner-CYi2Y-2W.js")]);
221
221
  const project = await discoverProject({
222
222
  cwd: parsed.cwd,
223
223
  packagePatterns: parsed.packagePatterns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {
@@ -36,7 +36,6 @@
36
36
  "clean": "npx rimraf dist node_modules",
37
37
  "build": "vp pack",
38
38
  "dev": "vp pack --watch",
39
- "analyze": "tsx src/bin/cli.ts analyze --check",
40
39
  "test": "vp test run -c vitest.config.ts",
41
40
  "test:coverage": "vp test run -c vitest.config.ts --coverage",
42
41
  "fmt": "vp fmt .",
@@ -48,7 +47,7 @@
48
47
  "bench": "npm run build --silent && npm run bench:analyze && node --import tsx benchmarks/cli.mjs --gate",
49
48
  "bench:analyze": "vp test bench --run -c vitest.bench.config.ts",
50
49
  "bench:json": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate --json",
51
- "check": "npm run analyze && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
50
+ "check": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
52
51
  "prepack": "npm run build",
53
52
  "prepublishOnly": "npm run check && npm run test:templates"
54
53
  },
@@ -57,7 +56,6 @@
57
56
  "js-yaml": "^5.2.1",
58
57
  "minimatch": "^10.2.5",
59
58
  "npm-registry-fetch": "^19.1.1",
60
- "playwright-core": "^1.62.0",
61
59
  "semver": "^7.8.5",
62
60
  "tsx": "^4.23.1",
63
61
  "typescript": "^6.0.3"
@@ -88,12 +86,5 @@
88
86
  },
89
87
  "engines": {
90
88
  "node": "^20.19.0 || >=22.12.0"
91
- },
92
- "askr": {
93
- "analyze": {
94
- "exclude": [
95
- "templates/**"
96
- ]
97
- }
98
89
  }
99
90
  }
@@ -1,35 +0,0 @@
1
- import { Browser } from "playwright-core";
2
- //#region src/bin/verify-hydration.d.ts
3
- type CliIo = Pick<Console, "error" | "log">;
4
- interface ParsedVerifyHydrationArgs {
5
- cwd: string;
6
- outputDir: string;
7
- routes: string[];
8
- rootSelector: string;
9
- buildScript: string;
10
- build: boolean;
11
- timeoutMs: number;
12
- browserChannel?: string;
13
- help: boolean;
14
- errors: string[];
15
- }
16
- interface RouteMetadata {
17
- path: string;
18
- filePath: string;
19
- status?: string;
20
- }
21
- interface StaticOutputServer {
22
- origin: string;
23
- close(): Promise<void>;
24
- }
25
- interface VerifyHydrationDeps {
26
- runBuild?: (cwd: string, script: string) => Promise<void>;
27
- launchBrowser?: (channel?: string) => Promise<Browser>;
28
- startServer?: (outputDir: string, routes: readonly RouteMetadata[]) => Promise<StaticOutputServer>;
29
- }
30
- declare function parseVerifyHydrationArgs(args: string[], defaultCwd?: string): ParsedVerifyHydrationArgs;
31
- declare function startStaticOutputServer(outputDir: string, routes: readonly RouteMetadata[]): Promise<StaticOutputServer>;
32
- declare function verifyHydrationRoutes(browser: Browser, origin: string, routes: readonly RouteMetadata[], rootSelector: string, timeoutMs: number): Promise<string[]>;
33
- declare function runVerifyHydrationCli(args?: string[], deps?: VerifyHydrationDeps, io?: CliIo): Promise<number>;
34
- //#endregion
35
- export { parseVerifyHydrationArgs, runVerifyHydrationCli, startStaticOutputServer, verifyHydrationRoutes };
@@ -1,325 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { spawn } from "node:child_process";
6
- import http from "node:http";
7
- //#region src/bin/verify-hydration.ts
8
- const helpText = `
9
- askr verify-hydration - Verify SSG DOM structure in a real browser
10
-
11
- Usage:
12
- askr verify-hydration [--output <dir>] [--route <path> ...]
13
-
14
- Options:
15
- --cwd <dir> Project directory (default: current directory)
16
- --output <dir> Generated SSG output (default: dist)
17
- --route <path> Route to verify; repeat to select a route set
18
- --root <selector> Hydrated application root (default: #app)
19
- --build-script <name> npm script that builds SSG output (default: build)
20
- --no-build Verify existing output without running a build
21
- --timeout <ms> Per-route browser timeout (default: 10000)
22
- --browser-channel <id> Browser channel: chrome, msedge, or playwright
23
- --help Show this help message
24
-
25
- When --route is omitted, routes are read from <output>/metadata.json.
26
- `;
27
- function parsePositiveInteger(value, option, errors) {
28
- const parsed = Number(value);
29
- if (!Number.isSafeInteger(parsed) || parsed < 1) {
30
- errors.push(`${option} must be a positive integer`);
31
- return;
32
- }
33
- return parsed;
34
- }
35
- function parseVerifyHydrationArgs(args, defaultCwd = process.cwd()) {
36
- const parsed = {
37
- cwd: defaultCwd,
38
- outputDir: "dist",
39
- routes: [],
40
- rootSelector: "#app",
41
- buildScript: "build",
42
- build: true,
43
- timeoutMs: 1e4,
44
- help: false,
45
- errors: []
46
- };
47
- const takeValue = (index, option) => {
48
- const value = args[index + 1];
49
- if (!value || value.startsWith("-")) {
50
- parsed.errors.push(`Missing value for ${option}`);
51
- return;
52
- }
53
- return value;
54
- };
55
- for (let index = 0; index < args.length; index += 1) {
56
- const argument = args[index];
57
- if (argument === "--cwd" || argument === "--output" || argument === "--route" || argument === "--root" || argument === "--build-script" || argument === "--timeout" || argument === "--browser-channel") {
58
- const value = takeValue(index, argument);
59
- if (!value) continue;
60
- index += 1;
61
- if (argument === "--cwd") parsed.cwd = value;
62
- else if (argument === "--output") parsed.outputDir = value;
63
- else if (argument === "--route") parsed.routes.push(value);
64
- else if (argument === "--root") parsed.rootSelector = value;
65
- else if (argument === "--build-script") parsed.buildScript = value;
66
- else if (argument === "--browser-channel") parsed.browserChannel = value;
67
- else {
68
- const timeout = parsePositiveInteger(value, "--timeout", parsed.errors);
69
- if (timeout) parsed.timeoutMs = timeout;
70
- }
71
- } else if (argument === "--no-build") parsed.build = false;
72
- else if (argument === "--help" || argument === "-h") parsed.help = true;
73
- else parsed.errors.push(`Unknown option: ${argument}`);
74
- }
75
- parsed.cwd = path.resolve(defaultCwd, parsed.cwd);
76
- parsed.outputDir = path.resolve(parsed.cwd, parsed.outputDir);
77
- return parsed;
78
- }
79
- function normalizeRoute(route) {
80
- const pathname = new URL(route, "http://askr.local").pathname;
81
- return pathname === "/" ? pathname : pathname.replace(/\/+$/, "");
82
- }
83
- async function readRouteMetadata(outputDir, selectedRoutes) {
84
- const metadataPath = path.join(outputDir, "metadata.json");
85
- let metadata;
86
- try {
87
- metadata = JSON.parse(await fs.readFile(metadataPath, "utf8"));
88
- } catch (error) {
89
- const detail = error instanceof Error ? error.message : String(error);
90
- throw new Error(`Could not read SSG route metadata at ${metadataPath}: ${detail}`);
91
- }
92
- const routes = metadata.routes;
93
- if (!Array.isArray(routes)) throw new Error(`Invalid SSG route metadata at ${metadataPath}: routes must be an array.`);
94
- const valid = routes.filter((entry) => Boolean(entry && typeof entry === "object" && typeof entry.path === "string" && typeof entry.filePath === "string" && entry.status !== "error" && entry.status !== "removed"));
95
- const byPath = new Map(valid.map((entry) => [normalizeRoute(entry.path), entry]));
96
- if (selectedRoutes.length === 0) return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
97
- return [...new Set(selectedRoutes.map(normalizeRoute))].map((route) => {
98
- const entry = byPath.get(route);
99
- if (!entry) throw new Error(`Route ${route} is not present in ${metadataPath}.`);
100
- return entry;
101
- });
102
- }
103
- function contentType(filePath) {
104
- const extension = path.extname(filePath).toLowerCase();
105
- if (extension === ".html") return "text/html; charset=utf-8";
106
- if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8";
107
- if (extension === ".css") return "text/css; charset=utf-8";
108
- if (extension === ".json") return "application/json; charset=utf-8";
109
- if (extension === ".svg") return "image/svg+xml";
110
- if (extension === ".png") return "image/png";
111
- if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
112
- if (extension === ".webp") return "image/webp";
113
- return "application/octet-stream";
114
- }
115
- function closeServer(server) {
116
- return new Promise((resolve, reject) => {
117
- server.close((error) => error ? reject(error) : resolve());
118
- });
119
- }
120
- async function startStaticOutputServer(outputDir, routes) {
121
- const outputRoot = path.resolve(outputDir);
122
- const routeFiles = new Map(routes.map((entry) => [normalizeRoute(entry.path), path.resolve(outputRoot, entry.filePath)]));
123
- for (const [route, filePath] of routeFiles) {
124
- const relative = path.relative(outputRoot, filePath);
125
- if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Route ${route} resolves outside the SSG output directory.`);
126
- }
127
- const server = http.createServer(async (request, response) => {
128
- try {
129
- const pathname = normalizeRoute(new URL(request.url ?? "/", "http://askr.local").pathname);
130
- const candidate = routeFiles.get(pathname) ?? path.resolve(outputRoot, `.${pathname}`);
131
- const relative = path.relative(outputRoot, candidate);
132
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
133
- response.writeHead(404).end("Not found");
134
- return;
135
- }
136
- const filePath = (await fs.stat(candidate).catch(() => null))?.isDirectory() ? path.join(candidate, "index.html") : candidate;
137
- const content = await fs.readFile(filePath);
138
- response.writeHead(200, {
139
- "cache-control": "no-store",
140
- "content-type": contentType(filePath)
141
- });
142
- response.end(content);
143
- } catch {
144
- response.writeHead(404).end("Not found");
145
- }
146
- });
147
- await new Promise((resolve, reject) => {
148
- server.once("error", reject);
149
- server.listen(0, "127.0.0.1", resolve);
150
- });
151
- const address = server.address();
152
- if (!address || typeof address === "string") {
153
- await closeServer(server);
154
- throw new Error("Hydration verification server did not bind a TCP port.");
155
- }
156
- return {
157
- origin: `http://127.0.0.1:${address.port}`,
158
- close: () => closeServer(server)
159
- };
160
- }
161
- async function runNpmBuild(cwd, script) {
162
- await new Promise((resolve, reject) => {
163
- const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", script], {
164
- cwd,
165
- stdio: "inherit"
166
- });
167
- const timer = setTimeout(() => {
168
- child.kill("SIGTERM");
169
- reject(/* @__PURE__ */ new Error(`npm run ${script} timed out after 300000ms.`));
170
- }, 3e5);
171
- child.once("error", (error) => {
172
- clearTimeout(timer);
173
- reject(error);
174
- });
175
- child.once("exit", (code, signal) => {
176
- clearTimeout(timer);
177
- if (code === 0) resolve();
178
- else reject(/* @__PURE__ */ new Error(`npm run ${script} failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}.`));
179
- });
180
- });
181
- }
182
- async function launchChromium(channel) {
183
- const { chromium } = await import("playwright-core");
184
- const requested = channel ?? process.env.ASKR_BROWSER_CHANNEL ?? "chrome";
185
- try {
186
- return requested === "playwright" ? await chromium.launch({ headless: true }) : await chromium.launch({
187
- channel: requested,
188
- headless: true
189
- });
190
- } catch (error) {
191
- const detail = error instanceof Error ? error.message : String(error);
192
- throw new Error(`Could not launch the ${requested} browser channel. Install Chrome, pass --browser-channel msedge, or run "npx playwright-core install chromium" and pass --browser-channel playwright. ${detail}`);
193
- }
194
- }
195
- async function snapshotRoot(page, selector) {
196
- return page.evaluate((rootSelector) => {
197
- const root = globalThis.document.querySelector(rootSelector);
198
- if (!root) throw new Error(`Hydration root not found: ${rootSelector}`);
199
- const lines = [];
200
- const pending = [{
201
- node: root,
202
- path: rootSelector
203
- }];
204
- while (pending.length > 0) {
205
- const current = pending.pop();
206
- if (!current) break;
207
- const { node, path: nodePath } = current;
208
- if (node.nodeType !== 1) continue;
209
- if (node.matches("script, style, link, meta, noscript, template")) continue;
210
- lines.push(`${nodePath} <${node.tagName.toLowerCase()}>`);
211
- const children = [...node.childNodes].filter((child) => child.nodeType === 1 && !child.matches("script, style, link, meta, noscript, template"));
212
- for (let index = children.length - 1; index >= 0; index -= 1) pending.push({
213
- node: children[index],
214
- path: `${nodePath}/${index}`
215
- });
216
- }
217
- return { lines };
218
- }, selector);
219
- }
220
- function firstDifference(expected, actual) {
221
- const length = Math.max(expected.lines.length, actual.lines.length);
222
- for (let index = 0; index < length; index += 1) if (expected.lines[index] !== actual.lines[index]) return {
223
- index,
224
- expected: expected.lines[index] ?? "<missing>",
225
- actual: actual.lines[index] ?? "<missing>"
226
- };
227
- return null;
228
- }
229
- async function loadSnapshot(context, url, selector, timeoutMs, settleHydration) {
230
- const page = await context.newPage();
231
- const errors = [];
232
- page.on("pageerror", (error) => errors.push(error.message));
233
- page.on("console", (message) => {
234
- if (message.type() === "error") errors.push(`console: ${message.text()}`);
235
- });
236
- try {
237
- page.setDefaultTimeout(timeoutMs);
238
- const response = await page.goto(url, {
239
- waitUntil: "load",
240
- timeout: timeoutMs
241
- });
242
- if (!response?.ok()) throw new Error(`HTTP ${response?.status() ?? "failure"} loading ${url}`);
243
- if (settleHydration) {
244
- let timer;
245
- try {
246
- await Promise.race([page.evaluate(() => {
247
- const animationFrame = globalThis.requestAnimationFrame;
248
- return new Promise((resolve) => animationFrame(() => animationFrame(() => resolve())));
249
- }), new Promise((_resolve, reject) => {
250
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Hydration timeout: did not settle within ${timeoutMs}ms.`)), timeoutMs);
251
- })]);
252
- } finally {
253
- if (timer) clearTimeout(timer);
254
- }
255
- }
256
- return {
257
- snapshot: await snapshotRoot(page, selector),
258
- errors
259
- };
260
- } finally {
261
- await page.close();
262
- }
263
- }
264
- async function verifyHydrationRoutes(browser, origin, routes, rootSelector, timeoutMs) {
265
- const failures = [];
266
- const staticContext = await browser.newContext({ javaScriptEnabled: false });
267
- const hydratedContext = await browser.newContext({ javaScriptEnabled: true });
268
- try {
269
- for (const route of routes) {
270
- const url = `${origin}${normalizeRoute(route.path)}`;
271
- try {
272
- const expected = await loadSnapshot(staticContext, url, rootSelector, timeoutMs, false);
273
- const actual = await loadSnapshot(hydratedContext, url, rootSelector, timeoutMs, true);
274
- const difference = firstDifference(expected.snapshot, actual.snapshot);
275
- if (difference) failures.push(`${route.path}: DOM diverged at normalized entry ${difference.index}\n static: ${difference.expected}\n hydrated: ${difference.actual}`);
276
- for (const error of actual.errors) failures.push(`${route.path}: browser error: ${error}`);
277
- } catch (error) {
278
- const detail = error instanceof Error ? error.message : String(error);
279
- failures.push(`${route.path}: ${detail}`);
280
- }
281
- }
282
- } finally {
283
- await Promise.all([staticContext.close(), hydratedContext.close()]);
284
- }
285
- return failures;
286
- }
287
- async function runVerifyHydrationCli(args = process.argv.slice(2), deps = {}, io = console) {
288
- const parsed = parseVerifyHydrationArgs(args);
289
- if (parsed.help) {
290
- io.log(helpText);
291
- return 0;
292
- }
293
- if (parsed.errors.length > 0) {
294
- for (const error of parsed.errors) io.error(`Error: ${error}`);
295
- return 1;
296
- }
297
- let server;
298
- let browser;
299
- try {
300
- if (parsed.build) await (deps.runBuild ?? runNpmBuild)(parsed.cwd, parsed.buildScript);
301
- const routes = await readRouteMetadata(parsed.outputDir, parsed.routes);
302
- if (routes.length === 0) throw new Error("SSG metadata contains no successful routes to verify.");
303
- server = await (deps.startServer ?? startStaticOutputServer)(parsed.outputDir, routes);
304
- browser = await (deps.launchBrowser ?? launchChromium)(parsed.browserChannel);
305
- const failures = await verifyHydrationRoutes(browser, server.origin, routes, parsed.rootSelector, parsed.timeoutMs);
306
- if (failures.length > 0) {
307
- for (const failure of failures) io.error(`Hydration verification failed: ${failure}`);
308
- return 1;
309
- }
310
- io.log(`Verified hydration DOM for ${routes.length} route(s).`);
311
- return 0;
312
- } catch (error) {
313
- io.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
314
- return 1;
315
- } finally {
316
- await browser?.close().catch(() => void 0);
317
- await server?.close().catch(() => void 0);
318
- }
319
- }
320
- async function main() {
321
- process.exit(await runVerifyHydrationCli());
322
- }
323
- if (isDirectExecution(import.meta.url)) main();
324
- //#endregion
325
- export { parseVerifyHydrationArgs, runVerifyHydrationCli, startStaticOutputServer, verifyHydrationRoutes };