Skip to content
This repository was archived by the owner on Mar 30, 2022. It is now read-only.

Project-Setup/github_pwa

Repository files navigation

Example Github Page PWA with NextJs, code splitting Redux-Toolkit, Typescript, Eslint, Jest and Emotion.

Highlight

  • Multi-page React Progressive Web App
  • Installable for offline use through Chrome on desktop or mobile
  • Can be statically hosted on Github Page for free (or as a regular web app hosted on a custom server)
  • Dynamically loaded Redux reducers for code splitting
  • Prefetch security sensitive content at build time
  • All in Typescript/Javascript with CSS-in-JS
  • Easy testing with Jest and Enzyme
  • Eslint helps practice standard coding styles

Versions

  • NextJs v9.4.2
  • Redux-Toolkit v1.3.6
  • Emotion v10
  • Typescript v3.9.2

Other Project Setup

Usage of this example setup

  1. setup node env
    nvm use npm install
  2. remove unwanted files in public/, src/
  3. add .env and other .env files
  4. preview dev progress on http://localhost:3000/
    npm run dev
  5. export to docs/ for Github Page deploy
    npm run export
  6. read Setup for notes

Setup

  1. install nvm in the os
  2. nvm install node git init
  3. add .gitignore
  4. node -v > .nvmrc
  5. npm init -y
  1. npm i -S next react react-dom
  2. add a script to your package.json like this:
    { "scripts": { "dev": "next", "build": "next build", "start": "next start" } }
  1. npm i -D typescript @types/react @types/react-dom @types/node
  2. create tsconfig.json
    { "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, "alwaysStrict": true, "esModuleInterop": true, "isolatedModules": true, "jsx": "preserve", "lib": [ "dom", "es2017" ], "module": "esnext", "moduleResolution": "node", "noEmit": true, "typeRoots": [ "./node_modules/@types" ], "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "resolveJsonModule": true, "removeComments": false, "skipLibCheck": true, "strict": true, "target": "esnext", "forceConsistentCasingInFileNames": true, "baseUrl": "./src" }, "exclude": [ "node_modules", "next.config.js" ], "include": [ "**/*.ts", "**/*.tsx" ] }
  1. create src/pages folder (or pages)
  2. create pages.tsx under src/pages/ (i.e. src/pages/index.tsx for / route)
  1. npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-import-resolver-typescript npm i -D eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-import eslint-plugin-react-hooks npm i -D prettier eslint-config-prettier eslint-plugin-prettier
  2. create .eslintrc.js
    module.exports = { parser: '@typescript-eslint/parser', // Specifies the ESLint parser extends: [ 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 'airbnb', //Uses airbnb recommended rules 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. ], parserOptions: { ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports ecmaFeatures: { jsx: true, // Allows for the parsing of JSX }, }, env: { browser: true, node: true }, rules: { // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. '@typescript-eslint/explicit-function-return-type': 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { 'vars': 'all', 'args': 'after-used', 'ignoreRestSiblings': false }], 'react/jsx-filename-extension': [1, { 'extensions': ['.js', '.jsx', '.ts', '.tsx'] }], 'react/jsx-first-prop-new-line': 0, '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-function-return-type': 0, '@typescript-eslint/no-namespace': 'off', 'jsx-a11y/anchor-is-valid': [ 'error', { 'components': [ 'Link' ], 'specialLink': [ 'hrefLeft', 'hrefRight' ], 'aspects': [ 'invalidHref', 'preferButton' ] }], 'react/prop-types': 'off', 'import/extensions': [1, { 'extensions': ['.js', '.jsx', '.ts', '.tsx'] }], 'import/no-extraneous-dependencies': [ 'error', { 'devDependencies': true } ], 'comma-dangle': [ 'error', { 'arrays': 'always-multiline', 'objects': 'always-multiline', 'imports': 'always-multiline', 'exports': 'always-multiline', 'functions': 'never' } ], "react-hooks/rules-of-hooks": "error", 'react-hooks/exhaustive-deps': 'off', 'no-bitwise': 'off' }, plugins: [ '@typescript-eslint/eslint-plugin', 'react-hooks', ], settings: { 'import/resolver': { node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, typescript: {}, }, react: { version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use }, }, };
  3. create .prettierrc.js
    module.exports = { semi: true, trailingComma: 'es5', singleQuote: true, printWidth: 80, tabWidth: 2, };
  1. npm i -D jest babel-jest
  2. add scripts in package.json
    "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" },
  3. npm i -D enzyme enzyme-adapter-react-16 enzyme-to-json npm i -D typescript @types/enzyme @types/enzyme-adapter-react-16 @types/jest
  4. create jest.config.js
    module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js'], testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$', globals: { NODE_ENV: 'test', }, snapshotSerializers: ['enzyme-to-json/serializer'], transform: { '^.+\\.(j|t)sx?$': 'babel-jest', }, coveragePathIgnorePatterns: [ '/node_modules/', 'jest.setup.js', '<rootDir>/configs/', 'jest.config.js', '.json', '.snap', ], setupFiles: ['<rootDir>/jest/jest.setup.js'], coverageReporters: ['json', 'lcov', 'text', 'text-summary'], moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/mocks.js', '\\.(css|less|scss)$': '<rootDir>/__mocks__/mocks.js', }, moduleDirectories: ['node_modules', 'src'], };
  5. create babel.config.js
    module.exports = { presets: ['next/babel'], };
  6. create jest/jest.setup.js
    import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { join } from 'path'; import { loadEnvConfig } from 'next/dist/lib/load-env-config'; // to load '.env' files in test loadEnvConfig(join(__dirname, '.../')); Enzyme.configure({ adapter: new Adapter() });
  7. change env in .eslintrc.js
    env: { browser: true, node: true, jest: true },
  1. npm i -S @emotion/core npm i -D @emotion/babel-preset-css-prop jest-emotion eslint-plugin-emotion
  2. change babel.config.js
    module.exports = { presets: [ [ 'next/babel', { 'preset-env': {}, 'preset-react': {}, }, ], '@emotion/babel-preset-css-prop', ], };
  3. add rules and plugins to .eslintrc.js
    module.exports = { // ... rules: { // ... "emotion/no-vanilla": "error", "emotion/import-from-emotion": "error", "emotion/styled-import": "error", }, // ... plugins: [ 'emotion', // ... ], // ... }
  4. add jest/jest.setupAfterEnv.js
    import { matchers } from 'jest-emotion'; expect.extend(matchers);
  5. add serializers and setup files to jest/jest.config.js
    // ... snapshotSerializers: ['enzyme-to-json/serializer', 'jest-emotion'], // ... setupFilesAfterEnv: ['<rootDir>/jest.setupAfterEnv.js'], // ...

(deploy to /docs intead of using gh-pages branch; replace {folder} with the project name in github repo)

  1. add .env.production
NEXT_PUBLIC_LINK_PREFIX=/{folder}
  1. create LINK_PREFIX in next.config.js
    const LINK_PREFIX = process.env.NEXT_PUBLIC_LINK_PREFIX || ''; module.exports = () => ({ assetPrefix: LINK_PREFIX, });
  2. change as prop in next/Link to add linkPrefix, similar to src/features/link/Link.tsx in the example setup
  3. change scripts in package.json
    { "scripts": { "export": "NODE_ENV=production npm run build && next export -o docs && touch docs/.nojekyll" } }

Optional:

Optional chaining

  1. npm i -D @babel/plugin-proposal-nullish-coalescing-operator @babel/plugin-proposal-optional-chaining
  2. add the plugins to babel.config.js
    module.exports = { presets: [ // ... ], plugins: [ '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', ], };

  1. npm i -S react-redux @reduxjs/toolkit npm i -D @types/react-redux
  2. either use next-redux-wrapper package (npm i -P next-redux-wrapper) or copy the withRedux.tsx from the example setup src/utils/redux
  3. create custom makeStore function, _app.tsx page and other redux setup as examples in next-redux-wrapper repo shows
  1. copy configureStore.ts, DynamicStoreWrap.tsx from the example setup src/utils/redux, and objectAssign.ts from src/utils/common
  2. change src/_app.tsx similar to the example setup
  1. npm i -S next-pwa next-manifest
  2. change next.config.js
     const isProd = process.env.NODE_ENV === 'production'; const FOLDER = LINK_PREFIX && LINK_PREFIX.substring(1); // tranfrom precache url for browsers that encode dynamic routes // i.e. "[id].js" => "%5Bid%5D.js" const encodeUriTransform = async (manifestEntries) => { const manifest = manifestEntries.map((entry) => { entry.url = encodeURI(entry.url); return entry; }); return { manifest, warnings: [] }; }; module.exports = () => withManifest( withPWA({ // ... // service worker pwa: { disable: !isProd, subdomainPrefix: LINK_PREFIX, dest: 'public', navigationPreload: true, }, // manifest manifest: { /* eslint-disable @typescript-eslint/camelcase */ output: 'public', short_name: FOLDER, name: FOLDER, start_url: `${LINK_PREFIX}/`, background_color: THEME_COLOR, display: 'standalone', scope: `${LINK_PREFIX}/`, dir: 'ltr', // text direction: left to right theme_color: THEME_COLOR, icons: [ { src: `${LINK_PREFIX}${ICON_192_PATH}`, sizes: '192x192', type: 'image/png', }, { src: `${LINK_PREFIX}${ICON_512_PATH}`, sizes: '512x512', type: 'image/png', }, ], }, }) );
  3. add public/icons folder and include corresponding icon files in the folder
  4. copy ManifestHead.tsx from the example setup src/features/head
  5. import ManifestHead in pages

Notes:

  1. NextJs, next-pwa, workbox are still growing their api, so this project setup will be modified in the future for easier setup.
  2. There is a known error on the workbox: GoogleChrome/workbox#2178.
  3. Only direct children in next/head will be picked up at build time, so all next/link wrapped elements must be inserted (useEffect) after the next/head is loaded.

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •