Skip to content

Commit bc19ddd

Browse files
authoredJul 16, 2020
feat: improve url() resolving algorithm
1 parent d139ec1 commit bc19ddd

13 files changed

+456
-206
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ Thumbs.db
1717
*.sublime-project
1818
*.sublime-workspace
1919
/test/fixtures/import/import-absolute.css
20+
/test/fixtures/url/url-absolute.css

‎README.md

+34-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Type: `Boolean|Function`
125125
Default: `true`
126126

127127
Enables/Disables `url`/`image-set` functions handling.
128-
Control `url()` resolving. Absolute URLs and root-relative URLs are not resolving.
128+
Control `url()` resolving. Absolute URLs are not resolving.
129129

130130
Examples resolutions:
131131

@@ -1174,6 +1174,39 @@ module.exports = {
11741174
};
11751175
```
11761176

1177+
### Resolve unresolved URLs using an alias
1178+
1179+
**index.css**
1180+
1181+
```css
1182+
.class {
1183+
background: url(/assets/unresolved/img.png);
1184+
}
1185+
```
1186+
1187+
**webpack.config.js**
1188+
1189+
```js
1190+
module.exports = {
1191+
module: {
1192+
rules: [
1193+
{
1194+
test: /\.css$/i,
1195+
use: ['style-loader', 'css-loader'],
1196+
},
1197+
],
1198+
},
1199+
resolve: {
1200+
alias: {
1201+
'/assets/unresolved/img.png': path.resolve(
1202+
__dirname,
1203+
'assets/real-path-to-img/img.png'
1204+
),
1205+
},
1206+
},
1207+
};
1208+
```
1209+
11771210
## Contributing
11781211

11791212
Please take a moment to read our contributing guidelines if you haven't yet done so.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"dist"
4141
],
4242
"peerDependencies": {
43-
"webpack": "^4.0.0 || ^5.0.0"
43+
"webpack": "^4.27.0 || ^5.0.0"
4444
},
4545
"dependencies": {
4646
"camelcase": "^6.0.0",

‎src/index.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
MIT License http://www.opensource.org/licenses/mit-license.php
33
Author Tobias Koppers @sokra
44
*/
5-
import { getOptions, isUrlRequest, stringifyRequest } from 'loader-utils';
5+
import { getOptions, stringifyRequest } from 'loader-utils';
66
import postcss from 'postcss';
77
import postcssPkg from 'postcss/package.json';
88
import validateOptions from 'schema-utils';
@@ -22,6 +22,7 @@ import {
2222
getModulesPlugins,
2323
normalizeSourceMap,
2424
shouldUseModulesPlugins,
25+
isUrlRequestable,
2526
} from './utils';
2627

2728
export default function loader(content, map, meta) {
@@ -95,11 +96,19 @@ export default function loader(content, map, meta) {
9596
}
9697

9798
if (options.url !== false && exportType === 'full') {
99+
const urlResolver = this.getResolve({
100+
mainFields: ['asset'],
101+
conditionNames: ['asset'],
102+
});
103+
98104
plugins.push(
99105
urlParser({
106+
context: this.context,
107+
rootContext: this.rootContext,
100108
filter: getFilter(options.url, this.resourcePath, (value) =>
101-
isUrlRequest(value)
109+
isUrlRequestable(value)
102110
),
111+
resolver: urlResolver,
103112
urlHandler: (url) => stringifyRequest(this, url),
104113
})
105114
);

‎src/plugins/postcss-url-parser.js

+203-91
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,256 @@
1+
import { promisify } from 'util';
2+
13
import postcss from 'postcss';
24
import valueParser from 'postcss-value-parser';
35

4-
import { normalizeUrl } from '../utils';
6+
import { normalizeUrl, resolveRequests } from '../utils';
57

68
const pluginName = 'postcss-url-parser';
79

810
const isUrlFunc = /url/i;
911
const isImageSetFunc = /^(?:-webkit-)?image-set$/i;
1012
const needParseDecl = /(?:url|(?:-webkit-)?image-set)\(/i;
1113

14+
const walkCssAsync = promisify(walkCss);
15+
1216
function getNodeFromUrlFunc(node) {
1317
return node.nodes && node.nodes[0];
1418
}
1519

16-
function walkUrls(parsed, callback) {
17-
parsed.walk((node) => {
18-
if (node.type !== 'function') {
19-
return;
20-
}
20+
function ruleValidate(rule, decl, result, options) {
21+
// https://www.w3.org/TR/css-syntax-3/#typedef-url-token
22+
if (rule.url.replace(/^[\s]+|[\s]+$/g, '').length === 0) {
23+
result.warn(
24+
`Unable to find uri in '${decl ? decl.toString() : decl.value}'`,
25+
{ node: decl }
26+
);
2127

22-
if (isUrlFunc.test(node.value)) {
23-
const { nodes } = node;
24-
const isStringValue = nodes.length !== 0 && nodes[0].type === 'string';
25-
const url = isStringValue ? nodes[0].value : valueParser.stringify(nodes);
28+
return false;
29+
}
2630

27-
callback(getNodeFromUrlFunc(node), url, false, isStringValue);
31+
if (options.filter && !options.filter(rule.url)) {
32+
return false;
33+
}
2834

29-
// Do not traverse inside `url`
30-
// eslint-disable-next-line consistent-return
31-
return false;
32-
}
35+
return true;
36+
}
3337

34-
if (isImageSetFunc.test(node.value)) {
35-
for (const nNode of node.nodes) {
36-
const { type, value } = nNode;
38+
function walkCss(css, result, options, callback) {
39+
const accumulator = [];
3740

38-
if (type === 'function' && isUrlFunc.test(value)) {
39-
const { nodes } = nNode;
41+
css.walkDecls((decl) => {
42+
if (!needParseDecl.test(decl.value)) {
43+
return;
44+
}
4045

41-
const isStringValue =
42-
nodes.length !== 0 && nodes[0].type === 'string';
43-
const url = isStringValue
44-
? nodes[0].value
45-
: valueParser.stringify(nodes);
46+
const parsed = valueParser(decl.value);
4647

47-
callback(getNodeFromUrlFunc(nNode), url, false, isStringValue);
48-
}
48+
parsed.walk((node) => {
49+
if (node.type !== 'function') {
50+
return;
51+
}
4952

50-
if (type === 'string') {
51-
callback(nNode, value, true, true);
53+
if (isUrlFunc.test(node.value)) {
54+
const { nodes } = node;
55+
const isStringValue = nodes.length !== 0 && nodes[0].type === 'string';
56+
const url = isStringValue
57+
? nodes[0].value
58+
: valueParser.stringify(nodes);
59+
60+
const rule = {
61+
node: getNodeFromUrlFunc(node),
62+
url,
63+
needQuotes: false,
64+
isStringValue,
65+
};
66+
67+
if (ruleValidate(rule, decl, result, options)) {
68+
accumulator.push({
69+
decl,
70+
rule,
71+
parsed,
72+
});
5273
}
74+
75+
// Do not traverse inside `url`
76+
// eslint-disable-next-line consistent-return
77+
return false;
5378
}
5479

55-
// Do not traverse inside `image-set`
56-
// eslint-disable-next-line consistent-return
57-
return false;
58-
}
80+
if (isImageSetFunc.test(node.value)) {
81+
for (const nNode of node.nodes) {
82+
const { type, value } = nNode;
83+
84+
if (type === 'function' && isUrlFunc.test(value)) {
85+
const { nodes } = nNode;
86+
const isStringValue =
87+
nodes.length !== 0 && nodes[0].type === 'string';
88+
const url = isStringValue
89+
? nodes[0].value
90+
: valueParser.stringify(nodes);
91+
92+
const rule = {
93+
node: getNodeFromUrlFunc(nNode),
94+
url,
95+
needQuotes: false,
96+
isStringValue,
97+
};
98+
99+
if (ruleValidate(rule, decl, result, options)) {
100+
accumulator.push({
101+
decl,
102+
rule,
103+
parsed,
104+
});
105+
}
106+
}
107+
108+
if (type === 'string') {
109+
const rule = {
110+
node: nNode,
111+
url: value,
112+
needQuotes: true,
113+
isStringValue: true,
114+
};
115+
116+
if (ruleValidate(rule, decl, result, options)) {
117+
accumulator.push({
118+
decl,
119+
rule,
120+
parsed,
121+
});
122+
}
123+
}
124+
}
125+
126+
// Do not traverse inside `image-set`
127+
// eslint-disable-next-line consistent-return
128+
return false;
129+
}
130+
});
59131
});
132+
133+
callback(null, accumulator);
60134
}
61135

62136
export default postcss.plugin(pluginName, (options) => (css, result) => {
63-
const importsMap = new Map();
64-
const replacementsMap = new Map();
137+
return new Promise(async (resolve, reject) => {
138+
const importsMap = new Map();
139+
const replacementsMap = new Map();
140+
const urlToHelper = require.resolve('../runtime/getUrl.js');
65141

66-
let hasHelper = false;
142+
let parsedResults;
67143

68-
let index = 0;
144+
try {
145+
parsedResults = await walkCssAsync(css, result, options);
146+
} catch (error) {
147+
reject(error);
148+
}
149+
150+
if (parsedResults.length === 0) {
151+
resolve();
69152

70-
css.walkDecls((decl) => {
71-
if (!needParseDecl.test(decl.value)) {
72153
return;
73154
}
74155

75-
const parsed = valueParser(decl.value);
156+
const tasks = [];
76157

77-
walkUrls(parsed, (node, url, needQuotes, isStringValue) => {
78-
// https://www.w3.org/TR/css-syntax-3/#typedef-url-token
79-
if (url.replace(/^[\s]+|[\s]+$/g, '').length === 0) {
80-
result.warn(
81-
`Unable to find uri in '${decl ? decl.toString() : decl.value}'`,
82-
{ node: decl }
83-
);
158+
let index = 0;
159+
let hasHelper = false;
84160

85-
return;
86-
}
161+
for (const parsedResult of parsedResults) {
162+
index += 1;
87163

88-
if (options.filter && !options.filter(url)) {
89-
return;
164+
if (!hasHelper) {
165+
result.messages.push({
166+
pluginName,
167+
type: 'import',
168+
value: {
169+
// 'CSS_LOADER_GET_URL_IMPORT'
170+
order: 2,
171+
importName: '___CSS_LOADER_GET_URL_IMPORT___',
172+
url: options.urlHandler
173+
? options.urlHandler(urlToHelper)
174+
: urlToHelper,
175+
index,
176+
},
177+
});
178+
179+
hasHelper = true;
90180
}
91181

182+
const { decl, rule } = parsedResult;
183+
const { node, url, needQuotes, isStringValue } = rule;
92184
const splittedUrl = url.split(/(\?)?#/);
93185
const [urlWithoutHash, singleQuery, hashValue] = splittedUrl;
94186
const hash =
95187
singleQuery || hashValue
96188
? `${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}`
97189
: '';
98190

99-
const normalizedUrl = normalizeUrl(urlWithoutHash, isStringValue);
191+
let normalizedUrl = normalizeUrl(
192+
urlWithoutHash,
193+
isStringValue,
194+
options.rootContext
195+
);
196+
197+
let prefixSuffix = '';
198+
199+
const queryParts = normalizedUrl.split('!');
200+
201+
if (queryParts.length > 1) {
202+
normalizedUrl = queryParts.pop();
203+
prefixSuffix = queryParts.join('!');
204+
}
100205

101206
const importKey = normalizedUrl;
102-
let importName = importsMap.get(importKey);
103207

104-
index += 1;
208+
let importName = importsMap.get(importKey);
105209

106210
if (!importName) {
107211
importName = `___CSS_LOADER_URL_IMPORT_${importsMap.size}___`;
108212
importsMap.set(importKey, importName);
109213

110-
if (!hasHelper) {
111-
const urlToHelper = require.resolve('../runtime/getUrl.js');
112-
113-
result.messages.push({
114-
pluginName,
115-
type: 'import',
116-
value: {
117-
// 'CSS_LOADER_GET_URL_IMPORT'
118-
order: 2,
119-
importName: '___CSS_LOADER_GET_URL_IMPORT___',
120-
url: options.urlHandler
121-
? options.urlHandler(urlToHelper)
122-
: urlToHelper,
123-
index,
124-
},
125-
});
126-
127-
hasHelper = true;
128-
}
129-
130-
result.messages.push({
131-
pluginName,
132-
type: 'import',
133-
value: {
134-
// 'CSS_LOADER_URL_IMPORT'
135-
order: 3,
136-
importName,
137-
url: options.urlHandler
138-
? options.urlHandler(normalizedUrl)
139-
: normalizedUrl,
140-
index,
141-
},
142-
});
214+
tasks.push(
215+
Promise.resolve(index).then(async (currentIndex) => {
216+
const { resolver, context } = options;
217+
218+
let resolvedUrl;
219+
220+
try {
221+
resolvedUrl = await resolveRequests(resolver, context, [
222+
...new Set([normalizedUrl, url]),
223+
]);
224+
} catch (error) {
225+
throw error;
226+
}
227+
228+
if (prefixSuffix) {
229+
resolvedUrl = `${prefixSuffix}!${resolvedUrl}`;
230+
}
231+
232+
result.messages.push({
233+
pluginName,
234+
type: 'import',
235+
value: {
236+
// 'CSS_LOADER_URL_IMPORT'
237+
order: 3,
238+
importName,
239+
url: options.urlHandler
240+
? options.urlHandler(resolvedUrl)
241+
: resolvedUrl,
242+
index: currentIndex,
243+
},
244+
});
245+
})
246+
);
143247
}
144248

145-
const replacementKey = JSON.stringify({ importKey, hash, needQuotes });
249+
const replacementKey = JSON.stringify({
250+
importKey,
251+
hash,
252+
needQuotes,
253+
});
146254
let replacementName = replacementsMap.get(replacementKey);
147255

148256
if (!replacementName) {
@@ -168,9 +276,13 @@ export default postcss.plugin(pluginName, (options) => (css, result) => {
168276
node.type = 'word';
169277
// eslint-disable-next-line no-param-reassign
170278
node.value = replacementName;
171-
});
279+
// eslint-disable-next-line no-param-reassign
280+
decl.value = parsedResult.parsed.toString();
281+
}
172282

173-
// eslint-disable-next-line no-param-reassign
174-
decl.value = parsed.toString();
283+
Promise.all(tasks).then(
284+
() => resolve(),
285+
(error) => reject(error)
286+
);
175287
});
176288
});

‎src/utils.js

-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ function getModuleCode(
314314

315315
const { css, map } = result;
316316
const sourceMapValue = sourceMap && map ? `,${map}` : '';
317-
318317
let code = JSON.stringify(css);
319318
let beforeCode = '';
320319

‎test/__snapshots__/url-option.test.js.snap

+156-108
Large diffs are not rendered by default.

‎test/fixtures/url/url-absolute.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import css from './url-absolute.css';
2+
3+
__export__ = css;
4+
5+
export default css;

‎test/fixtures/url/url-unresolved.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.class {
2+
background: url('unresolved.png');
3+
}

‎test/fixtures/url/url-unresolved.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import css from './url-unresolved.css';
2+
3+
__export__ = css;
4+
5+
export default css;

‎test/fixtures/url/url.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ b {
182182
background: url('img-simple.png');
183183
}
184184

185-
.not-resolved {
186-
background: url('/img-simple.png');
185+
.root-relative {
186+
background: url('/url/img-simple.png');
187187
}
188188

189189
.above-below {

‎test/helpers/getCompiler.js

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default (fixture, loaderOptions = {}, config = {}) => {
4141
__dirname,
4242
'../fixtures/modules/composes'
4343
),
44+
'/img.png': path.resolve(__dirname, '../fixtures/url/img.png'),
4445
},
4546
},
4647
optimization: {

‎test/url-option.test.js

+34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
14
import {
25
compile,
36
getCompiler,
@@ -66,4 +69,35 @@ describe('"url" option', () => {
6669
expect(getWarnings(stats)).toMatchSnapshot('warnings');
6770
expect(getErrors(stats)).toMatchSnapshot('errors');
6871
});
72+
73+
it('should resolve absolute path', async () => {
74+
// Create the file with absolute path
75+
const fileDirectory = path.resolve(__dirname, 'fixtures', 'url');
76+
const file = path.resolve(fileDirectory, 'url-absolute.css');
77+
const absoluteUrlpath = path.resolve(fileDirectory, 'img.png');
78+
79+
const code = `\n.background {background: url(${absoluteUrlpath}); }`;
80+
81+
fs.writeFileSync(file, code);
82+
83+
const compiler = getCompiler('./url/url-absolute.js');
84+
const stats = await compile(compiler);
85+
86+
expect(getModuleSource('./url/url-absolute.css', stats)).toMatchSnapshot(
87+
'module'
88+
);
89+
expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
90+
'result'
91+
);
92+
expect(getWarnings(stats)).toMatchSnapshot('warnings');
93+
expect(getErrors(stats)).toMatchSnapshot('errors');
94+
});
95+
96+
it('should emit warning when unresolved import', async () => {
97+
const compiler = getCompiler('./url/url-unresolved.js');
98+
const stats = await compile(compiler);
99+
100+
expect(getWarnings(stats)).toMatchSnapshot('warnings');
101+
expect(getErrors(stats, true)).toMatchSnapshot('errors');
102+
});
69103
});

0 commit comments

Comments
 (0)
Please sign in to comment.