@crouton-kit/capture 1.4.2 → 1.4.11

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.
Files changed (110) hide show
  1. package/README.md +187 -0
  2. package/bin/capture +31278 -7312
  3. package/package.json +3 -1
  4. package/src/capture.ts +21 -18
  5. package/src/cdp/args.ts +269 -49
  6. package/src/cdp/bridge/server.ts +6 -5
  7. package/src/cdp/bridge/spawn.ts +41 -97
  8. package/src/cdp/commands/bridge-serve.ts +11 -21
  9. package/src/cdp/commands/cdp.ts +44 -22
  10. package/src/cdp/commands/heap/census.ts +61 -0
  11. package/src/cdp/commands/heap/common.ts +77 -0
  12. package/src/cdp/commands/heap/diff.ts +92 -0
  13. package/src/cdp/commands/heap/index.ts +38 -0
  14. package/src/cdp/commands/heap/objects.ts +42 -0
  15. package/src/cdp/commands/heap/retainers.ts +52 -0
  16. package/src/cdp/commands/heap/snapshot.ts +94 -0
  17. package/src/cdp/commands/lib.ts +9 -17
  18. package/src/cdp/commands/lighthouse.ts +414 -0
  19. package/src/cdp/commands/measure/census.ts +2 -1
  20. package/src/cdp/commands/measure/check.ts +70 -16
  21. package/src/cdp/commands/measure/diff.ts +1 -2
  22. package/src/cdp/commands/measure/explain.ts +44 -7
  23. package/src/cdp/commands/measure/index.ts +24 -32
  24. package/src/cdp/commands/measure/map-ax.ts +2 -1
  25. package/src/cdp/commands/measure/map-focus.ts +2 -1
  26. package/src/cdp/commands/measure/map-layers.ts +5 -3
  27. package/src/cdp/commands/measure/map-scroll.ts +2 -1
  28. package/src/cdp/commands/measure/snap.ts +5 -4
  29. package/src/cdp/commands/measure/sweep.ts +16 -11
  30. package/src/cdp/commands/measure/text.ts +147 -0
  31. package/src/cdp/commands/motion/index.ts +10 -18
  32. package/src/cdp/commands/motion/jank.ts +13 -22
  33. package/src/cdp/commands/motion/mask.ts +10 -9
  34. package/src/cdp/commands/motion/rec.ts +228 -26
  35. package/src/cdp/commands/motion/timeline.ts +1 -1
  36. package/src/cdp/commands/page/click.ts +15 -5
  37. package/src/cdp/commands/page/elements.ts +43 -11
  38. package/src/cdp/commands/page/exec.ts +53 -18
  39. package/src/cdp/commands/page/index.ts +20 -20
  40. package/src/cdp/commands/page/inspect.ts +338 -0
  41. package/src/cdp/commands/page/navigate.ts +83 -38
  42. package/src/cdp/commands/page/repeat.ts +176 -0
  43. package/src/cdp/commands/page/scroll.ts +10 -5
  44. package/src/cdp/commands/page/shot.ts +66 -31
  45. package/src/cdp/commands/page/type.ts +6 -2
  46. package/src/cdp/commands/perf/index.ts +32 -0
  47. package/src/cdp/commands/perf/insights.ts +214 -0
  48. package/src/cdp/commands/perf/trace.ts +235 -0
  49. package/src/cdp/commands/perf/vitals.ts +76 -0
  50. package/src/cdp/commands/session/collectors.ts +54 -0
  51. package/src/cdp/commands/stub.ts +10 -0
  52. package/src/cdp/commands/tab/close.ts +2 -2
  53. package/src/cdp/commands/tab/index.ts +16 -23
  54. package/src/cdp/commands/tab/launch.ts +1 -1
  55. package/src/cdp/commands/tab/mock/index.ts +24 -0
  56. package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
  57. package/src/cdp/commands/tab/mock/start.ts +65 -0
  58. package/src/cdp/commands/tab/mock/stop.ts +39 -0
  59. package/src/cdp/commands/tab/network.ts +22 -13
  60. package/src/cdp/commands/tab/reset.ts +11 -12
  61. package/src/cdp/connection.ts +19 -39
  62. package/src/cdp/detect.ts +27 -7
  63. package/src/cdp/dispatch.ts +25 -4
  64. package/src/cdp/domains.ts +2 -2
  65. package/src/cdp/har-recorder.ts +0 -1
  66. package/src/cdp/heap-snapshot.ts +647 -0
  67. package/src/cdp/host/client.ts +22 -0
  68. package/src/cdp/host/collector.ts +74 -0
  69. package/src/cdp/host/collectors/heap.ts +79 -0
  70. package/src/cdp/host/collectors/intercept.ts +247 -0
  71. package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
  72. package/src/cdp/host/collectors/trace.ts +108 -0
  73. package/src/cdp/host/core.ts +217 -0
  74. package/src/cdp/host/handle.ts +47 -0
  75. package/src/cdp/host/held-client.ts +91 -0
  76. package/src/cdp/host/kinds.ts +97 -0
  77. package/src/cdp/host/lifecycle.ts +123 -0
  78. package/src/cdp/host/server.ts +88 -0
  79. package/src/cdp/measure/check.ts +181 -16
  80. package/src/cdp/measure/collectors/hittest.ts +16 -10
  81. package/src/cdp/measure/collectors/scroll.ts +15 -7
  82. package/src/cdp/measure/collectors/style-provenance.ts +11 -0
  83. package/src/cdp/measure/collectors/styles.ts +4 -4
  84. package/src/cdp/measure/explain.ts +14 -2
  85. package/src/cdp/measure/map-layers.ts +52 -8
  86. package/src/cdp/measure/snapshot.ts +5 -3
  87. package/src/cdp/measure/sweep.ts +29 -6
  88. package/src/cdp/measure/text.ts +227 -0
  89. package/src/cdp/motion/jank.ts +29 -83
  90. package/src/cdp/motion/mask.ts +18 -9
  91. package/src/cdp/motion/recorder.ts +86 -731
  92. package/src/cdp/scope-lock.ts +9 -9
  93. package/src/cdp/screenshot.ts +51 -21
  94. package/src/cdp/timing.ts +5 -5
  95. package/src/cdp/trace-analysis.ts +320 -0
  96. package/src/cdp/types.ts +31 -1
  97. package/src/cdp.ts +4 -6
  98. package/src/har-manager.ts +2 -4
  99. package/src/interact.ts +50 -23
  100. package/src/output/artifact.ts +49 -5
  101. package/src/output/render.ts +60 -9
  102. package/src/output/selection.ts +6 -0
  103. package/src/session/artifacts.ts +122 -12
  104. package/src/session/commands.ts +348 -113
  105. package/src/session/coordinator.ts +0 -93
  106. package/src/session/har-redact.ts +29 -23
  107. package/src/session-context.ts +13 -9
  108. package/src/cdp/bridge/protocol.ts +0 -224
  109. package/src/cdp/recorder-client.ts +0 -326
  110. package/src/cdp/tab-lock.ts +0 -132
