Skip to content

Commit ef7d3ac

Browse files
committed
fix: apply :global() to whole selector + add strict mode
1 parent ae51f4d commit ef7d3ac

File tree

5 files changed

+150
-23
lines changed

5 files changed

+150
-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: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ 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+
},
1819
};
1920

2021
let moduleClasses = {};
@@ -35,7 +36,7 @@ function generateName(resourcePath, styles, className) {
3536
interpolateName({ resourcePath }, localName, { content })
3637
.replace(/\./g, '-')
3738
);
38-
39+
3940
// replace unwanted characters from [path]
4041
if (regex.pathUnallowed.test(interpolatedName)) {
4142
interpolatedName = interpolatedName.replace(regex.pathUnallowed, '_');
@@ -72,20 +73,38 @@ const markup = async ({ content, filename }) => {
7273
const styles = content.match(regex.style);
7374
moduleClasses[filename] = {};
7475

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

85104
const customInterpolatedName = pluginOptions.getLocalIdent(
86105
{
87106
context: path.dirname(filename),
88-
resourcePath :filename,
107+
resourcePath: filename,
89108
},
90109
{
91110
interpolatedName,
@@ -101,18 +120,19 @@ const markup = async ({ content, filename }) => {
101120
moduleClasses[filename][className] = customInterpolatedName;
102121
replacement = customInterpolatedName;
103122
}
104-
}
105-
return replacement;
106-
})};
123+
return replacement;
124+
}),
125+
};
107126
};
108127

128+
const GLOBALIZE_PLACEHOLDER = '__to_globalize__';
109129
const style = async ({ content, filename }) => {
110130
let code = content;
111131

112132
if (!moduleClasses.hasOwnProperty(filename)) {
113133
return { code };
114134
}
115-
135+
116136
const classes = moduleClasses[filename];
117137

118138
if (Object.keys(classes).length === 0) {
@@ -122,11 +142,35 @@ const style = async ({ content, filename }) => {
122142
for (const className in classes) {
123143
code = code.replace(
124144
regex.class(className),
125-
() => `:global(.${classes[className]})`
145+
() => `.${GLOBALIZE_PLACEHOLDER}${classes[className]}`
126146
);
127147
}
128148

129-
return { code };
149+
let codeOutput = '';
150+
let cssRules = code.split('}');
151+
152+
// Remove last element of the split. It should be the empty string that comes after the last '}'.
153+
const lastChunk = cssRules.pop();
154+
155+
// We wrap all css selector containing a scoped CSS class in :global() svelte css statement
156+
for (const cssRule of cssRules) {
157+
let [selector, rule] = cssRule.split('{');
158+
if (selector.includes(GLOBALIZE_PLACEHOLDER)) {
159+
const selectorTrimmed = selector.trim();
160+
selector = selector.replace(
161+
selectorTrimmed,
162+
`:global(${selectorTrimmed.replace(
163+
new RegExp(GLOBALIZE_PLACEHOLDER, 'g'),
164+
''
165+
)})`
166+
);
167+
}
168+
codeOutput += `${selector}{${rule}}`;
169+
}
170+
171+
codeOutput += lastChunk;
172+
173+
return { code: codeOutput };
130174
};
131175

132176
module.exports = (options) => {
@@ -136,5 +180,5 @@ module.exports = (options) => {
136180
return {
137181
markup,
138182
style,
139-
}
140-
};
183+
};
184+
};

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: 71 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,71 @@ 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('using dynamic classes', () => {
83+
describe('when matched class is empty', () => {
84+
// The parser will identify a class named ''
85+
const source =
86+
'<style>.red { font-size: 20px; }</style>' + '<span class={`$style.${color}`}>Red</span>';
87+
88+
test('throws an exception', async () => {
89+
await expect(compiler({ source })).rejects.toThrow(
90+
'Invalid class name in file src/App.svelte.\nThis usually happens when using dynamic classes with svelte-preprocess-cssmodules.'
91+
);
92+
});
93+
});
94+
95+
describe('when matched class could be a valid class but does not match any style definition', () => {
96+
// The parser will identify a class named 'color'
97+
const source =
98+
'<style>.colorred { font-size: 20px; }</style>' +
99+
'<span class={`$style.color${color}`}>Red</span>';
100+
101+
it('in strict mode, it throw an exception', async () => {
102+
await expect(compiler({ source }, { strict: true })).rejects.toThrow(
103+
'Classname "color" was not found in declared src/App.svelte <style>'
104+
);
105+
});
106+
107+
// TODO: fix, this is probably not a result one would expect
108+
it('in non-strict mode, it removes the resulting class', async () => {
109+
const output = await compiler({ source }, { strict: false });
110+
expect(output).toEqual(
111+
'<style>.colorred { font-size: 20px; }</style><span class={`${color}`}>Red</span>'
112+
);
113+
});
114+
});
115+
116+
describe('when matched class is an invalid class', () => {
117+
// The parser will identify a class named 'color-'
118+
const source =
119+
'<style>.color-red { font-size: 20px; }</style>' +
120+
'<span class={`$style.color-${color}`}>Red</span>';
121+
122+
it('throws an exception when resulting class is invalid', async () => {
123+
await expect(compiler({ source })).rejects.toThrow('Classname "color-" in file src/App.svelte is not valid');
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)