mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-05 03:21:16 +08:00
We plan to enable source maps for release builds behind a GN flag. For this to work rollup needs to consume the source maps already generated by tsc and merge it with its own source map. Rollup can't do this natively so this CL adds a plugin that can. R=jacktfranklin@chromium.org Bug: 1430396 Change-Id: Ibb38c7892326907431e54b7c7dbd7b912b59ccb9 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4397707 Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Simon Zünd <szuend@chromium.org>
378 lines
9.5 KiB
JavaScript
378 lines
9.5 KiB
JavaScript
var atob = require("atob")
|
|
var urlLib = require("url")
|
|
var pathLib = require("path")
|
|
var decodeUriComponentLib = require("decode-uri-component")
|
|
|
|
|
|
|
|
function resolveUrl(/* ...urls */) {
|
|
return Array.prototype.reduce.call(arguments, function(resolved, nextUrl) {
|
|
return urlLib.resolve(resolved, nextUrl)
|
|
})
|
|
}
|
|
|
|
function convertWindowsPath(aPath) {
|
|
return pathLib.sep === "\\" ? aPath.replace(/\\/g, "/").replace(/^[a-z]:\/?/i, "/") : aPath
|
|
}
|
|
|
|
function customDecodeUriComponent(string) {
|
|
// `decodeUriComponentLib` turns `+` into ` `, but that's not wanted.
|
|
return decodeUriComponentLib(string.replace(/\+/g, "%2B"))
|
|
}
|
|
|
|
function callbackAsync(callback, error, result) {
|
|
setImmediate(function() { callback(error, result) })
|
|
}
|
|
|
|
function parseMapToJSON(string, data) {
|
|
try {
|
|
return JSON.parse(string.replace(/^\)\]\}'/, ""))
|
|
} catch (error) {
|
|
error.sourceMapData = data
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function readSync(read, url, data) {
|
|
var readUrl = customDecodeUriComponent(url)
|
|
try {
|
|
return String(read(readUrl))
|
|
} catch (error) {
|
|
error.sourceMapData = data
|
|
throw error
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var innerRegex = /[#@] sourceMappingURL=([^\s'"]*)/
|
|
|
|
var sourceMappingURLRegex = RegExp(
|
|
"(?:" +
|
|
"/\\*" +
|
|
"(?:\\s*\r?\n(?://)?)?" +
|
|
"(?:" + innerRegex.source + ")" +
|
|
"\\s*" +
|
|
"\\*/" +
|
|
"|" +
|
|
"//(?:" + innerRegex.source + ")" +
|
|
")" +
|
|
"\\s*"
|
|
)
|
|
|
|
function getSourceMappingUrl(code) {
|
|
var match = code.match(sourceMappingURLRegex)
|
|
return match ? match[1] || match[2] || "" : null
|
|
}
|
|
|
|
|
|
|
|
function resolveSourceMap(code, codeUrl, read, callback) {
|
|
var mapData
|
|
try {
|
|
mapData = resolveSourceMapHelper(code, codeUrl)
|
|
} catch (error) {
|
|
return callbackAsync(callback, error)
|
|
}
|
|
if (!mapData || mapData.map) {
|
|
return callbackAsync(callback, null, mapData)
|
|
}
|
|
var readUrl = customDecodeUriComponent(mapData.url)
|
|
read(readUrl, function(error, result) {
|
|
if (error) {
|
|
error.sourceMapData = mapData
|
|
return callback(error)
|
|
}
|
|
mapData.map = String(result)
|
|
try {
|
|
mapData.map = parseMapToJSON(mapData.map, mapData)
|
|
} catch (error) {
|
|
return callback(error)
|
|
}
|
|
callback(null, mapData)
|
|
})
|
|
}
|
|
|
|
function resolveSourceMapSync(code, codeUrl, read) {
|
|
var mapData = resolveSourceMapHelper(code, codeUrl)
|
|
if (!mapData || mapData.map) {
|
|
return mapData
|
|
}
|
|
mapData.map = readSync(read, mapData.url, mapData)
|
|
mapData.map = parseMapToJSON(mapData.map, mapData)
|
|
return mapData
|
|
}
|
|
|
|
var dataUriRegex = /^data:([^,;]*)(;[^,;]*)*(?:,(.*))?$/
|
|
|
|
/**
|
|
* The media type for JSON text is application/json.
|
|
*
|
|
* {@link https://tools.ietf.org/html/rfc8259#section-11 | IANA Considerations }
|
|
*
|
|
* `text/json` is non-standard media type
|
|
*/
|
|
var jsonMimeTypeRegex = /^(?:application|text)\/json$/
|
|
|
|
/**
|
|
* JSON text exchanged between systems that are not part of a closed ecosystem
|
|
* MUST be encoded using UTF-8.
|
|
*
|
|
* {@link https://tools.ietf.org/html/rfc8259#section-8.1 | Character Encoding}
|
|
*/
|
|
var jsonCharacterEncoding = "utf-8"
|
|
|
|
function base64ToBuf(b64) {
|
|
var binStr = atob(b64)
|
|
var len = binStr.length
|
|
var arr = new Uint8Array(len)
|
|
for (var i = 0; i < len; i++) {
|
|
arr[i] = binStr.charCodeAt(i)
|
|
}
|
|
return arr
|
|
}
|
|
|
|
function decodeBase64String(b64) {
|
|
if (typeof TextDecoder === "undefined" || typeof Uint8Array === "undefined") {
|
|
return atob(b64)
|
|
}
|
|
var buf = base64ToBuf(b64);
|
|
// Note: `decoder.decode` method will throw a `DOMException` with the
|
|
// `"EncodingError"` value when an coding error is found.
|
|
var decoder = new TextDecoder(jsonCharacterEncoding, {fatal: true})
|
|
return decoder.decode(buf);
|
|
}
|
|
|
|
function resolveSourceMapHelper(code, codeUrl) {
|
|
codeUrl = convertWindowsPath(codeUrl)
|
|
|
|
var url = getSourceMappingUrl(code)
|
|
if (!url) {
|
|
return null
|
|
}
|
|
|
|
var dataUri = url.match(dataUriRegex)
|
|
if (dataUri) {
|
|
var mimeType = dataUri[1] || "text/plain"
|
|
var lastParameter = dataUri[2] || ""
|
|
var encoded = dataUri[3] || ""
|
|
var data = {
|
|
sourceMappingURL: url,
|
|
url: null,
|
|
sourcesRelativeTo: codeUrl,
|
|
map: encoded
|
|
}
|
|
if (!jsonMimeTypeRegex.test(mimeType)) {
|
|
var error = new Error("Unuseful data uri mime type: " + mimeType)
|
|
error.sourceMapData = data
|
|
throw error
|
|
}
|
|
try {
|
|
data.map = parseMapToJSON(
|
|
lastParameter === ";base64" ? decodeBase64String(encoded) : decodeURIComponent(encoded),
|
|
data
|
|
)
|
|
} catch (error) {
|
|
error.sourceMapData = data
|
|
throw error
|
|
}
|
|
return data
|
|
}
|
|
|
|
var mapUrl = resolveUrl(codeUrl, url)
|
|
return {
|
|
sourceMappingURL: url,
|
|
url: mapUrl,
|
|
sourcesRelativeTo: mapUrl,
|
|
map: null
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function resolveSources(map, mapUrl, read, options, callback) {
|
|
if (typeof options === "function") {
|
|
callback = options
|
|
options = {}
|
|
}
|
|
var pending = map.sources ? map.sources.length : 0
|
|
var result = {
|
|
sourcesResolved: [],
|
|
sourcesContent: []
|
|
}
|
|
|
|
if (pending === 0) {
|
|
callbackAsync(callback, null, result)
|
|
return
|
|
}
|
|
|
|
var done = function() {
|
|
pending--
|
|
if (pending === 0) {
|
|
callback(null, result)
|
|
}
|
|
}
|
|
|
|
resolveSourcesHelper(map, mapUrl, options, function(fullUrl, sourceContent, index) {
|
|
result.sourcesResolved[index] = fullUrl
|
|
if (typeof sourceContent === "string") {
|
|
result.sourcesContent[index] = sourceContent
|
|
callbackAsync(done, null)
|
|
} else {
|
|
var readUrl = customDecodeUriComponent(fullUrl)
|
|
read(readUrl, function(error, source) {
|
|
result.sourcesContent[index] = error ? error : String(source)
|
|
done()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function resolveSourcesSync(map, mapUrl, read, options) {
|
|
var result = {
|
|
sourcesResolved: [],
|
|
sourcesContent: []
|
|
}
|
|
|
|
if (!map.sources || map.sources.length === 0) {
|
|
return result
|
|
}
|
|
|
|
resolveSourcesHelper(map, mapUrl, options, function(fullUrl, sourceContent, index) {
|
|
result.sourcesResolved[index] = fullUrl
|
|
if (read !== null) {
|
|
if (typeof sourceContent === "string") {
|
|
result.sourcesContent[index] = sourceContent
|
|
} else {
|
|
var readUrl = customDecodeUriComponent(fullUrl)
|
|
try {
|
|
result.sourcesContent[index] = String(read(readUrl))
|
|
} catch (error) {
|
|
result.sourcesContent[index] = error
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
var endingSlash = /\/?$/
|
|
|
|
function resolveSourcesHelper(map, mapUrl, options, fn) {
|
|
options = options || {}
|
|
mapUrl = convertWindowsPath(mapUrl)
|
|
var fullUrl
|
|
var sourceContent
|
|
var sourceRoot
|
|
for (var index = 0, len = map.sources.length; index < len; index++) {
|
|
sourceRoot = null
|
|
if (typeof options.sourceRoot === "string") {
|
|
sourceRoot = options.sourceRoot
|
|
} else if (typeof map.sourceRoot === "string" && options.sourceRoot !== false) {
|
|
sourceRoot = map.sourceRoot
|
|
}
|
|
// If the sourceRoot is the empty string, it is equivalent to not setting
|
|
// the property at all.
|
|
if (sourceRoot === null || sourceRoot === '') {
|
|
fullUrl = resolveUrl(mapUrl, map.sources[index])
|
|
} else {
|
|
// Make sure that the sourceRoot ends with a slash, so that `/scripts/subdir` becomes
|
|
// `/scripts/subdir/<source>`, not `/scripts/<source>`. Pointing to a file as source root
|
|
// does not make sense.
|
|
fullUrl = resolveUrl(mapUrl, sourceRoot.replace(endingSlash, "/"), map.sources[index])
|
|
}
|
|
sourceContent = (map.sourcesContent || [])[index]
|
|
fn(fullUrl, sourceContent, index)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function resolve(code, codeUrl, read, options, callback) {
|
|
if (typeof options === "function") {
|
|
callback = options
|
|
options = {}
|
|
}
|
|
if (code === null) {
|
|
var mapUrl = codeUrl
|
|
var data = {
|
|
sourceMappingURL: null,
|
|
url: mapUrl,
|
|
sourcesRelativeTo: mapUrl,
|
|
map: null
|
|
}
|
|
var readUrl = customDecodeUriComponent(mapUrl)
|
|
read(readUrl, function(error, result) {
|
|
if (error) {
|
|
error.sourceMapData = data
|
|
return callback(error)
|
|
}
|
|
data.map = String(result)
|
|
try {
|
|
data.map = parseMapToJSON(data.map, data)
|
|
} catch (error) {
|
|
return callback(error)
|
|
}
|
|
_resolveSources(data)
|
|
})
|
|
} else {
|
|
resolveSourceMap(code, codeUrl, read, function(error, mapData) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
if (!mapData) {
|
|
return callback(null, null)
|
|
}
|
|
_resolveSources(mapData)
|
|
})
|
|
}
|
|
|
|
function _resolveSources(mapData) {
|
|
resolveSources(mapData.map, mapData.sourcesRelativeTo, read, options, function(error, result) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
mapData.sourcesResolved = result.sourcesResolved
|
|
mapData.sourcesContent = result.sourcesContent
|
|
callback(null, mapData)
|
|
})
|
|
}
|
|
}
|
|
|
|
function resolveSync(code, codeUrl, read, options) {
|
|
var mapData
|
|
if (code === null) {
|
|
var mapUrl = codeUrl
|
|
mapData = {
|
|
sourceMappingURL: null,
|
|
url: mapUrl,
|
|
sourcesRelativeTo: mapUrl,
|
|
map: null
|
|
}
|
|
mapData.map = readSync(read, mapUrl, mapData)
|
|
mapData.map = parseMapToJSON(mapData.map, mapData)
|
|
} else {
|
|
mapData = resolveSourceMapSync(code, codeUrl, read)
|
|
if (!mapData) {
|
|
return null
|
|
}
|
|
}
|
|
var result = resolveSourcesSync(mapData.map, mapData.sourcesRelativeTo, read, options)
|
|
mapData.sourcesResolved = result.sourcesResolved
|
|
mapData.sourcesContent = result.sourcesContent
|
|
return mapData
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
resolveSourceMap: resolveSourceMap,
|
|
resolveSourceMapSync: resolveSourceMapSync,
|
|
resolveSources: resolveSources,
|
|
resolveSourcesSync: resolveSourcesSync,
|
|
resolve: resolve,
|
|
resolveSync: resolveSync,
|
|
parseMapToJSON: parseMapToJSON
|
|
}
|