├── integration ├── using-app │ ├── next-env.d.ts │ ├── pages │ │ ├── hooks.tsx │ │ ├── index.tsx │ │ ├── router.tsx │ │ └── _app.tsx │ ├── tsconfig.json │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.test.ts ├── using-error │ ├── next-env.d.ts │ ├── pages │ │ ├── invalid.tsx │ │ ├── missing.tsx │ │ ├── unhandled.tsx │ │ └── _app.tsx │ ├── tsconfig.json │ └── index.test.ts ├── using-pages │ ├── next-env.d.ts │ ├── pages │ │ ├── hooks.tsx │ │ ├── index.tsx │ │ ├── hooks-ssr.tsx │ │ ├── ssr.tsx │ │ └── router.tsx │ ├── utils │ │ └── with-apollo.tsx │ ├── tsconfig.json │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.test.ts ├── using-app-class │ ├── next-env.d.ts │ ├── pages │ │ ├── hooks.tsx │ │ ├── index.tsx │ │ ├── router.tsx │ │ └── _app.tsx │ ├── tsconfig.json │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.test.ts ├── using-app-no-ssr │ ├── next-env.d.ts │ ├── pages │ │ ├── hooks.tsx │ │ ├── index.tsx │ │ ├── router.tsx │ │ └── _app.tsx │ ├── tsconfig.json │ └── index.test.ts └── next-test-utils.ts ├── src ├── index.ts ├── apollo.ts ├── types.ts └── withApollo.tsx ├── .editorconfig ├── .gitignore ├── .github └── workflows │ └── nodejs.yml ├── tsconfig.json ├── tslint.json ├── LICENSE ├── package.json ├── README_v1.md └── README.md /integration/using-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/using-error/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/using-pages/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/using-app-class/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import withApollo from './withApollo'; 2 | 3 | export * from './types'; 4 | export { default as initApollo } from './apollo'; 5 | export { withApollo }; 6 | export default withApollo; 7 | -------------------------------------------------------------------------------- /integration/using-error/pages/invalid.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {() => { 16 | throw new Error('invalid'); 17 | }} 18 | 19 | ); 20 | 21 | export default Index; 22 | -------------------------------------------------------------------------------- /integration/using-error/pages/missing.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {() => { 16 | throw new Error('missing'); 17 | }} 18 | 19 | ); 20 | 21 | export default Index; 22 | -------------------------------------------------------------------------------- /integration/using-error/pages/unhandled.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {() => { 16 | throw new Error('unhandled'); 17 | }} 18 | 19 | ); 20 | 21 | export default Index; 22 | -------------------------------------------------------------------------------- /integration/using-app/pages/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => { 14 | const { loading, data } = useQuery(QUERY); 15 | 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }; 22 | 23 | export default Index; 24 | -------------------------------------------------------------------------------- /integration/using-app-class/pages/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => { 14 | const { loading, data } = useQuery(QUERY); 15 | 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }; 22 | 23 | export default Index; 24 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/pages/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => { 14 | const { loading, data } = useQuery(QUERY); 15 | 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }; 22 | 23 | export default Index; 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /integration/using-app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {({ loading, data }: any) => { 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }} 22 |
23 | ); 24 | 25 | export default Index; 26 | -------------------------------------------------------------------------------- /integration/using-app-class/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {({ loading, data }: any) => { 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }} 22 |
23 | ); 24 | 25 | export default Index; 26 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | 4 | const QUERY = gql` 5 | { 6 | hire { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | const Index = () => ( 14 | 15 | {({ loading, data }: any) => { 16 | if (loading || !data) { 17 | return

loading

; 18 | } 19 | 20 | return

{data.hire.name}

; 21 | }} 22 |
23 | ); 24 | 25 | export default Index; 26 | -------------------------------------------------------------------------------- /integration/using-pages/pages/hooks.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { useQuery } from '@apollo/react-hooks'; 3 | import withApollo from '../utils/with-apollo'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | const Index = () => { 15 | const { loading, data } = useQuery(QUERY); 16 | 17 | if (loading || !data) { 18 | return

loading

; 19 | } 20 | 21 | return

{data.hire.name}

; 22 | }; 23 | 24 | export default withApollo(Index); 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test fixtures 2 | .next 3 | out 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | 15 | # Dependency directories 16 | node_modules/ 17 | 18 | # Compiled files 19 | lib 20 | dist 21 | 22 | # Yarn 23 | .yarn-integrity 24 | 25 | # Npm 26 | .npm 27 | 28 | # Optional eslint cache 29 | .eslintcache 30 | 31 | # Output of 'npm pack' 32 | *.tgz 33 | 34 | # dotenv environment variables file 35 | .env 36 | 37 | -------------------------------------------------------------------------------- /integration/using-app/pages/router.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | let e = false; 15 | 16 | const Index = () => { 17 | const router = useRouter(); 18 | useQuery(QUERY); 19 | 20 | if (!router || e) { 21 | e = true; 22 | return

null router

; 23 | } 24 | 25 | return

all good

; 26 | }; 27 | 28 | export default Index; 29 | -------------------------------------------------------------------------------- /integration/using-app-class/pages/router.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | let e = false; 15 | 16 | const Index = () => { 17 | const router = useRouter(); 18 | useQuery(QUERY); 19 | 20 | if (!router || e) { 21 | e = true; 22 | return

null router

; 23 | } 24 | 25 | return

all good

; 26 | }; 27 | 28 | export default Index; 29 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/pages/router.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks'; 2 | import gql from 'graphql-tag'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | let e = false; 15 | 16 | const Index = () => { 17 | const router = useRouter(); 18 | useQuery(QUERY); 19 | 20 | if (!router || e) { 21 | e = true; 22 | return

null router

; 23 | } 24 | 25 | return

all good

; 26 | }; 27 | 28 | export default Index; 29 | -------------------------------------------------------------------------------- /integration/using-pages/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query } from 'react-apollo'; 3 | import withApollo from '../utils/with-apollo'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | const Index = () => ( 15 | 16 | {({ loading, data }: any) => { 17 | if (loading || !data) { 18 | return

loading

; 19 | } 20 | 21 | return

{data.hire.name}

; 22 | }} 23 |
24 | ); 25 | 26 | export default withApollo(Index); 27 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install, build, and test 21 | run: | 22 | yarn install 23 | yarn test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "baseUrl": "./", 6 | "rootDir": "src", 7 | "outDir": "lib", 8 | "moduleResolution": "node", 9 | "jsx": "react", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitAny": true, 14 | "esModuleInterop": true, 15 | "declaration": true, 16 | "pretty": true, 17 | "typeRoots": ["./node_modules/@types"], 18 | "lib": ["dom"] 19 | }, 20 | "exclude": ["node_modules", "integration", "lib"] 21 | } 22 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/react-hooks'; 2 | import ApolloClient, { InMemoryCache } from 'apollo-boost'; 3 | import withApollo from '../../../lib'; 4 | 5 | const App = ({ Component, pageProps, apollo }) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default withApollo( 12 | ({ initialState }) => 13 | new ApolloClient({ 14 | uri: 'http://mocked.com/graphql', 15 | cache: new InMemoryCache().restore(initialState || {}) 16 | }) 17 | )(App); 18 | -------------------------------------------------------------------------------- /integration/using-pages/pages/hooks-ssr.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { useQuery } from '@apollo/react-hooks'; 3 | import { getDataFromTree } from '@apollo/react-ssr'; 4 | import withApollo from '../utils/with-apollo'; 5 | 6 | const QUERY = gql` 7 | { 8 | hire { 9 | id 10 | name 11 | } 12 | } 13 | `; 14 | 15 | const Index = () => { 16 | const { loading, data } = useQuery(QUERY); 17 | 18 | if (loading || !data) { 19 | return

loading

; 20 | } 21 | 22 | return

{data.hire.name}

; 23 | }; 24 | 25 | export default withApollo(Index, { getDataFromTree }); 26 | -------------------------------------------------------------------------------- /integration/using-pages/pages/ssr.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { Query, getDataFromTree } from 'react-apollo'; 3 | import withApollo from '../utils/with-apollo'; 4 | 5 | const QUERY = gql` 6 | { 7 | hire { 8 | id 9 | name 10 | } 11 | } 12 | `; 13 | 14 | const Index = () => ( 15 | 16 | {({ loading, data }: any) => { 17 | if (loading || !data) { 18 | return

loading

; 19 | } 20 | 21 | return

{data.hire.name}

; 22 | }} 23 |
24 | ); 25 | 26 | export default withApollo(Index, { getDataFromTree }); 27 | -------------------------------------------------------------------------------- /integration/using-pages/pages/router.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { useQuery } from '@apollo/react-hooks'; 3 | import { useRouter } from 'next/router'; 4 | import withApollo from '../utils/with-apollo'; 5 | 6 | const QUERY = gql` 7 | { 8 | hire { 9 | id 10 | name 11 | } 12 | } 13 | `; 14 | 15 | let e = false; 16 | 17 | const Index = () => { 18 | const router = useRouter(); 19 | useQuery(QUERY); 20 | 21 | if (!router || e) { 22 | e = true; 23 | return

null router

; 24 | } 25 | 26 | return

all good

; 27 | }; 28 | 29 | export default withApollo(Index); 30 | -------------------------------------------------------------------------------- /integration/using-pages/utils/with-apollo.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/react-hooks'; 2 | import ApolloClient, { InMemoryCache } from 'apollo-boost'; 3 | import withApollo from '../../../lib'; 4 | 5 | export default withApollo( 6 | ({ initialState }) => 7 | new ApolloClient({ 8 | uri: 'http://mocked.com/graphql', 9 | cache: new InMemoryCache().restore(initialState || {}) 10 | }), 11 | { 12 | render: ({ Page, props }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /integration/using-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/react-hooks'; 2 | import { getDataFromTree } from '@apollo/react-ssr'; 3 | import ApolloClient, { InMemoryCache } from 'apollo-boost'; 4 | import withApollo from '../../../lib'; 5 | 6 | const App = ({ Component, pageProps, apollo }) => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default withApollo( 13 | ({ initialState }) => 14 | new ApolloClient({ 15 | uri: 'http://mocked.com/graphql', 16 | cache: new InMemoryCache().restore(initialState || {}) 17 | }), 18 | { getDataFromTree } 19 | )(App); 20 | -------------------------------------------------------------------------------- /integration/using-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | // next.js defaults 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /integration/using-error/tsconfig.json: -------------------------------------------------------------------------------- 1 | // next.js defaults 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /integration/using-pages/tsconfig.json: -------------------------------------------------------------------------------- 1 | // next.js defaults 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /integration/using-app-class/tsconfig.json: -------------------------------------------------------------------------------- 1 | // next.js defaults 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | // next.js defaults 2 | { 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /integration/using-app-class/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/react-hooks'; 2 | import { getDataFromTree } from '@apollo/react-ssr'; 3 | import ApolloClient, { InMemoryCache } from 'apollo-boost'; 4 | import App from 'next/app'; 5 | import withApollo from '../../../lib'; 6 | 7 | class MyApp extends App { 8 | public render() { 9 | const { Component, pageProps, apollo } = this.props; 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | export default withApollo( 20 | ({ initialState }) => 21 | new ApolloClient({ 22 | uri: 'http://mocked.com/graphql', 23 | cache: new InMemoryCache().restore(initialState || {}) 24 | }), 25 | { getDataFromTree } 26 | )(MyApp); 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:latest", 5 | "tslint-eslint-rules", 6 | "tslint-config-prettier" 7 | ], 8 | "rulesDirectory": [ 9 | "tslint-plugin-prettier" 10 | ], 11 | "rules": { 12 | "prettier": [ 13 | true, 14 | { 15 | "singleQuote": true 16 | } 17 | ], 18 | "curly": [ 19 | true, 20 | "ignore-same-line" 21 | ], 22 | "variable-name": [ 23 | true, 24 | "ban-keywords", 25 | "check-format", 26 | "allow-leading-underscore", 27 | "allow-pascal-case" 28 | ], 29 | "interface-name": [ 30 | true, 31 | "never-prefix" 32 | ], 33 | "array-type": false, 34 | "object-literal-sort-keys": false, 35 | "no-implicit-dependencies": false, 36 | "no-this-assignment": false, 37 | "max-classes-per-file": false, 38 | "no-submodule-imports": false, 39 | "prefer-object-spread": false 40 | }, 41 | "jsRules": {} 42 | } 43 | -------------------------------------------------------------------------------- /integration/using-error/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/react-hooks'; 2 | import { getDataFromTree } from '@apollo/react-ssr'; 3 | import ApolloClient, { InMemoryCache } from 'apollo-boost'; 4 | import withApollo from '../../../lib'; 5 | 6 | const App = ({ Component, pageProps, apollo }) => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default withApollo( 13 | ({ initialState }) => 14 | new ApolloClient({ 15 | uri: 'http://mocked.com/graphql', 16 | cache: new InMemoryCache().restore(initialState || {}) 17 | }), 18 | { 19 | getDataFromTree, 20 | onError: (error, ctx) => { 21 | if (error.message === 'missing') { 22 | ctx.res.statusCode = 404; 23 | ctx.res.end('Not Found'); 24 | } else if (error.message === 'invalid') { 25 | ctx.res.statusCode = 500; 26 | ctx.res.end('Internal Server Error'); 27 | } else { 28 | throw error; 29 | } 30 | } 31 | } 32 | )(App); 33 | -------------------------------------------------------------------------------- /integration/using-app/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Using _app @apollo/react-hooks support loads useQuery data on the server 1`] = ` 4 | Object { 5 | "data": Object { 6 | "ROOT_QUERY": Object { 7 | "hire": Object { 8 | "generated": false, 9 | "id": "User:uniqueid", 10 | "type": "id", 11 | "typename": "User", 12 | }, 13 | }, 14 | "User:uniqueid": Object { 15 | "__typename": "User", 16 | "id": "uniqueid", 17 | "name": "Next Apollo", 18 | }, 19 | }, 20 | } 21 | `; 22 | 23 | exports[`Using _app react-apollo support loads data on the server 1`] = ` 24 | Object { 25 | "data": Object { 26 | "ROOT_QUERY": Object { 27 | "hire": Object { 28 | "generated": false, 29 | "id": "User:uniqueid", 30 | "type": "id", 31 | "typename": "User", 32 | }, 33 | }, 34 | "User:uniqueid": Object { 35 | "__typename": "User", 36 | "id": "uniqueid", 37 | "name": "Next Apollo", 38 | }, 39 | }, 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /src/apollo.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client'; 2 | // Polyfill fetch 3 | import 'isomorphic-unfetch'; 4 | import { InitApolloClient, InitApolloOptions } from './types'; 5 | 6 | let _apolloClient: ApolloClient; 7 | 8 | export default function initApollo( 9 | clientFn: InitApolloClient, 10 | options?: InitApolloOptions 11 | ): ApolloClient { 12 | if (!clientFn) { 13 | throw new Error( 14 | '[withApollo] the first param is missing and is required to get the ApolloClient' 15 | ); 16 | } 17 | 18 | if (typeof window === 'undefined') { 19 | return getClient(clientFn, options); 20 | } 21 | if (!_apolloClient) { 22 | _apolloClient = getClient(clientFn, options); 23 | } 24 | 25 | return _apolloClient; 26 | } 27 | 28 | function getClient( 29 | clientFn: InitApolloClient, 30 | options: InitApolloOptions = {} 31 | ) { 32 | if (typeof clientFn !== 'function') { 33 | throw new Error( 34 | '[withApollo] requires a function that returns an ApolloClient' 35 | ); 36 | } 37 | 38 | return clientFn(options); 39 | } 40 | -------------------------------------------------------------------------------- /integration/using-app-class/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Using _app with a Class Component @apollo/react-hooks support loads useQuery data on the server 1`] = ` 4 | Object { 5 | "data": Object { 6 | "ROOT_QUERY": Object { 7 | "hire": Object { 8 | "generated": false, 9 | "id": "User:uniqueid", 10 | "type": "id", 11 | "typename": "User", 12 | }, 13 | }, 14 | "User:uniqueid": Object { 15 | "__typename": "User", 16 | "id": "uniqueid", 17 | "name": "Next Apollo", 18 | }, 19 | }, 20 | } 21 | `; 22 | 23 | exports[`Using _app with a Class Component react-apollo support loads data on the server 1`] = ` 24 | Object { 25 | "data": Object { 26 | "ROOT_QUERY": Object { 27 | "hire": Object { 28 | "generated": false, 29 | "id": "User:uniqueid", 30 | "type": "id", 31 | "typename": "User", 32 | }, 33 | }, 34 | "User:uniqueid": Object { 35 | "__typename": "User", 36 | "id": "uniqueid", 37 | "name": "Next Apollo", 38 | }, 39 | }, 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luis Fernando Alvarez D. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /integration/using-pages/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Using pages @apollo/react-hooks support loads useQuery data on the server 1`] = ` 4 | Object { 5 | "apollo": null, 6 | "apolloState": Object { 7 | "data": Object { 8 | "ROOT_QUERY": Object { 9 | "hire": Object { 10 | "generated": false, 11 | "id": "User:uniqueid", 12 | "type": "id", 13 | "typename": "User", 14 | }, 15 | }, 16 | "User:uniqueid": Object { 17 | "__typename": "User", 18 | "id": "uniqueid", 19 | "name": "Next Apollo", 20 | }, 21 | }, 22 | }, 23 | } 24 | `; 25 | 26 | exports[`Using pages react-apollo support loads data on the server 1`] = ` 27 | Object { 28 | "apollo": null, 29 | "apolloState": Object { 30 | "data": Object { 31 | "ROOT_QUERY": Object { 32 | "hire": Object { 33 | "generated": false, 34 | "id": "User:uniqueid", 35 | "type": "id", 36 | "typename": "User", 37 | }, 38 | }, 39 | "User:uniqueid": Object { 40 | "__typename": "User", 41 | "id": "uniqueid", 42 | "name": "Next Apollo", 43 | }, 44 | }, 45 | }, 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client'; 2 | import { IncomingHttpHeaders } from 'http'; 3 | import { NextPage, NextPageContext } from 'next'; 4 | import { AppContext } from 'next/app'; 5 | import { NextRouter } from 'next/dist/client/router'; 6 | import { ReactNode } from 'react'; 7 | 8 | export interface WithApolloOptions { 9 | getDataFromTree?: ( 10 | tree: ReactNode, 11 | context?: { [key: string]: any } 12 | ) => Promise; 13 | render?: (props: { Page: NextPage; props: any }) => any; 14 | onError?: (error: Error, ctx?: NextPageContext) => void; 15 | } 16 | 17 | export interface WithApolloState { 18 | data?: TCache; 19 | } 20 | 21 | export interface WithApolloProps { 22 | apolloState: WithApolloState; 23 | apollo: ApolloClient; 24 | router: NextRouter; 25 | } 26 | 27 | export interface InitApolloOptions { 28 | ctx?: NextPageContext; 29 | headers?: IncomingHttpHeaders; 30 | router?: NextRouter; 31 | initialState?: TCache; 32 | } 33 | 34 | export type InitApolloClient = ( 35 | options: InitApolloOptions 36 | ) => ApolloClient; 37 | 38 | export interface ApolloPageContext extends NextPageContext { 39 | // Custom prop added by withApollo 40 | apolloClient: ApolloClient; 41 | } 42 | 43 | export interface ApolloAppContext extends AppContext { 44 | ctx: ApolloPageContext; 45 | AppTree: any; 46 | } 47 | 48 | export type ApolloContext = ApolloPageContext | ApolloAppContext; 49 | -------------------------------------------------------------------------------- /integration/using-error/index.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { 3 | nextServer, 4 | nextBuild, 5 | nextExport, 6 | startApp, 7 | fetchViaHTTP 8 | } from '../next-test-utils'; 9 | 10 | const appDir = __dirname; 11 | let appPort: number; 12 | let server: any; 13 | let app: any; 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 30; 16 | 17 | beforeAll(async () => { 18 | await nextBuild(appDir); 19 | app = nextServer({ 20 | dir: appDir, 21 | dev: false, 22 | quiet: true 23 | }); 24 | server = await startApp(app); 25 | appPort = server.address().port; 26 | }); 27 | 28 | afterAll(() => server.close()); 29 | 30 | beforeEach(() => { 31 | // Fake GraphQL server 32 | nock('http://mocked.com') 33 | .post(`/graphql`) 34 | .reply(200, { 35 | data: { 36 | hire: { __typename: 'User', id: 'uniqueid', name: 'Next Apollo' } 37 | } 38 | }); 39 | }); 40 | 41 | describe('Using onError', () => { 42 | it('breaks build from unhandled GraphQL errors', async () => { 43 | const { stderr } = await nextExport(appDir, [], { stderr: true }); 44 | expect(stderr).toMatch('unhandled'); 45 | }); 46 | 47 | it('returns 500 via context', async () => { 48 | const response = await fetchViaHTTP(appPort, '/invalid'); 49 | expect(response.status).toEqual(500); 50 | const text = await response.text(); 51 | expect(text).toMatch('Internal Server Error'); 52 | }); 53 | 54 | it('returns 404 via context', async () => { 55 | const response = await fetchViaHTTP(appPort, '/missing'); 56 | expect(response.status).toEqual(404); 57 | const text = await response.text(); 58 | expect(text).toMatch('Not Found'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /integration/using-app-no-ssr/index.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { 3 | nextServer, 4 | nextBuild, 5 | startApp, 6 | renderViaHTTP 7 | } from '../next-test-utils'; 8 | 9 | const appDir = __dirname; 10 | let appPort: number; 11 | let server: any; 12 | let app: any; 13 | 14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 30; 15 | 16 | beforeAll(async () => { 17 | await nextBuild(appDir); 18 | app = nextServer({ 19 | dir: appDir, 20 | dev: false, 21 | quiet: true 22 | }); 23 | server = await startApp(app); 24 | appPort = server.address().port; 25 | }); 26 | 27 | afterAll(() => server.close()); 28 | 29 | beforeEach(() => { 30 | // Fake GraphQL server 31 | nock('http://mocked.com') 32 | .post(`/graphql`) 33 | .reply(200, { 34 | data: { 35 | hire: { __typename: 'User', id: 'uniqueid', name: 'Next Apollo' } 36 | } 37 | }); 38 | }); 39 | 40 | describe('Using _app without SSR', () => { 41 | describe('react-apollo support', () => { 42 | it('loads data on the client', async () => { 43 | const html = await renderViaHTTP(appPort, '/'); 44 | expect(html).toContain('

loading

'); 45 | }); 46 | }); 47 | 48 | describe('@apollo/react-hooks support', () => { 49 | it('loads useQuery data on the client', async () => { 50 | const html = await renderViaHTTP(appPort, '/hooks'); 51 | expect(html).toContain('

loading

'); 52 | }); 53 | }); 54 | 55 | describe('ssr smoke', () => { 56 | it('useRouter is never null', async () => { 57 | const html = await renderViaHTTP(appPort, '/router'); 58 | 59 | if (!html.includes('

all good

')) { 60 | throw new Error(` 61 | The built in next hook useRouter() returned null during a render. 62 | getDataFromTree should be called on AppTree no App so the Context 63 | is always provided. 64 | `); 65 | } 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /integration/using-app/index.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { 3 | nextServer, 4 | nextBuild, 5 | startApp, 6 | renderViaHTTP, 7 | extractNextData 8 | } from '../next-test-utils'; 9 | 10 | const appDir = __dirname; 11 | let appPort: number; 12 | let server: any; 13 | let app: any; 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 30; 16 | 17 | beforeAll(async () => { 18 | await nextBuild(appDir); 19 | app = nextServer({ 20 | dir: appDir, 21 | dev: false, 22 | quiet: true 23 | }); 24 | server = await startApp(app); 25 | appPort = server.address().port; 26 | }); 27 | 28 | afterAll(() => server.close()); 29 | 30 | beforeEach(() => { 31 | // Fake GraphQL server 32 | nock('http://mocked.com') 33 | .post(`/graphql`) 34 | .reply(200, { 35 | data: { 36 | hire: { __typename: 'User', id: 'uniqueid', name: 'Next Apollo' } 37 | } 38 | }); 39 | }); 40 | 41 | describe('Using _app', () => { 42 | describe('react-apollo support', () => { 43 | it('loads data on the server', async () => { 44 | const html = await renderViaHTTP(appPort, '/'); 45 | expect(html).toContain('

Next Apollo

'); 46 | 47 | const { apolloState } = extractNextData(html); 48 | expect(apolloState).toMatchSnapshot(); 49 | }); 50 | }); 51 | 52 | describe('@apollo/react-hooks support', () => { 53 | it('loads useQuery data on the server', async () => { 54 | const html = await renderViaHTTP(appPort, '/hooks'); 55 | expect(html).toContain('

Next Apollo

'); 56 | 57 | const { apolloState } = extractNextData(html); 58 | expect(apolloState).toMatchSnapshot(); 59 | }); 60 | }); 61 | 62 | describe('ssr smoke', () => { 63 | it('useRouter is never null', async () => { 64 | const html = await renderViaHTTP(appPort, '/router'); 65 | 66 | if (!html.includes('

all good

')) { 67 | throw new Error(` 68 | The built in next hook useRouter() returned null during a render. 69 | getDataFromTree should be called on AppTree no App so the Context 70 | is always provided. 71 | `); 72 | } 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /integration/using-app-class/index.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { 3 | nextServer, 4 | nextBuild, 5 | startApp, 6 | renderViaHTTP, 7 | extractNextData 8 | } from '../next-test-utils'; 9 | 10 | const appDir = __dirname; 11 | let appPort: number; 12 | let server: any; 13 | let app: any; 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 30; 16 | 17 | beforeAll(async () => { 18 | await nextBuild(appDir); 19 | app = nextServer({ 20 | dir: appDir, 21 | dev: false, 22 | quiet: true 23 | }); 24 | server = await startApp(app); 25 | appPort = server.address().port; 26 | }); 27 | 28 | afterAll(() => server.close()); 29 | 30 | beforeEach(() => { 31 | // Fake GraphQL server 32 | nock('http://mocked.com') 33 | .post(`/graphql`) 34 | .reply(200, { 35 | data: { 36 | hire: { __typename: 'User', id: 'uniqueid', name: 'Next Apollo' } 37 | } 38 | }); 39 | }); 40 | 41 | describe('Using _app with a Class Component', () => { 42 | describe('react-apollo support', () => { 43 | it('loads data on the server', async () => { 44 | const html = await renderViaHTTP(appPort, '/'); 45 | expect(html).toContain('

Next Apollo

'); 46 | 47 | const { apolloState } = extractNextData(html); 48 | expect(apolloState).toMatchSnapshot(); 49 | }); 50 | }); 51 | 52 | describe('@apollo/react-hooks support', () => { 53 | it('loads useQuery data on the server', async () => { 54 | const html = await renderViaHTTP(appPort, '/hooks'); 55 | expect(html).toContain('

Next Apollo

'); 56 | 57 | const { apolloState } = extractNextData(html); 58 | expect(apolloState).toMatchSnapshot(); 59 | }); 60 | }); 61 | 62 | describe('ssr smoke', () => { 63 | it('useRouter is never null', async () => { 64 | const html = await renderViaHTTP(appPort, '/router'); 65 | 66 | if (!html.includes('

all good

')) { 67 | throw new Error(` 68 | The built in next hook useRouter() returned null during a render. 69 | getDataFromTree should be called on AppTree no App so the Context 70 | is always provided. 71 | `); 72 | } 73 | }); 74 | }); 75 | }) 76 | 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-with-apollo", 3 | "version": "5.3.0", 4 | "description": "Apollo HOC for Next.js", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "fix": "tslint -p tsconfig.json --fix", 12 | "tslint": "tslint -c tslint.json -p tsconfig.json -t codeFrame", 13 | "build": "tsc", 14 | "watch": "tsc -w", 15 | "test": "yarn tslint && yarn build && jest", 16 | "prepublishOnly": "yarn test" 17 | }, 18 | "author": "lfades", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/lfades/next-with-apollo.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/lfades/next-with-apollo/issues" 26 | }, 27 | "prettier": { 28 | "singleQuote": true 29 | }, 30 | "jest": { 31 | "rootDir": "integration", 32 | "testMatch": [ 33 | "**/*.test.[jt]s?(x)" 34 | ], 35 | "bail": true, 36 | "preset": "ts-jest/presets/js-with-babel", 37 | "testEnvironment": "node", 38 | "globals": { 39 | "ts-jest": { 40 | "isolatedModules": true 41 | } 42 | } 43 | }, 44 | "dependencies": { 45 | "isomorphic-unfetch": "^3.0.0" 46 | }, 47 | "peerDependencies": { 48 | "next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", 49 | "react": "^16.6.0 || ^17.0.0 || ^18.0.0", 50 | "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" 51 | }, 52 | "devDependencies": { 53 | "@apollo/react-hooks": "^3.0.1", 54 | "@apollo/react-ssr": "^3.1.3", 55 | "@types/cross-spawn": "^6.0.1", 56 | "@types/jest": "^24.0.25", 57 | "@types/node": "^13.1.2", 58 | "@types/node-fetch": "^2.5.4", 59 | "@types/react": "^16.9.17", 60 | "@types/react-dom": "^16.9.4", 61 | "apollo-boost": "^0.4.7", 62 | "apollo-client": "^2.6.8", 63 | "cross-spawn": "^7.0.1", 64 | "get-port": "^5.1.0", 65 | "graphql": "^14.5.8", 66 | "jest": "^24.9.0", 67 | "next": "^9.1.6", 68 | "nock": "^11.7.0", 69 | "node-fetch": "2.6.1", 70 | "prettier": "^1.19.1", 71 | "react": "^16.12.0", 72 | "react-apollo": "^3.0.1", 73 | "react-dom": "^16.12.0", 74 | "ts-jest": "^24.2.0", 75 | "tslint": "^5.20.1", 76 | "tslint-config-prettier": "^1.18.0", 77 | "tslint-eslint-rules": "^5.4.0", 78 | "tslint-plugin-prettier": "^2.1.0", 79 | "typescript": "^3.7.4" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /integration/using-pages/index.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { 3 | nextServer, 4 | nextBuild, 5 | startApp, 6 | renderViaHTTP, 7 | extractNextData 8 | } from '../next-test-utils'; 9 | 10 | const appDir = __dirname; 11 | let appPort: number; 12 | let server: any; 13 | let app: any; 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 30; 16 | 17 | beforeAll(async () => { 18 | await nextBuild(appDir); 19 | app = nextServer({ 20 | dir: appDir, 21 | dev: false, 22 | quiet: true 23 | }); 24 | server = await startApp(app); 25 | appPort = server.address().port; 26 | }); 27 | 28 | afterAll(() => server.close()); 29 | 30 | beforeEach(() => { 31 | // Fake GraphQL server 32 | nock('http://mocked.com') 33 | .post(`/graphql`) 34 | .reply(200, { 35 | data: { 36 | hire: { __typename: 'User', id: 'uniqueid', name: 'Next Apollo' } 37 | } 38 | }); 39 | }); 40 | 41 | describe('Using pages', () => { 42 | describe('react-apollo support', () => { 43 | it('loads data on the server', async () => { 44 | const html = await renderViaHTTP(appPort, '/ssr'); 45 | expect(html).toContain('

Next Apollo

'); 46 | 47 | const { pageProps } = extractNextData(html); 48 | expect(pageProps).toMatchSnapshot(); 49 | }); 50 | 51 | it('loads loading state with SSR disabled', async () => { 52 | const html = await renderViaHTTP(appPort, '/'); 53 | expect(html).toContain('

loading

'); 54 | 55 | const { pageProps } = extractNextData(html); 56 | expect(pageProps).toEqual({}); 57 | }); 58 | }); 59 | 60 | describe('@apollo/react-hooks support', () => { 61 | it('loads useQuery data on the server', async () => { 62 | const html = await renderViaHTTP(appPort, '/hooks-ssr'); 63 | expect(html).toContain('

Next Apollo

'); 64 | 65 | const { pageProps } = extractNextData(html); 66 | expect(pageProps).toMatchSnapshot(); 67 | }); 68 | 69 | it('loads useQuery loading state with SSR disabled', async () => { 70 | const html = await renderViaHTTP(appPort, '/hooks'); 71 | expect(html).toContain('

loading

'); 72 | 73 | const { pageProps } = extractNextData(html); 74 | expect(pageProps).toEqual({}); 75 | }); 76 | }); 77 | 78 | it('useRouter is never null', async () => { 79 | const html = await renderViaHTTP(appPort, '/router'); 80 | 81 | if (!html.includes('

all good

')) { 82 | throw new Error(` 83 | The built in next hook useRouter() returned null during a render. 84 | getDataFromTree should be called on AppTree no App so the Context 85 | is always provided. 86 | `); 87 | } 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /integration/next-test-utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import http from 'http'; 3 | import spawn from 'cross-spawn'; 4 | import nextServer from 'next'; 5 | import fetch, { RequestInit } from 'node-fetch'; 6 | 7 | /** 8 | * These utils are very similar to the ones used by Next.js in their tests 9 | */ 10 | 11 | type FirstArgument = T extends (arg: infer U) => any ? U : any; 12 | 13 | type NextServerOptions = FirstArgument; 14 | 15 | type NextCommandOptions = { 16 | env?: any; 17 | stderr?: boolean; 18 | stdout?: boolean; 19 | }; 20 | 21 | function promiseCall(obj: any, method: string, ...args: any) { 22 | return new Promise((resolve, reject) => { 23 | const newArgs = [ 24 | ...args, 25 | function(err: Error, res: any) { 26 | if (err) return reject(err); 27 | resolve(res); 28 | } 29 | ]; 30 | 31 | obj[method](...newArgs); 32 | }); 33 | } 34 | 35 | export { nextServer }; 36 | 37 | export async function startApp(options: NextServerOptions) { 38 | const app = nextServer(options); 39 | 40 | await app.prepare(); 41 | 42 | const handler = app.getRequestHandler(); 43 | const server = http.createServer(handler); 44 | 45 | (server as any).__app = app; 46 | 47 | await promiseCall(server, 'listen'); 48 | 49 | return server; 50 | } 51 | 52 | export async function stopApp(server: http.Server) { 53 | const app = (server as any)._app; 54 | 55 | if (app) await app.close(); 56 | await promiseCall(server, 'close'); 57 | } 58 | 59 | export function runNextCommand( 60 | args: string[], 61 | options: NextCommandOptions = {} 62 | ) { 63 | const nextDir = path.dirname(require.resolve('next/package')); 64 | const nextBin = path.join(nextDir, 'dist/bin/next'); 65 | const cwd = nextDir; 66 | const env = { ...process.env, ...options.env, NODE_ENV: '' }; 67 | 68 | return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { 69 | console.log(`Running command "next ${args.join(' ')}"`); 70 | const instance = spawn('node', [nextBin, ...args], { 71 | cwd, 72 | env, 73 | stdio: ['ignore', 'pipe', 'pipe'] 74 | }); 75 | 76 | let stderrOutput = ''; 77 | if (options.stderr) { 78 | instance.stderr!.on('data', function(chunk) { 79 | stderrOutput += chunk; 80 | }); 81 | } 82 | 83 | let stdoutOutput = ''; 84 | if (options.stdout) { 85 | instance.stdout!.on('data', function(chunk) { 86 | stdoutOutput += chunk; 87 | }); 88 | } 89 | 90 | instance.on('close', () => { 91 | resolve({ 92 | stdout: stdoutOutput, 93 | stderr: stderrOutput 94 | }); 95 | }); 96 | 97 | instance.on('error', (err: any) => { 98 | err.stdout = stdoutOutput; 99 | err.stderr = stderrOutput; 100 | reject(err); 101 | }); 102 | }); 103 | } 104 | 105 | export function nextBuild( 106 | dir: string, 107 | args: string[] = [], 108 | opts?: NextCommandOptions 109 | ) { 110 | return runNextCommand(['build', dir, ...args], opts); 111 | } 112 | 113 | export function nextExport( 114 | dir: string, 115 | args: string[] = [], 116 | opts?: NextCommandOptions 117 | ) { 118 | return runNextCommand(['export', dir, ...args], opts); 119 | } 120 | 121 | export function fetchViaHTTP( 122 | appPort: number, 123 | pathname: string, 124 | opts?: RequestInit 125 | ) { 126 | const url = `http://localhost:${appPort}${pathname}`; 127 | return fetch(url, opts); 128 | } 129 | 130 | export function renderViaHTTP(appPort: number, pathname: string) { 131 | return fetchViaHTTP(appPort, pathname).then(res => res.text()); 132 | } 133 | 134 | export function extractNextData(html: string) { 135 | const R = /