Skip to content

Commit

Permalink
feat: generate source maps using sass with asset/resource
Browse files Browse the repository at this point in the history
  • Loading branch information
cap-Bernardito committed Jul 21, 2021
1 parent 1b453fb commit dad7087
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 21 deletions.
77 changes: 69 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,65 @@ 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.
- 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: false,
// [`sourceMapEmbed`](https://sass-lang.com/documentation/js-api#sourcemapembed). Default: false
// sourceMapEmbed: "",
// [`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
26 changes: 18 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,20 @@ async function loader(content) {
}
});

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

if (map) {
if (useSourceMap === "external") {
const outFile = `${sassOptions.resolvedOutFile}.map`;

this.emitFile(outFile, 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
32 changes: 30 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,34 @@ async function getSassOptions(
options.sourceMapContents = true;
options.omitSourceMapUrl = true;
options.sourceMapEmbed = false;
} else if (useSourceMap === "external") {
options.sourceMap = true;
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
32 changes: 32 additions & 0 deletions 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\\";
Expand Down
8 changes: 6 additions & 2 deletions test/__snapshots__/validate-options.test.js.snap
Expand Up @@ -61,9 +61,13 @@ exports[`validate options should throw an error on the "sassOptions" option with

exports[`validate options should throw an error on the "sourceMap" option with "string" value 1`] = `
"Invalid options object. Sass Loader has been initialized using an options object that does not match the API schema.
- options.sourceMap should be a boolean.
- options.sourceMap should be one of these:
\\"external\\" | boolean
-> Enables/Disables generation of source maps.
-> Read more at https://github.com/webpack-contrib/sass-loader#sourcemap"
-> Read more at https://github.com/webpack-contrib/sass-loader#sourcemap
Details:
* options.sourceMap should be \\"external\\".
* options.sourceMap should be a boolean."
`;

exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
Expand Down
75 changes: 75 additions & 0 deletions test/sourceMap-options.test.js
Expand Up @@ -235,6 +235,81 @@ 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: "external",
sassOptions: {
outFile: "style.css",
},
};
const compiler = getCompiler(testId, {
rules: [
{
test: /\.s[ac]ss$/i,
type: "asset/resource",
generator: {
filename: "[name].css",
},
use: [
{
loader: path.join(__dirname, "../src/cjs.js"),
options,
},
],
},
],
});
const stats = await compile(compiler);
const { compilation } = stats;

expect(compilation.getAsset("language.css")).toBeDefined();
expect(compilation.getAsset("style.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: "external",
sassOptions: {
outFile: "assets/[name].css",
omitSourceMapUrl: false,
sourceMapEmbed: false,
sourceMapContents: false,
},
};
const compiler = getCompiler(testId, {
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${path.sep}language.css.map`)
).toBeDefined();
expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");
});
});
});
});

0 comments on commit dad7087

Please sign in to comment.