| //@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], | |
| }; |