├── .dockerignore
├── .eslintrc.js
├── .eslintrc.production.js
├── .github
├── renovate.json
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierrc
├── .storybook
├── main.js
└── webpack.config.js
├── .testcaferc.json
├── .vscode
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── e2e
├── config
│ └── index.ts
└── specs
│ └── HomePage.e2e.ts
├── jest.config.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── serverless.yml
├── src
├── __tests__
│ └── pages
│ │ └── about.spec.ts
├── components
│ └── AboutUs
│ │ ├── AboutUs.stories.tsx
│ │ ├── AboutUs.tsx
│ │ └── index.ts
├── containers
│ ├── AboutPage
│ │ ├── AboutPage.stories.tsx
│ │ ├── AboutPage.tsx
│ │ └── index.ts
│ └── HomePage
│ │ ├── HomePage.tsx
│ │ └── index.tsx
├── pages
│ ├── about.tsx
│ └── index.tsx
└── setupTests.ts
├── tsconfig.jest.json
├── tsconfig.json
├── tsconfig.storybook.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # Next.js
5 | .next
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: './.eslintrc.production.js',
3 | // We can relax some settings here for nicer development experience; warnings will crash in CI
4 | rules: {
5 | '@typescript-eslint/no-unused-vars': [
6 | 'warn',
7 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
8 | ],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/.eslintrc.production.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | reportUnusedDisableDirectives: true,
5 | plugins: [
6 | 'react',
7 | 'prettier',
8 | 'jest',
9 | 'react-hooks',
10 | 'import',
11 | 'testing-library',
12 | 'testcafe',
13 | '@typescript-eslint',
14 | ],
15 | extends: [
16 | 'eslint:recommended',
17 | 'airbnb',
18 | 'prettier',
19 | 'prettier/react',
20 | 'plugin:jest/recommended',
21 | 'plugin:testing-library/react',
22 | 'plugin:testcafe/recommended',
23 | 'plugin:@typescript-eslint/recommended',
24 | ],
25 | env: {
26 | browser: true,
27 | jest: true,
28 | node: true,
29 | es6: true,
30 | },
31 | rules: {
32 | 'arrow-body-style': ['off'],
33 | 'react/jsx-filename-extension': [1, { extensions: ['.tsx'] }],
34 | 'no-use-before-define': ['off'],
35 |
36 | 'jsx-a11y/anchor-is-valid': ['off'],
37 |
38 | 'jest/expect-expect': ['off'],
39 |
40 | 'import/no-unresolved': ['off'],
41 | 'import/no-extraneous-dependencies': ['off'],
42 | 'import/extensions': ['off'],
43 | 'import/prefer-default-export': ['off'],
44 | 'import/order': [
45 | 1,
46 | {
47 | 'newlines-between': 'always',
48 | groups: [
49 | 'builtin',
50 | ['external', 'internal'],
51 | 'parent',
52 | ['sibling', 'index'],
53 | ],
54 | },
55 | ],
56 |
57 | '@typescript-eslint/explicit-module-boundary-types': 'off',
58 | '@typescript-eslint/no-unused-vars': [
59 | 'error',
60 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
61 | ],
62 | },
63 | overrides: [
64 | {
65 | files: ['*.e2e.ts'],
66 | rules: {
67 | 'jest/expect-expect': ['off'],
68 | 'jest/no-test-callback': ['off'],
69 | 'jest/no-done-callback': ['off'],
70 | },
71 | },
72 | ],
73 | };
74 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "labels": ["dependencies", "automerge"],
4 | "packageRules": [
5 | {
6 | "groupName": "monorepo:ky",
7 | "packageNames": [
8 | "ky",
9 | "ky-universal"
10 | ]
11 | },
12 | {
13 | "groupName": "monorepo:@testing-library",
14 | "packageNames": [
15 | "@testing-library/jest-dom",
16 | "@testing-library/react",
17 | "@testing-library/user-event"
18 | ]
19 | },
20 | {
21 | "groupName": "monorepo:testcafe",
22 | "packageNames": [
23 | "testcafe",
24 | "@testing-library/testcafe"
25 | ]
26 | },
27 | {
28 | "groupName": "monorepo:eslint",
29 | "packageNames": [
30 | "@typescript-eslint/eslint-plugin",
31 | "@typescript-eslint/parser",
32 | "eslint",
33 | "eslint-config-airbnb",
34 | "eslint-config-prettier",
35 | "eslint-config-import",
36 | "eslint-plugin-jest",
37 | "eslint-plugin-jsx-a11y",
38 | "eslint-plugin-prettier",
39 | "eslint-plugin-react",
40 | "eslint-plugin-react-hooks",
41 | "eslint-plugin-testcafe",
42 | "eslint-plugin-testing-library"
43 | ]
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 |
4 | env:
5 | CI: true
6 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
7 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
8 | AWS_S3_STATE_BUCKET: ${{ secrets.AWS_S3_STATE_BUCKET }}
9 | PR_NAME: master
10 |
11 | jobs:
12 | unit_tests:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 1
19 | - uses: actions/setup-node@v2
20 | with:
21 | node-version: '14.x'
22 | - name: Get yarn cache directory path
23 | id: yarn-cache-dir-path
24 | run: echo "::set-output name=dir::$(yarn cache dir)"
25 | - uses: actions/cache@v2
26 | id: yarn-cache
27 | with:
28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-yarn-
32 | - name: install, lint and unit test
33 | run: |
34 | yarn install --frozen-lockfile
35 | yarn lint
36 | yarn test:unit -- --coverage
37 | - name: Upload coverage to Codecov
38 | uses: codecov/codecov-action@v1
39 | with:
40 | token: ${{ secrets.CODECOV_TOKEN }}
41 |
42 | e2e_tests:
43 | runs-on: ubuntu-latest
44 | env:
45 | ARTIFACTS_PATH: artifacts/
46 |
47 | steps:
48 | - uses: actions/checkout@v2
49 | with:
50 | fetch-depth: 1
51 | - uses: actions/setup-node@v2
52 | with:
53 | node-version: '14.x'
54 | - name: Get yarn cache directory path
55 | id: yarn-cache-dir-path
56 | run: echo "::set-output name=dir::$(yarn cache dir)"
57 | - uses: actions/cache@v2
58 | id: yarn-cache
59 | with:
60 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
61 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
62 | restore-keys: |
63 | ${{ runner.os }}-yarn-
64 | - name: Install packages
65 | run: |
66 | yarn install --frozen-lockfile
67 | - name: build application
68 | run: yarn build
69 | - name: start application
70 | run: |
71 | mkdir -p $ARTIFACTS_PATH
72 | yarn start 2>&1 | tee $ARTIFACTS_PATH/serve.log &
73 | node_modules/wait-on/bin/wait-on http-get://localhost:3000 --timeout 60000
74 | - name: test:e2e
75 | run: |
76 | mkdir -p $ARTIFACTS_PATH/screenshots
77 | mkdir -p $ARTIFACTS_PATH/videos
78 | xvfb-run --server-args="-screen 0 1280x720x24" yarn test:e2e
79 | - uses: actions/upload-artifact@v2
80 | if: failure()
81 | with:
82 | name: artifacts
83 | path: artifacts
84 |
85 | deploy:
86 | runs-on: ubuntu-latest
87 | needs: [unit_tests, e2e_tests]
88 | if: github.ref == 'refs/heads/master'
89 |
90 | steps:
91 | - uses: actions/checkout@v2
92 | with:
93 | fetch-depth: 1
94 | - uses: actions/setup-node@v2
95 | with:
96 | node-version: '14.x'
97 | - name: Install packages
98 | run: yarn install --frozen-lockfile
99 | - name: Deploy
100 | run: |
101 | aws s3 sync s3://$AWS_S3_STATE_BUCKET/$PR_NAME/.serverless .serverless --delete
102 | npx serverless
103 | aws s3 sync .serverless s3://$AWS_S3_STATE_BUCKET/$PR_NAME/.serverless --delete
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 | package-lock.json
4 |
5 | # Next.js
6 | .next
7 |
8 | # Testing
9 | coverage
10 |
11 | # Serveless
12 | .serverless
13 | .serverless_nextjs
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../src/**/*.stories.tsx'],
3 | addons: [
4 | '@storybook/addon-essentials',
5 | '@storybook/addon-links',
6 | '@storybook/addon-storysource',
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = ({ config }) => {
4 | // Required for absolute imports in Storybook
5 | config.resolve.modules.push(path.resolve(process.cwd(), 'src'));
6 |
7 | config.resolve.extensions.push('.ts', '.tsx');
8 | return config;
9 | };
10 |
--------------------------------------------------------------------------------
/.testcaferc.json:
--------------------------------------------------------------------------------
1 | {
2 | "quarantineMode": true,
3 | "skipJsErrors": true,
4 | "concurrency": 2,
5 | "screenshots": {
6 | "takeOnFails": true,
7 | "pathPattern": "${USERAGENT}/${FIXTURE}/${TEST}-${QUARANTINE_ATTEMPT}.png",
8 | "path": "artifacts/screenshots/"
9 | },
10 | "videoPath": "artifacts/videos/",
11 | "videoOptions": {
12 | "failedOnly": true,
13 | "pathPattern": "${USERAGENT}/${FIXTURE}/${TEST}-${QUARANTINE_ATTEMPT}.mp4"
14 | },
15 | "clientScripts": ["./node_modules/@testing-library/dom/dist/@testing-library/dom.umd.js"]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "files.insertFinalNewline": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true
6 | },
7 | "[javascript]": {
8 | "editor.defaultFormatter": "esbenp.prettier-vscode"
9 | },
10 | "[typescript]": {
11 | "editor.defaultFormatter": "esbenp.prettier-vscode"
12 | },
13 | "[typescriptreact]": {
14 | "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-alpine as build
2 |
3 | COPY . /src
4 | WORKDIR /src
5 |
6 | RUN yarn install --frozen-lockfile
7 | RUN yarn build
8 | RUN yarn install --frozen-lockfile --production --ignore-scripts --prefer-offline
9 |
10 | FROM node:14-alpine
11 |
12 | WORKDIR /usr/app
13 |
14 | COPY --from=build /src/node_modules /usr/app/node_modules
15 | COPY --from=build /src/package.json /usr/app/package.json
16 | COPY --from=build /src/.next /usr/app/.next
17 |
18 | ENV NODE_ENV production
19 |
20 | CMD ["npm", "start"]
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Matej Snuderl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # next-aws-lambda
2 |
3 | [](https://github.com/Meemaw/next-aws-lambda/actions) [](https://codecov.io/gh/Meemaw/next-aws-lambda) [](https://github.com/prettier/prettier) [](https://opensource.org/)
4 |
5 | Minimal starter boilerplate project for deploying Serveless [Next.js](https://nextjs.org/) to AWS Lambda@Edge.
6 |
7 | ## Features:
8 |
9 | - Next.js **v10.x** compatible
10 | - Unit testing with [jest](https://jestjs.io/), [RTL](https://testing-library.com/) and [next-page-tester](https://github.com/toomuchdesign/next-page-tester)
11 | - E2E testing with [testcafe](https://devexpress.github.io/testcafe/)
12 | - Linting with [prettier](https://prettier.io/) and [eslint](https://eslint.org/)
13 | - Test coverage using [codecov](https://www.codecov.io/)
14 | - [Github action](https://github.com/features/actions) workflow for out of the box CI/CD
15 | - Storybook
16 |
17 | ## Deployment
18 |
19 | For deployment [serverless-next component](https://github.com/serverless-nextjs/serverless-next.js/) is used: A zero configuration Next.js 10.x serverless component for AWS Lambda@Edge aiming for full feature parity.
20 |
21 | ## Demo
22 |
23 | Demo available [here](https://d2922rq9ynjh4y.cloudfront.net)
24 |
--------------------------------------------------------------------------------
/e2e/config/index.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | baseURL: process.env.BASE_URL || 'http://localhost:3000/',
3 | };
4 |
--------------------------------------------------------------------------------
/e2e/specs/HomePage.e2e.ts:
--------------------------------------------------------------------------------
1 | import 'testcafe';
2 | import { getByText } from '@testing-library/testcafe';
3 |
4 | import config from '../config';
5 |
6 | fixture('').page(config.baseURL);
7 |
8 | test('Can navigate to about page', async (t) => {
9 | await t
10 | .expect(getByText('This is our homepage.').exists)
11 | .ok('Renders HomePage text');
12 |
13 | await t
14 | .click(getByText('About Us'))
15 | .expect(getByText('We are a cool company.').exists)
16 | .ok('Render AboutPage text');
17 | });
18 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types';
2 |
3 | const config: Config.InitialOptions = {
4 | transform: {
5 | '^.+\\.tsx?$': 'ts-jest',
6 | },
7 | setupFilesAfterEnv: ['/src/setupTests.ts'],
8 | globals: {
9 | 'ts-jest': {
10 | tsconfig: 'tsconfig.jest.json',
11 | },
12 | },
13 | // Required for absolute imports in Jest
14 | moduleDirectories: ['node_modules', 'src'],
15 | };
16 |
17 | export default config;
18 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | target: 'serverless',
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-aws-lambda",
3 | "version": "0.0.0-development",
4 | "description": "Minimal starter boilerplate project for Serverless Next.js on AWS Lambda",
5 | "license": "MIT",
6 | "author": "Meemaw ",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/Meemaw/next-aws-lambda"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/Meemaw/next-aws-lambda/issues"
13 | },
14 | "scripts": {
15 | "dev": "next dev",
16 | "dev:background": "yarn dev 2>&1 > /dev/null &",
17 | "build": "next build",
18 | "start": "next start",
19 | "test": "yarn test:unit",
20 | "test:unit": "jest",
21 | "test:e2e": "testcafe chrome e2e/specs",
22 | "test:e2e:headless": "testcafe chrome:headless e2e/specs",
23 | "lint": "concurrently \"yarn prettier\" \"yarn eslint\"",
24 | "eslint": "eslint --max-warnings 0 'src/**/*.{ts,tsx}' 'e2e/**/*.{ts,tsx}' --config .eslintrc.production.js",
25 | "prettier": "prettier -l 'src/**/*' 'e2e/**/*'",
26 | "prettier:fix": "prettier -l 'src/**/*' 'e2e/**/*' --write",
27 | "storybook": "start-storybook"
28 | },
29 | "dependencies": {
30 | "ky": "0.25.1",
31 | "ky-universal": "0.8.2",
32 | "next": "10.0.7",
33 | "react": "17.0.2",
34 | "react-dom": "17.0.2"
35 | },
36 | "devDependencies": {
37 | "@ffmpeg-installer/ffmpeg": "1.0.20",
38 | "@storybook/addon-actions": "6.1.21",
39 | "@storybook/addon-links": "6.1.21",
40 | "@storybook/addon-storysource": "6.1.21",
41 | "@storybook/react": "6.1.21",
42 | "@testing-library/jest-dom": "5.11.9",
43 | "@testing-library/react": "11.2.5",
44 | "@testing-library/testcafe": "4.3.1",
45 | "@testing-library/user-event": "12.7.1",
46 | "@types/jest": "26.0.21",
47 | "@types/node": "14.14.31",
48 | "@types/react": "17.0.2",
49 | "@typescript-eslint/eslint-plugin": "4.18.0",
50 | "@typescript-eslint/parser": "4.18.0",
51 | "babel-loader": "8.2.2",
52 | "concurrently": "6.0.1",
53 | "eslint": "7.22.0",
54 | "eslint-config-airbnb": "18.2.1",
55 | "eslint-config-prettier": "7.2.0",
56 | "eslint-plugin-import": "2.22.1",
57 | "eslint-plugin-jest": "24.3.2",
58 | "eslint-plugin-jsx-a11y": "6.4.1",
59 | "eslint-plugin-prettier": "3.3.1",
60 | "eslint-plugin-react": "7.22.0",
61 | "eslint-plugin-react-hooks": "4.2.0",
62 | "eslint-plugin-testcafe": "0.2.1",
63 | "eslint-plugin-testing-library": "3.10.2",
64 | "jest": "26.6.3",
65 | "next-page-tester": "0.22.0",
66 | "prettier": "2.2.1",
67 | "testcafe": "1.12.0",
68 | "ts-jest": "26.5.1",
69 | "ts-node": "10.0.0",
70 | "typescript": "4.2.2",
71 | "wait-on": "5.3.0"
72 | },
73 | "husky": {
74 | "hooks": {
75 | "pre-commit": "pretty-quick --staged"
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | name: next-aws-lambda
2 |
3 | website:
4 | component: '@sls-next/serverless-component'
5 | inputs:
6 | memory:
7 | defaultLambda: 1024
8 | apiLambda: 1024
9 | cloudfront:
10 | comment: 'next-aws-lambda (Managed by @sls-next/serverless-component)'
11 |
--------------------------------------------------------------------------------
/src/__tests__/pages/about.spec.ts:
--------------------------------------------------------------------------------
1 | import { getPage } from 'next-page-tester';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | describe('/about', () => {
6 | it('Should work', async () => {
7 | const { page } = await getPage({ route: '/about' });
8 | render(page);
9 |
10 | expect(screen.getByText('We are a cool company.')).toBeInTheDocument();
11 | expect(
12 | screen.getByText(
13 | '{"userId":1,"id":1,"title":"delectus aut autem","completed":false}'
14 | )
15 | ).toBeInTheDocument();
16 |
17 | userEvent.click(screen.getByText('Home'));
18 |
19 | await screen.findByText('This is our homepage.');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/AboutUs/AboutUs.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Meta } from '@storybook/react';
3 |
4 | import { AboutUs } from './AboutUs';
5 |
6 | export default {
7 | title: 'AboutUs',
8 | component: AboutUs,
9 | } as Meta;
10 |
11 | export const Default = () => ;
12 |
--------------------------------------------------------------------------------
/src/components/AboutUs/AboutUs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const AboutUs = () => {
4 | return (
5 |
6 |
About
7 |
We are a cool company.
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/AboutUs/index.ts:
--------------------------------------------------------------------------------
1 | export { AboutUs } from './AboutUs';
2 |
--------------------------------------------------------------------------------
/src/containers/AboutPage/AboutPage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Meta } from '@storybook/react';
3 |
4 | import { AboutPage } from './AboutPage';
5 |
6 | export default {
7 | title: 'AboutPage',
8 | component: AboutPage,
9 | } as Meta;
10 |
11 | export const Default = () => ;
12 |
--------------------------------------------------------------------------------
/src/containers/AboutPage/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { AboutUs } from 'components/AboutUs';
4 | import { Todo } from 'pages/about';
5 |
6 | type Props = {
7 | todos: Todo[];
8 | };
9 |
10 | export const AboutPage = ({ todos }: Props) => {
11 | return (
12 | <>
13 |
14 | -
15 |
16 | Home
17 |
18 |
19 | - About Us
20 |
21 |
22 |
23 |
24 | TODOS:
25 | {JSON.stringify(todos)}
26 | >
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/containers/AboutPage/index.ts:
--------------------------------------------------------------------------------
1 | export { AboutPage } from './AboutPage';
2 |
--------------------------------------------------------------------------------
/src/containers/HomePage/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 |
4 | export const HomePage = () => {
5 | return (
6 | <>
7 |
8 | - Home
9 | -
10 |
11 | About Us
12 |
13 |
14 |
15 |
16 | This is our homepage.
17 | >
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/containers/HomePage/index.tsx:
--------------------------------------------------------------------------------
1 | export { HomePage } from './HomePage';
2 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ky from 'ky-universal';
3 | import { GetServerSideProps } from 'next';
4 | import { AboutPage } from 'containers/AboutPage';
5 |
6 | export type Todo = {
7 | userId: number;
8 | id: number;
9 | title: string;
10 | completed: boolean;
11 | };
12 |
13 | type Props = {
14 | todos: Todo[];
15 | };
16 |
17 | const About = ({ todos }: Props) => {
18 | return ;
19 | };
20 |
21 | export const getServerSideProps: GetServerSideProps = async (_ctx) => {
22 | const todos = await ky('https://jsonplaceholder.typicode.com/todos/1').json<
23 | Todo[]
24 | >();
25 | return { props: { todos } };
26 | };
27 |
28 | export default About;
29 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HomePage } from 'containers/HomePage';
3 |
4 | const Home = () => {
5 | return ;
6 | };
7 |
8 | export default Home;
9 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import '@testing-library/jest-dom/extend-expect';
3 |
4 | // Mock global.scrollTo, since Next.js Link component trigger it
5 | global.scrollTo = jest.fn();
6 |
7 | // Suppress validateDOMNesting error logs
8 | const consoleError = console.error;
9 | console.error = jest.fn((error) => {
10 | if (!error.includes('validateDOMNesting')) {
11 | consoleError(error);
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": "src"
17 | },
18 | "exclude": ["node_modules", "e2e"],
19 | "include": ["next-env.d.ts", "src"]
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.storybook.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------