versioncam 0.5.3 → 0.7.1
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 +103 -0
- package/README.md +108 -6
- 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/preview.js +4 -2
- package/dist/cli/commands/preview.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 +45 -4
- package/dist/config.js +134 -8
- 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/dist/render/render.d.ts +7 -1
- package/dist/render/render.js +51 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/sequence.d.ts +10 -5
- package/dist/render/sequence.js +44 -29
- package/dist/render/sequence.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 +15 -1
- package/plugin/skills/versioncam/onboarding.md +9 -3
- package/dist/page/assets/index-BtUbUEdN.js +0 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright";
|
|
2
|
+
/**
|
|
3
|
+
* Record a clip's responses once, and serve them on every run after.
|
|
4
|
+
*
|
|
5
|
+
* Some of what a clip films is not the app's to decide: a map's tiles, a
|
|
6
|
+
* model's answer, a job's status, a count that grows. Each comes from a
|
|
7
|
+
* server, at the server's speed and in its words of the day, so two
|
|
8
|
+
* recordings of one clip differ wherever one of them reaches the page, and
|
|
9
|
+
* the clip can never be re-rendered the same. The fix every app reached for
|
|
10
|
+
* was a mock written by hand, which is right once and wrong the day the
|
|
11
|
+
* endpoint changes. A replay is the server's own answer, written down the
|
|
12
|
+
* first time the clip runs and kept beside the clip as an input: the
|
|
13
|
+
* requests the config names are answered from it, and a request it does not
|
|
14
|
+
* hold fails the clip, with its URL, instead of quietly going live.
|
|
15
|
+
*
|
|
16
|
+
* The file is a HAR (one per clip, `<dir>/<clip>.har`), so a browser's
|
|
17
|
+
* developer tools can open it. Text bodies are written into it, where a
|
|
18
|
+
* diff can read them; binary bodies (a tile, an image) go beside it, under
|
|
19
|
+
* `bodies/`, named by their hash, so clips that load the same tiles store
|
|
20
|
+
* them once.
|
|
21
|
+
*/
|
|
22
|
+
export type ReplayMode = "auto" | "record" | "replay" | "live";
|
|
23
|
+
/** A glob, in Playwright's syntax, or a regular expression, over the whole URL. */
|
|
24
|
+
export type ReplayPattern = string | RegExp;
|
|
25
|
+
export type ReplayConfig = {
|
|
26
|
+
/** The requests to record and replay, by URL. */
|
|
27
|
+
record: ReplayPattern[];
|
|
28
|
+
/**
|
|
29
|
+
* `"auto"`, the default, replays a clip whose file exists and records one
|
|
30
|
+
* whose file does not. `"record"` always records, `"replay"` fails a clip
|
|
31
|
+
* without a file, and `"live"` leaves every request to the network.
|
|
32
|
+
*/
|
|
33
|
+
mode?: ReplayMode;
|
|
34
|
+
/** Where the files go. Default `replay/` beside the config. Commit it. */
|
|
35
|
+
dir?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Responses the page reads over time, an event stream or lines of JSON,
|
|
38
|
+
* delivered one event or one line every `chunkMs` of the page's clock, so
|
|
39
|
+
* the same piece lands on the same frame every run.
|
|
40
|
+
*/
|
|
41
|
+
pace?: {
|
|
42
|
+
url: ReplayPattern;
|
|
43
|
+
chunkMs: number;
|
|
44
|
+
}[];
|
|
45
|
+
/** Query parameters left out of the file and of the match, by name. */
|
|
46
|
+
redact?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* What a request the replay does not hold does: `"fail"`, the default,
|
|
49
|
+
* fails the clip and names it; `"live"` sends it to the network and warns.
|
|
50
|
+
*/
|
|
51
|
+
onMissing?: "fail" | "live";
|
|
52
|
+
};
|
|
53
|
+
export type ResolvedReplay = {
|
|
54
|
+
record: RegExp[];
|
|
55
|
+
mode: ReplayMode;
|
|
56
|
+
dir: string;
|
|
57
|
+
pace: {
|
|
58
|
+
url: RegExp;
|
|
59
|
+
chunkMs: number;
|
|
60
|
+
}[];
|
|
61
|
+
redact: string[];
|
|
62
|
+
onMissing: "fail" | "live";
|
|
63
|
+
};
|
|
64
|
+
/** How old a replay may be before `check` says so. */
|
|
65
|
+
export declare const STALE_DAYS = 90;
|
|
66
|
+
/**
|
|
67
|
+
* A glob in Playwright's syntax as a regular expression, so the recorder and
|
|
68
|
+
* the script it puts in the page match a URL the same way: `**` crosses `/`,
|
|
69
|
+
* `*` does not, `{a,b}` is either, and every other character is itself, `?`
|
|
70
|
+
* included. Anchored at both ends, as Playwright anchors a glob.
|
|
71
|
+
*/
|
|
72
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
73
|
+
export declare function patternOf(pattern: ReplayPattern): RegExp;
|
|
74
|
+
/**
|
|
75
|
+
* The URL a request is known by in the file: without its fragment and
|
|
76
|
+
* without the query parameters `redact` names, which are gone from the file
|
|
77
|
+
* and so cannot be part of the match.
|
|
78
|
+
*/
|
|
79
|
+
export declare function urlKey(url: string, redact: readonly string[]): string;
|
|
80
|
+
type Header = {
|
|
81
|
+
name: string;
|
|
82
|
+
value: string;
|
|
83
|
+
};
|
|
84
|
+
type HarContent = {
|
|
85
|
+
size: number;
|
|
86
|
+
mimeType: string;
|
|
87
|
+
text?: string;
|
|
88
|
+
encoding?: "base64";
|
|
89
|
+
/** A binary body, under the replay's `bodies/`, by its SHA-256. */
|
|
90
|
+
_file?: string;
|
|
91
|
+
};
|
|
92
|
+
export type HarEntry = {
|
|
93
|
+
startedDateTime: string;
|
|
94
|
+
time: number;
|
|
95
|
+
request: {
|
|
96
|
+
method: string;
|
|
97
|
+
url: string;
|
|
98
|
+
httpVersion: string;
|
|
99
|
+
headers: Header[];
|
|
100
|
+
queryString: Header[];
|
|
101
|
+
cookies: [];
|
|
102
|
+
headersSize: -1;
|
|
103
|
+
bodySize: number;
|
|
104
|
+
postData?: {
|
|
105
|
+
mimeType: string;
|
|
106
|
+
text: string;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
response: {
|
|
110
|
+
status: number;
|
|
111
|
+
statusText: string;
|
|
112
|
+
httpVersion: string;
|
|
113
|
+
headers: Header[];
|
|
114
|
+
cookies: [];
|
|
115
|
+
content: HarContent;
|
|
116
|
+
redirectURL: string;
|
|
117
|
+
headersSize: -1;
|
|
118
|
+
bodySize: number;
|
|
119
|
+
};
|
|
120
|
+
cache: Record<string, never>;
|
|
121
|
+
timings: {
|
|
122
|
+
send: number;
|
|
123
|
+
wait: number;
|
|
124
|
+
receive: number;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export type Har = {
|
|
128
|
+
log: {
|
|
129
|
+
version: "1.2";
|
|
130
|
+
creator: {
|
|
131
|
+
name: string;
|
|
132
|
+
version: string;
|
|
133
|
+
};
|
|
134
|
+
comment: string;
|
|
135
|
+
_versioncam: {
|
|
136
|
+
clip: string;
|
|
137
|
+
recorded: string;
|
|
138
|
+
recorder: string;
|
|
139
|
+
};
|
|
140
|
+
entries: HarEntry[];
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
export declare function sha256(bytes: Uint8Array): string;
|
|
144
|
+
/**
|
|
145
|
+
* One recorded exchange, as the file keeps it: the secrets and the redacted
|
|
146
|
+
* parameters gone, a text body in the entry, a binary one handed to `store`
|
|
147
|
+
* and named here by its hash.
|
|
148
|
+
*/
|
|
149
|
+
export declare function entryOf(exchange: {
|
|
150
|
+
method: string;
|
|
151
|
+
url: string;
|
|
152
|
+
requestHeaders: Record<string, string>;
|
|
153
|
+
postData: string | null;
|
|
154
|
+
status: number;
|
|
155
|
+
statusText: string;
|
|
156
|
+
responseHeaders: Record<string, string>;
|
|
157
|
+
body: Uint8Array;
|
|
158
|
+
}, options: {
|
|
159
|
+
redact: readonly string[];
|
|
160
|
+
recorded: string;
|
|
161
|
+
store: (hash: string, bytes: Uint8Array) => void;
|
|
162
|
+
}): HarEntry;
|
|
163
|
+
/**
|
|
164
|
+
* The entries of a replay, by what a request is known by, each list in the
|
|
165
|
+
* order the requests were made, and served in that order: the third poll of
|
|
166
|
+
* a job's status gets the third answer the job gave, and every poll after
|
|
167
|
+
* the last gets the last. Among answers to one URL, one recorded for the
|
|
168
|
+
* same request body is taken first, for an app that posts several queries
|
|
169
|
+
* to one endpoint in an order that can vary.
|
|
170
|
+
*/
|
|
171
|
+
export declare class ReplayBook {
|
|
172
|
+
private readonly byKey;
|
|
173
|
+
private readonly used;
|
|
174
|
+
private readonly redact;
|
|
175
|
+
constructor(entries: readonly HarEntry[], redact: readonly string[]);
|
|
176
|
+
answer(method: string, url: string, postData: string | null): HarEntry | null;
|
|
177
|
+
}
|
|
178
|
+
/** The file a clip's replay is in. */
|
|
179
|
+
export declare function replayFile(dir: string, clipId: string): string;
|
|
180
|
+
export declare function readReplay(file: string): Har;
|
|
181
|
+
/** What `doctor` and `check` say about a clip's replay. */
|
|
182
|
+
export declare function describeReplay(dir: string, clipId: string, now?: Date): {
|
|
183
|
+
file: string;
|
|
184
|
+
exists: false;
|
|
185
|
+
} | {
|
|
186
|
+
file: string;
|
|
187
|
+
exists: true;
|
|
188
|
+
requests: number;
|
|
189
|
+
recorded: string;
|
|
190
|
+
ageDays: number;
|
|
191
|
+
recorder: string;
|
|
192
|
+
};
|
|
193
|
+
/** Which mode a clip runs in, once the file's presence is known. */
|
|
194
|
+
export declare function effectiveMode(mode: ReplayMode, fileExists: boolean): "record" | "replay" | "live";
|
|
195
|
+
/**
|
|
196
|
+
* A clip's replay while it runs: the routes it installs, what it recorded,
|
|
197
|
+
* what it could not answer, and how many answers are still on their way.
|
|
198
|
+
*/
|
|
199
|
+
export declare class ClipReplay {
|
|
200
|
+
readonly mode: "record" | "replay" | "live";
|
|
201
|
+
readonly file: string;
|
|
202
|
+
/** Requests the replay matched and did not hold, as "METHOD url". */
|
|
203
|
+
readonly misses: string[];
|
|
204
|
+
/** Requests sent live because the replay did not hold them (`onMissing: "live"`). */
|
|
205
|
+
readonly wentLive: string[];
|
|
206
|
+
private readonly recorded;
|
|
207
|
+
private readonly bodies;
|
|
208
|
+
private readonly book;
|
|
209
|
+
private readonly when;
|
|
210
|
+
private inFlight;
|
|
211
|
+
private waiters;
|
|
212
|
+
private readonly config;
|
|
213
|
+
private readonly clipId;
|
|
214
|
+
private readonly recorder;
|
|
215
|
+
constructor(config: ResolvedReplay, clipId: string, recorder: string, override?: ReplayMode);
|
|
216
|
+
/** The page-side script that paces streamed bodies, or null when nothing is paced. */
|
|
217
|
+
pacingScript(): string | null;
|
|
218
|
+
/** Install the routes. Before the page is opened, so its first request is caught. */
|
|
219
|
+
install(context: BrowserContext): Promise<void>;
|
|
220
|
+
/**
|
|
221
|
+
* Resolves once every request the replay has taken is answered and its body
|
|
222
|
+
* is in the page. The driver waits on it after each tick, so a request made
|
|
223
|
+
* in a frame is answered within that frame on every run, however long the
|
|
224
|
+
* network or the disk took: a tile arrives on the frame it was asked for.
|
|
225
|
+
*/
|
|
226
|
+
idle(timeoutMs?: number): Promise<void>;
|
|
227
|
+
private settled;
|
|
228
|
+
private handle;
|
|
229
|
+
private record;
|
|
230
|
+
private serve;
|
|
231
|
+
/**
|
|
232
|
+
* What stops the clip: a request the replay was asked for and did not
|
|
233
|
+
* hold. Null when there is none.
|
|
234
|
+
*/
|
|
235
|
+
failure(): string | null;
|
|
236
|
+
/**
|
|
237
|
+
* Write what was recorded. Only for a clip that passed: a replay of half a
|
|
238
|
+
* clip would serve a later run a different app than the one it shows.
|
|
239
|
+
*/
|
|
240
|
+
write(): void;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Deliver a paced response a piece at a time on the page's own clock.
|
|
244
|
+
*
|
|
245
|
+
* The recorder cannot stream a response into the page: Playwright fulfils a
|
|
246
|
+
* request whole. So the page does it. For a URL the config paces, `fetch`
|
|
247
|
+
* hands back a response whose body is a stream that gives out one event of
|
|
248
|
+
* an event stream, or one line of anything else, every `chunkMs`, on the
|
|
249
|
+
* page's `setTimeout`, which the recorder's clock advances a frame at a
|
|
250
|
+
* time; and `EventSource` dispatches one event every `chunkMs` the same
|
|
251
|
+
* way. The same piece therefore lands on the same frame every run, at any
|
|
252
|
+
* frame rate. `window.__recorderReplay.pending` counts the bodies still
|
|
253
|
+
* arriving, and a settle waits for it to reach none.
|
|
254
|
+
*
|
|
255
|
+
* An `EventSource` whose stream has ended reports an error and stays
|
|
256
|
+
* closed, instead of reconnecting as a live one would: a replay would only
|
|
257
|
+
* answer it with the same stream again.
|
|
258
|
+
*/
|
|
259
|
+
export declare function pacedReplayScript(pace: readonly {
|
|
260
|
+
url: RegExp;
|
|
261
|
+
chunkMs: number;
|
|
262
|
+
}[]): string;
|
|
263
|
+
export {};
|