Spaces:
Running
Running
var fs = require('fs'); | |
var path = require('path'); | |
var isAllowedResource = require('./is-allowed-resource'); | |
var matchDataUri = require('./match-data-uri'); | |
var rebaseLocalMap = require('./rebase-local-map'); | |
var rebaseRemoteMap = require('./rebase-remote-map'); | |
var Token = require('../tokenizer/token'); | |
var hasProtocol = require('../utils/has-protocol'); | |
var isDataUriResource = require('../utils/is-data-uri-resource'); | |
var isRemoteResource = require('../utils/is-remote-resource'); | |
var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/; | |
function applySourceMaps(tokens, context, callback) { | |
var applyContext = { | |
callback: callback, | |
fetch: context.options.fetch, | |
index: 0, | |
inline: context.options.inline, | |
inlineRequest: context.options.inlineRequest, | |
inlineTimeout: context.options.inlineTimeout, | |
inputSourceMapTracker: context.inputSourceMapTracker, | |
localOnly: context.localOnly, | |
processedTokens: [], | |
rebaseTo: context.options.rebaseTo, | |
sourceTokens: tokens, | |
warnings: context.warnings | |
}; | |
return context.options.sourceMap && tokens.length > 0 | |
? doApplySourceMaps(applyContext) | |
: callback(tokens); | |
} | |
function doApplySourceMaps(applyContext) { | |
var singleSourceTokens = []; | |
var lastSource = findTokenSource(applyContext.sourceTokens[0]); | |
var source; | |
var token; | |
var l; | |
for (l = applyContext.sourceTokens.length; applyContext.index < l; applyContext.index++) { | |
token = applyContext.sourceTokens[applyContext.index]; | |
source = findTokenSource(token); | |
if (source != lastSource) { | |
singleSourceTokens = []; | |
lastSource = source; | |
} | |
singleSourceTokens.push(token); | |
applyContext.processedTokens.push(token); | |
if (token[0] == Token.COMMENT && MAP_MARKER_PATTERN.test(token[1])) { | |
return fetchAndApplySourceMap(token[1], source, singleSourceTokens, applyContext); | |
} | |
} | |
return applyContext.callback(applyContext.processedTokens); | |
} | |
function findTokenSource(token) { | |
var scope; | |
var metadata; | |
if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT || token[0] == Token.RAW) { | |
metadata = token[2][0]; | |
} else { | |
scope = token[1][0]; | |
metadata = scope[2][0]; | |
} | |
return metadata[2]; | |
} | |
function fetchAndApplySourceMap(sourceMapComment, source, singleSourceTokens, applyContext) { | |
return extractInputSourceMapFrom(sourceMapComment, applyContext, function(inputSourceMap) { | |
if (inputSourceMap) { | |
applyContext.inputSourceMapTracker.track(source, inputSourceMap); | |
applySourceMapRecursively(singleSourceTokens, applyContext.inputSourceMapTracker); | |
} | |
applyContext.index++; | |
return doApplySourceMaps(applyContext); | |
}); | |
} | |
function extractInputSourceMapFrom(sourceMapComment, applyContext, whenSourceMapReady) { | |
var uri = MAP_MARKER_PATTERN.exec(sourceMapComment)[1]; | |
var absoluteUri; | |
var sourceMap; | |
var rebasedMap; | |
if (isDataUriResource(uri)) { | |
sourceMap = extractInputSourceMapFromDataUri(uri); | |
return whenSourceMapReady(sourceMap); | |
} if (isRemoteResource(uri)) { | |
return loadInputSourceMapFromRemoteUri(uri, applyContext, function(sourceMap) { | |
var parsedMap; | |
if (sourceMap) { | |
parsedMap = JSON.parse(sourceMap); | |
rebasedMap = rebaseRemoteMap(parsedMap, uri); | |
whenSourceMapReady(rebasedMap); | |
} else { | |
whenSourceMapReady(null); | |
} | |
}); | |
} | |
// at this point `uri` is already rebased, see lib/reader/rebase.js#rebaseSourceMapComment | |
// it is rebased to be consistent with rebasing other URIs | |
// however here we need to resolve it back to read it from disk | |
absoluteUri = path.resolve(applyContext.rebaseTo, uri); | |
sourceMap = loadInputSourceMapFromLocalUri(absoluteUri, applyContext); | |
if (sourceMap) { | |
rebasedMap = rebaseLocalMap(sourceMap, absoluteUri, applyContext.rebaseTo); | |
return whenSourceMapReady(rebasedMap); | |
} | |
return whenSourceMapReady(null); | |
} | |
function extractInputSourceMapFromDataUri(uri) { | |
var dataUriMatch = matchDataUri(uri); | |
var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii'; | |
var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8'; | |
var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4]; | |
var buffer = Buffer.from(data, encoding); | |
buffer.charset = charset; | |
return JSON.parse(buffer.toString()); | |
} | |
function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) { | |
var isAllowed = isAllowedResource(uri, true, applyContext.inline); | |
var isRuntimeResource = !hasProtocol(uri); | |
if (applyContext.localOnly) { | |
applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.'); | |
return whenLoaded(null); | |
} if (isRuntimeResource) { | |
applyContext.warnings.push('Cannot fetch "' + uri + '" as no protocol given.'); | |
return whenLoaded(null); | |
} if (!isAllowed) { | |
applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); | |
return whenLoaded(null); | |
} | |
applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function(error, body) { | |
if (error) { | |
applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error); | |
return whenLoaded(null); | |
} | |
whenLoaded(body); | |
}); | |
} | |
function loadInputSourceMapFromLocalUri(uri, applyContext) { | |
var isAllowed = isAllowedResource(uri, false, applyContext.inline); | |
var sourceMap; | |
if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) { | |
applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.'); | |
return null; | |
} if (!isAllowed) { | |
applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); | |
return null; | |
} if (!fs.statSync(uri).size) { | |
applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is empty.'); | |
return null; | |
} | |
sourceMap = fs.readFileSync(uri, 'utf-8'); | |
return JSON.parse(sourceMap); | |
} | |
function applySourceMapRecursively(tokens, inputSourceMapTracker) { | |
var token; | |
var i, l; | |
for (i = 0, l = tokens.length; i < l; i++) { | |
token = tokens[i]; | |
switch (token[0]) { | |
case Token.AT_RULE: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.AT_RULE_BLOCK: | |
applySourceMapRecursively(token[1], inputSourceMapTracker); | |
applySourceMapRecursively(token[2], inputSourceMapTracker); | |
break; | |
case Token.AT_RULE_BLOCK_SCOPE: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.NESTED_BLOCK: | |
applySourceMapRecursively(token[1], inputSourceMapTracker); | |
applySourceMapRecursively(token[2], inputSourceMapTracker); | |
break; | |
case Token.NESTED_BLOCK_SCOPE: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.COMMENT: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.PROPERTY: | |
applySourceMapRecursively(token, inputSourceMapTracker); | |
break; | |
case Token.PROPERTY_BLOCK: | |
applySourceMapRecursively(token[1], inputSourceMapTracker); | |
break; | |
case Token.PROPERTY_NAME: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.PROPERTY_VALUE: | |
applySourceMapTo(token, inputSourceMapTracker); | |
break; | |
case Token.RULE: | |
applySourceMapRecursively(token[1], inputSourceMapTracker); | |
applySourceMapRecursively(token[2], inputSourceMapTracker); | |
break; | |
case Token.RULE_SCOPE: | |
applySourceMapTo(token, inputSourceMapTracker); | |
} | |
} | |
return tokens; | |
} | |
function applySourceMapTo(token, inputSourceMapTracker) { | |
var value = token[1]; | |
var metadata = token[2]; | |
var newMetadata = []; | |
var i, l; | |
for (i = 0, l = metadata.length; i < l; i++) { | |
newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length)); | |
} | |
token[2] = newMetadata; | |
} | |
module.exports = applySourceMaps; | |