diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..e98af08e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +dist +es +lib +build diff --git a/.eslintrc.js b/.eslintrc.js index a0cfca0f..5e73ead0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'no-unused-vars': 'error', - 'linebreak-style': ['error', 'unix'], + 'linebreak-style': ['error', 'unix'] } -}; +} diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md index c4e4b686..c45d3571 100644 --- a/.github/ISSUE_TEMPLATE/Bug_Report.md +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -4,7 +4,6 @@ about: Bugs or any unexpected issues. title: '' labels: bug assignees: '' - --- ### Package diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md index 1bf953a5..db6a2935 100644 --- a/.github/ISSUE_TEMPLATE/Feature_Request.md +++ b/.github/ISSUE_TEMPLATE/Feature_Request.md @@ -4,7 +4,6 @@ about: I have a suggestion for a new feature! title: '' labels: feature assignees: '' - --- ### Description diff --git a/.gitignore b/.gitignore index 25b10add..60754ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist es packages/graphql-hooks/README.md packages/*/LICENSE +build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..94247d71 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +es +lib +build +CHANGELOG.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c202fe2c..b6ba1058 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,8 @@ We use [Lerna](https://lernajs.io) to manage this monorepo, you will find the di Clone the repository and run `npm install`. This will install the root dependencies and all of the dependencies required by each package, using `lerna bootstrap`. +If you want to test your changes against an example app then take a look at our [fastify-ssr example](examples/fastify-ssr). + All contributions should use the [prettier](https://prettier.io/) formatter, pass linting and pass tests. You can do this by running: diff --git a/README.md b/README.md index 0e7d6edc..3b11c07b 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,14 @@ const client = new GraphQLClient(config) ```js import { ClientContext } from 'graphql-hooks' - - {/* children can now consume the client context */} - + +function App() { + return ( + + {/* children can now consume the client context */} + + ) +} ``` To access the `GraphQLClient` instance, call `React.useContext(ClientContext)`: diff --git a/examples/fastify-ssr/README.md b/examples/fastify-ssr/README.md new file mode 100644 index 00000000..bb02e7ef --- /dev/null +++ b/examples/fastify-ssr/README.md @@ -0,0 +1,32 @@ +# GraphQL Hooks Fastify SSR Example + +This example uses [Fastify](https://github.com/fastify/fastify) to serve a graphql server and do server side rendering. It's very basic but showcases the different functionality that `graphql-hooks` offers. This example is intended more for local development of the `graphql-hooks` packages. + +## How to use + +### Running as part of this repo + +In the root of this repository run: + +```bash +npm install +lerna run build +cd examples/fastify-ssr +npm run watch +``` + +To develop `packages/` with this example locally, you'll need to run `lerna run build` from the root to rebuild files after they've been changed. + +### Download the example in isolation: + +```bash +curl https://codeload.github.com/nearform/graphql-hooks/tar.gz/master | tar -xz --strip=2 graphql-hooks-master/examples/fastify-ssr +cd fastify-ssr +``` + +Install it and run: + +```bash +npm install +npm run watch +``` diff --git a/examples/fastify-ssr/package.json b/examples/fastify-ssr/package.json new file mode 100644 index 00000000..3298a4d6 --- /dev/null +++ b/examples/fastify-ssr/package.json @@ -0,0 +1,67 @@ +{ + "name": "fastify-ssr", + "version": "1.0.0", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build:client": "webpack", + "build:server": "babel src -d lib --copy-files", + "prebuild": "rm -rf build lib", + "build": "npm run build:server && npm run build:client", + "start": "node ./lib/index.js", + "watch:client": "webpack --watch", + "watch:server": "nodemon --watch src --ignore src/client --exec 'npm run build:server && npm run start | pino-pretty'", + "prewatch": "npm run prebuild", + "watch": "npm run build:client && npm run watch:server & npm run watch:client" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@reach/router": "^1.2.1", + "babel-plugin-dynamic-import-node": "^2.2.0", + "fastify": "^1.14.1", + "fastify-gql": "^0.8.0", + "fastify-static": "^1.1.0", + "graphql-hooks": "^3.0.0", + "graphql-hooks-memcache": "^1.0.4", + "graphql-hooks-ssr": "^1.0.1", + "isomorphic-unfetch": "^3.0.0", + "react": "16.8.4", + "react-dom": "16.8.4", + "react-tree-walker": "^4.3.0" + }, + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.2.2", + "@babel/preset-env": "^7.3.1", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "nodemon": "^1.18.9", + "pino-pretty": "^2.5.0", + "webpack": "^4.29.1", + "webpack-cli": "^3.2.3", + "webpack-manifest-plugin": "^2.0.4", + "webpack-merge": "^4.2.1" + }, + "browserslist": "> 0.25%, not dead", + "babel": { + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-proposal-object-rest-spread", + "dynamic-import-node" + ] + } +} diff --git a/examples/fastify-ssr/src/app/AppShell.js b/examples/fastify-ssr/src/app/AppShell.js new file mode 100644 index 00000000..de1f335b --- /dev/null +++ b/examples/fastify-ssr/src/app/AppShell.js @@ -0,0 +1,30 @@ +import React from 'react' +import { Link, Router } from '@reach/router' + +// components +import NotFoundPage from './pages/NotFoundPage' +import HomePage from './pages/HomePage' +import PaginationPage from './pages/PaginationPage' + +class AppShell extends React.Component { + render() { + return ( +
+

