Skip to content

Commit

Permalink
Merge pull request #106 from postcss/seamusleahy-implicit-components
Browse files Browse the repository at this point in the history
Add implicitComponents and implicitUtilities option
  • Loading branch information
simonsmith authored Feb 27, 2017
2 parents b3b5b14 + 4da1756 commit cefa3ee
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 13 deletions.
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,32 @@ bemLinter({
});
```

### Defining a component
### Defining a component and utilities

The plugin will only run if it finds special comments that
define a named component or a group of utilities.
The plugin will only lint the CSS if it knows the context of the CSS: is it a utility or a
component. To define the context, use the configuration options to define it based on the filename
(`css/components/*.css`) or use a special comment to define context for the CSS after it.
When defining a component, the component name is needed.

These definitions can be provided in two syntaxes: concise and verbose.
#### Define components and utilities implicitly based on their filename

When defining a component base on the filename, the name of the file (minus the extension) will be
used implicitly as the component name for linting.
The configuration option for implicit components take:

- Enable it for all files: `implicitComponents: true`
- Enable it for files that match a glob pattern: `implicitComponents: 'components/**/*.css'`
- Enable it for files that match one of multiple glob patterns: `implicitComponents: ['components/**/*.css', 'others/**/*.css']`

The CSS will implicitly be linted as utilities in files marked as such by their filename.
The configuration option for implicit utilities take:

- Enable it for files that match a glob pattern: `implicitUtilities: 'utils/*.css'`
- Enable it for files that match one of multiple glob patterns: `implicitUtilities: ['util/*.css', 'bar/**/*.css']`

#### Define components/utilities with a comment

These comment definitions can be provided in two syntaxes: concise and verbose.

- Concise definition syntax: `/** @define ComponentName */` or `/** @define utilities */`
- Verbose definition syntax: `/* postcss-bem-linter: define ComponentName */` or `/* postcss-bem-linter: define utilities */`.
Expand Down Expand Up @@ -313,6 +333,17 @@ Weak mode:
.MyComponent .other {}
```

Implicit:

```javascript
bemLinter({
preset: 'bem',
implicitComponents: 'components/**/*.css',
implicitUtilities: 'utils/*.css'
});
```


Utilities:

```css
Expand Down
27 changes: 27 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ var validateUtilities = require('./lib/validate-utilities');
var validateSelectors = require('./lib/validate-selectors');
var generateConfig = require('./lib/generate-config');
var toRegexp = require('./lib/to-regexp');
var path = require('path');
var checkImplicit = require('./lib/check-implicit');

