Skip to content

Commit

Permalink
Merge pull request #14 from movableink/require-purgeable-class-names
Browse files Browse the repository at this point in the history
Require purgeable class names
  • Loading branch information
alexlafroscia committed Mar 15, 2021
2 parents 77c712e + aa76c23 commit 4d611ba
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 4 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ This is a collection of custom [`ember-template-lint`](https://github.com/ember-

## Rules

| Name | Description |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |
| [`no-expression-like-strings`](./docs/rules/no-expression-like-strings.md) | Catch strings that you probably meant to be Handlebars expressions |
| [`no-forbidden-elements`](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-forbidden-elements.md#no-forbidden-elements) | Catch `<b>, <i>` that you probably meant to be `<strong>, <em>` expressions |
| Name | Description |
| :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |
| [`no-expression-like-strings`](./docs/rules/no-expression-like-strings.md) | Catch strings that you probably meant to be Handlebars expressions |
| [`no-forbidden-elements`][no-forbidden-elements-docs] | Catch `<b>, <i>` that you probably meant to be `<strong>, <em>` expressions |
| [`require-purgeable-class-names`](./docs/rules/require-purgeable-class-names.md) | Require class names are written such that they can be detected by PurgeCSS |

## Configurations

Expand Down Expand Up @@ -49,3 +50,5 @@ module.exports = {
],
};
```

[no-forbidden-elements-docs]: https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-forbidden-elements.md#no-forbidden-elements
21 changes: 21 additions & 0 deletions docs/rules/require-purgeable-class-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# `require-purgeable-class-names`

This lint rules helps ensure that CSS classes are not concatenated dynamically at run-time. Doing so prevents the classes from being detected by [PurgeCSS](https://purgecss.com). Writing purgeable class names is important, as it allows for the automatic removal of unused classes provided by a utility library. This pattern has become especially popular due to the widespread adopting of Tailwind CSS.

## Examples

### Forbidden

```hbs
<div class="text-white bg-{{color}}"></div>
```

### Allowed

```hbs
<div class="text-white {{if @error "bg-red" "bg-black"}}"></div>
```

## When Not To Use This

If it is impossible to know the full set of required class names in your application ahead of time, you'll want to disable this rule.
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {

rules: {
'no-expression-like-strings': require('./lib/rules/no-expression-like-strings'),
'require-purgeable-class-names': require('./lib/rules/require-purgeable-class-names'),
},

configurations: {
Expand All @@ -25,6 +26,8 @@ module.exports = {
'@movable/template-lint-plugin:avoid-deprecated-elements',
],
rules: {
'require-purgeable-class-names': true,

// Overrides to built-in "recommended" rules
'no-inline-styles': {
allowDynamicStyles: true,
Expand Down
61 changes: 61 additions & 0 deletions lib/rules/require-purgeable-class-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

const { Rule } = require('ember-template-lint');

const DOES_NOT_END_IN_WHITESPACE_REGEX = /(\S)$/;

const ERROR_MESSAGE =
'CSS class names should not be created through concatentation, since they cannot be detected for purging. Dynamically select between full class names instead.';

function isClassAttribute(node) {
return node.name === 'class' || node.name === '@className' || node.name === '@classNames';
}

function isConcatStatement(node) {
return node.type === 'ConcatStatement';
}

function isInvalidMustachePrefix(classString) {
return DOES_NOT_END_IN_WHITESPACE_REGEX.test(classString);
}

module.exports = class RequirePurgeableClassNames extends Rule {
visitor() {
return {
AttrNode(node) {
if (isClassAttribute(node) && isConcatStatement(node.value)) {
for (const part of node.value.parts) {
// Skip processing this "part" if we're not looking at a dynamic segment
if (part.type !== 'MustacheStatement') {
continue;
}

const currentPartIndex = node.value.parts.indexOf(part);
const previousPart = node.value.parts[currentPartIndex - 1];

// This dynamic segment can't be part of a concatentated class if there is no previous "part"
if (!previousPart) {
continue;
}

// If the previous "text" part doesn't end with whitespace, then the mustache statement is being used
// to dynamically concatenate a class name
if (isInvalidMustachePrefix(previousPart.chars)) {
const classList = previousPart.chars.split(/\s/);
const lastClassString = classList[classList.length - 1];

this.log({
message: ERROR_MESSAGE,
line: part.loc && part.loc.start.line,
column: part.loc && part.loc.start.column - lastClassString.length,
source: lastClassString + this.sourceForNode(part),
});
}
}
}
},
};
}
};

module.exports.ERROR_MESSAGE = ERROR_MESSAGE;
75 changes: 75 additions & 0 deletions tests/lib/rules/require-purgeable-class-names.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

const generateRuleTests = require('../../helpers/rule-test-harness');
const { ERROR_MESSAGE } = require('../../../lib/rules/require-purgeable-class-names');

generateRuleTests({
name: 'require-purgeable-class-names',
config: true,

good: [
`<div class="bg-red" />`,
`<div class="bg-red {{color}}" />`,
`<div
class="
bg-
{{color}}
"
/>`,
`<div some-other-attribute="bg-{{color}}" />`,
`<MyComponent @className="bg-red" />`,
],

bad: [
{
template: `<div class="bg-{{color}}" />`,
result: {
message: ERROR_MESSAGE,
line: 1,
column: 12,
source: 'bg-{{color}}',
},
},
{
template: `<div class="bg-{{color}} text-{{color}}" />`,
results: [
{
message: ERROR_MESSAGE,
line: 1,
column: 12,
source: 'bg-{{color}}',
},
{
message: ERROR_MESSAGE,
line: 1,
column: 25,
source: 'text-{{color}}',
},
],
},
{
template: `
<div
class="
bg-{{color}}
"
/>
`,
result: {
message: ERROR_MESSAGE,
line: 4,
column: 12,
source: 'bg-{{color}}',
},
},
{
template: `<MyComponent @className="bg-{{color}}" />`,
result: {
message: ERROR_MESSAGE,
line: 1,
column: 25,
source: 'bg-{{color}}',
},
},
],
});

0 comments on commit 4d611ba

Please sign in to comment.