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 ec2ea6e
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 18 deletions.
98 changes: 90 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,39 @@ module.exports = {
};
```

#### `external`

Allows to generate a sourceMap using [`sass`](https://sass-lang.com/documentation/js-api#sourcemap), without the participation of a `sass-loader`.

See the example below in the [`examples`](#examples) section.

**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: "external",
},
},
],
},
],
},
};
```

### `additionalData`

Type: `String|Function`
Expand Down Expand Up @@ -681,6 +716,53 @@ module.exports = {
};
```

### Generate source maps for sass with asset/resource

Allows to generate a sourceMap using [`sass`](https://sass-lang.com/documentation/js-api#sourcemap), without the participation of a `sass-loader`.

This requires:

- specify the [`asset modules`](https://webpack.js.org/guides/asset-modules/) type for scss/sass files
- disable webpack source maps generation
- set "sourceMap" option in "sassGenerated" 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 = {
devtool: false,
module: {
rules: [
{
test: /\.s[ac]ss$/i,
type: "asset/resource",
generator: {
filename: "assets/[name].css",
},
use: [
{
loader: "sass-loader",
options: {
sourceMap: "external",
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"),
},
},
},
],
},
],
},
};
```

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
Expand Down
37 changes: 34 additions & 3 deletions src/index.js
Expand Up @@ -27,14 +27,21 @@ async function loader(content) {
return;
}

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

if (!sourcemapType) {
sourcemapType = options.sourceMap === "external" ? "external" : "off";
} else {
sourcemapType = "internal";
}

const sassOptions = await getSassOptions(
this,
options,
content,
implementation,
useSourceMap
sourcemapType
);
const shouldUseWebpackImporter =
typeof options.webpackImporter === "boolean"
Expand All @@ -51,6 +58,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
Expand All @@ -67,7 +92,7 @@ async function loader(content) {
let map = result.map ? JSON.parse(result.map) : null;

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

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

if (sourceMapShouldBeEmmited && map) {
this.emitFile(sassOptions.sourceMap, JSON.stringify(map));

map = null;
}

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
27 changes: 24 additions & 3 deletions src/utils.js
Expand Up @@ -115,15 +115,15 @@ function isSupportedFibers() {
* @param {object} loaderOptions
* @param {string} content
* @param {object} implementation
* @param {boolean} useSourceMap
* @param {string} sourcemapType
* @returns {Object}
*/
async function getSassOptions(
loaderContext,
loaderOptions,
content,
implementation,
useSourceMap
sourcemapType
) {
const options = klona(
loaderOptions.sassOptions
Expand Down Expand Up @@ -172,7 +172,7 @@ async function getSassOptions(
options.outputStyle = "compressed";
}

if (useSourceMap) {
if (sourcemapType === "internal") {
// 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 @@ -186,6 +186,27 @@ async function getSassOptions(
options.sourceMapEmbed = false;
}

if (sourcemapType === "external") {
options.sourceMap =
typeof options.sourceMap !== "undefined" ? options.sourceMap : true;
options.outFile =
typeof options.outFile !== "undefined"
? options.outFile
: path.join(loaderContext.rootContext, "style.css.map");
options.sourceMapContents =
typeof options.sourceMapContents !== "undefined"
? options.sourceMapContents
: true;
options.omitSourceMapUrl =
typeof options.omitSourceMapUrl !== "undefined"
? options.omitSourceMapUrl
: false;
options.sourceMapEmbed =
typeof options.sourceMapEmbed !== "undefined"
? options.sourceMapEmbed
: false;
}

const { resourcePath } = loaderContext;
const ext = path.extname(resourcePath);

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
69 changes: 69 additions & 0 deletions test/sourceMap-options.test.js
Expand Up @@ -235,6 +235,75 @@ 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",
};
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: "external",
sassOptions: {
sourceMap: "assets/[name].css.map",
},
};
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");
});
});
});
});

0 comments on commit ec2ea6e

Please sign in to comment.