├── .babelrc ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── App.jsx ├── styleQueries.js └── styleguide.js ├── index.js ├── package.json ├── scripts └── ci.sh ├── src ├── __snapshots__ │ └── index.test.js.snap ├── index.js ├── index.test.js └── utils.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "loose": true, 7 | "modules": false 8 | } 9 | ], 10 | "@babel/react" 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-object-rest-spread", 14 | ], 15 | "env": { 16 | "commonjs": { 17 | "plugins": [ 18 | "@babel/plugin-transform-modules-commonjs" 19 | ] 20 | }, 21 | "test": { 22 | "plugins": [ 23 | "@babel/plugin-transform-modules-commonjs" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "prettier/react"], 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "commonjs": true 8 | }, 9 | "plugins": ["react", "import", "prettier"], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "settings": { 18 | "import/resolver": { 19 | "node": { 20 | "extensions": [".js", ".jsx"] 21 | } 22 | } 23 | }, 24 | "rules": { 25 | "react/prop-types": 0, 26 | "prettier/prettier": [ 27 | "error", 28 | { 29 | "trailingComma": "es5", 30 | "tabWidth": 4, 31 | "printWidth": 100 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directory 7 | node_modules 8 | 9 | # Optional npm cache directory 10 | .npm 11 | 12 | # Optional REPL history 13 | .node_repl_history 14 | 15 | package-lock.json 16 | 17 | dist/ 18 | es/ 19 | lib/ 20 | coverage/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __snapshots__/ 2 | node_modules/ 3 | dist/ 4 | lib/ 5 | coverage/ 6 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | 4 | node_js: 5 | - 8 6 | 7 | cache: 8 | yarn: true 9 | directories: 10 | - ".eslintcache" 11 | - node_modules 12 | 13 | script: 14 | - yarn ci -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are open to, and grateful for, any contributions made by the community. 4 | 5 | ## Reporting Issues 6 | 7 | Before opening an issue, please search the [issue tracker](https://github.com/braposo/graphql-css/issues) to make sure your issue hasn't already been reported. 8 | 9 | ## Development 10 | 11 | Visit the [Issue tracker](https://github.com/braposo/graphql-css/issues) to find a list of open issues that need attention. 12 | 13 | Fork, then clone the repo: 14 | 15 | ``` 16 | git clone https://github.com/your-username/graphql-css.git 17 | ``` 18 | 19 | Build package for dev mode. It will automatically watch any changes in `src/` forlder: 20 | 21 | ``` 22 | yarn run dev 23 | ``` 24 | 25 | ### Building and testing 26 | 27 | Build package: 28 | 29 | ``` 30 | yarn run build 31 | ``` 32 | 33 | To run the tests: 34 | 35 | ``` 36 | yarn run test 37 | ``` 38 | 39 | ### New Features 40 | 41 | Please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept. 42 | 43 | ## Submitting Changes 44 | 45 | * Open a new issue in the [Issue tracker](https://github.com/braposo/graphql-css/issues). 46 | * Fork the repo. 47 | * Create a new feature branch based off the `master` branch. 48 | * Make sure all tests pass and there are no linting errors. 49 | * Submit a pull request, referencing any issues it addresses. 50 | 51 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 52 | 53 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements. 54 | 55 | Thank you for contributing! 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 EDITED 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | `graphql-css` is a blazing fast CSS-in-GQL™ library that converts GraphQL queries into styles for your components. 4 | 5 | Comes with a bunch of utilities so it's easy to integrate with your favourite way of building components. 6 | 7 | [![Build Status][build-badge]][travis] 8 | [![Code Coverage][coverage-badge]][coverage] 9 | [![npm version][version-badge]][npm] 10 | [![npm downloads][downloads-badge]][npm] 11 | [![gzip size][size-badge]][size] 12 | [![MIT License][license-badge]][license] 13 | 14 | ![Module format][modules-badge] 15 | ![Prettier format][prettier-badge] 16 | [![PRs Welcome][prs-badge]][prs] 17 | ![Blazing Fast][fast-badge] 18 | ![Modern][modern-badge] 19 | ![Enterprise Grade][enterprise-badge] 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm install graphql-css 25 | # or 26 | yarn add graphql-css 27 | ``` 28 | 29 | #### Dependencies 30 | 31 | `graphql-css` requires `graphql` to be installed as a peer dependency. It's compatible with [React hooks](https://reactjs.org/docs/hooks-intro.html) so you can use it with React's latest version. 32 | 33 | ## Quick start 34 | 35 | ```jsx 36 | import useGqlCSS from "graphql-css"; 37 | import styles from "your-style-guide"; 38 | 39 | const App = () => { 40 | const { styled } = useGqlCSS(styles); 41 | const H2 = styled.h2` 42 | { 43 | typography { 44 | h2 45 | } 46 | marginLeft: spacing { 47 | xl 48 | } 49 | color: colors { 50 | green 51 | } 52 | } 53 | `; 54 | return

This is a styled text

