forked from webpack-contrib/sass-loader
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
363 lines (316 loc) · 12.5 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
'use strict';
var utils = require('loader-utils');
var sass = require('node-sass');
var path = require('path');
var os = require('os');
var fs = require('fs');
var async = require('async');
// A typical sass error looks like this
var SassError = {
message: 'invalid property name',
column: 14,
line: 1,
file: 'stdin',
status: 1
};
// libsass uses this precedence when importing files without extension
var extPrecedence = ['.scss', '.sass', '.css'];
var matchCss = /\.css$/;
// This queue makes sure node-sass leaves one thread available for executing
// fs tasks when running the custom importer code.
// This can be removed as soon as node-sass implements a fix for this.
var threadPoolSize = process.env.UV_THREADPOOL_SIZE || 4;
var asyncSassJobQueue = async.queue(sass.render, threadPoolSize - 1);
/**
* The sass-loader makes node-sass available to webpack modules.
*
* @param {string} content
* @returns {string}
*/
module.exports = function (content) {
var callback = this.async();
var isSync = typeof callback !== 'function';
var self = this;
var resourcePath = this.resourcePath;
var result;
var opt;
/**
* Enhances the sass error with additional information about what actually went wrong.
*
* @param {SassError} err
*/
function formatSassError(err) {
// Instruct webpack to hide the JS stack from the console
// Usually you're only interested in the SASS stack in this case.
err.hideStack = true;
// The file property is missing in rare cases.
// No improvement in the error is possible.
if (!err.file) {
return;
}
var msg = err.message;
if (err.file === 'stdin') {
err.file = resourcePath;
}
// node-sass returns UNIX-style paths
err.file = path.normalize(err.file);
// The 'Current dir' hint of node-sass does not help us, we're providing
// additional information by reading the err.file property
msg = msg.replace(/\s*Current dir:\s*/, '');
err.message = getFileExcerptIfPossible(err) +
msg.charAt(0).toUpperCase() + msg.slice(1) + os.EOL +
' in ' + err.file + ' (line ' + err.line + ', column ' + err.column + ')';
}
/**
* Returns an importer that uses webpack's resolving algorithm.
*
* It's important that the returned function has the correct number of arguments
* (based on whether the call is sync or async) because otherwise node-sass doesn't exit.
*
* @returns {function}
*/
function getWebpackImporter() {
if (isSync) {
return function syncWebpackImporter(url, fileContext) {
var dirContext;
var request;
// node-sass returns UNIX-style paths
fileContext = path.normalize(fileContext);
request = utils.urlToRequest(url, opt.root);
dirContext = fileToDirContext(fileContext);
return resolveSync(dirContext, url, getImportsToResolve(request));
};
}
return function asyncWebpackImporter(url, fileContext, done) {
var dirContext;
var request;
// node-sass returns UNIX-style paths
fileContext = path.normalize(fileContext);
request = utils.urlToRequest(url, opt.root);
dirContext = fileToDirContext(fileContext);
resolve(dirContext, url, getImportsToResolve(request), done);
};
}
/**
* Tries to resolve the first url of importsToResolve. If that resolve fails, the next url is tried.
* If all imports fail, the import is passed to libsass which also take includePaths into account.
*
* @param {string} dirContext
* @param {string} originalImport
* @param {Array} importsToResolve
* @returns {object}
*/
function resolveSync(dirContext, originalImport, importsToResolve) {
var importToResolve = importsToResolve.shift();
var resolvedFilename;
if (!importToResolve) {
// No import possibilities left. Let's pass that one back to libsass...
return {
file: originalImport
};
}
try {
resolvedFilename = self.resolveSync(dirContext, importToResolve);
// Add the resolvedFilename as dependency. Although we're also using stats.includedFiles, this might come
// in handy when an error occurs. In this case, we don't get stats.includedFiles from node-sass.
addNormalizedDependency(resolvedFilename);
// By removing the CSS file extension, we trigger node-sass to include the CSS file instead of just linking it.
resolvedFilename = resolvedFilename.replace(matchCss, '');
return {
file: resolvedFilename
};
} catch (err) {
return resolveSync(dirContext, originalImport, importsToResolve);
}
}
/**
* Tries to resolve the first url of importsToResolve. If that resolve fails, the next url is tried.
* If all imports fail, the import is passed to libsass which also take includePaths into account.
*
* @param {string} dirContext
* @param {string} originalImport
* @param {Array} importsToResolve
* @param {function} done
*/
function resolve(dirContext, originalImport, importsToResolve, done) {
var importToResolve = importsToResolve.shift();
if (!importToResolve) {
// No import possibilities left. Let's pass that one back to libsass...
done({
file: originalImport
});
return;
}
self.resolve(dirContext, importToResolve, function onWebpackResolve(err, resolvedFilename) {
if (err) {
resolve(dirContext, originalImport, importsToResolve, done);
return;
}
// Add the resolvedFilename as dependency. Although we're also using stats.includedFiles, this might come
// in handy when an error occurs. In this case, we don't get stats.includedFiles from node-sass.
addNormalizedDependency(resolvedFilename);
// By removing the CSS file extension, we trigger node-sass to include the CSS file instead of just linking it.
resolvedFilename = resolvedFilename.replace(matchCss, '');
// Use self.loadModule() before calling done() to make imported files available to
// other webpack tools like postLoaders etc.?
done({
file: resolvedFilename.replace(matchCss, '')
});
});
}
function fileToDirContext(fileContext) {
// The first file is 'stdin' when we're using the data option
if (fileContext === 'stdin') {
fileContext = resourcePath;
}
return path.dirname(fileContext);
}
// When files have been imported via the includePaths-option, these files need to be
// introduced to webpack in order to make them watchable.
function addIncludedFilesToWebpack(includedFiles) {
includedFiles.forEach(addNormalizedDependency);
}
function addNormalizedDependency(file) {
// node-sass returns UNIX-style paths
self.dependency(path.normalize(file));
}
this.cacheable();
opt = utils.parseQuery(this.query);
opt.data = content;
// Skip empty files, otherwise it will stop webpack, see issue #21
if (opt.data.trim() === '') {
return isSync ? content : callback(null, content);
}
// opt.outputStyle
if (!opt.outputStyle && this.minimize) {
opt.outputStyle = 'compressed';
}
// opt.sourceMap
// Not using the `this.sourceMap` flag because css source maps are different
// @see https://github.com/webpack/css-loader/pull/40
if (opt.sourceMap) {
// deliberately overriding the sourceMap option
// this value is (currently) ignored by libsass when using the data input instead of file input
// however, it is still necessary for correct relative paths in result.map.sources
opt.sourceMap = this.options.output.path + '/sass.map';
opt.omitSourceMapUrl = true;
// If sourceMapContents option is not set, set it to true otherwise maps will be empty/null
// when exported by webpack-extract-text-plugin.
if ('sourceMapContents' in opt === false) {
opt.sourceMapContents = true;
}
}
// indentedSyntax is a boolean flag
opt.indentedSyntax = Boolean(opt.indentedSyntax);
opt.importer = getWebpackImporter();
// functions can't be set in query, load from sassLoader section in webpack options
if (this.options.sassLoader) {
opt.functions = this.options.sassLoader.functions;
}
// start the actual rendering
if (isSync) {
try {
result = sass.renderSync(opt);
addIncludedFilesToWebpack(result.stats.includedFiles);
return result.css.toString();
} catch (err) {
formatSassError(err);
err.file && this.dependency(err.file);
throw err;
}
}
asyncSassJobQueue.push(opt, function onRender(err, result) {
if (err) {
formatSassError(err);
err.file && self.dependency(err.file);
callback(err);
return;
}
if (result.map && result.map !== '{}') {
result.map = JSON.parse(result.map);
result.map.file = resourcePath;
// The first source is 'stdin' according to libsass because we've used the data input
// Now let's override that value with the correct relative path
result.map.sources[0] = path.relative(self.options.output.path, resourcePath);
} else {
result.map = null;
}
addIncludedFilesToWebpack(result.stats.includedFiles);
callback(null, result.css.toString(), result.map);
});
};
/**
* Tries to get an excerpt of the file where the error happened.
* Uses err.line and err.column.
*
* Returns an empty string if the excerpt could not be retrieved.
*
* @param {SassError} err
* @returns {string}
*/
function getFileExcerptIfPossible(err) {
var content;
try {
content = fs.readFileSync(err.file, 'utf8');
return os.EOL +
content.split(os.EOL)[err.line - 1] + os.EOL +
new Array(err.column - 1).join(' ') + '^' + os.EOL +
' ';
} catch (err) {
// If anything goes wrong here, we don't want any errors to be reported to the user
return '';
}
}
/**
* When libsass tries to resolve an import, it uses this "funny" algorithm:
*
* - Imports with no file extension:
* - Prefer modules starting with '_'
* - File extension precedence: .scss, .sass, .css
* - Imports with file extension:
* - If the file is a CSS-file, do not include it all, but just link it via @import url()
* - The exact file name must match (no auto-resolving of '_'-modules)
*
* Since the sass-loader uses webpack to resolve the modules, we need to simulate that algorithm. This function
* returns an array of import paths to try.
*
* @param {string} originalImport
* @returns {Array}
*/
function getImportsToResolve(originalImport) {
var ext = path.extname(originalImport);
var basename = path.basename(originalImport);
var dirname = path.dirname(originalImport);
var startsWithUnderscore = basename.charAt(0) === '_';
var paths = [];
function add(file) {
// No path.sep required here, because imports inside SASS are usually with /
paths.push(dirname + '/' + file);
}
if (originalImport.charAt(0) !== '.') {
// If true: originalImport is a module import like 'bootstrap-sass...'
if (dirname === '.') {
// If true: originalImport is just a module import without a path like 'bootstrap-sass'
// In this case we don't do that auto-resolving dance at all.
return [originalImport];
}
}
if (ext) {
if (ext === '.scss' || ext === '.sass') {
add(basename);
}/* else {
Leave unknown extensions (like .css) untouched
}*/
} else {
if (!startsWithUnderscore) {
// Prefer modules starting with '_' first
extPrecedence.forEach(function (ext) {
add('_' + basename + ext);
});
}
extPrecedence.forEach(function (ext) {
add(basename + ext);
});
}
return paths;
}