Skip to content

Commit d779eb1

Browse files
feat: escape getLocalIdent by default (#1196)
BREAKING CHANGE: returned value from the `getLocalIdent` escapes by default, the `exportName` value is always unescaped
1 parent dd52931 commit d779eb1

File tree

5 files changed

+157
-29
lines changed

5 files changed

+157
-29
lines changed

src/utils.js

+39-26
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ const filenameReservedRegex = /[<>:"/\\|?*]/g;
4949
// eslint-disable-next-line no-control-regex
5050
const reControlChars = /[\u0000-\u001f\u0080-\u009f]/g;
5151

52+
function escapeLocalident(localident) {
53+
return cssesc(
54+
localident
55+
// For `[hash]` placeholder
56+
.replace(/^((-?[0-9])|--)/, '_$1')
57+
.replace(filenameReservedRegex, '-')
58+
.replace(reControlChars, '-')
59+
.replace(/\./g, '-'),
60+
{ isIdentifier: true }
61+
);
62+
}
63+
5264
function defaultGetLocalIdent(
5365
loaderContext,
5466
localIdentName,
@@ -60,19 +72,9 @@ function defaultGetLocalIdent(
6072
const request = normalizePath(path.relative(context, resourcePath));
6173

6274
// eslint-disable-next-line no-param-reassign
63-
options.content = `${hashPrefix + request}\x00${unescape(localName)}`;
75+
options.content = `${hashPrefix + request}\x00${localName}`;
6476

65-
// Using `[path]` placeholder outputs `/` we need escape their
66-
// Also directories can contains invalid characters for css we need escape their too
67-
return cssesc(
68-
interpolateName(loaderContext, localIdentName, options)
69-
// For `[hash]` placeholder
70-
.replace(/^((-?[0-9])|--)/, '_$1')
71-
.replace(filenameReservedRegex, '-')
72-
.replace(reControlChars, '-')
73-
.replace(/\./g, '-'),
74-
{ isIdentifier: true }
75-
).replace(/\\\[local\\]/gi, localName);
77+
return interpolateName(loaderContext, localIdentName, options);
7678
}
7779

7880
function normalizeUrl(url, isStringValue) {
@@ -143,7 +145,8 @@ function getModulesOptions(rawOptions, loaderContext) {
143145
localIdentHashPrefix: '',
144146
// eslint-disable-next-line no-undefined
145147
localIdentRegExp: undefined,
146-
getLocalIdent: defaultGetLocalIdent,
148+
// eslint-disable-next-line no-undefined
149+
getLocalIdent: undefined,
147150
namedExport: false,
148151
exportLocalsConvention: 'asIs',
149152
exportOnlyLocals: false,
@@ -281,32 +284,42 @@ function getModulesPlugins(options, loaderContext) {
281284
extractImports(),
282285
modulesScope({
283286
generateScopedName(exportName) {
284-
let localIdent = getLocalIdent(
285-
loaderContext,
286-
localIdentName,
287-
exportName,
288-
{
289-
context: localIdentContext,
290-
hashPrefix: localIdentHashPrefix,
291-
regExp: localIdentRegExp,
292-
}
293-
);
287+
let localIdent;
288+
289+
if (typeof getLocalIdent !== 'undefined') {
290+
localIdent = getLocalIdent(
291+
loaderContext,
292+
localIdentName,
293+
unescape(exportName),
294+
{
295+
context: localIdentContext,
296+
hashPrefix: localIdentHashPrefix,
297+
regExp: localIdentRegExp,
298+
}
299+
);
300+
}
294301

295302
// A null/undefined value signals that we should invoke the default
296303
// getLocalIdent method.
297-
if (localIdent == null) {
304+
if (typeof localIdent === 'undefined' || localIdent === null) {
298305
localIdent = defaultGetLocalIdent(
299306
loaderContext,
300307
localIdentName,
301-
exportName,
308+
unescape(exportName),
302309
{
303310
context: localIdentContext,
304311
hashPrefix: localIdentHashPrefix,
305312
regExp: localIdentRegExp,
306313
}
307314
);
315+
316+
return escapeLocalident(localIdent).replace(
317+
/\\\[local\\]/gi,
318+
exportName
319+
);
308320
}
309-
return localIdent;
321+
322+
return escapeLocalident(localIdent);
310323
},
311324
exportGlobals: options.modules.exportGlobals,
312325
}),

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

+55-3
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,69 @@ Array [
137137

138138
exports[`"modules" option issue #861: warnings 1`] = `Array []`;
139139

140+
exports[`"modules" option issue #966 - values in selectors aren't escaped properly: errors 1`] = `Array []`;
141+
142+
exports[`"modules" option issue #966 - values in selectors aren't escaped properly: module 1`] = `
143+
"// Imports
144+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
145+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
146+
// Module
147+
___CSS_LOADER_EXPORT___.push([module.id, \\"._7-foo-class {\\\\n color: red;\\\\n}\\\\n\\\\n.\\\\\\\\--bar-class {\\\\n color: red;\\\\n}\\\\n\\\\n.\\\\\\\\--baz-class {\\\\n color: red;\\\\n}\\\\n\\\\n.fooBaz-class-continuation {\\\\n color: red;\\\\n}\\\\n\\\\n.some.class {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
148+
// Exports
149+
___CSS_LOADER_EXPORT___.locals = {
150+
\\"foo-class\\": \\"_7-foo-class\\",
151+
\\"bar-class\\": \\"--bar-class\\",
152+
\\"baz-class\\": \\"--baz-class\\",
153+
\\"fooBaz-class\\": \\"fooBaz-class-continuation\\",
154+
\\"some\\": \\"some\\",
155+
\\"class\\": \\"class\\"
156+
};
157+
export default ___CSS_LOADER_EXPORT___;
158+
"
159+
`;
160+
161+
exports[`"modules" option issue #966 - values in selectors aren't escaped properly: result 1`] = `
162+
Array [
163+
Array [
164+
"./modules/issue-966/issue-966.css",
165+
"._7-foo-class {
166+
color: red;
167+
}
168+
169+
.\\\\--bar-class {
170+
color: red;
171+
}
172+
173+
.\\\\--baz-class {
174+
color: red;
175+
}
176+
177+
.fooBaz-class-continuation {
178+
color: red;
179+
}
180+
181+
.some.class {
182+
color: red;
183+
}
184+
",
185+
"",
186+
],
187+
]
188+
`;
189+
190+
exports[`"modules" option issue #966 - values in selectors aren't escaped properly: warnings 1`] = `Array []`;
191+
140192
exports[`"modules" option issue #966: errors 1`] = `Array []`;
141193

142194
exports[`"modules" option issue #966: module 1`] = `
143195
"// Imports
144196
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
145197
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
146198
// Module
147-
___CSS_LOADER_EXPORT___.push([module.id, \\".button.hey {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
199+
___CSS_LOADER_EXPORT___.push([module.id, \\".button-hey {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
148200
// Exports
149201
___CSS_LOADER_EXPORT___.locals = {
150-
\\"button\\": \\"button.hey\\"
202+
\\"button\\": \\"button-hey\\"
151203
};
152204
export default ___CSS_LOADER_EXPORT___;
153205
"
@@ -157,7 +209,7 @@ exports[`"modules" option issue #966: result 1`] = `
157209
Array [
158210
Array [
159211
"./modules/issue-966/button.css",
160-
".button.hey {
212+
".button-hey {
161213
color: red;
162214
}
163215
",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.foo-class {
2+
color: red;
3+
}
4+
5+
.bar-class {
6+
color: red;
7+
}
8+
9+
.baz-class {
10+
color: red;
11+
}
12+
13+
.fooBaz-class {
14+
color: red;
15+
}
16+
17+
.some.class {
18+
color: red;
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import css from './issue-966.css';
2+
3+
__export__ = css;
4+
5+
export default css;

test/modules-option.test.js

+39
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,45 @@ describe('"modules" option', () => {
491491
expect(getErrors(stats)).toMatchSnapshot('errors');
492492
});
493493

494+
it("issue #966 - values in selectors aren't escaped properly", async () => {
495+
const compiler = getCompiler('./modules/issue-966/issue-966.js', {
496+
modules: {
497+
getLocalIdent: (loaderContext, localIdentName, localName) => {
498+
if (localName === 'foo-class') {
499+
return `7-${localName}`;
500+
}
501+
502+
if (localName === 'bar-class') {
503+
return `>-${localName}`;
504+
}
505+
506+
if (localName === 'baz-class') {
507+
return `\u0000-${localName}`;
508+
}
509+
510+
if (localName === 'fooBaz-class') {
511+
return `${localName}.continuation`;
512+
}
513+
514+
return null;
515+
},
516+
localIdentName: '[local]',
517+
},
518+
});
519+
const stats = await compile(compiler);
520+
521+
expect(
522+
getModuleSource('./modules/issue-966/issue-966.css', stats)
523+
).toMatchSnapshot('module');
524+
525+
expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
526+
'result'
527+
);
528+
529+
expect(getWarnings(stats)).toMatchSnapshot('warnings');
530+
expect(getErrors(stats)).toMatchSnapshot('errors');
531+
});
532+
494533
it('issue #967', async () => {
495534
const compiler = getCompiler('./modules/issue-967/path-placeholder.js', {
496535
modules: {

0 commit comments

Comments
 (0)