diff --git a/README.md b/README.md index 09d5122..96b03b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # `stylelint-processor-styled-components` -Lint the CSS in your [styled components](https://github.com/styled-components/styled-components) with [stylelint](http://stylelint.io/)! +Lint your [styled components](https://github.com/styled-components/styled-components) with [stylelint](http://stylelint.io/)! [![Build Status][build-badge]][build-url] [![Coverage Status][coverage-badge]][coverage-url] @@ -9,8 +9,6 @@ Lint the CSS in your [styled components](https://github.com/styled-components/st ![Video of project in use](http://imgur.com/br9zdHb.gif) -**NOTE**: This is currently in beta. We're getting close to being fully production ready, and have now covered most normal use cases. There are still some edge cases though, but we are working hard on getting them fixed for a v1.0 release in the near future. Please keep using it and submit bug reports! - ## Usage ### Installation @@ -18,11 +16,16 @@ Lint the CSS in your [styled components](https://github.com/styled-components/st You need: - `stylelint` (duh) -- This processor (to add `styled-components` support) -- The standard config for stylelint (or any config you like) +- This processor, to extract styles from `styled-components` +- The [`stylelint-config-styled-components-processor`](https://github.com/styled-components/stylelint-config-styled-components-processor) to disable stylelint rules that clash with `styled-components` +- Your favorite `stylelint` config! (for example [`stylelint-config-standard`](https://github.com/stylelint/stylelint-config-standard)) ``` -npm install --save-dev stylelint-processor-styled-components stylelint stylelint-config-standard +(npm install --save-dev + stylelint + stylelint-processor-styled-components + stylelint-config-styled-components-processor + stylelint-config-standard) ``` ### Setup @@ -32,82 +35,162 @@ Add a `.stylelintrc` file to the root of your project: ```JSON { "processors": ["stylelint-processor-styled-components"], - "extends": "stylelint-config-standard", + "extends": [ + "stylelint-config-standard", + "stylelint-config-styled-components-processor" + ], "syntax": "scss" } ``` -> Setting the `syntax` to `scss` is needed for nesting and interpolation support! - -Then you need to actually run `stylelint`. +> **NOTE:** Setting the `syntax` to `scss` is needed for nesting and interpolation support! -Add a `lint:css` script to your `package.json`. This script will run `stylelint` with a path to all of your files containing `styled-components` code: +Then you need to run `stylelint`. Add a `lint:css` script to your `package.json` which runs `stylelint` with a glob to all of your styled components: ```JSON { "scripts": { - "lint:css": "stylelint './components/**/*.js'" + "lint:css": "stylelint './src/**/*.js'" } } ``` -> **NOTE:** Don't worry about passing in files that don't contain any styled-components code – we take care of that. +> **NOTE:** The processor ignores javascript files that don't contain any `styled-components`, so don't worry about being too broad as long as you restrict it to javascript (or TypeScript). -Now you can lint your CSS by running this script! 🎉 +Now you can lint your CSS by running the script! 🎉 ``` npm run lint:css ``` -### Webpack +#### Webpack -For use with Webpack you can use the [`stylelint-custom-processor-loader`](https://github.com/emilgoldsmith/stylelint-custom-processor-loader). +If you want to lint on build, rather than as a separate command, you can use the [`stylelint-custom-processor-loader`](https://github.com/emilgoldsmith/stylelint-custom-processor-loader) for webpack. -### Syntax notes -#### Turning rules off from within your CSS +### Processor specific stylelint rules + +When using this processor a couple of stylelint rules throw errors that cannot be prevented, like [`no-empty-source`](https://stylelint.io/user-guide/rules/no-empty-source) or [`no-missing-end-of-source-newline`](https://stylelint.io/user-guide/rules/no-missing-end-of-source-newline). There's also a couple rules which we need to enforce, like [`no-vendor-prefix` rules](https://stylelint.io/user-guide/rules/property-no-vendor-prefix). (`styled-components` automatically vendor prefixes your code, so you don't need to do it manually) + +The [`stylelint-config-styled-components-processor`](https://github.com/styled-components/stylelint-config-styled-components-processor) will automatically disable rules that cause conflicts. -Turning off rules with `stylelint-disable`-like comments (see the [stylelint documentation](https://stylelint.io/user-guide/configuration/#turning-rules-off-from-within-your-css) for all allowed syntax) is fully supported inside and outside of the tagged template literals, do note though that what actually happens behind the scene is that all `stylelint-(disable|enable)` comments are moved into the compiled css that is actually linted, so something like this: +> **NOTE:** You can override rules defined in shared configs in your custom `.stylelintrc`. +### Interpolation tagging + +Sometimes `stylelint` can throw an error (e.g. `CssSyntaxError`) even though nothing is wrong with your CSS. This is often due to an interpolation, more specifically the fact that the processor doesn't know what you're interpolating. + +A simplified example: ```js -/* stylelint-disable */ -import React from 'react'; -import styled from 'styled-components'; +const something = 'background'; -const Wrapper = styled.div` - /* stylelint-disable */ - background-color: red; -`; +const Button = styled.div` + ${something}: papayawhip; +` ``` -or even + +When you have interpolations in your styles the processor can't know what they are, so it makes a good guess and replaces them with a syntactically equivalent placeholder value. Since `stylelint` is not a code flow analysis tool this doesn't cover all edge cases and the processor will get it wrong every now and then. + +Interpolation tagging allows you to tell the processor what an interpolation is in case it guesses wrong; it can then replace the interpolation with a syntactically correct value based on your tag. + +For example: + ```js -/* stylelint-disable */ -import React from 'react'; -import styled from 'styled-components'; +const something = 'background'; + +const Button = styled.div` + // Tell the processor that "something" is a property + ${/* sc-prop */ something}: papayawhip; +` +``` + +Now the processor knows that the `something` interpolation is a property, and it can replace the interpolation with a property for linting. + +To tag an interpolation add a comment at either the start or the end of the interpolation. (`${/* sc-tag */ foo}` or `${bar /* sc-tag */}`) Tags start with `sc-` and, if specified, a tag overrides the processors guess about what the interpolation is. + +#### Tags +The full list of supported tags: + +- `sc-block` +- `sc-selector` +- `sc-declaration` +- `sc-property` +- `sc-value` + +> **NOTE:** If you are in doubt of the vocabulary you can refer to [this CSS vocabulary list](http://apps.workflower.fi/vocabs/css/en) with examples. + +For example, when you interpolate another styled component, what you really interpolate is its unique selector. Since the processor doesn't know that, you can tell it to replace it with a selector when linting: + +```js const Wrapper = styled.div` - /* stylelint-disable-next-line */ - background-color: red; + ${/* sc-selector */ Button} { + color: red; + } `; ``` -would throw a stylelint error similar to `All rules have already been disabled (CssSyntaxError)`. +You can also use shorthand tags to avoid cluttering the code. For example: + +```js +const Wrapper = styled.div` + ${/* sc-sel */ Button} { + color: red; + } +`; +``` -#### Interpolation linting +##### `sc-custom` -We do not currently support linting interpolations as it could be a big performance hit though we aspire to have at least partial support in the future. You can of course lint your own mixins in their separate files, but it won't be linted in context, the implementation currently just inserts relevant dummy values. This, we are afraid, means you won't be able to lint cases such as `declaration-block-no-duplicate-properties` etc. and won't be able to lint outside mixins such as [polished](https://github.com/styled-components/polished). +**`sc-custom` is meant to be used as a last resort escape hatch. Prefer to use the standard tags if possible!** -#### Template literal style and indentation +On top of the above standard tags the processor also has the `sc-custom` tag to allow you to cover more unique and uncommon edge cases. With the `sc-custom` tag you can decide yourself what the placeholder value will be. -In order to have stylelint correctly apply indentation rules we need to do a bit of opinionated preprocessing on the `styled-components` styles, which results in us only officially supporting one coding style when it comes to `styled-components` tagged template literals. This style consists of always placing the closing backtick on the base level of indentation as follows: +For example: -**Right** ```js +// Switch between left and right based on language settings passed through via the theme +const rtlSwitch = props => props.theme.dir === 'rtl' ? 'left' : 'right'; + const Button = styled.button` - color: red; -` + background: green; + // Tell the processor to replace the interpolation with "left" + // when linting + margin-${/* sc-custom 'left' */ rtlSwitch}: 12.5px; +`; ``` +### Syntax notes + +#### Turning rules off from within your JS/CSS + +Turn off rules with `stylelint-disable` comments (see the [stylelint documentation](https://stylelint.io/user-guide/configuration/#turning-rules-off-from-within-your-css) for all allowed syntax) both inside and outside of the tagged template literals. + +```js +import React from 'react'; +import styled from 'styled-components'; + +// Disable stylelint from within the tagged template literal +const Wrapper = styled.div` + /* stylelint-disable */ + background-color: 123; +`; + +// Or from the JavaScript around the tagged template literal +/* stylelint-disable */ +const Wrapper = styled.div` + background-color: 123; +`; +``` + +#### Template literal style and indentation + +In order to have stylelint correctly apply indentation rules the processor needs to do a bit of opinionated preprocessing on the styles, which results in us only officially supporting one indentation style. (the supported style is the "default" one as shown in all the documentation) + +The important thing is that you put the closing backtick on the base level of indentation as follows: + +**Right** + ```js if (condition) { const Button = styled.button` @@ -117,6 +200,7 @@ if (condition) { ``` **Wrong** + ```js if (condition) { const Button = styled.button` @@ -136,7 +220,7 @@ It may be that other tagged template literal styles are coincidentally supported ## License -Licensed under the MIT License, Copyright © 2016 Maximilian Stoiber. See [LICENSE.md](./LICENSE.md) for more information! +Licensed under the MIT License, Copyright © 2017 Maximilian Stoiber. See [LICENSE.md](./LICENSE.md) for more information! Based on Mapbox' excellent [`stylelint-processor-markdown`](https://github.com/mapbox/stylelint-processor-markdown), thanks to @davidtheclark! diff --git a/package-lock.json b/package-lock.json index d4fddec..5e2717d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1082,6 +1082,7 @@ "requires": { "anymatch": "1.3.0", "async-each": "1.0.1", + "fsevents": "1.1.2", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1980,6 +1981,905 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, "function-bind": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", @@ -3832,6 +4732,13 @@ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "dev": true, + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index 9f63562..003bf29 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,9 @@ "typescript": "~2.4.0", "stylelint": "^8.0.0" }, + "peerDependency": { + "stylelint-config-styled-components-processor": "^0.1.0" + }, "dependencies": { "babel-traverse": "^6.16.0", "babylon": "^6.12.0", diff --git a/src/index.js b/src/index.js index 20c205a..56a6f3e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,5 @@ const path = require('path') const parse = require('./parsers/index') -// TODO Fix ampersand in selectors -// TODO ENFORCE THESE RULES -// value-no-vendor-prefix – don't allow vendor prefixes -// property-no-vendor-prefix – don't allow vendor prefixes - -const ignoredRules = [ - // Don't throw if there's no styled-components in a file - 'no-empty-source', - // We don't care about end-of-source newlines, users cannot control them - 'no-missing-end-of-source-newline' -] const sourceMapsCorrections = {} @@ -30,23 +19,15 @@ module.exports = (/* options */) => ({ // Fix sourcemaps result(stylelintResult, filepath) { const lineCorrection = sourceMapsCorrections[filepath] - const newWarnings = stylelintResult.warnings.reduce((prevWarnings, warning) => { - if (ignoredRules.includes(warning.rule)) return prevWarnings - const correctedWarning = Object.assign(warning, { + const warnings = stylelintResult.warnings.map(warning => + Object.assign({}, warning, { // Replace "brace" with "backtick" in warnings, e.g. // "Unexpected empty line before closing backtick" (instead of "brace") text: warning.text.replace(/brace/, 'backtick'), line: lineCorrection[warning.line] }) - prevWarnings.push(correctedWarning) - return prevWarnings - }, []) - - if (newWarnings.length === 0) { - // eslint-disable-next-line no-param-reassign - stylelintResult.errored = false - } + ) - return Object.assign(stylelintResult, { warnings: newWarnings }) + return Object.assign({}, stylelintResult, { warnings }) } }) diff --git a/src/parsers/index.js b/src/parsers/index.js index 36d847c..9480045 100644 --- a/src/parsers/index.js +++ b/src/parsers/index.js @@ -17,7 +17,7 @@ const getTTLContent = require('../utils/tagged-template-literal.js').getTaggedTe const parseImports = require('../utils/parse').parseImports const getSourceMap = require('../utils/parse').getSourceMap -const processStyledComponentsFile = ast => { +const processStyledComponentsFile = (ast, absolutePath) => { const extractedCSS = [] let ignoreRuleComments = [] let importedNames = { @@ -46,8 +46,10 @@ const processStyledComponentsFile = ast => { if (hasAttrsCall(node)) { processedNode.tag = getAttrsObject(node) } - if (!helper && !isStyled(processedNode, importedNames.default) && !isExtendCall(node)) return - const content = getTTLContent(processedNode) + if (!helper && !isStyled(processedNode, importedNames.default) && !isExtendCall(node)) { + return + } + const content = getTTLContent(node, absolutePath) const fixedContent = fixIndentation(content).text const wrapperFn = helper === 'keyframes' ? wrapKeyframes : wrapSelector const wrappedContent = wrapperFn(fixedContent) diff --git a/src/utils/general.js b/src/utils/general.js index 62f0c91..3085643 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -70,6 +70,29 @@ const wrapKeyframes = content => `@keyframes {${content}}\n` */ const isStylelintComment = comment => /^\s*stylelint-(?:enable|disable)(?:\s.*)?$/.test(comment) +const extrapolateShortenedCommand = (commands, shortCommand, absolutePath, location) => { + let extrapolatedCommand = null + // We use .some so we can break the loop using return true + commands.some(singleCommand => { + if (singleCommand.substr(0, shortCommand.length) === shortCommand) { + if (extrapolatedCommand === null) { + // This is the first time we found a match + extrapolatedCommand = singleCommand + } else { + // We have already found another command which means this is not a unique short command. + // This will probably never throw, as all our current commands start with different letters + throw new Error( + `ERROR at ${absolutePath} line ${location.line} column ${location.column}:` + + '\nYou shortened a Styled Components interpolation tag ambiguously, add a few more characters to fix this error' + ) + } + } + // continue loop + return false + }) + return extrapolatedCommand +} + exports.wrapKeyframes = wrapKeyframes exports.wrapSelector = wrapSelector exports.fixIndentation = fixIndentation @@ -77,3 +100,4 @@ exports.reverseString = reverseString exports.nextNonWhitespaceChar = nextNonWhitespaceChar exports.isLastDeclarationCompleted = isLastDeclarationCompleted exports.isStylelintComment = isStylelintComment +exports.extrapolateShortenedCommand = extrapolateShortenedCommand diff --git a/src/utils/tagged-template-literal.js b/src/utils/tagged-template-literal.js index 3a2ea83..09906fa 100644 --- a/src/utils/tagged-template-literal.js +++ b/src/utils/tagged-template-literal.js @@ -1,5 +1,6 @@ const nextNonWhitespaceChar = require('./general').nextNonWhitespaceChar const isLastDeclarationCompleted = require('./general').isLastDeclarationCompleted +const extrapolateShortenedCommand = require('./general').extrapolateShortenedCommand /** * Check if a node is a tagged template literal @@ -11,10 +12,103 @@ const isTaggedTemplateLiteral = node => node.type === 'TaggedTemplateExpression' */ const hasInterpolations = node => !node.quasi.quasis[0].tail +/** + * Retrieves all the starting and ending comments of a TTL expression + */ +const retrieveStartEndComments = expression => + (expression.leadingComments || []).concat(expression.trailingComments || []) + +/** + * Checks if given comment value is an interpolation tag + */ +const isScTag = comment => /^\s*?sc-[a-z]/.test(comment) + +/** + * Checks if an interpolation has an sc comment tag + */ +const hasInterpolationTag = expression => { + const relevantComments = retrieveStartEndComments(expression).map( + commentObject => commentObject.value + ) + return relevantComments.some(isScTag) +} + +const extractScTagInformation = comment => { + const matchArray = comment.match(/^\s*?sc-([a-z]+)\s*(?:(?:'(.*?)')|(?:"(.*?)"))?\s*$/) + if (matchArray === null) { + return null + } + return { + command: matchArray[1], + // This is only cared about if command is custom + customPlaceholder: matchArray[2] || matchArray[3] + } +} + +const interpolationTagAPI = ['block', 'selector', 'declaration', 'property', 'value', 'custom'] +/** + * Enact the interpolation tagging API + */ +const parseInterpolationTag = (expression, id, absolutePath) => { + const relevantComments = retrieveStartEndComments(expression) + let substitute + relevantComments.some(comment => { + if (isScTag(comment.value)) { + // We always assume that there is only one sc tag in an interpolation + const scTagInformation = extractScTagInformation(comment.value) + if (scTagInformation === null) { + throw new Error( + `ERROR at ${absolutePath} line ${comment.loc.start.line} column ${comment.loc.start + .column}:` + + '\nWe were unable to parse your Styled Components interpolation tag, this is most likely due to lack of quotes in an sc-custom tag, refer to the documentation for correct format' + ) + } + scTagInformation.command = extrapolateShortenedCommand( + interpolationTagAPI, + scTagInformation.command, + absolutePath, + comment.loc.start + ) + switch (scTagInformation.command) { + case 'selector': + substitute = 'div' + break + + case 'block': + case 'declaration': + substitute = `-styled-mixin${id}: dummyValue;` + break + + case 'property': + substitute = `-styled-mixin${id}` + break + + case 'value': + substitute = '$dummyValue' + break + + case 'custom': + substitute = scTagInformation.customPlaceholder + break + + default: + throw new Error( + `ERROR at ${absolutePath} line ${comment.loc.start.line} column ${comment.loc.start + .column}:` + + '\nYou tagged a Styled Components interpolation with an invalid sc- tag. Refer to the documentation to see valid interpolation tags' + ) + } + return true // Break loop + } + return false // Continue loop + }) + return substitute +} + /** * Merges the interpolations in a parsed tagged template literals with the strings */ -const interleave = (quasis, expressions) => { +const interleave = (quasis, expressions, absolutePath) => { // Used for making sure our dummy mixins are all unique let count = 0 let css = '' @@ -24,7 +118,11 @@ const interleave = (quasis, expressions) => { css += prevText let substitute - if (isLastDeclarationCompleted(css)) { + if (hasInterpolationTag(expressions[i])) { + substitute = parseInterpolationTag(expressions[i], count, absolutePath) + count += 1 + } else if (isLastDeclarationCompleted(css)) { + // No sc tag so we guess defaults /** This block assumes that if you put an interpolation in the position * of the start of a declaration that the interpolation will * contain a full declaration and not later in the template literal @@ -54,9 +152,9 @@ const interleave = (quasis, expressions) => { * * TODO Cover edge cases */ -const getTaggedTemplateLiteralContent = node => { +const getTaggedTemplateLiteralContent = (node, absolutePath) => { if (hasInterpolations(node)) { - return interleave(node.quasi.quasis, node.quasi.expressions) + return interleave(node.quasi.quasis, node.quasi.expressions, absolutePath) } else { return node.quasi.quasis[0].value.raw } @@ -65,3 +163,6 @@ const getTaggedTemplateLiteralContent = node => { exports.isTaggedTemplateLiteral = isTaggedTemplateLiteral exports.getTaggedTemplateLiteralContent = getTaggedTemplateLiteralContent exports.interleave = interleave +exports.hasInterpolationTag = hasInterpolationTag +exports.parseInterpolationTag = parseInterpolationTag +exports.extractScTagInformation = extractScTagInformation diff --git a/test/fixtures/interpolation-tagging/invalid-custom.js b/test/fixtures/interpolation-tagging/invalid-custom.js new file mode 100644 index 0000000..9a63451 --- /dev/null +++ b/test/fixtures/interpolation-tagging/invalid-custom.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +const Button1 = styled.button` + color: red; + ${/* sc-custom no quotes present */ 'dummy'} +`; diff --git a/test/fixtures/interpolation-tagging/invalid-tag.js b/test/fixtures/interpolation-tagging/invalid-tag.js new file mode 100644 index 0000000..1bf1da6 --- /dev/null +++ b/test/fixtures/interpolation-tagging/invalid-tag.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +const Button1 = styled.button` + color: red; + ${/* sc-invalid */ 'dummy'} +`; diff --git a/test/fixtures/interpolation-tagging/valid.js b/test/fixtures/interpolation-tagging/valid.js new file mode 100644 index 0000000..d799e96 --- /dev/null +++ b/test/fixtures/interpolation-tagging/valid.js @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +// Test block +const Button1 = styled.button` + color: red; + ${/* sc-block */ 'dummy'} +`; + +// Test selector +const Button2 = styled.button` + color: red; + ${/* sc-selector */ ':hover'} { + background-color: blue; + } +`; + +// Test declaration +const Button3 = styled.button` + color: red; + ${/* sc-declaration */ 'dummy'} +`; + +// Test property +const Button4 = styled.button` + color: red; + ${/* sc-property */ 'background-color'}: blue; +`; + +// Test value +const Button5 = styled.button` + color: red; + background-color: ${/* sc-value */ 'blue'}; +`; + +// Test custom +const bool = true; +const Button6 = styled.button` + color: red; + margin-${/* sc-custom 'left' */ bool ? 'left' : 'right'}: 10px; +`; diff --git a/test/interpolation-tagging.test.js b/test/interpolation-tagging.test.js new file mode 100644 index 0000000..afb6b1f --- /dev/null +++ b/test/interpolation-tagging.test.js @@ -0,0 +1,88 @@ +const stylelint = require('stylelint') +const path = require('path') + +const processor = path.join(__dirname, '../src/index.js') +const rules = { + 'block-no-empty': true, + 'declaration-block-no-duplicate-properties': true, + indentation: 2 +} + +describe('interpolation-tagging', () => { + let fixture + let data + + beforeEach(done => { + stylelint + .lint({ + files: [fixture], + syntax: 'scss', + config: { + processors: [processor], + rules + } + }) + .then(result => { + data = result + done() + }) + .catch(err => { + data = err + done() + }) + }) + + describe('valid', () => { + beforeAll(() => { + fixture = path.join(__dirname, './fixtures/interpolation-tagging/valid.js') + }) + + it('should have one result', () => { + expect(data.results.length).toEqual(1) + }) + + it('should use the right file', () => { + expect(data.results[0].source).toEqual(fixture) + }) + + it('should not have errored', () => { + expect(data.errored).toEqual(false) + }) + + it('should not have any warnings', () => { + expect(data.results[0].warnings.length).toEqual(0) + }) + }) + + describe('invalid tag', () => { + beforeAll(() => { + fixture = path.join(__dirname, './fixtures/interpolation-tagging/invalid-tag.js') + }) + + it('should throw an error', () => { + expect(data).toEqual(expect.any(Error)) + }) + + it('should throw correct error', () => { + expect(data.message).toMatch( + /fixtures\/interpolation-tagging\/invalid-tag\.js line 5 column 4:\n.*invalid sc- tag/ + ) + }) + }) + + describe('invalid custom tag', () => { + beforeAll(() => { + fixture = path.join(__dirname, './fixtures/interpolation-tagging/invalid-custom.js') + }) + + it('should throw an error', () => { + expect(data).toEqual(expect.any(Error)) + }) + + it('should throw correct error', () => { + expect(data.message).toMatch( + /fixtures\/interpolation-tagging\/invalid-custom\.js line 5 column 4:\n.*We were unable to parse/ + ) + }) + }) +}) diff --git a/test/simple.test.js b/test/simple.test.js index b7862b1..046a452 100644 --- a/test/simple.test.js +++ b/test/simple.test.js @@ -36,6 +36,7 @@ describe('simple', () => { done() }) .catch(err => { + // eslint-disable-next-line console.log(err) data = err done() diff --git a/test/utils.test.js b/test/utils.test.js index 4eed147..a68b618 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,9 +1,14 @@ const interleave = require('../src/utils/tagged-template-literal').interleave +const hasInterpolationTag = require('../src/utils/tagged-template-literal').hasInterpolationTag +const parseInterpolationTag = require('../src/utils/tagged-template-literal').parseInterpolationTag +const extractScTagInformation = require('../src/utils/tagged-template-literal') + .extractScTagInformation const isLastDeclarationCompleted = require('../src/utils/general').isLastDeclarationCompleted const nextNonWhitespaceChar = require('../src/utils/general').nextNonWhitespaceChar const reverseString = require('../src/utils/general').reverseString const isStylelintComment = require('../src/utils/general').isStylelintComment const fixIndentation = require('../src/utils/general').fixIndentation +const extrapolateShortenedCommand = require('../src/utils/general').extrapolateShortenedCommand describe('utils', () => { describe('interleave', () => { @@ -374,4 +379,144 @@ describe('utils', () => { expect(fixIndentation(test3).text).toBe(test3) }) }) + + describe('hasInterpolationTag', () => { + const fn = hasInterpolationTag + it('works for starting comment', () => { + const expression = { + leadingComments: [{ value: ' sc-block ' }], + trailingComments: [] + } + expect(fn(expression)).toBe(true) + }) + + it('correctly identifies lack of tag', () => { + const expression = { + leadingComments: [{ value: 'some test value' }], + trailingComments: [{ value: 'second test value' }] + } + expect(fn(expression)).toBe(false) + }) + + it('handles tag not being first comment', () => { + const expression = { + leadingComments: [{ value: 'some test value' }, { value: 'second test value' }], + trailingComments: [{ value: ' sc-s' }] + } + expect(fn(expression)).toBe(true) + }) + }) + + describe('parseInterpolationTag', () => { + const fn = parseInterpolationTag + const prepExpression = command => ({ + leadingComments: [ + { value: 'some test comment' }, + { + value: `sc-${command}`, + loc: { + start: { + line: 1, + column: 3 + } + } + } + ], + trailingComments: [] + }) + it('handles the API', () => { + const selectorExpression = prepExpression('selector') + expect(fn(selectorExpression, 1, 'path/to/file')).toBe('div') + + const blockExpression = prepExpression('block') + expect(fn(blockExpression, 1, 'path/to/file')).toBe('-styled-mixin1: dummyValue;') + + const declarationExpression = prepExpression('declaration') + expect(fn(declarationExpression, 1, 'path/to/file')).toBe('-styled-mixin1: dummyValue;') + + const propertyExpression = prepExpression('property') + expect(fn(propertyExpression, 1, 'path/to/file')).toBe('-styled-mixin1') + + const valueExpression = prepExpression('value') + expect(fn(valueExpression, 1, 'path/to/file')).toBe('$dummyValue') + + const customExpression = prepExpression('custom " my test placeholder"') + expect(fn(customExpression, 1, 'path/to/file')).toBe(' my test placeholder') + }) + + it('throws on invalid tag', () => { + const invalidExpression = prepExpression('invalid') + expect(fn.bind(null, invalidExpression, 1, 'path/to/file')).toThrow( + /path\/to\/file line 1 column 3:\n.*invalid sc- tag/ + ) + }) + }) + + describe('extrapolateShortenedCommand', () => { + const fn = extrapolateShortenedCommand + const commands = ['hello', 'heaven', 'command'] + + it('handles correctly shortened commands', () => { + expect(fn(commands, 'hel')).toBe('hello') + expect(fn(commands, 'hea')).toBe('heaven') + expect(fn(commands, 'c')).toBe('command') + expect(fn(commands, 'comm')).toBe('command') + }) + + it('handles full commands', () => { + expect(fn(commands, 'hello')).toBe('hello') + expect(fn(commands, 'heaven')).toBe('heaven') + expect(fn(commands, 'command')).toBe('command') + }) + + it('rejects ambigously shortened commands', () => { + expect(fn.bind(this, commands, 'h')).toThrow() + expect(fn.bind(this, commands, 'he', '/path/to/file', { line: 4, column: 6 })).toThrow( + /path\/to\/file line 4 column 6:/ + ) + }) + + it('rejects nonsense', () => { + expect(fn(commands, 'nonsense')).toBeNull() + expect(fn(commands, 'asdfasfd')).toBeNull() + expect(fn(commands, 'x')).toBeNull() + }) + }) + + describe('extractScTagInformation', () => { + const fn = extractScTagInformation + it('handles normal Sc Tag', () => { + expect(fn(' sc-block ')).toEqual({ + command: 'block', + customPlaceholder: undefined + }) + + expect(fn('sc-selector')).toEqual({ + command: 'selector', + customPlaceholder: undefined + }) + + expect(fn('sc-block ')).toEqual({ + command: 'block', + customPlaceholder: undefined + }) + + expect(fn('sc-block ')).toEqual({ + command: 'block', + customPlaceholder: undefined + }) + }) + + it('handles custom placeholder', () => { + expect(fn(' sc-custom "placeholder test" ')).toEqual({ + command: 'custom', + customPlaceholder: 'placeholder test' + }) + + expect(fn(" sc-custom 'placeholder test' ")).toEqual({ + command: 'custom', + customPlaceholder: 'placeholder test' + }) + }) + }) })