Skip to content

Commit 76f1480

Browse files
fix: resolution algorithm for CSS modules
1 parent f9b8ef9 commit 76f1480

22 files changed

+299
-110
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ exports.locals = {
415415

416416
To import a local classname from another module.
417417

418+
> i We strongly recommend that you specify the extension when importing a file, since it is possible to import a file with any extension and it is not known in advance which file to use.
419+
418420
```css
419421
:local(.continueButton) {
420422
composes: button from 'library/button.css';

src/index.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,19 @@ export default function loader(content, map, meta) {
4343
const preRequester = getPreRequester(this);
4444
const urlHandler = (url) =>
4545
stringifyRequest(this, preRequester(options.importLoaders) + url);
46+
const icssResolver = this.getResolve({
47+
mainFields: ['css', 'style', 'main', '...'],
48+
mainFiles: ['index', '...'],
49+
});
4650

47-
plugins.push(icssParser({ urlHandler }));
51+
plugins.push(
52+
icssParser({
53+
context: this.context,
54+
rootContext: this.rootContext,
55+
resolver: icssResolver,
56+
urlHandler,
57+
})
58+
);
4859

4960
if (options.import !== false && exportType === 'full') {
5061
const resolver = this.getResolve({
@@ -136,8 +147,6 @@ export default function loader(content, map, meta) {
136147
}
137148
}
138149

139-
apiImports.sort((a, b) => a.index - b.index);
140-
141150
/*
142151
* Order
143152
* CSS_LOADER_ICSS_IMPORT: [],
@@ -153,6 +162,12 @@ export default function loader(content, map, meta) {
153162
(b.index < a.index) - (a.index < b.index)
154163
);
155164
});
165+
apiImports.sort((a, b) => {
166+
return (
167+
(b.order < a.order) - (a.order < b.order) ||
168+
(b.index < a.index) - (a.index < b.index)
169+
);
170+
});
156171

157172
const { localsConvention } = options;
158173
const esModule =

src/plugins/postcss-icss-parser.js

+110-49
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import postcss from 'postcss';
22
import { extractICSS, replaceValueSymbols, replaceSymbols } from 'icss-utils';
3-
import { urlToRequest } from 'loader-utils';
43

5-
function makeRequestableIcssImports(icssImports) {
4+
import { normalizeUrl, resolveRequests, isUrlRequestable } from '../utils';
5+
6+
function makeRequestableIcssImports(icssImports, rootContext) {
67
return Object.keys(icssImports).reduce((accumulator, url) => {
78
const tokensMap = icssImports[url];
89
const tokens = Object.keys(tokensMap);
@@ -11,16 +12,27 @@ function makeRequestableIcssImports(icssImports) {
1112
return accumulator;
1213
}
1314

14-
const normalizedUrl = urlToRequest(url);
15+
const isRequestable = isUrlRequestable(url);
16+
17+
let normalizedUrl;
18+
19+
if (isRequestable) {
20+
normalizedUrl = normalizeUrl(url, true, rootContext);
21+
}
1522

16-
if (!accumulator[normalizedUrl]) {
23+
const key = typeof normalizedUrl !== 'undefined' ? normalizedUrl : url;
24+
25+
if (!accumulator[key]) {
1726
// eslint-disable-next-line no-param-reassign
18-
accumulator[normalizedUrl] = tokensMap;
27+
accumulator[key] = { url, tokenMap: tokensMap };
1928
} else {
2029
// eslint-disable-next-line no-param-reassign
21-
accumulator[normalizedUrl] = {
22-
...accumulator[normalizedUrl],
23-
...tokensMap,
30+
accumulator[key] = {
31+
url,
32+
tokenMap: {
33+
...accumulator[key].tokenMap,
34+
...tokensMap,
35+
},
2436
};
2537
}
2638

@@ -31,55 +43,104 @@ function makeRequestableIcssImports(icssImports) {
3143
export default postcss.plugin(
3244
'postcss-icss-parser',
3345
(options) => (css, result) => {
34-
const importReplacements = Object.create(null);
35-
const extractedICSS = extractICSS(css);
36-
const icssImports = makeRequestableIcssImports(extractedICSS.icssImports);
37-
38-
for (const [importIndex, url] of Object.keys(icssImports).entries()) {
39-
const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`;
40-
41-
result.messages.push(
42-
{
43-
type: 'import',
44-
value: {
45-
// 'CSS_LOADER_ICSS_IMPORT'
46-
order: 0,
47-
importName,
48-
url: options.urlHandler ? options.urlHandler(url) : url,
49-
},
50-
},
51-
{
52-
type: 'api-import',
53-
value: { type: 'internal', importName, dedupe: true },
54-
}
46+
return new Promise(async (resolve, reject) => {
47+
const importReplacements = Object.create(null);
48+
const extractedICSS = extractICSS(css);
49+
const icssImports = makeRequestableIcssImports(
50+
extractedICSS.icssImports,
51+
options.rootContext
5552
);
5653

57-
const tokenMap = icssImports[url];
58-
const tokens = Object.keys(tokenMap);
59-
60-
for (const [replacementIndex, token] of tokens.entries()) {
61-
const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`;
62-
const localName = tokenMap[token];
54+
const tasks = [];
55+
56+
let index = 0;
57+
58+
for (const [importIndex, normalizedUrl] of Object.keys(
59+
icssImports
60+
).entries()) {
61+
const { url } = icssImports[normalizedUrl];
62+
63+
index += 1;
64+
65+
tasks.push(
66+
Promise.resolve(index).then(async (currentIndex) => {
67+
const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`;
68+
const { resolver, context } = options;
69+
70+
let resolvedUrl;
71+
72+
try {
73+
resolvedUrl = await resolveRequests(resolver, context, [
74+
...new Set([normalizedUrl, url]),
75+
]);
76+
} catch (error) {
77+
throw error;
78+
}
79+
80+
result.messages.push(
81+
{
82+
type: 'import',
83+
value: {
84+
// 'CSS_LOADER_ICSS_IMPORT'
85+
order: 0,
86+
importName,
87+
url: options.urlHandler(resolvedUrl),
88+
index: currentIndex,
89+
},
90+
},
91+
{
92+
type: 'api-import',
93+
value: {
94+
// 'CSS_LOADER_ICSS_IMPORT'
95+
order: 0,
96+
type: 'internal',
97+
importName,
98+
dedupe: true,
99+
index: currentIndex,
100+
},
101+
}
102+
);
103+
104+
const { tokenMap } = icssImports[normalizedUrl];
105+
const tokens = Object.keys(tokenMap);
106+
107+
for (const [replacementIndex, token] of tokens.entries()) {
108+
const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`;
109+
const localName = tokenMap[token];
110+
111+
importReplacements[token] = replacementName;
112+
113+
result.messages.push({
114+
type: 'icss-replacement',
115+
value: { replacementName, importName, localName },
116+
});
117+
}
118+
})
119+
);
120+
}
63121

64-
importReplacements[token] = replacementName;
122+
try {
123+
await Promise.all(tasks);
124+
} catch (error) {
125+
reject(error);
126+
}
65127

66-
result.messages.push({
67-
type: 'icss-replacement',
68-
value: { replacementName, importName, localName },
69-
});
128+
if (Object.keys(importReplacements).length > 0) {
129+
replaceSymbols(css, importReplacements);
70130
}
71-
}
72131

73-
if (Object.keys(importReplacements).length > 0) {
74-
replaceSymbols(css, importReplacements);
75-
}
132+
const { icssExports } = extractedICSS;
76133

77-
const { icssExports } = extractedICSS;
134+
for (const name of Object.keys(icssExports)) {
135+
const value = replaceValueSymbols(
136+
icssExports[name],
137+
importReplacements
138+
);
78139

79-
for (const name of Object.keys(icssExports)) {
80-
const value = replaceValueSymbols(icssExports[name], importReplacements);
140+
result.messages.push({ type: 'export', value: { name, value } });
141+
}
81142

82-
result.messages.push({ type: 'export', value: { name, value } });
83-
}
143+
resolve();
144+
});
84145
}
85146
);

src/plugins/postcss-import-parser.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ export default postcss.plugin(pluginName, (options) => (css, result) => {
137137
// 'CSS_LOADER_AT_RULE_IMPORT'
138138
order: 1,
139139
importName,
140-
url: options.urlHandler
141-
? options.urlHandler(resolvedUrl)
142-
: resolvedUrl,
140+
url: options.urlHandler(resolvedUrl),
143141
index: currentIndex,
144142
},
145143
});
@@ -148,6 +146,8 @@ export default postcss.plugin(pluginName, (options) => (css, result) => {
148146
result.messages.push({
149147
type: 'api-import',
150148
value: {
149+
// 'CSS_LOADER_AT_RULE_IMPORT'
150+
order: 1,
151151
type: 'internal',
152152
importName,
153153
media,
@@ -161,7 +161,14 @@ export default postcss.plugin(pluginName, (options) => (css, result) => {
161161
result.messages.push({
162162
pluginName,
163163
type: 'api-import',
164-
value: { type: 'external', url, media, index: currentIndex },
164+
value: {
165+
// 'CSS_LOADER_AT_RULE_IMPORT'
166+
order: 1,
167+
type: 'external',
168+
url,
169+
media,
170+
index: currentIndex,
171+
},
165172
});
166173
})
167174
);

test/__snapshots__/import-option.test.js.snap

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`"import" option should emit warning when unresolved import: errors 1`] = `
4+
Array [
5+
"ModuleBuildError: Module build failed (from \`replaced original path\`):
6+
Error: Can't resolve 'unresolved-file.css' in '/test/fixtures/import'",
7+
]
8+
`;
9+
10+
exports[`"import" option should emit warning when unresolved import: warnings 1`] = `Array []`;
11+
312
exports[`"import" option should keep original order: errors 1`] = `Array []`;
413

514
exports[`"import" option should keep original order: module 1`] = `

0 commit comments

Comments
 (0)