Skip to content

Commit 343e30c

Browse files
authored
misc(build): build-bundle to share esbuild-plugins (#16876)
1 parent 67cbfb4 commit 343e30c

6 files changed

Lines changed: 184 additions & 85 deletions

File tree

build/build-bundle-mcp.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -366,25 +366,7 @@ async function buildBundle(entryPath, distPath) {
366366
'import.meta': (id) => `{url: '${path.relative(LH_ROOT, id)}'}`,
367367
}),
368368
]),
369-
{
370-
name: 'postprocess',
371-
setup({onEnd}) {
372-
onEnd(async (result) => {
373-
if (result.errors.length) return;
374-
const codeFile = result.outputFiles?.find(file => file.path.endsWith('.js'));
375-
const mapFile = result.outputFiles?.find(file => file.path.endsWith('.js.map'));
376-
if (!codeFile) throw new Error('missing output');
377-
378-
const code = codeFile.text;
379-
if (code.includes('inflate_fast')) {
380-
throw new Error('Expected zlib inflate code to have been removed');
381-
}
382-
383-
await fs.promises.writeFile(codeFile.path, code);
384-
if (mapFile) await fs.promises.writeFile(mapFile.path, mapFile.text);
385-
});
386-
},
387-
},
369+
plugins.postprocess(),
388370
],
389371
});
390372
}

build/build-bundle.js

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
* in the browser (as long as they have access to a debugger protocol Connection).
1010
*/
1111

12-
import fs from 'fs';
1312
import path from 'path';
1413
import {execSync} from 'child_process';
1514
import {createRequire} from 'module';
@@ -159,6 +158,10 @@ async function buildBundle(entryPath, distPath, opts = {minify: true}) {
159158
// Because of page-functions!
160159
keepNames: true,
161160
inject: ['./build/process-global.js'],
161+
alias: {
162+
'debug': require.resolve('debug/src/browser.js'),
163+
'lighthouse-logger': require.resolve('../lighthouse-logger/index.js'),
164+
},
162165
/** @type {esbuild.Plugin[]} */
163166
plugins: [
164167
plugins.replaceModules({
@@ -203,66 +206,7 @@ async function buildBundle(entryPath, distPath, opts = {minify: true}) {
203206
'import.meta': (id) => `{url: '${path.relative(LH_ROOT, id)}'}`,
204207
}),
205208
]),
206-
{
207-
name: 'alias',
208-
setup({onResolve}) {
209-
onResolve({filter: /\.*/}, (args) => {
210-
/** @type {Record<string, string>} */
211-
const entries = {
212-
'debug': require.resolve('debug/src/browser.js'),
213-
'lighthouse-logger': require.resolve('../lighthouse-logger/index.js'),
214-
};
215-
if (args.path in entries) {
216-
return {path: entries[args.path]};
217-
}
218-
});
219-
},
220-
},
221-
{
222-
name: 'postprocess',
223-
setup({onEnd}) {
224-
onEnd(async (result) => {
225-
if (result.errors.length) {
226-
return;
227-
}
228-
229-
const codeFile = result.outputFiles?.find(file => file.path.endsWith('.js'));
230-
const mapFile = result.outputFiles?.find(file => file.path.endsWith('.js.map'));
231-
if (!codeFile) {
232-
throw new Error('missing output');
233-
}
234-
235-
// Just make sure the above shimming worked.
236-
let code = codeFile.text;
237-
if (code.includes('inflate_fast')) {
238-
throw new Error('Expected zlib inflate code to have been removed');
239-
}
240-
241-
// Get rid of our extra license comments.
242-
// All comments would have been moved to the end of the file, so removing some will not break
243-
// source maps.
244-
// https://stackoverflow.com/a/35923766
245-
const re = /\/\*\*\s*\n([^*]|(\*(?!\/)))*\*\/\n/g;
246-
let hasSeenFirst = false;
247-
code = code.replace(re, (match) => {
248-
if (match.includes('@license') && match.match(/Lighthouse Authors|Google/)) {
249-
if (hasSeenFirst) {
250-
return '';
251-
}
252-
253-
hasSeenFirst = true;
254-
}
255-
256-
return match;
257-
});
258-
259-
await fs.promises.writeFile(codeFile.path, code);
260-
if (mapFile) {
261-
await fs.promises.writeFile(mapFile.path, mapFile.text);
262-
}
263-
});
264-
},
265-
},
209+
plugins.postprocess({removeExtraLicenses: true}),
266210
],
267211
});
268212
}

build/build-lightrider-bundles.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,22 @@ async function buildStaticServerBundle() {
6565
});
6666
}
6767

68-
await Promise.all([
69-
buildEntryPoint(),
70-
buildReportGenerator(),
71-
buildStaticServerBundle(),
72-
]);
68+
async function runBuild() {
69+
fs.mkdirSync(distDir, {recursive: true});
70+
await Promise.all([
71+
buildEntryPoint(),
72+
buildReportGenerator(),
73+
buildStaticServerBundle(),
74+
]);
75+
}
76+
77+
if (import.meta.main) {
78+
await runBuild();
79+
}
80+
81+
export {
82+
buildEntryPoint,
83+
buildReportGenerator,
84+
buildStaticServerBundle,
85+
runBuild,
86+
};

build/esbuild-plugins.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,56 @@ function lighthouseShimPlugin(options) {
381381
};
382382
}
383383

