Skip to content

fix: apply :global() to whole selector + add strict mode #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

#### `getLocalIdent`

Expand Down
71 changes: 51 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ const pluginOptions = {
includePaths: [],
localIdentName: '[local]-[hash:base64:6]',
getLocalIdent: getLocalIdent,
strict: false,
};

const regex = {
module: /\$(style)?\.(:?[\w\d-]*)/gm,
style: /<style(\s[^]*?)?>([^]*?)<\/style>/gi,
pathUnallowed: /[<>:"/\\|?*]/g,
class: (className) => {
return new RegExp(`\\.(${className})\\b(?![-_])`, 'gm')
}
return new RegExp(`\\.(${className})\\b(?![-_])`, 'gm');
},
classSelector: (className) => {
return new RegExp(`\\S*\\.(${className})\\b(?![-_])\\S*`, 'gm');
},
};

let moduleClasses = {};
Expand All @@ -35,7 +39,7 @@ function generateName(resourcePath, styles, className) {
interpolateName({ resourcePath }, localName, { content })
.replace(/\./g, '-')
);

// replace unwanted characters from [path]
if (regex.pathUnallowed.test(interpolatedName)) {
interpolatedName = interpolatedName.replace(regex.pathUnallowed, '_');
Expand Down Expand Up @@ -72,20 +76,38 @@ const markup = async ({ content, filename }) => {
const styles = content.match(regex.style);
moduleClasses[filename] = {};

return { code: content.replace(regex.module, (match, key, className) => {
let replacement = '';
if (styles.length) {
if (regex.class(className).test(styles[0])) {
const interpolatedName = generateName(
filename,
styles[0],
className
return {
code: content.replace(regex.module, (match, key, className) => {
let replacement = '';
if (!className.length) {
throw new Error(
`Invalid class name in file ${filename}.\n`+
'This usually happens when using dynamic classes with svelte-preprocess-cssmodules.'
);
}

if (!regex.class(className).test(`.${className}`)) {
throw new Error(`Classname "${className}" in file ${filename} is not valid`);
}

if (styles.length) {
if (!regex.class(className).test(styles[0])) {
if (pluginOptions.strict) {
throw new Error(
`Classname "${className}" was not found in declared ${filename} <style>`
);
} else {
// In non-strict mode, we just remove $style classes that don't have a definition
return '';
}
}

const interpolatedName = generateName(filename, styles[0], className);

const customInterpolatedName = pluginOptions.getLocalIdent(
{
context: path.dirname(filename),
resourcePath :filename,
resourcePath: filename,
},
{
interpolatedName,
Expand All @@ -101,9 +123,9 @@ const markup = async ({ content, filename }) => {
moduleClasses[filename][className] = customInterpolatedName;
replacement = customInterpolatedName;
}
}
return replacement;
})};
return replacement;
}),
};
};

const style = async ({ content, filename }) => {
Expand All @@ -112,7 +134,7 @@ const style = async ({ content, filename }) => {
if (!moduleClasses.hasOwnProperty(filename)) {
return { code };
}

const classes = moduleClasses[filename];

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

for (const className in classes) {
code = code.replace(
regex.class(className),
() => `:global(.${classes[className]})`
regex.classSelector(className),
(match) => {
const generatedClass = match.replace(
regex.class(className),
() => `.${classes[className]}`
);

return generatedClass.indexOf(':global(') !== -1
? generatedClass
: `:global(${generatedClass})`;
}
);
}

Expand All @@ -136,5 +167,5 @@ module.exports = (options) => {
return {
markup,
style,
}
};
};
};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions test/remove.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,16 @@ test('[Shorthand] Remove unused CSS Modules from HTML attribute', async () => {

expect(output).toBe(expectedOutput);
});

describe('in strict mode', () => {
test('Throws an exception', async () => {
await expect(compiler({
source,
}, {
localIdentName: '[local]-123456',
strict: true,
})).rejects.toThrow(
'Classname \"blue\" was not found in declared src/App.svelte <style>'
);
});
});
190 changes: 188 additions & 2 deletions test/replace.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const compiler = require('./compiler.js');

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

test('Generate CSS Modules from HTML attributes, Replace CSS className', async () => {
const output = await compiler({
Expand Down Expand Up @@ -55,3 +56,188 @@ test('[Shorthand] Avoid generated class to end with a hyphen', async () => {
});
expect(output).toBe('<style>:global(.red) { color: red; }</style>\n<span class="red">Red</span>');
});

describe('combining multiple classes', () => {
const style = '<style>span.red.large:hover { font-size: 20px; } \n.red { color: red; }</style>';
const source = style + '\n<span class="$style.red $style.large">Red</span>';

const expectedStyle =
'<style>:global(span.red-123456.large-123456:hover) { font-size: 20px; } \n:global(.red-123456) { color: red; }</style>';
const expectedOutput = expectedStyle + '\n<span class="red-123456 large-123456">Red</span>';

test('Generate CSS Modules from HTML attributes, Replace CSS className', async () => {
const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123456',
}
);

expect(output).toBe(expectedOutput);
});
});

describe('Classname is part of a selector', () => {

test('CSS Modules class targetting children', async () => {
const source =
'<style>\n' +
'div.red > sup { font-size: 12px; }\n' +
'.red { color: red; }\n' +
'</style>\n' +
'<div class="$style.red">Red<sup>*</sup></div>';

const expectedOutput =
'<style>\n' +
':global(div.red-123) > sup { font-size: 12px; }\n' +
':global(.red-123) { color: red; }\n' +
'</style>\n' +
'<div class="red-123">Red<sup>*</sup></div>';

const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123',
}
);

expect(output).toBe(expectedOutput);
});

test('CSS Modules class has a parent', async () => {
const source =
'<style>\n' +
'div .semibold .red { font-size: 20px; }\n' +
'.red { color: red; }\n' +
'.semibold { font-weight: 600; }\n' +
'</style>\n' +
'<div><strong class="$style.semibold"><span class="$style.red">Red</span></strong></div>';

const expectedOutput =
'<style>\n' +
'div :global(.semibold-123) :global(.red-123) { font-size: 20px; }\n' +
':global(.red-123) { color: red; }\n' +
':global(.semibold-123) { font-weight: 600; }\n' +
'</style>\n' +
'<div><strong class="semibold-123"><span class="red-123">Red</span></strong></div>';

const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123',
}
);

expect(output).toBe(expectedOutput);
});

test('CSS Modules class has a global parent', async () => {
const source =
'<style>\n' +
':global(div) .red { font-size: 20px; }\n' +
'.red { color: red; }\n' +
'</style>\n' +
'<div><span class="$style.red">Red</span></div>';

const expectedOutput =
'<style>\n' +
':global(div) :global(.red-123) { font-size: 20px; }\n' +
':global(.red-123) { color: red; }\n' +
'</style>\n' +
'<div><span class="red-123">Red</span></div>';

const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123',
}
);

expect(output).toBe(expectedOutput);
});

test('CSS Modules class is used within a media query', async () => {
const source =
'<style>\n' +
'@media (min-width: 37.5em) {\n' +
'.red { color: red; }\n' +
'div.bold { font-weight: bold; }\n' +
'}\n' +
'</style>\n' +
'<div class="$style.bold"><span class="$style.red">Red</span></div>';

const expectedOutput =
'<style>\n' +
'@media (min-width: 37.5em) {\n' +
':global(.red-123) { color: red; }\n' +
':global(div.bold-123) { font-weight: bold; }\n' +
'}\n' +
'</style>\n' +
'<div class="bold-123"><span class="red-123">Red</span></div>';

const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123',
}
);

expect(output).toBe(expectedOutput);
});
});

