├── .nvmrc ├── packages └── e2e │ ├── .nvmrc │ ├── cypress │ ├── fixtures │ │ └── .gitkeep │ ├── support │ │ └── index.js │ ├── integration │ │ └── basic.spec.js │ └── plugins │ │ └── index.js │ ├── cypress.json │ └── package.json ├── .browserslistrc ├── next-env.d.ts ├── src ├── __mocks__ │ └── fileMock.js ├── utils │ ├── fixtures │ │ └── welcomeFixture.tsx │ ├── AllTheProviders.tsx │ ├── testUtils.tsx │ ├── RouterProvider.tsx │ ├── UrqlProvider.tsx │ └── fetchMocks.tsx ├── css │ └── tailwind.css ├── @types │ ├── typings.d.ts │ └── extend-expect.d.ts ├── components │ └── Layout.tsx ├── __tests__ │ └── pages │ │ └── index.test.tsx └── helpers │ ├── initUrqlClient.ts │ └── withUrqlClient.tsx ├── renovate.json ├── static ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── manifest.json ├── tailwind.config.js ├── .nowignore ├── .eslintignore ├── .prettierignore ├── .storybook ├── addons.js ├── webpack.config.js └── config.js ├── workflows └── action-cypress │ ├── .dockerignore │ ├── entrypoint.sh │ └── Dockerfile ├── stories ├── ui.stories.tsx └── base.css ├── babel.config.js ├── .gitignore ├── scripts └── deploy-ci.sh ├── jest ├── identity-obj-proxy-esm.js └── setup.js ├── postcss.config.js ├── now.json ├── pages ├── api │ └── graphql.ts ├── _app.tsx ├── index.tsx └── _document.tsx ├── jest.config.js ├── .github └── workflows │ └── workflow.yml ├── tsconfig.json ├── .eslintrc.js ├── next.config.js ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.3 2 | -------------------------------------------------------------------------------- /packages/e2e/.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.3 2 | -------------------------------------------------------------------------------- /packages/e2e/cypress/fixtures/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 4 versions 3 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": "chrome" 3 | } 4 | -------------------------------------------------------------------------------- /src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/fixtures/welcomeFixture.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Anthony', 3 | }; 4 | -------------------------------------------------------------------------------- /src/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/next-level/master/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {}, 4 | }, 5 | variants: {}, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | .next 5 | coverage 6 | dist 7 | .DS_Store 8 | /bundledOutputs/ 9 | storybook-static 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | .next 5 | coverage 6 | dist 7 | .DS_Store 8 | /bundledOutputs/ 9 | storybook-static 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | .next 5 | coverage 6 | dist 7 | .DS_Store 8 | /bundledOutputs/ 9 | storybook-static 10 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-viewport/register'; 4 | -------------------------------------------------------------------------------- /packages/e2e/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | const baseHost = 'http://localhost:3000'; 2 | 3 | Cypress.Commands.add('openPage', (pageUrl = '/') => { 4 | cy.visit(`${baseHost}${pageUrl}`); 5 | }); 6 | -------------------------------------------------------------------------------- /workflows/action-cypress/.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore all files by default 2 | * 3 | # include required files with an exception 4 | !entrypoint.sh 5 | !LICENSE 6 | !README.md 7 | !THIRD_PARTY_NOTICE.md 8 | -------------------------------------------------------------------------------- /src/@types/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Global { 3 | page: any; 4 | fetch: any; 5 | } 6 | } 7 | 8 | declare module '*.css'; 9 | 10 | declare module 'react-ssr-prepass'; 11 | -------------------------------------------------------------------------------- /stories/ui.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import './base.css'; 5 | 6 | storiesOf('Div', module).add('Fake', () => { 7 | return
Fake
; 8 | }); 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache(true); 3 | 4 | return { 5 | presets: ['next/babel'], 6 | plugins: [ 7 | '@babel/proposal-class-properties', 8 | '@babel/proposal-object-rest-spread', 9 | ], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | .next 5 | coverage 6 | dist 7 | .DS_Store 8 | /bundledOutputs/ 9 | storybook-static 10 | 11 | # Cypress 12 | packages/e2e/cypress.env.json 13 | packages/e2e/cypress/videos/* 14 | packages/e2e/cypress/screenshots/* 15 | -------------------------------------------------------------------------------- /src/@types/extend-expect.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jest { 2 | interface Matchers { 3 | toHaveAttribute: (attr: string, value?: string) => R; 4 | toHaveTextContent: (text: string) => R; 5 | toHaveClass: (className: string) => R; 6 | toBeInTheDOM: () => R; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout: React.FC = ({ children }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /scripts/deploy-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | BRANCH=$(echo "$GITHUB_REF" | sed -e 's/refs\/heads\///') 6 | 7 | if echo "$BRANCH" | grep "^master$"; then 8 | echo 'Deploying production'; 9 | yarn deploy:production 10 | else 11 | echo 'Deploying staging'; 12 | yarn deploy:staging 13 | fi; 14 | -------------------------------------------------------------------------------- /jest/identity-obj-proxy-esm.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = new Proxy( 3 | {}, 4 | { 5 | get: function getter(target, key) { 6 | if (key === '__esModule') { 7 | // True instead of false to pretend we're an ES module. 8 | return true; 9 | } 10 | return key; 11 | }, 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /jest/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine, jest */ 2 | 3 | global.__DEV__ = true; 4 | 5 | // date 6 | const constantDate = new Date(1506747294096); 7 | 8 | global.RealDate = Date; 9 | 10 | global.FakeDate = class extends Date { 11 | constructor() { 12 | super(); 13 | return constantDate; 14 | } 15 | }; 16 | 17 | global.Date = global.FakeDate; 18 | -------------------------------------------------------------------------------- /stories/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box !important; 3 | } 4 | 5 | html { 6 | font-size: 10px; 7 | } 8 | 9 | body { 10 | font-size: 1.6rem; 11 | margin: 0; 12 | font-family: system-ui, -apple-system, BlinkMacSystemFont, segoeUI, Roboto, 13 | Ubuntu, 'Helvetica Neue', sans-serif; 14 | -webkit-tap-highlight-color: transparent; 15 | } 16 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ config }) => { 2 | // typescript 3 | config.module.rules.push({ 4 | test: /\.(ts|tsx)$/, 5 | use: [ 6 | { 7 | loader: require.resolve('awesome-typescript-loader'), 8 | }, 9 | ], 10 | }); 11 | config.resolve.extensions.push('.ts', '.tsx'); 12 | 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/AllTheProviders.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RouterProvider from '../utils/RouterProvider'; 3 | import UrqlProvider from './UrqlProvider'; 4 | 5 | const AllTheProviders = ({ children }) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export default AllTheProviders; 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const purgecss = require('@fullhuman/postcss-purgecss')({ 2 | content: ['./src/**/*.tsx', './pages/**/*.tsx'], 3 | defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [], 4 | }); 5 | 6 | module.exports = { 7 | plugins: [ 8 | require('tailwindcss'), 9 | require('autoprefixer'), 10 | ...(process.env.NODE_ENV === 'production' ? [purgecss] : []), 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dblechoc/e2e", 3 | "private": true, 4 | "version": "0.0.0", 5 | "engines": { 6 | "node": "10.x" 7 | }, 8 | "scripts": { 9 | "e2e": "cypress run" 10 | }, 11 | "devDependencies": { 12 | "bluebird": "3.7.2", 13 | "cypress": "3.8.1", 14 | "faker": "4.1.0", 15 | "graphql-request": "1.8.2", 16 | "ora": "4.0.3", 17 | "uuid": "3.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/e2e/cypress/integration/basic.spec.js: -------------------------------------------------------------------------------- 1 | context('Index Integrations', () => { 2 | let welcome; 3 | 4 | before(() => { 5 | cy.task('getWelcome', 'beautiful').then(providedWelcome => { 6 | welcome = providedWelcome; 7 | }); 8 | }); 9 | 10 | beforeEach(() => { 11 | cy.openPage(); 12 | }); 13 | 14 | it('can display a welcome message', () => { 15 | cy.contains(`Hello ${welcome.name}!`); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/testUtils.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import AllTheProviders from '../utils/AllTheProviders'; 4 | 5 | const customRender = (ui: React.ReactElement, options?: any) => 6 | render(ui, { wrapper: AllTheProviders, ...options }); 7 | 8 | // re-export everything 9 | export * from '@testing-library/react'; 10 | 11 | // override render method 12 | export { customRender as render }; 13 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Next Level", 3 | "short_name": "next-level", 4 | "background_color": "#FFFFFF", 5 | "theme_color": "#673AB7", 6 | "description": "next.js now.sh pwa", 7 | "display": "standalone", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/static/android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "/static/android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/RouterProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { parse } from 'querystring'; 3 | import { NextRouter } from 'next/router'; 4 | 5 | const RouterContext = React.createContext(null as any); 6 | 7 | const defaultRouter: any = { 8 | route: 'index', 9 | pathname: '/', 10 | query: parse('/'), 11 | asPath: '/', 12 | }; 13 | 14 | const RouterProvider = ({ router = defaultRouter, children }) => { 15 | return ( 16 | {children} 17 | ); 18 | }; 19 | 20 | export default RouterProvider; 21 | -------------------------------------------------------------------------------- /workflows/action-cypress/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ -n "$NPM_AUTH_TOKEN" ]; then 6 | # Respect NPM_CONFIG_USERCONFIG if it is provided, default to $HOME/.npmrc 7 | NPM_CONFIG_USERCONFIG="${NPM_CONFIG_USERCONFIG-"$HOME/.npmrc"}" 8 | NPM_REGISTRY_URL="${NPM_REGISTRY_URL-registry.npmjs.org}" 9 | 10 | # Allow registry.npmjs.org to be overridden with an environment variable 11 | printf "//$NPM_REGISTRY_URL/:_authToken=$NPM_AUTH_TOKEN\nregistry=$NPM_REGISTRY_URL" > "$NPM_CONFIG_USERCONFIG" 12 | chmod 0600 "$NPM_CONFIG_USERCONFIG" 13 | fi 14 | 15 | sh -c "yarn $*" 16 | -------------------------------------------------------------------------------- /src/utils/UrqlProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | createClient, 4 | dedupExchange, 5 | cacheExchange, 6 | fetchExchange, 7 | Provider as ActualProvider, 8 | Client, 9 | } from 'urql'; 10 | 11 | type Props = { 12 | urqlClient?: Client; 13 | }; 14 | 15 | const UrqlProvider: React.FC = ({ 16 | urqlClient = createClient({ 17 | url: 'http://localhost:666/api/graphql', 18 | exchanges: [dedupExchange, cacheExchange, fetchExchange], 19 | }), 20 | children, 21 | }) => { 22 | return {children}; 23 | }; 24 | 25 | export default UrqlProvider; 26 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "next-level", 4 | "alias": "next-level.now.sh", 5 | "public": true, 6 | "regions": ["all"], 7 | "builds": [ 8 | { "src": "/static/*", "use": "@now/static" }, 9 | { "src": "next.config.js", "use": "@now/next" } 10 | ], 11 | "routes": [ 12 | { "src": "/static/(.*)", "dest": "/static/$1" }, 13 | { 14 | "src": "/service-worker.js$", 15 | "dest": "/_next/static/service-worker.js", 16 | "headers": { 17 | "cache-control": "public, max-age=43200, immutable", 18 | "Service-Worker-Allowed": "/" 19 | } 20 | }, 21 | { "src": "/(.*)", "dest": "/$1" } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer, gql } from 'apollo-server-micro'; 2 | 3 | const typeDefs = gql` 4 | type Query { 5 | welcome(name: String!): Welcome 6 | } 7 | 8 | type Welcome { 9 | name: String! 10 | } 11 | `; 12 | 13 | const resolvers = { 14 | Query: { 15 | welcome: (_: any, { name }, _context: any) => ({ 16 | name, 17 | }), 18 | }, 19 | }; 20 | 21 | const server = new ApolloServer({ 22 | typeDefs, 23 | resolvers, 24 | introspection: true, 25 | playground: true, 26 | }); 27 | 28 | export const config = { 29 | api: { 30 | bodyParser: false, 31 | }, 32 | }; 33 | 34 | export default server.createHandler({ path: '/api/graphql' }); 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['jest-canvas-mock', '/jest/setup.js'], 3 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 4 | moduleFileExtensions: ['web.js', 'js', 'jsx', 'json', 'ts', 'tsx', 'bs.js'], 5 | modulePathIgnorePatterns: ['dist'], 6 | moduleNameMapper: { 7 | '^.+\\.css$': '/jest/identity-obj-proxy-esm.js', 8 | '^.+\\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 9 | '/__mocks__/fileMock.js', 10 | }, 11 | transform: { 12 | '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', 13 | }, 14 | transformIgnorePatterns: ['/node_modules/(?!(bs-platform|re-classnames)/)'], 15 | testRegex: '/__tests__/.*\\.(js|jsx|ts|tsx)$', 16 | testPathIgnorePatterns: [ 17 | '/dist/', 18 | '/packages/e2e/', 19 | '/packages/*/dist', 20 | ], 21 | clearMocks: true, 22 | }; 23 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | configure, 3 | addDecorator, 4 | getStorybook, 5 | setAddon, 6 | } from '@storybook/react'; 7 | import centered from '@storybook/addon-centered/react'; 8 | import createPercyAddon from '@percy-io/percy-storybook'; 9 | import AllTheProviders from '../src/utils/AllTheProviders'; 10 | 11 | // automatically import all files ending in *.stories.js 12 | const req = require.context('../stories', true, /.stories.tsx?$/); 13 | function loadStories() { 14 | addDecorator(centered); 15 | addDecorator(fn => {fn()}); 16 | req.keys().forEach(filename => req(filename)); 17 | } 18 | 19 | const { percyAddon, serializeStories } = createPercyAddon(); 20 | setAddon(percyAddon); 21 | 22 | configure(loadStories, module); 23 | 24 | // NOTE: if you're using the Storybook options addon, call serializeStories *BEFORE* the setOptions call 25 | serializeStories(getStorybook); 26 | -------------------------------------------------------------------------------- /src/__tests__/pages/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GlobalWithFetchMock } from 'jest-fetch-mock'; 3 | import { render, waitForElement } from '../../utils/testUtils'; 4 | import Index from '../../../pages/index'; 5 | import { mockFetchWelcomeOnce } from '../../utils/fetchMocks'; 6 | 7 | const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock; 8 | 9 | describe('Index', () => { 10 | beforeEach(() => { 11 | // eslint-disable-next-line global-require 12 | customGlobal.fetch = require('jest-fetch-mock'); 13 | customGlobal.fetchMock = customGlobal.fetch; 14 | }); 15 | 16 | afterEach(() => { 17 | customGlobal.fetch.resetMocks(); 18 | }); 19 | 20 | it('renders a welcome text', async () => { 21 | const welcome = mockFetchWelcomeOnce()!; 22 | 23 | const { getByText } = render(); 24 | 25 | await waitForElement(() => getByText(`Hello ${welcome.name}!`)); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../src/css/tailwind.css'; 2 | import App, { Container } from 'next/app'; 3 | import Head from 'next/head'; 4 | import React from 'react'; 5 | import { Provider, Client } from 'urql'; 6 | import withUrqlClient from '../src/helpers/withUrqlClient'; 7 | 8 | interface Props extends App { 9 | urqlClient: Client; 10 | } 11 | 12 | class MyApp extends App { 13 | render() { 14 | const { Component, pageProps, urqlClient } = this.props; 15 | return ( 16 | 17 | 18 | 22 | Next Level 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default withUrqlClient(MyApp); 33 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Install, Test, Snapshot, e2e and Deploy 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | 10 | - name: Install 11 | uses: ./workflows/action-cypress/ 12 | with: 13 | args: install 14 | 15 | - name: Test 16 | uses: ./workflows/action-cypress/ 17 | with: 18 | args: ci 19 | 20 | - name: Snapshot UI 21 | uses: ./workflows/action-cypress/ 22 | env: 23 | PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} 24 | with: 25 | args: snapshot-ui 26 | 27 | - name: End to End 28 | uses: ./workflows/action-cypress/ 29 | with: 30 | args: e2e 31 | 32 | - name: Deploy 33 | uses: ./workflows/action-cypress/ 34 | env: 35 | NOW_TOKEN: ${{ secrets.NOW_TOKEN }} 36 | with: 37 | args: deploy 38 | -------------------------------------------------------------------------------- /src/utils/fetchMocks.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalWithFetchMock } from 'jest-fetch-mock'; 2 | import welcomeFixture from './fixtures/welcomeFixture'; 3 | 4 | const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock; 5 | 6 | type Welcome = typeof welcomeFixture; 7 | 8 | export function mockFetchWelcomeOnce({ 9 | delay = 0, 10 | welcome = welcomeFixture, 11 | }: { delay?: number; welcome?: Welcome | null } = {}) { 12 | customGlobal.fetch.mockResponseOnce( 13 | () => 14 | new Promise(resolve => 15 | setTimeout( 16 | () => 17 | resolve({ 18 | body: JSON.stringify({ 19 | data: { 20 | welcome, 21 | }, 22 | }), 23 | }), 24 | delay, 25 | ), 26 | ), 27 | ); 28 | 29 | return welcome; 30 | } 31 | 32 | export function mockFetchErrorResponseOnce(message = 'fake error message') { 33 | customGlobal.fetch.mockRejectOnce(new Error(message)); 34 | 35 | return message; 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "removeComments": false, 12 | "preserveConstEnums": true, 13 | "sourceMap": true, 14 | "skipLibCheck": true, 15 | "typeRoots": ["./node_modules/@types"], 16 | "lib": ["dom", "es2015", "es2016", "es2017.object"], 17 | "strictNullChecks": true, 18 | "rootDir": "./", 19 | "baseUrl": "./", 20 | "allowJs": true, 21 | "strict": false, 22 | "forceConsistentCasingInFileNames": true, 23 | "esModuleInterop": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx"], 29 | "exclude": ["node_modules"], 30 | "awesomeTypescriptLoaderOptions": { 31 | "useBabel": true, 32 | "babelCore": "@babel/core" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/initUrqlClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createClient, 3 | dedupExchange, 4 | cacheExchange, 5 | fetchExchange, 6 | ssrExchange, 7 | Client, 8 | } from 'urql'; 9 | import fetch from 'isomorphic-fetch'; 10 | 11 | // Polyfill fetch() on the server 12 | if (typeof window === 'undefined') { 13 | global.fetch = fetch; 14 | } 15 | 16 | let urqlClient: Client | null = null; 17 | let ssrCache: any = null; 18 | 19 | export default function initUrqlClient(baseUrl: string, initialState = {}) { 20 | // Create a new client for every server-side rendered request to reset its state 21 | // for each rendered page 22 | // Reuse the client on the client-side however 23 | const isServer = typeof window === 'undefined'; 24 | if (isServer || !urqlClient) { 25 | ssrCache = ssrExchange({ initialState }); 26 | 27 | urqlClient = createClient({ 28 | url: `${baseUrl}/api/graphql`, 29 | // Active suspense mode on the server-side 30 | suspense: isServer, 31 | exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange], 32 | }); 33 | } 34 | 35 | // Return both the cache and the client 36 | return [urqlClient, ssrCache]; 37 | } 38 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { useQuery } from 'urql'; 4 | import gql from 'graphql-tag'; 5 | import Layout from '../src/components/Layout'; 6 | 7 | type QueryResponse = { 8 | welcome: { 9 | name: string; 10 | }; 11 | }; 12 | 13 | const welcomeQuery = gql` 14 | query($name: String!) { 15 | welcome(name: $name) { 16 | name 17 | } 18 | } 19 | `; 20 | 21 | const welcomeQueryVars = { 22 | name: 'beautiful', 23 | }; 24 | 25 | const Index: NextPage = () => { 26 | const [welcomeResult] = useQuery({ 27 | query: welcomeQuery, 28 | variables: welcomeQueryVars, 29 | }); 30 | 31 | if (welcomeResult.error) { 32 | return
{`Error loading welcome message: ${welcomeResult.error}`}
; 33 | } else if (welcomeResult.fetching || !welcomeResult.data) { 34 | return
Loading
; 35 | } 36 | 37 | const { welcome } = welcomeResult.data; 38 | 39 | return ( 40 | 41 |

42 | {`Hello ${welcome.name}!`} 43 |

44 |
45 | ); 46 | }; 47 | 48 | export default Index; 49 | -------------------------------------------------------------------------------- /packages/e2e/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const ora = require('ora'); 2 | const Promise = require('bluebird'); 3 | const { GraphQLClient } = require('graphql-request'); 4 | 5 | const queries = { 6 | welcomeQuery: ` 7 | query ($name: String!) { 8 | welcome(name: $name) { 9 | name 10 | } 11 | } 12 | `, 13 | }; 14 | 15 | const makeGraphRequest = (apiUrl, operation, variables, user) => { 16 | const opts = {}; 17 | 18 | if (user && user.JWT) { 19 | opts.headers = { 20 | Authorization: `Bearer ${user.JWT}`, 21 | }; 22 | } 23 | 24 | const client = new GraphQLClient(apiUrl, opts); 25 | return client.request(operation, variables); 26 | }; 27 | 28 | const apiUrl = 'http://localhost:3000/api/graphql'; 29 | 30 | const getWelcomeAsync = name => { 31 | return new Promise((resolve, reject) => { 32 | return makeGraphRequest(apiUrl, queries.welcomeQuery, { 33 | name, 34 | }) 35 | .then(({ welcome }) => { 36 | if (welcome) { 37 | resolve(welcome); 38 | } else { 39 | reject(new Error('Could not load welcome message')); 40 | } 41 | }) 42 | .catch(error => { 43 | reject(error); 44 | }); 45 | }); 46 | }; 47 | 48 | module.exports = (on, config) => { 49 | on('task', { 50 | getWelcome(name) { 51 | const spinner = ora('Looking for welcome message').start(); 52 | return getWelcomeAsync(name) 53 | .tap(() => { 54 | spinner.succeed('Found welcome message'); 55 | }) 56 | .tapCatch(err => { 57 | spinner.fail(err.message); 58 | }); 59 | }, 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /workflows/action-cypress/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | LABEL repository="https://github.com/sync/reason-graphql-demo" 4 | LABEL homepage="http://github.com/sync" 5 | LABEL maintainer="sync@github.com>" 6 | 7 | LABEL com.github.actions.name="GitHub Action for cypress" 8 | LABEL com.github.actions.description="Wraps the yarn CLI to enable common yarn commands with extra stuff added for cypress." 9 | LABEL com.github.actions.icon="package" 10 | LABEL com.github.actions.color="brown" 11 | 12 | # "fake" dbus address to prevent errors 13 | # https://github.com/SeleniumHQ/docker-selenium/issues/87 14 | ENV DBUS_SESSION_BUS_ADDRESS=/dev/null 15 | 16 | # For Cypress 17 | ENV CI=1 18 | 19 | # https://github.com/GoogleChrome/puppeteer/blob/9de34499ef06386451c01b2662369c224502ebe7/docs/troubleshooting.md#running-puppeteer-in-docker 20 | RUN apt-get update && apt-get install -y wget --no-install-recommends \ 21 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 22 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 23 | && apt-get update \ 24 | && apt-get -y install procps git less openssh-client python-dev python-pip \ 25 | && apt-get -y install libgtk2.0-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 xvfb \ 26 | && apt-get -y install curl groff jq zip libpng-dev \ 27 | && apt-get install -y dbus-x11 google-chrome-unstable \ 28 | --no-install-recommends 29 | 30 | RUN npm install -g yarn 31 | RUN npm install -g --unsafe-perm now 32 | 33 | # It's a good idea to use dumb-init to help prevent zombie chrome processes. 34 | ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init 35 | RUN chmod +x /usr/local/bin/dumb-init 36 | 37 | COPY "entrypoint.sh" "/entrypoint.sh" 38 | ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"] 39 | CMD ["help"] 40 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | sourceType: 'module', 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | 'prettier/react', 12 | 'prettier/@typescript-eslint', 13 | 'plugin:jest/recommended', 14 | 'plugin:cypress/recommended', 15 | ], 16 | plugins: ['jest', '@typescript-eslint', 'cypress'], 17 | env: { 18 | browser: true, 19 | node: true, 20 | es6: true, 21 | }, 22 | settings: { 23 | react: { 24 | version: 'detect', 25 | }, 26 | }, 27 | overrides: [ 28 | { 29 | files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], 30 | rules: { 31 | '@typescript-eslint/no-undef': 'off', 32 | '@typescript-eslint/no-unused-vars': 'off', 33 | '@typescript-eslint/spaced-comment': 'off', 34 | '@typescript-eslint/no-restricted-globals': 'off', 35 | '@typescript-eslint/explicit-member-accessibility': 'off', 36 | '@typescript-eslint/explicit-function-return-type': 'off', 37 | '@typescript-eslint/camelcase': 'off', 38 | '@typescript-eslint/no-var-requires': 'off', 39 | '@typescript-eslint/class-name-casing': 'off', 40 | '@typescript-eslint/no-explicit-any': 'off', 41 | '@typescript-eslint/prefer-interface': 'off', 42 | '@typescript-eslint/no-non-null-assertion': 'off', 43 | }, 44 | }, 45 | { 46 | files: ['**/__tests__/**', '**/__mocks__/**'], 47 | globals: { 48 | mockData: true, 49 | }, 50 | env: { 51 | jest: true, 52 | }, 53 | }, 54 | ], 55 | globals: { 56 | fetch: true, 57 | __DEV__: true, 58 | window: true, 59 | FormData: true, 60 | XMLHttpRequest: true, 61 | requestAnimationFrame: true, 62 | cancelAnimationFrame: true, 63 | page: true, 64 | browser: true, 65 | 'cypress/globals': true, 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require('next-transpile-modules'); 2 | const withOffline = require('next-offline'); 3 | const withCSS = require('@zeit/next-css'); 4 | 5 | const isDev = process.env.NODE_ENV !== 'production'; 6 | const isProd = process.env.NODE_ENV === 'production'; 7 | const disableServerless = Boolean(process.env.DISABLE_SERVERLESS); 8 | 9 | const baseTarget = disableServerless ? {} : { target: 'serverless' }; 10 | 11 | const config = { 12 | env: { 13 | isDev, 14 | isProd, 15 | }, 16 | ...baseTarget, 17 | crossOrigin: 'anonymous', 18 | webpack: config => { 19 | const rules = config.module.rules; 20 | 21 | // don't even ask my why 22 | config.node = { 23 | fs: 'empty', 24 | }; 25 | 26 | // some react native library need this 27 | rules.push({ 28 | test: /\.(gif|jpe?g|png|svg)$/, 29 | use: { 30 | loader: 'url-loader', 31 | options: { 32 | name: '[name].[ext]', 33 | }, 34 | }, 35 | }); 36 | // .mjs before .js (fixing failing now.sh deploy) 37 | config.resolve.extensions = [ 38 | '.wasm', 39 | '.mjs', 40 | '.web.js', 41 | '.web.jsx', 42 | '.ts', 43 | '.tsx', 44 | '.js', 45 | '.jsx', 46 | '.json', 47 | ]; 48 | 49 | return config; 50 | }, 51 | dontAutoRegisterSw: true, 52 | workboxOpts: { 53 | swDest: 'static/service-worker.js', 54 | runtimeCaching: [ 55 | { 56 | urlPattern: /^https?.*/, 57 | handler: 'NetworkFirst', 58 | options: { 59 | cacheName: 'https-calls', 60 | networkTimeoutSeconds: 15, 61 | expiration: { 62 | maxEntries: 150, 63 | maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month 64 | }, 65 | cacheableResponse: { 66 | statuses: [0, 200], 67 | }, 68 | }, 69 | }, 70 | ], 71 | }, 72 | pageExtensions: ['jsx', 'js', 'web.js', 'web.jsx', 'ts', 'tsx'], 73 | }; 74 | 75 | module.exports = withOffline(withCSS(withTM(config))); 76 | -------------------------------------------------------------------------------- /src/helpers/withUrqlClient.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ssrPrepass from 'react-ssr-prepass'; 3 | import { Client } from 'urql'; 4 | import { AppContext } from 'next/app'; 5 | import initUrqlClient from './initUrqlClient'; 6 | 7 | export interface Props { 8 | urqlClient: Client; 9 | urqlState: any; 10 | baseUrl: string; 11 | } 12 | 13 | const withUrqlClient = (App: any) => { 14 | return class WithUrql extends React.Component { 15 | static async getInitialProps(appCtx: AppContext) { 16 | const { 17 | Component, 18 | router, 19 | ctx: { req }, 20 | } = appCtx; 21 | 22 | // Run the wrapped component's getInitialProps function 23 | let appProps = {}; 24 | if (App.getInitialProps) { 25 | appProps = await App.getInitialProps(appCtx); 26 | } 27 | 28 | // getInitialProps is universal, but we only want 29 | // to run server-side rendered suspense on the server 30 | const isBrowser = typeof window !== 'undefined'; 31 | if (isBrowser) { 32 | return appProps; 33 | } 34 | 35 | function getBaseUrl(req) { 36 | const protocol = req.headers['x-forwarded-proto'] || 'http'; 37 | const host = req.headers['x-forwarded-host'] || req.headers.host; 38 | return `${protocol}://${host}`; 39 | } 40 | 41 | const baseUrl = req ? getBaseUrl(req) : ''; 42 | 43 | const [urqlClient, ssrCache] = initUrqlClient(baseUrl); 44 | 45 | // Run suspense and hence all urql queries 46 | await ssrPrepass( 47 | , 53 | ); 54 | 55 | // Extract the SSR query data from urql's SSR cache 56 | const urqlState = ssrCache.extractData(); 57 | 58 | return { 59 | ...appProps, 60 | baseUrl, 61 | urqlState, 62 | }; 63 | } 64 | 65 | urqlClient: Client | null; 66 | 67 | constructor(props: Props) { 68 | super(props); 69 | 70 | if (props.urqlClient) { 71 | this.urqlClient = props.urqlClient; 72 | } else { 73 | // Create the urql client and rehydrate the prefetched data 74 | const [urqlClient] = initUrqlClient(props.baseUrl, props.urqlState); 75 | this.urqlClient = urqlClient; 76 | } 77 | } 78 | 79 | render() { 80 | return ; 81 | } 82 | }; 83 | }; 84 | 85 | export default withUrqlClient; 86 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document'; 2 | import React from 'react'; 3 | 4 | const serviceWorkerRegistration = ` 5 | document.addEventListener('DOMContentLoaded', event => { 6 | if ('serviceWorker' in navigator) { 7 | window.addEventListener('load', () => { 8 | navigator.serviceWorker.register('/service-worker.js', { scope: "/" }).then(registration => { 9 | console.log('SW registered: ', registration) 10 | }).catch(registrationError => { 11 | console.log('SW registration failed: ', registrationError) 12 | }) 13 | }) 14 | } 15 | }) 16 | `; 17 | 18 | export default class MyDocument extends Document { 19 | static getInitialProps({ renderPage }) { 20 | const page = renderPage(); 21 | 22 | const styles = [ 23 |