UNPKG

6.96 kBJavaScriptView Raw
1/*
2 * MIT License http://opensource.org/licenses/MIT
3 * Author: Ben Holloway @bholloway
4 */
5'use strict';
6
7var path = require('path'),
8 fs = require('fs'),
9 loaderUtils = require('loader-utils'),
10 camelcase = require('camelcase'),
11 SourceMapConsumer = require('source-map').SourceMapConsumer;
12
13var adjustSourceMap = require('adjust-sourcemap-loader/lib/process');
14
15var valueProcessor = require('./lib/value-processor');
16var joinFn = require('./lib/join-function');
17var logToTestHarness = require('./lib/log-to-test-harness');
18
19var PACKAGE_NAME = require('./package.json').name;
20
21/**
22 * A webpack loader that resolves absolute url() paths relative to their original source file.
23 * Requires source-maps to do any meaningful work.
24 * @param {string} content Css content
25 * @param {object} sourceMap The source-map
26 * @returns {string|String}
27 */
28function resolveUrlLoader(content, sourceMap) {
29 /* jshint validthis:true */
30
31 // details of the file being processed
32 var loader = this;
33
34 // a relative loader.context is a problem
35 if (/^\./.test(loader.context)) {
36 return handleAsError(
37 'webpack misconfiguration',
38 'loader.context is relative, expected absolute'
39 );
40 }
41
42 // webpack 1: prefer loader query, else options object
43 // webpack 2: prefer loader options
44 // webpack 3: deprecate loader.options object
45 // webpack 4: loader.options no longer defined
46 var options = Object.assign(
47 {
48 sourceMap: loader.sourceMap,
49 engine : 'postcss',
50 silent : false,
51 absolute : false,
52 keepQuery: false,
53 removeCR : false,
54 root : false,
55 debug : false,
56 join : joinFn.defaultJoin
57 },
58 !!loader.options && loader.options[camelcase(PACKAGE_NAME)],
59 loaderUtils.getOptions(loader)
60 );
61
62 // maybe log options for the test harness
63 logToTestHarness(options);
64
65 // defunct options
66 if ('attempts' in options) {
67 handleAsWarning(
68 'loader misconfiguration',
69 '"attempts" option is defunct (consider "join" option if search is needed)'
70 );
71 }
72 if ('includeRoot' in options) {
73 handleAsWarning(
74 'loader misconfiguration',
75 '"includeRoot" option is defunct (consider "join" option if search is needed)'
76 );
77 }
78 if ('fail' in options) {
79 handleAsWarning(
80 'loader misconfiguration',
81 '"fail" option is defunct'
82 );
83 }
84
85 // validate join option
86 if (typeof options.join !== 'function') {
87 return handleAsError(
88 'loader misconfiguration',
89 '"join" option must be a Function'
90 );
91 } else if (options.join.length !== 2) {
92 return handleAsError(
93 'loader misconfiguration',
94 '"join" Function must take exactly 2 arguments (filename and options hash)'
95 );
96 }
97
98 // validate root option
99 if (typeof options.root === 'string') {
100 var isValid = (options.root === '') ||
101 (path.isAbsolute(options.root) && fs.existsSync(options.root) && fs.statSync(options.root).isDirectory());
102
103 if (!isValid) {
104 return handleAsError(
105 'loader misconfiguration',
106 '"root" option must be an empty string or an absolute path to an existing directory'
107 );
108 }
109 } else if (options.root !== false) {
110 handleAsWarning(
111 'loader misconfiguration',
112 '"root" option must be string where used or false where unused'
113 );
114 }
115
116 // loader result is cacheable
117 loader.cacheable();
118
119 // incoming source-map
120 var sourceMapConsumer, absSourceMap;
121 if (sourceMap) {
122
123 // support non-standard string encoded source-map (per less-loader)
124 if (typeof sourceMap === 'string') {
125 try {
126 sourceMap = JSON.parse(sourceMap);
127 }
128 catch (exception) {
129 return handleAsError(
130 'source-map error',
131 'cannot parse source-map string (from less-loader?)'
132 );
133 }
134 }
135
136 // leverage adjust-sourcemap-loader's codecs to avoid having to make any assumptions about the sourcemap
137 // historically this is a regular source of breakage
138 try {
139 absSourceMap = adjustSourceMap(loader, {format: 'absolute'}, sourceMap);
140 }
141 catch (exception) {
142 return handleAsError(
143 'source-map error',
144 exception.message
145 );
146 }
147
148 // prepare the adjusted sass source-map for later look-ups
149 sourceMapConsumer = new SourceMapConsumer(absSourceMap);
150 }
151
152 // choose a CSS engine
153 var enginePath = /^\w+$/.test(options.engine) && path.join(__dirname, 'lib', 'engine', options.engine + '.js');
154 var isValidEngine = fs.existsSync(enginePath);
155 if (!isValidEngine) {
156 return handleAsError(
157 'loader misconfiguration',
158 '"engine" option is not valid'
159 );
160 }
161
162 // process async
163 var callback = loader.async();
164 Promise
165 .resolve(require(enginePath)(loader.resourcePath, content, {
166 outputSourceMap : !!options.sourceMap,
167 transformDeclaration: valueProcessor(loader.resourcePath, options),
168 absSourceMap : absSourceMap,
169 sourceMapConsumer : sourceMapConsumer,
170 removeCR : options.removeCR
171 }))
172 .catch(onFailure)
173 .then(onSuccess);
174
175 function onFailure(error) {
176 callback(encodeError('CSS error', error));
177 }
178
179 function onSuccess(reworked) {
180 if (reworked) {
181 // complete with source-map
182 // source-map sources are relative to the file being processed
183 if (options.sourceMap) {
184 var finalMap = adjustSourceMap(loader, {format: 'sourceRelative'}, reworked.map);
185 callback(null, reworked.content, finalMap);
186 }
187 // complete without source-map
188 else {
189 callback(null, reworked.content);
190 }
191 }
192 }
193
194 /**
195 * Push a warning for the given exception and return the original content.
196 * @param {string} label Summary of the error
197 * @param {string|Error} [exception] Optional extended error details
198 * @returns {string} The original CSS content
199 */
200 function handleAsWarning(label, exception) {
201 if (!options.silent) {
202 loader.emitWarning(encodeError(label, exception));
203 }
204 return content;
205 }
206
207 /**
208 * Push a warning for the given exception and return the original content.
209 * @param {string} label Summary of the error
210 * @param {string|Error} [exception] Optional extended error details
211 * @returns {string} The original CSS content
212 */
213 function handleAsError(label, exception) {
214 loader.emitError(encodeError(label, exception));
215 return content;
216 }
217
218 function encodeError(label, exception) {
219 return new Error(
220 [
221 PACKAGE_NAME,
222 ': ',
223 [label]
224 .concat(
225 (typeof exception === 'string') && exception ||
226 (exception instanceof Error) && [exception.message, exception.stack.split('\n')[1].trim()] ||
227 []
228 )
229 .filter(Boolean)
230 .join('\n ')
231 ].join('')
232 );
233 }
234}
235
236module.exports = Object.assign(resolveUrlLoader, joinFn);