versioncam 0.5.2 → 0.7.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 +93 -0
- package/README.md +95 -2
- package/dist/cli/commands/check.js +25 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/doctor.js +54 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install.d.ts +40 -3
- package/dist/cli/commands/install.js +66 -11
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/cli/commands/record.d.ts +6 -0
- package/dist/cli/commands/record.js +24 -3
- package/dist/cli/commands/record.js.map +1 -1
- package/dist/cli/commands/stability.js +10 -3
- package/dist/cli/commands/stability.js.map +1 -1
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +2 -0
- package/dist/cli/usage.js.map +1 -1
- package/dist/config.d.ts +37 -1
- package/dist/config.js +115 -3
- package/dist/config.js.map +1 -1
- package/dist/core/cursor.d.ts +22 -0
- package/dist/core/cursor.js +6 -0
- package/dist/core/cursor.js.map +1 -1
- package/dist/core/motion-defaults.d.ts +8 -0
- package/dist/core/motion-defaults.js +8 -0
- package/dist/core/motion-defaults.js.map +1 -1
- package/dist/core/timeline.d.ts +24 -0
- package/dist/core/timeline.js.map +1 -1
- package/dist/driver/clip.d.ts +6 -0
- package/dist/driver/clip.js +33 -2
- package/dist/driver/clip.js.map +1 -1
- package/dist/driver/file-window.d.ts +66 -0
- package/dist/driver/file-window.js +245 -0
- package/dist/driver/file-window.js.map +1 -0
- package/dist/driver/page-hooks.d.ts +23 -1
- package/dist/driver/page-hooks.js +36 -1
- package/dist/driver/page-hooks.js.map +1 -1
- package/dist/driver/replay.d.ts +263 -0
- package/dist/driver/replay.js +558 -0
- package/dist/driver/replay.js.map +1 -0
- package/dist/driver/session.d.ts +70 -4
- package/dist/driver/session.js +267 -29
- package/dist/driver/session.js.map +1 -1
- package/dist/driver/settle.js +6 -0
- package/dist/driver/settle.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js.map +1 -1
- package/dist/page/assets/index-CnRDAiUz.js +2 -0
- package/dist/page/index.html +1 -1
- package/dist/publish/protocol.d.ts +12 -1
- package/dist/publish/protocol.js.map +1 -1
- package/dsl.md +68 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/versioncam/authoring.md +6 -1
- package/plugin/skills/versioncam/onboarding.md +9 -3
- package/dist/page/assets/index-BtUbUEdN.js +0 -2
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
/** Headers removed from every recorded request and response. */
|
|
5
|
+
const SECRET_HEADERS = new Set([
|
|
6
|
+
"authorization",
|
|
7
|
+
"cookie",
|
|
8
|
+
"set-cookie",
|
|
9
|
+
"x-api-key",
|
|
10
|
+
"proxy-authorization",
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* A body has to be served as the bytes it was, so these never go back to the
|
|
14
|
+
* page with it: the encoding was undone when it was recorded, and the length
|
|
15
|
+
* is whatever the bytes are now.
|
|
16
|
+
*/
|
|
17
|
+
const TRANSPORT_HEADERS = new Set([
|
|
18
|
+
"content-encoding",
|
|
19
|
+
"content-length",
|
|
20
|
+
"transfer-encoding",
|
|
21
|
+
"connection",
|
|
22
|
+
]);
|
|
23
|
+
/** How old a replay may be before `check` says so. */
|
|
24
|
+
export const STALE_DAYS = 90;
|
|
25
|
+
/**
|
|
26
|
+
* A glob in Playwright's syntax as a regular expression, so the recorder and
|
|
27
|
+
* the script it puts in the page match a URL the same way: `**` crosses `/`,
|
|
28
|
+
* `*` does not, `{a,b}` is either, and every other character is itself, `?`
|
|
29
|
+
* included. Anchored at both ends, as Playwright anchors a glob.
|
|
30
|
+
*/
|
|
31
|
+
export function globToRegExp(glob) {
|
|
32
|
+
let out = "^";
|
|
33
|
+
let inGroup = false;
|
|
34
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
35
|
+
const c = glob[i];
|
|
36
|
+
if (c === "\\" && i + 1 < glob.length) {
|
|
37
|
+
out += escapeRegExp(glob[i + 1]);
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
else if (c === "*") {
|
|
41
|
+
const starStar = glob[i + 1] === "*";
|
|
42
|
+
out += starStar ? ".*" : "[^/]*";
|
|
43
|
+
if (starStar)
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
else if (c === "{") {
|
|
47
|
+
inGroup = true;
|
|
48
|
+
out += "(?:";
|
|
49
|
+
}
|
|
50
|
+
else if (c === "}" && inGroup) {
|
|
51
|
+
inGroup = false;
|
|
52
|
+
out += ")";
|
|
53
|
+
}
|
|
54
|
+
else if (c === "," && inGroup) {
|
|
55
|
+
out += "|";
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
out += escapeRegExp(c);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return new RegExp(`${out}$`);
|
|
62
|
+
}
|
|
63
|
+
function escapeRegExp(text) {
|
|
64
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
65
|
+
}
|
|
66
|
+
export function patternOf(pattern) {
|
|
67
|
+
return typeof pattern === "string" ? globToRegExp(pattern) : pattern;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The URL a request is known by in the file: without its fragment and
|
|
71
|
+
* without the query parameters `redact` names, which are gone from the file
|
|
72
|
+
* and so cannot be part of the match.
|
|
73
|
+
*/
|
|
74
|
+
export function urlKey(url, redact) {
|
|
75
|
+
const parsed = new URL(url);
|
|
76
|
+
parsed.hash = "";
|
|
77
|
+
for (const name of redact)
|
|
78
|
+
parsed.searchParams.delete(name);
|
|
79
|
+
return parsed.href;
|
|
80
|
+
}
|
|
81
|
+
function withoutSecrets(headers) {
|
|
82
|
+
return Object.entries(headers)
|
|
83
|
+
.filter(([name]) => !SECRET_HEADERS.has(name.toLowerCase()))
|
|
84
|
+
.map(([name, value]) => ({ name, value }))
|
|
85
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
86
|
+
}
|
|
87
|
+
/** Text a person can read in a diff, or bytes that only mean something whole. */
|
|
88
|
+
function isText(mimeType) {
|
|
89
|
+
return /^text\/|json|javascript|xml|x-www-form-urlencoded|event-stream|svg/i.test(mimeType);
|
|
90
|
+
}
|
|
91
|
+
export function sha256(bytes) {
|
|
92
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One recorded exchange, as the file keeps it: the secrets and the redacted
|
|
96
|
+
* parameters gone, a text body in the entry, a binary one handed to `store`
|
|
97
|
+
* and named here by its hash.
|
|
98
|
+
*/
|
|
99
|
+
export function entryOf(exchange, options) {
|
|
100
|
+
const url = urlKey(exchange.url, options.redact);
|
|
101
|
+
const mimeType = Object.entries(exchange.responseHeaders).find(([name]) => name.toLowerCase() === "content-type")?.[1] ?? "application/octet-stream";
|
|
102
|
+
let content;
|
|
103
|
+
if (isText(mimeType)) {
|
|
104
|
+
content = {
|
|
105
|
+
size: exchange.body.length,
|
|
106
|
+
mimeType,
|
|
107
|
+
text: Buffer.from(exchange.body).toString("utf8"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const hash = sha256(exchange.body);
|
|
112
|
+
options.store(hash, exchange.body);
|
|
113
|
+
content = { size: exchange.body.length, mimeType, _file: hash };
|
|
114
|
+
}
|
|
115
|
+
const requestType = Object.entries(exchange.requestHeaders).find(([name]) => name.toLowerCase() === "content-type")?.[1] ?? "application/octet-stream";
|
|
116
|
+
return {
|
|
117
|
+
startedDateTime: options.recorded,
|
|
118
|
+
time: 0,
|
|
119
|
+
request: {
|
|
120
|
+
method: exchange.method,
|
|
121
|
+
url,
|
|
122
|
+
httpVersion: "HTTP/1.1",
|
|
123
|
+
headers: withoutSecrets(exchange.requestHeaders),
|
|
124
|
+
queryString: [...new URL(url).searchParams].map(([name, value]) => ({
|
|
125
|
+
name,
|
|
126
|
+
value,
|
|
127
|
+
})),
|
|
128
|
+
cookies: [],
|
|
129
|
+
headersSize: -1,
|
|
130
|
+
bodySize: exchange.postData?.length ?? 0,
|
|
131
|
+
...(exchange.postData !== null
|
|
132
|
+
? { postData: { mimeType: requestType, text: exchange.postData } }
|
|
133
|
+
: {}),
|
|
134
|
+
},
|
|
135
|
+
response: {
|
|
136
|
+
status: exchange.status,
|
|
137
|
+
statusText: exchange.statusText,
|
|
138
|
+
httpVersion: "HTTP/1.1",
|
|
139
|
+
headers: withoutSecrets(exchange.responseHeaders),
|
|
140
|
+
cookies: [],
|
|
141
|
+
content,
|
|
142
|
+
redirectURL: "",
|
|
143
|
+
headersSize: -1,
|
|
144
|
+
bodySize: exchange.body.length,
|
|
145
|
+
},
|
|
146
|
+
cache: {},
|
|
147
|
+
timings: { send: 0, wait: 0, receive: 0 },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The entries of a replay, by what a request is known by, each list in the
|
|
152
|
+
* order the requests were made, and served in that order: the third poll of
|
|
153
|
+
* a job's status gets the third answer the job gave, and every poll after
|
|
154
|
+
* the last gets the last. Among answers to one URL, one recorded for the
|
|
155
|
+
* same request body is taken first, for an app that posts several queries
|
|
156
|
+
* to one endpoint in an order that can vary.
|
|
157
|
+
*/
|
|
158
|
+
export class ReplayBook {
|
|
159
|
+
byKey = new Map();
|
|
160
|
+
used = new Map();
|
|
161
|
+
redact;
|
|
162
|
+
constructor(entries, redact) {
|
|
163
|
+
this.redact = redact;
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const key = `${entry.request.method} ${entry.request.url}`;
|
|
166
|
+
const list = this.byKey.get(key) ?? [];
|
|
167
|
+
list.push(entry);
|
|
168
|
+
this.byKey.set(key, list);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
answer(method, url, postData) {
|
|
172
|
+
const key = `${method} ${urlKey(url, this.redact)}`;
|
|
173
|
+
const list = this.byKey.get(key);
|
|
174
|
+
if (!list || list.length === 0)
|
|
175
|
+
return null;
|
|
176
|
+
const used = this.used.get(key) ?? new Set();
|
|
177
|
+
this.used.set(key, used);
|
|
178
|
+
const unused = list
|
|
179
|
+
.map((entry, index) => ({ entry, index }))
|
|
180
|
+
.filter(({ index }) => !used.has(index));
|
|
181
|
+
const pick = unused.find(({ entry }) => (entry.request.postData?.text ?? null) === postData) ?? unused[0];
|
|
182
|
+
if (!pick)
|
|
183
|
+
return list[list.length - 1];
|
|
184
|
+
used.add(pick.index);
|
|
185
|
+
return pick.entry;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** The file a clip's replay is in. */
|
|
189
|
+
export function replayFile(dir, clipId) {
|
|
190
|
+
return join(dir, `${clipId}.har`);
|
|
191
|
+
}
|
|
192
|
+
export function readReplay(file) {
|
|
193
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
194
|
+
}
|
|
195
|
+
/** What `doctor` and `check` say about a clip's replay. */
|
|
196
|
+
export function describeReplay(dir, clipId, now = new Date()) {
|
|
197
|
+
const file = replayFile(dir, clipId);
|
|
198
|
+
if (!existsSync(file))
|
|
199
|
+
return { file, exists: false };
|
|
200
|
+
const har = readReplay(file);
|
|
201
|
+
const recorded = har.log._versioncam?.recorded ?? har.log.entries[0]?.startedDateTime ?? "";
|
|
202
|
+
const ageDays = recorded
|
|
203
|
+
? Math.floor((now.getTime() - new Date(recorded).getTime()) / 86_400_000)
|
|
204
|
+
: Number.NaN;
|
|
205
|
+
return {
|
|
206
|
+
file,
|
|
207
|
+
exists: true,
|
|
208
|
+
requests: har.log.entries.length,
|
|
209
|
+
recorded,
|
|
210
|
+
ageDays,
|
|
211
|
+
recorder: har.log._versioncam?.recorder ?? har.log.creator.version,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Which mode a clip runs in, once the file's presence is known. */
|
|
215
|
+
export function effectiveMode(mode, fileExists) {
|
|
216
|
+
if (mode === "auto")
|
|
217
|
+
return fileExists ? "replay" : "record";
|
|
218
|
+
return mode;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A clip's replay while it runs: the routes it installs, what it recorded,
|
|
222
|
+
* what it could not answer, and how many answers are still on their way.
|
|
223
|
+
*/
|
|
224
|
+
export class ClipReplay {
|
|
225
|
+
mode;
|
|
226
|
+
file;
|
|
227
|
+
/** Requests the replay matched and did not hold, as "METHOD url". */
|
|
228
|
+
misses = [];
|
|
229
|
+
/** Requests sent live because the replay did not hold them (`onMissing: "live"`). */
|
|
230
|
+
wentLive = [];
|
|
231
|
+
recorded = [];
|
|
232
|
+
bodies = new Map();
|
|
233
|
+
book;
|
|
234
|
+
when = new Date().toISOString();
|
|
235
|
+
inFlight = 0;
|
|
236
|
+
waiters = [];
|
|
237
|
+
config;
|
|
238
|
+
clipId;
|
|
239
|
+
recorder;
|
|
240
|
+
constructor(config, clipId, recorder, override) {
|
|
241
|
+
this.config = config;
|
|
242
|
+
this.clipId = clipId;
|
|
243
|
+
this.recorder = recorder;
|
|
244
|
+
this.file = replayFile(config.dir, clipId);
|
|
245
|
+
this.mode = effectiveMode(override ?? config.mode, existsSync(this.file));
|
|
246
|
+
if (this.mode === "replay" && !existsSync(this.file)) {
|
|
247
|
+
throw new Error(`Clip "${clipId}" replays its responses from ${this.file}, and there is no such file. ` +
|
|
248
|
+
"Record it once: `versioncam record " +
|
|
249
|
+
clipId +
|
|
250
|
+
" --replay record`.");
|
|
251
|
+
}
|
|
252
|
+
this.book =
|
|
253
|
+
this.mode === "replay"
|
|
254
|
+
? new ReplayBook(readReplay(this.file).log.entries, config.redact)
|
|
255
|
+
: null;
|
|
256
|
+
}
|
|
257
|
+
/** The page-side script that paces streamed bodies, or null when nothing is paced. */
|
|
258
|
+
pacingScript() {
|
|
259
|
+
if (this.mode === "live" || this.config.pace.length === 0)
|
|
260
|
+
return null;
|
|
261
|
+
return pacedReplayScript(this.config.pace);
|
|
262
|
+
}
|
|
263
|
+
/** Install the routes. Before the page is opened, so its first request is caught. */
|
|
264
|
+
async install(context) {
|
|
265
|
+
if (this.mode === "live")
|
|
266
|
+
return;
|
|
267
|
+
for (const pattern of this.config.record) {
|
|
268
|
+
await context.route(pattern, (route) => this.handle(route));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Resolves once every request the replay has taken is answered and its body
|
|
273
|
+
* is in the page. The driver waits on it after each tick, so a request made
|
|
274
|
+
* in a frame is answered within that frame on every run, however long the
|
|
275
|
+
* network or the disk took: a tile arrives on the frame it was asked for.
|
|
276
|
+
*/
|
|
277
|
+
async idle(timeoutMs = 30_000) {
|
|
278
|
+
if (this.inFlight === 0)
|
|
279
|
+
return;
|
|
280
|
+
await new Promise((resolve) => {
|
|
281
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
282
|
+
this.waiters.push(() => {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
resolve();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
settled() {
|
|
289
|
+
this.inFlight -= 1;
|
|
290
|
+
if (this.inFlight > 0)
|
|
291
|
+
return;
|
|
292
|
+
const waiting = this.waiters;
|
|
293
|
+
this.waiters = [];
|
|
294
|
+
for (const wake of waiting)
|
|
295
|
+
wake();
|
|
296
|
+
}
|
|
297
|
+
async handle(route) {
|
|
298
|
+
this.inFlight += 1;
|
|
299
|
+
const request = route.request();
|
|
300
|
+
try {
|
|
301
|
+
if (this.mode === "record")
|
|
302
|
+
await this.record(route, request);
|
|
303
|
+
else
|
|
304
|
+
await this.serve(route, request);
|
|
305
|
+
// Fulfilled is not yet received: the body is in the page once its
|
|
306
|
+
// response has finished, and only then is the frame's picture the one
|
|
307
|
+
// with the answer in it.
|
|
308
|
+
const response = await request.response().catch(() => null);
|
|
309
|
+
await response?.finished().catch(() => undefined);
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
this.settled();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async record(route, request) {
|
|
316
|
+
let response;
|
|
317
|
+
try {
|
|
318
|
+
response = await route.fetch({ timeout: 60_000 });
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
await route.abort("failed").catch(() => undefined);
|
|
322
|
+
console.warn(`[recorder] replay: ${request.method()} ${request.url()} failed while recording (${error.message.split("\n")[0]}); not recorded.`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const body = await response.body();
|
|
326
|
+
await route.fulfill({ response, body }).catch(() => undefined);
|
|
327
|
+
this.recorded.push(entryOf({
|
|
328
|
+
method: request.method(),
|
|
329
|
+
url: request.url(),
|
|
330
|
+
requestHeaders: await request.allHeaders(),
|
|
331
|
+
postData: request.postData(),
|
|
332
|
+
status: response.status(),
|
|
333
|
+
statusText: response.statusText(),
|
|
334
|
+
responseHeaders: response.headers(),
|
|
335
|
+
body,
|
|
336
|
+
}, {
|
|
337
|
+
redact: this.config.redact,
|
|
338
|
+
recorded: this.when,
|
|
339
|
+
store: (hash, bytes) => this.bodies.set(hash, bytes),
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
async serve(route, request) {
|
|
343
|
+
const entry = this.book?.answer(request.method(), request.url(), request.postData());
|
|
344
|
+
if (!entry) {
|
|
345
|
+
const what = `${request.method()} ${request.url()}`;
|
|
346
|
+
if (this.config.onMissing === "live") {
|
|
347
|
+
this.wentLive.push(what);
|
|
348
|
+
console.warn(`[recorder] replay: ${what} is not in ${this.file}; sent to the network.`);
|
|
349
|
+
await route.fallback();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.misses.push(what);
|
|
353
|
+
await route.abort("failed").catch(() => undefined);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const { content } = entry.response;
|
|
357
|
+
const body = content._file
|
|
358
|
+
? readFileSync(join(this.config.dir, "bodies", content._file))
|
|
359
|
+
: content.encoding === "base64"
|
|
360
|
+
? Buffer.from(content.text ?? "", "base64")
|
|
361
|
+
: Buffer.from(content.text ?? "", "utf8");
|
|
362
|
+
const headers = {};
|
|
363
|
+
for (const { name, value } of entry.response.headers) {
|
|
364
|
+
if (!TRANSPORT_HEADERS.has(name.toLowerCase()))
|
|
365
|
+
headers[name] = value;
|
|
366
|
+
}
|
|
367
|
+
await route
|
|
368
|
+
.fulfill({ status: entry.response.status, headers, body })
|
|
369
|
+
.catch(() => undefined);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* What stops the clip: a request the replay was asked for and did not
|
|
373
|
+
* hold. Null when there is none.
|
|
374
|
+
*/
|
|
375
|
+
failure() {
|
|
376
|
+
if (this.misses.length === 0)
|
|
377
|
+
return null;
|
|
378
|
+
const shown = this.misses.slice(0, 5).join("\n ");
|
|
379
|
+
const more = this.misses.length > 5 ? `\n and ${this.misses.length - 5} more` : "";
|
|
380
|
+
return (`Clip "${this.clipId}" asked for ${this.misses.length} response(s) its replay does not hold:\n ${shown}${more}\n` +
|
|
381
|
+
`The app has changed what it fetches since ${this.file} was recorded. ` +
|
|
382
|
+
`Record it again: \`versioncam record ${this.clipId} --replay record\`.`);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Write what was recorded. Only for a clip that passed: a replay of half a
|
|
386
|
+
* clip would serve a later run a different app than the one it shows.
|
|
387
|
+
*/
|
|
388
|
+
write() {
|
|
389
|
+
if (this.mode !== "record")
|
|
390
|
+
return;
|
|
391
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
392
|
+
if (this.bodies.size > 0) {
|
|
393
|
+
mkdirSync(join(this.config.dir, "bodies"), { recursive: true });
|
|
394
|
+
for (const [hash, bytes] of this.bodies) {
|
|
395
|
+
const path = join(this.config.dir, "bodies", hash);
|
|
396
|
+
if (!existsSync(path))
|
|
397
|
+
writeFileSync(path, bytes);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const har = {
|
|
401
|
+
log: {
|
|
402
|
+
version: "1.2",
|
|
403
|
+
creator: { name: "versioncam", version: this.recorder },
|
|
404
|
+
comment: `Recorded for the clip "${this.clipId}" on ${this.when.slice(0, 10)}. ` +
|
|
405
|
+
"Authorization, cookies and API key headers were removed, and the query " +
|
|
406
|
+
`parameters ${this.config.redact.length > 0 ? this.config.redact.join(", ") : "none named"} left out.`,
|
|
407
|
+
_versioncam: {
|
|
408
|
+
clip: this.clipId,
|
|
409
|
+
recorded: this.when,
|
|
410
|
+
recorder: this.recorder,
|
|
411
|
+
},
|
|
412
|
+
entries: this.recorded,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
writeFileSync(this.file, `${JSON.stringify(har, null, 2)}\n`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Deliver a paced response a piece at a time on the page's own clock.
|
|
420
|
+
*
|
|
421
|
+
* The recorder cannot stream a response into the page: Playwright fulfils a
|
|
422
|
+
* request whole. So the page does it. For a URL the config paces, `fetch`
|
|
423
|
+
* hands back a response whose body is a stream that gives out one event of
|
|
424
|
+
* an event stream, or one line of anything else, every `chunkMs`, on the
|
|
425
|
+
* page's `setTimeout`, which the recorder's clock advances a frame at a
|
|
426
|
+
* time; and `EventSource` dispatches one event every `chunkMs` the same
|
|
427
|
+
* way. The same piece therefore lands on the same frame every run, at any
|
|
428
|
+
* frame rate. `window.__recorderReplay.pending` counts the bodies still
|
|
429
|
+
* arriving, and a settle waits for it to reach none.
|
|
430
|
+
*
|
|
431
|
+
* An `EventSource` whose stream has ended reports an error and stays
|
|
432
|
+
* closed, instead of reconnecting as a live one would: a replay would only
|
|
433
|
+
* answer it with the same stream again.
|
|
434
|
+
*/
|
|
435
|
+
export function pacedReplayScript(pace) {
|
|
436
|
+
const rules = pace.map(({ url, chunkMs }) => ({
|
|
437
|
+
source: url.source,
|
|
438
|
+
flags: url.flags.replace("g", ""),
|
|
439
|
+
chunkMs,
|
|
440
|
+
}));
|
|
441
|
+
return `(() => {
|
|
442
|
+
const rules = ${JSON.stringify(rules)}.map((r) => ({ test: new RegExp(r.source, r.flags), chunkMs: r.chunkMs }));
|
|
443
|
+
const state = { pending: 0 };
|
|
444
|
+
window.__recorderReplay = state;
|
|
445
|
+
const absolute = (url) => new URL(String(url), location.href).href;
|
|
446
|
+
const paceOf = (url) => {
|
|
447
|
+
for (const rule of rules) if (rule.test.test(url)) return rule.chunkMs;
|
|
448
|
+
return null;
|
|
449
|
+
};
|
|
450
|
+
const later = (ms, fn) => window.setTimeout(fn, ms);
|
|
451
|
+
const pieces = (text, type) => {
|
|
452
|
+
const parts = /event-stream/i.test(type) ? text.split(/(?<=\\r?\\n\\r?\\n)/) : text.split(/(?<=\\n)/);
|
|
453
|
+
return parts.filter((part) => part.length > 0);
|
|
454
|
+
};
|
|
455
|
+
const native = window.fetch;
|
|
456
|
+
window.fetch = async function (input, init) {
|
|
457
|
+
const url = absolute(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
458
|
+
const ms = paceOf(url);
|
|
459
|
+
if (ms === null) return native.apply(this, arguments);
|
|
460
|
+
const response = await native.apply(this, arguments);
|
|
461
|
+
if (response.type === "opaque" || !response.body || response.status === 204 || response.status === 304) return response;
|
|
462
|
+
const parts = pieces(await response.text(), response.headers.get("content-type") || "");
|
|
463
|
+
const encoder = new TextEncoder();
|
|
464
|
+
const signal = init && init.signal;
|
|
465
|
+
state.pending += 1;
|
|
466
|
+
let open = true;
|
|
467
|
+
const done = () => { if (open) { open = false; state.pending -= 1; } };
|
|
468
|
+
const body = new ReadableStream({
|
|
469
|
+
start(controller) {
|
|
470
|
+
let i = 0;
|
|
471
|
+
const next = () => {
|
|
472
|
+
if (!open) return;
|
|
473
|
+
if (signal && signal.aborted) { done(); controller.error(new DOMException("The operation was aborted.", "AbortError")); return; }
|
|
474
|
+
if (i >= parts.length) { done(); controller.close(); return; }
|
|
475
|
+
controller.enqueue(encoder.encode(parts[i]));
|
|
476
|
+
i += 1;
|
|
477
|
+
later(ms, next);
|
|
478
|
+
};
|
|
479
|
+
next();
|
|
480
|
+
},
|
|
481
|
+
cancel() { done(); },
|
|
482
|
+
});
|
|
483
|
+
const headers = new Headers(response.headers);
|
|
484
|
+
headers.delete("content-length");
|
|
485
|
+
headers.delete("content-encoding");
|
|
486
|
+
const paced = new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
487
|
+
Object.defineProperty(paced, "url", { value: response.url });
|
|
488
|
+
return paced;
|
|
489
|
+
};
|
|
490
|
+
const NativeEventSource = window.EventSource;
|
|
491
|
+
if (typeof NativeEventSource === "function") {
|
|
492
|
+
const parse = (text) => text.split(/\\r?\\n\\r?\\n/).map((block) => {
|
|
493
|
+
const event = { type: "message", data: [], id: "" };
|
|
494
|
+
for (const line of block.split(/\\r?\\n/)) {
|
|
495
|
+
if (!line || line.startsWith(":")) continue;
|
|
496
|
+
const colon = line.indexOf(":");
|
|
497
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
498
|
+
const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
499
|
+
if (field === "event") event.type = value;
|
|
500
|
+
else if (field === "data") event.data.push(value);
|
|
501
|
+
else if (field === "id") event.id = value;
|
|
502
|
+
}
|
|
503
|
+
return event;
|
|
504
|
+
}).filter((event) => event.data.length > 0);
|
|
505
|
+
class PacedEventSource extends EventTarget {
|
|
506
|
+
constructor(url, init) {
|
|
507
|
+
super();
|
|
508
|
+
this.url = absolute(url);
|
|
509
|
+
this.withCredentials = Boolean(init && init.withCredentials);
|
|
510
|
+
this.readyState = 0;
|
|
511
|
+
this.onopen = null;
|
|
512
|
+
this.onmessage = null;
|
|
513
|
+
this.onerror = null;
|
|
514
|
+
const ms = paceOf(this.url);
|
|
515
|
+
state.pending += 1;
|
|
516
|
+
let open = true;
|
|
517
|
+
this._done = () => { if (open) { open = false; state.pending -= 1; } };
|
|
518
|
+
native(this.url, { credentials: this.withCredentials ? "include" : "same-origin", headers: { Accept: "text/event-stream" } })
|
|
519
|
+
.then((response) => response.text())
|
|
520
|
+
.then((text) => {
|
|
521
|
+
if (this.readyState === 2) return this._done();
|
|
522
|
+
this.readyState = 1;
|
|
523
|
+
this._fire(new Event("open"));
|
|
524
|
+
const events = parse(text);
|
|
525
|
+
let i = 0;
|
|
526
|
+
const origin = new URL(this.url).origin;
|
|
527
|
+
const next = () => {
|
|
528
|
+
if (this.readyState === 2) return this._done();
|
|
529
|
+
if (i >= events.length) { this.readyState = 2; this._fire(new Event("error")); return this._done(); }
|
|
530
|
+
const e = events[i];
|
|
531
|
+
i += 1;
|
|
532
|
+
this._fire(new MessageEvent(e.type, { data: e.data.join("\\n"), lastEventId: e.id, origin }));
|
|
533
|
+
later(ms, next);
|
|
534
|
+
};
|
|
535
|
+
next();
|
|
536
|
+
}, () => { this.readyState = 2; this._fire(new Event("error")); this._done(); });
|
|
537
|
+
}
|
|
538
|
+
_fire(event) {
|
|
539
|
+
const handler = this["on" + event.type];
|
|
540
|
+
if (typeof handler === "function") handler.call(this, event);
|
|
541
|
+
this.dispatchEvent(event);
|
|
542
|
+
}
|
|
543
|
+
close() { this.readyState = 2; this._done(); }
|
|
544
|
+
}
|
|
545
|
+
const EventSourceOrPaced = function EventSource(url, init) {
|
|
546
|
+
if (paceOf(absolute(url)) === null) return new NativeEventSource(url, init);
|
|
547
|
+
return new PacedEventSource(url, init);
|
|
548
|
+
};
|
|
549
|
+
EventSourceOrPaced.prototype = NativeEventSource.prototype;
|
|
550
|
+
for (const [name, value] of [["CONNECTING", 0], ["OPEN", 1], ["CLOSED", 2]]) {
|
|
551
|
+
Object.defineProperty(EventSourceOrPaced, name, { value });
|
|
552
|
+
Object.defineProperty(PacedEventSource.prototype, name, { value });
|
|
553
|
+
}
|
|
554
|
+
window.EventSource = EventSourceOrPaced;
|
|
555
|
+
}
|
|
556
|
+
})();`;
|
|
557
|
+
}
|
|
558
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/driver/replay.ts"],"names":[],"mappings":"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"}
|