const fs = require('fs'); const path = require('path'); const { SourceMapConsumer } = require('source-map'); const pify = require('pify'); const { codeFrameColumns } = require('@babel/code-frame'); const fsAsync = pify(fs); // // Utility to help check if sourcemaps are working // // searches `dist/chrome/inpage.js` for "new Error" statements // and prints their source lines using the sourcemaps. // if not working it may error or print minified garbage // start().catch((error) => { console.error(error); process.exit(1); }); async function start() { const targetFiles = [ `common-0.js`, `background-0.js`, `ui-0.js`, 'phishing-detect.js', // `contentscript.js`, skipped because the validator is erroneously sampling the inlined `inpage.js` script `inpage.js`, ]; let valid = true; for (const buildName of targetFiles) { const fileIsValid = await validateSourcemapForFile({ buildName }); valid = valid && fileIsValid; } if (!valid) { process.exit(1); } } async function validateSourcemapForFile({ buildName }) { console.log(`build "${buildName}"`); const platform = `chrome`; // load build and sourcemaps let rawBuild; try { const filePath = path.join( __dirname, `/../dist/${platform}/`, `${buildName}`, ); rawBuild = await fsAsync.readFile(filePath, 'utf8'); } catch (_) { // empty } if (!rawBuild) { throw new Error( `SourcemapValidator - failed to load source file for "${buildName}"`, ); } // attempt to load in dist mode let rawSourceMap; try { const filePath = path.join( __dirname, `/../dist/sourcemaps/`, `${buildName}.map`, ); rawSourceMap = await fsAsync.readFile(filePath, 'utf8'); } catch (_) { // empty } // attempt to load in dev mode try { const filePath = path.join( __dirname, `/../dist/${platform}/`, `${buildName}.map`, ); rawSourceMap = await fsAsync.readFile(filePath, 'utf8'); } catch (_) { // empty } if (!rawSourceMap) { throw new Error( `SourcemapValidator - failed to load sourcemaps for "${buildName}"`, ); } const consumer = await new SourceMapConsumer(rawSourceMap); const hasContentsOfAllSources = consumer.hasContentsOfAllSources(); if (!hasContentsOfAllSources) { console.warn('SourcemapValidator - missing content of some sources...'); } console.log(` sampling from ${consumer.sources.length} files`); let sampleCount = 0; let valid = true; const buildLines = rawBuild.split('\n'); const targetString = 'new Error'; const matchesPerLine = buildLines.map((line) => indicesOf(targetString, line), ); matchesPerLine.forEach((matchIndices, lineIndex) => { matchIndices.forEach((matchColumn) => { sampleCount += 1; const position = { line: lineIndex + 1, column: matchColumn }; const result = consumer.originalPositionFor(position); // warn if source content is missing if (!result.source) { valid = false; const location = { start: { line: position.line, column: position.column + 1 }, }; const codeSample = codeFrameColumns(rawBuild, location, { message: `missing source for position`, highlightCode: true, }); console.error( `missing source for position, in bundle "${buildName}"\n${codeSample}`, ); return; } const sourceContent = consumer.sourceContentFor(result.source); const sourceLines = sourceContent.split('\n'); const sourceLine = sourceLines[result.line - 1]; // this sometimes includes the whole line though we tried to match somewhere in the middle const portion = sourceLine.slice(result.column); const foundValidSource = portion.includes(targetString); if (!foundValidSource) { valid = false; const location = { start: { line: result.line + 1, column: result.column + 1 }, }; const codeSample = codeFrameColumns(sourceContent, location, { message: `expected to see ${JSON.stringify(targetString)}`, highlightCode: true, }); console.error( `Sourcemap seems invalid, ${result.source}\n${codeSample}`, ); } }); }); console.log(` checked ${sampleCount} samples`); return valid; } function indicesOf(substring, string) { const a = []; let i = -1; while ((i = string.indexOf(substring, i + 1)) >= 0) { a.push(i); } return a; }