diff --git a/README b/README new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_UkVBRE1F --- /dev/null +++ b/README @@ -0,0 +1,47 @@ +# CookieCutter React TS CwClient + +This is a [CookieCutter](https://github.com/cookiecutter/cookiecutter) template to create +a React, with TypeScript application, initialized with a simple example for +[@cubicweb/Client](https://www.npmjs.com/package/@cubicweb/client) + +## How to use it + +First of all you need to +[install CookieCutter](https://cookiecutter.readthedocs.io/en/stable/installation.html#install-cookiecutter) +if this is not already installed: +```bash +python3 -m pip install --user cookiecutter +``` + +then you can create the new project using the CookieCutter template: +```bash +cookiecutter ssh://git@forge.extranet.logilab.fr/cubicweb/cookiecutter-react-ts-cwclient +``` + +The prompt will ask several questions about project details: +- `project_slug`: is the project name, which will be used as `package.json` + name and folder name +- `description`: is the project description which will appear in the + `package.json` +- `author`: is the project author name which will appear in the `package.json` +- `license`: is the project license which will appear in the `package.json` +- `cubicweb_server`: is the CubicWeb instance you want to query (/!\ this is + ths CubicWeb instance URL, not the API one, the `/api` will be added + automatically) + +then you can go to the folder: +```bash +cd {project_slug} +``` +and install all needed dependencies: +```bash +yarn +``` +you can now start the dev server using the folowing command: +```bash +yarn start +``` + +This application uses React-Router with only one route, which renders a list of +CWUser. This is a simple example to show how it works and what we can do. You +can now modify everything you need to adapt to your proper needs. diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_Y29va2llY3V0dGVyLmpzb24= --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,7 @@ +{ + "project_slug": "cubicweb-react-ts-app", + "description": "CubicWeb React TypeScript app", + "author": "Logilab", + "license": "LGPL-3.0-or-later", + "cubicweb_server": "https://www.semweb.pro" +} diff --git a/{{ cookiecutter.project_slug }}/.env.example b/{{ cookiecutter.project_slug }}/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS8uZW52LmV4YW1wbGU= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.env.example @@ -0,0 +1,1 @@ +PORT=3000 diff --git a/{{ cookiecutter.project_slug }}/.hgignore b/{{ cookiecutter.project_slug }}/.hgignore new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS8uaGdpZ25vcmU= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.hgignore @@ -0,0 +1,5 @@ +/node_modules +/build +.env +.env.local +.log diff --git a/{{ cookiecutter.project_slug }}/README.md b/{{ cookiecutter.project_slug }}/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9SRUFETUUubWQ= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/README.md @@ -0,0 +1,4 @@ +# {{ cookiecutter.project_slug }} +License: {{ cookiecutter.license }} + +{{ cookiecutter.description }} diff --git a/{{ cookiecutter.project_slug }}/environement.d.ts b/{{ cookiecutter.project_slug }}/environement.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9lbnZpcm9uZW1lbnQuZC50cw== --- /dev/null +++ b/{{ cookiecutter.project_slug }}/environement.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + NODE_ENV: 'development' | 'production'; + } + } +} + +export {}; diff --git a/{{ cookiecutter.project_slug }}/index.html b/{{ cookiecutter.project_slug }}/index.html new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9pbmRleC5odG1s --- /dev/null +++ b/{{ cookiecutter.project_slug }}/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="description" content="{{ cookiecutter.description }}" /> + <title>{{ cookiecutter.project_slug }}</title> + </head> + <body> + <noscript>You need to enable javascript to run this app</noscript> + <div id="root"></div> + </body> +</html> diff --git a/{{ cookiecutter.project_slug }}/modules.d.ts b/{{ cookiecutter.project_slug }}/modules.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9tb2R1bGVzLmQudHM= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/modules.d.ts @@ -0,0 +1,28 @@ +//------------------------------------------------------------------------------ +// Decleare all custom static modules in this file +//------------------------------------------------------------------------------ + +declare module "*.jpg" { + const src: string; + export default src; +} + +//------------------------------------------------------------------------------ + +declare module "*.png" { + const src: string; + export default src; +} + +//------------------------------------------------------------------------------ + +declare module "*.svg" { + import * as React from "react"; + + export const ReactComponent: React.FunctionComponent< + React.SVGProps<SVGSVGElement> & { title?: string } + >; + + const src: string; + export default src; +} diff --git a/{{ cookiecutter.project_slug }}/package.json b/{{ cookiecutter.project_slug }}/package.json new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9wYWNrYWdlLmpzb24= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/package.json @@ -0,0 +1,64 @@ +{ + "homepage": ".", + "name": "{{ cookiecutter.project_slug }}", + "description": "{{ cookiecutter.description }} ", + "license": "{{ cookiecutter.license }}", + "author": "{{ cookiecutter.author }}", + "dependencies": { + "@cubicweb/client": "^1.5.0", + "dotenv": "^16.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^4.4.0", + "react-router-dom": "^6.3.0" + }, + "devDependencies": { + "@types/node": "^18.7.6", + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "@types/react-router-dom": "^5.3.3", + "@typescript-eslint/eslint-plugin": "^5.33.1", + "@typescript-eslint/parser": "^5.33.1", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.7.1", + "eslint": "^8.22.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.6.1", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-webpack-plugin": "^3.2.0", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.0", + "mini-css-extract-plugin": "^2.6.1", + "react-dev-utils": "^12.0.1", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.5", + "ts-loader": "^9.3.1", + "typescript": "^4.7.4", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.10.0", + "webpack-manifest-plugin": "^5.0.0" + }, + "scripts": { + "start": "webpack serve --config webpack/webpack.config.js --mode development", + "build": "webpack --config webpack/webpack.config.js --mode production" + }, + "eslintConfig": { + "extends": [ + "react-app" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/{{ cookiecutter.project_slug }}/src/CubicWebClient.ts b/{{ cookiecutter.project_slug }}/src/CubicWebClient.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvQ3ViaWNXZWJDbGllbnQudHM= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/CubicWebClient.ts @@ -0,0 +1,12 @@ +import React from "react"; +import { Client } from "@cubicweb/client"; + +const client = new Client("http://localhost:8080/api"); + +export const CWClientContext = React.createContext<{ + client: Client; + cwserver: string; +}>({ + client, + cwserver: "http://localhost:8080", +}); diff --git a/{{ cookiecutter.project_slug }}/src/assets/public/favicon.ico b/{{ cookiecutter.project_slug }}/src/assets/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..650566545d0860dcd0d96bf93b7013c5ab4dc55f GIT binary patch literal 34494 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 diff --git a/{{ cookiecutter.project_slug }}/src/assets/public/img/logo192.png b/{{ cookiecutter.project_slug }}/src/assets/public/img/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..5673c0fd83ec1004e9c1d67384a1111e9318e2f8 GIT binary patch literal 8598 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 Bgkk^y diff --git a/{{ cookiecutter.project_slug }}/src/assets/public/manifest.json b/{{ cookiecutter.project_slug }}/src/assets/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvYXNzZXRzL3B1YmxpYy9tYW5pZmVzdC5qc29u --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/assets/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "@react/starter", + "name": "react-starter-kit", + "icons": [ + { + "src": "img/logo192.png", + "type": "image/png", + "sizes": "192x192" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/{{ cookiecutter.project_slug }}/src/assets/public/robots.txt b/{{ cookiecutter.project_slug }}/src/assets/public/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvYXNzZXRzL3B1YmxpYy9yb2JvdHMudHh0 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/assets/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.css b/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.css new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvY29tcG9uZW50cy9jd3VzZXJzL0NXVXNlcnMuY3Nz --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.css @@ -0,0 +1,3 @@ +.cwusers ul { + list-style: none; +} diff --git a/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.tsx b/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvY29tcG9uZW50cy9jd3VzZXJzL0NXVXNlcnMudHN4 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/components/cwusers/CWUsers.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +import { BsPersonCircle } from "react-icons/bs"; + +import { CWClientContext } from "../../CubicWebClient"; +import { useCWUsers, CWUser } from "../../hooks/useCWUser"; + +import "./CWUsers.css"; + +export const CWUsers: React.FC = () => { + const { cwserver } = React.useContext(CWClientContext); + const cwusers: CWUser[] | null = useCWUsers(); + if (cwusers === null) { + return <div>Loading ...</div>; + } + return ( + <div className="cwusers"> + <h1>CWUsers from {cwserver}</h1> + <ul> + {cwusers.map((cwuser) => ( + <li> + <BsPersonCircle /> - {cwuser.firstname} {cwuser.surname} ( + {cwuser.login}) + </li> + ))} + </ul> + </div> + ); +}; diff --git a/{{ cookiecutter.project_slug }}/src/configs.ts b/{{ cookiecutter.project_slug }}/src/configs.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvY29uZmlncy50cw== --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/configs.ts @@ -0,0 +1,4 @@ +// Envirement +export const env = { + mode: process.env.NODE_ENV, +}; diff --git a/{{ cookiecutter.project_slug }}/src/hooks/useCWUser.ts b/{{ cookiecutter.project_slug }}/src/hooks/useCWUser.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvaG9va3MvdXNlQ1dVc2VyLnRz --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/hooks/useCWUser.ts @@ -0,0 +1,40 @@ +import * as React from "react"; + +import { ResultSet } from "@cubicweb/client"; + +import { CWClientContext } from "../CubicWebClient"; + +export interface CWUser { + eid: number; + login: string; + firstname: string | null; + surname: string | null; +} + +export function useCWUsers(): CWUser[] | null { + const { client } = React.useContext(CWClientContext); + + const [cwusers, setCWUsers] = React.useState<CWUser[] | null>(null); + + React.useEffect(() => { + client + .execute( + "Any U, L, F, S WHERE U is CWUser, U login L, U firstname F, U surname S" + ) + .then((data: ResultSet) => { + setCWUsers( + data.map( + ([eid, login, firstname, surname]) => + ({ + eid, + login, + firstname, + surname, + } as CWUser) + ) + ); + }); + }, [client]); + + return cwusers; +} diff --git a/{{ cookiecutter.project_slug }}/src/index.tsx b/{{ cookiecutter.project_slug }}/src/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS9zcmMvaW5kZXgudHN4 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { render as renderApp } from "react-dom"; + +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Client } from "@cubicweb/client"; + +import { CWClientContext } from "CubicWebClient"; +import { CWUsers } from "./components/cwusers/CWUsers"; + +const cwserver = "{{ cookiecutter.cubicweb_server }}"; +const client = new Client(`${cwserver}/api`); +const contextValue = { client, cwserver }; + +renderApp( + <React.StrictMode> + <CWClientContext.Provider value={contextValue}> + <BrowserRouter> + <Routes> + <Route path="/" element={<CWUsers />} /> + </Routes> + </BrowserRouter> + </CWClientContext.Provider> + </React.StrictMode>, + document.getElementById("root") +); diff --git a/{{ cookiecutter.project_slug }}/tsconfig.json b/{{ cookiecutter.project_slug }}/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS90c2NvbmZpZy5qc29u --- /dev/null +++ b/{{ cookiecutter.project_slug }}/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedLocals": true, + "strict": true, + "baseUrl": "src", + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "outDir": "./build/", + "removeComments": true, + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["dom", "dom.iterable", "esnext"], + "target": "es6", + "skipLibCheck": true + }, + "include": ["**/*.ts", "**/*.d.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/{{ cookiecutter.project_slug }}/webpack/paths.js b/{{ cookiecutter.project_slug }}/webpack/paths.js new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS93ZWJwYWNrL3BhdGhzLmpz --- /dev/null +++ b/{{ cookiecutter.project_slug }}/webpack/paths.js @@ -0,0 +1,14 @@ +const path = require("path"); + +//------------------------------------------------------------------------------ + +const rootDir = path.resolve(__dirname, "../"); + +module.exports = { + modules: path.resolve(path.join(rootDir, "node_modules")), + public: path.resolve(path.join(rootDir, "src/assets/public")), + src: path.resolve(path.join(rootDir, "src")), + build: path.resolve(path.join(rootDir, "build")), + index: path.resolve(path.join(rootDir, "src/index.tsx")), + index_html: path.resolve(path.join(rootDir, "index.html")), +}; diff --git a/{{ cookiecutter.project_slug }}/webpack/plugins/build-wiper.js b/{{ cookiecutter.project_slug }}/webpack/plugins/build-wiper.js new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS93ZWJwYWNrL3BsdWdpbnMvYnVpbGQtd2lwZXIuanM= --- /dev/null +++ b/{{ cookiecutter.project_slug }}/webpack/plugins/build-wiper.js @@ -0,0 +1,13 @@ +const fs = require('fs-extra'); + +//------------------------------------------------------------------------------ + +module.exports = class BuildWiper { + constructor (buildPath) { + this.buildPath = buildPath; + } + + apply() { + fs.emptyDirSync(this.buildPath); + } +} diff --git a/{{ cookiecutter.project_slug }}/webpack/plugins/check-port.js b/{{ cookiecutter.project_slug }}/webpack/plugins/check-port.js new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS93ZWJwYWNrL3BsdWdpbnMvY2hlY2stcG9ydC5qcw== --- /dev/null +++ b/{{ cookiecutter.project_slug }}/webpack/plugins/check-port.js @@ -0,0 +1,50 @@ +const net = require('net'); +const chalk = require('react-dev-utils/chalk'); + +//------------------------------------------------------------------------------ + +/** + * Check if a port is already in use + * @param {number} port Target port to check + * @param {(inUse: boolean) => void} result A callback that provides the result + */ +function portInUse(port, result) { + const server = net.createServer(); + + // Handle port is used + server.once('error', (error) => { + if (error.code === 'EADDRINUSE') { + result(true); + }; + }); + + // Handle port is free to use + server.once('listening', () => { + server.close(); + result(false); + }); + + server.listen(port); +}; + +//------------------------------------------------------------------------------ + +const pluginName = 'check-port-plugin'; + +module.exports = class CheckPortPlugin { + constructor(port) { + this.port = port; + } + + apply(compiler) { + // Check port before compilation happens + compiler.hooks.initialize.tap(pluginName, () => { + portInUse(this.port, (isInUse) => { + if (isInUse) { + console.log(chalk.yellow(`Port ${this.port} is already in use`)); + process.exit(1); + } + }); + }); + } +} diff --git a/{{ cookiecutter.project_slug }}/webpack/webpack.config.js b/{{ cookiecutter.project_slug }}/webpack/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..fe5b8f04807cd5e2c1980e28ac948aa8a1e670d7_e3sgY29va2llY3V0dGVyLnByb2plY3Rfc2x1ZyB9fS93ZWJwYWNrL3dlYnBhY2suY29uZmlnLmpz --- /dev/null +++ b/{{ cookiecutter.project_slug }}/webpack/webpack.config.js @@ -0,0 +1,253 @@ +//------------------------------------------------------------------------------ +// This app is configured to use typescript. +// See https://webpack.js.org/concepts/ on how to configure this file. +//------------------------------------------------------------------------------ + +"use-strict"; + +//------------------------------------------------------------------------------ + +const path = require("path"); +const webpack = require("webpack"); + +// Configure envirement (NOTE: This must be done as early as pissible) +require("dotenv").config(); + +//------------------------------------------------------------------------------ + +const TerserPlugin = require("terser-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const ESLintPlugin = require("eslint-webpack-plugin"); + +const CheckPortPlugin = require("./plugins/check-port"); +const BuildFolderWiper = require("./plugins/build-wiper"); + +const paths = require("./paths"); + +//------------------------------------------------------------------------------ + +const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); +const eslintFormatter = require("react-dev-utils/eslintFormatter"); + +//------------------------------------------------------------------------------ + +const requiredFiles = [paths.index_html, paths.index]; + +if (!checkRequiredFiles(requiredFiles)) { + process.exit(1); +} + +//------------------------------------------------------------------------------ + +const mainTsRejex = /\.(ts|tsx)$/; +const mainCssRejex = /\.(css)$/; +const assetsRejex = /\.(jpg|jpeg|png|gif|mp3|mp4|svg)$/; + +//------------------------------------------------------------------------------ + +module.exports = function (_, webpackEnv) { + const isDevelopment = webpackEnv.mode === "development"; + const isProduction = webpackEnv.mode === "production"; + const isProfile = process.argv.includes("--profile"); + const isProductionProfile = isProduction && isProfile; + const port = process.env.PORT || 8080; + + const getStyleLoaders = () => [ + isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader, + { + loader: "css-loader", + options: { + sourceMap: isDevelopment, + }, + }, + ]; + + return { + target: "web", + entry: paths.index, + output: { + path: paths.build, + filename: isDevelopment + ? "static/js/[name].js" + : isProduction && "static/js/[name].[contenthash:8].js", + chunkFilename: isDevelopment + ? "static/js/[name].chunk.js" + : isProduction && "static/js/[name].[contenthash:8].chunk.js", + publicPath: "/", + assetModuleFilename: "assets/[hash][ext]", + }, + devtool: isDevelopment ? "source-map" : false, + devServer: { + client: { + logging: "none", + overlay: { + // Show only errors on the browser + errors: true, + warnings: false, + }, + }, + headers: [ + // Security headers + { + key: "Strict-Transport-Security", + value: "max-age=63072000; preload", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + { + key: "X-Frame-Options", + value: "deny", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "Referrer-Policy", + value: "origin-when-cross-origin", + }, + ], + historyApiFallback: true, // Nessessary for react router to work + host: "0.0.0.0", // Allow local network access to the server + hot: true, + port, + }, + module: { + rules: [ + { + test: mainTsRejex, + exclude: /node_modules/, + use: ["ts-loader"], + }, + { + test: mainCssRejex, + exclude: /node_modules/, + use: getStyleLoaders(), + }, + { + test: assetsRejex, + exclude: [ + /node_modules/, + /\.(js|mjs|jsx|ts|tsx)$/, + /\.html$/, + /\.json$/, + ], + use: [ + { + loader: "file-loader", + options: { + outputPath: "static/media", + name: "[name].[contenthash:8].[ext]", + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: "asset/resource", + }, + ], + }, + resolve: { + extensions: ["*", ".js", ".ts", ".tsx"], + modules: [paths.src, paths.modules], + fallback: { + contentBase: paths.build, + events: false, + url: false, + }, + }, + optimization: { + minimize: isProduction, + minimizer: [ + new TerserPlugin({ + terserOptions: { + parse: { + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + comparisons: false, + inline: 2, + }, + mangle: { + safari10: true, + }, + keep_classnames: isProductionProfile, + keep_fnames: isProductionProfile, + output: { + ecma: 5, + comments: false, + ascii_only: true, + }, + sourceMap: false, + }, + }), + ], + }, + plugins: [ + new ESLintPlugin({ formatter: eslintFormatter }), + new HtmlWebpackPlugin({ + template: paths.index_html, + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + }), + isProduction && + new MiniCssExtractPlugin({ + filename: "static/css/[name].[contenthash:8].css", + chunkFilename: "static/css/[name].[contenthash:8].chunk.css", + }), + new WebpackManifestPlugin({ + fileName: "asset-manifest.json", + publicPath: "/", + generate: (seed, files, entrypoints) => { + const manifestFiles = files.reduce((manifest, file) => { + manifest[file.name] = file.path; + return manifest; + }, seed); + + const entrypointFiles = entrypoints.main.filter((fileName) => { + return !fileName.endsWith(".map"); + }); + + return { + files: manifestFiles, + entrypoints: entrypointFiles, + }; + }, + }), + new CopyWebpackPlugin({ + patterns: [ + { + from: paths.public, + to: ".", + }, + ], + }), + isDevelopment && new CheckPortPlugin(port), + isProduction && new BuildFolderWiper(paths.build), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify(webpackEnv.mode), + }), + new webpack.EnvironmentPlugin([ + // Pass all public env variables here + ]), + ].filter(Boolean), + }; +};