ssrwire 0.3.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 +24 -1
- package/CONTRIBUTING.md +2 -1
- package/PUBLISHING.md +15 -15
- package/README.md +109 -6
- 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 +74 -9
- 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 +16 -1
- package/dist/config.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 +1 -0
- package/dist/redact.js.map +1 -1
- package/dist/types.d.ts +83 -1
- package/dist/types.d.ts.map +1 -1
- package/examples/github-actions.yml +2 -2
- package/examples/ssrwire.config.yml +4 -2
- package/package.json +1 -1
- package/src/audit-report.ts +269 -0
- package/src/audit.ts +2 -0
- package/src/cli.ts +103 -11
- package/src/compare.ts +949 -0
- package/src/comparison-reporters.ts +276 -0
- package/src/config.ts +15 -1
- package/src/index.ts +27 -0
- package/src/redact.ts +1 -0
- package/src/types.ts +97 -1
package/dist/types.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface DocumentSignals {
|
|
|
30
30
|
readonly descriptions: readonly ElementSignal[];
|
|
31
31
|
readonly canonicals: readonly ElementSignal[];
|
|
32
32
|
readonly robots: readonly RobotsSignal[];
|
|
33
|
-
/** Present on probes
|
|
33
|
+
/** Present on current probes; optional for low-level callers and older in-memory results. */
|
|
34
34
|
readonly socialMetadata?: readonly SocialMetadataSignal[];
|
|
35
35
|
readonly h1s: readonly ElementSignal[];
|
|
36
36
|
readonly firstMainText?: ElementSignal;
|
|
@@ -103,6 +103,8 @@ export interface TargetExpectations {
|
|
|
103
103
|
readonly maxCriticalMs?: number;
|
|
104
104
|
}
|
|
105
105
|
export interface AuditTarget {
|
|
106
|
+
/** Stable identity used to match the same target across reports with different origins. */
|
|
107
|
+
readonly id?: string;
|
|
106
108
|
readonly url: string;
|
|
107
109
|
readonly expectations: TargetExpectations;
|
|
108
110
|
}
|
|
@@ -170,6 +172,8 @@ export interface AuditSummary {
|
|
|
170
172
|
readonly incomplete: number;
|
|
171
173
|
}
|
|
172
174
|
export interface AuditResult {
|
|
175
|
+
/** Version of the persisted audit-report contract, independent of the package version. */
|
|
176
|
+
readonly schemaVersion: 1;
|
|
173
177
|
readonly version: string;
|
|
174
178
|
readonly generatedAt: string;
|
|
175
179
|
readonly durationMs: number;
|
|
@@ -178,4 +182,82 @@ export interface AuditResult {
|
|
|
178
182
|
readonly summary: AuditSummary;
|
|
179
183
|
}
|
|
180
184
|
export type ReportFormat = "terminal" | "json" | "sarif";
|
|
185
|
+
export type ComparisonKind = "regression" | "fixed" | "changed";
|
|
186
|
+
export type ComparisonScope = "target" | "agent" | "finding" | "response" | "metadata" | "timing";
|
|
187
|
+
export interface ComparisonChange {
|
|
188
|
+
readonly kind: ComparisonKind;
|
|
189
|
+
readonly scope: ComparisonScope;
|
|
190
|
+
readonly code: string;
|
|
191
|
+
readonly message: string;
|
|
192
|
+
readonly agent?: string;
|
|
193
|
+
readonly field?: string;
|
|
194
|
+
readonly baseline?: string | number | boolean;
|
|
195
|
+
readonly candidate?: string | number | boolean;
|
|
196
|
+
}
|
|
197
|
+
export interface ComparisonTimelineEvent {
|
|
198
|
+
readonly key: string;
|
|
199
|
+
readonly label: string;
|
|
200
|
+
readonly medianMs: number;
|
|
201
|
+
readonly location?: ElementLocation | "mixed";
|
|
202
|
+
readonly observedByByte?: number;
|
|
203
|
+
}
|
|
204
|
+
export interface ComparisonTimelineSnapshot {
|
|
205
|
+
readonly samples: number;
|
|
206
|
+
readonly events: readonly ComparisonTimelineEvent[];
|
|
207
|
+
}
|
|
208
|
+
export interface ComparisonTimelineLane {
|
|
209
|
+
readonly agent: string;
|
|
210
|
+
readonly label: string;
|
|
211
|
+
readonly baseline?: ComparisonTimelineSnapshot;
|
|
212
|
+
readonly candidate?: ComparisonTimelineSnapshot;
|
|
213
|
+
}
|
|
214
|
+
export type TargetComparisonStatus = "matched" | "added" | "removed";
|
|
215
|
+
export interface TargetComparison {
|
|
216
|
+
readonly key: string;
|
|
217
|
+
readonly id?: string;
|
|
218
|
+
readonly status: TargetComparisonStatus;
|
|
219
|
+
readonly baselineUrl?: string;
|
|
220
|
+
readonly candidateUrl?: string;
|
|
221
|
+
readonly changes: readonly ComparisonChange[];
|
|
222
|
+
readonly timelines: readonly ComparisonTimelineLane[];
|
|
223
|
+
}
|
|
224
|
+
export interface AuditReportDescriptor {
|
|
225
|
+
readonly label: string;
|
|
226
|
+
readonly version: string;
|
|
227
|
+
readonly schemaVersion: 1;
|
|
228
|
+
readonly generatedAt: string;
|
|
229
|
+
readonly repeat: number;
|
|
230
|
+
}
|
|
231
|
+
export interface ComparisonThresholds {
|
|
232
|
+
readonly timingRegressionMs: number;
|
|
233
|
+
readonly timingRegressionPercent: number;
|
|
234
|
+
}
|
|
235
|
+
export interface ComparisonSummary {
|
|
236
|
+
readonly targets: number;
|
|
237
|
+
readonly matchedTargets: number;
|
|
238
|
+
readonly addedTargets: number;
|
|
239
|
+
readonly removedTargets: number;
|
|
240
|
+
readonly unchangedTargets: number;
|
|
241
|
+
readonly regressions: number;
|
|
242
|
+
readonly fixed: number;
|
|
243
|
+
readonly changed: number;
|
|
244
|
+
}
|
|
245
|
+
export interface AuditComparison {
|
|
246
|
+
readonly schemaVersion: 1;
|
|
247
|
+
readonly kind: "comparison";
|
|
248
|
+
readonly version: string;
|
|
249
|
+
readonly generatedAt: string;
|
|
250
|
+
readonly baseline: AuditReportDescriptor;
|
|
251
|
+
readonly candidate: AuditReportDescriptor;
|
|
252
|
+
readonly thresholds: ComparisonThresholds;
|
|
253
|
+
readonly results: readonly TargetComparison[];
|
|
254
|
+
readonly summary: ComparisonSummary;
|
|
255
|
+
}
|
|
256
|
+
export interface CompareAuditOptions {
|
|
257
|
+
readonly baselineLabel?: string;
|
|
258
|
+
readonly candidateLabel?: string;
|
|
259
|
+
readonly timingRegressionMs?: number;
|
|
260
|
+
readonly timingRegressionPercent?: number;
|
|
261
|
+
}
|
|
262
|
+
export type ComparisonReportFormat = "terminal" | "json" | "html";
|
|
181
263
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACnC;AAED,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,SAAS,GACT,QAAQ,GACR,UAAU,GACV,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,qBAAqB,GACrB,eAAe,CAAC;AAEpB,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;CAC3C;AAED,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,EAAE,SAAS,aAAa,EAAE,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
if: github.event_name == 'pull_request'
|
|
28
28
|
continue-on-error: true
|
|
29
29
|
run: >-
|
|
30
|
-
npx --yes ssrwire@0.
|
|
30
|
+
npx --yes ssrwire@0.4.0 check
|
|
31
31
|
--config ssrwire.config.yml
|
|
32
32
|
--format sarif
|
|
33
33
|
--output reports/ssrwire.sarif
|
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
if: github.event_name != 'pull_request'
|
|
39
39
|
continue-on-error: true
|
|
40
40
|
run: >-
|
|
41
|
-
npx --yes ssrwire@0.
|
|
41
|
+
npx --yes ssrwire@0.4.0 check
|
|
42
42
|
--config ssrwire.config.yml
|
|
43
43
|
--format sarif
|
|
44
44
|
--output reports/ssrwire.sarif
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Replace these URLs with a small set of high-value SSR routes.
|
|
2
2
|
targets:
|
|
3
|
-
-
|
|
3
|
+
- id: home
|
|
4
|
+
url: https://example.com/
|
|
4
5
|
expectedStatus: 200
|
|
5
6
|
expectedFinalUrl: https://example.com/
|
|
6
7
|
require:
|
|
@@ -12,7 +13,8 @@ targets:
|
|
|
12
13
|
openGraph: true
|
|
13
14
|
twitterCard: true
|
|
14
15
|
|
|
15
|
-
-
|
|
16
|
+
- id: pricing
|
|
17
|
+
url: https://example.com/pricing/
|
|
16
18
|
expectedStatus: 200
|
|
17
19
|
require:
|
|
18
20
|
title: true
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -76,6 +89,24 @@ function parseFailOn(value: string): CliOptions["failOn"] {
|
|
|
76
89
|
throw new InvalidArgumentError("Expected error, warning, or never.");
|
|
77
90
|
}
|
|
78
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
|
+
|
|
79
110
|
function addCheckOptions(command: Command): Command {
|
|
80
111
|
return command
|
|
81
112
|
.option("-c, --config <path>", "configuration file")
|
|
@@ -120,6 +151,16 @@ async function writeReport(path: string, report: string): Promise<void> {
|
|
|
120
151
|
await writeFile(absolute, report, "utf8");
|
|
121
152
|
}
|
|
122
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
|
+
|
|
123
164
|
async function check(urls: readonly string[], options: CliOptions): Promise<void> {
|
|
124
165
|
const config = await loadConfig({
|
|
125
166
|
...(options.config ? { configPath: options.config } : {}),
|
|
@@ -149,6 +190,38 @@ async function check(urls: readonly string[], options: CliOptions): Promise<void
|
|
|
149
190
|
process.exitCode = reportExitCode(audit.summary, options.failOn);
|
|
150
191
|
}
|
|
151
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
|
+
|
|
152
225
|
async function initialize(path: string, force: boolean): Promise<void> {
|
|
153
226
|
const absolute = resolve(path);
|
|
154
227
|
if (!force) {
|
|
@@ -175,21 +248,40 @@ export async function main(argv: readonly string[] = process.argv): Promise<void
|
|
|
175
248
|
.exitOverride()
|
|
176
249
|
.showHelpAfterError();
|
|
177
250
|
|
|
178
|
-
addCheckOptions(program)
|
|
179
|
-
.argument("[urls...]", "HTTP or HTTPS URLs to inspect")
|
|
180
|
-
.action(async (urls: string[], _options: CliOptions, command: Command) => {
|
|
181
|
-
await check(urls, optionsFrom(command));
|
|
182
|
-
});
|
|
183
|
-
|
|
184
251
|
addCheckOptions(
|
|
185
252
|
program
|
|
186
|
-
.command("check")
|
|
253
|
+
.command("check", { isDefault: true })
|
|
187
254
|
.description("inspect one or more SSR responses")
|
|
188
255
|
.argument("[urls...]", "HTTP or HTTPS URLs to inspect"),
|
|
189
256
|
).action(async (urls: string[], _options: CliOptions, command: Command) => {
|
|
190
257
|
await check(urls, optionsFrom(command));
|
|
191
258
|
});
|
|
192
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
|
+
|
|
193
285
|
program
|
|
194
286
|
.command("init")
|
|
195
287
|
.description("create a documented starter configuration")
|