├── .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 | --------------------------------------------------------------------------------