From 9a195d64c4ef06d90dc156c61fbe1b2175669ee0 Mon Sep 17 00:00:00 2001 From: cap-Bernardito Date: Fri, 22 May 2020 17:44:00 +0300 Subject: [PATCH] fix: issue 55 --- src/index.js | 20 +--- src/utils.js | 40 +++++++ test/__snapshots__/loader.test.js.snap | 18 ++- .../sourceMapperRegexp.test.js.snap | 27 +++++ test/fixtures/normal-file2.js | 3 + test/loader.test.js | 12 ++ test/sourceMapperRegexp.test.js | 106 ++++++++++++++++++ 7 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 test/__snapshots__/sourceMapperRegexp.test.js.snap create mode 100644 test/fixtures/normal-file2.js create mode 100644 test/sourceMapperRegexp.test.js diff --git a/src/index.js b/src/index.js index 0debb6a..635b492 100644 --- a/src/index.js +++ b/src/index.js @@ -19,16 +19,9 @@ import { readFile, getContentFromSourcesContent, isUrlRequest, + getSourceMappingUrl, } from './utils'; -// Matches only the last occurrence of sourceMappingURL -const baseRegex = - '\\s*[@#]\\s*sourceMappingURL\\s*=\\s*([^\\s]*)(?![\\S\\s]*sourceMappingURL)'; -// Matches /* ... */ comments -const regex1 = new RegExp(`/\\*${baseRegex}\\s*\\*/`); -// Matches // .... comments -const regex2 = new RegExp(`//${baseRegex}($|\n|\r\n?)`); - export default function loader(input, inputMap) { const options = getOptions(this); @@ -37,17 +30,16 @@ export default function loader(input, inputMap) { baseDataPath: 'options', }); - const match = input.match(regex1) || input.match(regex2); + let { url } = getSourceMappingUrl(input); + const { replacementString } = getSourceMappingUrl(input); const callback = this.async(); - if (!match) { + if (!url) { callback(null, input, inputMap); return; } - let [, url] = match; - const dataURL = parseDataURL(url); const { context, resolve, addDependency, emitWarning } = this; @@ -60,7 +52,7 @@ export default function loader(input, inputMap) { labelToName(dataURL.mimeType.parameters.get('charset')) || 'UTF-8'; map = decode(dataURL.body, dataURL.encodingName); - map = JSON.parse(map); + map = JSON.parse(map.replace(/^\)\]\}'/, '')); } catch (error) { emitWarning( `Cannot parse inline SourceMap with Charset ${dataURL.encodingName}: ${error}` @@ -226,6 +218,6 @@ export default function loader(input, inputMap) { delete resultMap.sourcesContent; } - callback(null, input.replace(match[0], ''), resultMap); + callback(null, input.replace(replacementString, ''), resultMap); } } diff --git a/src/utils.js b/src/utils.js index b3ff87d..b0eb785 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,6 +3,28 @@ import path from 'path'; import sourceMap from 'source-map'; +// Matches only the last occurrence of sourceMappingURL +const innerRegex = /\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*/; + +/* eslint-disable prefer-template */ +const sourceMappingURLRegex = RegExp( + '(?:' + + '/\\*' + + '(?:\\s*\r?\n(?://)?)?' + + '(?:' + + innerRegex.source + + ')' + + '\\s*' + + '\\*/' + + '|' + + '//(?:' + + innerRegex.source + + ')' + + ')' + + '\\s*' +); +/* eslint-enable prefer-template */ + async function flattenSourceMap(map) { const consumer = await new sourceMap.SourceMapConsumer(map); let generatedMap; @@ -80,9 +102,27 @@ function isUrlRequest(url) { return true; } +function getSourceMappingUrl(code) { + const lines = code.split(/^/m); + let match; + + for (let i = lines.length - 1; i >= 0; i--) { + match = lines[i].match(sourceMappingURLRegex); + if (match) { + break; + } + } + + return { + url: match ? match[1] || match[2] || '' : null, + replacementString: match ? match[0] : null, + }; +} + export { flattenSourceMap, readFile, getContentFromSourcesContent, isUrlRequest, + getSourceMappingUrl, }; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 7b4d020..e17faf2 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -6,6 +6,17 @@ exports[`source-map-loader should leave normal files untouched: errors 1`] = `Ar exports[`source-map-loader should leave normal files untouched: warnings 1`] = `Array []`; +exports[`source-map-loader should leave normal files with fake source-map untouched: css 1`] = ` +"// without SourceMap +anInvalidDirective = \\"\\\\n/*# sourceMappingURL=data:application/json;base64,\\"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+\\" */\\"; +// comment +" +`; + +exports[`source-map-loader should leave normal files with fake source-map untouched: errors 1`] = `Array []`; + +exports[`source-map-loader should leave normal files with fake source-map untouched: warnings 1`] = `Array []`; + exports[`source-map-loader should process external SourceMaps (external sources): css 1`] = ` "with SourceMap // comment" @@ -136,7 +147,12 @@ exports[`source-map-loader should skip invalid base64 SourceMap: css 1`] = ` exports[`source-map-loader should skip invalid base64 SourceMap: errors 1`] = `Array []`; -exports[`source-map-loader should skip invalid base64 SourceMap: warnings 1`] = `Array []`; +exports[`source-map-loader should skip invalid base64 SourceMap: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Cannot parse inline SourceMap with Charset UTF-8: SyntaxError: Unexpected end of JSON input", +] +`; exports[`source-map-loader should support absolute sourceRoot paths in sourcemaps: css 1`] = ` "with SourceMap diff --git a/test/__snapshots__/sourceMapperRegexp.test.js.snap b/test/__snapshots__/sourceMapperRegexp.test.js.snap new file mode 100644 index 0000000..8627b50 --- /dev/null +++ b/test/__snapshots__/sourceMapperRegexp.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`source-map-loader should be null: result 1`] = `null`; + +exports[`source-map-loader should be null: result 2`] = `null`; + +exports[`source-map-loader should find last map: result 1`] = `"/sample-source-map-last.map"`; + +exports[`source-map-loader should not include quotes: result 1`] = `"data:application/json;base64,"`; + +exports[`source-map-loader should work: result 1`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 2`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 3`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 4`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 5`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 6`] = `"absolute-sourceRoot-source-map.map"`; + +exports[`source-map-loader should work: result 7`] = `"http://sampledomain.com/external-source-map2.map"`; + +exports[`source-map-loader should work: result 8`] = `"data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ=="`; + +exports[`source-map-loader should work: result 9`] = `"/sample-source-map.map"`; diff --git a/test/fixtures/normal-file2.js b/test/fixtures/normal-file2.js new file mode 100644 index 0000000..07b85d9 --- /dev/null +++ b/test/fixtures/normal-file2.js @@ -0,0 +1,3 @@ +// without SourceMap +anInvalidDirective = "\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */"; +// comment diff --git a/test/loader.test.js b/test/loader.test.js index 04c57d4..85f55e2 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -26,6 +26,18 @@ describe('source-map-loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('should leave normal files with fake source-map untouched', async () => { + const testId = 'normal-file2.js'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.map).toBeUndefined(); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + it('should process inlined SourceMaps', async () => { const testId = 'inline-source-map.js'; const compiler = getCompiler(testId); diff --git a/test/sourceMapperRegexp.test.js b/test/sourceMapperRegexp.test.js new file mode 100644 index 0000000..c819f87 --- /dev/null +++ b/test/sourceMapperRegexp.test.js @@ -0,0 +1,106 @@ +import { getSourceMappingUrl } from '../src/utils'; + +describe('source-map-loader', () => { + it('should work', async () => { + const code = `/*#sourceMappingURL=absolute-sourceRoot-source-map.map*/`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = `/* #sourceMappingURL=absolute-sourceRoot-source-map.map */`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = `//#sourceMappingURL=absolute-sourceRoot-source-map.map`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = `//@sourceMappingURL=absolute-sourceRoot-source-map.map`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = ` // #sourceMappingURL=absolute-sourceRoot-source-map.map`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = ` // # sourceMappingURL = absolute-sourceRoot-source-map.map `; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = `// #sourceMappingURL = http://sampledomain.com/external-source-map2.map`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = `// @sourceMappingURL=data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should work', async () => { + const code = ` + with SourceMap + + // #sourceMappingURL = /sample-source-map.map + // comment + `; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should find last map', async () => { + const code = ` + with SourceMap + // #sourceMappingURL = /sample-source-map-1.map + // #sourceMappingURL = /sample-source-map-2.map + // #sourceMappingURL = /sample-source-map-last.map + // comment + `; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should be null', async () => { + const code = `" + /*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should be null', async () => { + const code = `anInvalidDirective = "\\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); + + it('should not include quotes', async () => { + const code = `// # sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+"`; + const { url } = getSourceMappingUrl(code); + + expect(url).toMatchSnapshot('result'); + }); +});