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

Project-Setup/github_sql_pwa

Repository files navigation

Github Page PWA boilerplate with NextJs, code splitting Redux-Toolkit, Sql.js, Typeorm, 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
  • On browser SQL database
  • 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
  • Sql.js v1.2.2
  • Typeorm v0.2.24
  • 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
  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
  1. npm i -S typeorm reflect-metadata sql.js localforage next-transpile-modules
    npm i -D webpack babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
  2. add to compilerOptions in tsconfig.json
    {
      "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
      }
    }
  3. add to the plugins in babel.config.js
// ...
  plugins: [
    'babel-plugin-transform-typescript-metadata',
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }],
    // ...
  ]
// ...
  1. add to the top of _app.tsx or whatever the entry file
    import 'reflect-metadata';
    import localforage from 'localforage';
    // ...
    declare global {
      interface Window {
        localforage?: LocalForage;
      }
    }
    
    // ... 
    // ...inside the App
      useEffect(() => {
        window.localforage = localforage;
        return () => {
          window.localforage = undefined;
        };
      }, []);
    // ...
  2. add to next.config.js
    // ...
    const withTM = require('next-transpile-modules')(['typeorm/browser']);
    const webpack = require('webpack');
    const { dependencies } = require('./package-lock.json');
    // ...
    module.exports = () => 
      /* ... other wrappers, like withPWA()
      add withTM wrap innermost */
      withTM({
        webpack: (config, { isServer }) => {
          config.plugins.push(
              new webpack.ProvidePlugin({
                'window.SQL': 'sql.js/dist/sql-wasm.js',
              })
            );
            if (!isServer) {
              config.node = {
                fs: 'empty',
                net: 'empty',
                tls: 'empty',
              };
            }
            return config;
        }
    
        // ...other existing configs
        env: {
          NEXT_PUBLIC_SQL_JS_VERSION: dependencies['sql.js'].version || '',
        }
      })
    // ...
  3. copy connection.ts from src/sql/connection in the example setup and modify the defaultEntities

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

  •  
  •  
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy