diff --git a/.dockerignore b/.dockerignore index 7d245c9..fd51b21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ node_modules/ package-lock.json -build/ \ No newline at end of file +build/ +pnpm-lock.yaml +.vscode diff --git a/.gitignore b/.gitignore index 7d245c9..4b0bf20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ package-lock.json -build/ \ No newline at end of file +build/ +pnpm-lock.yaml +.vscode \ No newline at end of file diff --git a/README.md b/README.md index aa2f91f..cd85c3b 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,6 @@ dokku git:from-image publitefrontend publite/frontend:latest # TODO -- Create ServiceWorker (make it PWA) -- Migrate from LocalStorage to IndexedDB -- Add page position persistance +- Migrate pagination cache and book state from LocalStorage to IndexedDB - Add menu with book view setting -- Add move to page by number - Optimize page spliting algorythm (rewrite it) -- Fix css modules bundling diff --git a/package.json b/package.json index f76ed26..81a82e1 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,28 @@ { + "private": true, "scripts": { - "dev": "SNOWPACK_PUBLIC_API_URL=http://localhost:8081 SNOWPACK_PUBLIC_BASE_URL=http://localhost:8080 snowpack dev", - "build": "snowpack build", - "test": "echo \"Error: no test specified\" && exit 1" + "build": "webpack --config ./webpack.config.prod.js", + "dev": "webpack serve --config ./webpack.config.dev.js --stats-error-details", + "test": "echo \"Error: no test specified\" && exit 1", + "h": "cat ./webpack.config.dev.js" }, "devDependencies": { - "@snowpack/plugin-typescript": "^1.2.1", - "@types/node": "^16.3.3", - "@types/react": "^17.0.14", + "@svgr/webpack": "^5.5.0", + "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", - "@types/snowpack-env": "^2.3.3", - "snowpack": "^3.8.0", - "snowpack-plugin-svgr": "^0.1.2", - "typescript": "^4.3.5" + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^6.2.0", + "esbuild-loader": "^2.13.1", + "fork-ts-checker-webpack-plugin": "^6.2.13", + "style-loader": "^3.2.1", + "typescript": "^4.3.5", + "typescript-plugin-css-modules": "^3.4.0", + "webpack": "^5.46.0", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "^3.11.2" }, "dependencies": { + "idb": "^6.1.2", "react": "^17.0.2", "react-dom": "^17.0.2", "wouter": "^2.7.4" diff --git a/public/images/icons-128.png b/public/images/icons-128.png new file mode 100644 index 0000000..d53c483 Binary files /dev/null and b/public/images/icons-128.png differ diff --git a/public/images/icons-192.png b/public/images/icons-192.png new file mode 100644 index 0000000..517218a Binary files /dev/null and b/public/images/icons-192.png differ diff --git a/public/images/icons-512.png b/public/images/icons-512.png new file mode 100644 index 0000000..cd7b0e5 Binary files /dev/null and b/public/images/icons-512.png differ diff --git a/public/images/screenshot1.jpg b/public/images/screenshot1.jpg new file mode 100644 index 0000000..9f3a6fa Binary files /dev/null and b/public/images/screenshot1.jpg differ diff --git a/public/images/screenshot2.jpg b/public/images/screenshot2.jpg new file mode 100644 index 0000000..d20e545 Binary files /dev/null and b/public/images/screenshot2.jpg differ diff --git a/public/images/screenshot3.jpg b/public/images/screenshot3.jpg new file mode 100644 index 0000000..73a0562 Binary files /dev/null and b/public/images/screenshot3.jpg differ diff --git a/public/index.html b/public/index.html index 1df4b4e..cdd9798 100644 --- a/public/index.html +++ b/public/index.html @@ -3,12 +3,15 @@ + + + Publite
- + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..13928f7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,47 @@ +{ + "short_name": "Publite", + "name": "Publite: eBook reader", + "icons": [ + { + "src": "images/icons-128.png", + "type": "image/png", + "sizes": "128x128", + "purpose": "any maskable" + }, + { + "src": "/images/icons-192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "/images/icons-512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any maskable" + } + ], + "start_url": "/", + "background_color": "#ffffff", + "display": "standalone", + "scope": "/", + "theme_color": "#000000", + "description": "eBook reader supporting EPUB and FB2 files", + "screenshots": [ + { + "src": "/images/screenshot1.jpg", + "type": "image/jpg", + "sizes": "1080x2400" + }, + { + "src": "/images/screenshot2.jpg", + "type": "image/jpg", + "sizes": "1080x2400" + }, + { + "src": "/images/screenshot3.jpg", + "type": "image/jpg", + "sizes": "1080x2400" + } + ] +} diff --git a/snowpack.config.js b/snowpack.config.js deleted file mode 100644 index abe1a45..0000000 --- a/snowpack.config.js +++ /dev/null @@ -1,28 +0,0 @@ -// Snowpack Configuration File -// See all supported options: https://www.snowpack.dev/reference/configuration - -/** @type {import("snowpack").SnowpackUserConfig } */ -module.exports = { - plugins: ["@snowpack/plugin-typescript", "snowpack-plugin-svgr"], - packageOptions: { - polyfillNode: true, - }, - mount: { - public: "/", - src: "/dist", - }, - optimize: { - // bundle: true, - }, - routes: [ - { match: "routes", src: "robots.txt", dest: "/robots.txt" }, - { match: "routes", src: ".*", dest: "/index.html" }, - ], - devOptions: { - open: "none", - }, - alias: { - "~": "./src", - }, - exclude: ["**/node_modules/**/*", "**/*.test.*"], -}; diff --git a/src/App/index.tsx b/src/App/index.tsx index 86179bb..a870f44 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { Route, Switch } from "wouter"; -import { BookListContextProvider } from "~/context"; import { Bookshelf } from "~/pages/Bookshelf"; import { BookView } from "~/pages/BookView"; @@ -24,19 +23,17 @@ export const App = () => { )} - - - - - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/src/constants.ts b/src/constants.ts index f62fdca..d48c442 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,5 @@ -export const API_URL = import.meta.env.SNOWPACK_PUBLIC_API_URL; -export const BASE_URL = import.meta.env.SNOWPACK_PUBLIC_BASE_URL; +export const API_URL = process.env.PUBLIC_API_URL || ""; +export const BASE_URL = process.env.PUBLIC_BASE_URL || ""; +export const CACHE = "v1.0.1"; +export const DB_NAME = "publite"; +export const DB_VERSION = 101; diff --git a/src/context.tsx b/src/context.tsx deleted file mode 100644 index a1cae71..0000000 --- a/src/context.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import { useLibrary, UseLibraryReturnTuple } from "./hooks/useLibrary"; -import { BookT } from "./types/book"; - -export const BookListContext = React.createContext([ - {}, - (book: BookT) => {}, - [], -]); - -export const BookListContextProvider: React.FC = ({ children }) => { - const library = useLibrary(); - - return ( - - {children} - - ); -}; diff --git a/src/hooks/useBookState.ts b/src/hooks/useBookState.ts index 633dd1c..1ba4e95 100644 --- a/src/hooks/useBookState.ts +++ b/src/hooks/useBookState.ts @@ -21,11 +21,8 @@ export const useBookState = ( }, [hash]); useEffect(() => { - console.log(Boolean(!ready && state?.currentPage && goToPage)); if (!ready && state?.currentPage && pagesReady) { - console.log("Go to", state.currentPage); goToPage(state.currentPage); - console.log("Ready"); setReady(true); } else if (hash && !ready && pagesReady && typeof state === "object") { saveBookState(hash, { currentPage: 0 }); @@ -36,7 +33,6 @@ export const useBookState = ( useEffect( () => () => { if (hash) { - console.log(currentPage); if (ready && state) saveBookState(hash, state); else saveBookState(hash, { currentPage: currentPage.current || 0 }); } diff --git a/src/index.tsx b/src/index.tsx index 78f3909..83c40a3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; +import * as ServiceWorker from "./registerServiceWorker"; import "./index.css"; @@ -12,6 +13,4 @@ ReactDOM.render( document.getElementById("root") ); -if (import.meta.hot) { - import.meta.hot.accept(); -} +ServiceWorker.register(); diff --git a/src/pages/BookView/index.tsx b/src/pages/BookView/index.tsx index c28d1c6..292d6dc 100644 --- a/src/pages/BookView/index.tsx +++ b/src/pages/BookView/index.tsx @@ -1,29 +1,52 @@ -import React, { MouseEventHandler, useContext, useEffect, useRef } from "react"; +import React, { MouseEventHandler, useState, useEffect, useRef } from "react"; import { Redirect, useRoute } from "wouter"; import styles from "./BookView.module.css"; -import { BookListContext } from "~/context"; import { usePagination } from "~/hooks/usePagination"; import { IPageProps } from "~/types/page"; import { useBookState } from "~/hooks/useBookState"; +import { BookT } from "~/types/book"; +import { API_URL } from "~/constants"; +import { validateResponse } from "~/utils/api"; export const BookView = ({ setLoading, loading }: IPageProps) => { useEffect(() => setLoading(true), []); + const [hasErr, setHasErr] = useState(false); + const [book, setBook] = useState(); + const [_, params] = useRoute("/:hash"); - const [books] = useContext(BookListContext); const contentRef = useRef(null); const pageContainerRef = useRef(null); const pageRef = useRef(null); + useEffect(() => { + if (params?.hash) { + (async () => { + try { + const res = await fetch(API_URL + "/book/" + params.hash); + if (!res.ok) throw new Error(res.status + " " + res.statusText); + + const book: unknown = await res.json(); + + if (validateResponse(book)) setBook(book); + } catch (err) { + if (process.env.NODE_ENV === "development") console.error(err); + setHasErr(true); + setLoading(false); + } + })(); + } + }, []); + const [pagesReady, goToPage, currentPage, pagesNumber] = usePagination( contentRef, pageContainerRef, pageRef, - books && loading ? params?.hash : undefined, - params?.hash && books && loading ? books[params.hash]?.content : undefined + book ? params?.hash : undefined, + params?.hash ? book?.content : undefined ); const currentPageRef = useRef(currentPage); @@ -68,35 +91,33 @@ export const BookView = ({ setLoading, loading }: IPageProps) => { } }; - if (books) { - if (params?.hash && params.hash in books) - return ( - <> -
-
-
-
-
-
-
- - - {currentPage + 1} / {pagesNumber} - - -
- - ); - return ; - } else return <>; + if (hasErr) return ; + + return ( + <> +
+
+
+
+
+
+
+ + + {currentPage + 1} / {pagesNumber} + + +
+ + ); }; diff --git a/src/pages/Bookshelf/index.tsx b/src/pages/Bookshelf/index.tsx index 77d9620..12785cf 100644 --- a/src/pages/Bookshelf/index.tsx +++ b/src/pages/Bookshelf/index.tsx @@ -1,16 +1,29 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import styles from "./Bookshelf.module.css"; import { BookItem } from "./BookItem"; import { AddBook } from "./AddBook"; -import { BookListContext } from "~/context"; import { IPageProps } from "~/types/page"; +import { API_URL } from "~/constants"; +import { BookT } from "~/types/book"; +import { connected as swConnected } from "~/utils/serviceFetch"; export const Bookshelf = ({ setLoading }: IPageProps) => { useEffect(() => setLoading(true), []); - const [books] = useContext(BookListContext); + const [books, setBooks] = useState([]); + + useEffect(() => { + swConnected.then(async () => { + try { + const res = await fetch(API_URL + "/list"); + setBooks(await res.json()); + } catch (err) { + if (process.env.NODE_ENV === "development") console.error(err); + } + }); + }, []); useEffect(() => { if (books) setLoading(false); diff --git a/src/pages/UploadForm/index.tsx b/src/pages/UploadForm/index.tsx index 820af34..ab9c05b 100644 --- a/src/pages/UploadForm/index.tsx +++ b/src/pages/UploadForm/index.tsx @@ -1,18 +1,15 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useState } from "react"; import { useLocation } from "wouter"; import PlusIcon from "~/assets/plus.svg"; import styles from "./UploadForm.module.css"; import { submitFile, validateResponse, validState } from "~/utils/api"; -import { BookListContext } from "~/context"; import { IPageProps } from "~/types/page"; export const UploadForm = ({ setLoading }: IPageProps) => { const [error, setError] = useState(""); const [_, setLocation] = useLocation(); - const [__, saveBook] = useContext(BookListContext); - const processFile = async (file: File | undefined) => { try { if (validState(file)) { @@ -22,10 +19,7 @@ export const UploadForm = ({ setLoading }: IPageProps) => { const res = await submitFile(file); setLoading(false); - if (validateResponse(res)) { - saveBook(res); - setLocation("/"); - } + if (validateResponse(res)) setLocation("/"); } } catch (err) { setLoading(false); diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts new file mode 100644 index 0000000..05ff16e --- /dev/null +++ b/src/registerServiceWorker.ts @@ -0,0 +1,16 @@ +export const register = () => { + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => + navigator.serviceWorker + .register("/sw.js", { scope: "" }) + .then((registration) => { + if (process.env.NODE_ENV === "development") + console.log( + "Successfully registered ServiceWorker with scope:", + registration.scope + ); + }) + .catch((err) => console.error(err)) + ); + } +}; diff --git a/src/serviceWorker/cache.ts b/src/serviceWorker/cache.ts new file mode 100644 index 0000000..6ef2ece --- /dev/null +++ b/src/serviceWorker/cache.ts @@ -0,0 +1,23 @@ +import { CACHE } from "../constants"; + +const getCache = () => caches.open(CACHE); + +/** + * Caches static files for application + */ +export const precache = async () => + (await getCache()).addAll(["/", "/index.js", "/sw.js"]); + +/** + * Requests file from network or gets it from cache if offline + */ +export const fromCache = async (request: Request): Promise => { + try { + const response = await fetch(request); + (await getCache()).put(request, response.clone()); + return response; + } catch (err) { + const response = await (await getCache()).match(request); + return response || new Response(); + } +}; diff --git a/src/serviceWorker/db.ts b/src/serviceWorker/db.ts new file mode 100644 index 0000000..5571c0f --- /dev/null +++ b/src/serviceWorker/db.ts @@ -0,0 +1,31 @@ +import { openDB as idbOpenDB } from "idb"; +import { BookT } from "~/types/book"; +import { DB_NAME, DB_VERSION } from "../constants"; +import { PubliteDB } from "./schema"; + +/** + * Opens IndexedDB for interactions + */ +export const openDB = () => + idbOpenDB(DB_NAME, DB_VERSION, { + upgrade: (db, oldVersion, _, tsx) => { + if (oldVersion < 1) db.createObjectStore("Books", { keyPath: "hash" }); + }, + }); + +/** + * Saves IBook object in IndexedDB + */ +export const saveBook = async (book: BookT) => + (await openDB()).add("Books", book); + +/** + * Returns all books saved in IndexedDB + */ +export const getBooks = async () => (await openDB()).getAll("Books"); + +/** + * Gets book from IndexedDB by hash + */ +export const getBook = async (hash: string) => + (await openDB()).get("Books", hash); diff --git a/src/serviceWorker/fetchHandlers.ts b/src/serviceWorker/fetchHandlers.ts new file mode 100644 index 0000000..6872d0a --- /dev/null +++ b/src/serviceWorker/fetchHandlers.ts @@ -0,0 +1,59 @@ +import { getBook, getBooks, saveBook } from "./db"; +import { composeResponseStatus } from "./utils"; + +export interface PathHandler { + /** Path start for handler */ + path: string; + /** Function returning Response object */ + getResponse: () => Response | Promise; +} + +/** + * Routes fetch request path to specified handler + */ +export const handle = (requestPath: string, table: PathHandler[]) => { + for (const { path, getResponse: response } of table) + if (requestPath.startsWith(path)) return response(); + + return new Response(); +}; + +/** + * Converts book to html with publiteBackend server. + * + * First fetch handler with network request + */ +export const handleBookUpload = async (request: Request) => { + try { + const res = await fetch(request); + if (res.ok) { + const book = await res.json(); + await saveBook(book); + return new Response(JSON.stringify(book)); + } else throw new Error(res.status.toString() + res.statusText); + } catch (err) { + return new Response(JSON.stringify(err), composeResponseStatus(err)); + } +}; + +/** + * Gets all books from database + */ +export const handleBooks = async () => { + const list = await getBooks(); + + return new Response(JSON.stringify(list)); +}; + +/** + * Gets book from database + */ +export const handleBook = async (request: Request, hash: string) => { + const book = await getBook(hash); + if (book) return new Response(JSON.stringify(book)); + + return new Response(null, { + status: 404, + statusText: "No such book :(", + }); +}; diff --git a/src/serviceWorker/index.ts b/src/serviceWorker/index.ts new file mode 100644 index 0000000..1707290 --- /dev/null +++ b/src/serviceWorker/index.ts @@ -0,0 +1,39 @@ +import { API_URL } from "~/constants"; +import { fromCache, precache } from "./cache"; +import { openDB as createDB } from "./db"; +import { + handle, + handleBook, + handleBooks, + handleBookUpload, + PathHandler, +} from "./fetchHandlers"; +import { getHash } from "./utils"; + +declare const self: ServiceWorkerGlobalScope; + +self.addEventListener("install", (event) => event.waitUntil(precache())); + +self.addEventListener("activate", (event) => { + self.clients.claim(); + event.waitUntil(createDB()); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + const path = new URL(request.url).pathname; + + let handlers: PathHandler[]; + + if (request.url.startsWith(API_URL)) { + handlers = [ + { path: "/list", getResponse: () => handleBooks() }, + { path: "/book/", getResponse: () => handleBook(request, getHash(path)) }, + { path: "/uploadfile", getResponse: () => handleBookUpload(request) }, + ]; + } else { + handlers = [{ path: "", getResponse: () => fromCache(request) }]; + } + + event.respondWith(handle(path, handlers)); +}); diff --git a/src/serviceWorker/schema.ts b/src/serviceWorker/schema.ts new file mode 100644 index 0000000..5615b19 --- /dev/null +++ b/src/serviceWorker/schema.ts @@ -0,0 +1,9 @@ +import { DBSchema } from "idb"; +import { BookT } from "../types/book"; + +export interface PubliteDB extends DBSchema { + Books: { + key: string; + value: BookT; + }; +} diff --git a/src/serviceWorker/utils.ts b/src/serviceWorker/utils.ts new file mode 100644 index 0000000..a842d73 --- /dev/null +++ b/src/serviceWorker/utils.ts @@ -0,0 +1,15 @@ +/** + * Gets hash string from book path + */ +export const getHash = (path: string) => { + let hashLength = path.length - "/book/".length; + if (path.endsWith("/")) hashLength--; + + return path.substr("/book/".length, hashLength); +}; + +export const composeResponseStatus = (err: Error): ResponseInit => { + if (err.name === "NetowrkError") + return { status: 503, statusText: err.message }; + else return { status: 500, statusText: "Something bad happened (IDK)" }; +}; diff --git a/src/utils/api.ts b/src/utils/api.ts index 13f6fde..e14b4ad 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -34,7 +34,7 @@ export const submitFile = async ( return await res.json(); } catch (err) { - console.log("Network error:", err.message); + console.error("Network error:", err.message); throw err; } }; @@ -43,9 +43,9 @@ export const validateResponse = (content: unknown): content is BookT => { if (content && typeof content === "object") for (const key of requiredBookProps) if (!(key in content)) { - if (import.meta.env.NODE_ENV === "development") - console.log(`${key} is not specified in server response`); - return false; + if (process.env.NODE_ENV === "development") + console.error(`${key} is not specified in server response`); + throw new Error(`${key} is not specified in server response`); } return true; diff --git a/src/utils/serviceFetch.ts b/src/utils/serviceFetch.ts new file mode 100644 index 0000000..2e8685d --- /dev/null +++ b/src/utils/serviceFetch.ts @@ -0,0 +1,6 @@ +export const connected = new Promise((resolve) => { + if (navigator.serviceWorker.controller) return resolve(); + navigator.serviceWorker.addEventListener("controllerchange", (e) => + resolve() + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 4c356d1..a4ea4a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,18 @@ { "compilerOptions": { - "target": "es6", - "strict": true, "esModuleInterop": true, - "skipLibCheck": true, + "isolatedModules": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "lib": ["DOM"], + "lib": ["DOM", "webworker"], "module": "ES2020", "moduleResolution": "node", - "resolveJsonModule": true, "paths": { "~/*": ["./src/*"] - } + }, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es6" } } diff --git a/webpack.config.common.js b/webpack.config.common.js new file mode 100644 index 0000000..fe3aff8 --- /dev/null +++ b/webpack.config.common.js @@ -0,0 +1,53 @@ +const path = require("path"); +const webpack = require("webpack"); + +const CopyPlugin = require("copy-webpack-plugin"); +const ForkTsCheckerPlugin = require("fork-ts-checker-webpack-plugin"); + +module.exports = { + entry: { + "index.js": "./src/index.tsx", + "sw.js": "./src/serviceWorker/index.ts", + }, + output: { + path: path.resolve(__dirname, `./build/`), + clean: true, + filename: "[name]", + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: "esbuild-loader", + options: { + loader: "tsx", + target: "es2015", + }, + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, + { + test: /\.svg$/, + use: ["@svgr/webpack"], + }, + ], + }, + resolve: { + extensions: [".ts", ".tsx", ".js", ".json"], + alias: { "~": path.resolve(__dirname, "src/") }, + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.PUBLIC_API_URL": JSON.stringify( + "http://localhost:8081" + ), + }), + new ForkTsCheckerPlugin(), + new CopyPlugin({ + patterns: [{ from: "./public", to: "." }], + }), + ], +}; diff --git a/webpack.config.dev.js b/webpack.config.dev.js new file mode 100644 index 0000000..da9a17d --- /dev/null +++ b/webpack.config.dev.js @@ -0,0 +1,27 @@ +const path = require("path"); +const webpack = require("webpack"); + +const webpackConfig = require("./webpack.config.common"); + +module.exports = { + ...webpackConfig, + mode: "development", + watchOptions: { ignored: /node_modules/ }, + devServer: { + contentBase: path.join(__dirname, "build"), + compress: true, + port: 8080, + hot: false, + inline: false, + historyApiFallback: { + index: "index.html", + }, + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify("development"), + "process.env.PUBLIC_BASE_URL": JSON.stringify("http://localhost:8080"), + }), + ...webpackConfig.plugins, + ], +}; diff --git a/webpack.config.prod.js b/webpack.config.prod.js new file mode 100644 index 0000000..486866d --- /dev/null +++ b/webpack.config.prod.js @@ -0,0 +1,21 @@ +const { ESBuildMinifyPlugin } = require("esbuild-loader"); +const webpack = require("webpack"); + +const webpackConfig = require("./webpack.config.common"); + +module.exports = { + ...webpackConfig, + optimization: { + minimizer: [new ESBuildMinifyPlugin()], + }, + mode: "production", + plugins: [ + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify("production"), + "process.env.PUBLIC_BASE_URL": JSON.stringify( + "https://publite.dmitriy.icu" + ), + }), + ...webpackConfig.plugins, + ], +};