├── .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 | ,
33 | ];
34 |
35 | return { ...page, styles: React.Children.toArray(styles) };
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {process.env.isProd && (
54 | <>
55 |
60 |
66 |
72 |
73 | >
74 | )}
75 |
76 |
77 |
78 |
79 |
80 |
81 | {process.env.isProd && (
82 |
86 | )}
87 |
88 |
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## next-level
2 |
3 | [View the application](https://next-level.now.sh/)
4 |
5 | Ultra high performance progressive web application built with React and Next.js.
6 |
7 | [](https://github.com/ebidel/lighthouse-badge)
8 | [](https://github.com/ebidel/lighthouse-badge)
9 | [](https://github.com/ebidel/lighthouse-badge)
10 | [](https://github.com/ebidel/lighthouse-badge)
11 | [](https://github.com/ebidel/lighthouse-badge)
12 |
13 | [](https://percy.io/Dblechoc/next-level)
14 |
15 | ## Features
16 |
17 | - Progressive web app
18 | - offline
19 | - install prompts on supported platforms
20 | - Server side rendering
21 | - Next.js 9.x (canary)
22 | - Webpack 4.x
23 | - Babel 7.x
24 | - Now.sh 2.x
25 | - Yarn (monorepo with workspaces)
26 |
27 | ## Things to know
28 |
29 | - A production build is deployed from a merge to master
30 | - A staging build is deployed from a PR against master
31 |
32 | ## Setting up the project locally
33 |
34 | First of all make sure you are using node `10.15.3` (any node 10.x would also do) and latest yarn, you can always have a look at the `engines` section of the `package.json`. Why node 8.10. We are using Now.sh to make the app available online and underneath it's using AWS lambda and you have to use Node 8.
35 |
36 | ```sh
37 | $ yarn (install)
38 | $ yarn dev
39 | ```
40 |
41 | After doing this, you'll have a server with hot-reloading running at [http://localhost:3000](http://localhost:3000).
42 |
43 | ## Run tests and friends
44 |
45 | We don't want to use snapshots, we use also use [react-testing-library](https://github.com/testing-library/react-testing-library) to avoid having to use enzyme and to enforce best test practices.
46 |
47 | ```sh
48 | $ yarn format
49 | $ yarn typecheck
50 | $ yarn lint
51 | $ yarn test
52 | ```
53 |
54 | or
55 |
56 | ```sh
57 | $ yarn ci
58 | ```
59 |
60 | ## End to end tests
61 |
62 | We use cypress. Please check `e2e` for more details.
63 | If you wan to add a new test use the following command and wait for cypress to open
64 |
65 | ```
66 | yarn e2e-build
67 | yarn start
68 | yarn workspace @dblechoc/e2e cypress open
69 | ```
70 |
71 | ## Storybook
72 |
73 | This is where we list all our components (comes with hot reloading)
74 |
75 | ```sh
76 | $ yarn storybook
77 | ```
78 |
79 | After doing this, you'll have a showcase page running at [http://localhost:6006](http://localhost:6006)
80 |
81 | ## CI
82 |
83 | We are using [Github Actions](https://help.github.com/en/articles/about-github-actions).
84 |
85 | ## Next maintenance
86 |
87 | All the documenation is located here: [https://nextjs.org/docs/#setup](https://nextjs.org/docs/#setup)
88 |
89 | ## Now maintenance
90 |
91 | All the documentation is located here: [https://zeit.co/docs](https://zeit.co/docs)
92 |
93 | ```sh
94 | # Install
95 | $ curl -sfLS https://zeit.co/download.sh | sh
96 | ```
97 |
98 | ### Useful commands
99 |
100 | ```sh
101 | # check all running instances
102 | $ now ls
103 |
104 | # check logs for a given instance
105 | $ now logs your-app.now.sh --all
106 |
107 | # check all alias (running instances to your-app.now.sh)
108 | $ now alias ls
109 | ```
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-level",
3 | "license": "ISC",
4 | "private": true,
5 | "version": "1.0.0",
6 | "engines": {
7 | "node": "10.x"
8 | },
9 | "workspaces": {
10 | "packages": [
11 | "packages/*"
12 | ]
13 | },
14 | "scripts": {
15 | "dev": "next dev",
16 | "build": "next build",
17 | "start": "DISABLE_SERVERLESS=true NODE_ENV=production next start",
18 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md,html}\"",
19 | "lint": "eslint '**/*.js{,x}' '**/*.ts{,x}'",
20 | "typecheck": "tsc --noEmit",
21 | "test": "NODE_ENV=test jest",
22 | "test-watch": "NODE_ENV=test jest -o --watch",
23 | "ci": "yarn typecheck && yarn lint && yarn test",
24 | "storybook": "start-storybook -p 6006",
25 | "build-storybook": "build-storybook",
26 | "snapshot-ui": "build-storybook && percy-storybook --widths=320,1280",
27 | "e2e-build": "DISABLE_SERVERLESS=true yarn build",
28 | "e2e-run": "yarn --cwd packages/e2e e2e",
29 | "e2e": "yarn e2e-build && start-server-and-test start 3000 e2e-run",
30 | "deploy": "scripts/deploy-ci.sh",
31 | "deploy:production": "now --token $NOW_TOKEN --target production",
32 | "deploy:staging": "now --token $NOW_TOKEN --target staging"
33 | },
34 | "dependencies": {
35 | "apollo-server-micro": "2.9.15",
36 | "graphql": "14.5.8",
37 | "graphql-tag": "2.10.1",
38 | "isomorphic-fetch": "2.2.1",
39 | "next": "9.1.7",
40 | "react": "16.12.0",
41 | "react-dom": "16.12.0",
42 | "react-is": "16.12.0",
43 | "react-ssr-prepass": "1.0.8",
44 | "urql": "1.7.0"
45 | },
46 | "devDependencies": {
47 | "@babel/core": "7.7.7",
48 | "@babel/plugin-proposal-class-properties": "7.7.4",
49 | "@babel/plugin-proposal-object-rest-spread": "7.7.7",
50 | "@fullhuman/postcss-purgecss": "1.3.0",
51 | "@percy-io/percy-storybook": "2.1.0",
52 | "@storybook/addon-actions": "5.2.8",
53 | "@storybook/addon-centered": "5.2.8",
54 | "@storybook/addon-links": "5.2.8",
55 | "@storybook/addon-viewport": "5.2.8",
56 | "@storybook/addons": "5.2.8",
57 | "@storybook/react": "5.2.8",
58 | "@testing-library/jest-dom": "4.2.4",
59 | "@testing-library/react": "9.4.0",
60 | "@types/isomorphic-fetch": "0.0.35",
61 | "@types/jest": "24.0.25",
62 | "@types/react": "16.9.17",
63 | "@typescript-eslint/eslint-plugin": "2.15.0",
64 | "@typescript-eslint/parser": "2.15.0",
65 | "@zeit/next-css": "1.0.1",
66 | "autoprefixer": "9.7.3",
67 | "awesome-typescript-loader": "5.2.1",
68 | "babel-loader": "8.0.6",
69 | "eslint": "6.8.0",
70 | "eslint-config-prettier": "6.9.0",
71 | "eslint-plugin-cypress": "2.8.1",
72 | "eslint-plugin-jest": "23.3.0",
73 | "eslint-plugin-react": "7.17.0",
74 | "husky": "4.0.1",
75 | "identity-obj-proxy": "3.0.0",
76 | "jest": "24.9.0",
77 | "jest-canvas-mock": "2.2.0",
78 | "jest-fetch-mock": "3.0.1",
79 | "jest-mock-console": "1.0.0",
80 | "lint-staged": "9.5.0",
81 | "next-offline": "4.0.6",
82 | "next-transpile-modules": "2.3.1",
83 | "prettier": "1.19.1",
84 | "start-server-and-test": "1.10.6",
85 | "styled-components": "4.4.1",
86 | "tailwindcss": "1.1.4",
87 | "typescript": "3.7.4",
88 | "webpack": "4.41.5"
89 | },
90 | "prettier": {
91 | "singleQuote": true,
92 | "trailingComma": "all",
93 | "bracketSpacing": true
94 | },
95 | "lint-staged": {
96 | "*.{ts,tsx,js,jsx,json,css,md,html}": [
97 | "prettier --write",
98 | "git add"
99 | ]
100 | },
101 | "husky": {
102 | "hooks": {
103 | "pre-commit": "lint-staged",
104 | "pre-push": "yarn lint",
105 | "post-commit": "git update-index -g"
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------