GraphQL Hooks

+ + + + + + +
+ ) + } +} + +AppShell.propTypes = {} + +export default AppShell diff --git a/examples/fastify-ssr/src/app/components/Hello.js b/examples/fastify-ssr/src/app/components/Hello.js new file mode 100644 index 00000000..069fcd53 --- /dev/null +++ b/examples/fastify-ssr/src/app/components/Hello.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useQuery } from 'graphql-hooks' + +const HELLO_QUERY = ` + query Hello($name: String) { + hello(name: $name) + } +` + +function HelloComponent({ user }) { + const { loading, error, data } = useQuery(HELLO_QUERY, { + variables: { name: user.name } + }) + + if (loading) return 'loading HelloComponent...' + if (error) return 'error HelloComponent' + + return
{data.hello}
+} + +HelloComponent.propTypes = { + user: PropTypes.shape({ + name: PropTypes.string + }) +} + +export default HelloComponent diff --git a/examples/fastify-ssr/src/app/pages/HomePage.js b/examples/fastify-ssr/src/app/pages/HomePage.js new file mode 100644 index 00000000..1504ce8a --- /dev/null +++ b/examples/fastify-ssr/src/app/pages/HomePage.js @@ -0,0 +1,82 @@ +import React, { Fragment } from 'react' + +import { useQuery, useManualQuery, useMutation } from 'graphql-hooks' + +// components +import HelloComponent from '../components/Hello' + +const HOMEPAGE_QUERY = ` + query HomepageQuery { + users { + name + } + } +` + +const GET_FIRST_USER_QUERY = ` + query FirstUser { + firstUser { + name + } + } +` + +const CREATE_USER_MUTATION = ` + mutation CreateUser($name: String!) { + createUser(name: $name) { + name + } + } +` + +function HomePage() { + const [name, setName] = React.useState('') + const { loading, data, error, refetch: refetchUsers } = useQuery( + HOMEPAGE_QUERY + ) + const [createUser] = useMutation(CREATE_USER_MUTATION) + + const [getFirstUser, { data: firstUserData }] = useManualQuery( + GET_FIRST_USER_QUERY + ) + + async function createNewUser() { + await createUser({ variables: { name } }) + setName('') + refetchUsers() + } + + return ( +
+ Home page + {loading &&
...loading
} + {error &&
error occured
} + {!loading && !error && data.users && ( + + List of users: + {data.users.length === 0 && No users found} + {!!data.users.length && ( + + )} + + + )} +
+ setName(e.target.value)} + /> + +
+ +
First User: {firstUserData && firstUserData.firstUser.name}
+
+ ) +} + +export default HomePage diff --git a/examples/fastify-ssr/src/app/pages/NotFoundPage.js b/examples/fastify-ssr/src/app/pages/NotFoundPage.js new file mode 100644 index 00000000..f8b89857 --- /dev/null +++ b/examples/fastify-ssr/src/app/pages/NotFoundPage.js @@ -0,0 +1,7 @@ +import React from 'react' + +function NotFoundPage() { + return
404 - Not Found
+} + +export default NotFoundPage diff --git a/examples/fastify-ssr/src/app/pages/PaginationPage.js b/examples/fastify-ssr/src/app/pages/PaginationPage.js new file mode 100644 index 00000000..5a107588 --- /dev/null +++ b/examples/fastify-ssr/src/app/pages/PaginationPage.js @@ -0,0 +1,36 @@ +import React from 'react' + +import { useQuery } from 'graphql-hooks' + +const USERS_QUERY = ` + query UsersQuery($skip: Int, $limit: Int) { + users(skip: $skip, limit: $limit) { + name + } + } +` + +function PaginationPage() { + const [page, setPage] = React.useState(1) + const { data } = useQuery(USERS_QUERY, { + variables: { + limit: 1, + skip: page - 1 + } + }) + + return ( +
+ User Pagination Users: + + + +
+ ) +} + +export default PaginationPage diff --git a/examples/fastify-ssr/src/client/js/app-shell.js b/examples/fastify-ssr/src/client/js/app-shell.js new file mode 100644 index 00000000..76ecaf5d --- /dev/null +++ b/examples/fastify-ssr/src/client/js/app-shell.js @@ -0,0 +1,22 @@ +import React from 'react' +import { hydrate } from 'react-dom' + +import AppShell from '../../app/AppShell' + +// graphql-hooks +import { GraphQLClient, ClientContext } from 'graphql-hooks' +import memCache from 'graphql-hooks-memcache' + +const initialState = window.__INITIAL_STATE__ +const client = new GraphQLClient({ + url: '/graphql', + cache: memCache({ initialState }) +}) + +const App = ( + + + +) + +hydrate(App, document.getElementById('app-root')) diff --git a/examples/fastify-ssr/src/index.js b/examples/fastify-ssr/src/index.js new file mode 100644 index 00000000..6a926796 --- /dev/null +++ b/examples/fastify-ssr/src/index.js @@ -0,0 +1 @@ +require('./server')() diff --git a/examples/fastify-ssr/src/server/graphql/index.js b/examples/fastify-ssr/src/server/graphql/index.js new file mode 100644 index 00000000..b7897841 --- /dev/null +++ b/examples/fastify-ssr/src/server/graphql/index.js @@ -0,0 +1,18 @@ +const fastifyGQL = require('fastify-gql') + +const schema = require('./schema') +const resolvers = require('./resolvers') + +function registerGraphQL(fastify, opts, next) { + fastify.register(fastifyGQL, { + schema, + resolvers, + graphiql: true + }) + + next() +} + +registerGraphQL[Symbol.for('skip-override')] = true + +module.exports = registerGraphQL diff --git a/examples/fastify-ssr/src/server/graphql/resolvers.js b/examples/fastify-ssr/src/server/graphql/resolvers.js new file mode 100644 index 00000000..f60c20ba --- /dev/null +++ b/examples/fastify-ssr/src/server/graphql/resolvers.js @@ -0,0 +1,28 @@ +const users = [ + { + name: 'Brian' + }, + { + name: 'Jack' + }, + { + name: 'Joe' + } +] + +module.exports = { + Query: { + users: (_, { skip = 0, limit }) => { + const end = limit ? skip + limit : undefined + return users.slice(skip, end) + }, + firstUser: () => users[0], + hello: (_, { name }) => `Hello ${name}` + }, + Mutation: { + createUser: (_, user) => { + users.push(user) + return user + } + } +} diff --git a/examples/fastify-ssr/src/server/graphql/schema.js b/examples/fastify-ssr/src/server/graphql/schema.js new file mode 100644 index 00000000..dcbffb0c --- /dev/null +++ b/examples/fastify-ssr/src/server/graphql/schema.js @@ -0,0 +1,15 @@ +module.exports = ` + type User { + name: String + } + + type Query { + users(skip: Int, limit: Int): [User] + firstUser: User + hello(name: String): String + } + + type Mutation { + createUser(name: String!): User + } +` diff --git a/examples/fastify-ssr/src/server/handlers/app-shell.js b/examples/fastify-ssr/src/server/handlers/app-shell.js new file mode 100644 index 00000000..48592dfb --- /dev/null +++ b/examples/fastify-ssr/src/server/handlers/app-shell.js @@ -0,0 +1,73 @@ +const React = require('react') +const ReactDOMServer = require('react-dom/server') +const { ServerLocation } = require('@reach/router') + +// graphql-hooks +const { getInitialState } = require('graphql-hooks-ssr') +const { GraphQLClient, ClientContext } = require('graphql-hooks') +const memCache = require('graphql-hooks-memcache') + +// components +const { default: AppShell } = require('../../app/AppShell') + +// helpers +const { getBundlePath } = require('../helpers/manifest') + +function renderHead() { + return ` + + Hello World! + + ` +} + +async function renderScripts({ initialState }) { + const appShellBundlePath = await getBundlePath('app-shell.js') + return ` + + + ` +} + +async function appShellHandler(req, reply) { + const head = renderHead() + + const client = new GraphQLClient({ + url: 'http://127.0.0.1:3000/graphql', + cache: memCache(), + fetch: require('isomorphic-unfetch'), + logErrors: true + }) + + const App = ( + + + + + + ) + + const initialState = await getInitialState({ App, client }) + const content = ReactDOMServer.renderToString(App) + const scripts = await renderScripts({ initialState }) + + const html = ` + + + ${head} + +
${content}
+ ${scripts} + + + ` + + reply.type('text/html').send(html) +} + +module.exports = appShellHandler diff --git a/examples/fastify-ssr/src/server/helpers/manifest.js b/examples/fastify-ssr/src/server/helpers/manifest.js new file mode 100644 index 00000000..a7a34a9b --- /dev/null +++ b/examples/fastify-ssr/src/server/helpers/manifest.js @@ -0,0 +1,37 @@ +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') + +const readFileAsync = promisify(fs.readFile) + +const manifestPath = path.join(process.cwd(), 'build/public/js/manifest.json') +let cachedManifest + +getManifest() + +async function getManifest() { + if (cachedManifest) { + return cachedManifest + } + + try { + cachedManifest = JSON.parse(await readFileAsync(manifestPath)) + return cachedManifest + } catch (error) { + if (error.code !== 'ENOENT') { + throw error + } + + return {} + } +} + +async function getBundlePath(manifestKey) { + const manifest = await getManifest() + return manifest[manifestKey] || manifestKey +} + +module.exports = { + getManifest, + getBundlePath +} diff --git a/examples/fastify-ssr/src/server/index.js b/examples/fastify-ssr/src/server/index.js new file mode 100644 index 00000000..0ff9c12f --- /dev/null +++ b/examples/fastify-ssr/src/server/index.js @@ -0,0 +1,25 @@ +const path = require('path') +const fastify = require('fastify') + +// plugins +const graphqlPlugin = require('./graphql') + +// handlers +const appShellHandler = require('./handlers/app-shell') + +module.exports = () => { + const app = fastify({ + logger: true + }) + + app.register(require('fastify-static'), { + root: path.join(process.cwd(), 'build/public') + }) + + app.register(graphqlPlugin) + + app.get('/', appShellHandler) + app.get('/users', appShellHandler) + + app.listen(3000) +} diff --git a/examples/fastify-ssr/webpack.config.js b/examples/fastify-ssr/webpack.config.js new file mode 100644 index 00000000..2da01f64 --- /dev/null +++ b/examples/fastify-ssr/webpack.config.js @@ -0,0 +1,80 @@ +const path = require('path') +const merge = require('webpack-merge') +const ManifestPlugin = require('webpack-manifest-plugin') + +const PATHS = { + build: path.join(__dirname, 'build'), + src: path.join(__dirname, 'src'), + node_modules: path.join(__dirname, 'node_modules') +} + +const commonConfig = { + entry: { + 'app-shell': path.join(PATHS.src, 'client/js/app-shell.js') + }, + devtool: '#cheap-module-source-map', + module: { + rules: [ + { + test: /\.(js|jsx)$/, + loader: 'babel-loader', + options: { + babelrc: false, + presets: [ + [ + '@babel/preset-env', + { + targets: '> 5%, not dead' + } + ], + '@babel/preset-react' + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + 'dynamic-import-node' + ] + } + } + ] + }, + resolve: { + extensions: ['.js', '.jsx', '.json'], + modules: [PATHS.src, 'node_modules'], + symlinks: false + }, + output: { + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: '/js/', + path: path.join(PATHS.build, 'public/js') + }, + plugins: [new ManifestPlugin()] +} + +const productionConfig = { + mode: 'production', + optimization: { + minimize: false + }, + output: { + filename: '[chunkhash].[name].js', + chunkFilename: '[chunkhash].[name].js', + publicPath: '/js/', + path: path.join(PATHS.build, 'public/js') + } +} + +const developmentConfig = { + mode: 'development' +} + +module.exports = () => { + if ( + process.env.NODE_ENV === 'production' || + process.env.NODE_ENV === 'staging' + ) { + return merge(commonConfig, productionConfig) + } + + return merge(commonConfig, developmentConfig) +} diff --git a/lerna.json b/lerna.json index 84a85206..f850e601 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "packages": ["packages/*"], + "packages": ["packages/*", "examples/*"], "version": "independent", "command": { "bootstrap": { @@ -11,6 +11,9 @@ }, "version": { "conventionalCommits": true + }, + "bootstrap": { + "hoist": ["react", "react-dom"] } } } diff --git a/package.json b/package.json index cb8af8ad..4e5cd525 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "test:coverage": "jest --coverage", "postinstall": "lerna bootstrap --no-ci", "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", - "eslint": "eslint *.js packages/*/*.js packages/*/{src,test}/**/*.js", - "prettier": "prettier *.js *.md packages/*/*.{js,md} packages/*/{src,test}/**/*.{js,md} --write", + "eslint": "eslint '**/*.js'", + "prettier": "prettier '**/*.js' '**/*.md' --write", "release": "lerna publish" }, "devDependencies": { @@ -35,17 +35,7 @@ "rollup-plugin-terser": "4.0.4" }, "lint-staged": { - "./packages/*/{src,test}/**/*.js": [ - "eslint", - "prettier --write", - "git add" - ], - "./packages/*/*.js": [ - "eslint", - "prettier --write", - "git add" - ], - "*.js": [ + "**/*.js": [ "eslint", "prettier --write", "git add"