Skip to content

Commit 3ad88b6

Browse files
authored
Rewrite scripts to get globals in browser (#300)
1 parent d10fa84 commit 3ad88b6

File tree

12 files changed

+324
-189
lines changed

12 files changed

+324
-189
lines changed

.github/workflows/update.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ jobs:
2121
check-latest: true
2222
node-version: latest
2323
- run: npm install
24-
- run: npm run update
24+
- run: |
25+
npm install puppeteer --save-dev
26+
npm run update
2527
- uses: actions/upload-artifact@v4
2628
with:
2729
name: data
@@ -43,7 +45,9 @@ jobs:
4345
with:
4446
name: data
4547
path: data
46-
- run: npm run update
48+
- run: |
49+
npm install puppeteer --save-dev
50+
npm run update
4751
- uses: actions/upload-artifact@v4
4852
with:
4953
name: data
@@ -68,7 +72,9 @@ jobs:
6872
path: data
6973
# https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md
7074
- run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
71-
- run: npm run update
75+
- run: |
76+
npm install puppeteer --save-dev
77+
npm run update
7278
- uses: peter-evans/create-pull-request@v7
7379
with:
7480
commit-message: Update globals

package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"nano-spawn": "^0.2.0",
5555
"npm-run-all2": "^8.0.1",
5656
"outdent": "^0.8.0",
57-
"puppeteer": "^24.8.2",
57+
"puppeteer": "^24.9.0",
5858
"shelljs": "^0.9.2",
5959
"tsd": "^0.32.0",
6060
"type-fest": "^4.41.0",
@@ -90,6 +90,20 @@
9090
"rules": {
9191
"n/no-unsupported-features/node-builtins": "off"
9292
}
93+
},
94+
{
95+
"files": [
96+
"scripts/browser/assets/**/*.mjs"
97+
],
98+
"envs": [
99+
"browser",
100+
"worker",
101+
"serviceworker"
102+
],
103+
"rules": {
104+
"n/no-unsupported-features/node-builtins": "off",
105+
"unicorn/prefer-add-event-listener": "off"
106+
}
93107
}
94108
]
95109
},
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {initAudioWorklet} from './main.mjs';
2+
3+
initAudioWorklet();

