Skip to content

Commit f070e7e

Browse files
authored
Merge pull request #8 from adrien-k/fix-combining-classes
fix: apply :global() to whole selector + add strict mode
2 parents ae51f4d + 58845d5 commit f070e7e

File tree

5 files changed

+254
-23
lines changed

5 files changed

+254
-23
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Pass an object of the following properties
6666
| `localIdentName` | `{String}` | `"[local]-[hash:base64:6]"` | A rule using any available token from [webpack interpolateName](https://github.com/webpack/loader-utils#interpolatename) |
6767
| `includePaths` | `{Array}` | `[]` (Any) | An array of paths to be processed |
6868
| `getLocalIdent` | `Function` | `undefined` | Generate the classname by specifying a function instead of using the built-in interpolation |
69+
| `strict` | `Boolean` | `false` | When true, an exception is raised when a class is used while not being defined in `<style>`
6970

7071
#### `getLocalIdent`
7172

index.js

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ const pluginOptions = {
66
includePaths: [],
77
localIdentName: '[local]-[hash:base64:6]',
88
getLocalIdent: getLocalIdent,
9+
strict: false,
910
};
1011

1112
const regex = {
1213
module: /\$(style)?\.(:?[\w\d-]*)/gm,
1314
style: /<style(\s[^]*?)?>([^]*?)<\/style>/gi,
1415
pathUnallowed: /[<>:"/\\|?*]/g,
1516
class: (className) => {
16-
return new RegExp(`\\.(${className})\\b(?![-_])`, 'gm')
17-
}
17+
return new RegExp(`\\.(${className})\\b(?![-_])`, 'gm');
18+
},
19+
classSelector: (className) => {
20+
return new RegExp(`\\S*\\.(${className})\\b(?![-_])\\S*`, 'gm');
21+
},
1822
};
1923

2024
let moduleClasses = {};
@@ -35,7 +39,7 @@ function generateName(resourcePath, styles, className) {
3539
interpolateName({ resourcePath }, localName, { content })
3640
.replace(/\./g, '-')
3741
);
38-
42+
3943
// replace unwanted characters from [path]
4044
if (regex.pathUnallowed.test(interpolatedName)) {
4145
interpolatedName = interpolatedName.replace(regex.pathUnallowed, '_');
@@ -72,20 +76,38 @@ const markup = async ({ content, filename }) => {
7276
const styles = content.match(regex.style);
7377
moduleClasses[filename] = {};
7478

75-
return { code: content.replace(regex.module, (match, key, className) => {
76-
let replacement = '';
77-
if (styles.length) {
78-
if (regex.class(className).test(styles[0])) {
79-
const interpolatedName = generateName(
80-
filename,
81-
styles[0],
82-
className
79+
return {
80+
code: content.replace(regex.module, (match, key, className) => {
81+
let replacement = '';
82+
if (!className.length) {
83+
throw new Error(
84+
`Invalid class name in file ${filename}.\n`+
85+
'This usually happens when using dynamic classes with svelte-preprocess-cssmodules.'
8386
);
87+
}
88+
89+
if (!regex.class(className).test(`.${className}`)) {
90+
throw new Error(`Classname "${className}" in file ${filename} is not valid`);
91+
}
92+
93+
if (styles.length) {
94+
if (!regex.class(className).test(styles[0])) {
95+
if (pluginOptions.strict) {
96+
throw new Error(
97+
`Classname "${className}" was not found in declared ${filename} <style>`
98+
);
99+
} else {
100+
// In non-strict mode, we just remove $style classes that don't have a definition
101+
return '';
102+
}
103+
}
104+
105+
const interpolatedName = generateName(filename, styles[0], className);
84106

85107
const customInterpolatedName = pluginOptions.getLocalIdent(
86108
{
87109
context: path.dirname(filename),
88-
resourcePath :filename,
110+
resourcePath: filename,
89111
},
90112
{
91113
interpolatedName,
@@ -101,9 +123,9 @@ const markup = async ({ content, filename }) => {
101123
moduleClasses[filename][className] = customInterpolatedName;
102124
replacement = customInterpolatedName;
103125
}
104-
}
105-
return replacement;
106-
})};
126+
return replacement;
127+
}),
128+
};
107129
};
108130

109131
const style = async ({ content, filename }) => {
@@ -112,7 +134,7 @@ const style = async ({ content, filename }) => {
112134
if (!moduleClasses.hasOwnProperty(filename)) {
113135
return { code };
114136
}
115-
137+
116138
const classes = moduleClasses[filename];
117139

118140
if (Object.keys(classes).length === 0) {
@@ -121,8 +143,17 @@ const style = async ({ content, filename }) => {
121143

122144
for (const className in classes) {
123145
code = code.replace(
124-
regex.class(className),
125-
() => `:global(.${classes[className]})`
146+
regex.classSelector(className),
147+
(match) => {
148+
const generatedClass = match.replace(
149+
regex.class(className),
150+
() => `.${classes[className]}`
151+
);
152+
153+
return generatedClass.indexOf(':global(') !== -1
154+
? generatedClass
155+
: `:global(${generatedClass})`;
156+
}
126157
);
127158
}
128159

@@ -136,5 +167,5 @@ module.exports = (options) => {
136167
return {
137168
markup,
138169
style,
139-
}
140-
};
170+
};
171+
};

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/remove.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,16 @@ test('[Shorthand] Remove unused CSS Modules from HTML attribute', async () => {
2222

2323
expect(output).toBe(expectedOutput);
2424
});
25+
26+
describe('in strict mode', () => {
27+
test('Throws an exception', async () => {
28+
await expect(compiler({
29+
source,
30+
}, {
31+
localIdentName: '[local]-123456',
32+
strict: true,
33+
})).rejects.toThrow(
34+
'Classname \"blue\" was not found in declared src/App.svelte <style>'
35+
);
36+
});
37+
});

test/replace.test.js

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const compiler = require('./compiler.js');
22

3-
const source = '<style>.red { color: red; }</style>\n<span class="$style.red">Red</span>';
4-
const sourceShorthand = '<style>.red { color: red; }</style>\n<span class="$.red">Red</span>';
3+
const style = '<style>.red { color: red; }</style>';
4+
const source = style + '\n<span class="$style.red">Red</span>';
5+
const sourceShorthand = style + '\n<span class="$.red">Red</span>';
56

67
test('Generate CSS Modules from HTML attributes, Replace CSS className', async () => {
78
const output = await compiler({
@@ -55,3 +56,188 @@ test('[Shorthand] Avoid generated class to end with a hyphen', async () => {
5556
});
5657
expect(output).toBe('<style>:global(.red) { color: red; }</style>\n<span class="red">Red</span>');
5758
});
59+
60+
describe('combining multiple classes', () => {
61+
const style = '<style>span.red.large:hover { font-size: 20px; } \n.red { color: red; }</style>';
62+
const source = style + '\n<span class="$style.red $style.large">Red</span>';
63+
64+
const expectedStyle =
65+
'<style>:global(span.red-123456.large-123456:hover) { font-size: 20px; } \n:global(.red-123456) { color: red; }</style>';
66+
const expectedOutput = expectedStyle + '\n<span class="red-123456 large-123456">Red</span>';
67+
68+
test('Generate CSS Modules from HTML attributes, Replace CSS className', async () => {
69+
const output = await compiler(
70+
{
71+
source,
72+
},
73+
{
74+
localIdentName: '[local]-123456',
75+
}
76+
);
77+
78+
expect(output).toBe(expectedOutput);
79+
});
80+
});
81+
82+
describe('Classname is part of a selector', () => {
83+
84+
test('CSS Modules class targetting children', async () => {
85+
const source =
86+
'<style>\n' +
87+
'div.red > sup { font-size: 12px; }\n' +
88+
'.red { color: red; }\n' +
89+
'</style>\n' +
90+
'<div class="$style.red">Red<sup>*</sup></div>';
91+
92+
const expectedOutput =
93+
'<style>\n' +
94+
':global(div.red-123) > sup { font-size: 12px; }\n' +
95+
':global(.red-123) { color: red; }\n' +
96+
'</style>\n' +
97+
'<div class="red-123">Red<sup>*</sup></div>';
98+
99+
const output = await compiler(
100+
{
101+
source,
102+
},
103+
{
104+
localIdentName: '[local]-123',
105+
}
106+
);
107+
108+
expect(output).toBe(expectedOutput);
109+
});
110+
111+
test('CSS Modules class has a parent', async () => {
112+
const source =
113+
'<style>\n' +
114+
'div .semibold .red { font-size: 20px; }\n' +
115+
'.red { color: red; }\n' +
116+
'.semibold { font-weight: 600; }\n' +
117+
'</style>\n' +
118+
'<div><strong class="$style.semibold"><span class="$style.red">Red</span></strong></div>';
119+
120+
const expectedOutput =
121+
'<style>\n' +
122+
'div :global(.semibold-123) :global(.red-123) { font-size: 20px; }\n' +
123+
':global(.red-123) { color: red; }\n' +
124+
':global(.semibold-123) { font-weight: 600; }\n' +
125+
'</style>\n' +
126+
'<div><strong class="semibold-123"><span class="red-123">Red</span></strong></div>';
127+
128+
const output = await compiler(
129+
{
130+
source,
131+
},
132+
{
133+
localIdentName: '[local]-123',
134+
}
135+
);
136+
137+
expect(output).toBe(expectedOutput);
138+
});
139+
140+
test('CSS Modules class has a global parent', async () => {
141+
const source =
142+
'<style>\n' +
143+
':global(div) .red { font-size: 20px; }\n' +
144+
'.red { color: red; }\n' +
145+
'</style>\n' +
146+
'<div><span class="$style.red">Red</span></div>';
147+
148+
const expectedOutput =
149+
'<style>\n' +
150+
':global(div) :global(.red-123) { font-size: 20px; }\n' +
151+
':global(.red-123) { color: red; }\n' +
152+
'</style>\n' +
153+
'<div><span class="red-123">Red</span></div>';
154+
155+
const output = await compiler(
156+
{
157+
source,
158+
},
159+
{
160+
localIdentName: '[local]-123',
161+
}
162+
);
163+
164+
expect(output).toBe(expectedOutput);
165+
});
166+
167+
test('CSS Modules class is used within a media query', async () => {
168+
const source =
169+
'<style>\n' +
170+
'@media (min-width: 37.5em) {\n' +
171+
'.red { color: red; }\n' +
172+
'div.bold { font-weight: bold; }\n' +
173+
'}\n' +
174+
'</style>\n' +
175+
'<div class="$style.bold"><span class="$style.red">Red</span></div>';
176+
177+
const expectedOutput =
178+
'<style>\n' +
179+
'@media (min-width: 37.5em) {\n' +
180+
':global(.red-123) { color: red; }\n' +
181+
':global(div.bold-123) { font-weight: bold; }\n' +
182+
'}\n' +
183+
'</style>\n' +
184+
'<div class="bold-123"><span class="red-123">Red</span></div>';
185+
186+
const output = await compiler(
187+
{
188+
source,
189+
},
190+
{
191+
localIdentName: '[local]-123',
192+
}
193+
);
194+
195+
expect(output).toBe(expectedOutput);
196+
});
197+
});
198+
199+
describe('using dynamic classes', () => {
200+
describe('when matched class is empty', () => {
201+
// The parser will identify a class named ''
202+
const source =
203+
'<style>.red { font-size: 20px; }</style>' + '<span class={`$style.${color}`}>Red</span>';
204+
205+
test('throws an exception', async () => {
206+
await expect(compiler({ source })).rejects.toThrow(
207+
'Invalid class name in file src/App.svelte.\nThis usually happens when using dynamic classes with svelte-preprocess-cssmodules.'
208+
);
209+
});
210+
});
211+
212+
describe('when matched class could be a valid class but does not match any style definition', () => {
213+
// The parser will identify a class named 'color'
214+
const source =
215+
'<style>.colorred { font-size: 20px; }</style>' +
216+
'<span class={`$style.color${color}`}>Red</span>';
217+
218+
it('in strict mode, it throw an exception', async () => {
219+
await expect(compiler({ source }, { strict: true })).rejects.toThrow(
220+
'Classname "color" was not found in declared src/App.svelte <style>'
221+
);
222+
});
223+
224+
// TODO: fix, this is probably not a result one would expect
225+
it('in non-strict mode, it removes the resulting class', async () => {
226+
const output = await compiler({ source }, { strict: false });
227+
expect(output).toEqual(
228+
'<style>.colorred { font-size: 20px; }</style><span class={`${color}`}>Red</span>'
229+
);
230+
});
231+
});
232+
233+
describe('when matched class is an invalid class', () => {
234+
// The parser will identify a class named 'color-'
235+
const source =
236+
'<style>.color-red { font-size: 20px; }</style>' +
237+
'<span class={`$style.color-${color}`}>Red</span>';
238+
239+
it('throws an exception when resulting class is invalid', async () => {
240+
await expect(compiler({ source })).rejects.toThrow('Classname "color-" in file src/App.svelte is not valid');
241+
});
242+
});
243+
});

0 commit comments

Comments
 (0)