diff --git a/src/index.js b/src/index.js index b4d7c311..2e9a56dd 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,6 @@ import { getOptions, isUrlRequest } from 'loader-utils'; import schema from './options.json'; import { importParser, icssParser, urlParser } from './plugins'; import { - normalizeSourceMap, getModulesPlugins, getFilter, getImportCode, @@ -75,8 +74,7 @@ export default function loader(content, map, meta) { to: this.currentRequest.split('!').pop(), map: options.sourceMap ? { - // Some loaders (example `"postcss-loader": "1.x.x"`) always generates source map, we should remove it - prev: sourceMap && map ? normalizeSourceMap(map) : null, + prev: sourceMap && map ? map : null, inline: false, annotation: false, } diff --git a/src/utils.js b/src/utils.js index c5b00d10..aae5ff13 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,6 +16,7 @@ import localByDefault from 'postcss-modules-local-by-default'; import extractImports from 'postcss-modules-extract-imports'; import modulesScope from 'postcss-modules-scope'; import camelCase from 'camelcase'; +import RequestShortener from 'webpack/lib/RequestShortener'; const whitespace = '[\\x20\\t\\r\\n\\f]'; const unescapeRegExp = new RegExp( @@ -156,31 +157,28 @@ function getModulesPlugins(options, loaderContext) { ]; } -function normalizeSourceMap(map) { - let newMap = map; - - // Some loader emit source map as string - // Strip any JSON XSSI avoidance prefix from the string (as documented in the source maps specification), and then parse the string as JSON. - if (typeof newMap === 'string') { - newMap = JSON.parse(newMap); - } +function normalizeSourceMap(map, rootContext) { + const newMap = map.toJSON(); + const requestShortener = new RequestShortener(rootContext); // Source maps should use forward slash because it is URLs (https://github.com/mozilla/source-map/issues/91) // We should normalize path because previous loaders like `sass-loader` using backslash when generate source map if (newMap.file) { - newMap.file = normalizePath(newMap.file); + newMap.file = requestShortener.shorten(newMap.file); } if (newMap.sourceRoot) { - newMap.sourceRoot = normalizePath(newMap.sourceRoot); + newMap.sourceRoot = requestShortener.shorten(newMap.sourceRoot); } if (newMap.sources) { - newMap.sources = newMap.sources.map((source) => normalizePath(source)); + newMap.sources = newMap.sources.map((source) => + requestShortener.shorten(source) + ); } - return newMap; + return JSON.stringify(newMap); } function getImportPrefix(loaderContext, importLoaders) { @@ -372,7 +370,10 @@ function getModuleCode( } const { css, map } = result; - const sourceMapValue = sourceMap && map ? `,${map}` : ''; + const sourceMapValue = + sourceMap && map + ? `,${normalizeSourceMap(map, loaderContext.rootContext)}` + : ''; let cssCode = JSON.stringify(css); @@ -501,7 +502,6 @@ export { normalizeUrl, getFilter, getModulesPlugins, - normalizeSourceMap, getImportCode, getModuleCode, getExportCode, diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index ef5ed1b2..826e60ad 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`loader should have same "contenthash" with "css-loader" and with source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "css-loader" and with source maps: warnings 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "css-loader" and without source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "css-loader" and without source maps: warnings 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "postcss-loader" and with source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "postcss-loader" and with source maps: warnings 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "postcss-loader" and without source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "postcss-loader" and without source maps: warnings 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "sass-loader" and with source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "sass-loader" and with source maps: warnings 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "sass-loader" and without source maps: errors 1`] = `Array []`; + +exports[`loader should have same "contenthash" with "sass-loader" and without source maps: warnings 1`] = `Array []`; + exports[`loader should reuse \`ast\` from "postcss-loader": errors 1`] = `Array []`; exports[`loader should reuse \`ast\` from "postcss-loader": module 1`] = ` diff --git a/test/fixtures/contenthash/basic-css.js b/test/fixtures/contenthash/basic-css.js new file mode 100644 index 00000000..72afb950 --- /dev/null +++ b/test/fixtures/contenthash/basic-css.js @@ -0,0 +1,5 @@ +import css from './basic.css'; + +__export__ = css; + +export default css; diff --git a/test/fixtures/contenthash/basic-postcss.js b/test/fixtures/contenthash/basic-postcss.js new file mode 100644 index 00000000..ff2b03ea --- /dev/null +++ b/test/fixtures/contenthash/basic-postcss.js @@ -0,0 +1,5 @@ +import css from './basic.postcss.css'; + +__export__ = css; + +export default css; diff --git a/test/fixtures/contenthash/basic-sass.js b/test/fixtures/contenthash/basic-sass.js new file mode 100644 index 00000000..68ce09f0 --- /dev/null +++ b/test/fixtures/contenthash/basic-sass.js @@ -0,0 +1,5 @@ +import css from './basic.scss'; + +__export__ = css; + +export default css; diff --git a/test/fixtures/contenthash/basic.css b/test/fixtures/contenthash/basic.css new file mode 100644 index 00000000..fdece7b4 --- /dev/null +++ b/test/fixtures/contenthash/basic.css @@ -0,0 +1,3 @@ +a { + color: red; +} diff --git a/test/fixtures/contenthash/basic.postcss.css b/test/fixtures/contenthash/basic.postcss.css new file mode 100644 index 00000000..3223fac4 --- /dev/null +++ b/test/fixtures/contenthash/basic.postcss.css @@ -0,0 +1,7 @@ +a { + color: rgb(0 0 100% / 90%); + + &:hover { + color: rebeccapurple; + } +} diff --git a/test/fixtures/contenthash/basic.scss b/test/fixtures/contenthash/basic.scss new file mode 100644 index 00000000..ae47c032 --- /dev/null +++ b/test/fixtures/contenthash/basic.scss @@ -0,0 +1,9 @@ +$var: red; + +a { + color: $var; + + &:hover { + color: $var; + } +} diff --git a/test/loader.test.js b/test/loader.test.js index 68741413..fa704189 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -308,4 +308,300 @@ describe('loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should have same "contenthash" with "css-loader" and without source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-css.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.css$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { sourceMap: false }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.fd612b1d69f7c1e6ba5f.bundle.js' + : 'main.b38d5f87c88c55a4258e.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should have same "contenthash" with "css-loader" and with source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-css.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.css$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { sourceMap: true }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.4e80ca040390d63ea450.bundle.js' + : 'main.54b9712c1981e48c12c4.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should have same "contenthash" with "postcss-loader" and without source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-postcss.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.css$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { + sourceMap: false, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: () => [postcssPresetEnv({ stage: 0 })], + sourceMap: false, + }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.47a6533d9b651ffbecad.bundle.js' + : 'main.f8e62206a43c13393798.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should have same "contenthash" with "postcss-loader" and with source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-postcss.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.css$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { + sourceMap: true, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: () => [postcssPresetEnv({ stage: 0 })], + sourceMap: true, + }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.0a16c4c25cba08ab696a.bundle.js' + : 'main.d77dd6564bc6e6297cd4.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should have same "contenthash" with "sass-loader" and without source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-sass.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.s[ca]ss$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { + sourceMap: false, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: () => [postcssPresetEnv({ stage: 0 })], + sourceMap: false, + }, + }, + { + loader: 'sass-loader', + options: { + // eslint-disable-next-line global-require + implementation: require('sass'), + sourceMap: false, + }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.f3d743a96cdd6e368436.bundle.js' + : 'main.9ae036ace8bbbebbd185.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should have same "contenthash" with "sass-loader" and with source maps', async () => { + const compiler = getCompiler( + './contenthash/basic-sass.js', + {}, + { + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].[contenthash].bundle.js', + chunkFilename: '[name].[contenthash].chunk.js', + publicPath: '/webpack/public/path/', + }, + module: { + rules: [ + { + test: /\.s[ca]ss$/i, + rules: [ + { + loader: path.resolve(__dirname, '../src'), + options: { + sourceMap: true, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: () => [postcssPresetEnv({ stage: 0 })], + sourceMap: true, + }, + }, + { + loader: 'sass-loader', + options: { + // eslint-disable-next-line global-require + implementation: require('sass'), + sourceMap: true, + }, + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + const isWebpack5 = version[0] === '5'; + + expect( + stats.compilation.assets[ + isWebpack5 + ? 'main.021e49d811fb525fefec.bundle.js' + : 'main.a12ab765493c74c80be2.bundle.js' + ] + ).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); });