@farmslot/adapter-web 0.4.0 → 0.5.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 CHANGED
@@ -6,6 +6,16 @@ All notable changes to `@farmslot/adapter-web` are tracked here.
6
6
 
7
7
  - Active-development baseline; add user-facing changes here before release or package publication.
8
8
 
9
+ ## 0.5.0 - 2026-10-08
10
+
11
+ - `launchBrowser` takes an optional `progress` callback for the caller's stage: starting the browser, waiting for the CDP listener (with the attempt count, about every 5 s), loading the extension over CDP and opening the start window. The `[launch]` log lines are unchanged.
12
+ - Publish with adapter-sdk 0.7.0 and recipe-runner 0.26.0 so consumers share one adapter-sdk copy.
13
+
14
+ ## 0.4.1 - 2026-10-07
15
+
16
+ - `network-observer`: `createExtensionNetworkObserver` takes an optional `extensionId`, the extension to capture. Without it the observer still takes the first extension with a target, which in a branded Chrome can be a component or policy extension listed before the one under test.
17
+ - Publish with adapter-sdk 0.6.0 and recipe-runner 0.26.0.
18
+
9
19
  ## 0.4.0 - 2026-10-06
10
20
 
11
21
  - Add `network-observer` (`createExtensionNetworkObserver({ cdpPort, runtimeDir })`) and `performance-observer` (`createExtensionPerformanceBackend({ cdpPort, extensionId, uiPaths, kind, platform, markerPrefix })`), the Extension network and performance observers moved from mm-harness. The network observer attaches to every target of the loaded extension and feeds their Network events to a `@farmslot/recipe-runner/cdp-broker`; it returns an `@farmslot/adapter-sdk` `NetworkCaptureBackend`. The performance observer traces the extension page found at the first of `uiPaths` with exactly one open renderer, through `@farmslot/recipe-runner/runtime/cdp-trace`. Both run on `connectBrowserCdp`; `connectTimeoutMs` and `commandTimeoutMs` default to 10 s. The package now depends on `@farmslot/recipe-runner` and `@farmslot/adapter-sdk`.
@@ -56,6 +56,16 @@ export type LaunchBrowserOptions = {
56
56
  * Progress lines (default stderr).
57
57
  */
58
58
  log?: ((line: string) => void) | undefined;
59
+ /**
60
+ * What the launch is waiting for, for a caller's stage handle (`stage.progress`).
61
+ */
62
+ progress?: ((progress: {
63
+ message?: string;
64
+ waitingFor?: string;
65
+ current?: number;
66
+ total?: number;
67
+ unit?: string;
68
+ }) => void) | undefined;
59
69
  };
60
70
  /**
61
71
  * @typedef {object} LaunchBrowserOptions
@@ -77,6 +87,8 @@ export type LaunchBrowserOptions = {
77
87
  * @property {string} [rerunCommand] Command named in "Next:" hints.
78
88
  * @property {number} [focusSettleMs] macOS: wait before the one focus check (default 1000).
79
89
  * @property {(line: string) => void} [log] Progress lines (default stderr).
90
+ * @property {(progress: { message?: string, waitingFor?: string, current?: number, total?: number, unit?: string }) => void} [progress]
91
+ * What the launch is waiting for, for a caller's stage handle (`stage.progress`).
80
92
  */
81
93
  /**
82
94
  * @param {LaunchBrowserOptions} options
@@ -1,12 +1,15 @@
1
1
  /**
2
- * @param {{ cdpPort: number, runtimeDir: string, connectTimeoutMs?: number, commandTimeoutMs?: number }} options
2
+ * @param {{ cdpPort: number, runtimeDir: string, extensionId?: string, connectTimeoutMs?: number, commandTimeoutMs?: number }} options
3
3
  * `runtimeDir` keys the broker socket; the timeouts bound the CDP connection
4
- * and every command, broker call and capture control.
4
+ * and every command, broker call and capture control. `extensionId` names the
5
+ * extension to capture; without it, the first extension with a target is
6
+ * used, which can be a component or policy extension in a branded Chrome.
5
7
  * @returns {Promise<import('@farmslot/adapter-sdk').NetworkCaptureBackend>}
6
8
  */