@@ -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
- let captureRootIdentity: Identity | undefined;
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 verifyRoot(actual: Identity): void { if (captureRootIdentity) { if (!sameIdentity(captureRootIdentity, actual)) throw new Error('capture root changed since it was first established'); } else captureRootIdentity = actual; }
129
- function rootOrAncestor(current: string): boolean { return current === CAPTURE_ROOT || CAPTURE_ROOT.startsWith(`${current}${path.sep}`); }
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 privateComponent = current === CAPTURE_ROOT || current.startsWith(`${CAPTURE_ROOT}${path.sep}`);
144
- const darwinVarAlias = process.platform === 'darwin' && current === '/var' && CAPTURE_ROOT.startsWith('/var/');
145
- const hookOperation: ArtifactHookOperation = current === CAPTURE_ROOT || darwinVarAlias ? 'root-bootstrap' : operationKind;
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 a kernel-owned alias for /private/var. It is the only
160
- // host alias accepted before the configured root; all user-controlled components
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() && darwinVarAlias) {
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 === CAPTURE_ROOT) { verifyRoot(expected); hook('root-bootstrap', 'afterRootPinned', current, component); }
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 { const resolved = path.resolve(targetPath); const relative = path.relative(CAPTURE_ROOT, resolved); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`artifact path escapes capture root ${CAPTURE_ROOT}: ${targetPath}`); return resolved; }
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;