-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
misc(build): use rollup to build lighthouse-core bundles #12771
Changes from all commits
f5cda27
f9ddfa4
3050c68
aaaf2de
229ae00
bc23310
b3d9037
823c6f5
dffb095
d7e7c1b
0fda799
213d949
b975d11
f90efbf
950e1eb
7cdce5e
4c96a26
dc0fdd1
03ca850
1dfa92d
a1a0ff6
3cf5303
667b536
db22bea
b1a3906
9cb65d1
2a99682
ed47297
e74eba3
e5d5782
1741e04
b52af24
4051326
031781e
fb203e1
41512b9
655c69c
507e38c
806744c
36056c4
fb5a762
a250469
ab3f498
5c91976
37065bf
3f57709
f4494ab
0b6d25c
8d1d7a1
38cac7e
546685d
0adea44
dfa0cdc
1ed7697
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,25 +12,15 @@ | |
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const assert = require('assert').strict; | ||
const mkdir = fs.promises.mkdir; | ||
const LighthouseRunner = require('../lighthouse-core/runner.js'); | ||
const exorcist = require('exorcist'); | ||
const browserify = require('browserify'); | ||
const terser = require('terser'); | ||
|
||
const inlineFs = require('./plugins/browserify-inline-fs.js'); | ||
const rollup = require('rollup'); | ||
const rollupPlugins = require('./rollup-plugins.js'); | ||
const Runner = require('../lighthouse-core/runner.js'); | ||
const {LH_ROOT} = require('../root.js'); | ||
|
||
const COMMIT_HASH = require('child_process') | ||
.execSync('git rev-parse HEAD') | ||
.toString().trim(); | ||
|
||
const audits = LighthouseRunner.getAuditList() | ||
.map(f => './lighthouse-core/audits/' + f.replace(/\.js$/, '')); | ||
|
||
const gatherers = LighthouseRunner.getGathererList() | ||
.map(f => './lighthouse-core/gather/gatherers/' + f.replace(/\.js$/, '')); | ||
|
||
// HACK: manually include the lighthouse-plugin-publisher-ads audits. | ||
/** @type {Array<string>} */ | ||
// @ts-expect-error | ||
|
@@ -45,144 +35,167 @@ const isLightrider = file => path.basename(file).includes('lightrider'); | |
// Set to true for source maps. | ||
const DEBUG = false; | ||
|
||
const today = (() => { | ||
const date = new Date(); | ||
const year = new Intl.DateTimeFormat('en', {year: 'numeric'}).format(date); | ||
const month = new Intl.DateTimeFormat('en', {month: 'short'}).format(date); | ||
const day = new Intl.DateTimeFormat('en', {day: '2-digit'}).format(date); | ||
return `${month} ${day} ${year}`; | ||
})(); | ||
const pkg = JSON.parse(fs.readFileSync(LH_ROOT + '/package.json', 'utf-8')); | ||
const banner = ` | ||
/** | ||
* Lighthouse v${pkg.version} ${COMMIT_HASH} (${today}) | ||
* | ||
* ${pkg.description} | ||
* | ||
* @homepage ${pkg.homepage} | ||
* @author ${pkg.author} | ||
* @license ${pkg.license} | ||
*/ | ||
`.trim(); | ||
|
||
/** | ||
* Browserify starting at the file at entryPath. Contains entry-point-specific | ||
* ignores (e.g. for DevTools or the extension) to trim the bundle depending on | ||
* the eventual use case. | ||
* Bundle starting at entryPath, writing the minified result to distPath. | ||
* @param {string} entryPath | ||
* @param {string} distPath | ||
* @param {{minify: boolean}=} opts | ||
* @return {Promise<void>} | ||
*/ | ||
async function browserifyFile(entryPath, distPath) { | ||
let bundle = browserify(entryPath, {debug: DEBUG}); | ||
|
||
bundle | ||
.plugin('browserify-banner', { | ||
pkg: Object.assign({COMMIT_HASH}, require('../package.json')), | ||
file: require.resolve('./banner.txt'), | ||
}) | ||
// Transform `fs.readFileSync`, etc into inline strings. | ||
.transform(inlineFs({verbose: DEBUG})) | ||
// Strip everything out of package.json includes except for the version. | ||
.transform('package-json-versionify'); | ||
|
||
// scripts will need some additional transforms, ignores and requires… | ||
bundle.ignore('source-map') | ||
.ignore('debug/node') | ||
.ignore('intl') | ||
.ignore('intl-pluralrules') | ||
.ignore('raven') | ||
.ignore('pako/lib/zlib/inflate.js'); | ||
|
||
// Don't include the desktop protocol connection. | ||
bundle.ignore(require.resolve('../lighthouse-core/gather/connections/cri.js')); | ||
|
||
// Don't include the stringified report in DevTools - see devtools-report-assets.js | ||
// Don't include in Lightrider - HTML generation isn't supported, so report assets aren't needed. | ||
if (isDevtools(entryPath) || isLightrider(entryPath)) { | ||
bundle.ignore(require.resolve('../report/generator/report-assets.js')); | ||
async function build(entryPath, distPath, opts = {minify: true}) { | ||
if (fs.existsSync(LH_ROOT + '/lighthouse-logger/node_modules')) { | ||
throw new Error('delete `lighthouse-logger/node_modules` because it messes up rollup bundle'); | ||
} | ||
|
||
// Don't include locales in DevTools. | ||
if (isDevtools(entryPath)) { | ||
bundle.ignore(require.resolve('../shared/localization/locales.js')); | ||
} | ||
// List of paths (absolute / relative to config-helpers.js) to include | ||
// in bundle and make accessible via config-helpers.js `requireWrapper`. | ||
const dynamicModulePaths = [ | ||
...Runner.getGathererList().map(gatherer => `../gather/gatherers/${gatherer}`), | ||
...Runner.getAuditList().map(gatherer => `../audits/${gatherer}`), | ||
]; | ||
|
||
// Expose the audits, gatherers, and computed artifacts so they can be dynamically loaded. | ||
// Exposed path must be a relative path from lighthouse-core/config/config-helpers.js (where loading occurs). | ||
const corePath = './lighthouse-core/'; | ||
const driverPath = `${corePath}gather/`; | ||
audits.forEach(audit => { | ||
bundle = bundle.require(audit, {expose: audit.replace(corePath, '../')}); | ||
}); | ||
gatherers.forEach(gatherer => { | ||
bundle = bundle.require(gatherer, {expose: gatherer.replace(driverPath, '../gather/')}); | ||
}); | ||
|
||
// HACK: manually include the lighthouse-plugin-publisher-ads audits. | ||
// Include lighthouse-plugin-publisher-ads. | ||
if (isDevtools(entryPath) || isLightrider(entryPath)) { | ||
bundle.require('lighthouse-plugin-publisher-ads'); | ||
dynamicModulePaths.push('lighthouse-plugin-publisher-ads'); | ||
pubAdsAudits.forEach(pubAdAudit => { | ||
bundle = bundle.require(pubAdAudit); | ||
dynamicModulePaths.push(pubAdAudit); | ||
}); | ||
} | ||
|
||
// browerify's url shim doesn't work with .URL in node_modules, | ||
// and within robots-parser, it does `var URL = require('url').URL`, so we expose our own. | ||
// @see https://github.com/GoogleChrome/lighthouse/issues/5273 | ||
const pathToURLShim = require.resolve('../lighthouse-core/lib/url-shim.js'); | ||
bundle = bundle.require(pathToURLShim, {expose: 'url'}); | ||
|
||
let bundleStream = bundle.bundle(); | ||
const bundledMapEntriesCode = dynamicModulePaths.map(modulePath => { | ||
const pathNoExt = modulePath.replace('.js', ''); | ||
return `['${pathNoExt}', require('${modulePath}')]`; | ||
}).join(',\n'); | ||
|
||
// Make sure path exists. | ||
await mkdir(path.dirname(distPath), {recursive: true}); | ||
await new Promise((resolve, reject) => { | ||
const writeStream = fs.createWriteStream(distPath); | ||
writeStream.on('finish', resolve); | ||
writeStream.on('error', reject); | ||
/** @type {Record<string, string>} */ | ||
const shimsObj = {}; | ||
|
||
// Extract the inline source map to an external file. | ||
if (DEBUG) bundleStream = bundleStream.pipe(exorcist(`${distPath}.map`)); | ||
bundleStream.pipe(writeStream); | ||
}); | ||
const modulesToIgnore = [ | ||
'intl-pluralrules', | ||
'intl', | ||
'pako/lib/zlib/inflate.js', | ||
'raven', | ||
'source-map', | ||
'ws', | ||
require.resolve('../lighthouse-core/gather/connections/cri.js'), | ||
]; | ||
|
||
if (isDevtools(distPath)) { | ||
let code = fs.readFileSync(distPath, 'utf-8'); | ||
// Add a comment for TypeScript, but not if in DEBUG mode so that source maps are not affected. | ||
// See lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js | ||
if (!DEBUG) { | ||
code = '// @ts-nocheck - Prevent tsc stepping into any required bundles.\n' + code; | ||
} | ||
// Don't include the stringified report in DevTools - see devtools-report-assets.js | ||
// Don't include in Lightrider - HTML generation isn't supported, so report assets aren't needed. | ||
if (isDevtools(entryPath) || isLightrider(entryPath)) { | ||
modulesToIgnore.push(require.resolve('../report/generator/report-assets.js')); | ||
} | ||
|
||
// DevTools build system expects `globalThis` rather than setting global variables. | ||
assert.ok(code.includes('\nrequire='), 'missing browserify require stub'); | ||
code = code.replace('\nrequire=', '\nglobalThis.require='); | ||
assert.ok(!code.includes('\nrequire='), 'contained unexpected browserify require stub'); | ||
// Don't include locales in DevTools. | ||
if (isDevtools(entryPath)) { | ||
shimsObj['./locales.js'] = 'export default {}'; | ||
} | ||
|
||
fs.writeFileSync(distPath, code); | ||
for (const modulePath of modulesToIgnore) { | ||
shimsObj[modulePath] = 'export default {}'; | ||
} | ||
} | ||
|
||
/** | ||
* Minify a javascript file, in place. | ||
* @param {string} filePath | ||
*/ | ||
async function minifyScript(filePath) { | ||
const code = fs.readFileSync(filePath, 'utf-8'); | ||
const result = await terser.minify(code, { | ||
ecma: 2019, | ||
output: { | ||
comments: /^!|Prevent tsc/, | ||
max_line_len: 1000, | ||
}, | ||
// The config relies on class names for gatherers. | ||
keep_classnames: true, | ||
// Runtime.evaluate errors if function names are elided. | ||
keep_fnames: true, | ||
sourceMap: DEBUG && { | ||
content: JSON.parse(fs.readFileSync(`${filePath}.map`, 'utf-8')), | ||
url: path.basename(`${filePath}.map`), | ||
}, | ||
shimsObj[require.resolve('../package.json')] = | ||
`export const version = ${JSON.stringify(require('../package.json').version)}`; | ||
|
||
const bundle = await rollup.rollup({ | ||
input: entryPath, | ||
context: 'globalThis', | ||
plugins: [ | ||
rollupPlugins.replace({ | ||
delimiters: ['', ''], | ||
values: { | ||
'/* BUILD_REPLACE_BUNDLED_MODULES */': `[\n${bundledMapEntriesCode},\n]`, | ||
'__dirname': (id) => `'${path.relative(LH_ROOT, path.dirname(id))}'`, | ||
'__filename': (id) => `'${path.relative(LH_ROOT, id)}'`, | ||
// This package exports to default in a way that causes Rollup to get confused, | ||
// resulting in MessageFormat being undefined. | ||
'require(\'intl-messageformat\').default': 'require(\'intl-messageformat\')', | ||
// Rollup doesn't replace this, so let's manually change it to false. | ||
'require.main === module': 'false', | ||
// TODO: Use globalThis directly. | ||
'global.isLightrider': 'globalThis.isLightrider', | ||
'global.isDevtools': 'globalThis.isDevtools', | ||
}, | ||
}), | ||
rollupPlugins.alias({ | ||
entries: { | ||
'debug': require.resolve('debug/src/browser.js'), | ||
'lighthouse-logger': require.resolve('../lighthouse-logger/index.js'), | ||
'url': require.resolve('../lighthouse-core/lib/url-shim.js'), | ||
}, | ||
}), | ||
rollupPlugins.shim({ | ||
...shimsObj, | ||
// Allows for plugins to import lighthouse. | ||
'lighthouse': ` | ||
import Audit from '${require.resolve('../lighthouse-core/audits/audit.js')}'; | ||
export {Audit}; | ||
`, | ||
}), | ||
rollupPlugins.json(), | ||
rollupPlugins.inlineFs({verbose: false}), | ||
rollupPlugins.commonjs({ | ||
// https://github.com/rollup/plugins/issues/922 | ||
ignoreGlobal: true, | ||
}), | ||
rollupPlugins.nodePolyfills(), | ||
rollupPlugins.nodeResolve({preferBuiltins: true}), | ||
// Rollup sees the usages of these functions in page functions (ex: see AnchorElements) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yikes, this is going to be really annoying to remember. maybe we should tweak our or better yet always force deps to be function parameters? there are just too many gotchas these days with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm for it. link should do the trick. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO if we're going to keep the magically-interspersed Node and This would allow type checking of functions we currently I have an old branch for this I can put up for discussion since I know there's a variety of opinions on each of these points :) edit: up at #12795 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (...expanding on the fact that custom page functions outside page-functions.js exists ...) My linked idea (having a const pageFunctions = {
...require('../../lib/page-functions.js'),
myFnDeclaredHere,
};
function myFnDeclaredHere() { }
// ...
function getMetaElements() {
return pageFunctions.getElementsInDocument('head meta').map(el => {
const meta = /** @type {HTMLMetaElement} */ (el);
pageFunctions.myFnDeclaredHere(); // also i exist.
return {
name: meta.name.toLowerCase(),
content: meta.content,
property: meta.attributes.property ? meta.attributes.property.value : undefined,
httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
charset: meta.attributes.charset ? meta.attributes.charset.value : undefined,
};
});
}
const code = pageFunctions.createEvalCode(getMetaElements, {
deps: [
pageFunctions.getElementsInDocument,
pageFunctions.myFnDeclaredHere,
],
});
// `pageFunctions.createEvalCode` would inject a var `pageFunctions` with all values passed to deps, using the `.name` as keys in theory (plus removing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #12795 has not been resolved–is this blocking for this PR? |
||
// and treats them as globals. Because the names are "taken" by the global, Rollup renames | ||
// the actual functions (getNodeDetails$1). The page functions expect a certain name, so | ||
// here we undo what Rollup did. | ||
rollupPlugins.postprocess([ | ||
[/getBoundingClientRect\$1/, 'getBoundingClientRect'], | ||
[/getElementsInDocument\$1/, 'getElementsInDocument'], | ||
[/getNodeDetails\$1/, 'getNodeDetails'], | ||
[/getRectCenterPoint\$1/, 'getRectCenterPoint'], | ||
[/isPositionFixed\$1/, 'isPositionFixed'], | ||
]), | ||
opts.minify && rollupPlugins.terser({ | ||
ecma: 2019, | ||
output: { | ||
comments: (node, comment) => { | ||
const text = comment.value; | ||
if (text.includes('The Lighthouse Authors') && comment.line > 1) return false; | ||
return /@ts-nocheck - Prevent tsc|@preserve|@license|@cc_on|^!/i.test(text); | ||
}, | ||
max_line_len: 1000, | ||
}, | ||
// The config relies on class names for gatherers. | ||
keep_classnames: true, | ||
// Runtime.evaluate errors if function names are elided. | ||
keep_fnames: true, | ||
}), | ||
], | ||
}); | ||
|
||
if (result.code) { | ||
fs.writeFileSync(filePath, result.code); | ||
} | ||
if (DEBUG && typeof result.map === 'string') { | ||
fs.writeFileSync(`${filePath}.map`, result.map); | ||
} | ||
} | ||
|
||
/** | ||
* Browserify starting at entryPath, writing the minified result to distPath. | ||
* @param {string} entryPath | ||
* @param {string} distPath | ||
* @return {Promise<void>} | ||
*/ | ||
async function build(entryPath, distPath) { | ||
await browserifyFile(entryPath, distPath); | ||
await minifyScript(distPath); | ||
await bundle.write({ | ||
file: distPath, | ||
banner, | ||
format: 'iife', | ||
sourcemap: DEBUG, | ||
}); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do this from the above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's wrapped in stringify because it emits valid JS.
version = ${pkg.version}
results inversion = 8.5.1
.one could argue for
version = '${pkg.version}'
but the quotes are a little subtle. also, not relevant here but that pattern breaks when there are invalid string characters (this was a big motivator for theExecutionContext.evaluate
api).