Skip to content

Commit 59ceff8

Browse files
authored
Get all browser globals from both chrome and firefox (#321)
1 parent 4a02a85 commit 59ceff8

File tree

5 files changed

+132
-65
lines changed

5 files changed

+132
-65
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@
5252
"@vitest/eslint-plugin": "^1.1.44",
5353
"ava": "^6.3.0",
5454
"cheerio": "^1.0.0",
55+
"esbuild": "^0.27.2",
5556
"eslint-plugin-jest": "^28.11.0",
5657
"get-port": "^7.1.0",
5758
"is-identifier": "^1.0.1",
5859
"nano-spawn": "^0.2.0",
5960
"npm-run-all2": "^8.0.1",
6061
"outdent": "^0.8.0",
61-
"puppeteer": "^24.27.0",
62+
"puppeteer": "^24.34.0",
6263
"shelljs": "^0.9.2",
6364
"tsd": "^0.32.0",
6465
"type-fest": "^4.41.0",

puppeteer.config.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
'use strict';
22

3+
const process = require('node:process');
4+
const path = require('node:path');
5+
6+
const IS_CI = Boolean(process.env.CI);
7+
38
// Will download when execute
49
module.exports = {
510
skipDownload: true,
11+
cacheDirectory: IS_CI ? path.join(__dirname, '.cache/puppeteer/') : undefined,
612
};

scripts/browser.mjs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import process from 'node:process';
2+
import assert from 'node:assert/strict';
3+
import puppeteer from 'puppeteer';
4+
5+
const puppeteerBrowsers = ['chrome', 'chrome-headless-shell', 'firefox'];
6+
7+
async function _downloadBrowser({browser}) {
8+
const {downloadBrowsers} = await import('puppeteer/internal/node/install.js');
9+
const originalEnv = {...process.env};
10+
11+
const envOverrides = {
12+
PUPPETEER_SKIP_DOWNLOAD: JSON.stringify(false),
13+
...Object.fromEntries(
14+
puppeteerBrowsers.map(name => [
15+
`PUPPETEER_${name.replaceAll('-', '_').toUpperCase()}_SKIP_DOWNLOAD`,
16+
JSON.stringify(name !== browser),
17+
]),
18+
),
19+
};
20+
21+
Object.assign(process.env, envOverrides);
22+
23+
try {
24+
await downloadBrowsers();
25+
} finally {
26+
for (const env of Object.keys(envOverrides)) {
27+
if (Object.hasOwn(originalEnv)) {
28+
process.env[env] = originalEnv[env];
29+
} else {
30+
delete process.env[env];
31+
}
32+
}
33+
}
34+
}
35+
36+
const browserInstallPromises = new Map();
37+
function downloadBrowser({browser}) {
38+
if (!browserInstallPromises.has(browser)) {
39+
browserInstallPromises.set(browser, _downloadBrowser({browser}));
40+
}
41+
42+
return browserInstallPromises.get(browser);
43+
}
44+
45+
async function _launchBrowser({browser: browserName}) {
46+
const browser = await puppeteer.launch({
47+
browser: browserName,
48+
enableExtensions: false,
49+
waitForInitialPage: false,
50+
});
51+
52+
try {
53+
const version = await browser.version();
54+
assert.ok(
55+
version.toLowerCase().startsWith(`${browserName}/`),
56+
`Unexpected browser version: '${version}', expected '${browserName}'.`,
57+
);
58+
} catch (error) {
59+
await browser.close();
60+
throw error;
61+
}
62+
63+
return browser;
64+
}
65+
66+
async function launchBrowser({browser}) {
67+
try {
68+
return await _launchBrowser({browser});
69+
} catch {
70+
await downloadBrowser({browser});
71+
return _launchBrowser({browser});
72+
}
73+
}
74+
75+
export {launchBrowser};

scripts/browser/server.mjs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
import fs from 'node:fs/promises';
2+
import {fileURLToPath} from 'node:url';
23
import http from 'node:http';
34
import {inspect} from 'node:util';
45
import getPort from 'get-port';
6+
import esbuild from 'esbuild';
7+
8+
// Firefox will support module service worker in v147
9+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
10+
async function transformScript(file) {
11+
const filename = '.service-worker-bundled.mjs';
12+
await esbuild.build({
13+
entryPoints: [fileURLToPath(file)],
14+
bundle: true,
15+
outfile: filename,
16+
});
17+
const content = await fs.readFile(filename, 'utf8');
18+
await fs.rm(filename);
19+
return content;
20+
}
521

622
async function startServer({silent = false, port: preferredPort} = {}) {
723
const port = await getPort({port: preferredPort});
@@ -27,7 +43,11 @@ async function startServer({silent = false, port: preferredPort} = {}) {
2743
let content;
2844

2945
try {
30-
content = await fs.readFile(file, 'utf8');
46+
content = await (
47+
url === '/assets/service-worker.mjs'
48+
? transformScript(file)
49+
: fs.readFile(file, 'utf8')
50+
);
3151
} catch (error) {
3252
if (!silent) {
3353
console.error(error);

scripts/get-browser-globals.mjs

Lines changed: 28 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
import process from 'node:process';
2-
import assert from 'node:assert/strict';
3-
import puppeteer from 'puppeteer';
1+
import {launchBrowser} from './browser.mjs';
42
import {createGlobals} from './utilities.mjs';
53
import {startServer} from './browser/server.mjs';
64

5+
const firefoxNonStandardGlobals = new Set([
6+
// Can't find documentation
7+
'Directory',
8+
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/Window/dump
9+
'dump',
10+
// Non-standard https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/InternalError
11+
'InternalError',
12+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1754441
13+
'InstallTrigger',
14+
// Can't find documentation
15+
'AnimationTrigger',
16+
// Legacy feature https://github.com/whatwg/html/issues/2741
17+
'ondragexit',
18+
]);
19+
720
const ignoredGlobals = new Set([
821
// Chrome only
922
'chrome',
@@ -20,16 +33,10 @@ const ignoredGlobals = new Set([
2033
'CSS2Properties',
2134
// Deprecated https://developer.mozilla.org/en-US/docs/Web/API/Window/captureEvents
2235
'captureEvents',
23-
// Can't find documentation
24-
'Directory',
25-
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/Window/dump
26-
'dump',
2736
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/Window/fullScreen
2837
'fullScreen',
2938
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/window/getDefaultComputedStyle
3039
'getDefaultComputedStyle',
31-
// Non-standard https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/InternalError
32-
'InternalError',
3340
// Can't find documentation
3441
'KeyEvent',
3542
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/MouseScrollEvent
@@ -58,6 +65,7 @@ const ignoredGlobals = new Set([
5865
'setResizable',
5966
// Non-standard https://developer.mozilla.org/en-US/docs/Web/API/Window/updateCommands
6067
'updateCommands',
68+
...firefoxNonStandardGlobals,
6169
]);
6270

6371
const shouldExclude = name =>
@@ -73,54 +81,8 @@ const isWritable = name =>
7381
name === 'location'
7482
|| name.startsWith('on');
7583

76-
const puppeteerBrowsers = [
77-
'chrome',
78-
'chrome-headless-shell',
79-
'firefox',
80-
];
81-
82-
async function downloadBrowser({product} = {}) {
83-
const {downloadBrowsers} = await import('puppeteer/internal/node/install.js');
84-
const originalEnv = {...process.env};
85-
86-
const envOverrides = {
87-
PUPPETEER_SKIP_DOWNLOAD: JSON.stringify(false),
88-
...Object.fromEntries(puppeteerBrowsers.map(browser => [
89-
`PUPPETEER_${browser.replaceAll('-', '_').toUpperCase()}_SKIP_DOWNLOAD`,
90-
JSON.stringify(browser !== product),
91-
])),
92-
};
93-
94-
Object.assign(process.env, envOverrides);
95-
96-
try {
97-
await downloadBrowsers();
98-
} finally {
99-
for (const env of Object.keys(envOverrides)) {
100-
if (Object.hasOwn(originalEnv)) {
101-
process.env[env] = originalEnv[env];
102-
} else {
103-
delete process.env[env];
104-
}
105-
}
106-
}
107-
}
108-
109-
async function getGlobalsInBrowser(environment, product = 'chrome') {
110-
await downloadBrowser({product});
111-
112-
const browser = await puppeteer.launch({browser: product});
113-
114-
try {
115-
const version = await browser.version();
116-
assert.ok(
117-
version.toLowerCase().startsWith(`${product}/`),
118-
`Unexpected browser version: '${version}', expected '${product}'.`,
119-
);
120-
} catch (error) {
121-
await browser.close();
122-
throw error;
123-
}
84+
async function _getGlobalsInBrowser(environment, browserName) {
85+
const browser = await launchBrowser({browser: browserName});
12486

12587
const page = await browser.newPage();
12688

@@ -135,15 +97,18 @@ async function getGlobalsInBrowser(environment, product = 'chrome') {
13597
}
13698
}
13799

100+
async function getGlobalsInBrowser(environment) {
101+
const results = await Promise.all(
102+
['chrome', 'firefox'].map(browser => _getGlobalsInBrowser(environment, browser)),
103+
);
104+
return results.flat().filter(name => !firefoxNonStandardGlobals.has(name));
105+
}
106+
138107
async function getBrowserGlobals() {
139-
const chromeGlobals = await getGlobalsInBrowser('browser');
140-
const firefoxGlobals = await getGlobalsInBrowser('browser', 'firefox');
108+
const properties = await getGlobalsInBrowser('browser');
141109

142110
return createGlobals(
143-
[
144-
...chromeGlobals,
145-
...firefoxGlobals,
146-
],
111+
properties,
147112
{
148113
shouldExclude,
149114
isWritable,

0 commit comments

Comments
 (0)