scripts/browser/assets/main.mjs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
const EXECUTE_COMMAND_SIGNAL = 'get-globals';
2+
3+
const environments = [
4+
{environment: 'browser', getGlobals: getBrowserGlobals},
5+
{environment: 'worker', getGlobals: getWebWorkerGlobals},
6+
{environment: 'serviceworker', getGlobals: getServiceWorkerGlobals},
7+
];
8+
9+
function getGlobalThisProperties({expectSecureContext = true} = {}) {
10+
if (expectSecureContext && !globalThis.isSecureContext) {
11+
throw new Error('Expected a secure context.');
12+
}
13+
14+
const keys = [];
15+
16+
for (
17+
let object = globalThis;
18+
object && object !== Object.prototype;
19+
object = Object.getPrototypeOf(object)
20+
) {
21+
keys.push(...Object.getOwnPropertyNames(object));
22+
}
23+
24+
return keys.filter(key => key !== 'constructor');
25+
}
26+
27+
function sendResult({
28+
port = globalThis,
29+
receivePort = port,
30+
sendPort = receivePort,
31+
getGlobals = getGlobalThisProperties,
32+
} = {}) {
33+
receivePort.onmessage = receivedMessage => {
34+
if (receivedMessage.data !== EXECUTE_COMMAND_SIGNAL) {
35+
return;
36+
}
37+
38+
const message = {};
39+
try {
40+
message.result = getGlobals();
41+
} catch (error) {
42+
message.error = error;
43+
throw error;
44+
} finally {
45+
const port = typeof sendPort === 'function' ? sendPort(receivedMessage) : sendPort;
46+
port.postMessage(message);
47+
}
48+
};
49+
}
50+
51+
function receiveResult({
52+
port,
53+
receivePort = port,
54+
sendPort = receivePort,
55+
}) {
56+
return new Promise((resolve, reject) => {
57+
receivePort.onmessage = ({data: {result, error}}) => {
58+
if (error) {
59+
reject(error);
60+
} else {
61+
resolve(result);
62+
}
63+
};
64+
65+
sendPort.postMessage(EXECUTE_COMMAND_SIGNAL);
66+
});
67+
}
68+
69+
let webWorker;
70+
function getWebWorkerGlobals() {
71+
webWorker ??= new Worker('./assets/web-worker.mjs', {type: 'module'});
72+
return receiveResult({port: webWorker});
73+
}
74+
75+
function initWebWorker() {
76+
sendResult();
77+
}
78+
79+
const SERVICE_WORK_URL = './assets/service-worker.mjs';
80+
let serviceWorker;
81+
async function getServiceWorkerGlobals() {
82+
const serviceWorkerContainer = navigator.serviceWorker;
83+
if (!serviceWorker) {
84+
let registration = await serviceWorkerContainer.getRegistration(SERVICE_WORK_URL);
85+
if (registration) {
86+
await registration.update();
87+
} else {
88+
registration = await serviceWorkerContainer.register(SERVICE_WORK_URL, {type: 'module'});
89+
}
90+
91+
serviceWorker = registration.active ?? registration.waiting ?? registration.installing;
92+
serviceWorkerContainer.startMessages();
93+
}
94+
95+
return receiveResult({receivePort: serviceWorkerContainer, sendPort: serviceWorker});
96+
}
97+
98+
async function initServiceWorker() {
99+
sendResult({
100+
sendPort: message => message.source,
101+
});
102+
}
103+
104+
async function getBrowserGlobals() {
105+
const globals = getGlobalThisProperties();
106+
const audioWorkletGlobals = await getAudioWorkletGlobals();
107+
return [...new Set([...globals, ...audioWorkletGlobals])];
108+
}
109+
110+
const AUDIO_WORKLET_PROCESSOR_NAME = `${EXECUTE_COMMAND_SIGNAL}-processor`;
111+
let audioWorkletNode;
112+
async function getAudioWorkletGlobals() {
113+
if (!audioWorkletNode) {
114+
const context = new AudioContext();
115+
await context.audioWorklet.addModule('./assets/audio-worklet.mjs');
116+
audioWorkletNode = new AudioWorkletNode(context, AUDIO_WORKLET_PROCESSOR_NAME);
117+
}
118+
119+
return receiveResult({port: audioWorkletNode.port});
120+
}
121+
122+
function initAudioWorklet() {
123+
registerProcessor(AUDIO_WORKLET_PROCESSOR_NAME, class AudioWorkletGetGlobalsProcessor extends AudioWorkletProcessor {
124+
constructor() {
125+
super();
126+
127+
sendResult({
128+
port: this.port,
129+
getGlobals: () => getGlobalThisProperties({expectSecureContext: false}),
130+
});
131+
}
132+
133+
process() {
134+
return true;
135+
}
136+
});
137+
}
138+
139+
function initPage() {
140+
// Exposed for Node.js to call
141+
Object.defineProperty(globalThis, '__getGlobals', {
142+
enumerable: false,
143+
value(environment) {
144+
return environments.find(({environment: name}) => name === environment).getGlobals();
145+
},
146+
});
147+
148+
const mainContainer = document.body;
149+
150+
for (const {environment, getGlobals} of environments) {
151+
const container = document.createElement('details');
152+
const summary = Object.assign(document.createElement('summary'), {
153+
textContent: environment,
154+
});
155+
const button = Object.assign(document.createElement('button'), {
156+
type: 'button',
157+
textContent: `Get '${environment}' globals`,
158+
});
159+
const result = document.createElement('pre');
160+
button.addEventListener('click', async () => {
161+
container.open = true;
162+
button.disabled = true;
163+
result.textContent = `Loading '${environment}' globals ...`;
164+
try {
165+
const globals = await getGlobals();
166+
result.textContent = JSON.stringify(globals, undefined, 2);
167+
} catch (error) {
168+
result.textContent = error;
169+
} finally {
170+
button.disabled = false;
171+
}
172+
});
173+
174+
container.append(summary);
175+
container.append(button);
176+
container.append(result);
177+
mainContainer.append(container);
178+
}
179+
}
180+
181+
export {
182+
initPage,
183+
initWebWorker,
184+
initServiceWorker,
185+
initAudioWorklet,
186+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {initServiceWorker} from './main.mjs';
2+
3+
initServiceWorker();

scripts/browser/assets/web-worker.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {initWebWorker} from './main.mjs';
2+
3+
initWebWorker();

scripts/browser/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
</head>
8+
<body>
9+
<script type="module">
10+
import {initPage} from './assets/main.mjs';
11+
initPage()
12+
</script>
13+
</body>
14+
</html>

scripts/browser/readme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Start debug
2+
3+
```sh
4+
node --watch start.mjs
5+
```

scripts/browser/server.mjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import fs from 'node:fs/promises';
2+
import http from 'node:http';
3+
import {inspect} from 'node:util';
4+
import getPort from 'get-port';
5+
6+
async function startServer({silent = false, port: preferredPort} = {}) {
7+
const port = await getPort({port: preferredPort});
8+
9+
const server = http.createServer(async (request, response) => {
10+
let {url} = request;
11+
if (!silent) {
12+
console.debug(url);
13+
}
14+
15+
if (url === '/') {
16+
url = '/index.html';
17+
}
18+
19+
// Only allow `.mjs` and `.html`
20+
if (!/\.(?:html|mjs)$/.test(url)) {
21+
response.statusCode = 400;
22+
return;
23+
}
24+
25+
const file = new URL(url.slice(1), import.meta.url);
26+
let content;
27+
28+
try {
29+
content = await fs.readFile(file, 'utf8');
30+
} catch (error) {
31+
if (!silent) {
32+
console.error(error);
33+
}
34+
35+
response.statusCode = error.code === 'ENOENT' ? 400 : 500;
36+
response.end(inspect(error));
37+
return;
38+
}
39+
40+
response.statusCode = 200;
41+
response.setHeader(
42+
'Content-Type',
43+
url.endsWith('.mjs') ? 'application/javascript' : 'text/html',
44+
);
45+
46+
response.end(content);
47+
});
48+
49+
// https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
50+
const hostname = '127.0.0.1';
51+
server.listen(port, hostname);
52+
53+
const url = `http://${hostname}:${port}`;
54+
55+
const close = () => new Promise(resolve => {
56+
server.close(resolve);
57+
});
58+
59+
return {
60+
url,
61+
close,
62+
};
63+
}
64+
65+
export {startServer};

scripts/browser/start.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import process from 'node:process';
2+
import {startServer} from './server.mjs';
3+
4+
const server = await startServer({port: 3000});
5+
6+
console.log(`Server started, navigate to ${server.url} start debug.`);
7+
8+
process.once('exit', async () => {
9+
await server.close();
10+
});

0 commit comments

Comments
 (0)