; 55 | }; 56 | ``` 57 | 58 | [![Edit graphql-css][codesandbox-badge]][codesandbox] 59 | 60 | ## API 61 | 62 | By default, `graphql-css` exports a hook-like function called `useGqlCSS`. 63 | 64 | It also exports a couple of other utilities: 65 | 66 | - `GqlCSS`: a component that provides the same declarative API 67 | - `gql`: the default export from `graphql-tag` so you don't have to install it if only using graphql-css 68 | 69 | ### useGqlCSS 70 | 71 | The main export is the `useGqlCSS` function that should be used in most cases. It provides these utilities: 72 | 73 | - `styled`: a styled-component inspired function to create components from gqlCSS queries 74 | - `getStyles`: a function to extract styles to an object 75 | - `GqlCSS`: a component that encapsulates the `styled` functionality 76 | 77 | `useGqlCSS` needs to be initialised with the styles from the styleguide in a JSON format (check examples folder for a detailed example). 78 | 79 | Here's how you can use it to create a new component with `styled`: 80 | 81 | ```jsx 82 | import useGqlCSS from "graphql-css"; 83 | ... 84 | const { styled } = useGqlCSS(styles); 85 | const Text = styled.p` 86 | { 87 | typography { 88 | fontSize: scale { 89 | l 90 | } 91 | } 92 | } 93 | `; 94 | ... 95 | This is a styled text 96 | ``` 97 | 98 | alternatively, you can also return the styles as an object with `getStyles` so you can use it with other CSS-in-JS libraries: 99 | 100 | ```jsx 101 | import useGqlCSS, { gql } from "graphql-css"; 102 | import styled from "@emotion/styled"; 103 | ... 104 | const query = gql` 105 | { 106 | color: colors { 107 | green 108 | } 109 | } 110 | `; 111 | const { getStyles } = useGqlCSS(styles); 112 | const StyledComponent = styled.div(getStyles(query)); 113 | ``` 114 | 115 | If you want to keep the declarative API you can also use the `GqlCSS`, which is an exact match to the main `GqlCSS` component exported by this library. The only difference is that the `useGqlCSS` version already has the styles initialised. 116 | 117 | ```jsx 118 | import useGqlCSS, { gql } from "graphql-css"; 119 | ... 120 | const { GqlCSS } = useGqlCSS(styles); 121 | const query = gql` 122 | { 123 | typography { 124 | h2 125 | } 126 | } 127 | `; 128 | ... 129 | This is a styled text 130 | ``` 131 | 132 | Please check the `GqlCSS` section below for a detailed reference. 133 | 134 | ### GqlCSS 135 | 136 | `` component allows for a more declarative API and accepts these props: 137 | 138 | | Prop | Type | Default | Definition | 139 | | --------- | ---------------- | ------- | ----------------------------------------------- | 140 | | styles | object | | The styleguide object with all the rules | 141 | | query | gql | | The gql query to get the styles | 142 | | component | string \|\| node | "div" | HTML element or React component to be displayed | 143 | 144 | All the remaining props are passed to the generated component. Here are some examples: 145 | 146 | ```jsx 147 | ... 148 | This is a styled text 149 | This is a styled H1 heading 150 | ... 151 | ``` 152 | 153 | ## Styles object 154 | 155 | The styles object is a valid JSON object that is used to define the styleguide of your project. Usually it includes definitions for colors, spacing, typography, etc. 156 | 157 | ```js 158 | const base = 4; 159 | const styles = { 160 | typography: { 161 | scale: { 162 | s: base * 3, 163 | base: base * 4, 164 | m: base * 6, 165 | l: base * 9, 166 | xl: base * 13, 167 | xxl: base * 20, 168 | unit: "px", 169 | }, 170 | weight: { 171 | thin: 300, 172 | normal: 400, 173 | bold: 700, 174 | bolder: 900, 175 | }, 176 | }, 177 | spacing: { 178 | s: base, 179 | base: base * 2, 180 | m: base * 4, 181 | l: base * 6, 182 | xl: base * 8, 183 | xxl: base * 10, 184 | unit: "px", 185 | }, 186 | colors: { 187 | blue: "blue", 188 | green: "green", 189 | red: "red", 190 | }, 191 | }; 192 | ``` 193 | 194 | This is completely up to you and one of the big advantages of using `graphql-css` as you can adapt it to your needs. As long as the styles and the queries match their structure, there shouldn't be much problem. 195 | 196 | You can also specify the unit of each property by definining the `unit` key. 197 | 198 | ```js 199 | scale: { 200 | s: base * 3, 201 | base: base * 4, 202 | m: base * 6, 203 | l: base * 9, 204 | xl: base * 13, 205 | xxl: base * 20, 206 | unit: "em" 207 | }, 208 | ``` 209 | 210 | ## Building the GraphQL query 211 | 212 | The GraphQL query follows the structure of the styles object with a few particular details. When building the query you need to alias the values you're getting from the style guide to the correspondent CSS property. Here's a quick example: 213 | 214 | ```js 215 | { 216 | typography { 217 | fontSize: scale { 218 | xl 219 | } 220 | fontWeight: weight { 221 | bold 222 | } 223 | } 224 | } 225 | ``` 226 | 227 | This also means that you can reuse the same query by using different alias: 228 | 229 | ```js 230 | { 231 | marginLeft: spacing { 232 | l 233 | } 234 | paddingTop: spacing { 235 | xl 236 | } 237 | } 238 | ``` 239 | 240 | #### Using fragments 241 | 242 | Because _This is just GraphQL™_, you can also create fragments that can then be included in other queries: 243 | 244 | ```js 245 | const h1Styles = gql` 246 | fragment H1 on Styles { 247 | base { 248 | typography { 249 | fontSize: scale { 250 | xl 251 | } 252 | fontWeight: weight { 253 | bold 254 | } 255 | } 256 | } 257 | } 258 | `; 259 | 260 | const otherH1Styles = gql` 261 | ${h1Styles} 262 | { 263 | ...H1 264 | base { 265 | color: colors { 266 | blue 267 | } 268 | } 269 | } 270 | `; 271 | ``` 272 | 273 | This is a powerful pattern that avoids lots of repetitions and allows for a bigger separation of concerns. 274 | 275 | #### Defining custom unit 276 | 277 | You can also override the pre-defined unit directly in your query by using the argument `unit`: 278 | 279 | ```js 280 | { 281 | marginLeft: spacing(unit: "em") { 282 | l 283 | } 284 | paddingTop: spacing { 285 | xl 286 | } 287 | } 288 | ``` 289 | 290 | This will return `{ marginLeft: "24em", paddingTop: "32px" }`. 291 | 292 | #### Using style variations (theming) 293 | 294 | One of the big advantages of CSS-in-GQL™ is that you can use the power of variables to build custom queries. In `graphql-css` that means that we can easily define variants (think themes) for specific components. 295 | 296 | Let's start with this style definition file: 297 | 298 | ```js 299 | const styles = { 300 | theme: { 301 | light: { 302 | button: { 303 | // button light styles 304 | }, 305 | }, 306 | dark: { 307 | button: { 308 | // button dark styles 309 | }, 310 | }, 311 | }, 312 | }; 313 | ``` 314 | 315 | We now have two options to handle theming, first using the `styled` function from `useGqlCSS`: 316 | 317 | ```jsx 318 | import useGqlCSS, { gql } from "graphql-css"; 319 | ... 320 | const { styled } = useGqlCSS(styles); 321 | const Button = styled.button` 322 | { 323 | theme(variant: ${props => props.variant}) { 324 | button 325 | } 326 | } 327 | `; 328 | ... 329 | 330 | ``` 331 | 332 | Alternatively, we can use GraphQL variables instead by using `getStyles`: 333 | 334 | ```jsx 335 | import useGqlCSS, { gql } from "graphql-css"; 336 | import styled from "@emotion/styled"; 337 | ... 338 | const { getStyles } = useGqlCSS(styles); 339 | const query = gql` 340 | { 341 | theme(variant: $variant) { 342 | button 343 | } 344 | } 345 | `; 346 | const LightButton = styled.button(getStyles(query, { variant: "light" })); 347 | ... 348 | Some text 349 | ``` 350 | 351 | ## Developing 352 | 353 | You can use `yarn run dev` to run it locally, but we recommend using the [CodeSandbox playground][codesandbox] for development. 354 | 355 | ## Contributing 356 | 357 | Please follow our [contributing guidelines](https://github.com/braposo/graphql-css/blob/master/CONTRIBUTING.md). 358 | 359 | ## License 360 | 361 | [MIT](https://github.com/davidgomes/graphql-css/blob/master/LICENSE) 362 | 363 | [npm]: https://www.npmjs.com/package/graphql-css 364 | [license]: https://github.com/braposo/graphql-css/blob/master/LICENSE 365 | [prs]: http://makeapullrequest.com 366 | [size]: https://unpkg.com/graphql-css/dist/graphql-css.min.js 367 | [version-badge]: https://img.shields.io/npm/v/graphql-css.svg?style=flat-square 368 | [downloads-badge]: https://img.shields.io/npm/dm/graphql-css.svg?style=flat-square 369 | [license-badge]: https://img.shields.io/npm/l/graphql-css.svg?style=flat-square 370 | [fast-badge]: https://img.shields.io/badge/🔥-Blazing%20Fast-red.svg?style=flat-square 371 | [modern-badge]: https://img.shields.io/badge/💎-Modern-44aadd.svg?style=flat-square 372 | [enterprise-badge]: https://img.shields.io/badge/🏢-Enterprise%20Grade-999999.svg?style=flat-square 373 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 374 | [size-badge]: http://img.badgesize.io/https://unpkg.com/graphql-css/dist/graphql-css.min.js?compression=gzip&style=flat-square 375 | [prettier-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square 376 | [build-badge]: https://img.shields.io/travis/braposo/graphql-css.svg?style=flat-square 377 | [travis]: https://travis-ci.org/braposo/graphql-css 378 | [coverage-badge]: https://img.shields.io/codecov/c/github/braposo/graphql-css.svg?style=flat-square 379 | [coverage]: https://codecov.io/github/braposo/graphql-css 380 | [modules-badge]: https://img.shields.io/badge/module%20formats-umd%2C%20cjs%2C%20esm-green.svg?style=flat-square 381 | [codesandbox-badge]: https://codesandbox.io/static/img/play-codesandbox.svg 382 | [codesandbox]: https://codesandbox.io/s/5vljjr4zo4 383 | -------------------------------------------------------------------------------- /examples/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import styleguide from "./styleguide"; 3 | import useGqlCSS, { GqlCSS, gql } from "../src"; 4 | import { h1Styles, h2Styles, stateStyles } from "./styleQueries"; 5 | import cxs from "cxs/component"; 6 | import PropTypes from "prop-types"; 7 | 8 | const Context = React.createContext(); 9 | 10 | function StatefulComponent() { 11 | const [variant, setVariant] = useState("normal"); 12 | const { styled, GqlCSS } = useGqlCSS(styleguide); 13 | const toggleVariant = () => setVariant(state => (state === "normal" ? "done" : "normal")); 14 | const OtherComponent = styled.button`{ 15 | theme(variant: ${props => props.variant}) { 16 | button 17 | } 18 | base { 19 | marginLeft: spacing { 20 | ${variant === "done" ? "m" : "xl"} 21 | } 22 | } 23 | }`; 24 | OtherComponent.propTypes = { 25 | variant: PropTypes.string.isRequired, 26 | }; 27 | 28 | return ( 29 | 30 | 31 | Using stateful component 32 | 33 | Other component sharing state 34 | 35 | ); 36 | } 37 | 38 | function SubscriberComponent() { 39 | const styleguide = useContext(Context); 40 | const { getStyles } = useGqlCSS(styleguide); 41 | const styles = getStyles(gql` 42 | { 43 | base { 44 | typography { 45 | fontSize: scale { 46 | m 47 | } 48 | } 49 | marginLeft: spacing { 50 | xl 51 | } 52 | color: colors { 53 | blue 54 | } 55 | } 56 | } 57 | `); 58 | const StyledComponent = cxs("h3")(styles); 59 | return Getting styles through context; 60 | } 61 | 62 | function App() { 63 | const { styled } = useGqlCSS(styleguide); 64 | const H2 = styled.h2(h2Styles); 65 | const H3 = styled.h3`{ 66 | base { 67 | marginLeft: spacing { 68 | m 69 | } 70 | } 71 | 72 | ${props => 73 | props.blue && 74 | ` 75 | base { 76 | color: colors { 77 | blue 78 | } 79 | } 80 | `} 81 | }`; 82 | H3.propTypes = { 83 | blue: PropTypes.bool, 84 | }; 85 | 86 | return ( 87 |
88 |

This is a styled text

89 |

Component with template literal

90 | 91 | A styled component 92 | 93 | 94 | 95 | 96 | 97 |
98 | ); 99 | } 100 | 101 | export default App; 102 | -------------------------------------------------------------------------------- /examples/styleQueries.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const h2Styles = gql` 4 | { 5 | base { 6 | typography { 7 | fontSize: scale { 8 | l 9 | } 10 | fontWeight: weight { 11 | bold 12 | } 13 | } 14 | marginLeft: spacing { 15 | xl 16 | } 17 | color: colors { 18 | green 19 | } 20 | } 21 | } 22 | `; 23 | 24 | export const h1Styles = gql` 25 | fragment H1 on styles { 26 | base { 27 | typography { 28 | fontSize: scale { 29 | xl 30 | } 31 | fontWeight: weight { 32 | bold 33 | } 34 | } 35 | marginLeft: spacing { 36 | l 37 | } 38 | color: colors { 39 | red 40 | } 41 | } 42 | } 43 | `; 44 | 45 | export const customH1Styles = gql` 46 | ${h1Styles} 47 | { 48 | ...H1 49 | base { 50 | marginLeft: spacing(unit: "em") { 51 | s 52 | } 53 | color: colors { 54 | blue 55 | } 56 | } 57 | } 58 | `; 59 | 60 | export const stateStyles = gql` 61 | { 62 | theme(variant: $variant) { 63 | button 64 | } 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /examples/styleguide.js: -------------------------------------------------------------------------------- 1 | const base = 4; 2 | const baseStyles = { 3 | typography: { 4 | scale: { 5 | s: base * 3, 6 | base: base * 4, 7 | m: base * 6, 8 | l: base * 9, 9 | xl: base * 13, 10 | xxl: base * 20, 11 | unit: "px", 12 | }, 13 | weight: { 14 | thin: 300, 15 | normal: 400, 16 | bold: 700, 17 | bolder: 900, 18 | }, 19 | }, 20 | spacing: { 21 | s: base, 22 | base: base * 2, 23 | m: base * 4, 24 | l: base * 6, 25 | xl: base * 8, 26 | xxl: base * 10, 27 | unit: "px", 28 | }, 29 | colors: { 30 | blue: "blue", 31 | green: "green", 32 | red: "red", 33 | }, 34 | }; 35 | 36 | const styles = { 37 | base: baseStyles, 38 | theme: { 39 | normal: { 40 | button: { 41 | fontSize: baseStyles.typography.scale.l, 42 | backgroundColor: baseStyles.colors.red, 43 | padding: baseStyles.spacing.l, 44 | cursor: "pointer", 45 | }, 46 | }, 47 | done: { 48 | button: { 49 | fontSize: baseStyles.typography.scale.l, 50 | backgroundColor: baseStyles.colors.green, 51 | padding: baseStyles.spacing.l, 52 | cursor: "pointer", 53 | }, 54 | }, 55 | }, 56 | }; 57 | 58 | export default styles; 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./examples/App"; 4 | 5 | render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-css", 3 | "version": "2.0.0", 4 | "description": "A blazing fast and battle-tested CSS-in-GQL™ library.", 5 | "main": "./lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "build": "yarn run build:commonjs && yarn run build:es && npm run build:umd && npm run build:umd:min", 9 | "build:es": "babel src -d es --ignore '**/*.test.js'", 10 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src -d lib --ignore '**/*.test.js'", 11 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js --output dist/graphql-css.js --mode development", 12 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack src/index.js --output dist/graphql-css.min.js --mode production", 13 | "clean": "rimraf lib dist es coverage", 14 | "dev": "yarn run clean && cross-env BABEL_ENV=commonjs babel src -d lib --watch", 15 | "format": "prettier --write \"**/*.{js,md,ts,json}\" *.{js,md,ts,json}", 16 | "lint": "eslint src/ --ext .js,.jsx", 17 | "precommit": "lint-staged", 18 | "prepack": "yarn run clean && yarn run test && yarn run build", 19 | "test": "jest --no-cache", 20 | "test:watch": "jest --watchAll --coverage", 21 | "ci": "scripts/ci.sh" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/braposo/graphql-css.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/braposo/graphql-css/issues" 29 | }, 30 | "homepage": "https://github.com/braposo/graphql-css#readme", 31 | "files": [ 32 | "es", 33 | "dist", 34 | "lib", 35 | "src" 36 | ], 37 | "keywords": [ 38 | "graphql", 39 | "css", 40 | "styles", 41 | "processor", 42 | "css-in-gql" 43 | ], 44 | "authors": [ 45 | "Bernardo Raposo (https://github.com/braposo)", 46 | "David Gomes (https://github.com/davidgomes)" 47 | ], 48 | "license": "MIT", 49 | "dependencies": { 50 | "cxs": "6.2.0", 51 | "graphql-anywhere": "^4.1.24", 52 | "graphql-tag": "2.10.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/cli": "^7.2.3", 56 | "@babel/core": "^7.2.2", 57 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 58 | "@babel/plugin-transform-modules-commonjs": "^7.2.0", 59 | "@babel/preset-env": "^7.2.3", 60 | "@babel/preset-react": "^7.0.0", 61 | "babel-core": "^7.0.0-bridge.0", 62 | "babel-eslint": "^10.0.1", 63 | "babel-jest": "^23.6.0", 64 | "babel-loader": "^8.0.4", 65 | "codecov": "^3.1.0", 66 | "cross-env": "^5.2.0", 67 | "eslint": "^5.11.1", 68 | "eslint-config-prettier": "^3.3.0", 69 | "eslint-plugin-import": "^2.14.0", 70 | "eslint-plugin-prettier": "^3.0.1", 71 | "eslint-plugin-react": "^7.12.0", 72 | "graphql": "^14.0.2", 73 | "husky": "^1.3.1", 74 | "jest": "^23.6.0", 75 | "lint-staged": "^8.1.0", 76 | "prettier": "^1.15.3", 77 | "prop-types": "^15.6.2", 78 | "react": "16.7.0-alpha.2", 79 | "react-dom": "16.7.0-alpha.2", 80 | "react-testing-library": "^5.4.2", 81 | "rimraf": "^2.6.2", 82 | "webpack": "^4.28.2", 83 | "webpack-cli": "^3.1.2" 84 | }, 85 | "peerDependencies": { 86 | "graphql": "^14.0.2", 87 | "react": "^16.2.0", 88 | "react-dom": "^16.2.0" 89 | }, 90 | "lint-staged": { 91 | "*.{js,md,ts,json}": [ 92 | "prettier --write", 93 | "git add" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "Building project" 4 | yarn build 5 | echo "\n\n" 6 | 7 | echo "Linting" 8 | yarn lint 9 | echo "\n\n" 10 | 11 | echo "Running tests" 12 | yarn test --coverage && codecov 13 | echo "\n\n" -------------------------------------------------------------------------------- /src/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GqlCSS it renders component with styles 1`] = ` 4 |
5 |
8 | Using component with styles 9 |
10 |
11 | `; 12 | 13 | exports[`GqlCSS it renders component without styles 1`] = ` 14 |
15 |
18 | Using component without styles 19 |
20 |
21 | `; 22 | 23 | exports[`useGqlCSS with GqlCSS it supports variables and stateful components 1`] = ` 24 |
25 |
29 | Using stateful component 30 |
31 |
32 | `; 33 | 34 | exports[`useGqlCSS with GqlCSS it supports variables and stateful components 2`] = ` 35 |
36 |
40 | Using stateful component 41 |
42 |
43 | `; 44 | 45 | exports[`useGqlCSS with styled it fails if interpolation is null 1`] = ` 46 |
47 |
50 | Some heading 51 |
52 |
53 | `; 54 | 55 | exports[`useGqlCSS with styled it fails if props don't exist 1`] = ` 56 |
57 |
60 | Some heading 61 |
62 |
63 | `; 64 | 65 | exports[`useGqlCSS with styled it handles style interpolation 1`] = ` 66 |
67 |

70 | Some heading 71 |

72 |
73 | `; 74 | 75 | exports[`useGqlCSS with styled it returns a styled component 1`] = ` 76 |
77 |

80 | Some heading 81 |

82 |
83 | `; 84 | 85 | exports[`useGqlCSS with styled it supports variables and stateful components 1`] = ` 86 |
87 |
90 | Other component sharing state 91 |
92 |
93 | `; 94 | 95 | exports[`useGqlCSS with styled it supports variables and stateful components 2`] = ` 96 |
97 |
100 | Other component sharing state 101 |
102 |
103 | `; 104 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import graphql from "graphql-anywhere"; 3 | import cxs from "cxs/component"; 4 | import { default as internalGql } from "graphql-tag"; 5 | import { isGqlQuery, smoosh, interleave, buildQuery, isPlainObject, domElements } from "./utils"; 6 | 7 | const resolver = (fieldName, root, args, context, { resultKey }) => { 8 | // if it's an aliased query add alias as prop 9 | if (fieldName !== resultKey) { 10 | return { 11 | ...root[fieldName], 12 | ...args, 13 | prop: resultKey, 14 | }; 15 | } 16 | 17 | let res = root[fieldName]; 18 | const rootUnit = root && root.unit; 19 | const argsUnit = args && args.unit; 20 | const argsVariant = args && args.variant; 21 | 22 | if (argsUnit || rootUnit) { 23 | const unit = argsUnit || rootUnit; 24 | res = root[fieldName] + unit; 25 | } 26 | 27 | if (argsVariant) { 28 | res = root[fieldName][argsVariant]; 29 | } 30 | 31 | // if has prop then use it as the key 32 | if (root.prop) { 33 | return { 34 | [root.prop]: res, 35 | }; 36 | } 37 | 38 | return res; 39 | }; 40 | 41 | const gqlcssFactory = (el, styles) => (query, ...interpolations) => { 42 | // It's an object from getStyles() 43 | if (isPlainObject(query) && !isGqlQuery(query)) { 44 | return cxs(el)(query); 45 | } 46 | 47 | // map domelements to factory so we can do gqlcss.h2`query` 48 | return cxs(el)(props => { 49 | try { 50 | const parsedQuery = isGqlQuery(query) 51 | ? query 52 | : internalGql(buildQuery(interleave(query, interpolations), props).join("")); 53 | 54 | return smoosh(graphql(resolver, parsedQuery, styles)); 55 | } catch (e) { 56 | // eslint-disable-next-line no-console 57 | console.error("Not a valid gql query. Did you forget a prop?"); 58 | return {}; 59 | } 60 | }); 61 | }; 62 | 63 | // Hook-like function that returns gqlcss template tag, getStyles function and GqlCSS component 64 | const useGqlCSS = (styles = {}) => { 65 | const getStyles = (query, variables) => { 66 | if (!isGqlQuery(query)) { 67 | throw new Error("Query must be a valid gql query"); 68 | } 69 | 70 | const generatedStyles = smoosh(graphql(resolver, query, styles, null, variables)); 71 | 72 | return generatedStyles; 73 | }; 74 | 75 | const gqlcss = gqlcssFactory("div", styles); 76 | domElements.forEach(domElement => { 77 | gqlcss[domElement] = gqlcssFactory(domElement, styles); 78 | }); 79 | 80 | const GqlCSSComponent = props => GqlCSS({ styles, ...props }); 81 | 82 | return { styled: gqlcss, getStyles, GqlCSS: GqlCSSComponent }; 83 | }; 84 | 85 | // Export Component for more declarative API 86 | export const GqlCSS = ({ component = "div", query, styles, variables, ...rest }) => { 87 | const { styled, getStyles } = useGqlCSS(styles); 88 | const Component = styled[component](getStyles(query, variables)); 89 | 90 | return ; 91 | }; 92 | 93 | // Export gql since it's already a dependency anyway 94 | export const gql = internalGql; 95 | 96 | // Export hook by default 97 | export default useGqlCSS; 98 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import React, { useState } from "react"; 4 | import { render, cleanup, fireEvent } from "react-testing-library"; 5 | import useGqlCSS, { gql, GqlCSS as GqlCSSComponent } from "./index"; 6 | import styles from "../examples/styleguide"; 7 | import { h2Styles, stateStyles } from "../examples/styleQueries"; 8 | import PropTypes from "prop-types"; 9 | 10 | afterEach(cleanup); 11 | 12 | global.console = { 13 | error: jest.fn(), 14 | }; 15 | 16 | describe("useGqlCSS", () => { 17 | describe("with styled", () => { 18 | it("it returns a styled component", () => { 19 | const { styled } = useGqlCSS(styles); 20 | const H2 = styled.h2(h2Styles); 21 | const { container } = render(

Some heading

); 22 | expect(container).toMatchSnapshot(); 23 | }); 24 | 25 | it("it supports variables and stateful components", () => { 26 | function StatefulComponent() { 27 | const [variant, setVariant] = useState("normal"); 28 | const { styled } = useGqlCSS(styles); 29 | const toggleVariant = () => 30 | setVariant(state => (state === "normal" ? "done" : "normal")); 31 | const Button = styled`{ 32 | theme(variant: ${props => props.variant}) { 33 | button 34 | } 35 | base { 36 | marginLeft: spacing { 37 | m 38 | } 39 | } 40 | }`; 41 | Button.propTypes = { 42 | variant: PropTypes.string.isRequired, 43 | }; 44 | 45 | return ( 46 | 49 | ); 50 | } 51 | 52 | const { container } = render(); 53 | 54 | expect(container).toMatchSnapshot(); 55 | 56 | fireEvent.click(container.firstChild); 57 | 58 | expect(container).toMatchSnapshot(); 59 | }); 60 | 61 | it("it fails if props don't exist", () => { 62 | const { styled } = useGqlCSS(styles); 63 | const H2 = styled`{ 64 | theme(variant: ${props => props.variant}) { 65 | button 66 | } 67 | base { 68 | marginLeft: spacing { 69 | m 70 | } 71 | } 72 | }`; 73 | H2.propTypes = { 74 | variant: PropTypes.string, 75 | }; 76 | 77 | const { container } = render(

Some heading

); 78 | expect(global.console.error).toHaveBeenCalledWith( 79 | "Not a valid gql query. Did you forget a prop?" 80 | ); 81 | expect(container).toMatchSnapshot(); 82 | }); 83 | 84 | it("it fails if interpolation is null", () => { 85 | const { styled } = useGqlCSS(styles); 86 | const H2 = styled`{ 87 | theme(variant: ${null}) { 88 | button 89 | } 90 | base { 91 | marginLeft: spacing { 92 | m 93 | } 94 | } 95 | }`; 96 | 97 | const { container } = render(

Some heading

); 98 | expect(global.console.error).toHaveBeenCalledWith( 99 | "Not a valid gql query. Did you forget a prop?" 100 | ); 101 | expect(container).toMatchSnapshot(); 102 | }); 103 | 104 | it("it handles style interpolation", () => { 105 | const { styled } = useGqlCSS(styles); 106 | const color = "blue"; 107 | const H3 = styled.h3`{ 108 | base { 109 | marginLeft: spacing { 110 | m 111 | } 112 | } 113 | 114 | ${props => 115 | props.primary && 116 | ` 117 | base { 118 | color: colors { 119 | ${color} 120 | } 121 | } 122 | `} 123 | }`; 124 | H3.propTypes = { 125 | primary: PropTypes.bool.isRequired, 126 | }; 127 | H3.defaultProps = { 128 | primary: false, 129 | }; 130 | 131 | const { container } = render(

Some heading

); 132 | expect(container).toMatchSnapshot(); 133 | }); 134 | }); 135 | 136 | describe("with getStyles", () => { 137 | it("it allows the extraction of styles", () => { 138 | const { getStyles } = useGqlCSS(styles); 139 | const query = gql` 140 | { 141 | base { 142 | typography { 143 | fontSize: scale { 144 | l 145 | } 146 | fontWeight: weight { 147 | bold 148 | } 149 | } 150 | marginLeft: spacing { 151 | xl 152 | } 153 | color: colors { 154 | green 155 | } 156 | } 157 | } 158 | `; 159 | const extractedStyles = getStyles(query); 160 | expect(extractedStyles).toEqual({ 161 | color: "green", 162 | fontSize: "36px", 163 | fontWeight: 700, 164 | marginLeft: "32px", 165 | }); 166 | }); 167 | 168 | it("it supports variables", () => { 169 | const { getStyles } = useGqlCSS(styles); 170 | const query = gql` 171 | { 172 | theme(variant: $variant) { 173 | button 174 | } 175 | } 176 | `; 177 | const extractedStyles = getStyles(query, { variant: "done" }); 178 | expect(extractedStyles).toEqual({ 179 | backgroundColor: "green", 180 | cursor: "pointer", 181 | fontSize: 36, 182 | padding: 24, 183 | }); 184 | }); 185 | 186 | it("it handles fragments", () => { 187 | const { getStyles } = useGqlCSS(styles); 188 | const headingStyles = gql` 189 | fragment Heading on Styles { 190 | base { 191 | typography { 192 | fontSize: scale { 193 | xl 194 | } 195 | fontWeight: weight { 196 | bold 197 | } 198 | } 199 | } 200 | } 201 | `; 202 | const query = gql` 203 | ${headingStyles} 204 | { 205 | ...Heading 206 | base { 207 | color: colors { 208 | blue 209 | } 210 | } 211 | } 212 | `; 213 | const extractedStyles = getStyles(query); 214 | expect(extractedStyles).toEqual({ 215 | color: "blue", 216 | fontSize: "52px", 217 | fontWeight: 700, 218 | }); 219 | }); 220 | 221 | it("it handles custom units", () => { 222 | const { getStyles } = useGqlCSS(styles); 223 | const query = gql` 224 | { 225 | base { 226 | typography { 227 | fontSize: scale { 228 | l 229 | } 230 | fontWeight: weight { 231 | bold 232 | } 233 | } 234 | marginLeft: spacing(unit: "em") { 235 | s 236 | } 237 | color: colors { 238 | green 239 | } 240 | } 241 | } 242 | `; 243 | const extractedStyles = getStyles(query); 244 | expect(extractedStyles).toEqual({ 245 | color: "green", 246 | fontSize: "36px", 247 | fontWeight: 700, 248 | marginLeft: "4em", 249 | }); 250 | }); 251 | 252 | it("it only supports gql queries", () => { 253 | const { getStyles } = useGqlCSS(styles); 254 | const query = "something else"; 255 | expect(() => getStyles(query)).toThrowError("Query must be a valid gql query"); 256 | }); 257 | }); 258 | 259 | describe("with GqlCSS", () => { 260 | it("it supports variables and stateful components", () => { 261 | function StatefulComponent() { 262 | const [variant, setVariant] = useState("normal"); 263 | const { GqlCSS } = useGqlCSS(styles); 264 | const toggleVariant = () => 265 | setVariant(state => (state === "normal" ? "done" : "normal")); 266 | 267 | return ( 268 | 274 | Using stateful component 275 | 276 | ); 277 | } 278 | 279 | const { container, queryByTestId } = render(); 280 | 281 | expect(container).toMatchSnapshot(); 282 | 283 | fireEvent.click(queryByTestId("stateful-component")); 284 | 285 | expect(container).toMatchSnapshot(); 286 | }); 287 | }); 288 | }); 289 | 290 | describe("GqlCSS", () => { 291 | it("it renders component without styles", () => { 292 | const { container } = render( 293 | Using component without styles 294 | ); 295 | 296 | expect(container).toMatchSnapshot(); 297 | }); 298 | 299 | it("it renders component with styles", () => { 300 | const { container } = render( 301 | 302 | Using component with styles 303 | 304 | ); 305 | 306 | expect(container).toMatchSnapshot(); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function isFalsish(chunk) { 2 | return chunk === undefined || chunk === null || chunk === false || chunk === ""; 3 | } 4 | 5 | export function isPlainObject(x) { 6 | return typeof x === "object" && x.constructor === Object; 7 | } 8 | 9 | export function isFunction(test) { 10 | return typeof test === "function"; 11 | } 12 | 13 | export function buildQuery(chunk, props) { 14 | if (Array.isArray(chunk)) { 15 | const ruleSet = []; 16 | 17 | for (let i = 0, len = chunk.length, result; i < len; i += 1) { 18 | result = buildQuery(chunk[i], props); 19 | 20 | if (result === null) continue; 21 | else if (Array.isArray(result)) ruleSet.push(...result); 22 | else ruleSet.push(result); 23 | } 24 | 25 | return ruleSet; 26 | } 27 | 28 | if (isFalsish(chunk)) { 29 | return null; 30 | } 31 | 32 | if (isFunction(chunk)) { 33 | return chunk(props); 34 | } 35 | 36 | return chunk.toString(); 37 | } 38 | 39 | export function isGqlQuery(query) { 40 | return typeof query === "object" && query.constructor === Object && query.kind === "Document"; 41 | } 42 | 43 | export function interleave(strings, interpolations = []) { 44 | const result = [strings[0]]; 45 | 46 | for (let i = 0, len = interpolations.length; i < len; i += 1) { 47 | result.push(interpolations[i], strings[i + 1]); 48 | } 49 | 50 | return result; 51 | } 52 | 53 | export function smoosh(object) { 54 | return Object.assign( 55 | {}, 56 | ...(function _flatten(objectBit) { 57 | return [].concat( 58 | ...Object.keys(objectBit).map(key => 59 | typeof objectBit[key] === "object" 60 | ? _flatten(objectBit[key]) 61 | : { [key]: objectBit[key] } 62 | ) 63 | ); 64 | })(object) 65 | ); 66 | } 67 | 68 | export const domElements = [ 69 | "a", 70 | "abbr", 71 | "address", 72 | "area", 73 | "article", 74 | "aside", 75 | "audio", 76 | "b", 77 | "base", 78 | "bdi", 79 | "bdo", 80 | "big", 81 | "blockquote", 82 | "body", 83 | "br", 84 | "button", 85 | "canvas", 86 | "caption", 87 | "cite", 88 | "code", 89 | "col", 90 | "colgroup", 91 | "data", 92 | "datalist", 93 | "dd", 94 | "del", 95 | "details", 96 | "dfn", 97 | "dialog", 98 | "div", 99 | "dl", 100 | "dt", 101 | "em", 102 | "embed", 103 | "fieldset", 104 | "figcaption", 105 | "figure", 106 | "footer", 107 | "form", 108 | "h1", 109 | "h2", 110 | "h3", 111 | "h4", 112 | "h5", 113 | "h6", 114 | "head", 115 | "header", 116 | "hgroup", 117 | "hr", 118 | "html", 119 | "i", 120 | "iframe", 121 | "img", 122 | "input", 123 | "ins", 124 | "kbd", 125 | "keygen", 126 | "label", 127 | "legend", 128 | "li", 129 | "link", 130 | "main", 131 | "map", 132 | "mark", 133 | "marquee", 134 | "menu", 135 | "menuitem", 136 | "meta", 137 | "meter", 138 | "nav", 139 | "noscript", 140 | "object", 141 | "ol", 142 | "optgroup", 143 | "option", 144 | "output", 145 | "p", 146 | "param", 147 | "picture", 148 | "pre", 149 | "progress", 150 | "q", 151 | "rp", 152 | "rt", 153 | "ruby", 154 | "s", 155 | "samp", 156 | "script", 157 | "section", 158 | "select", 159 | "small", 160 | "source", 161 | "span", 162 | "strong", 163 | "style", 164 | "sub", 165 | "summary", 166 | "sup", 167 | "table", 168 | "tbody", 169 | "td", 170 | "textarea", 171 | "tfoot", 172 | "th", 173 | "thead", 174 | "time", 175 | "title", 176 | "tr", 177 | "track", 178 | "u", 179 | "ul", 180 | "var", 181 | "video", 182 | "wbr", 183 | 184 | // SVG 185 | "circle", 186 | "clipPath", 187 | "defs", 188 | "ellipse", 189 | "foreignObject", 190 | "g", 191 | "image", 192 | "line", 193 | "linearGradient", 194 | "mask", 195 | "path", 196 | "pattern", 197 | "polygon", 198 | "polyline", 199 | "radialGradient", 200 | "rect", 201 | "stop", 202 | "svg", 203 | "text", 204 | "tspan", 205 | ]; 206 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | var webpack = require("webpack"); 3 | var path = require("path"); 4 | var env = process.env.NODE_ENV; 5 | 6 | var reactExternal = { 7 | root: "React", 8 | commonjs2: "react", 9 | commonjs: "react", 10 | amd: "React", 11 | }; 12 | 13 | var reactDomExternal = { 14 | commonjs: "react-dom", 15 | commonjs2: "react-dom", 16 | amd: "ReactDOM", 17 | root: "ReactDOM", 18 | }; 19 | 20 | var config = { 21 | externals: { 22 | react: reactExternal, 23 | "react-dom": reactDomExternal, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: ["babel-loader"], 30 | exclude: /node_modules/, 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | modules: [path.join(__dirname, "./src/"), "node_modules"], 36 | extensions: [".js", ".jsx"], 37 | }, 38 | output: { 39 | library: "GraphqlCSS", 40 | libraryTarget: "umd", 41 | }, 42 | plugins: [ 43 | new webpack.optimize.OccurrenceOrderPlugin(), 44 | new webpack.DefinePlugin({ 45 | "process.env.NODE_ENV": JSON.stringify(env), 46 | }), 47 | ], 48 | }; 49 | 50 | if (env === "production") { 51 | config.plugins.push( 52 | new webpack.LoaderOptionsPlugin({ 53 | minimize: true, 54 | }), 55 | new webpack.optimize.ModuleConcatenationPlugin() 56 | ); 57 | } 58 | 59 | module.exports = config; 60 | --------------------------------------------------------------------------------