diff --git a/README.md b/README.md index 1f1f43ea..b14f87ba 100644 --- a/README.md +++ b/README.md @@ -681,6 +681,54 @@ module.exports = { }; ``` +### Generate source maps for sass with asset/resource + +It is possible to extract files using [`asset modules`](https://webpack.js.org/guides/asset-modules/). + +This requires: + +- specify the `asset/resource` type for scss/sass files +- disable webpack source maps generation +- enable [`sourceMap`](https://sass-lang.com/documentation/js-api#sourcemap) in [`sassOptions`](#sassoptions) +- disable [`omitSourceMapUrl`](https://sass-lang.com/documentation/js-api#omitsourcemapurl) in [`sassOptions`](#sassoptions) +- disable [`sourceMapEmbed`](https://sass-lang.com/documentation/js-api#sourcemapembed) in [`sassOptions`](#sassoptions) + +**webpack.config.js** + +```javascript +module.exports = { + devtool: false, + module: { + rules: [ + { + test: /\.s[ac]ss$/i, + type: "asset/resource", + generator: { + filename: "assets/[name].css", + }, + use: [ + { + loader: "sass-loader", + options: { + sourceMap: false, + sassOptions: { + // If `sourceMap` is true, source map name will be "[name].css.map" + sourceMap: "assets/[name].css.map", + // A source map url is generated relative to this file + // The file name does not matter, only the directory structure is important + outFile: path.join(__dirname, "style.css"), + omitSourceMapUrl: false, + sourceMapEmbed: false, + }, + }, + }, + ], + }, + ], + }, +}; +``` + If you want to edit the original Sass files inside Chrome, [there's a good blog post](https://medium.com/@toolmantim/getting-started-with-css-sourcemaps-and-in-browser-sass-editing-b4daab987fb0). Checkout [test/sourceMap](https://github.com/webpack-contrib/sass-loader/tree/master/test) for a running example. ## Contributing diff --git a/src/index.js b/src/index.js index c1768953..8952cbbb 100644 --- a/src/index.js +++ b/src/index.js @@ -51,6 +51,24 @@ async function loader(content) { const render = getRenderFunctionFromSassImplementation(implementation); + const sourceMapShouldBeEmmited = + !sassOptions.omitSourceMapUrl && !sassOptions.sourceMapEmbed; + + if (sourceMapShouldBeEmmited && sassOptions.sourceMap) { + const filename = path.basename(this.resourcePath); + const mapNameTemplate = + sassOptions.sourceMap === true ? "[name].css.map" : sassOptions.sourceMap; + // eslint-disable-next-line no-underscore-dangle + const { path: mapFilename } = this._compilation.getPathWithInfo( + mapNameTemplate, + { + filename, + } + ); + + sassOptions.sourceMap = mapFilename; + } + render(sassOptions, (error, result) => { if (error) { // There are situations when the `file` property do not exist @@ -80,6 +98,12 @@ async function loader(content) { } }); + if (sourceMapShouldBeEmmited && map) { + this.emitFile(sassOptions.sourceMap, JSON.stringify(map)); + + map = null; + } + callback(null, result.css.toString(), map); }); } diff --git a/src/utils.js b/src/utils.js index 55a45b26..4f82e9b6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -180,7 +180,7 @@ async function getSassOptions( // all paths in sourceMap.sources will be relative to that path. // Pretty complicated... :( options.sourceMap = true; - options.outFile = path.join(loaderContext.rootContext, "style.css.map"); + options.outFile = path.join(loaderContext.rootContext, "style.css"); options.sourceMapContents = true; options.omitSourceMapUrl = true; options.sourceMapEmbed = false; diff --git a/test/__snapshots__/sourceMap-options.test.js.snap b/test/__snapshots__/sourceMap-options.test.js.snap index c420457e..bdf92835 100644 --- a/test/__snapshots__/sourceMap-options.test.js.snap +++ b/test/__snapshots__/sourceMap-options.test.js.snap @@ -1,5 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (node-sass) (sass): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (node-sass) (sass): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (node-sass) (scss): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (node-sass) (scss): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (node-sass) (sass): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (node-sass) (sass): warnings 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (node-sass) (scss): errors 1`] = `Array []`; + +exports[`sourceMap option should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (node-sass) (scss): warnings 1`] = `Array []`; + exports[`sourceMap option should generate source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (dart-sass) (sass): css 1`] = ` "@charset \\"UTF-8\\"; @import \\"./file.css\\"; diff --git a/test/sourceMap-options.test.js b/test/sourceMap-options.test.js index e89920fc..015164dd 100644 --- a/test/sourceMap-options.test.js +++ b/test/sourceMap-options.test.js @@ -235,6 +235,86 @@ describe("sourceMap option", () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); expect(getErrors(stats)).toMatchSnapshot("errors"); }); + + it(`should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "true" value (${implementationName}) (${syntax})`, async () => { + const testId = getTestId("language", syntax); + const options = { + implementation: getImplementationByName(implementationName), + sourceMap: false, + sassOptions: { + sourceMap: true, + outFile: path.join(__dirname, "style.css"), + sourceMapContents: true, + omitSourceMapUrl: false, + sourceMapEmbed: false, + }, + }; + const compiler = getCompiler(testId, { + devtool: false, + rules: [ + { + test: /\.s[ac]ss$/i, + type: "asset/resource", + generator: { + filename: "assets/[name].css", + }, + use: [ + { + loader: path.join(__dirname, "../src/cjs.js"), + options, + }, + ], + }, + ], + }); + const stats = await compile(compiler); + const { compilation } = stats; + + expect(compilation.getAsset("assets/language.css")).toBeDefined(); + expect(compilation.getAsset("language.css.map")).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it(`should generate and emit source maps when value has "false" value, but the "sassOptions.sourceMap" has the "string" value (${implementationName}) (${syntax})`, async () => { + const testId = getTestId("language", syntax); + const options = { + implementation: getImplementationByName(implementationName), + sourceMap: false, + sassOptions: { + sourceMap: "assets/[name].css.map", + outFile: path.join(__dirname, "styles.css"), + sourceMapContents: true, + omitSourceMapUrl: false, + sourceMapEmbed: false, + }, + }; + const compiler = getCompiler(testId, { + devtool: false, + rules: [ + { + test: /\.s[ac]ss$/i, + type: "asset/resource", + generator: { + filename: "assets/[name].css", + }, + use: [ + { + loader: path.join(__dirname, "../src/cjs.js"), + options, + }, + ], + }, + ], + }); + const stats = await compile(compiler); + const { compilation } = stats; + + expect(compilation.getAsset("assets/language.css")).toBeDefined(); + // expect(compilation.getAsset("assets/language.css.map")).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); }); }); });