Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: generate source maps using sass with asset/resource #968

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 70 additions & 8 deletions README.md
Expand Up @@ -119,13 +119,13 @@ Thankfully there are a two solutions to this problem:

## Options

| Name | Type | Default | Description |
| :---------------------------------------: | :------------------: | :-------------------------------------: | :---------------------------------------------------------------- |
| **[`implementation`](#implementation)** | `{Object\|String}` | `sass` | Setup Sass implementation to use. |
| **[`sassOptions`](#sassoptions)** | `{Object\|Function}` | defaults values for Sass implementation | Options for Sass. |
| **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. |
| **[`additionalData`](#additionaldata)** | `{String\|Function}` | `undefined` | Prepends/Appends `Sass`/`SCSS` code before the actual entry file. |
| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. |
| Name | Type | Default | Description |
| :---------------------------------------: | :---------------------: | :-------------------------------------: | :---------------------------------------------------------------- |
| **[`implementation`](#implementation)** | `{Object\|String}` | `sass` | Setup Sass implementation to use. |
| **[`sassOptions`](#sassoptions)** | `{Object\|Function}` | defaults values for Sass implementation | Options for Sass. |
| **[`sourceMap`](#sourcemap)** | `{Boolean\|"external"}` | `compiler.devtool` | Enables/Disables generation of source maps. |
| **[`additionalData`](#additionaldata)** | `{String\|Function}` | `undefined` | Prepends/Appends `Sass`/`SCSS` code before the actual entry file. |
| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. |

### `implementation`

Expand Down Expand Up @@ -394,9 +394,11 @@ module.exports = {

### `sourceMap`

Type: `Boolean`
Type: `Boolean | "external"`
Default: depends on the `compiler.devtool` value

#### `Boolean`

Enables/Disables generation of source maps.

By default generation of source maps depends on the [`devtool`](https://webpack.js.org/configuration/devtool/) option.
Expand Down Expand Up @@ -464,6 +466,66 @@ module.exports = {
};
```

#### `external`

Allows to generate a source map to external separate file without webpack source map processing.
Source map wil be emitted as external file.
It will be convenient, when you want to use [`webpack asset modules`](https://webpack.js.org/guides/asset-modules/) with the "asset/resource" value.

This requires:

- specify the [`asset modules`](https://webpack.js.org/guides/asset-modules/) "asset/resource" type for scss/sass files
- specify the [`outFile`](https://sass-lang.com/documentation/js-api#outfile) option in [`sassOptions`](#sassoptions) to the location that Sass expects the generated CSS to be saved.
"outFile" must be relative to the output directory. Default: "[name].css"
- set "sourceMap" option in "external" value. It will automatically:
- will turn on [`sourceMap`](https://sass-lang.com/documentation/js-api#sourcemap) option in [`sassOptions`](#sassoptions)
- will turn off [`omitSourceMapUrl`](https://sass-lang.com/documentatioff/js-api#omitsourcemapurl) option in [`sassOptions`](#sassoptions)
- will turn off [`sourceMapEmbed`](https://sass-lang.com/documentation/js-api#sourcemapembed) option in [`sassOptions`](#sassoptions)

**webpack.config.js**

```javascript
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
type: "asset/resource",
generator: {
filename: "assets/[name].css",
},
use: [
{
loader: "sass-loader",
options: {
sourceMap: "external",
// Optional options
sassOptions: {
// [`outFile`](https://sass-lang.com/documentation/js-api#outfile). A source map url is generated relative to this file. The file name does not matter, only the directory structure is important
outFile: "assets/[name].css",
// [`sourceMapContents`](https://sass-lang.com/documentation/js-api#sourcemapcontents). Default: true
// sourceMapContents: false,
// [`omitSourceMapUrl`](https://sass-lang.com/documentation/js-api#omitsourcemapurl). Default: false
// omitSourceMapUrl: true,
// [`sourceMapEmbed`](https://sass-lang.com/documentation/js-api#sourcemapembed). Default: false
// sourceMapEmbed: true,
// [`sourceMapRoot`](https://sass-lang.com/documentation/js-api#sourcemaproot). Default: ""
// sourceMapRoot: "",
},
},
},
],
},
],
},
};
```

Result:

The `assets/filename.css` file with `assets/filename.map.css` source map file will be generated without webpack processing.
In this case, urls and imports will not be processed.

### `additionalData`

Type: `String|Function`
Expand Down
29 changes: 21 additions & 8 deletions src/index.js
Expand Up @@ -28,7 +28,10 @@ async function loader(content) {
}

const useSourceMap =
typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap;
typeof options.sourceMap === "undefined"
? this.sourceMap
: options.sourceMap;

const sassOptions = await getSassOptions(
this,
options,
Expand Down Expand Up @@ -64,13 +67,6 @@ async function loader(content) {
return;
}

let map = result.map ? JSON.parse(result.map) : null;

// Modify source paths only for webpack, otherwise we do nothing
if (map && useSourceMap) {
map = normalizeSourceMap(map, this.rootContext);
}

result.stats.includedFiles.forEach((includedFile) => {
const normalizedIncludedFile = path.normalize(includedFile);

Expand All @@ -80,6 +76,23 @@ async function loader(content) {
}
});

let map = result.map ? JSON.parse(result.map) : null;

if (map) {
if (useSourceMap === "external" && !sassOptions.sourceMapEmbed) {
const outFile =
process.platform !== "win32"
? sassOptions.resolvedOutFile
: sassOptions.resolvedOutFile.split(path.sep).join("/");

this.emitFile(`${outFile}.map`, JSON.stringify(map));

map = null;
} else if (useSourceMap) {
map = normalizeSourceMap(map, this.rootContext);
}
}

callback(null, result.css.toString(), map);
});
}
Expand Down
9 changes: 8 additions & 1 deletion src/options.json
Expand Up @@ -42,7 +42,14 @@
"sourceMap": {
"description": "Enables/Disables generation of source maps.",
"link": "https://github.com/webpack-contrib/sass-loader#sourcemap",
"type": "boolean"
"anyOf": [
{
"enum": ["external"]
},
{
"type": "boolean"
}
]
},
"webpackImporter": {
"description": "Enables/Disables default `webpack` importer.",
Expand Down
34 changes: 32 additions & 2 deletions src/utils.js
Expand Up @@ -115,7 +115,7 @@ function isSupportedFibers() {
* @param {object} loaderOptions
* @param {string} content
* @param {object} implementation
* @param {boolean} useSourceMap
* @param {boolean|string} useSourceMap
* @returns {Object}
*/
async function getSassOptions(
Expand Down Expand Up @@ -172,7 +172,7 @@ async function getSassOptions(
options.outputStyle = "compressed";
}

if (useSourceMap) {
if (useSourceMap === true) {
// Deliberately overriding the sourceMap option here.
// node-sass won't produce source maps if the data option is used and options.sourceMap is not a string.
// In case it is a string, options.sourceMap should be a path where the source map is written.
Expand All @@ -184,6 +184,36 @@ async function getSassOptions(
options.sourceMapContents = true;
options.omitSourceMapUrl = true;
options.sourceMapEmbed = false;
} else if (useSourceMap === "external") {
options.sourceMap = true;
options.outFile =
typeof options.outFile !== "undefined" ? options.outFile : "[name].css";
options.omitSourceMapUrl =
typeof options.omitSourceMapUrl !== "undefined"
? options.omitSourceMapUrl
: false;
options.sourceMapEmbed =
typeof options.sourceMapEmbed !== "undefined"
? options.sourceMapEmbed
: false;
options.sourceMapContents =
typeof options.sourceMapContents !== "undefined"
? options.sourceMapContents
: true;

const outFileParsed = path.parse(options.outFile);

if (outFileParsed.name === "[name]") {
outFileParsed.name = path.parse(options.file).name;
outFileParsed.base = `${outFileParsed.name}${outFileParsed.ext}`;
}

options.resolvedOutFile = path.format(outFileParsed);

// eslint-disable-next-line no-underscore-dangle
const { outputPath } = loaderContext._compiler;

options.outFile = path.resolve(outputPath, options.resolvedOutFile);
}

const { resourcePath } = loaderContext;
Expand Down