browser-commander 0.26.3 → 0.28.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/.jscpd-baseline.json +24 -23
- package/CHANGELOG.md +22 -0
- package/README.md +6 -0
- package/eslint.config.js +11 -0
- package/package.json +9 -4
- package/src/bindings.js +19 -0
- package/src/browser/attach/snapshot-launch.js +8 -6
- package/src/browser/attach/snapshot.js +39 -2
- package/src/browser/browser-cookie-database.js +71 -2
- package/src/browser/browser-cookie-session.js +183 -0
- package/src/browser/browser-cookies.js +29 -0
- package/src/browser/connector.js +117 -13
- package/src/browser/debugging-port.js +4 -2
- package/src/browser/default-browser.js +33 -9
- package/src/browser/launch-diagnostics.js +102 -0
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +54 -9
- package/src/browser/migration/sqlite-snapshot.js +5 -27
- package/src/browser/navigation.js +173 -98
- package/src/browser/persistent-session.js +360 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +142 -70
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-cookies.js +78 -0
- package/src/browser/session-persistence.js +72 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +70 -0
- package/src/browser/storage-state.js +32 -6
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +66 -1
- package/src/cli/commands.js +113 -2
- package/src/cli/high-level-methods.js +40 -3
- package/src/cli/sessions.js +11 -3
- package/src/core/commander-features.js +105 -0
- package/src/core/constants.js +1 -0
- package/src/core/dialog-manager.js +7 -0
- package/src/core/managed-navigation.js +101 -0
- package/src/core/navigation-manager.js +66 -80
- package/src/core/navigation-operation.js +25 -0
- package/src/core/navigation-readiness.js +68 -16
- package/src/core/navigation-safety.js +51 -0
- package/src/core/network-tracker.js +39 -92
- package/src/core/page-session.js +3 -2
- package/src/core/page-trigger-manager.js +130 -42
- package/src/core/readiness.js +76 -14
- package/src/core/subscriptions.js +20 -0
- package/src/core/webdriver-page.js +38 -0
- package/src/elements/locators.js +61 -37
- package/src/elements/reusable.js +92 -0
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +38 -21
- package/src/exports.js +14 -0
- package/src/factory.js +17 -16
- package/src/high-level/universal-logic.js +57 -21
- package/src/index.js +2 -0
- package/src/interactions/click.js +47 -0
- package/src/interactions/overlays.js +28 -0
- package/src/interactions/scroll.js +8 -2
- package/src/traces/bundle.js +14 -5
- package/src/traces/checkpoints.js +99 -0
- package/src/traces/links.js +117 -1
- package/src/traces/mutation-stream.js +1 -0
- package/src/traces/network.js +257 -0
- package/src/traces/page-capture.js +143 -52
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +221 -245
- package/src/traces/render.js +168 -0
- package/src/traces/rolling.js +180 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +159 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +47 -4
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/e2e/navigation-budget.e2e.test.js +132 -0
- package/tests/e2e/reusable-session.e2e.test.js +231 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
- package/tests/unit/browser/attach/snapshot.test.js +33 -0
- package/tests/unit/browser/default-browser.test.js +18 -0
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launch-diagnostics.test.js +52 -0
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -0
- package/tests/unit/browser/real-browser.test.js +1 -1
- package/tests/unit/browser/session-workflows.test.js +206 -0
- package/tests/unit/browser/storage-state.test.js +7 -2
- package/tests/unit/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/navigation-budget.test.js +120 -0
- package/tests/unit/core/navigation-manager.test.js +23 -1
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/webdriver-page.test.js +33 -0
- package/tests/unit/elements/reusable-helpers.test.js +83 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/links.test.js +2 -2
- package/tests/unit/traces/network.test.js +97 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { connectBrowser, pickForegroundPage } from './connector.js';
|
|
6
|
+
import { launchAndConnectRealBrowserWithDependencies } from './real-browser.js';
|
|
7
|
+
import { assertDedicatedUserDataDir } from './system-browser.js';
|
|
8
|
+
import { startDetachedProcess } from '../utilities/subprocess.js';
|
|
9
|
+
|
|
10
|
+
export const SESSION_METADATA = '.browser-commander-session.json';
|
|
11
|
+
const activity = new WeakMap();
|
|
12
|
+
export function touchSessionPage(page) {
|
|
13
|
+
activity.get(page)?.();
|
|
14
|
+
}
|
|
15
|
+
const read = (file) =>
|
|
16
|
+
fs
|
|
17
|
+
.readFile(file, 'utf8')
|
|
18
|
+
.then(JSON.parse)
|
|
19
|
+
.catch(() => null);
|
|
20
|
+
export async function writeSessionMetadata(file, value) {
|
|
21
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
22
|
+
try {
|
|
23
|
+
await fs.writeFile(temporary, JSON.stringify(value), { mode: 0o600 });
|
|
24
|
+
await fs.rename(temporary, file);
|
|
25
|
+
} finally {
|
|
26
|
+
await fs.rm(temporary, { force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function probeSession(endpoint) {
|
|
30
|
+
return fetch(`${endpoint}/json/version`, {
|
|
31
|
+
signal: AbortSignal.timeout(1000),
|
|
32
|
+
})
|
|
33
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
34
|
+
.catch(() => null);
|
|
35
|
+
}
|
|
36
|
+
export async function acquireLease(file, retries = 50) {
|
|
37
|
+
let lease;
|
|
38
|
+
try {
|
|
39
|
+
lease = await fs.open(file, 'wx', 0o600);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code !== 'EEXIST') {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const owner = await read(file);
|
|
45
|
+
if (retries > 0) {
|
|
46
|
+
let alive = !owner?.pid;
|
|
47
|
+
if (owner?.pid) {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(owner.pid, 0);
|
|
50
|
+
alive = true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
alive = error.code !== 'ESRCH';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (alive) {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
57
|
+
return acquireLease(file, retries - 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!owner?.pid) {
|
|
61
|
+
throw new Error('Persistent session launch is already in progress', {
|
|
62
|
+
cause: error,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
let alive = true;
|
|
66
|
+
try {
|
|
67
|
+
process.kill(owner.pid, 0);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
alive = error.code !== 'ESRCH';
|
|
70
|
+
}
|
|
71
|
+
if (alive) {
|
|
72
|
+
throw new Error('Persistent session launch is already in progress', {
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await fs.rm(file);
|
|
77
|
+
lease = await fs.open(file, 'wx', 0o600);
|
|
78
|
+
}
|
|
79
|
+
const token = randomUUID();
|
|
80
|
+
await lease.writeFile(JSON.stringify({ pid: process.pid, token }));
|
|
81
|
+
return async () => {
|
|
82
|
+
await lease.close();
|
|
83
|
+
if ((await read(file))?.token === token) {
|
|
84
|
+
await fs.rm(file, { force: true });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function validate(options) {
|
|
89
|
+
if (
|
|
90
|
+
!options.userDataDir ||
|
|
91
|
+
!Number.isInteger(options.remoteDebuggingPort) ||
|
|
92
|
+
options.remoteDebuggingPort < 1 ||
|
|
93
|
+
options.remoteDebuggingPort > 65535
|
|
94
|
+
) {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
'connectOrLaunch requires a dedicated userDataDir and fixed remoteDebuggingPort'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isFinite(options.idleTimeoutMs) || options.idleTimeoutMs <= 0) {
|
|
100
|
+
throw new RangeError('idleTimeoutMs must be positive');
|
|
101
|
+
}
|
|
102
|
+
if (!['playwright', 'puppeteer'].includes(options.engine)) {
|
|
103
|
+
throw new TypeError('Persistent sessions require a Chromium CDP engine');
|
|
104
|
+
}
|
|
105
|
+
assertDedicatedUserDataDir(options.userDataDir);
|
|
106
|
+
}
|
|
107
|
+
async function open(options, file, endpoint) {
|
|
108
|
+
let metadata = await read(file);
|
|
109
|
+
const version = await probeSession(endpoint);
|
|
110
|
+
if (version) {
|
|
111
|
+
if (
|
|
112
|
+
!metadata ||
|
|
113
|
+
metadata.remoteDebuggingPort !== options.remoteDebuggingPort ||
|
|
114
|
+
metadata.webSocketDebuggerUrl !== version.webSocketDebuggerUrl
|
|
115
|
+
) {
|
|
116
|
+
throw new Error('Debugging port belongs to another browser/profile');
|
|
117
|
+
}
|
|
118
|
+
const session = await connectBrowser({
|
|
119
|
+
...options,
|
|
120
|
+
cdpEndpoint: endpoint,
|
|
121
|
+
targetId:
|
|
122
|
+
options.targetId ?? (!options.url ? metadata.targetId : undefined),
|
|
123
|
+
});
|
|
124
|
+
return { session, metadata, reused: true };
|
|
125
|
+
}
|
|
126
|
+
const session = await launchAndConnectRealBrowserWithDependencies(
|
|
127
|
+
{ ...options, keepOpen: false },
|
|
128
|
+
{
|
|
129
|
+
spawnBrowser: (file, args, settings) =>
|
|
130
|
+
startDetachedProcess(file, args, settings),
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
try {
|
|
134
|
+
const version = await probeSession(endpoint);
|
|
135
|
+
if (!version) {
|
|
136
|
+
throw new Error('Persistent browser endpoint disappeared after launch');
|
|
137
|
+
}
|
|
138
|
+
metadata = {
|
|
139
|
+
token: randomUUID(),
|
|
140
|
+
pid: session.browserProcess.pid,
|
|
141
|
+
userDataDir: path.resolve(options.userDataDir),
|
|
142
|
+
remoteDebuggingPort: options.remoteDebuggingPort,
|
|
143
|
+
webSocketDebuggerUrl: version.webSocketDebuggerUrl,
|
|
144
|
+
};
|
|
145
|
+
return { session, metadata, reused: false };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await session.close();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function remember(page, engine, file, metadata, idleTimeoutMs) {
|
|
152
|
+
const cdp =
|
|
153
|
+
engine === 'playwright'
|
|
154
|
+
? await page.context().newCDPSession(page)
|
|
155
|
+
: await page.target().createCDPSession();
|
|
156
|
+
try {
|
|
157
|
+
metadata.targetId = (
|
|
158
|
+
await cdp.send('Target.getTargetInfo')
|
|
159
|
+
).targetInfo.targetId;
|
|
160
|
+
} finally {
|
|
161
|
+
await cdp.detach();
|
|
162
|
+
}
|
|
163
|
+
metadata.lastActive = Date.now();
|
|
164
|
+
metadata.idleTimeoutMs = idleTimeoutMs;
|
|
165
|
+
await writeSessionMetadata(file, metadata);
|
|
166
|
+
}
|
|
167
|
+
export async function closeOwnedSession(file, metadata) {
|
|
168
|
+
if ((await read(file))?.token !== metadata.token) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const version = await probeSession(
|
|
172
|
+
`http://127.0.0.1:${metadata.remoteDebuggingPort}`
|
|
173
|
+
);
|
|
174
|
+
if (version?.webSocketDebuggerUrl === metadata.webSocketDebuggerUrl) {
|
|
175
|
+
await closeRemoteBrowser(version.webSocketDebuggerUrl);
|
|
176
|
+
}
|
|
177
|
+
await fs.rm(file, { force: true });
|
|
178
|
+
}
|
|
179
|
+
async function lifecycle(
|
|
180
|
+
{ session, metadata, reused },
|
|
181
|
+
options,
|
|
182
|
+
file,
|
|
183
|
+
endpoint
|
|
184
|
+
) {
|
|
185
|
+
const { engine, idleTimeoutMs } = options;
|
|
186
|
+
let page = session.page,
|
|
187
|
+
disconnected = false,
|
|
188
|
+
closed = false,
|
|
189
|
+
activityQueue = Promise.resolve();
|
|
190
|
+
const touch = () => {
|
|
191
|
+
activityQueue = activityQueue
|
|
192
|
+
.then(async () => {
|
|
193
|
+
const release = await acquireLease(`${file}.lock`);
|
|
194
|
+
try {
|
|
195
|
+
const current = await read(file);
|
|
196
|
+
if (current?.token === metadata.token) {
|
|
197
|
+
current.lastActive = Date.now();
|
|
198
|
+
await writeSessionMetadata(file, current);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
await release();
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.catch(() => {});
|
|
205
|
+
};
|
|
206
|
+
const bind = (value) => {
|
|
207
|
+
activity.set(value, touch);
|
|
208
|
+
value.on?.('framenavigated', touch);
|
|
209
|
+
};
|
|
210
|
+
const unbind = (value) => {
|
|
211
|
+
activity.delete(value);
|
|
212
|
+
value.off?.('framenavigated', touch);
|
|
213
|
+
};
|
|
214
|
+
await remember(page, engine, file, metadata, idleTimeoutMs);
|
|
215
|
+
bind(page);
|
|
216
|
+
const detach = async () => {
|
|
217
|
+
if (disconnected) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
touch();
|
|
221
|
+
await activityQueue;
|
|
222
|
+
unbind(page);
|
|
223
|
+
disconnected = true;
|
|
224
|
+
try {
|
|
225
|
+
await session.downloads?.dispose();
|
|
226
|
+
} finally {
|
|
227
|
+
await session.detach();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const close = async () => {
|
|
231
|
+
if (closed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!disconnected) {
|
|
235
|
+
await detach().catch(() => {});
|
|
236
|
+
}
|
|
237
|
+
const release = await acquireLease(`${file}.lock`);
|
|
238
|
+
try {
|
|
239
|
+
await closeOwnedSession(file, metadata);
|
|
240
|
+
} finally {
|
|
241
|
+
await release();
|
|
242
|
+
}
|
|
243
|
+
closed = true;
|
|
244
|
+
};
|
|
245
|
+
const result = {
|
|
246
|
+
...session,
|
|
247
|
+
detach,
|
|
248
|
+
close,
|
|
249
|
+
touch,
|
|
250
|
+
reused,
|
|
251
|
+
cdpEndpoint: endpoint,
|
|
252
|
+
userDataDir: path.resolve(options.userDataDir),
|
|
253
|
+
remoteDebuggingPort: options.remoteDebuggingPort,
|
|
254
|
+
temporaryProfile: false,
|
|
255
|
+
};
|
|
256
|
+
result.reusePage = async (selection = {}) => {
|
|
257
|
+
if (disconnected) {
|
|
258
|
+
throw new Error('Persistent controller is detached');
|
|
259
|
+
}
|
|
260
|
+
const release = await acquireLease(`${file}.lock`);
|
|
261
|
+
try {
|
|
262
|
+
const current = await read(file);
|
|
263
|
+
if (current?.token !== metadata.token) {
|
|
264
|
+
throw new Error('Persistent session ownership changed');
|
|
265
|
+
}
|
|
266
|
+
const pages =
|
|
267
|
+
engine === 'playwright'
|
|
268
|
+
? page.context().pages()
|
|
269
|
+
: await session.browser.pages();
|
|
270
|
+
const selected = await pickForegroundPage(pages, {
|
|
271
|
+
targetId:
|
|
272
|
+
selection.targetId ??
|
|
273
|
+
(!selection.url ? metadata.targetId : undefined),
|
|
274
|
+
...selection,
|
|
275
|
+
});
|
|
276
|
+
if (!selected) {
|
|
277
|
+
throw new Error('No reusable browser page');
|
|
278
|
+
}
|
|
279
|
+
unbind(page);
|
|
280
|
+
page = selected;
|
|
281
|
+
bind(page);
|
|
282
|
+
result.page = page;
|
|
283
|
+
await remember(page, engine, file, metadata, idleTimeoutMs);
|
|
284
|
+
} finally {
|
|
285
|
+
await release();
|
|
286
|
+
}
|
|
287
|
+
return page;
|
|
288
|
+
};
|
|
289
|
+
if (!reused) {
|
|
290
|
+
await startDetachedProcess(process.execPath, [
|
|
291
|
+
fileURLToPath(new URL('./session-watchdog.js', import.meta.url)),
|
|
292
|
+
file,
|
|
293
|
+
metadata.token,
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
export async function closeRemoteBrowser(endpoint) {
|
|
299
|
+
await new Promise((resolve, reject) => {
|
|
300
|
+
const socket = new globalThis.WebSocket(endpoint);
|
|
301
|
+
let acknowledged = false;
|
|
302
|
+
const timer = setTimeout(() => {
|
|
303
|
+
socket.close();
|
|
304
|
+
reject(new Error('Browser.close timed out'));
|
|
305
|
+
}, 3000);
|
|
306
|
+
socket.onopen = () =>
|
|
307
|
+
socket.send(JSON.stringify({ id: 1, method: 'Browser.close' }));
|
|
308
|
+
socket.onclose = () => {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
resolve();
|
|
311
|
+
};
|
|
312
|
+
socket.onerror = () => {
|
|
313
|
+
// Chrome can close the TCP connection without a WebSocket close frame
|
|
314
|
+
// after acknowledging Browser.close. Its ensuing close event completes
|
|
315
|
+
// shutdown; a pre-acknowledgement connection error remains a failure.
|
|
316
|
+
if (!acknowledged) {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
reject(new Error('Browser.close connection failed'));
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
socket.onmessage = ({ data }) => {
|
|
322
|
+
const response = JSON.parse(data);
|
|
323
|
+
if (response.id !== 1) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (response.error) {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
reject(new Error(response.error.message ?? 'Browser.close failed'));
|
|
329
|
+
socket.close();
|
|
330
|
+
} else {
|
|
331
|
+
acknowledged = true;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/** Reuse an owned profile/port; detach survives the controller; close terminates it. */
|
|
337
|
+
export async function connectOrLaunch(settings = {}) {
|
|
338
|
+
const options = {
|
|
339
|
+
engine: 'playwright',
|
|
340
|
+
idleTimeoutMs: 30 * 60 * 1000,
|
|
341
|
+
...settings,
|
|
342
|
+
};
|
|
343
|
+
validate(options);
|
|
344
|
+
await fs.mkdir(options.userDataDir, { recursive: true, mode: 0o700 });
|
|
345
|
+
const file = path.join(path.resolve(options.userDataDir), SESSION_METADATA);
|
|
346
|
+
const release = await acquireLease(`${file}.lock`);
|
|
347
|
+
const endpoint = `http://127.0.0.1:${options.remoteDebuggingPort}`;
|
|
348
|
+
let owned;
|
|
349
|
+
try {
|
|
350
|
+
owned = await open(options, file, endpoint);
|
|
351
|
+
return await lifecycle(owned, options, file, endpoint);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (owned && !owned.reused) {
|
|
354
|
+
await owned.session.close().catch(() => {});
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
} finally {
|
|
358
|
+
await release();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -106,6 +106,26 @@ export async function configureUserDataDir(
|
|
|
106
106
|
) {
|
|
107
107
|
assertObject(preferences, 'preferences');
|
|
108
108
|
assertObject(localState, 'localState');
|
|
109
|
+
for (const key of [
|
|
110
|
+
'session.restore_on_startup',
|
|
111
|
+
'session.startup_urls',
|
|
112
|
+
'homepage',
|
|
113
|
+
'homepage_is_newtabpage',
|
|
114
|
+
'browser.show_home_button',
|
|
115
|
+
'extensions.settings',
|
|
116
|
+
'default_search_provider_data',
|
|
117
|
+
]) {
|
|
118
|
+
let value = preferences;
|
|
119
|
+
for (const part of key.split('.')) {
|
|
120
|
+
value = value?.[part];
|
|
121
|
+
}
|
|
122
|
+
if (value !== undefined || Object.hasOwn(preferences, key)) {
|
|
123
|
+
process.emitWarning(
|
|
124
|
+
`Chrome protects ${key} in Secure Preferences and may reset this value. Use browser settings or managed policy to set it.`,
|
|
125
|
+
{ code: 'PROTECTED_CHROME_PREFERENCE' }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
109
129
|
if (
|
|
110
130
|
defaultBrowserCheck !== undefined &&
|
|
111
131
|
typeof defaultBrowserCheck !== 'boolean'
|