Skip to content
This repository has been 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-pageReactProgressive Web App
  • Installable forofflineuse through Chrome on desktop ormobile
  • Can be statically hosted onGithub Pagefor free (or as a regular web app hosted on a custom server)
  • Dynamically loadedReduxreducers forcode splitting
  • Prefetch security sensitive content at build time
  • All inTypescript/Javascript withCSS-in-JS
  • Easy testing withJestandEnzyme
  • Eslinthelps 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 inpublic/,src/
  3. add.envand other.env files
  4. preview dev progress onhttp://localhost:3000/
    npm run dev
  5. export todocs/for Github Page deploy
    npm runexport
  6. readSetupfor 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. createtsconfig.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. createsrc/pagesfolder (orpages)
  2. createpages.tsxundersrc/pages/(i.e.src/pages/index.tsxfor/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 inpackage.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. createjest.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. createbabel.config.js
    module.exports={
    presets:['next/babel'],
    };
  6. createjest/jest.setup.js
    importEnzymefrom'enzyme';
    importAdapterfrom'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:newAdapter()});
  7. changeenvin.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. changebabel.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. addjest/jest.setupAfterEnv.js
    import{matchers}from'jest-emotion';
    
    expect.extend(matchers);
  5. add serializers and setup files tojest/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. createLINK_PREFIXinnext.config.js
    constLINK_PREFIX=process.env.NEXT_PUBLIC_LINK_PREFIX||'';
    module.exports=()=>({
    assetPrefix:LINK_PREFIX,
    });
  2. changeasprop innext/Linkto addlinkPrefix,similar tosrc/features/link/Link.tsxin the example setup
  3. changescriptsinpackage.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 tobabel.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 usenext-redux-wrapperpackage (npm i -P next-redux-wrapper) or copy thewithRedux.tsxfrom the example setupsrc/utils/redux
  3. create custommakeStorefunction,_app.tsxpage and other redux setup as examples innext-redux-wrapperrepo shows
  1. copyconfigureStore.ts,DynamicStoreWrap.tsxfrom the example setupsrc/utils/redux,andobjectAssign.tsfromsrc/utils/common
  2. changesrc/_app.tsxsimilar to the example setup
  1. npm i -S next-pwa next-manifest
  2. changenext.config.js
    constisProd=process.env.NODE_ENV==='production';
    constFOLDER=LINK_PREFIX&&LINK_PREFIX.substring(1);
    
    // tranfrom precache url for browsers that encode dynamic routes
    // i.e. "[id].js" => "%5Bid%5D.js"
    constencodeUriTransform=async(manifestEntries)=>{
    constmanifest=manifestEntries.map((entry)=>{
    entry.url=encodeURI(entry.url);
    returnentry;
    });
    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. addpublic/iconsfolder and include corresponding icon files in the folder
  4. copyManifestHead.tsxfrom the example setupsrc/features/head
  5. importManifestHeadin 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 innext/headwill be picked up at build time, so allnext/linkwrapped elements must be inserted (useEffect) after thenext/headis loaded.