Skip to content

Commit 895044f

Browse files
committed
feat(source-maps): refactor source maps handling
* Remove sourceMappingURL from generated CSS since the less-loader does not know the final location of the source map * Pass source map as object to the next loader BREAKING CHANGE: Since the map is now passed as an object to the next loader, this could potentially break if another loader than the css-loader is used. The css-loader accepts both.
1 parent 0fd87db commit 895044f

File tree

5 files changed

+57
-46
lines changed

5 files changed

+57
-46
lines changed

src/index.js

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
const less = require('less');
22
const loaderUtils = require('loader-utils');
33
const cloneDeep = require('clone-deep');
4+
const pify = require('pify');
5+
const removeSourceMappingUrl = require('./removeSourceMappingUrl');
46

57
const trailingSlash = /[\\/]$/;
8+
const render = pify(less.render.bind(less));
69

710
function lessLoader(source) {
811
const loaderContext = this;
9-
const options = Object.assign(
10-
{
11-
filename: this.resource,
12-
paths: [],
13-
plugins: [],
14-
relativeUrls: true,
15-
compress: Boolean(this.minimize),
16-
},
17-
cloneDeep(loaderUtils.getOptions(loaderContext)),
18-
);
19-
const cb = loaderContext.async();
20-
const isSync = typeof cb !== 'function';
21-
let finalCb = cb || loaderContext.callback;
12+
const options = {
13+
plugins: [],
14+
relativeUrls: true,
15+
compress: Boolean(this.minimize),
16+
...cloneDeep(loaderUtils.getOptions(loaderContext)),
17+
};
18+
const done = loaderContext.async();
19+
const isSync = typeof done !== 'function';
2220
const webpackPlugin = {
2321
install(lessInstance, pluginManager) {
2422
const WebpackFileManager = getWebpackFileManager(loaderContext, options);
@@ -32,32 +30,26 @@ function lessLoader(source) {
3230
throw new Error('Synchronous compilation is not supported anymore. See https://github.com/webpack-contrib/less-loader/issues/84');
3331
}
3432

33+
// We need to set the filename because otherwise our WebpackFileManager will receive an undefined path for the entry
34+
options.filename = loaderContext.resource;
35+
3536
// It's safe to mutate the array now because it has already been cloned
3637
options.plugins.push(webpackPlugin);
3738

38-
if (options.sourceMap) {
39-
options.sourceMap = {
40-
outputSourceFiles: true,
41-
};
42-
}
43-
44-
less.render(source, options, (err, result) => {
45-
const cb = finalCb;
46-
47-
// Less is giving us double callbacks sometimes :(
48-
// Thus we need to mark the callback as "has been called"
49-
if (!finalCb) {
50-
return;
51-
}
52-
finalCb = null;
53-
54-
if (err) {
55-
cb(formatLessRenderError(err));
56-
return;
57-
}
58-
59-
cb(null, result.css, result.map);
60-
});
39+
render(source, options)
40+
.then(({ css, map }) => {
41+
return {
42+
// Removing the sourceMappingURL comment.
43+
// See removeSourceMappingUrl.js for the reasoning behind this.
44+
css: removeSourceMappingUrl(css),
45+
map: typeof map === 'string' ? JSON.parse(map) : map,
46+
};
47+
}, (lessError) => {
48+
throw formatLessRenderError(lessError);
49+
})
50+
.then(({ css, map }) => {
51+
done(null, css, map);
52+
}, done);
6153
}
6254

6355
function getWebpackFileManager(loaderContext, query) {

src/removeSourceMappingUrl.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const matchSourceMappingUrl = /\/\*# sourceMappingURL=[^*]+\*\//;
2+
3+
/**
4+
* Removes the sourceMappingURL comment. This is necessary because the less-loader
5+
* does not know where the final source map will be located. Thus, we remove every
6+
* reference to source maps. In a regular setup, the css-loader will embed the
7+
* source maps into the CommonJS module and the style-loader will translate it into
8+
* base64 blob urls.
9+
*
10+
* @param {string} content
11+
* @returns {string}
12+
*/
13+
function removeSourceMappingUrl(content) {
14+
return content.replace(matchSourceMappingUrl, '');
15+
}
16+
17+
module.exports = removeSourceMappingUrl;

test/helpers/createSpec.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { exec } = require('child_process');
44
const fs = require('fs');
55
const path = require('path');
6+
const removeSourceMappingUrl = require('../../src/removeSourceMappingUrl');
67

78
const projectPath = path.resolve(__dirname, '..', '..');
89
const lessFixtures = path.resolve(__dirname, '..', 'fixtures', 'less');
@@ -61,8 +62,13 @@ testIds
6162
throw (err || new Error(stdout || stderr));
6263
}
6364

64-
const cssContent = fs.readFileSync(cssFile, 'utf8')
65-
.replace(new RegExp(`(@import\\s+["'])${tildeReplacement}`, 'g'), '$1~');
65+
// We remove the source mapping url because the less-loader will do it also.
66+
// See removeSourceMappingUrl.js for the reasoning behind this.
67+
const cssContent = removeSourceMappingUrl(
68+
fs.readFileSync(cssFile, 'utf8')
69+
// Change back tilde replacements
70+
.replace(new RegExp(`(@import\\s+["'])${tildeReplacement}`, 'g'), '$1~'),
71+
);
6672

6773
fs.writeFileSync(lessFile, originalLessContent, 'utf8');
6874
fs.writeFileSync(cssFile, cssContent, 'utf8');

test/helpers/readFixture.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ function readSourceMap(testId) {
2222
.then(JSON.parse)
2323
// The map that is generated by our loader does not have a file property because the
2424
// output file is unknown when the Less API is used. That's why we need to remove that from our fixture.
25-
.then(({ file, sourcesContent, ...map }) => map) // eslint-disable-line no-unused-vars
26-
.then(JSON.stringify);
25+
.then(({ file, ...map }) => map); // eslint-disable-line no-unused-vars
2726
}
2827

2928
exports.readCssFixture = readCssFixture;

test/index.test.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,13 @@ test('should transform urls', async () => {
3636
});
3737

3838
test('should generate source maps', async () => {
39-
const [{ inspect }, expectedSourceMap] = await Promise.all([
39+
const [{ inspect }, expectedMap] = await Promise.all([
4040
compile('source-map', { sourceMap: true }),
4141
readSourceMap('source-map'),
4242
]);
43+
const [, actualMap] = inspect.arguments;
4344

44-
const map = JSON.parse(inspect.arguments[1]);
45-
46-
delete map.sourcesContent;
47-
48-
expect(JSON.stringify(map)).toEqual(expectedSourceMap);
45+
expect(actualMap).toEqual(expectedMap);
4946
});
5047

5148
test('should install plugins', async () => {

0 commit comments

Comments
 (0)