diff --git a/package-lock.json b/package-lock.json index 676374a..79a83e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12069,7 +12069,8 @@ "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true }, "nice-try": { "version": "1.0.5", diff --git a/package.json b/package.json index 0572eaf..d6ba112 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "dependencies": { "data-urls": "^2.0.0", "loader-utils": "^2.0.0", - "neo-async": "^2.6.1", "schema-utils": "^2.6.6", "source-map": "^0.6.0", "whatwg-encoding": "^1.0.5" diff --git a/src/index.js b/src/index.js index 303a9a9..5ff0623 100644 --- a/src/index.js +++ b/src/index.js @@ -6,14 +6,13 @@ import fs from 'fs'; import path from 'path'; import validateOptions from 'schema-utils'; -import async from 'neo-async'; import parseDataURL from 'data-urls'; +import { SourceMapConsumer } from 'source-map'; import { labelToName, decode } from 'whatwg-encoding'; import { getOptions, urlToRequest } from 'loader-utils'; -import flattenSourceMap from './utils/flatten'; - import schema from './options.json'; +import { flattenSourceMap, normalize, MapAgregator } from './utils'; // Matches only the last occurrence of sourceMappingURL const baseRegex = @@ -121,70 +120,68 @@ export default function loader(input, inputMap) { map = await flattenSourceMap(map); } - if (map.sourcesContent && map.sourcesContent.length >= map.sources.length) { - callback(null, input.replace(match[0], ''), map); - - return; - } - - const sourcePrefix = map.sourceRoot ? `${map.sourceRoot}${path.sep}` : ''; - - // eslint-disable-next-line no-param-reassign - map.sources = map.sources.map((s) => sourcePrefix + s); - - // eslint-disable-next-line no-param-reassign - delete map.sourceRoot; - - const missingSources = map.sourcesContent - ? map.sources.slice(map.sourcesContent.length) - : map.sources; - - async.map( - missingSources, - // eslint-disable-next-line no-shadow - (source, callback) => { - resolve(context, urlToRequest(source, true), (resolveError, result) => { - if (resolveError) { - emitWarning(`Cannot find source file '${source}': ${resolveError}`); + const mapConsumer = await new SourceMapConsumer(map); + + const resolvedSources = await Promise.all( + map.sources.map(async (source) => { + const fullPath = map.sourceRoot + ? `${map.sourceRoot}/${source}` + : source; + const sourceData = new MapAgregator({ + mapConsumer, + source, + fullPath, + emitWarning, + }); - callback(null, null); + if (path.isAbsolute(fullPath)) { + return sourceData.content; + } + + return new Promise((promiseResolve) => { + resolve( + context, + urlToRequest(fullPath, true), + (resolveError, result) => { + if (resolveError) { + emitWarning( + `Cannot find source file '${source}': ${resolveError}` + ); + + return promiseResolve(sourceData.placeholderContent); + } + + sourceData.setFullPath(result); + return promiseResolve(sourceData.content); + } + ); + }); + }) + ); - return; - } + const resultMap = { ...map }; + resultMap.sources = []; + resultMap.sourcesContent = []; - addDependency(result); + delete resultMap.sourceRoot; - fs.readFile(result, 'utf-8', (readFileError, content) => { - if (readFileError) { - emitWarning( - `Cannot open source file '${result}': ${readFileError}` - ); + resolvedSources.forEach((res) => { + // eslint-disable-next-line no-param-reassign + resultMap.sources.push(normalize(res.source)); + resultMap.sourcesContent.push(res.content); - callback(null, null); + if (res.source) { + addDependency(res.source); + } + }); - return; - } + const sourcesContentIsEmpty = + resultMap.sourcesContent.filter((entry) => !!entry).length === 0; - callback(null, { source: result, content }); - }); - }); - }, - (err, info) => { - // eslint-disable-next-line no-param-reassign - map.sourcesContent = map.sourcesContent || []; - - info.forEach((res) => { - if (res) { - // eslint-disable-next-line no-param-reassign - map.sources[map.sourcesContent.length] = res.source; - map.sourcesContent.push(res.content); - } else { - map.sourcesContent.push(null); - } - }); + if (sourcesContentIsEmpty) { + delete resultMap.sourcesContent; + } - processMap(map, context, callback); - } - ); + callback(null, input.replace(match[0], ''), resultMap); } } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..af3a43b --- /dev/null +++ b/src/utils.js @@ -0,0 +1,90 @@ +import fs from 'fs'; + +import sourceMap from 'source-map'; + +async function flattenSourceMap(map) { + const consumer = await new sourceMap.SourceMapConsumer(map); + let generatedMap; + + if (map.file) { + generatedMap = new sourceMap.SourceMapGenerator({ + file: map.file, + }); + } else { + generatedMap = new sourceMap.SourceMapGenerator(); + } + + consumer.sources.forEach((sourceFile) => { + const sourceContent = consumer.sourceContentFor(sourceFile, true); + generatedMap.setSourceContent(sourceFile, sourceContent); + }); + + consumer.eachMapping((mapping) => { + const { source } = consumer.originalPositionFor({ + line: mapping.generatedLine, + column: mapping.generatedColumn, + }); + + const mappings = { + source, + original: { + line: mapping.originalLine, + column: mapping.originalColumn, + }, + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn, + }, + }; + + if (source) { + generatedMap.addMapping(mappings); + } + }); + + return generatedMap.toJSON(); +} + +function normalize(path) { + return path.replace(/\\/g, '/'); +} + +function readFile(fullPath, charset, emitWarning) { + return new Promise((resolve) => { + fs.readFile(fullPath, charset, (readFileError, content) => { + if (readFileError) { + emitWarning(`Cannot open source file '${fullPath}': ${readFileError}`); + + resolve({ source: fullPath, content: null }); + } + + resolve({ source: fullPath, content }); + }); + }); +} + +class MapAgregator { + constructor({ mapConsumer, source, fullPath, emitWarning }) { + this.fullPath = fullPath; + this.sourceContent = mapConsumer.sourceContentFor(source, true); + this.emitWarning = emitWarning; + } + + setFullPath(path) { + this.fullPath = path; + } + + get content() { + return this.sourceContent + ? { source: this.fullPath, content: this.sourceContent } + : readFile(this.fullPath, 'utf-8', this.emitWarning); + } + + get placeholderContent() { + return this.sourceContent + ? { source: this.fullPath, content: this.sourceContent } + : { source: this.fullPath, content: null }; + } +} + +export { flattenSourceMap, normalize, MapAgregator }; diff --git a/src/utils/flatten.js b/src/utils/flatten.js deleted file mode 100644 index 0003eff..0000000 --- a/src/utils/flatten.js +++ /dev/null @@ -1,46 +0,0 @@ -import sourceMap from 'source-map'; - -async function FlattenSourceMap(map) { - const consumer = await new sourceMap.SourceMapConsumer(map); - let generatedMap; - - if (map.file) { - generatedMap = new sourceMap.SourceMapGenerator({ - file: map.file, - }); - } else { - generatedMap = new sourceMap.SourceMapGenerator(); - } - - consumer.sources.forEach((sourceFile) => { - const sourceContent = consumer.sourceContentFor(sourceFile, true); - generatedMap.setSourceContent(sourceFile, sourceContent); - }); - - consumer.eachMapping((mapping) => { - const { source } = consumer.originalPositionFor({ - line: mapping.generatedLine, - column: mapping.generatedColumn, - }); - - const mappings = { - source, - original: { - line: mapping.originalLine, - column: mapping.originalColumn, - }, - generated: { - line: mapping.generatedLine, - column: mapping.generatedColumn, - }, - }; - - if (source) { - generatedMap.addMapping(mappings); - } - }); - - return generatedMap.toJSON(); -} - -module.exports = FlattenSourceMap; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 78dce75..0f9f84f 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -50,7 +50,12 @@ Object { } `; -exports[`source-map-loader should process external SourceMaps: warnings 1`] = `Array []`; +exports[`source-map-loader should process external SourceMaps: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Cannot find source file 'external-source-map.txt': Error: Can't resolve './external-source-map.txt' in '/test/fixtures'", +] +`; exports[`source-map-loader should process inlined SourceMaps with charset: css 1`] = ` "with SourceMap @@ -64,7 +69,7 @@ Object { "file": "charset-inline-source-map.js", "mappings": "AAAA", "sources": Array [ - "charset-inline-source-map.txt", + "/test/fixtures/charset-inline-source-map.txt - (normalized for test)", ], "sourcesContent": Array [ "with SourceMap", @@ -96,7 +101,12 @@ Object { } `; -exports[`source-map-loader should process inlined SourceMaps: warnings 1`] = `Array []`; +exports[`source-map-loader should process inlined SourceMaps: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Cannot find source file 'inline-source-map.txt': Error: Can't resolve './inline-source-map.txt' in '/test/fixtures'", +] +`; exports[`source-map-loader should skip invalid base64 SourceMap: css 1`] = ` "without SourceMap @@ -133,7 +143,7 @@ Object { exports[`source-map-loader should support absolute sourceRoot paths in sourcemaps: warnings 1`] = `Array []`; exports[`source-map-loader should support indexed sourcemaps: css 1`] = ` -"with SourceMap +"console.log('with SourceMap') // Map taken from here // https://github.com/mozilla/source-map/blob/master/test/util.js - indexedTestMapDifferentSourceRoots " @@ -147,7 +157,7 @@ Object { "mappings": "CAAC,IAAI,IAAM,SAAU,GAClB,OAAO,IAAI;CCDb,IAAI,IAAM,SAAU,GAClB,OAAO", "names": Array [], "sources": Array [ - "/the/root/nested1.js", + "/test/fixtures/indexed-sourcemap/nested1.js - (normalized for test)", "/different/root/nested2.js", ], "sourcesContent": Array [ @@ -210,7 +220,12 @@ Object { } `; -exports[`source-map-loader should use last SourceMap directive: warnings 1`] = `Array []`; +exports[`source-map-loader should use last SourceMap directive: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Cannot find source file 'inline-source-map.txt': Error: Can't resolve './inline-source-map.txt' in '/test/fixtures'", +] +`; exports[`source-map-loader should warn on invalid SourceMap: css 1`] = ` "with SourceMap @@ -271,9 +286,6 @@ Object { "sources": Array [ "missing-source-map2.txt", ], - "sourcesContent": Array [ - null, - ], "version": 3, } `; diff --git a/test/fixtures/charset-inline-source-map.txt b/test/fixtures/charset-inline-source-map.txt new file mode 100644 index 0000000..c5b4408 --- /dev/null +++ b/test/fixtures/charset-inline-source-map.txt @@ -0,0 +1 @@ +// Content diff --git a/test/fixtures/indexed-sourcemap/file.js b/test/fixtures/indexed-sourcemap/file.js index 2be7348..83076fd 100644 --- a/test/fixtures/indexed-sourcemap/file.js +++ b/test/fixtures/indexed-sourcemap/file.js @@ -1,4 +1,4 @@ -with SourceMap +console.log('with SourceMap') //#sourceMappingURL=file.js.map // Map taken from here // https://github.com/mozilla/source-map/blob/master/test/util.js - indexedTestMapDifferentSourceRoots diff --git a/test/fixtures/indexed-sourcemap/file.js.map b/test/fixtures/indexed-sourcemap/file.js.map index 1dc44df..930cc52 100644 --- a/test/fixtures/indexed-sourcemap/file.js.map +++ b/test/fixtures/indexed-sourcemap/file.js.map @@ -15,8 +15,7 @@ ], "names": ["bar", "baz"], "mappings": "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID", - "file": "file.js", - "sourceRoot": "/the/root" + "file": "file.js" } }, { diff --git a/test/helpers/getCompiler.js b/test/helpers/getCompiler.js index b391311..6e34d5a 100644 --- a/test/helpers/getCompiler.js +++ b/test/helpers/getCompiler.js @@ -3,7 +3,25 @@ import path from 'path'; import webpack from 'webpack'; import { createFsFromVolume, Volume } from 'memfs'; -export default (fixture, loaderOptions = {}, config = {}) => { +export default ( + fixture, + loaderOptions = {}, + config = {}, + skipTestLoader = false +) => { + const loaders = [ + { + loader: path.resolve(__dirname, '../../src'), + options: loaderOptions || {}, + }, + ]; + + if (!skipTestLoader) { + loaders.unshift({ + loader: require.resolve('./testLoader'), + }); + } + const fullConfig = { mode: 'development', devtool: config.devtool || 'source-map', @@ -19,15 +37,7 @@ export default (fixture, loaderOptions = {}, config = {}) => { rules: [ { test: /\.js/i, - rules: [ - { - loader: require.resolve('./testLoader'), - }, - { - loader: path.resolve(__dirname, '../../src'), - options: loaderOptions || {}, - }, - ], + use: loaders, }, ], }, diff --git a/test/helpers/normalizeMap.js b/test/helpers/normalizeMap.js index 89c0bf9..bc5b279 100644 --- a/test/helpers/normalizeMap.js +++ b/test/helpers/normalizeMap.js @@ -33,13 +33,6 @@ function removeCWD(str) { let cwd = process.cwd(); if (isWin) { - // Todo: explore the issue - // if (str.includes('/')) { - // throw new Error( - // 'There should not be a forward slash in the Windows path' - // ); - // } - // eslint-disable-next-line no-param-reassign str = str.replace(/\\/g, '/'); cwd = cwd.replace(/\\/g, '/'); diff --git a/test/loader.test.js b/test/loader.test.js index e2261bf..04d21c3 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -8,6 +8,7 @@ import { getErrors, normalizeMap, getWarnings, + readAsset, } from './helpers'; describe('source-map-loader', () => { @@ -267,6 +268,8 @@ describe('source-map-loader', () => { const dependencies = [ path.join(currentDirPath, 'file.js'), path.join(currentDirPath, 'file.js.map'), + path.join(currentDirPath, 'nested1.js'), + `/different/root/nested2.js`, ]; dependencies.forEach((fixture) => { @@ -278,4 +281,28 @@ describe('source-map-loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should transform to webpack', async () => { + const currentDirPath = path.join( + __dirname, + 'fixtures', + 'indexed-sourcemap' + ); + + const testId = path.join(currentDirPath, 'file.js'); + const compiler = getCompiler(testId, {}, {}, true); + const stats = await compile(compiler); + const bundle = readAsset('main.bundle.js.map', compiler, stats); + + const dependencies = [ + 'indexed-sourcemap/nested1.js', + 'different/root/nested2.js', + 'webpack/bootstrap', + ]; + + // Todo: rewrite when we will fix issue whith unresolved paths + dependencies.forEach((fixture) => { + expect(bundle.indexOf(fixture) !== -1).toBe(true); + }); + }); });