web: migrate to rsbuild

This commit is contained in:
Abdullah Atta
2025-10-12 22:32:34 +05:00
parent 3b65ca56a1
commit 60fd092e00
18 changed files with 603 additions and 651 deletions

View File

@@ -55,6 +55,8 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --foreground-scripts --verbose");
await exec("bun electron-builder install-app-deps");
}
@@ -68,9 +70,9 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["bun", "run", "start:desktop"],
["yarn", "run", "start:desktop"],
path.join(__dirname, "..", "..", "web"),
(data) => data.includes("Network: use --host to expose")
(data) => data.includes("built in")
);
}
@@ -80,7 +82,7 @@ async function onChange(first) {
}
execAsync(
"yarn",
"bun",
["electron", path.join("build", "electron.js")],
true,
cleanup

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@
"@dnd-kit/sortable": "^8.0.0",
"@emotion/react": "11.11.1",
"@hazae41/foras": "^2.1.4",
"@henrygd/queue": "^1.0.6",
"@lingui/core": "5.1.2",
"@lingui/react": "5.1.2",
"@henrygd/queue": "^1.1.1",
"@lingui/core": "5.5.1",
"@lingui/react": "5.5.1",
"@mdi/js": "7.4.47",
"@mdi/react": "1.6.1",
"@notesnook-importer/core": "^2.2.2",
@@ -46,10 +46,10 @@
"@zip.js/zip.js": "^2.7.62",
"async-mutex": "0.5.0",
"axios": "^1.7.9",
"clipboard-polyfill": "4.1.0",
"clipboard-polyfill": "4.1.1",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "1.11.13",
"dayjs": "1.11.18",
"diffblazer": "^1.0.1",
"electron-trpc": "0.7.1",
"event-source-polyfill": "1.0.31",
@@ -58,7 +58,7 @@
"hash-wasm": "4.12.0",
"hotkeys-js": "^3.8.3",
"katex": "0.16.11",
"mac-scrollbar": "0.13.6",
"mac-scrollbar": "0.13.8",
"mutative": "^1.1.0",
"pdfjs-dist": "3.6.172",
"phone": "^3.1.14",
@@ -84,16 +84,17 @@
"zustand-mutative": "^1.2.0"
},
"devDependencies": {
"@babel/core": "7.22.5",
"@rsbuild/plugin-react": "^1.4.1",
"@rsbuild/plugin-svgr": "^1.2.2",
"@aaroon/workbox-rspack-plugin": "^0.3.2",
"@cloudflare/workers-types": "^4.20250224.0",
"@playwright/test": "1.48.2",
"@swc/plugin-react-remove-properties": "^6.0.2",
"@playwright/test": "1.56.0",
"@rsbuild/core": "^1.5.17",
"@rsdoctor/rspack-plugin": "^1.3.2",
"@swc/plugin-react-remove-properties": "^9.1.0",
"@trpc/server": "10.45.2",
"@types/babel__core": "^7.20.1",
"@types/event-source-polyfill": "1.0.5",
"@types/file-saver": "^2.0.7",
"@types/marked": "^6.0.0",
"@types/node-fetch": "^2.6.12",
"@types/platform": "^1.3.6",
"@types/react": "18.3.5",
"@types/react-avatar-editor": "^13.0.3",
@@ -101,24 +102,11 @@
"@types/react-modal": "3.16.3",
"@types/react-scroll-sync": "^0.9.0",
"@types/wicg-file-system-access": "^2023.10.5",
"@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.19",
"better-sqlite3-multiple-ciphers": "11.5.0",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
"dotenv": "16.4.7",
"esbuild": "0.21.5",
"file-loader": "^6.2.0",
"find-process": "^1.4.4",
"happy-dom": "16.0.1",
"ip": "^2.0.1",
"lorem-ipsum": "^2.0.4",
"otplib": "^12.0.1",
"rollup-plugin-visualizer": "^5.13.1",
"vite": "5.4.11",
"vite-plugin-env-compatible": "^2.0.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-svgr": "^4.3.0",
"vitest": "2.1.8",
"workbox-core": "^7.3.0",
"workbox-expiration": "^7.3.0",
@@ -127,14 +115,14 @@
"workbox-strategies": "^7.3.0"
},
"scripts": {
"start": "PLATFORM=web vite",
"start:desktop": "PLATFORM=desktop vite",
"start": "PLATFORM=web rsbuild dev",
"start:desktop": "PLATFORM=desktop rsbuild dev",
"start:test": "serve -s build/ -p 3000",
"build": "PLATFORM=web vite build",
"build:test": "PLATFORM=web TEST=true vite build",
"build:beta": "PLATFORM=web BETA=true vite build",
"build:desktop": "PLATFORM=desktop vite build",
"analyze": "ANALYZING=true PLATFORM=web vite build",
"build": "PLATFORM=web rsbuild build",
"build:test": "PLATFORM=web TEST=true rsbuild build",
"build:beta": "PLATFORM=web BETA=true rsbuild build",
"build:desktop": "PLATFORM=desktop rsbuild build",
"analyze": "RSDOCTOR=true rsbuild build",
"test": "playwright test -u"
},
"browserslist": {

246
apps/web/rsbuild.config.ts Normal file
View File

@@ -0,0 +1,246 @@
import { defineConfig, loadEnv, RsbuildPluginAPI } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginSvgr } from "@rsbuild/plugin-svgr";
import { InjectManifest } from "@aaroon/workbox-rspack-plugin";
import { execSync } from "child_process";
import { version } from "./package.json";
import path from "path";
import { readFileSync } from "fs";
const gitHash = (() => {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch (e) {
return process.env.GIT_HASH || "gitless";
}
})();
// const appVersion = version.replaceAll(".", "").replace("-beta", "");
const isBeta = version.includes("-beta");
const isTesting =
process.env.TEST === "true" || process.env.NODE_ENV === "development";
const isDesktop = process.env.PLATFORM === "desktop";
const isThemeBuilder = process.env.THEME_BUILDER === "true";
const { publicVars } = loadEnv({ prefixes: ["NN_"] });
export default defineConfig({
html: {
template: "./src/index.html"
},
output: {
sourceMap: !isDesktop,
cleanDistPath: true,
distPath: {
root: "./build",
assets: "assets",
js: "assets",
css: "assets",
cssAsync: "assets",
font: "assets",
image: "assets",
jsAsync: "assets",
svg: "assets",
wasm: "assets",
media: "assets"
}
},
source: {
entry: {
index: "./src/index.ts"
},
define: {
...publicVars,
APP_TITLE: `"${
isThemeBuilder ? "Notesnook Theme Builder" : "Notesnook"
}"`,
GIT_HASH: `"${gitHash}"`,
APP_VERSION: `"${version}"`,
PUBLIC_URL: `"${process.env.PUBLIC_URL || ""}"`,
IS_DESKTOP_APP: isDesktop,
PLATFORM: `"${process.env.PLATFORM}"`,
IS_TESTING: process.env.TEST === "true",
IS_BETA: isBeta,
IS_THEME_BUILDER: isThemeBuilder
}
},
resolve: {
dedupe: [
"react",
"react-dom",
"@mdi/js",
"@mdi/react",
"@emotion/react",
"katex",
"react-modal",
"dayjs",
"@streetwriters/kysely"
]
},
tools: {
rspack: {
externalsPresets: isDesktop
? { electronRenderer: true, electron: true }
: undefined,
plugins: [
...(isThemeBuilder || isDesktop || process.env.NODE_ENV !== "production"
? []
: [
new InjectManifest({
swSrc: "./src/service-worker.ts",
swDest: "service-worker.js",
mode: "production",
include: ["**/*.{js,css,html,wasm}", "**/Inter-*.woff2"],
exclude: [
"**/node_modules/**/*",
"**/code-lang-*.js",
"pdf.worker.min.js"
]
})
])
],
externals: isDesktop
? {
"node:crypto": "commonjs crypto",
stream: "commonjs stream",
"better-sqlite3-multiple-ciphers":
"commonjs better-sqlite3-multiple-ciphers",
path: "commonjs path",
fs: "commonjs fs",
url: "commonjs url",
util: "commonjs util",
"fs/promises": "commonjs fs/promises",
crypto: "commonjs crypto"
}
: { "node:crypto": "commonjs crypto" },
resolve: {
fallback: isDesktop
? {}
: {
fs: false,
path: false,
crypto: false,
util: false,
url: false
}
},
optimization: {
splitChunks: {
cacheGroups: {
codeLang: {
test: (module) => {
const resource =
module.nameForCondition && module.nameForCondition();
if (!resource) return false;
return (
(resource.includes("/editor/languages/") ||
resource.includes("/html/languages/") ||
resource.includes("/refractor/lang/")) &&
path.basename(resource) !== "index.js"
);
},
name(module) {
const resource =
module.nameForCondition && module.nameForCondition();
if (!resource) return;
const base = path.basename(resource, ".js");
return `code-lang-${base}`;
},
chunks: "all",
enforce: true
}
}
}
}
},
swc: {
jsc: {
experimental: {
plugins: isTesting
? []
: [
[
"@swc/plugin-react-remove-properties",
{
properties: ["^data-test-id$"]
}
]
]
}
}
}
},
plugins: [
pluginReact(),
pluginSvgr({
svgrOptions: {
icon: true,
namedExport: "ReactComponent"
}
}),
regexpAliasPlugin([
{
find: /desktop-bridge\/index.ts/gm,
replacement: isDesktop
? path.resolve(
__dirname,
"src/common/desktop-bridge/index.desktop.ts"
)
: path.resolve(__dirname, "src/common/desktop-bridge/index.ts")
},
{
find: /sqlite\/index.ts/gm,
replacement: isDesktop
? path.resolve(__dirname, "src/common/sqlite/index.desktop.ts")
: path.resolve(__dirname, "src/common/sqlite/index.ts")
}
]),
{
name: "bypass-webpack-require-plugin",
setup(api: RsbuildPluginAPI) {
api.transform(
{
test: /sqlite-kysely\.js$/
},
({ code }) => {
return code.replaceAll(/require/gm, "__non_webpack_require__");
}
);
}
},
{
name: "emit-editor-styles",
setup(api: RsbuildPluginAPI) {
api.transform(
{
test: (filename) => filename.endsWith("css")
},
(context) => {
if (
context.code.includes("KaTeX_Fraktur-Bold-") ||
context.code.includes("Hack typeface")
) {
context.emitFile("assets/editor-styles.css", context.code);
return "";
}
return context.code;
}
);
}
}
]
});
function regexpAliasPlugin(rules: { find: RegExp; replacement: string }[]) {
return {
name: "generic-alias-plugin",
setup(api: RsbuildPluginAPI) {
for (const { find, replacement } of rules) {
api.transform(
{
test: find
},
() => readFileSync(replacement, "utf-8")
);
}
}
};
}