7
- export function createExtensionNetworkObserver({ cdpPort, runtimeDir, connectTimeoutMs, commandTimeoutMs, }: {
9
+ export function createExtensionNetworkObserver({ cdpPort, runtimeDir, extensionId: requestedExtensionId, connectTimeoutMs, commandTimeoutMs, }: {
8
10
  cdpPort: number;
9
11
  runtimeDir: string;
12
+ extensionId?: string;
10
13
  connectTimeoutMs?: number;
11
14
  commandTimeoutMs?: number;
12
15
  }): Promise<import("@farmslot/adapter-sdk").NetworkCaptureBackend>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farmslot/adapter-web",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -79,7 +79,7 @@
79
79
  "prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/adapter-web"
80
80
  },
81
81
  "dependencies": {
82
- "@farmslot/adapter-sdk": "0.5.1",
82
+ "@farmslot/adapter-sdk": "0.7.0",
83
83
  "@farmslot/recipe-runner": "0.26.0",
84
84
  "ws": "^8.18.0"
85
85
  },
@@ -78,6 +78,8 @@ const BROWSER_RESOLVER_CLI = require.resolve('./browser-resolver.cjs');
78
78
  * @property {string} [rerunCommand] Command named in "Next:" hints.
79
79
  * @property {number} [focusSettleMs] macOS: wait before the one focus check (default 1000).
80
80
  * @property {(line: string) => void} [log] Progress lines (default stderr).
81
+ * @property {(progress: { message?: string, waitingFor?: string, current?: number, total?: number, unit?: string }) => void} [progress]
82
+ * What the launch is waiting for, for a caller's stage handle (`stage.progress`).
81
83
  */
82
84
 
83
85
  /**
@@ -117,6 +119,7 @@ function normalizeOptions(options) {
117
119
  if (!opts[key]) throw new Error(`Missing --${flag}`);
118
120
  }
119
121
  opts.log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
122
+ opts.progress = opts.progress ?? (() => {});
120
123
  return opts;
121
124
  }
122
125
 
@@ -254,12 +257,13 @@ function runLaunch(opts, acquireLock) {
254
257
  try {
255
258
  try {
256
259
  beginDetachedLaunch(cdpPort, profile, activeValidationLease);
260
+ opts.progress({ message: 'starting the browser' });
257
261
  if (application) {
258
262
  execFileSync('open', macBackgroundOpenArgs(application, chromeArgs), {
259
263
  env: sanitizedChildEnv(),
260
264
  stdio: ['ignore', logFd, logFd],
261
265
  });
262
- browserPid = waitForOwnedCdpPid(cdpPort, ownedBrowser);
266
+ browserPid = waitForOwnedCdpPid(cdpPort, ownedBrowser, opts.progress);
263
267
  if (browserPid === null) {
264
268
  stopProfileProcessesSync(profile, { waitForAppearanceMs: 400 });
265
269
  throw ownedListenerMissing();
@@ -271,7 +275,7 @@ function runLaunch(opts, acquireLock) {
271
275
  stdio: ['ignore', logFd, logFd],
272
276
  });
273
277
  child.unref();
274
- browserPid = waitForOwnedCdpPid(cdpPort, ownedBrowser);
278
+ browserPid = waitForOwnedCdpPid(cdpPort, ownedBrowser, opts.progress);
275
279
  if (browserPid === null) {
276
280
  stopProfileProcessesSync(profile, { extraPids: child.pid ? [child.pid] : [] });
277
281
  throw ownedListenerMissing();
@@ -342,6 +346,7 @@ function runLaunch(opts, acquireLock) {
342
346
  // Without it the slot has no window at all, so a failure fails the launch:
343
347
  // the owned browser is stopped (its markers kept if it cannot be).
344
348
  function openBackgroundWindow(opts, pid, url, clearLaunchMarkers) {
349
+ opts.progress({ message: 'opening the start window' });
345
350
  const result = spawnSync(
346
351
  process.execPath,
347
352
  [BROWSER_RESOLVER_CLI, 'open-window', '--port', String(opts.cdpPort), '--url', url],
@@ -399,6 +404,7 @@ function writeBrowserResolution(runtime, resolution) {
399
404
  }
400
405
 
401
406
  function loadExtensionOverCdp(opts, pid, initialUrl, clearLaunchMarkers) {
407
+ opts.progress({ message: 'loading the extension over CDP' });
402
408
  const result = spawnSync(
403
409
  process.execPath,
404
410
  [
@@ -495,10 +501,20 @@ function macApplicationForExecutable(executable) {
495
501
  return null;
496
502
  }
497
503
 
498
- function waitForOwnedCdpPid(port, ownedBrowser) {
499
- for (let i = 0; i < 300; i += 1) {
504
+ function waitForOwnedCdpPid(port, ownedBrowser, progress) {
505
+ const attempts = 300;
506
+ for (let i = 0; i < attempts; i += 1) {
500
507
  const pid = cdpListenerPids(port).find(ownedBrowser);
501
508
  if (pid !== undefined) return pid;
509
+ // Every 50 attempts (about 5 s): enough to show the wait is moving.
510
+ if (i > 0 && i % 50 === 0) {
511
+ progress({
512
+ waitingFor: `an owned CDP listener on 127.0.0.1:${port}`,
513
+ current: i,
514
+ total: attempts,
515
+ unit: 'attempts',
516
+ });
517
+ }
502
518
  spawnSync('sleep', ['0.1']);
503
519
  }
504
520
  return null;
@@ -27,14 +27,17 @@ const asRecord = (value) =>
27
27
  value && typeof value === 'object' && !Array.isArray(value) ? value : {};
28
28
 
29
29
  /**
30
- * @param {{ cdpPort: number, runtimeDir: string, connectTimeoutMs?: number, commandTimeoutMs?: number }} options
30
+ * @param {{ cdpPort: number, runtimeDir: string, extensionId?: string, connectTimeoutMs?: number, commandTimeoutMs?: number }} options
31
31
  * `runtimeDir` keys the broker socket; the timeouts bound the CDP connection
32
- * and every command, broker call and capture control.
32
+ * and every command, broker call and capture control. `extensionId` names the
33
+ * extension to capture; without it, the first extension with a target is
34
+ * used, which can be a component or policy extension in a branded Chrome.
33
35
  * @returns {Promise<import('@farmslot/adapter-sdk').NetworkCaptureBackend>}
34
36
  */
35
37
  async function createExtensionNetworkObserver({
36
38
  cdpPort,
37
39
  runtimeDir,
40
+ extensionId: requestedExtensionId,
38
41
  connectTimeoutMs = DEFAULT_TIMEOUT_MS,
39
42
  commandTimeoutMs = DEFAULT_TIMEOUT_MS,
40
43
  }) {
@@ -133,7 +136,7 @@ async function createExtensionNetworkObserver({
133
136
  await send('Target.setDiscoverTargets', { discover: true });
134
137
  const targetInfos = asRecord(await send('Target.getTargets')).targetInfos;
135
138
  const targets = Array.isArray(targetInfos) ? targetInfos : [];
136
- extensionId = extensionIdFromCdpTargets(targets);
139
+ extensionId = requestedExtensionId || extensionIdFromCdpTargets(targets);
137
140
  if (!extensionId) {
138
141
  connection.close();
139
142
  throw new Error('Extension CDP browser or loaded extension target is unavailable.');
@@ -141,7 +144,11 @@ async function createExtensionNetworkObserver({
141
144
  await Promise.all(targets.map(asBrowserCdpTarget).filter(Boolean).map(attachTarget));
142
145
  if (targetSessions.size === 0) {
143
146
  connection.close();
144
- throw new Error('Extension CDP exposes no attachable extension targets.');
147
+ throw new Error(
148
+ requestedExtensionId
149
+ ? `Extension ${extensionId} has no attachable CDP targets (service worker stopped and no extension page open?).`
150
+ : 'Extension CDP exposes no attachable extension targets.',
151
+ );
145
152
  }
146
153
 
147
154
  const sessions = new Map([[DEVICE_ID, { brokerReady: true }]]);