-
-
Notifications
You must be signed in to change notification settings - Fork 87
Description
Detail Bug Report
Summary
- Context: The
get-config.jsmodule loads lighthouse preset configurations and allows users to customize settings by merging user-provided settings with preset defaults. - Bug: The function mutates the cached preset configuration object instead of creating a new config object, causing settings from one call to leak into subsequent calls that use the same preset.
- Actual vs. expected: Settings from previous calls persist in the cached preset object, whereas each call should receive an isolated configuration based only on the preset defaults and the current call's parameters.
- Impact: In multi-request scenarios (production servers, CLI tools with multiple runs), user-provided settings from one request pollute subsequent requests using the same preset, leading to incorrect lighthouse configurations and potentially incorrect audit results.
Code with Bug
module.exports = async ({ preset: presetName, ...settings } = {}) => {
const config = (await preset(presetName)) || { extends: 'lighthouse:default' }
if (Object.keys(settings).length > 0) config.settings = { ...config.settings, ...settings } // <-- BUG 🔴 mutates cached preset config object via module cache
return config
}Explanation
Preset configs are loaded via dynamic import(), which is cached by Node.js. config is a reference to the cached preset object; assigning config.settings = ... updates that cached object. Subsequent calls using the same preset in the same process therefore start from the already-modified settings instead of the preset defaults.
Failing Test
Repro shown via two sequential calls:
- Call 1:
getConfig({ preset: 'lr-desktop', skipAudits: ['custom-audit-1'], output: 'html' })producesconfig1.settings.skipAudits === ['custom-audit-1']. - Call 2:
getConfig({ preset: 'lr-desktop', output: 'json' })expected preset defaults (e.g.['modern-http-insight','bf-cache']), but actual is['custom-audit-1'](polluted from call 1).
Module-cache mutation is confirmed by importing the same preset module before/after calling getConfig and observing original.default.settings.output change while original1.default === original2.default remains true.
Recommended Fix
Copy the preset config (and settings) before merging user overrides so the cached preset object is never mutated:
module.exports = async ({ preset: presetName, ...settings } = {}) => {
const presetConfig = (await preset(presetName)) || { extends: 'lighthouse:default' }
const config = { ...presetConfig }
if (Object.keys(settings).length > 0) {
config.settings = { ...presetConfig.settings, ...settings }
}
return config
}History
This bug was introduced in commit 75c31e5, which refactored config loading from synchronous require() to cached async import() and changed behavior from returning a new config object to mutating the imported preset config (config.settings = {...}), enabling cross-call settings pollution.