diff --git a/package-lock.json b/package-lock.json index 79a83e1..e4e0c79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12078,6 +12078,12 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index d6ba112..fac45d9 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "jest": "^26.0.1", "lint-staged": "^10.2.2", "memfs": "^3.1.2", + "node-fetch": "^2.6.0", "npm-run-all": "^4.1.5", "prettier": "^2.0.5", "standard-version": "^8.0.0", diff --git a/src/index.js b/src/index.js index 64a5211..85c1659 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import schema from './options.json'; import { flattenSourceMap, readFile, + fetchFile, getContentFromSourcesContent, getSourceMappingUrl, getRequestedUrl, @@ -31,6 +32,7 @@ export default async function loader(input, inputMap) { baseDataPath: 'options', }); + const { fetchReader } = options; let { url } = getSourceMappingUrl(input); const { replacementString } = getSourceMappingUrl(input); const callback = this.async(); @@ -143,6 +145,12 @@ export default async function loader(input, inputMap) { source ); + if (/^https?:\/\//.test(fullPath)) { + return originalData + ? { source: fullPath, content: originalData } + : fetchFile(fullPath, emitWarning, fetchReader); + } + if (path.isAbsolute(fullPath)) { return originalData ? { source: fullPath, content: originalData } diff --git a/src/options.json b/src/options.json index 97ea2a4..eab294e 100644 --- a/src/options.json +++ b/src/options.json @@ -1,4 +1,9 @@ { "type": "object", + "properties": { + "fetchReader": { + "instanceof": "Function" + } + }, "additionalProperties": false } diff --git a/src/utils.js b/src/utils.js index 283ef62..027b2f9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -77,12 +77,29 @@ async function readFile(fullPath, emitWarning, reader) { } catch (readFileError) { emitWarning(`Cannot open source file '${fullPath}': ${readFileError}`); - return { source: null, content: null }; + return { source: fullPath, content: null }; } return { source: fullPath, content: content.toString() }; } +async function fetchFile(url, emitWarning, reader) { + if (!reader) { + return { source: url, content: null }; + } + + let content; + + try { + content = await reader(url); + return { source: url, content }; + } catch (fetchError) { + emitWarning(`Cannot fetch source file '${url}': ${fetchError}`); + + return { source: url, content: null }; + } +} + function getContentFromSourcesContent(consumer, source) { return consumer.sourceContentFor(source, true); } @@ -141,6 +158,7 @@ function getRequestedUrl(url) { export { flattenSourceMap, readFile, + fetchFile, getContentFromSourcesContent, isUrlRequest, getSourceMappingUrl, diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index e17faf2..ecb2587 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -17,6 +17,33 @@ exports[`source-map-loader should leave normal files with fake source-map untouc exports[`source-map-loader should leave normal files with fake source-map untouched: warnings 1`] = `Array []`; +exports[`source-map-loader should not resolve SourceMap.sources to http: css 1`] = ` +"console.log('with SourceMap') +" +`; + +exports[`source-map-loader should not resolve SourceMap.sources to http: errors 1`] = `Array []`; + +exports[`source-map-loader should not resolve SourceMap.sources to http: map 1`] = ` +Object { + "file": "sources-http.js", + "mappings": "AAAA", + "sources": Array [ + "https:/github2.com/", + "https:/github.com/", + "https:/google.com/", + ], + "sourcesContent": Array [ + "static content", + null, + null, + ], + "version": 3, +} +`; + +exports[`source-map-loader should not resolve SourceMap.sources to http: warnings 1`] = `Array []`; + exports[`source-map-loader should process external SourceMaps (external sources): css 1`] = ` "with SourceMap // comment" @@ -139,6 +166,33 @@ exports[`source-map-loader should reject not exist file: SourceMaps: errors 1`] exports[`source-map-loader should reject not exist file: SourceMaps: warnings 1`] = `"TypeError [ERR_INVALID_FILE"`; +exports[`source-map-loader should resolve SourceMap.sources to http: css 1`] = ` +"console.log('with SourceMap') +" +`; + +exports[`source-map-loader should resolve SourceMap.sources to http: errors 1`] = `Array []`; + +exports[`source-map-loader should resolve SourceMap.sources to http: map 1`] = ` +Object { + "file": "sources-http.js", + "mappings": "AAAA", + "sources": Array [ + "https:/github2.com/", + "https:/github.com/", + "https:/google.com/", + ], + "sourcesContent": Array [ + "static content", + "some kind content", + "some kind content", + ], + "version": 3, +} +`; + +exports[`source-map-loader should resolve SourceMap.sources to http: warnings 1`] = `Array []`; + exports[`source-map-loader should skip invalid base64 SourceMap: css 1`] = ` "without SourceMap // @sourceMappingURL=data:application/source-map;base64,\\"something invalid\\" diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap new file mode 100644 index 0000000..4b1a398 --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validate options should throw an error on the "fetchReader" option with "[]" value 1`] = ` +"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema. + - options.fetchReader should be an instance of function." +`; + +exports[`validate options should throw an error on the "fetchReader" option with "{}" value 1`] = ` +"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema. + - options.fetchReader should be an instance of function." +`; + +exports[`validate options should throw an error on the "fetchReader" option with "1" value 1`] = ` +"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema. + - options.fetchReader should be an instance of function." +`; + +exports[`validate options should throw an error on the "fetchReader" option with "test" value 1`] = ` +"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema. + - options.fetchReader should be an instance of function." +`; + +exports[`validate options should throw an error on the "fetchReader" option with "true" value 1`] = ` +"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema. + - options.fetchReader should be an instance of function." +`; diff --git a/test/fixtures/fetch/sources-http.js b/test/fixtures/fetch/sources-http.js new file mode 100644 index 0000000..ad318c4 --- /dev/null +++ b/test/fixtures/fetch/sources-http.js @@ -0,0 +1,2 @@ +console.log('with SourceMap') +//#sourceMappingURL=sources-http.js.map diff --git a/test/fixtures/fetch/sources-http.js.map b/test/fixtures/fetch/sources-http.js.map new file mode 100644 index 0000000..b08bb24 --- /dev/null +++ b/test/fixtures/fetch/sources-http.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sources-http.js","sources":["https://github2.com/", "https://github.com/", "https://google.com/"],"sourcesContent":["static content"],"mappings":"AAAA"} diff --git a/test/loader.test.js b/test/loader.test.js index 85f55e2..3f9c64f 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -1,6 +1,8 @@ import path from 'path'; import fs from 'fs'; +import fetch from 'node-fetch'; + import { compile, getCodeFromBundle, @@ -94,6 +96,40 @@ describe('source-map-loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('should resolve SourceMap.sources to http', async () => { + const currentDirPath = path.join(__dirname, 'fixtures', 'fetch'); + + const testId = path.join(currentDirPath, 'sources-http.js'); + const compiler = getCompiler(testId, { + fetchReader(url) { + return fetch(url) + .then((res) => res.text()) + .then(() => 'some kind content'); + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(normalizeMap(codeFromBundle.map)).toMatchSnapshot('map'); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should not resolve SourceMap.sources to http', async () => { + const currentDirPath = path.join(__dirname, 'fixtures', 'fetch'); + + const testId = path.join(currentDirPath, 'sources-http.js'); + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(normalizeMap(codeFromBundle.map)).toMatchSnapshot('map'); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + it('should reject http SourceMaps', async () => { const testId = 'http-source-map.js'; const compiler = getCompiler(testId); diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 0000000..68f4844 --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,57 @@ +import { getCompiler, compile } from './helpers/index'; + +describe('validate options', () => { + const tests = { + fetchReader: { + success: [() => 'test;'], + failure: [1, 'test', true, [], {}], + }, + }; + + function stringifyValue(value) { + if ( + Array.isArray(value) || + (value && typeof value === 'object' && value.constructor === Object) + ) { + return JSON.stringify(value); + } + + return value; + } + + async function createTestCase(key, value, type) { + it(`should ${ + type === 'success' ? 'successfully validate' : 'throw an error on' + } the "${key}" option with "${stringifyValue(value)}" value`, async () => { + const compiler = getCompiler('./normal-file.js', { + [key]: value, + }); + let stats; + + try { + stats = await compile(compiler); + } finally { + if (type === 'success') { + expect(stats.hasErrors()).toBe(false); + } else if (type === 'failure') { + const { + compilation: { errors }, + } = stats; + + expect(errors).toHaveLength(1); + expect(() => { + throw new Error(errors[0].error.message); + }).toThrowErrorMatchingSnapshot(); + } + } + }); + } + + for (const [key, values] of Object.entries(tests)) { + for (const type of Object.keys(values)) { + for (const value of values[type]) { + createTestCase(key, value, type); + } + } + } +});