forked from bholloway/resolve-url-loader
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
243 lines (211 loc) · 8.58 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
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
var path = require('path'),
fs = require('fs'),
loaderUtils = require('loader-utils'),
rework = require('rework'),
visit = require('rework-visit'),
convert = require('convert-source-map'),
camelcase = require('camelcase'),
defaults = require('lodash.defaults'),
SourceMapConsumer = require('source-map').SourceMapConsumer;
var findFile = require('./lib/find-file'),
absoluteToRelative = require('./lib/sources-absolute-to-relative'),
relativeToAbsolute = require('./lib/sources-relative-to-absolute');
var PACKAGE_NAME = require('./package.json').name;
/**
* A webpack loader that resolves absolute url() paths relative to their original source file.
* Requires source-maps to do any meaningful work.
* @param {string} content Css content
* @param {object} sourceMap The source-map
* @returns {string|String}
*/
function resolveUrlLoader(content, sourceMap) {
/* jshint validthis:true */
// details of the file being processed
// we would normally use compilation.getPath(options.output.path) to get the most correct outputPath,
// however we need to match to the sass-loader and it does not do so
var loader = this,
filePath = loader.context,
outputPath = path.resolve(loader.options.output.path),
contextPath = path.resolve(loader.options.context);
// prefer loader query, else options object, else default values
var options = defaults(loaderUtils.parseQuery(loader.query), loader.options[camelcase(PACKAGE_NAME)], {
absolute : false,
sourceMap: false,
fail : false,
silent : false,
keepQuery: false,
root : null
});
// validate root directory
var resolvedRoot = (typeof options.root === 'string') && path.resolve(options.root) || undefined,
isValidRoot = resolvedRoot && fs.existsSync(resolvedRoot);
if (options.root && !isValidRoot) {
return handleException('"root" option does not resolve to a valid path');
}
// loader result is cacheable
loader.cacheable();
// incoming source-map
var sourceMapConsumer, contentWithMap, sourceRoot;
if (sourceMap) {
// expect sass-loader@>=4.0.0
// sourcemap sources relative to context path
try {
relativeToAbsolute(sourceMap.sources, contextPath, resolvedRoot);
}
catch (unused) {
// fallback to sass-loader@<4.0.0
// sourcemap sources relative to output path
try {
relativeToAbsolute(sourceMap.sources, outputPath, resolvedRoot);
}
catch (exception) {
return handleException('source-map error', exception.message);
}
}
// There are now absolute paths in the source map so we don't need it anymore
// However, later when we go back to relative paths, we need to add it again
sourceRoot = sourceMap.sourceRoot;
sourceMap.sourceRoot = undefined;
// prepare the adjusted sass source-map for later look-ups
sourceMapConsumer = new SourceMapConsumer(sourceMap);
// embed source-map in css for rework-css to use
contentWithMap = content + convert.fromObject(sourceMap).toComment({multiline: true});
}
// absent source map
else {
contentWithMap = content;
}
// process
// rework-css will throw on css syntax errors
var useMap = loader.sourceMap || options.sourceMap,
reworked;
try {
reworked = rework(contentWithMap, {source: loader.resourcePath})
.use(reworkPlugin)
.toString({
sourcemap : useMap,
sourcemapAsObject: useMap
});
}
// fail gracefully
catch (exception) {
return handleException('CSS error', exception);
}
// complete with source-map
if (useMap) {
// source-map sources seem to be relative to the file being processed
absoluteToRelative(reworked.map.sources, path.resolve(filePath, sourceRoot || '.'));
// Set source root again
reworked.map.sourceRoot = sourceRoot;
// need to use callback when there are multiple arguments
loader.callback(null, reworked.code, reworked.map);
}
// complete without source-map
else {
return reworked;
}
/**
* Push an error for the given exception and return the original content.
* @param {string} label Summary of the error
* @param {string|Error} [exception] Optional extended error details
* @returns {string} The original CSS content
*/
function handleException(label, exception) {
var rest = (typeof exception === 'string') ? [exception] :
(exception instanceof Error) ? [exception.message, exception.stack.split('\n')[1].trim()] :
[];
var message = ' resolve-url-loader cannot operate: ' + [label].concat(rest).filter(Boolean).join('\n ');
if (options.fail) {
loader.emitError(message);
}
else if (!options.silent) {
loader.emitWarning(message);
}
return content;
}
/**
* Plugin for css rework that follows SASS transpilation
* @param {object} stylesheet AST for the CSS output from SASS
*/
function reworkPlugin(stylesheet) {
var URL_STATEMENT_REGEX = /(url\s*\()\s*(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))\s*(\))/g;
// visit each node (selector) in the stylesheet recursively using the official utility method
// each node may have multiple declarations
visit(stylesheet, function visitor(declarations) {
if (declarations) {
declarations
.forEach(eachDeclaration);
}
});
/**
* Process a declaration from the syntax tree.
* @param declaration
*/
function eachDeclaration(declaration) {
var isValid = declaration.value && (declaration.value.indexOf('url') >= 0),
directory;
if (isValid) {
// reverse the original source-map to find the original sass file
var startPosApparent = declaration.position.start,
startPosOriginal = sourceMapConsumer && sourceMapConsumer.originalPositionFor(startPosApparent);
// we require a valid directory for the specified file
directory = startPosOriginal && startPosOriginal.source && path.dirname(startPosOriginal.source);
if (directory) {
// allow multiple url() values in the declaration
// split by url statements and process the content
// additional capture groups are needed to match quotations correctly
// escaped quotations are not considered
declaration.value = declaration.value
.split(URL_STATEMENT_REGEX)
.map(eachSplitOrGroup)
.join('');
}
// source-map present but invalid entry
else if (sourceMapConsumer) {
throw new Error('source-map information is not available at url() declaration');
}
}
/**
* Encode the content portion of <code>url()</code> statements.
* There are 4 capture groups in the split making every 5th unmatched.
* @param {string} token A single split item
* @param i The index of the item in the split
* @returns {string} Every 3 or 5 items is an encoded url everything else is as is
*/
function eachSplitOrGroup(token, i) {
var BACKSLASH_REGEX = /\\/g;
// we can get groups as undefined under certain match circumstances
var initialised = token || '';
// the content of the url() statement is either in group 3 or group 5
var mod = i % 7;
if ((mod === 3) || (mod === 5)) {
// split into uri and query/hash and then find the absolute path to the uri
var split = initialised.split(/([?#])/g),
uri = split[0],
absolute = uri && findFile.absolute(directory, uri, resolvedRoot),
query = options.keepQuery ? split.slice(1).join('') : '';
// use the absolute path (or default to initialised)
if (options.absolute) {
return absolute && absolute.replace(BACKSLASH_REGEX, '/').concat(query) || initialised;
}
// module relative path (or default to initialised)
else {
var relative = absolute && path.relative(filePath, absolute),
rootRelative = relative && loaderUtils.urlToRequest(relative, '~');
return (rootRelative) ? rootRelative.replace(BACKSLASH_REGEX, '/').concat(query) : initialised;
}
}
// everything else, including parentheses and quotation (where present) and media statements
else {
return initialised;
}
}
}
}
}
module.exports = resolveUrlLoader;