View File

@@ -20,14 +20,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createTRPCProxyClient } from "@trpc/client";
import { ipcLink } from "electron-trpc/renderer";
import type { AppRouter } from "@notesnook/desktop";
import { PATHS } from "@notesnook/desktop";
import { AppEventManager, AppEvents } from "../app-events";
import { TaskScheduler } from "../../utils/task-scheduler";
import { checkForUpdate } from "../../utils/updater";
import { showToast } from "../../utils/toast";
console.log("DESKTOP!");
export const desktop = createTRPCProxyClient<AppRouter>({
links: [ipcLink()]
});
export { PATHS };
attachListeners();
function attachListeners() {

View File

@@ -19,8 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createWriteStream } from "../../utils/stream-saver";
import { type desktop as bridge } from "./index.desktop";
console.log("NOT DESKTOP!");
export const desktop: typeof bridge | undefined = undefined;
export function createWritableStream(filename: string) {
return createWriteStream(filename);
}
export const PATHS: Record<string, string> | undefined = undefined;

View File

@@ -37,10 +37,9 @@ import {
import { showToast } from "../utils/toast";
import { readFile, showFilePicker } from "../utils/file-picker";
import { logger } from "../utils/logger";
import { PATHS } from "@notesnook/desktop";
import { TaskManager } from "./task-manager";
import { EVENTS } from "@notesnook/core";
import { createWritableStream } from "./desktop-bridge";
import { createWritableStream, PATHS } from "./desktop-bridge";
import { FeatureDialog, FeatureKeys } from "../dialogs/feature-dialog";
import { User } from "@notesnook/core";
import { LegacyBackupFile } from "@notesnook/core";
@@ -126,7 +125,10 @@ export async function createBackup(
})}-${new Date().getSeconds()}${mode === "full" ? "-full" : ""}`,
{ replacement: "-" }
);
const directory = Config.get("backupStorageLocation", PATHS.backupsDirectory);
const directory = Config.get(
"backupStorageLocation",
PATHS?.backupsDirectory
);
const ext = "nnbackupz";
const filePath = IS_DESKTOP_APP
? `${directory}/${filename}.${ext}`

View File

@@ -28,7 +28,7 @@ import {
Driver
} from "@streetwriters/kysely";
import { desktop } from "../desktop-bridge";
import Worker from "./sqlite.worker.desktop.ts?worker";
// import Worker from "./sqlite.worker.desktop.ts?worker";
import type { SQLiteWorker } from "./sqlite.worker.desktop";
import { wrap, Remote } from "comlink";
import { Mutex } from "async-mutex";
@@ -37,7 +37,9 @@ import { DialectOptions } from ".";
class SqliteDriver implements Driver {
connection?: DatabaseConnection;
private connectionMutex = new Mutex();
worker: Remote<SQLiteWorker> = wrap<SQLiteWorker>(new Worker());
worker: Remote<SQLiteWorker> = wrap<SQLiteWorker>(
new Worker(new URL("./sqlite.worker.desktop.ts", import.meta.url))
);
constructor(private readonly config: { name: string }) {}
async init(): Promise<void> {

View File

@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SharedWorker from "./shared-service.worker.ts?sharedworker";
// import SharedWorker from "./shared-service.worker.ts?sharedworker";
import { Mutex } from "async-mutex";
const sharedWorker = globalThis.SharedWorker
? new SharedWorker({
? new SharedWorker(new URL("./shared-service.worker.ts", import.meta.url), {
name: "SharedService"
})
: null;

View File

@@ -23,14 +23,17 @@ import type {
QueryResult
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import Worker from "./sqlite.worker.ts?worker";
// import Worker from "./sqlite.worker.ts?worker";
import type { SQLiteWorker } from "./sqlite.worker";
import SQLiteSyncURI from "./wa-sqlite.wasm?url";
import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
// import SQLiteSyncURI from "./wa-sqlite.wasm?url";
// import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
import { Mutex } from "async-mutex";
import { SharedService } from "./shared-service";
import { Remote, wrap } from "comlink";
const SQLiteSyncURI = new URL("./wa-sqlite.wasm", import.meta.url).href;
const SQLiteAsyncURI = new URL("./wa-sqlite-async.wasm", import.meta.url).href;
type Config = {
dbName: string;
async: boolean;
@@ -90,7 +93,9 @@ export class WaSqliteWorkerMultipleTabDriver implements Driver {
console.log("initializing worker");
this.needsInitialization = true;
const worker = new Worker();
const worker = new Worker(
new URL("./sqlite.worker.ts", import.meta.url)
);
worker.addEventListener(
"message",
(event) =>
@@ -235,8 +240,10 @@ export class WaSqliteWorkerSingleTabDriver implements Driver {
constructor(private readonly config: Config) {
console.log("single tab driver", config.dbName);
this.worker = wrap<SQLiteWorker>(
new Worker({ name: config.dbName })
)
new Worker(new URL("./sqlite.worker.ts", import.meta.url), {
name: config.dbName
})
);
}
async init(): Promise<void> {

View File

@@ -25,11 +25,12 @@ import { strings } from "@notesnook/intl";
import { useStore as useSettingStore } from "../../stores/setting-store";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { desktop } from "../../common/desktop-bridge";
import { PATHS } from "@notesnook/desktop";
import { desktop, PATHS } from "../../common/desktop-bridge";
const getDesktopBackupsDirectoryPath = () =>
useSettingStore.getState().backupStorageLocation || PATHS.backupsDirectory;
useSettingStore.getState().backupStorageLocation ||
PATHS?.backupsDirectory ||
"";
export const BackupExportSettings: SettingsGroup[] = [
{

View File

@@ -58,7 +58,6 @@
}
})();
</script>
<script type="module" src="./index.ts"></script>
<style>
html {
overscroll-behavior: none;

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { IFileStorage, File } from "@notesnook/streamable-fs";
import { IndexedDBKVStore } from "./key-value";
import OriginPrivateFileStoreWorker from "./opfs.worker?worker";
// import OriginPrivateFileStoreWorker from "./opfs.worker?worker";
import { OriginPrivateFileStoreWorkerType } from "./opfs.worker";
import { transfer, wrap } from "comlink";
@@ -151,7 +151,7 @@ export class CacheStorageFileStore implements IFileStorage {
export class OriginPrivateFileSystem implements IFileStorage {
private readonly worker = wrap<OriginPrivateFileStoreWorkerType>(
new OriginPrivateFileStoreWorker()
new Worker(new URL("./opfs.worker.ts", import.meta.url))
);
private created = false;
constructor(private readonly name: string) {

View File

@@ -18,10 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { INNCrypto } from "@notesnook/crypto";
import CryptoWorker from "./nncrypto.worker?worker";
// import CryptoWorker from "./nncrypto.worker?worker";
import { wrap } from "comlink";
export const NNCrypto = wrap<INNCrypto>(new CryptoWorker()) as INNCrypto;
export const NNCrypto = wrap<INNCrypto>(
new Worker(new URL("./nncrypto.worker.ts", import.meta.url))
) as INNCrypto;
// TODO: disable until we fix the `pull failed` errors for good.
// IS_DESKTOP_APP && window.NativeNNCrypto
// ? new window.NativeNNCrypto()

View File

@@ -17,9 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DesktopIntegration, PATHS } from "@notesnook/desktop";
import type { DesktopIntegration } from "@notesnook/desktop";
import { db } from "../common/db";
import { desktop } from "../common/desktop-bridge";
import { desktop, PATHS } from "../common/desktop-bridge";
import createStore from "../common/store";
import Config from "../utils/config";
import BaseStore from "./index";
@@ -53,7 +53,7 @@ class SettingStore extends BaseStore<SettingStore> {
fullBackupReminderOffset = Config.get("fullBackupReminderOffset", 0);
backupStorageLocation = Config.get(
"backupStorageLocation",
PATHS.backupsDirectory
PATHS?.backupsDirectory
);
doubleSpacedParagraphs = Config.get("doubleSpacedLines", true);
markdownShortcuts = Config.get("markdownShortcuts", false);

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import NetworkCheckWorker from "./network-check.worker.ts?worker";
// import NetworkCheckWorker from "./network-check.worker.ts?worker";
import type { NetworkCheck as NetworkWorker } from "./network-check.worker";
import { wrap, Remote } from "comlink";
@@ -26,7 +26,9 @@ export class NetworkCheck {
private network!: Remote<NetworkWorker>;
constructor() {
this.worker = new NetworkCheckWorker();
this.worker = new Worker(
new URL("./network-check.worker.ts", import.meta.url)
);
this.network = wrap<NetworkWorker>(this.worker);
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import TaskSchedulerWorker from "./task-scheduler.worker.ts?worker";
// import TaskSchedulerWorker from "./task-scheduler.worker.ts?worker";
import type {
TaskScheduler as TaskSchedulerType,
TaskSchedulerEvent
@@ -83,6 +83,6 @@ export class TaskScheduler {
function init() {
if (worker) return;
worker = new TaskSchedulerWorker();
worker = new Worker(new URL("./task-scheduler.worker.ts", import.meta.url));
if (worker) scheduler = wrap<TaskSchedulerType>(worker);
}

View File

@@ -1,278 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Plugin, PluginOption, ResolvedConfig, defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import svgrPlugin from "vite-plugin-svgr";
import envCompatible from "vite-plugin-env-compatible";
import { VitePWA } from "vite-plugin-pwa";
import autoprefixer from "autoprefixer";
import { WEB_MANIFEST } from "./web-manifest";
import { execSync } from "child_process";
import { version } from "./package.json";
import { visualizer } from "rollup-plugin-visualizer";
import { OutputPlugin } from "rollup";
import path from "path";
const gitHash = (() => {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch (e) {
return process.env.GIT_HASH || "gitless";
}
})();
// const appVersion = version.replaceAll(".", "").replace("-beta", "");
const isBeta = version.includes("-beta");
const isTesting =
process.env.TEST === "true" || process.env.NODE_ENV === "development";
const isDesktop = process.env.PLATFORM === "desktop";
const isThemeBuilder = process.env.THEME_BUILDER === "true";
const isAnalyzing = process.env.ANALYZING === "true";
export default defineConfig({
envPrefix: "NN_",
root: "src/",
publicDir: isThemeBuilder ? path.join(__dirname, "public") : "../public",
build: {
target: isDesktop ? "esnext" : "modules",
outDir: "../build",
minify: "esbuild",
cssMinify: true,
emptyOutDir: true,
sourcemap: !isDesktop,
rollupOptions: {
output: {
plugins: [emitEditorStyles()],
assetFileNames: "assets/[name]-[hash:12][extname]",
chunkFileNames: "assets/[name]-[hash:12].js",
manualChunks: (id: string) => {
if (
(id.includes("/editor/languages/") ||
id.includes("/html/languages/") ||
id.includes("/refractor/lang/")) &&
path.basename(id) !== "index.js"
)
return `code-lang-${path.basename(id, "js")}`;
return null;
}
}
}
},
define: {
APP_TITLE: `"${isThemeBuilder ? "Notesnook Theme Builder" : "Notesnook"}"`,
GIT_HASH: `"${gitHash}"`,
APP_VERSION: `"${version}"`,
PUBLIC_URL: `"${process.env.PUBLIC_URL || ""}"`,
IS_DESKTOP_APP: isDesktop,
PLATFORM: `"${process.env.PLATFORM}"`,
IS_TESTING: process.env.TEST === "true",
IS_BETA: isBeta,
IS_THEME_BUILDER: isThemeBuilder
},
logLevel: process.env.NODE_ENV === "production" ? "warn" : "info",
resolve: {
dedupe: [
"react",
"react-dom",
"@mdi/js",
"@mdi/react",
"@emotion/react",
"katex",
"react-modal",
"dayjs",
"@streetwriters/kysely"
],
alias: [
{
find: /\/desktop-bridge$/gm,
replacement: isDesktop
? "/desktop-bridge/index.desktop"
: "/desktop-bridge/index"
},
{
find: /\/sqlite$/gm,
replacement: isDesktop ? "/sqlite/index.desktop" : "/sqlite/index"
}
]
},
server: {
port: 3000
},
worker: {
format: "es",
rollupOptions: {
output: {
assetFileNames: "assets/[name]-[hash:12][extname]",
chunkFileNames: "assets/[name]-[hash:12].js",
inlineDynamicImports: true
}
}
},
css: {
postcss: {
plugins: [autoprefixer()]
}
},
plugins: [
...(isAnalyzing
? [
visualizer({
gzipSize: true,
brotliSize: true,
open: true
}) as PluginOption
]
: []),
...((isThemeBuilder || isDesktop) && process.env.NODE_ENV === "production"
? []
: [
VitePWA({
strategies: "injectManifest",
minify: true,
manifest: WEB_MANIFEST,
injectRegister: null,
srcDir: "",
filename: "service-worker.ts",
mode: "production",
workbox: { mode: "production" },
injectManifest: {
globPatterns: ["**/*.{js,css,html,wasm}", "**/Inter-*.woff2"],
globIgnores: [
"**/node_modules/**/*",
"**/code-lang-*.js",
"pdf.worker.min.js"
]
}
})
]),
react({
plugins: isTesting
? undefined
: [
[
"@swc/plugin-react-remove-properties",
{
properties: ["^data-test-id$"]
}
]
]
}),
envCompatible({
prefix: "NN_",
mountedPath: "process.env"
}),
svgrPlugin({
svgrOptions: {
icon: true,
namedExport: "ReactComponent"
// ...svgr options (https://react-svgr.com/docs/options/)
}
}),
...(isDesktop
? []
: [
prefetchPlugin({
excludeFn: (assetName) =>
assetName.includes("wa-sqlite-async") ||
!assetName.includes("wa-sqlite")
})
])
]
});
function emitEditorStyles(): OutputPlugin {
return {
name: "rollup-plugin-emit-editor-styles",
generateBundle(options, bundle) {
for (const file in bundle) {
const chunk = bundle[file];
if (
chunk.type === "asset" &&
chunk.fileName.endsWith(".css") &&
typeof chunk.source === "string" &&
(chunk.source.includes("KaTeX_Fraktur-Bold-") ||
chunk.source.includes("Hack typeface"))
) {
this.emitFile({
type: "asset",
fileName: "assets/editor-styles.css",
name: "editor-styles.css",
source: chunk.source
});
}
}
}
};
}
function prefetchPlugin(options?: {
excludeFn?: (assetName: string) => boolean;
}): Plugin {
let config: ResolvedConfig;
return {
name: "vite-plugin-bundle-prefetch",
apply: "build",
configResolved(resolvedConfig: ResolvedConfig) {
// store the resolved config
config = resolvedConfig;
},
transformIndexHtml(
html: string,
ctx: {
path: string;
filename: string;
bundle?: import("rollup").OutputBundle;
chunk?: import("rollup").OutputChunk;
}
) {
const bundles = Object.keys(ctx.bundle ?? {});
const isLegacy = bundles.some((bundle) => bundle.includes("legacy"));
if (isLegacy) {
//legacy build won't add prefetch
return html;
}
// remove map files
let modernBundles = bundles.filter(
(bundle) => bundle.endsWith(".map") === false
);
const excludeFn = options?.excludeFn;
if (excludeFn) {
modernBundles = modernBundles.filter((bundle) => !excludeFn(bundle));
}
// Remove existing files and concatenate them into link tags
const prefechBundlesString = modernBundles
.filter((bundle) => html.includes(bundle) === false)
.map((bundle) => `<link rel="prefetch" href="${config.base}${bundle}">`)
.join("\n");
// Use regular expression to get the content within <head> </head>
const headContent = html.match(/<head>([\s\S]*)<\/head>/)?.[1] ?? "";
// Insert the content of prefetch into the head
const newHeadContent = `${headContent}${prefechBundlesString}`;
// Replace the original head
html = html.replace(
/<head>([\s\S]*)<\/head>/,
`<head>${newHeadContent}</head>`
);
return html;
}
};
}