var DEFINE_VALUE = '([-_a-zA-Z0-9]+)\\s*(?:;\\s*(weak))?';
var DEFINE_DIRECTIVE = new RegExp(
Expand Down Expand Up @@ -88,6 +90,31 @@ module.exports = postcss.plugin('postcss-bem-linter', function(primaryOptions, s

function findRanges(root) {
var ranges = [];

if (root.source && root.source.input && root.source.input.file) {
var filename = root.source.input.file;
if (checkImplicit.isImplicitUtilities(config.implicitUtilities, filename)) {
ranges.push({
defined: 'utilities',
start: 0,
weakMode: false,
});
} else if (checkImplicit.isImplicitComponent(config.implicitComponents, filename)) {
var defined = path.basename(filename).split('.')[0]

if (defined !== UTILITIES_IDENT && !toRegexp(config.componentNamePattern).test(defined)) {
result.warn(
'Invalid component name from implicit conversion from filename ' + filename
);
}
ranges.push({
defined: defined,
start: 0,
weakMode: false,
});
}
}

root.walkComments(function(comment) {
var commentStartLine = (comment.source) ? comment.source.start.line : null;
if (!commentStartLine) return;
Expand Down
52 changes: 52 additions & 0 deletions lib/check-implicit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
var path = require('path');
var minimatch = require('minimatch');

function minimatchList(path, patternList, options) {
return patternList.some(function(pattern) {
return minimatch(path, pattern, options)
});
}

/**
* Check the file matches on of the globs.
*
* @param {string} filename - The filename to test
* @param {string[]} globs - An array of glob strings to test the filename against
* @return {boolean}
*/
function checkGlob(filename, globs) {
// PostCSS turns relative paths into absolute paths
filename = path.relative(process.cwd(), filename);
return minimatchList(filename, globs);
}

/**
* @param {string[]|boolean} implicitComponentsConfig - The configuration value implicitComponents
* @param {string} filename - The filename of the CSS file being checked
* @returns {boolean}
*/
function isImplicitComponent(implicitComponentsConfig, filename) {
if (Array.isArray(implicitComponentsConfig)) {
return checkGlob(filename, implicitComponentsConfig);
}

return Boolean(implicitComponentsConfig);
}

/**
* @param {string[]|boolean} implicitUtilitiesConfig - The configuration value implicitUtilities
* @param {string} filename - The filename of the CSS file being checked
* @return {boolean}
*/
function isImplicitUtilities(implicitUtilitiesConfig, filename) {
if (Array.isArray(implicitUtilitiesConfig)) {
return checkGlob(filename, implicitUtilitiesConfig);
}

return false;
}

module.exports = {
isImplicitComponent: isImplicitComponent,
isImplicitUtilities: isImplicitUtilities,
};
25 changes: 24 additions & 1 deletion lib/generate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,32 @@ module.exports = function(primaryOptions, secondaryOptions) {
presetOptions = primaryOptions.presetOptions;
}

var implicitComponents =
getImplicitDefineValue('implicitComponents', primaryOptions, secondaryOptions);
var implicitUtilities =
getImplicitDefineValue('implicitUtilities', primaryOptions, secondaryOptions);

return {
patterns: patterns,
presetOptions: presetOptions,
componentNamePattern: patterns.componentName || /^[-_a-zA-Z0-9]+$/,
}
implicitComponents: implicitComponents,
implicitUtilities: implicitUtilities,
};
}

function getImplicitDefineValue(key, primaryOptions, secondaryOptions) {
var implicitValue = false;

if (secondaryOptions && secondaryOptions[key] !== undefined) {
implicitValue = secondaryOptions[key];
} else if (primaryOptions && primaryOptions[key]) {
implicitValue = primaryOptions[key]
}

if (typeof implicitValue === 'string') {
implicitValue = [implicitValue];
}

return implicitValue;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"lib"
],
"dependencies": {
"minimatch": "^3.0.3",
"postcss": "^5.0.0",
"postcss-resolve-nested-selector": "^0.1.1"
},
Expand Down
38 changes: 38 additions & 0 deletions test/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,41 @@ describe('`@define` notation', function() {
});
});
});

describe('Implicit @define', function() {
describe('based on filename', function() {
var filename = process.cwd() + '/css/c/implicit-component.css';
var css = '.implicit-component-broken {}';

it('must complain when true', function() {
util.assertSingleFailure(css, {implicitComponents: true, preset: 'bem'}, null, filename);
});

it('must complain when string', function() {
util.assertSingleFailure(css, {implicitComponents: 'css/**/*.css', preset: 'bem'}, null, filename);
});

it('must complain when array', function() {
util.assertSingleFailure(css, {implicitComponents: ['css/c/*.css'], preset: 'bem'}, null, filename);
});

it('must complain about component name', function() {
util.assertSingleFailure(
css,
{
implicitComponents: true,
componentName: /[A-Z]+/,
componentSelectors: function() { return /.*/; },
},
null,
filename
);
});
});

describe('utilities', function() {
it('must complain', function() {
util.assertSingleFailure('.foo-bar {}', {implicitUtilities: ['utils/*.css'], preset: 'suit'}, null, 'utils/foo-bar.css');
});
});
});
16 changes: 8 additions & 8 deletions test/test-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@ var linter = require('..');
var fs = require('fs');
var postcss = require('postcss');

function getPostcssResult(css, primary, secondary) {
function getPostcssResult(css, primary, secondary, filename) {
var result = postcss()
.use(linter(primary, secondary))
.process(css);
.process(css, {from: filename});
return result;
}

function fixture(name) {
return fs.readFileSync(path.join(__dirname, 'fixtures', name + '.css'), 'utf8').trim();
}

function assertSuccess(css, primary, secondary) {
var result = getPostcssResult(css, primary, secondary);
function assertSuccess(css, primary, secondary, filename) {
var result = getPostcssResult(css, primary, secondary, filename);
assert.equal(result.warnings().length, 0);
}

function assertSingleFailure(css, primary, secondary) {
var result = getPostcssResult(css, primary, secondary);
function assertSingleFailure(css, primary, secondary, filename) {
var result = getPostcssResult(css, primary, secondary, filename);
assert.equal(result.warnings().length, 1);
}

function assertFailure(css, primary, secondary) {
var result = getPostcssResult(css, primary, secondary);
function assertFailure(css, primary, secondary, filename) {
var result = getPostcssResult(css, primary, secondary, filename);
assert.ok(result.warnings().length > 0);
}

Expand Down

0 comments on commit cefa3ee

Please sign in to comment.