Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
619 lines
21 KiB
Text
619 lines
21 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2019 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import path from 'path';
|
|
import {createRequire} from 'module';
|
|
import url from 'url';
|
|
|
|
import {isEqual} from 'lodash-es';
|
|
|
|
import * as constants from './constants.js';
|
|
import ConfigPlugin from './config-plugin.js';
|
|
import {Runner} from '../runner.js';
|
|
import * as i18n from '../lib/i18n/i18n.js';
|
|
import * as validation from './validation.js';
|
|
import {getModuleDirectory} from '../../shared/esm-utils.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
/** @typedef {typeof import('../gather/base-gatherer.js').default} GathererConstructor */
|
|
/** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
|
|
/** @typedef {InstanceType<GathererConstructor>} Gatherer */
|
|
|
|
function isBundledEnvironment() {
|
|
// If we're in DevTools or LightRider, we are definitely bundled.
|
|
// TODO: refactor and delete `global.isDevtools`.
|
|
if (global.isDevtools || global.isLightrider) return true;
|
|
|
|
try {
|
|
// Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
|
|
// `require.resolve` will only throw in atypical/bundled environments.
|
|
require.resolve('lighthouse-logger');
|
|
return false;
|
|
} catch (err) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If any items with identical `path` properties are found in the input array,
|
|
* merge their `options` properties into the first instance and then discard any
|
|
* other instances.
|
|
* @template {{path?: string, options: Record<string, unknown>}} T
|
|
* @param {T[]} items
|
|
* @return T[]
|
|
*/
|
|
const mergeOptionsOfItems = function(items) {
|
|
/** @type {T[]} */
|
|
const mergedItems = [];
|
|
|
|
for (const item of items) {
|
|
const existingItem = item.path && mergedItems.find(candidate => candidate.path === item.path);
|
|
if (!existingItem) {
|
|
mergedItems.push(item);
|
|
continue;
|
|
}
|
|
|
|
existingItem.options = Object.assign({}, existingItem.options, item.options);
|
|
}
|
|
|
|
return mergedItems;
|
|
};
|
|
|
|
/**
|
|
* Recursively merges config fragment objects in a somewhat Lighthouse-specific way.
|
|
*
|
|
* - `null` is treated similarly to `undefined` for whether a value should be overridden.
|
|
* - `overwriteArrays` controls array extension behavior:
|
|
* - true: Arrays are overwritten without any merging or concatenation.
|
|
* - false: Arrays are concatenated and de-duped by isEqual.
|
|
* - Objects are recursively merged.
|
|
* - If the `settings` key is encountered while traversing an object, its arrays are *always*
|
|
* overridden, not concatenated. (`overwriteArrays` is flipped to `true`)
|
|
*
|
|
* More widely typed than exposed merge() function, below.
|
|
* @param {Object<string, any>|Array<any>|undefined|null} base
|
|
* @param {Object<string, any>|Array<any>} extension
|
|
* @param {boolean=} overwriteArrays
|
|
*/
|
|
function _mergeConfigFragment(base, extension, overwriteArrays = false) {
|
|
// If the default value doesn't exist or is explicitly null, defer to the extending value
|
|
if (typeof base === 'undefined' || base === null) {
|
|
return extension;
|
|
} else if (typeof extension === 'undefined') {
|
|
return base;
|
|
} else if (Array.isArray(extension)) {
|
|
if (overwriteArrays) return extension;
|
|
if (!Array.isArray(base)) throw new TypeError(`Expected array but got ${typeof base}`);
|
|
const merged = base.slice();
|
|
extension.forEach(item => {
|
|
if (!merged.some(candidate => isEqual(candidate, item))) merged.push(item);
|
|
});
|
|
|
|
return merged;
|
|
} else if (typeof extension === 'object') {
|
|
if (typeof base !== 'object') throw new TypeError(`Expected object but got ${typeof base}`);
|
|
if (Array.isArray(base)) throw new TypeError('Expected object but got Array');
|
|
Object.keys(extension).forEach(key => {
|
|
const localOverwriteArrays = overwriteArrays ||
|
|
(key === 'settings' && typeof base[key] === 'object');
|
|
base[key] = _mergeConfigFragment(base[key], extension[key], localOverwriteArrays);
|
|
});
|
|
return base;
|
|
}
|
|
|
|
return extension;
|
|
}
|
|
|
|
/**
|
|
* Until support of jsdoc templates with constraints, type in config.d.ts.
|
|
* See https://github.com/Microsoft/TypeScript/issues/24283
|
|
* @type {LH.Config.Merge}
|
|
*/
|
|
const mergeConfigFragment = _mergeConfigFragment;
|
|
|
|
/**
|
|
* Merge an array of items by a caller-defined key. `mergeConfigFragment` is used to merge any items
|
|
* with a matching key.
|
|
*
|
|
* @template {Record<string, any>} T
|
|
* @param {Array<T>|null|undefined} baseArray
|
|
* @param {Array<T>|null|undefined} extensionArray
|
|
* @param {(item: T) => string} keyFn
|
|
* @return {Array<T>}
|
|
*/
|
|
function mergeConfigFragmentArrayByKey(baseArray, extensionArray, keyFn) {
|
|
/** @type {Map<string, {index: number, item: T}>} */
|
|
const itemsByKey = new Map();
|
|
const mergedArray = baseArray || [];
|
|
for (let i = 0; i < mergedArray.length; i++) {
|
|
const item = mergedArray[i];
|
|
itemsByKey.set(keyFn(item), {index: i, item});
|
|
}
|
|
|
|
for (const item of extensionArray || []) {
|
|
const baseItemEntry = itemsByKey.get(keyFn(item));
|
|
if (baseItemEntry) {
|
|
const baseItem = baseItemEntry.item;
|
|
const merged = typeof item === 'object' && typeof baseItem === 'object' ?
|
|
mergeConfigFragment(baseItem, item, true) :
|
|
item;
|
|
mergedArray[baseItemEntry.index] = merged;
|
|
} else {
|
|
mergedArray.push(item);
|
|
}
|
|
}
|
|
|
|
return mergedArray;
|
|
}
|
|
|
|
/**
|
|
* Expands a gatherer from user-specified to an internal gatherer definition format.
|
|
*
|
|
* Input Examples:
|
|
* - 'my-gatherer'
|
|
* - class MyGatherer extends Gatherer { }
|
|
* - {instance: myGathererInstance}
|
|
*
|
|
* @param {LH.Config.GathererJson} gatherer
|
|
* @return {{instance?: Gatherer, implementation?: GathererConstructor, path?: string}}
|
|
*/
|
|
function expandGathererShorthand(gatherer) {
|
|
if (typeof gatherer === 'string') {
|
|
// just 'path/to/gatherer'
|
|
return {path: gatherer};
|
|
} else if ('implementation' in gatherer || 'instance' in gatherer) {
|
|
// {implementation: GathererConstructor, ...} or {instance: GathererInstance, ...}
|
|
return gatherer;
|
|
} else if ('path' in gatherer) {
|
|
// {path: 'path/to/gatherer', ...}
|
|
if (typeof gatherer.path !== 'string') {
|
|
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
|
|
}
|
|
return gatherer;
|
|
} else if (typeof gatherer === 'function') {
|
|
// just GathererConstructor
|
|
return {implementation: gatherer};
|
|
} else if (gatherer && typeof gatherer.getArtifact === 'function') {
|
|
// just GathererInstance
|
|
return {instance: gatherer};
|
|
} else {
|
|
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expands the audits from user-specified JSON to an internal audit definition format.
|
|
* @param {LH.Config.AuditJson} audit
|
|
* @return {{id?: string, path: string, options?: {}} | {id?: string, implementation: Audit, path?: string, options?: {}}}
|
|
*/
|
|
function expandAuditShorthand(audit) {
|
|
if (typeof audit === 'string') {
|
|
// just 'path/to/audit'
|
|
return {path: audit, options: {}};
|
|
} else if ('implementation' in audit && typeof audit.implementation.audit === 'function') {
|
|
// {implementation: AuditClass, ...}
|
|
return audit;
|
|
} else if ('path' in audit && typeof audit.path === 'string') {
|
|
// {path: 'path/to/audit', ...}
|
|
return audit;
|
|
} else if ('audit' in audit && typeof audit.audit === 'function') {
|
|
// just AuditClass
|
|
return {implementation: audit, options: {}};
|
|
} else {
|
|
throw new Error('Invalid Audit type ' + JSON.stringify(audit));
|
|
}
|
|
}
|
|
|
|
/** @type {Map<string, Promise<any>>} */
|
|
const bundledModules = new Map(/* BUILD_REPLACE_BUNDLED_MODULES */);
|
|
|
|
/**
|
|
* Wraps `import`/`require` with an entrypoint for bundled dynamic modules.
|
|
* See build-bundle.js
|
|
* @param {string} requirePath
|
|
*/
|
|
async function requireWrapper(requirePath) {
|
|
// For windows.
|
|
if (path.isAbsolute(requirePath)) {
|
|
requirePath = url.pathToFileURL(requirePath).href;
|
|
}
|
|
|
|
/** @type {any} */
|
|
let module;
|
|
if (bundledModules.has(requirePath)) {
|
|
module = await bundledModules.get(requirePath);
|
|
} else if (requirePath.match(/\.(js|mjs|cjs)$/)) {
|
|
module = await import(requirePath);
|
|
} else {
|
|
requirePath += '.js';
|
|
module = await import(requirePath);
|
|
}
|
|
|
|
if (module.default) return module.default;
|
|
|
|
// Find a valid named export.
|
|
const methods = new Set(['meta']);
|
|
const possibleNamedExports = Object.keys(module).filter(key => {
|
|
if (!(module[key] && module[key] instanceof Object)) return false;
|
|
return Object.getOwnPropertyNames(module[key]).some(method => methods.has(method));
|
|
});
|
|
if (possibleNamedExports.length === 1) return possibleNamedExports[0];
|
|
if (possibleNamedExports.length > 1) {
|
|
throw new Error(`module '${requirePath}' has too many possible exports`);
|
|
}
|
|
|
|
throw new Error(`module '${requirePath}' missing default export`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} gathererPath
|
|
* @param {Array<string>} coreGathererList
|
|
* @param {string=} configDir
|
|
* @return {Promise<LH.Config.AnyGathererDefn>}
|
|
*/
|
|
async function requireGatherer(gathererPath, coreGathererList, configDir) {
|
|
const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);
|
|
|
|
let requirePath = `../gather/gatherers/${gathererPath}`;
|
|
if (!coreGatherer) {
|
|
// Otherwise, attempt to find it elsewhere. This throws if not found.
|
|
requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
|
|
}
|
|
|
|
const GathererClass = /** @type {GathererConstructor} */ (await requireWrapper(requirePath));
|
|
|
|
return {
|
|
instance: new GathererClass(),
|
|
implementation: GathererClass,
|
|
path: gathererPath,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} auditPath
|
|
* @param {Array<string>} coreAuditList
|
|
* @param {string=} configDir
|
|
* @return {Promise<LH.Config.AuditDefn['implementation']>}
|
|
*/
|
|
function requireAudit(auditPath, coreAuditList, configDir) {
|
|
// See if the audit is a Lighthouse core audit.
|
|
const auditPathJs = `${auditPath}.js`;
|
|
const coreAudit = coreAuditList.find(a => a === auditPathJs);
|
|
let requirePath = `../audits/${auditPath}`;
|
|
if (!coreAudit) {
|
|
if (isBundledEnvironment()) {
|
|
// This is for plugin bundling.
|
|
requirePath = auditPath;
|
|
} else {
|
|
// Otherwise, attempt to find it elsewhere. This throws if not found.
|
|
const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
|
|
if (isBundledEnvironment()) {
|
|
// Use a relative path so bundler can easily expose it.
|
|
requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
|
|
} else {
|
|
requirePath = absolutePath;
|
|
}
|
|
}
|
|
}
|
|
|
|
return requireWrapper(requirePath);
|
|
}
|
|
|
|
/**
|
|
* Creates a settings object from potential flags object by dropping all the properties
|
|
* that don't exist on Config.Settings.
|
|
* @param {Partial<LH.Flags>=} flags
|
|
* @return {LH.Util.RecursivePartial<LH.Config.Settings>}
|
|
*/
|
|
function cleanFlagsForSettings(flags = {}) {
|
|
/** @type {LH.Util.RecursivePartial<LH.Config.Settings>} */
|
|
const settings = {};
|
|
|
|
for (const key of Object.keys(flags)) {
|
|
if (key in constants.defaultSettings) {
|
|
// @ts-expect-error tsc can't yet express that key is only a single type in each iteration, not a union of types.
|
|
settings[key] = flags[key];
|
|
}
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* @param {LH.SharedFlagsSettings} settingsJson
|
|
* @param {LH.Flags|undefined} overrides
|
|
* @return {LH.Config.Settings}
|
|
*/
|
|
function resolveSettings(settingsJson = {}, overrides = undefined) {
|
|
// If a locale is requested in flags or settings, use it. A typical CLI run will not have one,
|
|
// however `lookupLocale` will always determine which of our supported locales to use (falling
|
|
// back if necessary).
|
|
// TODO: could do more work to sniff out the user's locale
|
|
const locale = i18n.lookupLocale(overrides?.locale || settingsJson.locale);
|
|
|
|
// Fill in missing settings with defaults
|
|
const {defaultSettings} = constants;
|
|
const settingWithDefaults = mergeConfigFragment(deepClone(defaultSettings), settingsJson, true);
|
|
|
|
// Override any applicable settings with CLI flags
|
|
const settingsWithFlags = mergeConfigFragment(
|
|
settingWithDefaults,
|
|
cleanFlagsForSettings(overrides),
|
|
true
|
|
);
|
|
|
|
// Locale is special and comes only from flags/settings/lookupLocale.
|
|
settingsWithFlags.locale = locale;
|
|
|
|
// Default constants uses the mobile UA. Explicitly stating to true asks LH to use the associated UA.
|
|
// It's a little awkward, but the alternatives are not allowing `true` or a dedicated `disableUAEmulation` setting.
|
|
if (settingsWithFlags.emulatedUserAgent === true) {
|
|
settingsWithFlags.emulatedUserAgent = constants.userAgents[settingsWithFlags.formFactor];
|
|
}
|
|
|
|
validation.assertValidSettings(settingsWithFlags);
|
|
return settingsWithFlags;
|
|
}
|
|
|
|
/**
|
|
* @param {LH.Config} config
|
|
* @param {string | undefined} configDir
|
|
* @param {{plugins?: string[]} | undefined} flags
|
|
* @return {Promise<LH.Config>}
|
|
*/
|
|
async function mergePlugins(config, configDir, flags) {
|
|
const configPlugins = config.plugins || [];
|
|
const flagPlugins = flags?.plugins || [];
|
|
const pluginNames = new Set([...configPlugins, ...flagPlugins]);
|
|
|
|
for (const pluginName of pluginNames) {
|
|
validation.assertValidPluginName(config, pluginName);
|
|
|
|
// In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
|
|
const pluginPath = isBundledEnvironment() ?
|
|
pluginName :
|
|
resolveModulePath(pluginName, configDir, 'plugin');
|
|
const rawPluginJson = await requireWrapper(pluginPath);
|
|
const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
|
|
|
|
config = mergeConfigFragment(config, pluginJson);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
|
|
/**
|
|
* Turns a GathererJson into a GathererDefn which involves a few main steps:
|
|
* - Expanding the JSON shorthand the full definition format.
|
|
* - `require`ing in the implementation.
|
|
* - Creating a gatherer instance from the implementation.
|
|
* @param {LH.Config.GathererJson} gathererJson
|
|
* @param {Array<string>} coreGathererList
|
|
* @param {string=} configDir
|
|
* @return {Promise<LH.Config.AnyGathererDefn>}
|
|
*/
|
|
async function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
|
|
const gathererDefn = expandGathererShorthand(gathererJson);
|
|
if (gathererDefn.instance) {
|
|
return {
|
|
instance: gathererDefn.instance,
|
|
implementation: gathererDefn.implementation,
|
|
path: gathererDefn.path,
|
|
};
|
|
} else if (gathererDefn.implementation) {
|
|
const GathererClass = gathererDefn.implementation;
|
|
return {
|
|
instance: new GathererClass(),
|
|
implementation: gathererDefn.implementation,
|
|
path: gathererDefn.path,
|
|
};
|
|
} else if (gathererDefn.path) {
|
|
const path = gathererDefn.path;
|
|
return requireGatherer(path, coreGathererList, configDir);
|
|
} else {
|
|
throw new Error('Invalid expanded Gatherer: ' + JSON.stringify(gathererDefn));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Take an array of audits and audit paths and require any paths (possibly
|
|
* relative to the optional `configDir`) using `resolveModule`,
|
|
* leaving only an array of AuditDefns.
|
|
* @param {LH.Config['audits']} audits
|
|
* @param {string=} configDir
|
|
* @return {Promise<Array<LH.Config.AuditDefn>|null>}
|
|
*/
|
|
async function resolveAuditsToDefns(audits, configDir) {
|
|
if (!audits) {
|
|
return null;
|
|
}
|
|
|
|
const coreList = Runner.getAuditList();
|
|
const auditDefnsPromises = audits.map(async (auditJson) => {
|
|
const auditDefn = expandAuditShorthand(auditJson);
|
|
let implementation;
|
|
if ('implementation' in auditDefn) {
|
|
implementation = auditDefn.implementation;
|
|
} else {
|
|
implementation = await requireAudit(auditDefn.path, coreList, configDir);
|
|
}
|
|
|
|
return {
|
|
implementation,
|
|
path: auditDefn.path,
|
|
options: auditDefn.options || {},
|
|
};
|
|
});
|
|
const auditDefns = await Promise.all(auditDefnsPromises);
|
|
|
|
const mergedAuditDefns = mergeOptionsOfItems(auditDefns);
|
|
mergedAuditDefns.forEach(audit => validation.assertValidAudit(audit));
|
|
return mergedAuditDefns;
|
|
}
|
|
|
|
/**
|
|
* Resolves the location of the specified module and returns an absolute
|
|
* string path to the file. Used for loading custom audits and gatherers.
|
|
* Throws an error if no module is found.
|
|
* @param {string} moduleIdentifier
|
|
* @param {string=} configDir The absolute path to the directory of the config file, if there is one.
|
|
* @param {string=} category Optional plugin category (e.g. 'audit') for better error messages.
|
|
* @return {string}
|
|
* @throws {Error}
|
|
*/
|
|
function resolveModulePath(moduleIdentifier, configDir, category) {
|
|
// module in a node_modules/ that is...
|
|
// | | Lighthouse globally installed | Lighthouse locally installed |
|
|
// |--------------------------------|-------------------------------|------------------------------|
|
|
// | global | 1. | 1. |
|
|
// | in current working directory | 2. | 1. |
|
|
// | relative to config.js file | 5. | - |
|
|
|
|
// module given by a path that is...
|
|
// | | Lighthouse globally/locally installed |
|
|
// |-------------------------------------------|---------------------------------------|
|
|
// | absolute | 1. |
|
|
// | relative to the current working directory | 3. |
|
|
// | relative to the config.js file | 4. |
|
|
|
|
// 1.
|
|
// First try straight `require()`. Unlikely to be specified relative to this
|
|
// file, but adds support for Lighthouse modules from npm since
|
|
// `require()` walks up parent directories looking inside any node_modules/
|
|
// present. Also handles absolute paths.
|
|
try {
|
|
return require.resolve(moduleIdentifier);
|
|
} catch (e) {}
|
|
|
|
// 2.
|
|
// Lighthouse globally installed, node_modules/ in current working directory.
|
|
// ex: lighthouse https://test.com
|
|
//
|
|
// working directory/
|
|
// |-- node_modules/
|
|
// |-- package.json
|
|
try {
|
|
return require.resolve(moduleIdentifier, {paths: [process.cwd()]});
|
|
} catch (e) {}
|
|
|
|
// 3.
|
|
// See if the module resolves relative to the current working directory.
|
|
// Most useful to handle the case of invoking Lighthouse as a module, since
|
|
// then the config is an object and so has no path.
|
|
const cwdPath = path.resolve(process.cwd(), moduleIdentifier);
|
|
try {
|
|
return require.resolve(cwdPath);
|
|
} catch (e) {}
|
|
|
|
const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
|
|
`\`${moduleIdentifier}\`.
|
|
Tried to resolve the module from these locations:
|
|
${getModuleDirectory(import.meta)}
|
|
${cwdPath}`;
|
|
|
|
if (!configDir) {
|
|
throw new Error(errorString);
|
|
}
|
|
|
|
// 4.
|
|
// Try looking up relative to the config file path. Just like the
|
|
// relative path passed to `require()` is found relative to the file it's
|
|
// in, this allows module paths to be specified relative to the config file.
|
|
const relativePath = path.resolve(configDir, moduleIdentifier);
|
|
try {
|
|
return require.resolve(relativePath);
|
|
} catch (requireError) {}
|
|
|
|
// 5.
|
|
// Lighthouse globally installed, node_modules/ in config directory.
|
|
// ex: lighthouse https://test.com --config-path=./config/config.js
|
|
//
|
|
// working directory/
|
|
// |-- config/
|
|
// |-- node_modules/
|
|
// |-- config.js
|
|
// |-- package.json
|
|
try {
|
|
return require.resolve(moduleIdentifier, {paths: [configDir]});
|
|
} catch (requireError) {}
|
|
|
|
throw new Error(errorString + `
|
|
${relativePath}`);
|
|
}
|
|
|
|
/**
|
|
* Many objects in the config can be an object whose properties are not serializable.
|
|
* We use a shallow clone for these objects instead.
|
|
* Any value that isn't an object will not be cloned.
|
|
*
|
|
* @template T
|
|
* @param {T} item
|
|
* @return {T}
|
|
*/
|
|
function shallowClone(item) {
|
|
if (typeof item === 'object') {
|
|
// Return copy of instance and prototype chain (in case item is instantiated class).
|
|
return Object.assign(
|
|
Object.create(
|
|
Object.getPrototypeOf(item)
|
|
),
|
|
item
|
|
);
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* // TODO(bckenny): could adopt "jsonified" type to ensure T will survive JSON
|
|
* round trip: https://github.com/Microsoft/TypeScript/issues/21838
|
|
* @template T
|
|
* @param {T} json
|
|
* @return {T}
|
|
*/
|
|
function deepClone(json) {
|
|
return JSON.parse(JSON.stringify(json));
|
|
}
|
|
|
|
/**
|
|
* Deep clone a config, copying over any "live" gatherer or audit that
|
|
* wouldn't make the JSON round trip.
|
|
* @param {LH.Config} json
|
|
* @return {LH.Config}
|
|
*/
|
|
function deepCloneConfigJson(json) {
|
|
const cloned = deepClone(json);
|
|
|
|
// Copy arrays that could contain non-serializable properties to allow for programmatic
|
|
// injection of audit and gatherer implementations.
|
|
if (Array.isArray(json.audits)) {
|
|
cloned.audits = json.audits.map(audit => shallowClone(audit));
|
|
}
|
|
|
|
if (Array.isArray(json.artifacts)) {
|
|
cloned.artifacts = json.artifacts.map(artifact => ({
|
|
...artifact,
|
|
gatherer: shallowClone(artifact.gatherer),
|
|
}));
|
|
}
|
|
|
|
return cloned;
|
|
}
|
|
|
|
export {
|
|
deepClone,
|
|
deepCloneConfigJson,
|
|
mergeConfigFragment,
|
|
mergeConfigFragmentArrayByKey,
|
|
mergeOptionsOfItems,
|
|
mergePlugins,
|
|
resolveAuditsToDefns,
|
|
resolveGathererToDefn,
|
|
resolveModulePath,
|
|
resolveSettings,
|
|
};
|