Example Github Page PWA with NextJs, code splitting Redux-Toolkit, Typescript, Eslint, Jest and Emotion.
- 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
- NextJs v9.4.2
- Redux-Toolkit v1.3.6
- Emotion v10
- Typescript v3.9.2
- [Nextjs_Ts_Eslint]NextJs, EmotionJs, Typescript
- [nextjs_redux_toolkit]NextJs, Redux-Toolkit
- [github_sql_pwa]Github page pwa setup with NextJs, code splitting Redux-Toolkit, Sql.js, Typeorm
- setup node env
nvm use npm install
- remove unwanted files in
public/
,src/
- add
.env
and other.env files - preview dev progress on
http://localhost:3000/
npm run dev
- export to
docs/
for Github Page deploynpm runexport
- readSetupfor notes
- install nvm in the os
-
nvm install node git init
- add
.gitignore
-
node -v>.nvmrc
-
npm init -y
-
npm i -S next react react-dom
- add a script to your package.json like this:
{ "scripts":{ "dev":"next", "build":"next build", "start":"next start" } }
-
npm i -D typescript @types/react @types/react-dom @types/node
- 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" ] }
- create
src/pages
folder (orpages
) - create
pages.tsx
undersrc/pages/
(i.e.src/pages/index.tsx
for/
route)
-
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
- 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 }, }, };
- create
.prettierrc.js
module.exports={ semi:true, trailingComma:'es5', singleQuote:true, printWidth:80, tabWidth:2, };
-
npm i -D jest babel-jest
- add scripts in
package.json
"scripts":{ "test":"jest", "test:watch":"jest --watch", "test:coverage":"jest --coverage" },
-
npm i -D enzyme enzyme-adapter-react-16 enzyme-to-json npm i -D typescript @types/enzyme @types/enzyme-adapter-react-16 @types/jest
- 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'], };
- create
babel.config.js
module.exports={ presets:['next/babel'], };
- create
jest/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()});
- change
env
in.eslintrc.js
env:{ browser:true, node:true, jest:true },
-
npm i -S @emotion/core npm i -D @emotion/babel-preset-css-prop jest-emotion eslint-plugin-emotion
- change
babel.config.js
module.exports={ presets:[ [ 'next/babel', { 'preset-env':{}, 'preset-react':{}, }, ], '@emotion/babel-preset-css-prop', ], };
- 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', //... ], //... }
- add
jest/jest.setupAfterEnv.js
import{matchers}from'jest-emotion'; expect.extend(matchers);
- 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)
- add
.env.production
NEXT_PUBLIC_LINK_PREFIX=/{folder}
- create
LINK_PREFIX
innext.config.js
constLINK_PREFIX=process.env.NEXT_PUBLIC_LINK_PREFIX||''; module.exports=()=>({ assetPrefix:LINK_PREFIX, });
- change
as
prop innext/Link
to addlinkPrefix
,similar tosrc/features/link/Link.tsx
in the example setup - change
scripts
inpackage.json
{ "scripts":{ "export":"NODE_ENV=production npm run build && next export -o docs && touch docs/.nojekyll" } }
-
npm i -D @babel/plugin-proposal-nullish-coalescing-operator @babel/plugin-proposal-optional-chaining
- add the plugins to
babel.config.js
module.exports={ presets:[ //... ], plugins:[ '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', ], };
-
npm i -S react-redux @reduxjs/toolkit npm i -D @types/react-redux
- either use
next-redux-wrapper
package (npm i -P next-redux-wrapper
) or copy thewithRedux.tsx
from the example setupsrc/utils/redux
- create custom
makeStore
function,_app.tsx
page and other redux setup as examples innext-redux-wrapper
repo shows
- copy
configureStore.ts
,DynamicStoreWrap.tsx
from the example setupsrc/utils/redux
,andobjectAssign.ts
fromsrc/utils/common
- change
src/_app.tsx
similar to the example setup
-
npm i -S next-pwa next-manifest
- change
next.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', }, ], }, }) );
- add
public/icons
folder and include corresponding icon files in the folder - copy
ManifestHead.tsx
from the example setupsrc/features/head
- import
ManifestHead
in pages
- NextJs, next-pwa, workbox are still growing their api, so this project setup will be modified in the future for easier setup.
- There is a known error on the workbox:GoogleChrome/workbox#2178.
- Only direct children in
next/head
will be picked up at build time, so allnext/link
wrapped elements must be inserted (useEffect) after thenext/head
is loaded.