ssrwire 0.2.0 → 0.4.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/CHANGELOG.md +47 -1
- package/CONTRIBUTING.md +3 -1
- package/PUBLISHING.md +15 -15
- package/README.md +179 -29
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +162 -6
- package/dist/analyze.js.map +1 -1
- package/dist/audit-report.d.ts +8 -0
- package/dist/audit-report.d.ts.map +1 -0
- package/dist/audit-report.js +243 -0
- package/dist/audit-report.js.map +1 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +2 -0
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -10
- package/dist/cli.js.map +1 -1
- package/dist/compare.d.ts +6 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +720 -0
- package/dist/compare.js.map +1 -0
- package/dist/comparison-reporters.d.ts +9 -0
- package/dist/comparison-reporters.d.ts.map +1 -0
- package/dist/comparison-reporters.js +223 -0
- package/dist/comparison-reporters.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/http-probe.d.ts.map +1 -1
- package/dist/http-probe.js +4 -0
- package/dist/http-probe.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +6 -0
- package/dist/redact.js.map +1 -1
- package/dist/reporters.d.ts.map +1 -1
- package/dist/reporters.js +27 -0
- package/dist/reporters.js.map +1 -1
- package/dist/social.d.ts +14 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +88 -0
- package/dist/social.js.map +1 -0
- package/dist/stability.d.ts.map +1 -1
- package/dist/stability.js +84 -4
- package/dist/stability.js.map +1 -1
- package/dist/stream-parser.d.ts.map +1 -1
- package/dist/stream-parser.js +21 -1
- package/dist/stream-parser.js.map +1 -1
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -1
- package/examples/github-actions.yml +2 -2
- package/examples/ssrwire.config.yml +8 -2
- package/package.json +5 -2
- package/src/analyze.ts +208 -7
- package/src/audit-report.ts +269 -0
- package/src/audit.ts +2 -0
- package/src/cli.ts +106 -12
- package/src/compare.ts +949 -0
- package/src/comparison-reporters.ts +276 -0
- package/src/config.ts +19 -1
- package/src/http-probe.ts +4 -0
- package/src/index.ts +29 -0
- package/src/redact.ts +6 -0
- package/src/reporters.ts +46 -0
- package/src/social.ts +116 -0
- package/src/stability.ts +123 -4
- package/src/stream-parser.ts +31 -1
- package/src/types.ts +117 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { AuditResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const AUDIT_SCHEMA_VERSION = 1 as const;
|
|
5
|
+
|
|
6
|
+
const nonNegativeNumber = z.number().finite().nonnegative();
|
|
7
|
+
const nonNegativeInteger = z.number().int().nonnegative();
|
|
8
|
+
const elementLocation = z.enum(["head", "body", "document"]);
|
|
9
|
+
const severity = z.enum(["info", "warning", "error"]);
|
|
10
|
+
|
|
11
|
+
const timingMark = z
|
|
12
|
+
.object({
|
|
13
|
+
atMs: nonNegativeNumber,
|
|
14
|
+
observedByByte: nonNegativeInteger,
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
|
|
18
|
+
const elementSignal = timingMark
|
|
19
|
+
.extend({
|
|
20
|
+
value: z.string(),
|
|
21
|
+
location: elementLocation,
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
const robotsSignal = elementSignal
|
|
26
|
+
.extend({ audience: z.enum(["robots", "googlebot", "bingbot"]) })
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
const socialMetadataSignal = elementSignal
|
|
30
|
+
.extend({
|
|
31
|
+
property: z.enum([
|
|
32
|
+
"og:title",
|
|
33
|
+
"og:type",
|
|
34
|
+
"og:url",
|
|
35
|
+
"og:image",
|
|
36
|
+
"og:description",
|
|
37
|
+
"twitter:card",
|
|
38
|
+
"twitter:title",
|
|
39
|
+
"twitter:description",
|
|
40
|
+
"twitter:image",
|
|
41
|
+
]),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
|
|
45
|
+
const jsonLdSignal = timingMark
|
|
46
|
+
.extend({
|
|
47
|
+
location: elementLocation,
|
|
48
|
+
valid: z.boolean().optional(),
|
|
49
|
+
types: z.array(z.string()),
|
|
50
|
+
bytes: nonNegativeInteger,
|
|
51
|
+
analysisLimit: z.string().optional(),
|
|
52
|
+
error: z.string().optional(),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
|
|
56
|
+
const documentSignals = z
|
|
57
|
+
.object({
|
|
58
|
+
title: elementSignal.optional(),
|
|
59
|
+
titles: z.array(elementSignal).optional(),
|
|
60
|
+
descriptions: z.array(elementSignal),
|
|
61
|
+
canonicals: z.array(elementSignal),
|
|
62
|
+
robots: z.array(robotsSignal),
|
|
63
|
+
socialMetadata: z.array(socialMetadataSignal).optional(),
|
|
64
|
+
h1s: z.array(elementSignal),
|
|
65
|
+
firstMainText: elementSignal.optional(),
|
|
66
|
+
jsonLd: z.array(jsonLdSignal),
|
|
67
|
+
headClosed: timingMark.optional(),
|
|
68
|
+
bodyStarted: timingMark.optional(),
|
|
69
|
+
documentClosed: timingMark.optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict();
|
|
72
|
+
|
|
73
|
+
const agentProfile = z
|
|
74
|
+
.object({
|
|
75
|
+
key: z.string().min(1),
|
|
76
|
+
label: z.string().min(1),
|
|
77
|
+
userAgent: z.string(),
|
|
78
|
+
requiresHeadMetadata: z.boolean(),
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
|
|
82
|
+
const redirectHop = z
|
|
83
|
+
.object({
|
|
84
|
+
url: z.string(),
|
|
85
|
+
status: z.number().int().min(100).max(599),
|
|
86
|
+
location: z.string(),
|
|
87
|
+
durationMs: nonNegativeNumber,
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
|
|
91
|
+
const headerSnapshot = z
|
|
92
|
+
.object({
|
|
93
|
+
values: z.record(z.string(), z.string()),
|
|
94
|
+
setCookiePresent: z.boolean(),
|
|
95
|
+
})
|
|
96
|
+
.strict();
|
|
97
|
+
|
|
98
|
+
const probeTimings = z
|
|
99
|
+
.object({
|
|
100
|
+
headersMs: nonNegativeNumber,
|
|
101
|
+
firstByteMs: nonNegativeNumber.optional(),
|
|
102
|
+
completeMs: nonNegativeNumber.optional(),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
|
|
106
|
+
const probeResult = z
|
|
107
|
+
.object({
|
|
108
|
+
requestedUrl: z.string(),
|
|
109
|
+
finalUrl: z.string(),
|
|
110
|
+
agent: agentProfile,
|
|
111
|
+
status: z.number().int().min(100).max(599).optional(),
|
|
112
|
+
redirects: z.array(redirectHop),
|
|
113
|
+
headers: headerSnapshot,
|
|
114
|
+
timings: probeTimings,
|
|
115
|
+
bytesRead: nonNegativeInteger,
|
|
116
|
+
bodySha256: z.string().optional(),
|
|
117
|
+
signals: documentSignals,
|
|
118
|
+
completion: z.enum([
|
|
119
|
+
"complete",
|
|
120
|
+
"max-bytes-exceeded",
|
|
121
|
+
"timeout",
|
|
122
|
+
"network-error",
|
|
123
|
+
"invalid-response",
|
|
124
|
+
]),
|
|
125
|
+
error: z.string().optional(),
|
|
126
|
+
sample: z.number().int().positive().optional(),
|
|
127
|
+
})
|
|
128
|
+
.strict();
|
|
129
|
+
|
|
130
|
+
const targetExpectations = z
|
|
131
|
+
.object({
|
|
132
|
+
statuses: z.array(z.number().int().min(100).max(599)).min(1),
|
|
133
|
+
finalUrl: z.string().optional(),
|
|
134
|
+
requireTitle: z.boolean(),
|
|
135
|
+
requireDescription: z.boolean(),
|
|
136
|
+
requireCanonical: z.boolean(),
|
|
137
|
+
requireH1: z.boolean(),
|
|
138
|
+
requireMainText: z.boolean(),
|
|
139
|
+
requireOpenGraph: z.boolean().optional(),
|
|
140
|
+
requireTwitterCard: z.boolean().optional(),
|
|
141
|
+
maxFirstByteMs: z.number().positive().finite().optional(),
|
|
142
|
+
maxCriticalMs: z.number().positive().finite().optional(),
|
|
143
|
+
})
|
|
144
|
+
.strict();
|
|
145
|
+
|
|
146
|
+
const auditTarget = z
|
|
147
|
+
.object({
|
|
148
|
+
id: z.string().min(1).optional(),
|
|
149
|
+
url: z.string(),
|
|
150
|
+
expectations: targetExpectations,
|
|
151
|
+
})
|
|
152
|
+
.strict();
|
|
153
|
+
|
|
154
|
+
const timingStats = z
|
|
155
|
+
.object({
|
|
156
|
+
samples: nonNegativeInteger,
|
|
157
|
+
minMs: nonNegativeNumber,
|
|
158
|
+
medianMs: nonNegativeNumber,
|
|
159
|
+
p95Ms: nonNegativeNumber,
|
|
160
|
+
maxMs: nonNegativeNumber,
|
|
161
|
+
spreadMs: nonNegativeNumber,
|
|
162
|
+
})
|
|
163
|
+
.strict();
|
|
164
|
+
|
|
165
|
+
const stabilityTimings = z
|
|
166
|
+
.object({
|
|
167
|
+
headers: timingStats.optional(),
|
|
168
|
+
firstByte: timingStats.optional(),
|
|
169
|
+
criticalSignals: timingStats.optional(),
|
|
170
|
+
complete: timingStats.optional(),
|
|
171
|
+
})
|
|
172
|
+
.strict();
|
|
173
|
+
|
|
174
|
+
const stabilityVariants = z
|
|
175
|
+
.object({
|
|
176
|
+
completion: nonNegativeInteger,
|
|
177
|
+
status: nonNegativeInteger,
|
|
178
|
+
finalUrl: nonNegativeInteger,
|
|
179
|
+
redirectChain: nonNegativeInteger,
|
|
180
|
+
bodySha256: nonNegativeInteger,
|
|
181
|
+
metadataValues: nonNegativeInteger,
|
|
182
|
+
metadataLocations: nonNegativeInteger,
|
|
183
|
+
})
|
|
184
|
+
.strict();
|
|
185
|
+
|
|
186
|
+
const agentStability = z
|
|
187
|
+
.object({
|
|
188
|
+
agent: agentProfile,
|
|
189
|
+
samples: nonNegativeInteger,
|
|
190
|
+
complete: nonNegativeInteger,
|
|
191
|
+
incomplete: nonNegativeInteger,
|
|
192
|
+
timings: stabilityTimings,
|
|
193
|
+
variants: stabilityVariants,
|
|
194
|
+
})
|
|
195
|
+
.strict();
|
|
196
|
+
|
|
197
|
+
const finding = z
|
|
198
|
+
.object({
|
|
199
|
+
code: z.string().min(1),
|
|
200
|
+
severity,
|
|
201
|
+
message: z.string(),
|
|
202
|
+
url: z.string(),
|
|
203
|
+
agent: z.string().optional(),
|
|
204
|
+
evidence: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
205
|
+
})
|
|
206
|
+
.strict();
|
|
207
|
+
|
|
208
|
+
const targetAuditResult = z
|
|
209
|
+
.object({
|
|
210
|
+
target: auditTarget,
|
|
211
|
+
probes: z.array(probeResult),
|
|
212
|
+
findings: z.array(finding),
|
|
213
|
+
stability: z.array(agentStability).optional(),
|
|
214
|
+
})
|
|
215
|
+
.strict();
|
|
216
|
+
|
|
217
|
+
const auditSummary = z
|
|
218
|
+
.object({
|
|
219
|
+
targets: nonNegativeInteger,
|
|
220
|
+
probes: nonNegativeInteger,
|
|
221
|
+
errors: nonNegativeInteger,
|
|
222
|
+
warnings: nonNegativeInteger,
|
|
223
|
+
info: nonNegativeInteger,
|
|
224
|
+
incomplete: nonNegativeInteger,
|
|
225
|
+
})
|
|
226
|
+
.strict();
|
|
227
|
+
|
|
228
|
+
const auditReportSchema = z
|
|
229
|
+
.object({
|
|
230
|
+
schemaVersion: z.literal(AUDIT_SCHEMA_VERSION),
|
|
231
|
+
version: z.string().min(1),
|
|
232
|
+
generatedAt: z.string().min(1),
|
|
233
|
+
durationMs: nonNegativeNumber,
|
|
234
|
+
repeat: z.number().int().min(1).max(10).optional(),
|
|
235
|
+
results: z.array(targetAuditResult),
|
|
236
|
+
summary: auditSummary,
|
|
237
|
+
})
|
|
238
|
+
.strict();
|
|
239
|
+
|
|
240
|
+
export class AuditReportError extends Error {
|
|
241
|
+
public constructor(message: string) {
|
|
242
|
+
super(message);
|
|
243
|
+
this.name = "AuditReportError";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function parseAuditReport(value: unknown, source = "audit report"): AuditResult {
|
|
248
|
+
const parsed = auditReportSchema.safeParse(value);
|
|
249
|
+
if (!parsed.success) {
|
|
250
|
+
const detail = parsed.error.issues
|
|
251
|
+
.slice(0, 5)
|
|
252
|
+
.map((issue) => `${issue.path.join(".") || "report"}: ${issue.message}`)
|
|
253
|
+
.join("; ");
|
|
254
|
+
throw new AuditReportError(`Invalid ${source}: ${detail}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The complete runtime schema above is the persisted counterpart of AuditResult.
|
|
258
|
+
return parsed.data as AuditResult;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function parseAuditReportText(text: string, source = "audit report"): AuditResult {
|
|
262
|
+
let value: unknown;
|
|
263
|
+
try {
|
|
264
|
+
value = JSON.parse(text) as unknown;
|
|
265
|
+
} catch {
|
|
266
|
+
throw new AuditReportError(`Could not parse ${source} as JSON.`);
|
|
267
|
+
}
|
|
268
|
+
return parseAuditReport(value, source);
|
|
269
|
+
}
|
package/src/audit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { analyzeTarget, summarizeAudit } from "./analyze.js";
|
|
2
|
+
import { AUDIT_SCHEMA_VERSION } from "./audit-report.js";
|
|
2
3
|
import { probeUrl } from "./http-probe.js";
|
|
3
4
|
import { redactAudit } from "./redact.js";
|
|
4
5
|
import { analyzeStability } from "./stability.js";
|
|
@@ -186,6 +187,7 @@ export async function runAudit(config: SsrWireConfig): Promise<AuditResult> {
|
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
const audit: AuditResult = {
|
|
190
|
+
schemaVersion: AUDIT_SCHEMA_VERSION,
|
|
189
191
|
version: VERSION,
|
|
190
192
|
generatedAt: new Date().toISOString(),
|
|
191
193
|
durationMs: Math.round(performance.now() - started),
|
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
4
4
|
import { runAudit } from "./audit.js";
|
|
5
|
+
import { AuditReportError, parseAuditReportText } from "./audit-report.js";
|
|
6
|
+
import { compareAudits } from "./compare.js";
|
|
7
|
+
import { renderComparisonReport } from "./comparison-reporters.js";
|
|
5
8
|
import { ConfigError, loadConfig } from "./config.js";
|
|
6
9
|
import { renderReport } from "./reporters.js";
|
|
7
|
-
import type { ReportFormat } from "./types.js";
|
|
10
|
+
import type { AuditResult, ComparisonReportFormat, ReportFormat } from "./types.js";
|
|
8
11
|
import { VERSION } from "./version.js";
|
|
9
12
|
|
|
10
13
|
interface CliOptions {
|
|
@@ -21,9 +24,19 @@ interface CliOptions {
|
|
|
21
24
|
readonly color: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
interface CompareCliOptions {
|
|
28
|
+
readonly format: ComparisonReportFormat;
|
|
29
|
+
readonly output?: string;
|
|
30
|
+
readonly failOn: "regression" | "never";
|
|
31
|
+
readonly timingRegressionMs: number;
|
|
32
|
+
readonly timingRegressionPercent: number;
|
|
33
|
+
readonly color: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
const CONFIG_TEMPLATE = `# SSRWire configuration
|
|
25
37
|
targets:
|
|
26
|
-
-
|
|
38
|
+
- id: home
|
|
39
|
+
url: https://example.com/
|
|
27
40
|
expectedStatus: 200
|
|
28
41
|
require:
|
|
29
42
|
title: true
|
|
@@ -31,6 +44,8 @@ targets:
|
|
|
31
44
|
canonical: true
|
|
32
45
|
h1: true
|
|
33
46
|
mainText: true
|
|
47
|
+
openGraph: false
|
|
48
|
+
twitterCard: false
|
|
34
49
|
|
|
35
50
|
agents:
|
|
36
51
|
- browser
|
|
@@ -74,6 +89,24 @@ function parseFailOn(value: string): CliOptions["failOn"] {
|
|
|
74
89
|
throw new InvalidArgumentError("Expected error, warning, or never.");
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
function parseComparisonFormat(value: string): ComparisonReportFormat {
|
|
93
|
+
if (value === "terminal" || value === "json" || value === "html") return value;
|
|
94
|
+
throw new InvalidArgumentError("Expected terminal, json, or html.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseComparisonFailOn(value: string): CompareCliOptions["failOn"] {
|
|
98
|
+
if (value === "regression" || value === "never") return value;
|
|
99
|
+
throw new InvalidArgumentError("Expected regression or never.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseNonNegativeNumber(value: string): number {
|
|
103
|
+
const parsed = Number(value);
|
|
104
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
105
|
+
throw new InvalidArgumentError("Expected a finite non-negative number.");
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
function addCheckOptions(command: Command): Command {
|
|
78
111
|
return command
|
|
79
112
|
.option("-c, --config <path>", "configuration file")
|
|
@@ -118,6 +151,16 @@ async function writeReport(path: string, report: string): Promise<void> {
|
|
|
118
151
|
await writeFile(absolute, report, "utf8");
|
|
119
152
|
}
|
|
120
153
|
|
|
154
|
+
async function readAuditFile(path: string, label: string): Promise<AuditResult> {
|
|
155
|
+
let text: string;
|
|
156
|
+
try {
|
|
157
|
+
text = await readFile(resolve(path), "utf8");
|
|
158
|
+
} catch {
|
|
159
|
+
throw new AuditReportError(`Could not read ${label} audit report: ${path}`);
|
|
160
|
+
}
|
|
161
|
+
return parseAuditReportText(text, `${label} audit report`);
|
|
162
|
+
}
|
|
163
|
+
|
|
121
164
|
async function check(urls: readonly string[], options: CliOptions): Promise<void> {
|
|
122
165
|
const config = await loadConfig({
|
|
123
166
|
...(options.config ? { configPath: options.config } : {}),
|
|
@@ -147,6 +190,38 @@ async function check(urls: readonly string[], options: CliOptions): Promise<void
|
|
|
147
190
|
process.exitCode = reportExitCode(audit.summary, options.failOn);
|
|
148
191
|
}
|
|
149
192
|
|
|
193
|
+
async function compareReports(
|
|
194
|
+
baselinePath: string,
|
|
195
|
+
candidatePath: string,
|
|
196
|
+
options: CompareCliOptions,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const [baseline, candidate] = await Promise.all([
|
|
199
|
+
readAuditFile(baselinePath, "baseline"),
|
|
200
|
+
readAuditFile(candidatePath, "candidate"),
|
|
201
|
+
]);
|
|
202
|
+
const comparison = compareAudits(baseline, candidate, {
|
|
203
|
+
baselineLabel: basename(baselinePath),
|
|
204
|
+
candidateLabel: basename(candidatePath),
|
|
205
|
+
timingRegressionMs: options.timingRegressionMs,
|
|
206
|
+
timingRegressionPercent: options.timingRegressionPercent,
|
|
207
|
+
});
|
|
208
|
+
const color =
|
|
209
|
+
options.color &&
|
|
210
|
+
!options.output &&
|
|
211
|
+
Boolean(process.stdout.isTTY) &&
|
|
212
|
+
!Reflect.has(process.env, "NO_COLOR");
|
|
213
|
+
const report = renderComparisonReport(comparison, options.format, { color });
|
|
214
|
+
|
|
215
|
+
if (options.output) {
|
|
216
|
+
await writeReport(options.output, report);
|
|
217
|
+
process.stderr.write(`SSRWire wrote ${options.format} comparison to ${options.output}\n`);
|
|
218
|
+
} else {
|
|
219
|
+
process.stdout.write(report);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.exitCode = options.failOn === "regression" && comparison.summary.regressions > 0 ? 1 : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
150
225
|
async function initialize(path: string, force: boolean): Promise<void> {
|
|
151
226
|
const absolute = resolve(path);
|
|
152
227
|
if (!force) {
|
|
@@ -168,26 +243,45 @@ export async function main(argv: readonly string[] = process.argv): Promise<void
|
|
|
168
243
|
const program = new Command();
|
|
169
244
|
program
|
|
170
245
|
.name("ssrwire")
|
|
171
|
-
.description("Inspect streamed SSR HTML and
|
|
246
|
+
.description("Inspect streamed SSR HTML, SEO, and social metadata delivery.")
|
|
172
247
|
.version(VERSION)
|
|
173
248
|
.exitOverride()
|
|
174
249
|
.showHelpAfterError();
|
|
175
250
|
|
|
176
|
-
addCheckOptions(program)
|
|
177
|
-
.argument("[urls...]", "HTTP or HTTPS URLs to inspect")
|
|
178
|
-
.action(async (urls: string[], _options: CliOptions, command: Command) => {
|
|
179
|
-
await check(urls, optionsFrom(command));
|
|
180
|
-
});
|
|
181
|
-
|
|
182
251
|
addCheckOptions(
|
|
183
252
|
program
|
|
184
|
-
.command("check")
|
|
253
|
+
.command("check", { isDefault: true })
|
|
185
254
|
.description("inspect one or more SSR responses")
|
|
186
255
|
.argument("[urls...]", "HTTP or HTTPS URLs to inspect"),
|
|
187
256
|
).action(async (urls: string[], _options: CliOptions, command: Command) => {
|
|
188
257
|
await check(urls, optionsFrom(command));
|
|
189
258
|
});
|
|
190
259
|
|
|
260
|
+
program
|
|
261
|
+
.command("compare")
|
|
262
|
+
.description("compare two SSRWire JSON audit reports")
|
|
263
|
+
.argument("<baseline>", "baseline JSON audit report")
|
|
264
|
+
.argument("<candidate>", "candidate JSON audit report")
|
|
265
|
+
.option("-f, --format <format>", "terminal, json, or html", parseComparisonFormat, "terminal")
|
|
266
|
+
.option("-o, --output <path>", "write the comparison to a file")
|
|
267
|
+
.option("--fail-on <level>", "regression or never", parseComparisonFailOn, "regression")
|
|
268
|
+
.option(
|
|
269
|
+
"--timing-regression-ms <ms>",
|
|
270
|
+
"minimum absolute median slowdown",
|
|
271
|
+
parseNonNegativeNumber,
|
|
272
|
+
250,
|
|
273
|
+
)
|
|
274
|
+
.option(
|
|
275
|
+
"--timing-regression-percent <percent>",
|
|
276
|
+
"minimum relative median slowdown",
|
|
277
|
+
parseNonNegativeNumber,
|
|
278
|
+
25,
|
|
279
|
+
)
|
|
280
|
+
.option("--no-color", "disable terminal colors")
|
|
281
|
+
.action(async (baselinePath: string, candidatePath: string, options: CompareCliOptions) => {
|
|
282
|
+
await compareReports(baselinePath, candidatePath, options);
|
|
283
|
+
});
|
|
284
|
+
|
|
191
285
|
program
|
|
192
286
|
.command("init")
|
|
193
287
|
.description("create a documented starter configuration")
|