UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

158 lines (137 loc) 6.48 kB
/** * @license Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {Audit} from './audit.js'; import {EntityClassification} from '../computed/entity-classification.js'; import * as i18n from '../lib/i18n/i18n.js'; import {Util} from '../../shared/util.js'; import UrlUtils from '../lib/url-utils.js'; const UIStrings = { /** Title of a Lighthouse audit that provides detail on HTTP to HTTPS redirects. This descriptive title is shown to users when HTTP traffic is redirected to HTTPS. */ title: 'Page has valid source maps', /** Title of a Lighthouse audit that provides detail on HTTP to HTTPS redirects. This descriptive title is shown to users when HTTP traffic is not redirected to HTTPS. */ failureTitle: 'Missing source maps for large first-party JavaScript', /** Description of a Lighthouse audit that tells the user that their JavaScript source maps are invalid or missing. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'Source maps translate minified code to the original source code. This helps ' + 'developers debug in production. In addition, Lighthouse is able to provide further ' + 'insights. Consider deploying source maps to take advantage of these benefits. ' + '[Learn more about source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps/).', /** Label for a column in a data table. Entries will be URLs to JavaScript source maps. */ columnMapURL: 'Map URL', /** Label for a possible error message indicating that a source map for a large, first-party JavaScript script is missing. */ missingSourceMapErrorMessage: 'Large JavaScript file is missing a source map', /** Label for a possible error message indicating that the content of a source map is invalid because it is missing items in the sourcesContent attribute. */ missingSourceMapItemsWarningMesssage: `{missingItems, plural, =1 {Warning: missing 1 item in \`.sourcesContent\`} other {Warning: missing # items in \`.sourcesContent\`} }`, }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); const LARGE_JS_BYTE_THRESHOLD = 500 * 1024; class ValidSourceMaps extends Audit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'valid-source-maps', title: str_(UIStrings.title), failureTitle: str_(UIStrings.failureTitle), description: str_(UIStrings.description), requiredArtifacts: ['Scripts', 'SourceMaps', 'URL', 'DevtoolsLog'], }; } /** * Returns true if the size of the script exceeds a static threshold. * @param {LH.Artifacts.Script} script * @param {LH.Artifacts.EntityClassification} classifiedEntities * @return {boolean} */ static isLargeFirstPartyJS(script, classifiedEntities) { const url = script.url; if (!script.length || !url) return false; if (!UrlUtils.isValid(url)) return false; if (!Util.createOrReturnURL(url).protocol.startsWith('http')) return false; const isLargeJS = script.length >= LARGE_JS_BYTE_THRESHOLD; return classifiedEntities.isFirstParty(url) && isLargeJS; } /** * @param {LH.Artifacts} artifacts * @param {LH.Audit.Context} context */ static async audit(artifacts, context) { const {SourceMaps} = artifacts; const devtoolsLog = artifacts.DevtoolsLog; const classifiedEntities = await EntityClassification.request( {URL: artifacts.URL, devtoolsLog}, context); /** @type {Set<string>} */ const isMissingMapForLargeFirstPartyScriptUrl = new Set(); let missingMapsForLargeFirstPartyFile = false; const results = []; for (const script of artifacts.Scripts) { const sourceMap = SourceMaps.find(m => m.scriptId === script.scriptId); const errors = []; const isLargeFirstParty = this.isLargeFirstPartyJS(script, classifiedEntities); if (isLargeFirstParty && (!sourceMap || !sourceMap.map)) { missingMapsForLargeFirstPartyFile = true; isMissingMapForLargeFirstPartyScriptUrl.add(script.url); errors.push({error: str_(UIStrings.missingSourceMapErrorMessage)}); } if (sourceMap && !sourceMap.map) { errors.push({error: sourceMap.errorMessage}); } // Sources content errors. if (sourceMap?.map) { const sourcesContent = sourceMap.map.sourcesContent || []; let missingSourcesContentCount = 0; for (let i = 0; i < sourceMap.map.sources.length; i++) { if (sourcesContent.length < i || !sourcesContent[i]) missingSourcesContentCount += 1; } if (missingSourcesContentCount > 0) { errors.push({error: str_(UIStrings.missingSourceMapItemsWarningMesssage, {missingItems: missingSourcesContentCount})}); } } if (sourceMap || errors.length) { results.push({ scriptUrl: script.url, sourceMapUrl: sourceMap?.sourceMapUrl, subItems: { type: /** @type {const} */ ('subitems'), items: errors, }, }); } } /** @type {LH.Audit.Details.TableColumnHeading[]} */ const headings = [ { key: 'scriptUrl', valueType: 'url', subItemsHeading: {key: 'error'}, label: str_(i18n.UIStrings.columnURL), }, {key: 'sourceMapUrl', valueType: 'url', label: str_(UIStrings.columnMapURL)}, ]; results.sort((a, b) => { // Show the items that can fail the audit first. const missingMapA = isMissingMapForLargeFirstPartyScriptUrl.has(a.scriptUrl); const missingMapB = isMissingMapForLargeFirstPartyScriptUrl.has(b.scriptUrl); if (missingMapA && !missingMapB) return -1; if (!missingMapA && missingMapB) return 1; // Then sort by whether one has errors and the other doesn't. if (a.subItems.items.length && !b.subItems.items.length) return -1; if (!a.subItems.items.length && b.subItems.items.length) return 1; // Then sort by script url. return b.scriptUrl.localeCompare(a.scriptUrl); }); // Only fails if `missingMapsForLargeFirstPartyFile` is true. All other errors are diagnostical. return { score: missingMapsForLargeFirstPartyFile ? 0 : 1, details: Audit.makeTableDetails(headings, results), }; } } export default ValidSourceMaps; export {UIStrings};