Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[js-code-coverage] Create merge_js_source_maps rule
During sourcemap generation there is a need to merge multiple sourcemaps when a rule doesn't provide support ingesting and transforming a generated file. This build rule is initially built to support merging sourcemaps appended via tsc. This will read the input file, check for multiple sourcemaps appended to the end of the file and in the case multiple are identified they get merged. These files are then sent to the output directory and the .manifest file updates its paths to point to the rewritten files. Bug: 1337530 Test: ./merge_js_source_maps_test.py Change-Id: Ice757ffd6382cb14b3eb56dbf73542cbde086df1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3810757 Commit-Queue: Ben Reich <benreich@chromium.org> Reviewed-by: Tibor Goldschwendt <tiborg@chromium.org> Reviewed-by: Bruce Dawson <brucedawson@chromium.org> Cr-Commit-Position: refs/heads/main@{#1033422}
- Loading branch information
Showing
8 changed files
with
465 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
benreich@chromium.org | ||
tiborg@chromium.org |
31 changes: 31 additions & 0 deletions
31
tools/code_coverage/merge_js_source_maps/merge_js_source_maps.gni
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Copyright 2022 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
import("//ui/webui/webui_features.gni") | ||
|
||
template("merge_js_source_maps") { | ||
assert(enable_webui_inline_sourcemaps) | ||
|
||
action(target_name) { | ||
forward_variables_from(invoker, | ||
[ | ||
"sources", | ||
"outputs", | ||
"deps", | ||
]) | ||
script = | ||
"//tools/code_coverage/merge_js_source_maps/merge_js_source_maps.py" | ||
args = [ "--manifest-files" ] + | ||
rebase_path(invoker.manifest_files, root_out_dir) + [ "--sources" ] + | ||
rebase_path(invoker.sources, root_out_dir) + [ "--outputs" ] + | ||
rebase_path(invoker.outputs, root_out_dir) | ||
inputs = | ||
[ "//tools/code_coverage/merge_js_source_maps/merge_js_source_maps.js" ] | ||
foreach(manifest, invoker.manifest_files) { | ||
outputs += [ get_path_info(manifest, "dir") + "/" + | ||
get_path_info(manifest, "name") + "__processed." + | ||
get_path_info(manifest, "extension") ] | ||
} | ||
} | ||
} |
172 changes: 172 additions & 0 deletions
172
tools/code_coverage/merge_js_source_maps/merge_js_source_maps.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// Copyright 2022 The Chromium Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
/** | ||
* @fileoverview Merges 2 inline sourcemaps for a input file and writes a new | ||
* file to an output directory. | ||
*/ | ||
|
||
import fs from 'fs'; | ||
import path from 'path'; | ||
|
||
import {ArgumentParser} from '../../../third_party/js_code_coverage/node_modules/argparse/argparse.js'; | ||
import {SourceMapConsumer, SourceMapGenerator} from '../../../third_party/js_code_coverage/node_modules/source-map/source-map.js'; | ||
|
||
/** | ||
* The prefix comment that indicates a data URL containing the sourcemap. | ||
*/ | ||
const SOURCEMAPPING_DATA_URL_PREFIX = | ||
'//# sourceMappingURL=data:application/json;base64,'; | ||
|
||
/** | ||
* Decode a base64 encoded string representing a sourcemap to its utf-8 | ||
* equivalent. | ||
* @param {string} contents Base64 encoded string. | ||
* @returns Decoded utf-8 string of the sourcemap. | ||
*/ | ||
function decodeBase64SourceMap(contents) { | ||
const removedLeadingComment = | ||
contents.replace(SOURCEMAPPING_DATA_URL_PREFIX, ''); | ||
const buf = Buffer.from(removedLeadingComment, 'base64'); | ||
return buf.toString('utf-8'); | ||
} | ||
|
||
/** | ||
* Helper to identify if a supplied line is an inline sourcemap. | ||
* @param {string} lineContents Contents of an individual line. | ||
* @returns True if line is an inline sourcemap, null otherwise. | ||
*/ | ||
function isSourceMapComment(lineContents) { | ||
return lineContents && lineContents.startsWith(SOURCEMAPPING_DATA_URL_PREFIX); | ||
} | ||
|
||
/** | ||
* Convert `contents` into a valid dataURL sourceMappingURL comment. | ||
* @param {string} contents A string representation of the sourcemap | ||
* @returns A base64 encoded dataURL with the `SOURCEMAPPING_DATA_URL_PREFIX` | ||
* prepended. | ||
*/ | ||
function encodeBase64SourceMap(contents) { | ||
const buf = Buffer.from(contents, 'utf-8'); | ||
return SOURCEMAPPING_DATA_URL_PREFIX + buf.toString('base64'); | ||
} | ||
|
||
/** | ||
* Merge multiple sourcemaps to a single file. | ||
* @param {!Array<string>} sourceMaps An array of stringified sourcemaps. | ||
* @returns Returns a single sourcemap as a string. | ||
*/ | ||
async function mergeSourcemaps(sourceMaps) { | ||
let generator = null; | ||
let originalSource = null; | ||
for (const sourcemap of sourceMaps) { | ||
const parsedMap = JSON.parse(sourcemap); | ||
if (!originalSource) { | ||
originalSource = parsedMap.sources[0]; | ||
} | ||
const consumer = await new SourceMapConsumer(parsedMap); | ||
if (generator) { | ||
generator.applySourceMap(consumer, originalSource); | ||
} else { | ||
generator = await SourceMapGenerator.fromSourceMap(consumer); | ||
} | ||
consumer.destroy(); | ||
} | ||
return generator.toString(); | ||
} | ||
|
||
/** | ||
* Processes all input files for multiple inlined sourcemaps and merges them. | ||
* @param {!Array<string>} inputFiles The list of TS / JS files to extract | ||
* sourcemaps from. | ||
*/ | ||
async function processFiles(inputFiles, outputFiles) { | ||
for (let i = 0; i < inputFiles.length; i++) { | ||
const inputFile = inputFiles[i]; | ||
const outputFile = outputFiles[i]; | ||
const fileContents = fs.readFileSync(inputFile, 'utf-8'); | ||
const inputLines = fileContents.split('\n'); | ||
|
||
// Skip any trailing blank lines to find the last non-null line. | ||
let lastNonNullLine = inputLines.length - 1; | ||
while (inputLines[lastNonNullLine].trim().length === 0 && | ||
lastNonNullLine > 0) { | ||
lastNonNullLine--; | ||
} | ||
|
||
// If the last non-null line identified is not a sourcemap, ignore this file | ||
// as it may have erroneously been marked for sourcemap merge. | ||
if (!isSourceMapComment(inputLines[lastNonNullLine])) { | ||
console.warn('Supplied file has no inline sourcemap', inputFile); | ||
fs.copyFileSync(inputFile, outputFile); | ||
continue; | ||
} | ||
|
||
// Extract out all the inline sourcemaps and decode them to their string | ||
// equivalent. | ||
const sourceMaps = [decodeBase64SourceMap(inputLines[lastNonNullLine])]; | ||
let sourceMapLineIdx = lastNonNullLine - 1; | ||
while (isSourceMapComment(inputLines[sourceMapLineIdx]) && | ||
sourceMapLineIdx > 0) { | ||
const sourceMap = decodeBase64SourceMap(inputLines[sourceMapLineIdx]); | ||
sourceMaps.push(sourceMap); | ||
sourceMapLineIdx--; | ||
} | ||
|
||
let mergedSourceMap = null; | ||
try { | ||
mergedSourceMap = await mergeSourcemaps(sourceMaps); | ||
} catch (e) { | ||
console.error(`Failed to merge inlined sourcemaps for ${inputFile}:`, e); | ||
fs.copyFileSync(inputFile, outputFile); | ||
continue; | ||
} | ||
|
||
// Drop off the lines that were previously identified as inline sourcemap | ||
// comments and replace them with the merged sourcemap. | ||
let finalFileContent = | ||
inputLines.slice(0, sourceMapLineIdx + 1).join('\n') + '\n'; | ||
if (mergedSourceMap) { | ||
finalFileContent += encodeBase64SourceMap(mergedSourceMap); | ||
} | ||
fs.writeFileSync(outputFile, finalFileContent); | ||
} | ||
} | ||
|
||
async function main() { | ||
const parser = | ||
new ArgumentParser({description: 'Merge multiple inlined sourcemaps'}); | ||
|
||
parser.add_argument('--sources', {help: 'Input files', nargs: '*'}); | ||
parser.add_argument('--outputs', {help: 'Output files', nargs: '*'}); | ||
parser.add_argument( | ||
'--manifest-files', {help: 'Output files', nargs: '*', required: false}); | ||
|
||
const argv = parser.parse_args(); | ||
await processFiles(argv.sources, argv.outputs); | ||
|
||
if (argv.manifest_files) { | ||
// TODO(crbug/1337530): Currently we just remove the final directory of the | ||
// `base_dir` key. This is definitely brittle and also subject to changes | ||
// made to the output directory. Consider updating this to be more robust. | ||
for (const manifestFile of argv.manifest_files) { | ||
try { | ||
const manifestFileContents = | ||
fs.readFileSync(manifestFile).toString('utf-8'); | ||
const manifest = JSON.parse(manifestFileContents); | ||
manifest.base_dir = path.parse(manifest.base_dir).dir; | ||
const parsedPath = path.parse(manifestFile); | ||
fs.writeFileSync( | ||
path.join( | ||
parsedPath.dir, | ||
(parsedPath.name + '__processed' + parsedPath.ext)), | ||
JSON.stringify(manifest)); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
} | ||
} | ||
} | ||
|
||
(async () => main())(); |
32 changes: 32 additions & 0 deletions
32
tools/code_coverage/merge_js_source_maps/merge_js_source_maps.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
#!/usr/bin/env vpython3 | ||
# Copyright 2022 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
import argparse | ||
import sys | ||
from pathlib import Path | ||
|
||
_HERE_DIR = Path(__file__).parent | ||
_SOURCE_MAP_MERGER = (_HERE_DIR / 'merge_js_source_maps.js').resolve() | ||
|
||
_NODE_PATH = (_HERE_DIR.parent.parent.parent / 'third_party' / 'node').resolve() | ||
sys.path.append(str(_NODE_PATH)) | ||
import node | ||
|
||
|
||
def main(argv): | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument('--sources', required=True, nargs="*") | ||
parser.add_argument('--outputs', required=True, nargs="*") | ||
parser.add_argument('--manifest-files', required=True, nargs="*") | ||
args = parser.parse_args(argv) | ||
|
||
node.RunNode([ | ||
str(_SOURCE_MAP_MERGER), '--manifest-files', *args.manifest_files, | ||
'--sources', *args.sources, '--outputs', *args.outputs | ||
]) | ||
|
||
|
||
if __name__ == '__main__': | ||
main(sys.argv[1:]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"name": "merge_js_source_maps", | ||
"version": "0.1", | ||
"description": "Merge 2 inline sourcemaps", | ||
"main": "merge_js_source_maps.js", | ||
"files": [ "merge_js_source_maps.js" ], | ||
"license": "SEE LICENSE IN ../../../LICENSE", | ||
"type" : "module" | ||
} |
32 changes: 32 additions & 0 deletions
32
tools/code_coverage/merge_js_source_maps/test/generated_file_pre_merge.js
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.