├── .gitignore
├── README.md
├── demo
└── index.js
├── karma.conf.js
├── package-npm.js
├── package.json
├── spec
├── blint-react.spec.js
└── test_index.js
├── src
└── blint-react.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # blint
2 |
3 | Blint is a pair of linting tools for the [css-bliss](https://github.com/gilbox/css-bliss) style
4 | guide: [blint-react](#blint-react) and [scss-lint-bliss](#scss-lint-bliss).
5 | They can be used individually or together. They complement each other very well
6 | since one enforces a number of rules that the other cannot, and vice-versa.
7 | `blint-react` focuses on linting markup, while `scss-lint-bliss` focuses on the styles.
8 | The goal is to guide the developer in building scalable CSS by enforcing
9 | the rules of css-bliss. This README file documents both linting tools, although only `blint-react`
10 | source code actually lives in this repo.
11 |
12 | -------------------------------------------------------------------
13 |
14 |
15 | ## blint-react
16 |
17 | `blint-react` works by monkey-patching `React#createElement` and linting the `className`
18 | prop of every single component instantiated by the application, in real-time. If
19 | a `className` fails the linter, an error is thrown which is intended to bring the application
20 | to it's knees, forcing the developer to fix the mistake before doing anything else.
21 |
22 | ### installation
23 |
24 | Install using npm:
25 |
26 | npm install blint-react
27 |
28 | Once installed, pull in `blint-react` as early as possible via:
29 |
30 | require('blint-react');
31 |
32 | That's all there is to is. Now sit back and worry less about reviewing bad css code.
33 |
34 | ### advantages
35 |
36 | `blint-react` has a full suite of [unit tests](https://github.com/gilbox/blint/blob/master/spec/blint-react.spec.js).
37 | In a properly configured build pipeline, `blint-react` will not compile into the minified bundle,
38 | adding zero overhead in production.
39 |
40 | ### error messages
41 |
42 | All linting error messages include a detailed explanation, an accurate
43 | stack trace pointing you right to the problem area, and often
44 | a link to css-bliss documentation about the rule. Sometimes you get a suggestion, for example an
45 | error related to a Module Modifier looks like:
46 |
47 | > In `div[className="Foo--bar"]` there is a CSS Module Modifier,
48 | > but no CSS Module specified. If you are passing a Module Modifier into
49 | > another element that will always combine it with a CSS Module, use a custom prop like
50 | > classModifier instead
51 |
52 | Note that this is an especially opinionated rule that assumes if you've created a
53 | `Foo` component which renders `
Hello World
` and would like to
54 | subclass an instance of the component with a module modifier you would do so with
55 | a custom prop (named `classModifer` for example), and not by passing the modifier into
56 | the `className` prop.
57 |
58 | In other words, instead of
59 |
60 |
61 |
62 | We must do:
63 |
64 |
65 |
66 | Where Foo's render function looks like:
67 |
68 | render() {
69 | return (
70 |
71 | Hello World
72 |
73 | )
74 | }
75 |
76 | ### rules
77 |
78 | - Enforces the Module class naming scheme. Any class beginning with an uppercase
79 | letter is assumed to be following the css-bliss Module naming rules. This includes
80 | so-called Module *Element*, *Element Modifier*, and *Module Modifier* classes.
81 |
82 | - Ensures that a DOM element has classes from as most one CSS Module.
83 |
84 | - A DOM element may have an Element class or a Module class, but not both.
85 |
86 | - A DOM element may have an Element class or a Module Modifier class, but not both.
87 |
88 | - A DOM element may have an Element Modifier class or a Module class, but not both.
89 |
90 | - If a DOM element has a Module Modifier class, it must have a Module class.
91 |
92 | - If a DOM element has an Element Modifier class, it must have an Element class.
93 |
94 | ### options
95 |
96 | There are currently no configurable options.
97 |
98 | -------------------------------------------------------------------
99 |
100 | ## scss-lint-bliss
101 |
102 | The [scss-lint-bliss](https://github.com/gilbox/scss-lint/tree/bliss) tool
103 | is a [fork](https://github.com/gilbox/scss-lint/tree/bliss)
104 | of the popular [scss-lint](https://github.com/brigade/scss-lint),
105 | with the addition of rules specific to css-bliss modules. It is *not* the goal of this project
106 | to maintain a fork. Instead, it will be converted to a plugin
107 | [when scss-lint's plugin system is available](https://github.com/brigade/scss-lint/issues/440).
108 |
109 | To install scss-lint-bliss simply install the gem:
110 |
111 | gem install scss-lint-bliss
112 |
113 | Which will install the global binary `scss-lint`. Note that this is the same binary filename
114 | used by the `scss-lint` gem. Then to lint some files do:
115 |
116 | scss-lint ./path/to/css/
117 |
118 | ### configuration
119 |
120 | It might interest you to [read about scss-lint configuration](https://github.com/gilbox/scss-lint#configuration).
121 | scss-lint-bliss adds the following options and defaults:
122 |
123 | Bliss::Module:
124 | enabled: true
125 | severity: error
126 | module_file_pattern: !ruby/regexp '/[\/\\]_?([A-Z][a-zA-Z0-9]+)\.scss/'
127 | allow_id_selector_in_module: false
128 | allow_attribute_selector_in_module: true
129 | allow_element_selector_in_module: true
130 |
131 | allow_module_margin: false
132 | allow_module_width: false
133 |
134 | allow_utility_classes_in_module: false
135 | ignored_utility_class_prefixes: ['is', 'ie']
136 |
137 | allow_utility_direct_styling: false
138 |
139 | ### `Bliss::Module:` linting behavior
140 |
141 | - only lints files matching the `module_file_pattern` regex pattern
142 |
143 | - does not allow any id selector in a Module unless `allow_id_selector_in_module` is `true`
144 |
145 | - does not allow any attribute selector in a Module unless `allow_attribute_selector_in_module` is `true`
146 |
147 | - does not allow any element selector in a Module unless `allow_element_selector_in_module` is `true`
148 |
149 | - does not allow a module to have a `margin` property unless the `allow_module_margin` option is `true` (todo: add support for `auto`)
150 |
151 | - does not allow a module to have a `width` property unless the `allow_module_width` option is `true`
152 |
153 | - Does not allow any utility class in a module unless it is included in the `ignored_utility_class_prefixes`
154 | list or the `allow_utility_classes_in_module` option is `true`
155 |
156 | - Does not allow a rule to end with a utility class as a descended selector
157 | (`.Foo .isOpen` is bad but `.Foo.isOpen` is good) unless `allow_utility_direct_styling` is `true`
158 |
159 | ### Reliability
160 |
161 | scss-lint-bliss is [fully unit tested](https://github.com/gilbox/scss-lint/blob/bliss/spec/scss_lint/linter/bliss/module_spec.rb)
162 | but could still be circumvented by the developer with ugly code.
163 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | const blint = require('../src/blint');
2 |
3 | console.log('hi');
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpackConfig = require('./webpack.config');
2 |
3 | module.exports = function(config) {
4 | var settings = {
5 |
6 | // base path that will be used to resolve all patterns (eg. files, exclude)
7 | //basePath: 'app/assets/javascripts/',
8 | basePath: '',
9 |
10 | frameworks: ['jasmine'],
11 |
12 | // list of files / patterns to load in the browser
13 | files: [
14 | 'spec/test_index.js'
15 | ],
16 |
17 | // list of files to exclude
18 | exclude: [
19 | '**/bundle.js',
20 | '**/vendor.js'
21 | ],
22 |
23 | // preprocess matching files before serving them to the browser
24 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
25 | preprocessors: {
26 | 'spec/test_index.js': ['webpack', 'sourcemap']
27 | },
28 |
29 | // test results reporter to use
30 | // possible values: 'dots', 'progress'
31 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
32 | reporters: ['spec'],
33 |
34 |
35 | // web server port
36 | port: 9876,
37 |
38 |
39 | // enable / disable colors in the output (reporters and logs)
40 | colors: true,
41 |
42 |
43 | // level of logging
44 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
45 | logLevel: config.LOG_INFO,
46 |
47 |
48 | // enable / disable watching file and executing tests whenever any file changes
49 | autoWatch: true,
50 |
51 |
52 | // start these browsers
53 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
54 | browsers: ['Chrome'],
55 |
56 |
57 | // Continuous Integration mode
58 | // if true, Karma captures browsers, runs the tests and exits
59 | singleRun: false,
60 |
61 |
62 | plugins: [
63 | require('karma-jasmine'),
64 | require('karma-chrome-launcher'),
65 | require('karma-phantomjs-launcher'),
66 | require('karma-webpack'),
67 | require('karma-sourcemap-loader'),
68 | require('karma-spec-reporter')
69 | ],
70 |
71 | // Specific required configurations for webpack
72 | webpack: {
73 | devtool: 'inline-source-map',
74 | resolve: webpackConfig.resolve,
75 | module: {
76 | loaders: [
77 | {test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel-loader']}
78 | ]
79 | },
80 | plugins: [
81 | ]
82 | }
83 |
84 | };
85 |
86 | config.set(settings);
87 | return settings;
88 | };
89 |
--------------------------------------------------------------------------------
/package-npm.js:
--------------------------------------------------------------------------------
1 | var p = require('./package');
2 |
3 | p.name='blint-react';
4 | p.main='src/blint-react.js';
5 | p.scripts=p.devDependencies=undefined;
6 |
7 | module.exports = p;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blint",
3 | "version": "1.0.0",
4 | "description": "css-bliss linter",
5 | "main": "src/blint-react.js",
6 | "repository": "https://github.com/gilbox/blint.git",
7 | "scripts": {
8 | "watch": "webpack --progress --colors --watch",
9 | "build-npm": "rm -rf build/npm && babel -d build/npm/ ./src/blint-react.js && cp README.md build/npm && node -p 'p=require(\"./package-npm\");JSON.stringify(p,null,2)' > build/npm/package.json",
10 | "chrome": "./node_modules/karma/bin/karma start",
11 | "chrome1": "./node_modules/karma/bin/karma start --single-run",
12 | "test": "npm run chrome1",
13 | "publish": "npm test && npm publish build/npm/"
14 |
15 | },
16 | "keywords": [
17 | "css",
18 | "scss",
19 | "lint",
20 | "linter",
21 | "css-bliss",
22 | "react"
23 | ],
24 | "author": "Gil Birman (http://gilbox.me/)",
25 | "license": "MIT",
26 | "devDependencies": {
27 | "babel": "^4.4.5",
28 | "babel-core": "^4.4.6",
29 | "babel-loader": "^4.0.0",
30 | "bundle-loader": "^0.5.2",
31 | "jasmine-core": "^2.2.0",
32 | "json-loader": "^0.5.1",
33 | "karma": "^0.12.31",
34 | "karma-chrome-launcher": "^0.1.8",
35 | "karma-jasmine": "^0.3.5",
36 | "karma-phantomjs-launcher": "^0.1.4",
37 | "karma-sourcemap-loader": "^0.3.4",
38 | "karma-spec-reporter": "0.0.19",
39 | "karma-webpack": "^1.5.0",
40 | "react": "^0.13.2"
41 | },
42 | "dependencies": {
43 | },
44 | "peerDependencies": {
45 | "react": ">=0.12.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/spec/blint-react.spec.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const originalCreateElement = React.createElement;
3 |
4 | const lint = require('../src/blint-react');
5 |
6 | describe('blint-react', () => {
7 | beforeEach(() => {
8 | jasmine.addMatchers({
9 | toPassBlint() {
10 | return {
11 | compare: function(className){
12 | var pass = false;
13 | var stack = '';
14 | try {
15 | lint(className);
16 | pass = true
17 | } catch(e) { stack = e.stack }
18 |
19 | var result = { pass };
20 | if(pass) {
21 | result.message = `Expected className '${className}' to pass the blint linter. ${stack}`;
22 | } else {
23 | result.message = `Expected className '${className}' to pass the blint linter. ${stack}`;
24 | }
25 | return result;
26 | }
27 | }
28 | }
29 | })
30 | });
31 |
32 | it('should monkey-patch React.createElement', () => {
33 | expect(React.createElement).not.toBe(originalCreateElement);
34 | });
35 |
36 | it('should fail linter when there are classes from more than one Module', () => {
37 | expect('FooModule FooOtherModule').not.toPassBlint();
38 | expect('FooModule BamBam-foo').not.toPassBlint();
39 | expect('FooModule Grammy-doo-mooMoo').not.toPassBlint();
40 | expect('One-uno Two-dos').not.toPassBlint();
41 | });
42 |
43 | it('should fail linter when there are multiple elements', () => {
44 | expect('Foo-one Foo-two').not.toPassBlint();
45 | });
46 |
47 | it('should fail linter when there is a module and an element', () => {
48 | expect('Foo Foo-two').not.toPassBlint();
49 | });
50 |
51 | it('should fail linter when there is a module modifier and an element', () => {
52 | expect('Foo--bar Foo-two').not.toPassBlint();
53 | });
54 |
55 | it('should fail linter when there is an element modifier without an element', () => {
56 | expect('Foo-bar--baz').not.toPassBlint();
57 | });
58 |
59 | it('should fail linter when there is an element modifier and a module modifier', () => {
60 | expect('Foo--bar Foo-bar--baz').not.toPassBlint();
61 | });
62 |
63 | it('should fail linter when there is a module modifier but no module', () => {
64 | expect('Foo--bar').not.toPassBlint();
65 | });
66 |
67 | it('should fail linter when there is an element modifier but no element', () => {
68 | expect('Foo-bar--baz').not.toPassBlint();
69 | });
70 |
71 | it('should fail linter when class naming is incorrect', () => {
72 | expect('Foo-One').not.toPassBlint();
73 | expect('Foo-one--Two').not.toPassBlint();
74 | expect('Foo-one-two').not.toPassBlint();
75 | expect('FooBar-bar--Baz').not.toPassBlint();
76 | expect('FooBar--Baz').not.toPassBlint();
77 | });
78 |
79 | it('should not fail linter when class naming is correct', () => {
80 | expect('Z').toPassBlint();
81 | expect('Z-a Z-a--b').toPassBlint();
82 | expect('Z1-a2 Z1-a2--b3').toPassBlint();
83 | });
84 |
85 | it('should not fail linter with valid input', () => {
86 | expect('Foo-one').toPassBlint();
87 | expect('Foo-one Foo-one--twoThree').toPassBlint();
88 | expect('Foo Foo--modifierOk').toPassBlint();
89 | });
90 | });
--------------------------------------------------------------------------------
/spec/test_index.js:
--------------------------------------------------------------------------------
1 | // This is to load all specs into one bundle
2 | var testsContext = require.context('./', true, /.spec.jsx?$/);
3 | testsContext.keys().forEach(testsContext);
4 |
--------------------------------------------------------------------------------
/src/blint-react.js:
--------------------------------------------------------------------------------
1 | // https://github.com/gilbox/blint
2 | if ("production" !== process.env.NODE_ENV) { // don't compile in production
3 |
4 | const React = require('react');
5 | const invariant = require('react/lib/invariant');
6 | const MODULE = 0;
7 | const MODULE_MODIFIER = 1;
8 | const ELEMENT = 2;
9 | const ELEMENT_MODIFIER = 3;
10 | const createElement = React.createElement;
11 |
12 | /**
13 | * Try to detect and throw an error if you break css-bliss rules.
14 | *
15 | * @param className {string} original className passed into React.createElement
16 | * @param elementType {string} used for logging error messages about an element
17 | * @returns undefined
18 | */
19 | const lint = function(className, elementType) {
20 | const classes = className.split(' ');
21 | const types = [0, 0, 0, 0];
22 | var moduleName;
23 |
24 | classes.forEach(c => {
25 | if (c.match(/^[A-Z]/)) {
26 | if (moduleName) {
27 | invariant(c.indexOf(moduleName) === 0,
28 | `In ${elementType}[className="${className}"] there are classes from more than one CSS Module: ${c.split('-')[0]} and ${moduleName}. ` +
29 | 'A DOM element should not have more than one Module because this breaks encapsulation. ' +
30 | 'Use a wrapping DOM element instead to compose CSS Modules, or refactor and use a Modifier. ' +
31 | 'https://github.com/gilbox/css-bliss#encapsulation');
32 | } else {
33 | moduleName = c.split('-')[0];
34 | }
35 |
36 | // This regex matches all allowable Module classes
37 | const matches = c.match(/^([A-Z]\w*)(\-([a-z]\w*))?(\-\-([a-z]\w*))?$/);
38 |
39 | invariant(matches, `In ${elementType}[className="${className}"] the class ${c} starts with an uppercase letter, indicating ` +
40 | 'that it belongs to a module. However, it does not conform to the css-bliss naming ' +
41 | 'conventions for modules https://github.com/gilbox/css-bliss#naming');
42 |
43 | const type = matches[5] && matches[3] ? ELEMENT_MODIFIER
44 | : matches[5] ? MODULE_MODIFIER
45 | : matches[3] ? ELEMENT
46 | : MODULE;
47 | types[type]++;
48 | }
49 | });
50 |
51 | invariant(!(types[MODULE] && types[ELEMENT]),
52 | `In ${elementType}[className="${className}"] there is a CSS Module and ` +
53 | 'an CSS Element. A DOM element ' +
54 | 'may have one or the other but not both. ' +
55 | 'https://github.com/gilbox/css-bliss/blob/master/common-mistakes.md#module-and-element-classes-applied-to-same-tag');
56 |
57 | invariant(!(types[MODULE_MODIFIER] && types[ELEMENT]),
58 | `In ${elementType}[className="${className}"] there is a CSS Module Modifier and an CSS Element. A DOM element ` +
59 | 'may have one or the other but not both.');
60 |
61 | invariant(!(types[ELEMENT_MODIFIER] && types[MODULE]),
62 | `In ${elementType}[className="${className}"] there is a CSS Module and an CSS Element Modifier. A DOM element ` +
63 | 'may have one or the other but not both.');
64 |
65 | invariant(!(types[ELEMENT_MODIFIER] && types[MODULE_MODIFIER]),
66 | `In ${elementType}[className="${className}"] there is a CSS Module Modifier ` +
67 | 'and an CSS Element Modifier. A DOM element ' +
68 | 'may have one or the other but not both.');
69 |
70 | invariant(types[ELEMENT] <= 1,
71 | `In ${elementType}[className="${className}"] there are multiple CSS Elements. ` +
72 | 'A DOM element may have at most one CSS Element.');
73 |
74 | invariant(!(types[MODULE_MODIFIER] && !types[MODULE]),
75 | `In ${elementType}[className="${className}"] there is a CSS Module Modifier, ` +
76 | 'but no CSS Module specified. If you are passing a Module Modifier into ' +
77 | 'another element that will always combine it with a CSS Module, use a custom prop like ' +
78 | 'classModifier instead');
79 |
80 | invariant(!(types[ELEMENT_MODIFIER] && !types[ELEMENT]),
81 | `In ${elementType}[className="${className}"] there is a CSS Element Modifier, ` +
82 | 'but no CSS Element specified. If you are passing an Element Modifier into ' +
83 | 'another element that will always combine it with a CSS Element, use a custom prop like ' +
84 | 'classModifier instead');
85 | };
86 |
87 | // monkeypatch React
88 | React.createElement = function(el, opts) {
89 | if (opts && opts.className) {
90 | const elementType = (typeof el === 'string') ? el : (el.displayName || '');
91 | lint(opts.className, elementType);
92 | }
93 | return createElement.apply(this, Array.prototype.slice.call(arguments));
94 | };
95 |
96 | module.exports = lint;
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | entry: {
6 | app: ['./demo/index.js']
7 | },
8 | output: {
9 | filename: "./build/demo/bundle.js"
10 | },
11 | //resolve: {
12 | // root: [
13 | // path.join(__dirname, "./src/")
14 | // ],
15 | // extensions: ['', '.js']
16 | //},
17 | module: {
18 | loaders: [
19 | {
20 | test: /\.jsx?$/,
21 | exclude: /node_modules/,
22 | loaders: ['babel-loader']
23 | },
24 | {test: /\.json$/, loader: "json"}
25 | ]
26 | },
27 | plugins: []
28 | };
29 |
--------------------------------------------------------------------------------