291 lines
8.6 KiB
JavaScript
291 lines
8.6 KiB
JavaScript
//@ts-check
|
|
const fs = require('graceful-fs');
|
|
const path = require('path');
|
|
|
|
const SOURCEMAP_URL_REGEX = /^\/\/#\s*sourceMappingURL=/;
|
|
const CHARSET_REGEX = /^;charset=([^;]+);/;
|
|
|
|
/**
|
|
* @param {*} logger
|
|
* @param {karmaSourcemapLoader.Config} config
|
|
* @returns {karmaSourcemapLoader.Preprocessor}
|
|
*/
|
|
function createSourceMapLocatorPreprocessor(logger, config) {
|
|
const options = (config && config.sourceMapLoader) || {};
|
|
const remapPrefixes = options.remapPrefixes;
|
|
const remapSource = options.remapSource;
|
|
const useSourceRoot = options.useSourceRoot;
|
|
const onlyWithURL = options.onlyWithURL;
|
|
const strict = options.strict;
|
|
const needsUpdate = remapPrefixes || remapSource || useSourceRoot;
|
|
const log = logger.create('preprocessor.sourcemap');
|
|
|
|
/**
|
|
* @param {string[]} sources
|
|
*/
|
|
function remapSources(sources) {
|
|
const all = sources.length;
|
|
let remapped = 0;
|
|
/** @type {Record<string, boolean>} */
|
|
const remappedPrefixes = {};
|
|
let remappedSource = false;
|
|
|
|
/**
|
|
* Replaces source path prefixes using a key:value map
|
|
* @param {string} source
|
|
* @returns {string | undefined}
|
|
*/
|
|
function handlePrefixes(source) {
|
|
if (!remapPrefixes) {
|
|
return undefined;
|
|
}
|
|
|
|
let sourcePrefix, targetPrefix, target;
|
|
for (sourcePrefix in remapPrefixes) {
|
|
targetPrefix = remapPrefixes[sourcePrefix];
|
|
if (source.startsWith(sourcePrefix)) {
|
|
target = targetPrefix + source.substring(sourcePrefix.length);
|
|
++remapped;
|
|
// Log only one remapping as an example for each prefix to prevent
|
|
// flood of messages on the console
|
|
if (!remappedPrefixes[sourcePrefix]) {
|
|
remappedPrefixes[sourcePrefix] = true;
|
|
log.debug(' ', source, '>>', target);
|
|
}
|
|
return target;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replaces source paths using a custom function
|
|
/**
|
|
* @param {string} source
|
|
* @returns {string | undefined}
|
|
*/
|
|
function handleMapper(source) {
|
|
if (!remapSource) {
|
|
return undefined;
|
|
}
|
|
|
|
const target = remapSource(source);
|
|
// Remapping is considered happenned only if the handler returns
|
|
// a non-empty path different from the existing one
|
|
if (target && target !== source) {
|
|
++remapped;
|
|
// Log only one remapping as an example to prevent flooding the console
|
|
if (!remappedSource) {
|
|
remappedSource = true;
|
|
log.debug(' ', source, '>>', target);
|
|
}
|
|
return target;
|
|
}
|
|
}
|
|
|
|
const result = sources.map((rawSource) => {
|
|
const source = rawSource.replace(/\\/g, '/');
|
|
|
|
const sourceWithRemappedPrefixes = handlePrefixes(source);
|
|
if (sourceWithRemappedPrefixes) {
|
|
// One remapping is enough; if a prefix was replaced, do not let
|
|
// the handler below check the source path any more
|
|
return sourceWithRemappedPrefixes;
|
|
}
|
|
|
|
return handleMapper(source) || source;
|
|
});
|
|
|
|
if (remapped) {
|
|
log.debug(' ...');
|
|
log.debug(' ', remapped, 'sources from', all, 'were remapped');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return function karmaSourcemapLoaderPreprocessor(content, file, done) {
|
|
/**
|
|
* Parses a string with source map as JSON and handles errors
|
|
* @param {string} data
|
|
* @returns {karmaSourcemapLoader.SourceMap | false | undefined}
|
|
*/
|
|
function parseMap(data) {
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (err) {
|
|
if (strict) {
|
|
done(new Error('malformed source map for' + file.originalPath + '\nError: ' + err));
|
|
// Returning `false` will make the caller abort immediately
|
|
return false;
|
|
}
|
|
log.warn('malformed source map for', file.originalPath);
|
|
log.warn('Error:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the sourceRoot property to a fixed or computed value
|
|
* @param {karmaSourcemapLoader.SourceMap} sourceMap
|
|
*/
|
|
function setSourceRoot(sourceMap) {
|
|
const sourceRoot = typeof useSourceRoot === 'function' ? useSourceRoot(file) : useSourceRoot;
|
|
if (sourceRoot) {
|
|
sourceMap.sourceRoot = sourceRoot;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs configured updates of the source map content
|
|
* @param {karmaSourcemapLoader.SourceMap} sourceMap
|
|
*/
|
|
function updateSourceMap(sourceMap) {
|
|
if (remapPrefixes || remapSource) {
|
|
sourceMap.sources = remapSources(sourceMap.sources);
|
|
}
|
|
if (useSourceRoot) {
|
|
setSourceRoot(sourceMap);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} data
|
|
* @returns {void}
|
|
*/
|
|
function sourceMapData(data) {
|
|
const sourceMap = parseMap(data);
|
|
if (sourceMap) {
|
|
// Perform the remapping only if there is a configuration for it
|
|
if (needsUpdate) {
|
|
updateSourceMap(sourceMap);
|
|
}
|
|
file.sourceMap = sourceMap;
|
|
} else if (sourceMap === false) {
|
|
return;
|
|
}
|
|
done(content);
|
|
}
|
|
|
|
/**
|
|
* @param {string} inlineData
|
|
*/
|
|
function inlineMap(inlineData) {
|
|
let charset = 'utf-8';
|
|
|
|
if (CHARSET_REGEX.test(inlineData)) {
|
|
const matches = inlineData.match(CHARSET_REGEX);
|
|
|
|
if (matches && matches.length === 2) {
|
|
charset = matches[1];
|
|
inlineData = inlineData.slice(matches[0].length - 1);
|
|
}
|
|
}
|
|
|
|
if (/^;base64,/.test(inlineData)) {
|
|
// base64-encoded JSON string
|
|
log.debug('base64-encoded source map for', file.originalPath);
|
|
const buffer = Buffer.from(inlineData.slice(';base64,'.length), 'base64');
|
|
//@ts-ignore Assume the parsed charset is supported by Buffer.
|
|
sourceMapData(buffer.toString(charset));
|
|
} else if (inlineData.startsWith(',')) {
|
|
// straight-up URL-encoded JSON string
|
|
log.debug('raw inline source map for', file.originalPath);
|
|
sourceMapData(decodeURIComponent(inlineData.slice(1)));
|
|
} else {
|
|
if (strict) {
|
|
done(new Error('invalid source map in ' + file.originalPath));
|
|
} else {
|
|
log.warn('invalid source map in', file.originalPath);
|
|
done(content);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} mapPath
|
|
* @param {boolean} optional
|
|
*/
|
|
function fileMap(mapPath, optional) {
|
|
fs.readFile(mapPath, function (err, data) {
|
|
// File does not exist
|
|
if (err && err.code === 'ENOENT') {
|
|
if (!optional) {
|
|
if (strict) {
|
|
done(new Error('missing external source map for ' + file.originalPath));
|
|
return;
|
|
} else {
|
|
log.warn('missing external source map for', file.originalPath);
|
|
}
|
|
}
|
|
done(content);
|
|
return;
|
|
}
|
|
|
|
// Error while reading the file
|
|
if (err) {
|
|
if (strict) {
|
|
done(
|
|
new Error('reading external source map failed for ' + file.originalPath + '\n' + err)
|
|
);
|
|
} else {
|
|
log.warn('reading external source map failed for', file.originalPath);
|
|
log.warn(err);
|
|
done(content);
|
|
}
|
|
return;
|
|
}
|
|
|
|
log.debug('external source map exists for', file.originalPath);
|
|
sourceMapData(data.toString());
|
|
});
|
|
}
|
|
|
|
// Remap source paths in a directly served source map
|
|
function convertMap() {
|
|
let sourceMap;
|
|
// Perform the remapping only if there is a configuration for it
|
|
if (needsUpdate) {
|
|
log.debug('processing source map', file.originalPath);
|
|
sourceMap = parseMap(content);
|
|
if (sourceMap) {
|
|
updateSourceMap(sourceMap);
|
|
content = JSON.stringify(sourceMap);
|
|
} else if (sourceMap === false) {
|
|
return;
|
|
}
|
|
}
|
|
done(content);
|
|
}
|
|
|
|
if (file.path.endsWith('.map')) {
|
|
return convertMap();
|
|
}
|
|
|
|
const lines = content.split(/\n/);
|
|
let lastLine = lines.pop();
|
|
while (typeof lastLine === 'string' && /^\s*$/.test(lastLine)) {
|
|
lastLine = lines.pop();
|
|
}
|
|
|
|
const mapUrl =
|
|
lastLine && SOURCEMAP_URL_REGEX.test(lastLine) && lastLine.replace(SOURCEMAP_URL_REGEX, '');
|
|
|
|
if (!mapUrl) {
|
|
if (onlyWithURL) {
|
|
done(content);
|
|
} else {
|
|
fileMap(file.path + '.map', true);
|
|
}
|
|
} else if (/^data:application\/json/.test(mapUrl)) {
|
|
inlineMap(mapUrl.slice('data:application/json'.length));
|
|
} else {
|
|
fileMap(path.resolve(path.dirname(file.path), mapUrl), false);
|
|
}
|
|
};
|
|
}
|
|
|
|
createSourceMapLocatorPreprocessor.$inject = ['logger', 'config'];
|
|
|
|
// PUBLISH DI MODULE
|
|
module.exports = {
|
|
'preprocessor:sourcemap': ['factory', createSourceMapLocatorPreprocessor],
|
|
};
|