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