describe('using dynamic classes', () => {
describe('when matched class is empty', () => {
// The parser will identify a class named ''
const source =
'<style>.red { font-size: 20px; }</style>' + '<span class={`$style.${color}`}>Red</span>';

test('throws an exception', async () => {
await expect(compiler({ source })).rejects.toThrow(
'Invalid class name in file src/App.svelte.\nThis usually happens when using dynamic classes with svelte-preprocess-cssmodules.'
);
});
});

describe('when matched class could be a valid class but does not match any style definition', () => {
// The parser will identify a class named 'color'
const source =
'<style>.colorred { font-size: 20px; }</style>' +
'<span class={`$style.color${color}`}>Red</span>';

it('in strict mode, it throw an exception', async () => {
await expect(compiler({ source }, { strict: true })).rejects.toThrow(
'Classname "color" was not found in declared src/App.svelte <style>'
);
});

// TODO: fix, this is probably not a result one would expect
it('in non-strict mode, it removes the resulting class', async () => {
const output = await compiler({ source }, { strict: false });
expect(output).toEqual(
'<style>.colorred { font-size: 20px; }</style><span class={`${color}`}>Red</span>'
);
});
});

describe('when matched class is an invalid class', () => {
// The parser will identify a class named 'color-'
const source =
'<style>.color-red { font-size: 20px; }</style>' +
'<span class={`$style.color-${color}`}>Red</span>';

it('throws an exception when resulting class is invalid', async () => {
await expect(compiler({ source })).rejects.toThrow('Classname "color-" in file src/App.svelte is not valid');
});
});
});