384+
/**
385+
* @param {{removeExtraLicenses?: boolean}} options
386+
* @return {esbuild.Plugin}
387+
*/
388+
function postprocess(options = {}) {
389+
return {
390+
name: 'postprocess',
391+
setup({onEnd}) {
392+
onEnd(async (result) => {
393+
if (result.errors.length) return;
394+
395+
const codeFile = result.outputFiles?.find(file => file.path.endsWith('.js'));
396+
const mapFile = result.outputFiles?.find(file => file.path.endsWith('.js.map'));
397+
if (!codeFile) throw new Error('missing output');
398+
399+
// Just make sure the shimming worked.
400+
let code = codeFile.text;
401+
if (code.includes('inflate_fast')) {
402+
throw new Error('Expected zlib inflate code to have been removed');
403+
}
404+
405+
if (options.removeExtraLicenses) {
406+
// Get rid of our extra license comments.
407+
// All comments would have been moved to the end of the file, so removing some will not break
408+
// source maps.
409+
// https://stackoverflow.com/a/35923766
410+
const re = /\/\*\*\s*\n([^*]|(\*(?!\/)))*\*\/\n/g;
411+
let hasSeenFirst = false;
412+
code = code.replace(re, (match) => {
413+
if (match.includes('@license') && match.match(/Lighthouse Authors|Google/)) {
414+
if (hasSeenFirst) return '';
415+
hasSeenFirst = true;
416+
}
417+
return match;
418+
});
419+
}
420+
421+
await fs.promises.writeFile(codeFile.path, code);
422+
if (mapFile) await fs.promises.writeFile(mapFile.path, mapFile.text);
423+
});
424+
},
425+
};
426+
}
427+
384428
export {
385429
partialLoaders,
386430
bulkLoader,
387431
replaceModules,
388432
ignoreBuiltins,
389433
umd,
390434
lighthouseShimPlugin,
435+
postprocess,
391436
};

build/test/build-bundle-test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import path from 'path';
8+
import fs from 'fs';
9+
10+
import puppeteer from 'puppeteer-core';
11+
import {getChromePath} from 'chrome-launcher';
12+
13+
import {LH_ROOT} from '../../shared/root.js';
14+
import {buildBundle} from '../build-bundle.js';
15+
16+
const TEST_HTML = `
17+
<!DOCTYPE html>
18+
<html lang="en">
19+
<head><title>Bundle Test</title></head>
20+
<body>
21+
<h1>Bundle Test Page</h1>
22+
</body>
23+
</html>
24+
`;
25+
26+
describe('Main Bundle build', () => {
27+
const bundlePath = `${LH_ROOT}/dist/lighthouse-test-bundle.js`;
28+
const entryPath = path.join(LH_ROOT, 'clients/devtools/devtools-entry.js');
29+
30+
before(async () => {
31+
await buildBundle(entryPath, bundlePath, {minify: false});
32+
});
33+
34+
after(() => {
35+
if (fs.existsSync(bundlePath)) fs.unlinkSync(bundlePath);
36+
if (fs.existsSync(bundlePath + '.map')) fs.unlinkSync(bundlePath + '.map');
37+
});
38+
39+
it('bundle exists', () => {
40+
expect(fs.existsSync(bundlePath)).toBe(true);
41+
});
42+
43+
it('bundle can run in a browser', async () => {
44+
const browser = await puppeteer.launch({
45+
executablePath: getChromePath(),
46+
});
47+
const page = await browser.newPage();
48+
await page.setContent(TEST_HTML, {waitUntil: 'networkidle0'});
49+
50+
// devtools-entry.js expects `global` to be defined.
51+
await page.evaluate(() => {
52+
globalThis.global = globalThis;
53+
});
54+
55+
// Inject the bundle
56+
await page.addScriptTag({path: bundlePath});
57+
58+
// Verify Lighthouse is available on the window
59+
// devtools-entry.js sets self.snapshot (and others) in worker/non-worker environments.
60+
const isLighthouseAvailable = await page.evaluate(() => {
61+
// @ts-expect-error
62+
return typeof globalThis.snapshot === 'function';
63+
});
64+
65+
await browser.close();
66+
expect(isLighthouseAvailable).toBe(true);
67+
});
68+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import path from 'path';
8+
import fs from 'fs';
9+
10+
import {LH_ROOT} from '../../shared/root.js';
11+
import {buildEntryPoint, buildReportGenerator, buildStaticServerBundle} from '../build-lightrider-bundles.js';
12+
13+
describe('Lightrider Bundle builds', () => {
14+
const distDir = path.join(LH_ROOT, 'dist', 'lightrider');
15+
const lrBundlePath = path.join(distDir, 'lighthouse-lr-bundle.js');
16+
const reportGenBundlePath = path.join(distDir, 'report-generator-bundle.js');
17+
const staticServerPath = path.join(distDir, 'static-server.js');
18+
19+
before(async () => {
20+
if (!fs.existsSync(distDir)) {
21+
fs.mkdirSync(distDir, {recursive: true});
22+
}
23+
});
24+
25+
it('builds the LR entry point bundle', async () => {
26+
await buildEntryPoint();
27+
expect(fs.existsSync(lrBundlePath)).toBe(true);
28+
const content = fs.readFileSync(lrBundlePath, 'utf8');
29+
expect(content).toContain('Lighthouse');
30+
});
31+
32+
it('builds the report generator bundle', async () => {
33+
await buildReportGenerator();
34+
expect(fs.existsSync(reportGenBundlePath)).toBe(true);
35+
const content = fs.readFileSync(reportGenBundlePath, 'utf8');
36+
// UMD bundle for ReportGenerator
37+
expect(content).toContain('ReportGenerator');
38+
});
39+
40+
it('builds the static server bundle', async () => {
41+
await buildStaticServerBundle();
42+
expect(fs.existsSync(staticServerPath)).toBe(true);
43+
const content = fs.readFileSync(staticServerPath, 'utf8');
44+
expect(content).toContain('module.exports');
45+
});
46+
});

0 commit comments

Comments
 (0)