├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── test ├── karma.handler.js ├── karma.prefixes.js ├── karma.shared.js ├── karma.source-root-function.js ├── karma.source-root-value.js ├── rollup.config.js ├── src │ ├── shared.js │ └── source.js ├── test-source-root.js └── test-sources.js └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | test/out 17 | coverage 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.0] - 2022-02-05 9 | 10 | ### Added 11 | 12 | - Allow remapping or otherwise changing source paths in source maps 13 | - Allow changing `sourceRoot` in source maps 14 | - Allow adapting the source map files alone, if served separately by the Karma web server 15 | - Add option `onlyWithURL` to disable the source map loading for files without `sourceMappingURL` 16 | - Add option `strict` for a strict error handling of invalid and/or missing source maps 17 | 18 | ### Fixed 19 | 20 | - Fix handling of raw (URI-encoded) source maps - trim the leading , before parsing the content 21 | - Warn about a missing external source map, is the source mapping URL is invalid 22 | - Handle malformed source map content as a warning or failure 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sergey Todyshev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # karma-sourcemap-loader 2 | 3 | > Preprocessor that locates and loads existing source maps. 4 | 5 | ## Why 6 | 7 | When you use karma not in isolation but as part of a build process (e.g. using grunt 8 | or gulp) it is often the case that the compilation/transpilation is done on a previous 9 | step of the process and not handled by karma preprocessors. In these cases source maps 10 | don't get loaded by karma and you lose the advantages of having them. Collecting 11 | the test code coverage using an instrumented code with source maps, for example. 12 | 13 | Another reason may be the need for modifying relative source paths in source maps 14 | to make sure that they point to source files in the project running the tests. 15 | 16 | ## How it works 17 | 18 | This plug-in supports both inline and external source maps. 19 | 20 | Inline source maps are located by searching "sourceMappingURL=" inside the javascript 21 | file, both plain text and base64-encoded maps are supported. 22 | 23 | External source maps are located by appending ".map" to the javascript file name. 24 | So if for example you have Hello.js, the preprocessor will try to load source map from 25 | Hello.js.map. 26 | 27 | ## Installation 28 | 29 | Just add `karma-sourcemap-loader` as a devDependency in your `package.json`. 30 | ```json 31 | { 32 | "devDependencies": { 33 | "karma-sourcemap-loader": "~0.3" 34 | } 35 | } 36 | ``` 37 | 38 | Or issue the following command: 39 | ```bash 40 | npm install karma-sourcemap-loader --save-dev 41 | ``` 42 | 43 | ## Configuration 44 | 45 | The code below shows a sample configuration of the preprocessor. 46 | 47 | ```js 48 | // karma.conf.js 49 | module.exports = function(config) { 50 | config.set({ 51 | plugins: ['karma-sourcemap-loader'], 52 | preprocessors: { 53 | '**/*.js': ['sourcemap'] 54 | } 55 | }); 56 | }; 57 | ``` 58 | 59 | The code below shows a configuration of the preprocessor with remapping of source file paths in source maps using path prefixes. The object `remapPrefixes` contains path prefixes as keys, which if they are detected in a source path, will be replaced by the key value. After the first detected prefix gets replaced, other prefixes will be ignored.. 60 | 61 | ```js 62 | // karma.conf.js 63 | module.exports = function(config) { 64 | config.set({ 65 | plugins: ['karma-sourcemap-loader'], 66 | preprocessors: { 67 | '**/*.js': ['sourcemap'] 68 | }, 69 | sourceMapLoader: { 70 | remapPrefixes: { 71 | '/myproject/': '../src/', 72 | '/otherdep/': '../node_modules/otherdep/' 73 | } 74 | } 75 | }); 76 | }; 77 | ``` 78 | 79 | The code below shows a configuration of the preprocessor with remapping of source file paths in source maps using a callback. The function `remapSource` receives an original source path and may return a changed source path. If it returns `undefined` or other false-y result, the source path will not be changed. 80 | 81 | ```js 82 | // karma.conf.js 83 | module.exports = function(config) { 84 | config.set({ 85 | plugins: ['karma-sourcemap-loader'], 86 | preprocessors: { 87 | '**/*.js': ['sourcemap'] 88 | }, 89 | sourceMapLoader: { 90 | remapSource(source) { 91 | if (source.startsWith('/myproject/')) { 92 | return '../src/' + source.substring(11); 93 | } 94 | } 95 | } 96 | }); 97 | }; 98 | ``` 99 | 100 | The code below shows a sample configuration of the preprocessor with changing the `sourceRoot` property to a custom value, which will change the location where the debugger should locate the source files. 101 | 102 | ```js 103 | // karma.conf.js 104 | module.exports = function(config) { 105 | config.set({ 106 | plugins: ['karma-sourcemap-loader'], 107 | preprocessors: { 108 | '**/*.js': ['sourcemap'] 109 | }, 110 | sourceMapLoader: { 111 | useSourceRoot: '/sources' 112 | } 113 | }); 114 | }; 115 | ``` 116 | 117 | The code below shows a sample configuration of the preprocessor with changing the `sourceRoot` property using a custom function to be able to compute the value depending on the path to the bundle. The `file` argument is the Karma file object `{ path, originalPath }` for the bundle. 118 | 119 | ```js 120 | // karma.conf.js 121 | module.exports = function(config) { 122 | config.set({ 123 | plugins: ['karma-sourcemap-loader'], 124 | preprocessors: { 125 | '**/*.js': ['sourcemap'] 126 | }, 127 | sourceMapLoader: { 128 | useSourceRoot(file) { 129 | return '/sources'; 130 | } 131 | } 132 | }); 133 | }; 134 | ``` 135 | 136 | The code below shows a sample configuration of the preprocessor with source map loading only for files with the `sourceMappingURL` set. The default behaviour is trying to load source maps for all JavaScript files, also those without the `sourceMappingURL` set. 137 | 138 | ```js 139 | // karma.conf.js 140 | module.exports = function(config) { 141 | config.set({ 142 | plugins: ['karma-sourcemap-loader'], 143 | preprocessors: { 144 | '**/*.js': ['sourcemap'] 145 | }, 146 | sourceMapLoader: { 147 | onlyWithURL: true 148 | } 149 | }); 150 | }; 151 | ``` 152 | 153 | The code below shows a sample configuration of the preprocessor with a strict error checking. A missing or an invalid source map will cause the test run fail. 154 | 155 | ```js 156 | // karma.conf.js 157 | module.exports = function(config) { 158 | config.set({ 159 | plugins: ['karma-sourcemap-loader'], 160 | preprocessors: { 161 | '**/*.js': ['sourcemap'] 162 | }, 163 | sourceMapLoader: { 164 | strict: true 165 | } 166 | }); 167 | }; 168 | ``` 169 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const fs = require('graceful-fs'); 3 | const path = require('path'); 4 | 5 | const SOURCEMAP_URL_REGEX = /^\/\/#\s*sourceMappingURL=/; 6 | const CHARSET_REGEX = /^;charset=([^;]+);/; 7 | 8 | /** 9 | * @param {*} logger 10 | * @param {karmaSourcemapLoader.Config} config 11 | * @returns {karmaSourcemapLoader.Preprocessor} 12 | */ 13 | function createSourceMapLocatorPreprocessor(logger, config) { 14 | const options = (config && config.sourceMapLoader) || {}; 15 | const remapPrefixes = options.remapPrefixes; 16 | const remapSource = options.remapSource; 17 | const useSourceRoot = options.useSourceRoot; 18 | const onlyWithURL = options.onlyWithURL; 19 | const strict = options.strict; 20 | const needsUpdate = remapPrefixes || remapSource || useSourceRoot; 21 | const log = logger.create('preprocessor.sourcemap'); 22 | 23 | /** 24 | * @param {string[]} sources 25 | */ 26 | function remapSources(sources) { 27 | const all = sources.length; 28 | let remapped = 0; 29 | /** @type {Record} */ 30 | const remappedPrefixes = {}; 31 | let remappedSource = false; 32 | 33 | /** 34 | * Replaces source path prefixes using a key:value map 35 | * @param {string} source 36 | * @returns {string | undefined} 37 | */ 38 | function handlePrefixes(source) { 39 | if (!remapPrefixes) { 40 | return undefined; 41 | } 42 | 43 | let sourcePrefix, targetPrefix, target; 44 | for (sourcePrefix in remapPrefixes) { 45 | targetPrefix = remapPrefixes[sourcePrefix]; 46 | if (source.startsWith(sourcePrefix)) { 47 | target = targetPrefix + source.substring(sourcePrefix.length); 48 | ++remapped; 49 | // Log only one remapping as an example for each prefix to prevent 50 | // flood of messages on the console 51 | if (!remappedPrefixes[sourcePrefix]) { 52 | remappedPrefixes[sourcePrefix] = true; 53 | log.debug(' ', source, '>>', target); 54 | } 55 | return target; 56 | } 57 | } 58 | } 59 | 60 | // Replaces source paths using a custom function 61 | /** 62 | * @param {string} source 63 | * @returns {string | undefined} 64 | */ 65 | function handleMapper(source) { 66 | if (!remapSource) { 67 | return undefined; 68 | } 69 | 70 | const target = remapSource(source); 71 | // Remapping is considered happenned only if the handler returns 72 | // a non-empty path different from the existing one 73 | if (target && target !== source) { 74 | ++remapped; 75 | // Log only one remapping as an example to prevent flooding the console 76 | if (!remappedSource) { 77 | remappedSource = true; 78 | log.debug(' ', source, '>>', target); 79 | } 80 | return target; 81 | } 82 | } 83 | 84 | const result = sources.map((rawSource) => { 85 | const source = rawSource.replace(/\\/g, '/'); 86 | 87 | const sourceWithRemappedPrefixes = handlePrefixes(source); 88 | if (sourceWithRemappedPrefixes) { 89 | // One remapping is enough; if a prefix was replaced, do not let 90 | // the handler below check the source path any more 91 | return sourceWithRemappedPrefixes; 92 | } 93 | 94 | return handleMapper(source) || source; 95 | }); 96 | 97 | if (remapped) { 98 | log.debug(' ...'); 99 | log.debug(' ', remapped, 'sources from', all, 'were remapped'); 100 | } 101 | 102 | return result; 103 | } 104 | 105 | return function karmaSourcemapLoaderPreprocessor(content, file, done) { 106 | /** 107 | * Parses a string with source map as JSON and handles errors 108 | * @param {string} data 109 | * @returns {karmaSourcemapLoader.SourceMap | false | undefined} 110 | */ 111 | function parseMap(data) { 112 | try { 113 | return JSON.parse(data); 114 | } catch (err) { 115 | if (strict) { 116 | done(new Error('malformed source map for' + file.originalPath + '\nError: ' + err)); 117 | // Returning `false` will make the caller abort immediately 118 | return false; 119 | } 120 | log.warn('malformed source map for', file.originalPath); 121 | log.warn('Error:', err); 122 | } 123 | } 124 | 125 | /** 126 | * Sets the sourceRoot property to a fixed or computed value 127 | * @param {karmaSourcemapLoader.SourceMap} sourceMap 128 | */ 129 | function setSourceRoot(sourceMap) { 130 | const sourceRoot = typeof useSourceRoot === 'function' ? useSourceRoot(file) : useSourceRoot; 131 | if (sourceRoot) { 132 | sourceMap.sourceRoot = sourceRoot; 133 | } 134 | } 135 | 136 | /** 137 | * Performs configured updates of the source map content 138 | * @param {karmaSourcemapLoader.SourceMap} sourceMap 139 | */ 140 | function updateSourceMap(sourceMap) { 141 | if (remapPrefixes || remapSource) { 142 | sourceMap.sources = remapSources(sourceMap.sources); 143 | } 144 | if (useSourceRoot) { 145 | setSourceRoot(sourceMap); 146 | } 147 | } 148 | 149 | /** 150 | * @param {string} data 151 | * @returns {void} 152 | */ 153 | function sourceMapData(data) { 154 | const sourceMap = parseMap(data); 155 | if (sourceMap) { 156 | // Perform the remapping only if there is a configuration for it 157 | if (needsUpdate) { 158 | updateSourceMap(sourceMap); 159 | } 160 | file.sourceMap = sourceMap; 161 | } else if (sourceMap === false) { 162 | return; 163 | } 164 | done(content); 165 | } 166 | 167 | /** 168 | * @param {string} inlineData 169 | */ 170 | function inlineMap(inlineData) { 171 | let charset = 'utf-8'; 172 | 173 | if (CHARSET_REGEX.test(inlineData)) { 174 | const matches = inlineData.match(CHARSET_REGEX); 175 | 176 | if (matches && matches.length === 2) { 177 | charset = matches[1]; 178 | inlineData = inlineData.slice(matches[0].length - 1); 179 | } 180 | } 181 | 182 | if (/^;base64,/.test(inlineData)) { 183 | // base64-encoded JSON string 184 | log.debug('base64-encoded source map for', file.originalPath); 185 | const buffer = Buffer.from(inlineData.slice(';base64,'.length), 'base64'); 186 | //@ts-ignore Assume the parsed charset is supported by Buffer. 187 | sourceMapData(buffer.toString(charset)); 188 | } else if (inlineData.startsWith(',')) { 189 | // straight-up URL-encoded JSON string 190 | log.debug('raw inline source map for', file.originalPath); 191 | sourceMapData(decodeURIComponent(inlineData.slice(1))); 192 | } else { 193 | if (strict) { 194 | done(new Error('invalid source map in ' + file.originalPath)); 195 | } else { 196 | log.warn('invalid source map in', file.originalPath); 197 | done(content); 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * @param {string} mapPath 204 | * @param {boolean} optional 205 | */ 206 | function fileMap(mapPath, optional) { 207 | fs.readFile(mapPath, function (err, data) { 208 | // File does not exist 209 | if (err && err.code === 'ENOENT') { 210 | if (!optional) { 211 | if (strict) { 212 | done(new Error('missing external source map for ' + file.originalPath)); 213 | return; 214 | } else { 215 | log.warn('missing external source map for', file.originalPath); 216 | } 217 | } 218 | done(content); 219 | return; 220 | } 221 | 222 | // Error while reading the file 223 | if (err) { 224 | if (strict) { 225 | done( 226 | new Error('reading external source map failed for ' + file.originalPath + '\n' + err) 227 | ); 228 | } else { 229 | log.warn('reading external source map failed for', file.originalPath); 230 | log.warn(err); 231 | done(content); 232 | } 233 | return; 234 | } 235 | 236 | log.debug('external source map exists for', file.originalPath); 237 | sourceMapData(data.toString()); 238 | }); 239 | } 240 | 241 | // Remap source paths in a directly served source map 242 | function convertMap() { 243 | let sourceMap; 244 | // Perform the remapping only if there is a configuration for it 245 | if (needsUpdate) { 246 | log.debug('processing source map', file.originalPath); 247 | sourceMap = parseMap(content); 248 | if (sourceMap) { 249 | updateSourceMap(sourceMap); 250 | content = JSON.stringify(sourceMap); 251 | } else if (sourceMap === false) { 252 | return; 253 | } 254 | } 255 | done(content); 256 | } 257 | 258 | if (file.path.endsWith('.map')) { 259 | return convertMap(); 260 | } 261 | 262 | const lines = content.split(/\n/); 263 | let lastLine = lines.pop(); 264 | while (typeof lastLine === 'string' && /^\s*$/.test(lastLine)) { 265 | lastLine = lines.pop(); 266 | } 267 | 268 | const mapUrl = 269 | lastLine && SOURCEMAP_URL_REGEX.test(lastLine) && lastLine.replace(SOURCEMAP_URL_REGEX, ''); 270 | 271 | if (!mapUrl) { 272 | if (onlyWithURL) { 273 | done(content); 274 | } else { 275 | fileMap(file.path + '.map', true); 276 | } 277 | } else if (/^data:application\/json/.test(mapUrl)) { 278 | inlineMap(mapUrl.slice('data:application/json'.length)); 279 | } else { 280 | fileMap(path.resolve(path.dirname(file.path), mapUrl), false); 281 | } 282 | }; 283 | } 284 | 285 | createSourceMapLocatorPreprocessor.$inject = ['logger', 'config']; 286 | 287 | // PUBLISH DI MODULE 288 | module.exports = { 289 | 'preprocessor:sourcemap': ['factory', createSourceMapLocatorPreprocessor], 290 | }; 291 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "strict": true 7 | }, 8 | "exclude": ["node_modules", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karma-sourcemap-loader", 3 | "version": "0.4.0", 4 | "description": "Karma plugin that locates and loads existing javascript source map files.", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:demerzel3/karma-sourcemap-loader.git" 12 | }, 13 | "keywords": [ 14 | "karma-plugin", 15 | "karma-preprocessor", 16 | "source-maps", 17 | "javascript" 18 | ], 19 | "author": { 20 | "name": "Gabriele Genta", 21 | "email": "gabriele.genta@gmail.com" 22 | }, 23 | "license": "MIT", 24 | "scripts": { 25 | "lint": "eslint .", 26 | "lint:fix": "eslint --fix .", 27 | "pretest": "cd test && rollup -c", 28 | "test": "c8 karma start test/karma.prefixes.js && c8 --no-clean karma start test/karma.handler.js && c8 --no-clean karma start test/karma.source-root-value.js && c8 --no-clean karma start test/karma.source-root-function.js && c8 report -r text -r lcov" 29 | }, 30 | "c8": { 31 | "reporter": [] 32 | }, 33 | "prettier": { 34 | "printWidth": 100, 35 | "trailingComma": "es5", 36 | "tabWidth": 2, 37 | "singleQuote": true 38 | }, 39 | "eslintConfig": { 40 | "ignorePatterns": [ 41 | "test/out" 42 | ], 43 | "env": { 44 | "browser": true, 45 | "commonjs": true, 46 | "es2020": true, 47 | "jasmine": true, 48 | "node": true 49 | }, 50 | "parserOptions": { 51 | "sourceType": "module" 52 | }, 53 | "extends": "eslint:recommended", 54 | "rules": { 55 | "no-var": "error", 56 | "prefer-const": "error" 57 | } 58 | }, 59 | "dependencies": { 60 | "graceful-fs": "^4.2.10" 61 | }, 62 | "devDependencies": { 63 | "@types/graceful-fs": "^4.1.6", 64 | "c8": "^7.12.0", 65 | "eslint": "^8.33.0", 66 | "jasmine-core": "^4.5.0", 67 | "karma": "^6.4.1", 68 | "karma-brief-reporter": "^0.2.2", 69 | "karma-chrome-launcher": "^3.1.1", 70 | "karma-jasmine": "^5.1.0", 71 | "prettier": "^2.8.3", 72 | "rollup": "^3.10.1", 73 | "rollup-sourcemap-path-transform": "^1.0.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/karma.handler.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./karma.shared'); 2 | 3 | module.exports = function (config) { 4 | config.set( 5 | Object.assign({}, sharedConfig(config, 'test-sources'), { 6 | sourceMapLoader: { 7 | remapSource(source) { 8 | if (source.startsWith('/test/')) { 9 | return `../src/${source.substring(6)}`; 10 | } 11 | }, 12 | }, 13 | }) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /test/karma.prefixes.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./karma.shared'); 2 | 3 | module.exports = function (config) { 4 | config.set( 5 | Object.assign({}, sharedConfig(config, 'test-sources'), { 6 | sourceMapLoader: { 7 | remapPrefixes: { '/test/': '../src/' }, 8 | }, 9 | }) 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /test/karma.shared.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, testName) { 2 | return { 3 | plugins: ['karma-jasmine', 'karma-brief-reporter', 'karma-chrome-launcher', require('..')], 4 | 5 | frameworks: ['jasmine'], 6 | 7 | files: [ 8 | { pattern: `${testName}.js`, nocache: true }, 9 | { pattern: 'out/*.js' }, 10 | // The type `html` is a workaround for JavaScript parsing errors; 11 | // the default type is `js` and there is no type for JSON files 12 | { pattern: 'out/*.map', type: 'html' }, 13 | ], 14 | 15 | preprocessors: { 16 | 'out/*.js': ['sourcemap'], 17 | 'out/*.map': ['sourcemap'], 18 | }, 19 | 20 | reporters: ['brief'], 21 | 22 | browsers: ['ChromeHeadless'], 23 | 24 | briefReporter: { renderOnRunCompleteOnly: !!process.env.CI }, 25 | 26 | // logLevel: config.LOG_DEBUG, 27 | autoWatch: false, 28 | singleRun: true, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /test/karma.source-root-function.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./karma.shared'); 2 | 3 | module.exports = function (config) { 4 | config.set( 5 | Object.assign({}, sharedConfig(config, 'test-source-root'), { 6 | sourceMapLoader: { 7 | // eslint-disable-next-line no-unused-vars 8 | useSourceRoot(file) { 9 | return '/sources'; 10 | }, 11 | }, 12 | }) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /test/karma.source-root-value.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./karma.shared'); 2 | 3 | module.exports = function (config) { 4 | config.set( 5 | Object.assign({}, sharedConfig(config, 'test-source-root'), { 6 | sourceMapLoader: { 7 | useSourceRoot: '/sources', 8 | onlyWithURL: true, // Just to complete the code coverage 9 | }, 10 | }) 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /test/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { readFile, writeFile } = require('fs/promises'); 2 | const { createPathTransform } = require('rollup-sourcemap-path-transform'); 3 | 4 | // Replace ../src/ with /test/ in source map sources 5 | const sourcemapPathTransform = createPathTransform({ 6 | prefixes: { '*src/': '/test/' }, 7 | requirePrefix: true, 8 | }); 9 | 10 | module.exports = [ 11 | { 12 | input: 'src/source.js', 13 | output: { 14 | file: 'out/bundle.js', 15 | format: 'iife', // Karma cannot load ES modules 16 | sourcemap: true, 17 | sourcemapPathTransform, 18 | plugins: [ 19 | { 20 | async writeBundle() { 21 | const bundle = await readFile('out/bundle.js', 'utf8'); 22 | // Remove the source mapping URL 23 | const nomapref = bundle.replace(/\r?\n\/\/#\s*sourceMappingURL=.+$/m, ''); 24 | await writeFile('out/bundle-nomapref.js', nomapref); 25 | // Set the source mapping URL to a missing file - append ".none" to it 26 | const missingmap = bundle.replace(/(\r?\n\/\/#\s*sourceMappingURL=.+)$/m, '$1.none'); 27 | await writeFile('out/bundle-missingmap.js', missingmap); 28 | // Corrupt the source map content 29 | const corruptedmap = bundle.replace( 30 | /(\r?\n\/\/#\s*sourceMappingURL=bundle).+$/m, 31 | '$1-corruptedmap.js.map' 32 | ); 33 | await writeFile('out/bundle-corruptedmap.js', corruptedmap); 34 | await writeFile('out/bundle-corruptedmap.js.map', '{'); 35 | }, 36 | }, 37 | ], 38 | }, 39 | }, 40 | { 41 | input: 'src/shared.js', 42 | output: { 43 | file: 'out/shared.js', 44 | format: 'iife', // Karma cannot load ES modules 45 | name: 'shared', 46 | sourcemap: 'inline', 47 | sourcemapPathTransform, 48 | plugins: [ 49 | { 50 | async writeBundle() { 51 | const shared = await readFile('out/shared.js', 'utf8'); 52 | const map = 53 | /\r?\n\/\/#\s*sourceMappingURL=data:application\/json;charset=utf-8;base64,(.+)$/m.exec( 54 | shared 55 | )[1]; 56 | // Create a raw (URI-encoded) source map 57 | const raw = shared.replace( 58 | /(\r?\n\/\/#\s*sourceMappingURL=data:application\/json).+$/m, 59 | `$1,${encodeURIComponent(Buffer.from(map, 'base64').toString())}` 60 | ); 61 | await writeFile('out/shared-raw.js', raw); 62 | // Make the source mapping URL invalid by replacing `,` with `;` 63 | const invalid = shared.replace( 64 | /(\r?\n\/\/#\s*sourceMappingURL=data:application\/json).+$/m, 65 | `$1;${encodeURIComponent(Buffer.from(map, 'base64').toString())}` 66 | ); 67 | await writeFile('out/shared-invalid.js', invalid); 68 | // Corrupt the source map content 69 | const corrupted = shared.replace( 70 | /(\r?\n\/\/#\s*sourceMappingURL=data:application\/json).+$/m, 71 | '$1;charset=utf-8;base64,ewo=' 72 | ); 73 | await writeFile('out/shared-corrupted.js', corrupted); 74 | }, 75 | }, 76 | ], 77 | }, 78 | }, 79 | ]; 80 | -------------------------------------------------------------------------------- /test/src/shared.js: -------------------------------------------------------------------------------- 1 | export default function shared() { 2 | console.log(); 3 | } 4 | -------------------------------------------------------------------------------- /test/src/source.js: -------------------------------------------------------------------------------- 1 | import shared from './shared.js'; 2 | shared(); 3 | -------------------------------------------------------------------------------- /test/test-source-root.js: -------------------------------------------------------------------------------- 1 | function fetchFile(name) { 2 | const url = `/base/out/${name}`; 3 | return fetch(url).then((response) => response.text()); 4 | } 5 | 6 | describe('set sourceRoot', () => { 7 | it('sets sourceRoot in an inline source map', async () => { 8 | const content = await fetchFile('shared.js'); 9 | expect(content).toBe(`var shared = (function () { 10 | 'use strict'; 11 | 12 | function shared() { 13 | console.log(); 14 | } 15 | 16 | return shared; 17 | 18 | })(); 19 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLmpzIiwic291cmNlcyI6WyIvdGVzdC9zaGFyZWQuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gc2hhcmVkKCkge1xuICBjb25zb2xlLmxvZygpO1xufVxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztFQUFlLFNBQVMsTUFBTSxHQUFHO0VBQ2pDLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO0VBQ2hCOzs7Ozs7OzsifQ== 20 | `); 21 | }); 22 | 23 | it('sets sourceRoot in an external source map', async () => { 24 | const map = JSON.parse(await fetchFile('bundle.js.map')); 25 | expect(map.sourceRoot).toBe('/sources'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/test-sources.js: -------------------------------------------------------------------------------- 1 | function fetchFile(name) { 2 | const url = `/base/out/${name}`; 3 | return fetch(url).then((response) => response.text()); 4 | } 5 | 6 | describe('remap source prefixes', () => { 7 | it('remaps sources in a base64 inline source map', async () => { 8 | const content = await fetchFile('shared.js'); 9 | expect(content).toBe(`var shared = (function () { 10 | 'use strict'; 11 | 12 | function shared() { 13 | console.log(); 14 | } 15 | 16 | return shared; 17 | 18 | })(); 19 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLmpzIiwic291cmNlcyI6WyIvdGVzdC9zaGFyZWQuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gc2hhcmVkKCkge1xuICBjb25zb2xlLmxvZygpO1xufVxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztFQUFlLFNBQVMsTUFBTSxHQUFHO0VBQ2pDLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO0VBQ2hCOzs7Ozs7OzsifQ== 20 | `); 21 | }); 22 | 23 | it('remaps sources in a raw inline source map', async () => { 24 | const content = await fetchFile('shared-raw.js'); 25 | expect(content).toBe(`var shared = (function () { 26 | 'use strict'; 27 | 28 | function shared() { 29 | console.log(); 30 | } 31 | 32 | return shared; 33 | 34 | })(); 35 | //# sourceMappingURL=data:application/json,%7B%22version%22%3A3%2C%22file%22%3A%22shared.js%22%2C%22sources%22%3A%5B%22%2Ftest%2Fshared.js%22%5D%2C%22sourcesContent%22%3A%5B%22export%20default%20function%20shared()%20%7B%5Cn%20%20console.log()%3B%5Cn%7D%5Cn%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22%3B%3B%3BEAAe%2CSAAS%2CMAAM%2CGAAG%3BEACjC%2CEAAE%2COAAO%2CCAAC%2CGAAG%2CEAAE%2CCAAC%3BEAChB%3B%3B%3B%3B%3B%3B%3B%3B%22%7D 36 | `); 37 | }); 38 | 39 | it('leaves an invalid inline source mapping intact', async () => { 40 | const code = await fetchFile('shared-invalid.js'); 41 | expect(code).toBe(`var shared = (function () { 42 | 'use strict'; 43 | 44 | function shared() { 45 | console.log(); 46 | } 47 | 48 | return shared; 49 | 50 | })(); 51 | //# sourceMappingURL=data:application/json;%7B%22version%22%3A3%2C%22file%22%3A%22shared.js%22%2C%22sources%22%3A%5B%22%2Ftest%2Fshared.js%22%5D%2C%22sourcesContent%22%3A%5B%22export%20default%20function%20shared()%20%7B%5Cn%20%20console.log()%3B%5Cn%7D%5Cn%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22%3B%3B%3BEAAe%2CSAAS%2CMAAM%2CGAAG%3BEACjC%2CEAAE%2COAAO%2CCAAC%2CGAAG%2CEAAE%2CCAAC%3BEAChB%3B%3B%3B%3B%3B%3B%3B%3B%22%7D 52 | `); 53 | }); 54 | 55 | it('leaves a corrupted inline source map content intact', async () => { 56 | const code = await fetchFile('shared-corrupted.js'); 57 | expect(code).toBe(`var shared = (function () { 58 | 'use strict'; 59 | 60 | function shared() { 61 | console.log(); 62 | } 63 | 64 | return shared; 65 | 66 | })(); 67 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,ewo= 68 | `); 69 | }); 70 | 71 | it('leaves a valid external source map reference intact', async () => { 72 | const code = await fetchFile('bundle.js'); 73 | expect(code).toBe(`(function () { 74 | 'use strict'; 75 | 76 | function shared() { 77 | console.log(); 78 | } 79 | 80 | shared(); 81 | 82 | })(); 83 | //# sourceMappingURL=bundle.js.map 84 | `); 85 | }); 86 | 87 | it('leaves an invalid external source map reference intact', async () => { 88 | const code = await fetchFile('bundle-missingmap.js'); 89 | expect(code).toBe(`(function () { 90 | 'use strict'; 91 | 92 | function shared() { 93 | console.log(); 94 | } 95 | 96 | shared(); 97 | 98 | })(); 99 | //# sourceMappingURL=bundle.js.map.none 100 | `); 101 | }); 102 | 103 | it('leaves a corrupted external source map content intact', async () => { 104 | const code = await fetchFile('bundle-corruptedmap.js'); 105 | expect(code).toBe(`(function () { 106 | 'use strict'; 107 | 108 | function shared() { 109 | console.log(); 110 | } 111 | 112 | shared(); 113 | 114 | })(); 115 | //# sourceMappingURL=bundle-corruptedmap.js.map 116 | `); 117 | }); 118 | 119 | it('leaves the code without an external source map reference intact', async () => { 120 | const code = await fetchFile('bundle-nomapref.js'); 121 | expect(code).toBe(`(function () { 122 | 'use strict'; 123 | 124 | function shared() { 125 | console.log(); 126 | } 127 | 128 | shared(); 129 | 130 | })(); 131 | `); 132 | }); 133 | 134 | it('remaps sources in an external source map', async () => { 135 | const map = JSON.parse(await fetchFile('bundle.js.map')); 136 | for (const source of map.sources) { 137 | expect(source).toContain('../src/'); 138 | } 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module karmaSourcemapLoader { 2 | type File = { 3 | path: string; 4 | originalPath: string; 5 | sourceMap?: SourceMap; 6 | }; 7 | 8 | type Config = { 9 | sourceMapLoader?: { 10 | remapPrefixes?: Record; 11 | remapSource?: (source: string) => string | false | null | undefined; 12 | useSourceRoot?: 13 | | ((file: File) => string | false | null | undefined) 14 | | string; 15 | onlyWithURL?: boolean; 16 | strict?: boolean; 17 | }; 18 | }; 19 | 20 | type Preprocessor = ( 21 | content: string, 22 | file: File, 23 | done: (result: string | Error) => void 24 | ) => void; 25 | 26 | type SourceMap = { 27 | sources: string[]; 28 | sourceRoot?: string; 29 | }; 30 | } 31 | --------------------------------------------------------------------------------