@crouton-kit/capture 1.4.2 → 1.4.10
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/README.md +178 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
package/src/session/artifacts.ts
CHANGED
|
@@ -107,7 +107,10 @@ function combineFailure(primary: unknown, secondary: unknown, message: string):
|
|
|
107
107
|
// The root is established lazily by the first pinned artifact transaction: every
|
|
108
108
|
// transaction traverses it, pinning its identity on first use and verifying it
|
|
109
109
|
// on every later traversal. Non-artifact invocations touch no filesystem state.
|
|
110
|
-
|
|
110
|
+
const artifactRoots = new Set<string>([CAPTURE_ROOT]);
|
|
111
|
+
const artifactRootIdentities = new Map<string, Identity>();
|
|
112
|
+
const ARTIFACT_ROOT_REGISTRY = path.join(CAPTURE_ROOT, '.artifact-roots.ndjson');
|
|
113
|
+
let loadingArtifactRoots = false;
|
|
111
114
|
let cwdPinned = false;
|
|
112
115
|
|
|
113
116
|
export interface SnapMeta { id: string; url: string | null; viewport: string | null; settled: boolean; capturedAt: string; }
|
|
@@ -125,8 +128,57 @@ function writeAll(fd: number, data: string | Buffer, role: Extract<ArtifactSysca
|
|
|
125
128
|
const wrote = fs.writeSync(fd, bytes, off, prescribed); if (!Number.isSafeInteger(wrote) || wrote <= 0 || wrote > prescribed) throw new Error('short private artifact write'); off += wrote;
|
|
126
129
|
} }
|
|
127
130
|
function privateDirHere(): void { const fd = fs.openSync('.', fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW); try { fs.fchmodSync(fd, DIR_MODE); } finally { fs.closeSync(fd); } }
|
|
128
|
-
function
|
|
129
|
-
|
|
131
|
+
function loadRegisteredArtifactRoots(): void {
|
|
132
|
+
if (loadingArtifactRoots || !fs.existsSync(CAPTURE_ROOT)) return;
|
|
133
|
+
loadingArtifactRoots = true;
|
|
134
|
+
try {
|
|
135
|
+
let records: string;
|
|
136
|
+
try { records = readPrivateFile(ARTIFACT_ROOT_REGISTRY).toString('utf8'); }
|
|
137
|
+
catch (error) { if (isMissing(error)) return; throw error; }
|
|
138
|
+
for (const [index, line] of records.split('\n').entries()) {
|
|
139
|
+
if (!line) continue;
|
|
140
|
+
let value: unknown;
|
|
141
|
+
try { value = JSON.parse(line); } catch { throw new Error(`invalid artifact root registry record ${index + 1}`); }
|
|
142
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || typeof (value as Record<string, unknown>).root !== 'string') throw new Error(`invalid artifact root registry record ${index + 1}`);
|
|
143
|
+
const root = (value as { root: string }).root;
|
|
144
|
+
if (!path.isAbsolute(root) || path.resolve(root) !== root) throw new Error(`invalid artifact root registry record ${index + 1}`);
|
|
145
|
+
artifactRoots.add(root);
|
|
146
|
+
}
|
|
147
|
+
} finally { loadingArtifactRoots = false; }
|
|
148
|
+
}
|
|
149
|
+
function rootForPath(target: string): string | undefined {
|
|
150
|
+
return [...artifactRoots].find((root) => target === root || target.startsWith(`${root}${path.sep}`));
|
|
151
|
+
}
|
|
152
|
+
/** Every root selected with --artifact-dir in this capture root, including the default root. */
|
|
153
|
+
export function registeredArtifactRoots(): readonly string[] {
|
|
154
|
+
loadRegisteredArtifactRoots();
|
|
155
|
+
return [...artifactRoots];
|
|
156
|
+
}
|
|
157
|
+
function verifyRoot(root: string, actual: Identity): void {
|
|
158
|
+
const prior = artifactRootIdentities.get(root);
|
|
159
|
+
if (prior && !sameIdentity(prior, actual)) throw new Error('artifact root changed since it was first established');
|
|
160
|
+
artifactRootIdentities.set(root, actual);
|
|
161
|
+
}
|
|
162
|
+
function rootOrAncestor(current: string): boolean {
|
|
163
|
+
return [...artifactRoots].some((root) => current === root || root.startsWith(`${current}${path.sep}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Admit and durably record one caller-selected artifact root for future Capture invocations. */
|
|
167
|
+
export function registerArtifactRoot(root: string): string {
|
|
168
|
+
if (!path.isAbsolute(root)) throw new Error('--artifact-dir must be an absolute path');
|
|
169
|
+
const resolved = path.resolve(root);
|
|
170
|
+
loadRegisteredArtifactRoots();
|
|
171
|
+
if (artifactRoots.has(resolved)) return resolved;
|
|
172
|
+
artifactRoots.add(resolved);
|
|
173
|
+
try {
|
|
174
|
+
inPinnedDirectory(resolved, true, () => undefined, 'root-bootstrap');
|
|
175
|
+
appendNdjsonPrivate(ARTIFACT_ROOT_REGISTRY, { root: resolved });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
artifactRoots.delete(resolved);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
return resolved;
|
|
181
|
+
}
|
|
130
182
|
|
|
131
183
|
/** Performs only synchronous single-component operations while cwd is inode-pinned. */
|
|
132
184
|
function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: () => T, operationKind: ArtifactHookOperation = 'traversal'): T {
|
|
@@ -140,9 +192,10 @@ function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: (
|
|
|
140
192
|
const root = path.parse(absoluteDir).root; process.chdir(root); let current = root;
|
|
141
193
|
for (const component of absoluteDir.slice(root.length).split(path.sep).filter(Boolean)) {
|
|
142
194
|
assertName(component); current = path.join(current, component);
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
195
|
+
const artifactRoot = rootForPath(current);
|
|
196
|
+
const privateComponent = artifactRoot !== undefined;
|
|
197
|
+
const darwinSystemAlias = process.platform === 'darwin' && (current === '/var' || current === '/tmp') && [...artifactRoots].some((root) => root.startsWith(`${current}/`));
|
|
198
|
+
const hookOperation: ArtifactHookOperation = artifactRoots.has(current) || darwinSystemAlias ? 'root-bootstrap' : operationKind;
|
|
146
199
|
let before: fs.Stats;
|
|
147
200
|
try { before = fs.lstatSync(component); hook(hookOperation, 'afterComponentLstat', current, component); }
|
|
148
201
|
catch (error) {
|
|
@@ -156,11 +209,11 @@ function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: (
|
|
|
156
209
|
catch (createError) { if (errno(createError) !== 'EEXIST') throw createError; }
|
|
157
210
|
before = fs.lstatSync(component);
|
|
158
211
|
}
|
|
159
|
-
// Darwin exposes /var as
|
|
160
|
-
// host
|
|
161
|
-
// including the configured root itself must be real directories.
|
|
212
|
+
// Darwin exposes /var and /tmp as kernel-owned aliases under /private. They are
|
|
213
|
+
// the only host aliases accepted before a configured root; all user-controlled
|
|
214
|
+
// components including the configured root itself must be real directories.
|
|
162
215
|
// Pin and compare the followed vnode, not the alias directory entry.
|
|
163
|
-
if (before.isSymbolicLink() &&
|
|
216
|
+
if (before.isSymbolicLink() && darwinSystemAlias) {
|
|
164
217
|
const expected = identity(fs.statSync(component));
|
|
165
218
|
hook(hookOperation, 'beforeComponentChdir', current, component);
|
|
166
219
|
process.chdir(component);
|
|
@@ -172,7 +225,7 @@ function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: (
|
|
|
172
225
|
if (!before.isDirectory()) throw new Error(`refusing non-directory artifact component: ${component}`);
|
|
173
226
|
const expected = identity(before); hook(hookOperation, 'beforeComponentChdir', current, component); process.chdir(component); hook(hookOperation, 'afterComponentChdirBeforeIdentityCheck', current, component);
|
|
174
227
|
if (!sameIdentity(expected, identity(fs.statSync('.')))) throw new Error(`artifact directory changed while pinning: ${component}`);
|
|
175
|
-
if (current
|
|
228
|
+
if (artifactRoots.has(current)) { verifyRoot(current, expected); hook('root-bootstrap', 'afterRootPinned', current, component); }
|
|
176
229
|
if (privateComponent) privateDirHere();
|
|
177
230
|
}
|
|
178
231
|
hook(absoluteDir === CAPTURE_ROOT ? 'root-bootstrap' : operationKind, 'afterParentPinned', absoluteDir); result = operation();
|
|
@@ -186,7 +239,13 @@ function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: (
|
|
|
186
239
|
return result as T;
|
|
187
240
|
}
|
|
188
241
|
|
|
189
|
-
export function assertUnderCaptureRoot(targetPath: string): string {
|
|
242
|
+
export function assertUnderCaptureRoot(targetPath: string): string {
|
|
243
|
+
const resolved = path.resolve(targetPath);
|
|
244
|
+
loadRegisteredArtifactRoots();
|
|
245
|
+
const root = rootForPath(resolved);
|
|
246
|
+
if (!root || resolved === root) throw new Error(`artifact path escapes capture root or registered artifact root: ${targetPath}`);
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
190
249
|
function pinnedParent<T>(target: string, operation: (name: string) => T, create = true, operationKind: ArtifactHookOperation = 'traversal'): T { const resolved = assertUnderCaptureRoot(target); const name = path.basename(resolved); assertName(name); return inPinnedDirectory(path.dirname(resolved), create, () => operation(name), operationKind); }
|
|
191
250
|
export function ensurePrivateDir(dirPath: string): string { const resolved = assertUnderCaptureRoot(dirPath); inPinnedDirectory(resolved, true, () => undefined); return resolved; }
|
|
192
251
|
function openRegular(name: string, flags: number, create = false, onOpened?: (fd: number) => void, closeRole: Extract<ArtifactSyscallRole, 'artifact-temp-close' | 'lock-owner-close'> = 'artifact-temp-close', hookOperation: ArtifactHookOperation = 'final-file'): number {
|
|
@@ -214,6 +273,57 @@ export function readPrivateFile(filePath: string): Buffer { return pinnedParent(
|
|
|
214
273
|
export function openPrivateAppendFd(filePath: string): number {
|
|
215
274
|
return pinnedParent(filePath, name => openRegular(name, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT, true));
|
|
216
275
|
}
|
|
276
|
+
|
|
277
|
+
/** A streamed artifact payload stays invisible at `<name>.part` until this
|
|
278
|
+
* writer fsyncs and atomically publishes it as `<name>`. */
|
|
279
|
+
export interface PrivateChunkWriter {
|
|
280
|
+
write(data: string | Buffer): void;
|
|
281
|
+
commit(): number;
|
|
282
|
+
discard(): void;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function openPrivateChunkWriter(filePath: string): PrivateChunkWriter {
|
|
286
|
+
const target = assertUnderCaptureRoot(filePath);
|
|
287
|
+
const partial = `${target}.part`;
|
|
288
|
+
const fd = openPrivateAppendFd(partial);
|
|
289
|
+
let closed = false;
|
|
290
|
+
let finished = false;
|
|
291
|
+
let bytes = 0;
|
|
292
|
+
const close = (): void => {
|
|
293
|
+
if (closed) return;
|
|
294
|
+
fs.fsyncSync(fd);
|
|
295
|
+
closeDescriptor(fd, 'artifact-temp-close');
|
|
296
|
+
closed = true;
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
write(data) {
|
|
300
|
+
if (finished) throw new Error('chunk writer is already finalized');
|
|
301
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
302
|
+
writeAll(fd, buffer);
|
|
303
|
+
bytes += buffer.length;
|
|
304
|
+
},
|
|
305
|
+
commit() {
|
|
306
|
+
if (finished) return bytes;
|
|
307
|
+
close();
|
|
308
|
+
pinnedParent(target, name => {
|
|
309
|
+
const partName = `${name}.part`;
|
|
310
|
+
const part = fs.lstatSync(partName);
|
|
311
|
+
if (!part.isFile()) throw new Error(`chunk artifact is not a regular file: ${partName}`);
|
|
312
|
+
try { if (fs.lstatSync(name).isSymbolicLink()) throw new Error(`refusing to replace symlinked private artifact: ${name}`); }
|
|
313
|
+
catch (error) { if (!isMissing(error)) throw error; }
|
|
314
|
+
fs.renameSync(partName, name);
|
|
315
|
+
});
|
|
316
|
+
finished = true;
|
|
317
|
+
return bytes;
|
|
318
|
+
},
|
|
319
|
+
discard() {
|
|
320
|
+
if (finished) return;
|
|
321
|
+
try { close(); } catch { if (!closed) { try { fs.closeSync(fd); } catch {} closed = true; } }
|
|
322
|
+
try { unlinkPrivateFile(partial); } catch (error) { if (!isMissing(error)) throw error; }
|
|
323
|
+
finished = true;
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
217
327
|
export function appendPrivateFile(filePath: string, data: string | Buffer): void { pinnedParent(filePath, name => { const fd = openRegular(name, fs.constants.O_WRONLY | fs.constants.O_APPEND); let primary: unknown; try { writeAll(fd, data); syscall('artifact-temp-fsync', () => fs.fsyncSync(fd)); } catch (error) { primary = error; } try { closeDescriptor(fd, 'artifact-temp-close'); } catch (closeError) { if (primary) combineFailure(primary, closeError, 'private artifact append failed'); throw closeError; } if (primary) throw primary; }); }
|
|
218
328
|
function cleanupOwnedFile(name: string, expected?: Identity): void {
|
|
219
329
|
if (!expected) return;
|