@farmslot/adapter-web 0.17.7 → 0.18.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 +4 -0
- package/README.md +6 -0
- package/dist/launch-browser.d.cts +3 -3
- package/dist/plain-web.d.mts +17 -0
- package/package.json +5 -1
- package/src/launch-browser.cjs +19 -11
- package/src/plain-web.mjs +219 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,10 @@ 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.18.0 - 2026-11-05
|
|
10
|
+
|
|
11
|
+
- Run ordinary websites in an owned extension-free browser with shared CDP actions, screenshots and whole-run recording. Existing extension launches retain their lifecycle.
|
|
12
|
+
|
|
9
13
|
## 0.17.7 - 2026-11-04
|
|
10
14
|
|
|
11
15
|
- Check host-resolver blocking in an owned blank document so strict application CSP needs no harness-only source. Keep application HTTP and WebSocket blocking checks unchanged.
|
package/README.md
CHANGED
|
@@ -108,6 +108,12 @@ const binding = createWalletRequestBinding({
|
|
|
108
108
|
});
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Plain websites
|
|
112
|
+
|
|
113
|
+
`@farmslot/adapter-web/plain-web` exports `createPlainWebAdapter({ manifestPath, url, serverLabel })`. The project supplies its action catalog and a URL function using its selected ports. The runtime owns an isolated browser and reuses shared CDP actions, screenshots and whole-run recording. The project owns its application server.
|
|
114
|
+
|
|
115
|
+
Browser stop is scoped to the selected checkout's profile and retains the application server. This path needs no extension, signer or wallet policy and makes no assumptions about the application framework or page selectors.
|
|
116
|
+
|
|
111
117
|
## Web-dapp adapter
|
|
112
118
|
|
|
113
119
|
`@farmslot/adapter-web/web-dapp` (ESM) is the lifecycle of a web app under test whose dev server the slot owns. `createWebDappAdapter({ id, signerModule, cli, hooks })` returns an `@farmslot/adapter-sdk` `ProjectRuntime`: launch, stop, runtime status, doctor checks, logs and the wallet request-log findings. It runs one slot browser per slot, and a wallet host attached to it over CDP that records the wallet requests (and, with `signer=injected`, answers them with the strict wallet from `dapp`).
|
|
@@ -3,9 +3,9 @@ export type LaunchBrowserOptions = {
|
|
|
3
3
|
chromeBin: string;
|
|
4
4
|
profile: string;
|
|
5
5
|
/**
|
|
6
|
-
* Unpacked extension to load.
|
|
6
|
+
* Unpacked extension to load; omitted for a plain web page.
|
|
7
7
|
*/
|
|
8
|
-
extensionDir
|
|
8
|
+
extensionDir?: string | undefined;
|
|
9
9
|
/**
|
|
10
10
|
* Holds browser-resolution.json and the runtime identity.
|
|
11
11
|
*/
|
|
@@ -72,7 +72,7 @@ export type LaunchBrowserOptions = {
|
|
|
72
72
|
* @property {number} cdpPort
|
|
73
73
|
* @property {string} chromeBin
|
|
74
74
|
* @property {string} profile
|
|
75
|
-
* @property {string} extensionDir Unpacked extension to load.
|
|
75
|
+
* @property {string} [extensionDir] Unpacked extension to load; omitted for a plain web page.
|
|
76
76
|
* @property {string} runtimeDir Holds browser-resolution.json and the runtime identity.
|
|
77
77
|
* @property {string} chromeLog
|
|
78
78
|
* @property {string} chromePid
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runtime files and the browser profile always belong to the selected checkout. */
|
|
2
|
+
export function plainWebRuntimeDir(target: any): string;
|
|
3
|
+
/**
|
|
4
|
+
* Use existing browser ownership, CDP actions and whole-run recording for a plain website.
|
|
5
|
+
* @param {PlainWebAdapterConfig} config
|
|
6
|
+
* @returns {import('@farmslot/adapter-sdk').ProjectRuntime}
|
|
7
|
+
*/
|
|
8
|
+
export function createPlainWebAdapter(config: PlainWebAdapterConfig): import("@farmslot/adapter-sdk").ProjectRuntime;
|
|
9
|
+
/** Releases only the selected checkout's owned profile. The application server is retained. */
|
|
10
|
+
export function stopPlainWebBrowser(target: any): {
|
|
11
|
+
stopped: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type PlainWebAdapterConfig = {
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
url: (target: string) => string;
|
|
16
|
+
serverLabel?: string;
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/adapter-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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": {
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"./web-dapp": {
|
|
69
69
|
"types": "./dist/web-dapp/index.d.mts",
|
|
70
70
|
"default": "./src/web-dapp/index.mjs"
|
|
71
|
+
},
|
|
72
|
+
"./plain-web": {
|
|
73
|
+
"types": "./dist/plain-web.d.mts",
|
|
74
|
+
"default": "./src/plain-web.mjs"
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
"scripts": {
|
package/src/launch-browser.cjs
CHANGED
|
@@ -67,7 +67,7 @@ const BROWSER_RESOLVER_CLI = require.resolve('./browser-resolver.cjs');
|
|
|
67
67
|
* @property {number} cdpPort
|
|
68
68
|
* @property {string} chromeBin
|
|
69
69
|
* @property {string} profile
|
|
70
|
-
* @property {string} extensionDir Unpacked extension to load.
|
|
70
|
+
* @property {string} [extensionDir] Unpacked extension to load; omitted for a plain web page.
|
|
71
71
|
* @property {string} runtimeDir Holds browser-resolution.json and the runtime identity.
|
|
72
72
|
* @property {string} chromeLog
|
|
73
73
|
* @property {string} chromePid
|
|
@@ -114,7 +114,6 @@ function normalizeOptions(options) {
|
|
|
114
114
|
const flags = {
|
|
115
115
|
chromeBin: 'chrome-bin',
|
|
116
116
|
profile: 'profile',
|
|
117
|
-
extensionDir: 'extension-dir',
|
|
118
117
|
runtimeDir: 'runtime-dir',
|
|
119
118
|
chromeLog: 'chrome-log',
|
|
120
119
|
chromePid: 'chrome-pid',
|
|
@@ -122,6 +121,10 @@ function normalizeOptions(options) {
|
|
|
122
121
|
for (const [key, flag] of Object.entries(flags)) {
|
|
123
122
|
if (!opts[key]) throw new Error(`Missing --${flag}`);
|
|
124
123
|
}
|
|
124
|
+
if (!opts.extensionDir && !opts.stopOnly) {
|
|
125
|
+
if (!opts.startUrl || !/^https?:$/u.test(new URL(opts.startUrl).protocol))
|
|
126
|
+
throw new Error('Plain web launch requires an explicit HTTP(S) --start-url');
|
|
127
|
+
}
|
|
125
128
|
opts.log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
126
129
|
opts.progress = opts.progress ?? (() => {});
|
|
127
130
|
return opts;
|
|
@@ -135,7 +138,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
135
138
|
if (!opts.stopOnly) {
|
|
136
139
|
if (!fs.existsSync(opts.chromeBin))
|
|
137
140
|
throw new Error(`Chrome binary not found: ${opts.chromeBin}`);
|
|
138
|
-
if (!fs.existsSync(path.join(extensionDir, 'manifest.json'))) {
|
|
141
|
+
if (extensionDir && !fs.existsSync(path.join(extensionDir, 'manifest.json'))) {
|
|
139
142
|
throw new Error(
|
|
140
143
|
`Extension dist manifest not found: ${path.join(extensionDir, 'manifest.json')}`,
|
|
141
144
|
);
|
|
@@ -189,7 +192,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
189
192
|
// run's Chrome would hold the lock and the write fails.
|
|
190
193
|
if (opts.stopOnly) return { stopped: true };
|
|
191
194
|
try {
|
|
192
|
-
removeExtensionWorkerRegistrations(profile);
|
|
195
|
+
if (extensionDir) removeExtensionWorkerRegistrations(profile);
|
|
193
196
|
} catch (error) {
|
|
194
197
|
const detail = error instanceof Error ? error.message : String(error);
|
|
195
198
|
throw new Error(
|
|
@@ -221,7 +224,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
221
224
|
const initialUrl =
|
|
222
225
|
opts.startUrl || extensionPageUrl(extensionDir, opts.homePage) || 'chrome://extensions/';
|
|
223
226
|
const browser = launchBrowserResolution(opts.browserResolution, opts.chromeBin);
|
|
224
|
-
const loadsOverCdp = browser.extensionLoading === CDP_LOAD_UNPACKED;
|
|
227
|
+
const loadsOverCdp = Boolean(extensionDir) && browser.extensionLoading === CDP_LOAD_UNPACKED;
|
|
225
228
|
let cdpLoad = null;
|
|
226
229
|
let owner = null;
|
|
227
230
|
const runtimeNonce = createRuntimeIdentityNonce();
|
|
@@ -238,10 +241,14 @@ function runLaunch(opts, acquireLock) {
|
|
|
238
241
|
...isolatedProfileArgs(),
|
|
239
242
|
'--disable-default-apps',
|
|
240
243
|
'--disable-popup-blocking',
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
...(extensionDir
|
|
245
|
+
? [
|
|
246
|
+
'--disable-extensions-file-access-check',
|
|
247
|
+
'--disable-extensions-content-verification',
|
|
248
|
+
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
249
|
+
...extensionLaunchArgs(extensionDir, browser.extensionLoading),
|
|
250
|
+
]
|
|
251
|
+
: []),
|
|
245
252
|
]);
|
|
246
253
|
// Initial tab: the caller's URL when provided, else the extension home. The
|
|
247
254
|
// extension home avoids leaving disposable chrome://newtab/ or extensions
|
|
@@ -333,7 +340,7 @@ function runLaunch(opts, acquireLock) {
|
|
|
333
340
|
cdpLoad = loadExtensionOverCdp(opts, browserPid, initialUrl, clearLaunchMarkers);
|
|
334
341
|
else if (application) openBackgroundWindow(opts, browserPid, initialUrl, clearLaunchMarkers);
|
|
335
342
|
owner = cdpLoad?.owner ?? ownerIdentity(cdpPort, profile, browserPid, log);
|
|
336
|
-
for (const problem of pruneExtraHomeTabs(cdpPort, extensionDir, opts)) {
|
|
343
|
+
for (const problem of extensionDir ? pruneExtraHomeTabs(cdpPort, extensionDir, opts) : []) {
|
|
337
344
|
log(`[launch] home-tab pruning: ${problem}`);
|
|
338
345
|
}
|
|
339
346
|
} finally {
|
|
@@ -658,7 +665,8 @@ function processIsOwnedBrowser(pid, profile, extensionDir, extensionOwnerRoot) {
|
|
|
658
665
|
const command = processCommand(pid);
|
|
659
666
|
if (!command) return false;
|
|
660
667
|
if (command.includes(`--user-data-dir=${profile}`)) return true;
|
|
661
|
-
const ownerRoot =
|
|
668
|
+
const ownerRoot =
|
|
669
|
+
extensionDir && extensionOwnerRoot ? extensionOwnerRoot(path.resolve(extensionDir)) : null;
|
|
662
670
|
return Boolean(ownerRoot && command.includes(`--load-extension=${ownerRoot}`));
|
|
663
671
|
}
|
|
664
672
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// A regular website in a slot-owned browser. The project owns its application server.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
import { CdpWebPage, listCdpTargets } from '@farmslot/recipe-runner/runtime/cdp';
|
|
7
|
+
|
|
8
|
+
import { assertSameOwner, cdpOwner } from './browser-cdp.cjs';
|
|
9
|
+
import { resolveBrowser } from './browser-resolver.cjs';
|
|
10
|
+
import { removeRuntimeIdentity } from './chrome-args.cjs';
|
|
11
|
+
import { launchBrowser } from './launch-browser.cjs';
|
|
12
|
+
import { stopProfileProcessesSync } from './validation-process-ownership.cjs';
|
|
13
|
+
|
|
14
|
+
/** @typedef {{
|
|
15
|
+
* manifestPath: string,
|
|
16
|
+
* url: (target: string) => string,
|
|
17
|
+
* serverLabel?: string
|
|
18
|
+
* }} PlainWebAdapterConfig
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function port(env = process.env) {
|
|
22
|
+
const value = Number(env.RECIPE_CDP_PORT ?? env.CDP_PORT);
|
|
23
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535)
|
|
24
|
+
throw new Error('The plain web runtime requires the assigned --cdp-port.');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runtime files and the browser profile always belong to the selected checkout. */
|
|
29
|
+
export function plainWebRuntimeDir(target) {
|
|
30
|
+
return path.join(fs.realpathSync(target), '.agent', 'web');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function browserOptions(target, env = process.env) {
|
|
34
|
+
const runtimeDir = plainWebRuntimeDir(target);
|
|
35
|
+
return {
|
|
36
|
+
cdpPort: port(env),
|
|
37
|
+
runtimeDir,
|
|
38
|
+
profile: path.join(runtimeDir, 'profile'),
|
|
39
|
+
chromeLog: path.join(runtimeDir, 'browser.log'),
|
|
40
|
+
chromePid: path.join(runtimeDir, 'browser.pid'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function owned(target, env = process.env) {
|
|
45
|
+
const options = browserOptions(target, env);
|
|
46
|
+
return cdpOwner(options.cdpPort, options.profile);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Use existing browser ownership, CDP actions and whole-run recording for a plain website.
|
|
51
|
+
* @param {PlainWebAdapterConfig} config
|
|
52
|
+
* @returns {import('@farmslot/adapter-sdk').ProjectRuntime}
|
|
53
|
+
*/
|
|
54
|
+
export function createPlainWebAdapter(config) {
|
|
55
|
+
const hint = 'Prepare the selected slot, then launch its plain web browser.';
|
|
56
|
+
async function page(target, env = process.env) {
|
|
57
|
+
const owner = owned(target, env);
|
|
58
|
+
const file = path.join(plainWebRuntimeDir(target), 'page.json');
|
|
59
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
const selected = (await listCdpTargets('127.0.0.1', port(env))).find(
|
|
61
|
+
(entry) => entry.id === state.pageId && entry.type === 'page',
|
|
62
|
+
);
|
|
63
|
+
assertSameOwner(owner);
|
|
64
|
+
if (!selected) throw new Error(`The owned application page is unavailable. ${hint}`);
|
|
65
|
+
return selected;
|
|
66
|
+
}
|
|
67
|
+
/** @type {import('@farmslot/adapter-sdk').ProjectRuntime} */
|
|
68
|
+
const runtime = {
|
|
69
|
+
id: 'web',
|
|
70
|
+
sdkVersion: 1,
|
|
71
|
+
headless: false,
|
|
72
|
+
targets: ['web', 'cli'],
|
|
73
|
+
resolveSlotPorts() {},
|
|
74
|
+
async runtimeStatus(target) {
|
|
75
|
+
try {
|
|
76
|
+
await page(target);
|
|
77
|
+
const response = await fetch(config.url(target), { signal: AbortSignal.timeout(5000) });
|
|
78
|
+
await response.body?.cancel();
|
|
79
|
+
if (!response.ok) throw new Error(`Application returned HTTP ${response.status}.`);
|
|
80
|
+
return {
|
|
81
|
+
decision: 'ready',
|
|
82
|
+
reasons: [],
|
|
83
|
+
devServer: { label: config.serverLabel ?? 'website', status: 'up' },
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
decision: 'blocked',
|
|
88
|
+
reasons: [error instanceof Error ? error.message : String(error)],
|
|
89
|
+
nextAction: hint,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
devServer: {
|
|
94
|
+
label: 'browser',
|
|
95
|
+
describe: () => 'The owned website browser; the application server belongs to the project',
|
|
96
|
+
stop(target) {
|
|
97
|
+
stopPlainWebBrowser(target);
|
|
98
|
+
return {
|
|
99
|
+
kind: 'stopped',
|
|
100
|
+
status: 0,
|
|
101
|
+
summary: 'Website browser stopped; application server retained.',
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
logSources: (target) => [
|
|
106
|
+
{ label: 'Browser', path: path.join(plainWebRuntimeDir(target), 'browser.log') },
|
|
107
|
+
],
|
|
108
|
+
appLogSource: () => null,
|
|
109
|
+
hints: { launch: hint, relaunch: hint, runtimeProbeRecovery: () => hint },
|
|
110
|
+
harness: {
|
|
111
|
+
kind: 'built-in',
|
|
112
|
+
verify(target) {
|
|
113
|
+
return {
|
|
114
|
+
inProcess: async () => async (_args, io) => {
|
|
115
|
+
const status = await runtime.runtimeStatus(target);
|
|
116
|
+
if (status.decision !== 'ready') {
|
|
117
|
+
io.err(status.reasons.join('; '));
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
io.out('ready');
|
|
121
|
+
return 0;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
runtimeContext: { forbiddenFields: [] },
|
|
127
|
+
readiness: { captureProviders: ['cdp'], readyIndicator: () => 'ready' },
|
|
128
|
+
recording: {
|
|
129
|
+
async target(context) {
|
|
130
|
+
const owner = owned(context.projectRoot, context.env);
|
|
131
|
+
await page(context.projectRoot, context.env);
|
|
132
|
+
assertSameOwner(owner);
|
|
133
|
+
return { kind: 'pid', pid: owner.pid };
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async launch({ target, jsonOutput }) {
|
|
137
|
+
const url = config.url(target);
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
140
|
+
throw new Error('Expected an HTTP(S) application URL.');
|
|
141
|
+
const options = browserOptions(target);
|
|
142
|
+
const resolution = await resolveBrowser({
|
|
143
|
+
env: {
|
|
144
|
+
...process.env,
|
|
145
|
+
RECIPE_HARNESS_BROWSER: process.env.RECIPE_HARNESS_BROWSER ?? 'chrome',
|
|
146
|
+
},
|
|
147
|
+
cft: () => ({
|
|
148
|
+
error:
|
|
149
|
+
'Select installed Chrome or set RECIPE_HARNESS_CHROME_BIN to an installed Chromium executable.',
|
|
150
|
+
}),
|
|
151
|
+
cachePath: path.join(options.runtimeDir, 'browser-probe.json'),
|
|
152
|
+
launchMethod: process.platform === 'darwin' ? 'launch-services' : 'spawn',
|
|
153
|
+
display: 'headful',
|
|
154
|
+
});
|
|
155
|
+
launchBrowser({ ...options, chromeBin: resolution.bin, startUrl: url });
|
|
156
|
+
try {
|
|
157
|
+
const owner = owned(target);
|
|
158
|
+
const deadline = Date.now() + 15_000;
|
|
159
|
+
let candidates = [];
|
|
160
|
+
do {
|
|
161
|
+
candidates = (await listCdpTargets('127.0.0.1', port())).filter(
|
|
162
|
+
(entry) => entry.type === 'page' && entry.url.startsWith(`${parsed.origin}/`),
|
|
163
|
+
);
|
|
164
|
+
if (candidates.length) break;
|
|
165
|
+
await delay(100);
|
|
166
|
+
} while (Date.now() < deadline);
|
|
167
|
+
if (candidates.length !== 1)
|
|
168
|
+
throw new Error('Expected one application page in the owned browser.');
|
|
169
|
+
assertSameOwner(owner);
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(options.runtimeDir, 'page.json'),
|
|
172
|
+
JSON.stringify({ pageId: candidates[0].id }),
|
|
173
|
+
{ mode: 0o600 },
|
|
174
|
+
);
|
|
175
|
+
const status = await runtime.runtimeStatus(target);
|
|
176
|
+
if (status.decision !== 'ready') throw new Error(status.reasons.join('; '));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
stopPlainWebBrowser(target);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (jsonOutput)
|
|
182
|
+
console.log(
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
command: 'launch',
|
|
186
|
+
status: 'pass',
|
|
187
|
+
adapter: 'web',
|
|
188
|
+
target,
|
|
189
|
+
url,
|
|
190
|
+
cdpPort: options.cdpPort,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
return 0;
|
|
194
|
+
},
|
|
195
|
+
actions: {
|
|
196
|
+
manifestPath: () => config.manifestPath,
|
|
197
|
+
semantic: [],
|
|
198
|
+
cdpTarget: { transport: 'cdp', probePath: '/json/version' },
|
|
199
|
+
ui: ({ createCdpWebUiTransport }) => ({
|
|
200
|
+
base: createCdpWebUiTransport({
|
|
201
|
+
getPage: async ({ context }) =>
|
|
202
|
+
CdpWebPage.connectToTarget(await page(context.projectRoot, context.env)),
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
return runtime;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Releases only the selected checkout's owned profile. The application server is retained. */
|
|
211
|
+
export function stopPlainWebBrowser(target) {
|
|
212
|
+
const runtimeDir = plainWebRuntimeDir(target);
|
|
213
|
+
// Stop also works without active port options, including a repeated closeout.
|
|
214
|
+
stopProfileProcessesSync(path.join(runtimeDir, 'profile'));
|
|
215
|
+
removeRuntimeIdentity(runtimeDir);
|
|
216
|
+
fs.rmSync(path.join(runtimeDir, 'browser.pid'), { force: true });
|
|
217
|
+
fs.rmSync(path.join(runtimeDir, 'page.json'), { force: true });
|
|
218
|
+
return { stopped: true };
|
|
219
|
+
}
|