ssrwire 0.1.0 → 0.3.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 +45 -1
- package/CONTRIBUTING.md +8 -5
- package/PUBLISHING.md +92 -78
- package/README.md +117 -28
- package/SECURITY.md +8 -4
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +162 -6
- package/dist/analyze.js.map +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +82 -7
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +17 -0
- package/dist/redact.js.map +1 -1
- package/dist/reporters.d.ts.map +1 -1
- package/dist/reporters.js +69 -2
- 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 +9 -0
- package/dist/stability.d.ts.map +1 -0
- package/dist/stability.js +326 -0
- package/dist/stability.js.map +1 -0
- 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 +47 -0
- package/dist/types.d.ts.map +1 -1
- package/examples/github-actions.yml +2 -2
- package/examples/ssrwire.config.yml +8 -0
- package/package.json +5 -2
- package/src/analyze.ts +208 -7
- package/src/audit.ts +116 -8
- package/src/cli.ts +7 -1
- package/src/config.ts +12 -0
- package/src/http-probe.ts +4 -0
- package/src/index.ts +6 -0
- package/src/redact.ts +17 -0
- package/src/reporters.ts +97 -2
- package/src/social.ts +116 -0
- package/src/stability.ts +475 -0
- package/src/stream-parser.ts +31 -1
- package/src/types.ts +62 -0
package/src/redact.ts
CHANGED
|
@@ -90,6 +90,11 @@ function redactSignals(signals: DocumentSignals, plan: RedactionPlan): DocumentS
|
|
|
90
90
|
descriptions: signals.descriptions.map((signal) => redactElement(signal, plan)),
|
|
91
91
|
canonicals: signals.canonicals.map((signal) => redactElement(signal, plan)),
|
|
92
92
|
robots: signals.robots.map((signal) => redactElement(signal, plan)),
|
|
93
|
+
...(signals.socialMetadata === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: {
|
|
96
|
+
socialMetadata: signals.socialMetadata.map((signal) => redactElement(signal, plan)),
|
|
97
|
+
}),
|
|
93
98
|
h1s: signals.h1s.map((signal) => redactElement(signal, plan)),
|
|
94
99
|
...(signals.firstMainText === undefined
|
|
95
100
|
? {}
|
|
@@ -160,6 +165,18 @@ export function redactAudit(audit: AuditResult, secrets: readonly string[]): Aud
|
|
|
160
165
|
},
|
|
161
166
|
},
|
|
162
167
|
probes: result.probes.map((probe) => redactProbeWithPlan(probe, plan)),
|
|
168
|
+
...(result.stability === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: {
|
|
171
|
+
stability: result.stability.map((summary) => ({
|
|
172
|
+
...summary,
|
|
173
|
+
agent: {
|
|
174
|
+
...summary.agent,
|
|
175
|
+
label: redactText(summary.agent.label, plan),
|
|
176
|
+
userAgent: redactText(summary.agent.userAgent, plan),
|
|
177
|
+
},
|
|
178
|
+
})),
|
|
179
|
+
}),
|
|
163
180
|
findings: result.findings.map((finding) => ({
|
|
164
181
|
...finding,
|
|
165
182
|
message: redactText(finding.message, plan),
|
package/src/reporters.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
effectiveTwitterCardSignal,
|
|
3
|
+
firstSocialSignal,
|
|
4
|
+
OPEN_GRAPH_REQUIRED_PROPERTIES,
|
|
5
|
+
TWITTER_CARD_REQUIRED_FIELDS,
|
|
6
|
+
} from "./social.js";
|
|
1
7
|
import type {
|
|
8
|
+
AgentStability,
|
|
2
9
|
AuditResult,
|
|
3
10
|
ElementSignal,
|
|
4
11
|
Finding,
|
|
@@ -70,8 +77,9 @@ function firstSignal(signals: readonly ElementSignal[]): ElementSignal | undefin
|
|
|
70
77
|
return signals.find((signal) => signal.value.trim().length > 0) ?? signals[0];
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
function probeRow(probe: ProbeResult): readonly string[] {
|
|
80
|
+
function probeRow(probe: ProbeResult, showSample: boolean): readonly string[] {
|
|
74
81
|
return [
|
|
82
|
+
...(showSample ? [String(probe.sample ?? "—")] : []),
|
|
75
83
|
truncate(probe.agent.label, 24),
|
|
76
84
|
probe.status === undefined ? "—" : String(probe.status),
|
|
77
85
|
probe.completion,
|
|
@@ -84,6 +92,59 @@ function probeRow(probe: ProbeResult): readonly string[] {
|
|
|
84
92
|
];
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
function formatSignalSet(signals: readonly (ElementSignal | undefined)[]): string {
|
|
96
|
+
const present = signals.filter((signal): signal is ElementSignal => signal !== undefined);
|
|
97
|
+
if (present.length < signals.length) return `${present.length}/${signals.length}`;
|
|
98
|
+
const arrivalMs = Math.max(...present.map((signal) => signal.atMs));
|
|
99
|
+
const location = present.some((signal) => signal.location === "body")
|
|
100
|
+
? "body"
|
|
101
|
+
: present.some((signal) => signal.location === "document")
|
|
102
|
+
? "document"
|
|
103
|
+
: "head";
|
|
104
|
+
return `${Math.round(arrivalMs)} ms/${location}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function socialRow(probe: ProbeResult, showSample: boolean): readonly string[] {
|
|
108
|
+
return [
|
|
109
|
+
...(showSample ? [String(probe.sample ?? "—")] : []),
|
|
110
|
+
truncate(probe.agent.label, 24),
|
|
111
|
+
formatSignalSet(
|
|
112
|
+
OPEN_GRAPH_REQUIRED_PROPERTIES.map((property) => firstSocialSignal(probe.signals, property)),
|
|
113
|
+
),
|
|
114
|
+
formatSignalSet(
|
|
115
|
+
TWITTER_CARD_REQUIRED_FIELDS.map((field) => effectiveTwitterCardSignal(probe.signals, field)),
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stabilityRows(stability: readonly AgentStability[]): readonly (readonly string[])[] {
|
|
121
|
+
const labels: Readonly<Record<keyof AgentStability["timings"], string>> = {
|
|
122
|
+
headers: "Headers",
|
|
123
|
+
firstByte: "First byte",
|
|
124
|
+
criticalSignals: "Critical signals",
|
|
125
|
+
complete: "Complete",
|
|
126
|
+
};
|
|
127
|
+
const rows: string[][] = [];
|
|
128
|
+
for (const summary of stability) {
|
|
129
|
+
for (const key of ["headers", "firstByte", "criticalSignals", "complete"] as const) {
|
|
130
|
+
const stats = summary.timings[key];
|
|
131
|
+
if (stats === undefined) continue;
|
|
132
|
+
rows.push([
|
|
133
|
+
truncate(summary.agent.label, 24),
|
|
134
|
+
`${summary.complete}/${summary.samples}`,
|
|
135
|
+
labels[key],
|
|
136
|
+
String(stats.samples),
|
|
137
|
+
formatMs(stats.minMs),
|
|
138
|
+
formatMs(stats.medianMs),
|
|
139
|
+
formatMs(stats.p95Ms),
|
|
140
|
+
formatMs(stats.maxMs),
|
|
141
|
+
formatMs(stats.spreadMs),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return rows;
|
|
146
|
+
}
|
|
147
|
+
|
|
87
148
|
function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
|
|
88
149
|
const widths = headers.map((header, column) =>
|
|
89
150
|
Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)),
|
|
@@ -116,12 +177,17 @@ export function renderTerminal(audit: AuditResult, options: ReporterOptions = {}
|
|
|
116
177
|
paint(`SSRWire ${audit.version}`, ANSI.bold, color),
|
|
117
178
|
paint(`Generated ${audit.generatedAt} in ${formatMs(audit.durationMs)}`, ANSI.dim, color),
|
|
118
179
|
];
|
|
180
|
+
if ((audit.repeat ?? 1) > 1) {
|
|
181
|
+
lines.push(paint(`${audit.repeat} sequential samples per URL and agent`, ANSI.dim, color));
|
|
182
|
+
}
|
|
119
183
|
|
|
120
184
|
for (const result of audit.results) {
|
|
185
|
+
const showSample = (audit.repeat ?? 1) > 1;
|
|
121
186
|
lines.push("", paint(terminalSafe(result.target.url), ANSI.bold, color));
|
|
122
187
|
lines.push(
|
|
123
188
|
renderTable(
|
|
124
189
|
[
|
|
190
|
+
...(showSample ? ["Sample"] : []),
|
|
125
191
|
"Agent",
|
|
126
192
|
"HTTP",
|
|
127
193
|
"Result",
|
|
@@ -132,10 +198,39 @@ export function renderTerminal(audit: AuditResult, options: ReporterOptions = {}
|
|
|
132
198
|
"Canonical",
|
|
133
199
|
"Main",
|
|
134
200
|
],
|
|
135
|
-
result.probes.map(probeRow),
|
|
201
|
+
result.probes.map((probe) => probeRow(probe, showSample)),
|
|
136
202
|
),
|
|
137
203
|
);
|
|
138
204
|
|
|
205
|
+
const showSocial =
|
|
206
|
+
result.target.expectations.requireOpenGraph === true ||
|
|
207
|
+
result.target.expectations.requireTwitterCard === true ||
|
|
208
|
+
result.probes.some((probe) => (probe.signals.socialMetadata?.length ?? 0) > 0);
|
|
209
|
+
if (showSocial) {
|
|
210
|
+
lines.push(
|
|
211
|
+
"",
|
|
212
|
+
"Social preview readiness",
|
|
213
|
+
renderTable(
|
|
214
|
+
[...(showSample ? ["Sample"] : []), "Agent", "Open Graph", "Twitter Card"],
|
|
215
|
+
result.probes.map((probe) => socialRow(probe, showSample)),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.stability !== undefined) {
|
|
221
|
+
const rows = stabilityRows(result.stability);
|
|
222
|
+
if (rows.length > 0) {
|
|
223
|
+
lines.push(
|
|
224
|
+
"",
|
|
225
|
+
"Stability timings (nearest-rank p95)",
|
|
226
|
+
renderTable(
|
|
227
|
+
["Agent", "Complete", "Metric", "N", "Min", "Median", "P95", "Max", "Spread"],
|
|
228
|
+
rows,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
139
234
|
if (result.findings.length === 0) {
|
|
140
235
|
lines.push("Findings: none");
|
|
141
236
|
} else {
|
package/src/social.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { DocumentSignals, SocialMetadataProperty, SocialMetadataSignal } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const SOCIAL_METADATA_PROPERTIES = [
|
|
4
|
+
"og:title",
|
|
5
|
+
"og:type",
|
|
6
|
+
"og:url",
|
|
7
|
+
"og:image",
|
|
8
|
+
"og:description",
|
|
9
|
+
"twitter:card",
|
|
10
|
+
"twitter:title",
|
|
11
|
+
"twitter:description",
|
|
12
|
+
"twitter:image",
|
|
13
|
+
] as const satisfies readonly SocialMetadataProperty[];
|
|
14
|
+
|
|
15
|
+
export const OPEN_GRAPH_PROPERTIES = [
|
|
16
|
+
"og:title",
|
|
17
|
+
"og:type",
|
|
18
|
+
"og:url",
|
|
19
|
+
"og:image",
|
|
20
|
+
"og:description",
|
|
21
|
+
] as const satisfies readonly SocialMetadataProperty[];
|
|
22
|
+
|
|
23
|
+
export const OPEN_GRAPH_REQUIRED_PROPERTIES = [
|
|
24
|
+
"og:title",
|
|
25
|
+
"og:type",
|
|
26
|
+
"og:url",
|
|
27
|
+
"og:image",
|
|
28
|
+
] as const satisfies readonly SocialMetadataProperty[];
|
|
29
|
+
|
|
30
|
+
export const TWITTER_PROPERTIES = [
|
|
31
|
+
"twitter:card",
|
|
32
|
+
"twitter:title",
|
|
33
|
+
"twitter:description",
|
|
34
|
+
"twitter:image",
|
|
35
|
+
] as const satisfies readonly SocialMetadataProperty[];
|
|
36
|
+
|
|
37
|
+
export const TWITTER_CARD_REQUIRED_FIELDS = ["card", "title", "description", "image"] as const;
|
|
38
|
+
export type TwitterCardField = (typeof TWITTER_CARD_REQUIRED_FIELDS)[number];
|
|
39
|
+
|
|
40
|
+
const SOCIAL_METADATA_PROPERTY_SET = new Set<string>(SOCIAL_METADATA_PROPERTIES);
|
|
41
|
+
const SOCIAL_URL_PROPERTY_SET = new Set<SocialMetadataProperty>([
|
|
42
|
+
"og:url",
|
|
43
|
+
"og:image",
|
|
44
|
+
"twitter:image",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function isSocialMetadataProperty(
|
|
48
|
+
value: string | undefined,
|
|
49
|
+
): value is SocialMetadataProperty {
|
|
50
|
+
return value !== undefined && SOCIAL_METADATA_PROPERTY_SET.has(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function socialSignals(
|
|
54
|
+
signals: DocumentSignals,
|
|
55
|
+
property: SocialMetadataProperty,
|
|
56
|
+
): readonly SocialMetadataSignal[] {
|
|
57
|
+
return (signals.socialMetadata ?? []).filter((signal) => signal.property === property);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function firstSocialSignal(
|
|
61
|
+
signals: DocumentSignals,
|
|
62
|
+
property: SocialMetadataProperty,
|
|
63
|
+
): SocialMetadataSignal | undefined {
|
|
64
|
+
return socialSignals(signals, property).find((signal) => signal.value.trim().length > 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function effectiveTwitterCardSignal(
|
|
68
|
+
signals: DocumentSignals,
|
|
69
|
+
field: TwitterCardField,
|
|
70
|
+
): SocialMetadataSignal | undefined {
|
|
71
|
+
if (field === "card") return firstSocialSignal(signals, "twitter:card");
|
|
72
|
+
if (field === "title") {
|
|
73
|
+
return firstSocialSignal(signals, "twitter:title") ?? firstSocialSignal(signals, "og:title");
|
|
74
|
+
}
|
|
75
|
+
if (field === "description") {
|
|
76
|
+
return (
|
|
77
|
+
firstSocialSignal(signals, "twitter:description") ??
|
|
78
|
+
firstSocialSignal(signals, "og:description")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return firstSocialSignal(signals, "twitter:image") ?? firstSocialSignal(signals, "og:image");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function normalizeSocialValue(
|
|
85
|
+
property: SocialMetadataProperty,
|
|
86
|
+
value: string,
|
|
87
|
+
baseUrl: string,
|
|
88
|
+
): string {
|
|
89
|
+
const normalized = value.trim().replace(/\s+/gu, " ");
|
|
90
|
+
if (normalized.length === 0) return "";
|
|
91
|
+
if (SOCIAL_URL_PROPERTY_SET.has(property)) {
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(normalized, baseUrl);
|
|
94
|
+
url.hash = "";
|
|
95
|
+
return url.href;
|
|
96
|
+
} catch {
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return property === "og:type" || property === "twitter:card"
|
|
101
|
+
? normalized.toLowerCase()
|
|
102
|
+
: normalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isAbsoluteHttpSocialUrl(value: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
const url = new URL(value.trim());
|
|
108
|
+
return (
|
|
109
|
+
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
110
|
+
url.username.length === 0 &&
|
|
111
|
+
url.password.length === 0
|
|
112
|
+
);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|