9 Commits
Author SHA1 Message Date
Yoshihiro OKUMURA 0433f49ddd style: restate the theme's sizes for border-box
modern-normalize makes every element border-box, so the rules that
set an explicit width or height and then added padding and a border
on top came out short: headings lost the space under them, the
horizontal rules went invisible and the item list icons narrowed.
Restate each as the total it used to render as.

Two rules are left alone on purpose: `.dbtools .index section` and
`.databases` both say `width: 50%`, and widening them to
`calc(50% + 20px)` drops the second float onto its own line.

Also stop centring the h3 bullet on the padding box, which the bottom
padding makes taller than the text -- it sat ~4.5px low both here and
on the deployed site. Pin it to the middle of the first line instead.
2026-09-11 21:24:41 +09:00
Yoshihiro OKUMURA c850376571 style: keep the browser's own font on form controls
modern-normalize hands form controls the inherited page font and puts
a system stack on `html`. The XOOPS theme styles `body` and leaves
the rest to the defaults it was authored against, so the search box
rendered in the theme font at the theme size rather than as the plain
controls it was drawn for.

State those defaults explicitly instead, so the rendering no longer
depends on whichever browser is in use.
2026-09-11 21:24:23 +09:00
Yoshihiro OKUMURA 8c1a6e7d2b fix: make the browser revalidate the archive data
sirv sent no Cache-Control for static-contents, only an ETag, so the
browser was free to apply heuristic caching and answer from a stale
copy without revalidating. A re-dump would then go unnoticed, and two
sites served from the same localhost port answered with each other's
JSON.

maxAge 0 sends `public, max-age=0, must-revalidate`; a matching ETag
still yields a 304, so nothing extra is transferred.
2026-09-11 21:24:14 +09:00
Yoshihiro OKUMURA a6bb5bb672 docs: write down the deployment procedure
The section covered the document root symlinks but not the steps
around them, and still described the build as excluding public/
symlinks — which went away with the switch to sirv.

- spell out the five steps: build, send the data, push the build, link
  the data in, check the result
- push the build before creating the symlinks, so that --delete needs
  no --exclude and each deploy recreates the links from scratch
- link the data in with a glob over static-contents/, so modules/ and
  uploads/ cannot drift apart
- use the real server paths instead of /var/www/bsi-ni
- drop the typecheck script, which only repeated what build already
  does, matching visiome.neuroinf.jp
2026-09-11 19:18:52 +09:00
Yoshihiro OKUMURA 792dbac92f style: switch to modern-normalize
normalize.css has had no release since 2018. modern-normalize is
maintained and is what visiome.neuroinf.jp already uses.

- load it from main.tsx before App, so the reset now applies before
  the theme instead of after it
- drop src/index.css, which held nothing but the old @import
- use the current favicon link form, matching visiome
2026-09-11 19:18:39 +09:00
Yoshihiro OKUMURA 4ee6597410 docs: drop the public/ symlink setup step
The dev and preview servers mount static-contents/ themselves since
the switch to sirv, so the symlinks the setup section asked for no
longer exist.
2026-09-11 18:28:55 +09:00
Yoshihiro OKUMURA 28424287fd build: split dependencies into vendor chunks
- split node_modules into vendor-react (react, react-dom, scheduler)
  and vendor-misc, so that react does not keep a single dependency
  chunk close to the 500 kB warning threshold; react also changes far
  less often than the rest, so the two now cache independently
- switch from the deprecated rollupOptions to rolldownOptions, and
  from manualChunks to the native output.codeSplitting: under vite 8
  manualChunks is only a compatibility shim and does not group the
  react packages as asked
- matches visiome.neuroinf.jp
2026-09-11 18:04:12 +09:00
Yoshihiro OKUMURA 65c543bad1 build: mount static-contents with sirv instead of symlinks
public/ held symlinks to the archive storage so that the dev server
would serve it, which forced a custom plugin to walk public/ and skip
them at build time, and still left `vite preview` returning index.html
for /modules and /uploads. Mount the directory as middleware in both
servers instead.

- serve ../static-contents with sirv from configureServer and
  configurePreviewServer, matching visiome.neuroinf.jp
- drop the public/modules and public/uploads symlinks
- drop the copy-public-files plugin and copyPublicDir: false, so the
  standard public directory copy applies again
- drop the symlinks from .gitignore and the biome includes
2026-09-11 17:51:04 +09:00
Yoshihiro OKUMURA 710b8885e3 build: track @types/node patches within the 24.x line
@types/node has no ncu policy here, so a run would move it off the
24.x line that matches the Node 24 runtime. Its "latest" dist-tag
points at the 22.x line, so the default target reports no updates at
all either. Select the target per package instead.

- add .ncurc.js selecting the "minor" target for @types/node
- pick up @types/node 24.13.4
2026-09-11 17:36:28 +09:00
13 changed files with 266 additions and 82 deletions
-4
View File
@@ -23,7 +23,3 @@ coverage
*.njsproj
*.sln
*.sw?
# Converted XOOPS data, symlinked in from the archive storage
/public/modules
/public/uploads
+6
View File
@@ -0,0 +1,6 @@
// Keep @types/node on the 24.x line to match the deployment target. Its
// "latest" dist-tag points at the 22.x line, so the default target would
// report no updates at all instead of offering 24.x patches.
export default {
target: (name) => (name === '@types/node' ? 'minor' : 'latest'),
};
+78 -20
View File
@@ -14,14 +14,7 @@ npm install
```
The converted XOOPS data (`modules/`, `uploads/`) is not tracked in git and is not part of the
build. Symlink it into `public/` so the dev server can serve it:
```sh
ln -s ../../static-contents/modules public/modules
ln -s ../../static-contents/uploads public/uploads
```
That assumes the repository is checked out next to `static-contents/`:
build. It lives in `static-contents/`, which sits next to the repository rather than inside it:
```
bsi-ni.brain.riken.jp/
@@ -29,6 +22,10 @@ bsi-ni.brain.riken.jp/
└── static-contents/ modules/ and uploads/
```
The dev and preview servers mount that directory at the site root, so nothing needs to be
linked into `public/`. It is deliberately kept out of `dist/`: it runs to gigabytes, and the
web server maps it in directly.
That data is produced by [xoops-static-exporter](https://git.ni.riken.jp/niu/xoops-static-exporter)
(`sites/bsi-ni.brain.riken.jp/`).
@@ -46,10 +43,6 @@ Type-checks the project (`tsc -b`) and builds the production bundle into `dist/`
Serves the contents of `dist/` locally to verify a production build.
### `npm run typecheck`
Runs the TypeScript project references build without emitting output.
### `npm run lint` / `npm run format` / `npm run check`
Runs [Biome](https://biomejs.dev/) (`biome.json`): `lint` reports problems, `format` rewrites
@@ -57,27 +50,92 @@ formatting, and `check` applies both plus import sorting.
## Deployment
`npm run build` writes a static bundle to `dist/` (a few MB). The archive data is deliberately left
out of it: `build.copyPublicDir` is off and only the real files in `public/` are emitted, so the
symlinked `modules/` and `uploads/` are skipped.
Build locally and copy the result up; the server does not need a toolchain.
On the server, place `dist/` at the document root and symlink the archive data into it:
### 1. Build and check
```sh
ln -s /path/to/archive/modules /var/www/bsi-ni/modules
ln -s /path/to/archive/uploads /var/www/bsi-ni/uploads
npm ci
npm run check # biome, writes fixes
npm run build # runs tsc -b, then vite build
npm run preview # localhost:4173 , serves dist/ + static-contents/
```
Apache needs `Options FollowSymLinks` and `Require all granted` on the archive directory.
`dist/` is a few MB. `static-contents/` is never part of it — the dev and preview servers mount it,
and in production the web server maps it in.
### 2. Send up the archive data
`modules/` and `uploads/`, about 1.1 GB. The server mirrors the local layout: `static-contents/` sits
next to the document root. Run this from the directory that holds both the repository and
`static-contents/`:
```
/data/www/bsi-ni.brain.riken.jp/
├── html/ document root
└── static-contents/
```
```sh
rsync -a --delete static-contents/ \
user@server:/data/www/bsi-ni.brain.riken.jp/static-contents/
```
### 3. Push the build
```sh
rsync -a --delete dist/ user@server:/data/www/bsi-ni.brain.riken.jp/html/
```
`--delete` clears out the previous build, and with it the data symlinks from step 4 — that is
deliberate, they are recreated right after. Anything left over from an older deployment goes too.
### 4. Link the archive data into the document root
```sh
cd /data/www/bsi-ni.brain.riken.jp/html
ln -s ../static-contents/* .
```
That links every top-level entry of `static-contents/` — currently `modules/` and `uploads/`:
```
/data/www/bsi-ni.brain.riken.jp/html/
├── index.html from dist/
├── assets/ from dist/
├── .htaccess from dist/
├── modules -> ../static-contents/modules
└── uploads -> ../static-contents/uploads
```
The glob keeps the set in step and picks up anything added to `static-contents/` later. Note that
it skips dotfiles, of which there are none today. The links are relative, so the whole tree can be
moved without rewriting them.
Run steps 3 and 4 back to back. In between, `/modules/...` and `/uploads/...` are 404 because
step 3 removed the links; that is why plain `ln -s` works here without `-f`, and why a stale or
wrong link repairs itself on the next deploy.
Apache needs `Options FollowSymLinks`, `AllowOverride All` and `Require all granted` on the
document root, and `Options FollowSymLinks` with `Require all granted` on `static-contents/`.
Use symlinks rather than `Alias`. An `Alias /modules ...` maps the URL outside the document root,
so `public/.htaccess` no longer applies to it and every legacy `/modules/<module>/*.php` URL stops
so `.htaccess` no longer applies to it and every legacy `/modules/<module>/*.php` URL stops
reaching the SPA redirect handler. With symlinks the existing precedence still holds: real files
are served directly, everything else falls through to `index.html`.
Because the app uses HTML5 history routing, the web server must fall back to `index.html` for
unknown paths — see `public/.htaccess`.
### 5. Check the deployed site
```sh
curl -I https://bsi-ni.brain.riken.jp/modules/xoonips/tree.json # 200
curl -I https://bsi-ni.brain.riken.jp/database/file/1.png # 301
```
Then load `/`, `/dbtools`, `/news` and an item page in a browser and confirm the images resolve.
## Learn More
- [Vite documentation](https://vite.dev/guide/)
+1 -1
View File
@@ -6,7 +6,7 @@
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!**/dist", "!public/modules", "!public/uploads", "!src/common/assets"]
"includes": ["**", "!**/dist", "!src/common/assets"]
},
"formatter": {
"enabled": true,
+2 -2
View File
@@ -1,8 +1,8 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
+60 -11
View File
@@ -13,7 +13,7 @@
"async-lock": "^1.4.1",
"ky": "^2.1.0",
"lokijs": "^1.5.12",
"normalize.css": "^8.0.1",
"modern-normalize": "^3.0.1",
"react": "^19.3.0",
"react-cookie": "^8.1.2",
"react-dom": "^19.3.0",
@@ -26,10 +26,11 @@
"@biomejs/biome": "^2.5.13",
"@types/async-lock": "^1.4.2",
"@types/lokijs": "^1.5.14",
"@types/node": "^24.13.3",
"@types/node": "^24.13.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"sirv": "^3.0.2",
"typescript": "~7.0.2",
"vite": "^8.3.0"
}
@@ -249,6 +250,13 @@
"url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -578,9 +586,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"version": "24.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz",
"integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1509,6 +1517,28 @@
"loose-envify": "cli.js"
}
},
"node_modules/modern-normalize": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-3.0.1.tgz",
"integrity": "sha512-VqlMdYi59Uch6fnUPxnpijWUQe+TW6zeWCvyr6Mb7JibheHzSuAAoJi2c71ZwIaWKpECpGpYHoaaBp6rBRr+/g==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
@@ -1528,12 +1558,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize.css": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==",
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1737,6 +1761,21 @@
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
"license": "MIT"
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1764,6 +1803,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+4 -4
View File
@@ -9,8 +9,7 @@
"preview": "vite preview",
"lint": "biome lint",
"format": "biome format --write",
"check": "biome check --write",
"typecheck": "tsc -b --noEmit"
"check": "biome check --write"
},
"dependencies": {
"@dr.pogodin/react-helmet": "^3.2.3",
@@ -18,7 +17,7 @@
"async-lock": "^1.4.1",
"ky": "^2.1.0",
"lokijs": "^1.5.12",
"normalize.css": "^8.0.1",
"modern-normalize": "^3.0.1",
"react": "^19.3.0",
"react-cookie": "^8.1.2",
"react-dom": "^19.3.0",
@@ -31,10 +30,11 @@
"@biomejs/biome": "^2.5.13",
"@types/async-lock": "^1.4.2",
"@types/lokijs": "^1.5.14",
"@types/node": "^24.13.3",
"@types/node": "^24.13.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"sirv": "^3.0.2",
"typescript": "~7.0.2",
"vite": "^8.3.0"
}
+20 -7
View File
@@ -82,7 +82,8 @@ hr {
border-width: 1px 0px 0px 0px;
border-style: solid;
border-color: #cccccc;
height: 1px;
/* border-box: 1 plus the 1px top border */
height: 2px;
}
ul,
@@ -199,7 +200,8 @@ tr.even:hover {
}
#main {
width: 902px;
/* border-box: 902 plus the 1px border on each side */
width: 904px;
margin: 10px auto;
line-height: 1.5;
border: 1px solid #c2c7c7;
@@ -325,7 +327,8 @@ tr.even:hover {
font-size: 123.1%;
font-weight: bold;
line-height: 37px;
height: 37px;
/* border-box: 37 plus the 1px border on each side */
height: 39px;
}
#main h2::after {
@@ -341,12 +344,16 @@ tr.even:hover {
#main h3 {
margin-left: 5px;
background: url(images/h3-bullet.png) no-repeat left center;
/* `center` centres the 11px bullet on the whole padding box, which the
bottom padding makes taller than the text -- it sat ~4.5px low. Pin it
to the middle of the first 20px line instead: (20 - 11) / 2. */
background: url(images/h3-bullet.png) no-repeat left 4.5px;
text-indent: 16px;
line-height: 20px;
padding: 0 0 5px 0;
font-size: 108%;
height: 24px;
/* border-box: 24 plus 5px of bottom padding and the 3px border */
height: 32px;
border-bottom: 3px double #cccccc;
}
@@ -363,11 +370,16 @@ tr.even:hover {
font-weight: 123.1%;
line-height: 24px;
border-bottom: none;
/* border-box: 24 plus 5px of bottom padding; this variant drops the border */
height: 29px;
/* this variant's line is 24px tall: (24 - 11) / 2 */
background-position: left 6.5px;
}
#main .whatsnew .news {
float: left;
border-left: 1px dotted #cccccc;
width: calc(100% - 150px);
/* border-box: the original width plus its 1px left border */
width: calc(100% - 149px);
}
#main .whatsnew .title .more {
margin: 0.3em 20px;
@@ -387,7 +399,8 @@ tr.even:hover {
#main .whatsnew .news ul li {
list-style: none;
white-space: nowrap;
height: 35px;
/* border-box: 35 plus the 1px top border */
height: 36px;
line-height: 35px;
border-top: 1px dotted #cccccc;
font-size: 93%;
+2 -1
View File
@@ -16,7 +16,8 @@
.database :global(.listTable .listExtra) {
vertical-align: middle;
text-align: center;
width: 65px;
/* border-box: 65 plus the cell's 1px of padding on each side */
width: 67px;
line-height: 0;
}
-1
View File
@@ -1 +0,0 @@
@import "normalize.css";
+2 -1
View File
@@ -1,9 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'modern-normalize/modern-normalize.css';
import './normalize-overrides.css';
import App from './App';
import IndexUtil from './database/lib/IndexUtil';
import ItemUtil from './database/lib/ItemUtil';
import './index.css';
import { loadSiteData, setSiteData } from './siteData';
const container = document.getElementById('root');
+44
View File
@@ -0,0 +1,44 @@
/*
* modern-normalize changes three things the XOOPS theme was not written for.
* The theme styles `body` and leaves the rest to the defaults it was authored
* against, so restate those defaults as explicit values here rather than
* leaving the rendering to depend on whichever browser is in use.
*/
/* The theme's widths and paddings are content-box measurements; modern-normalize
makes every element border-box. */
*,
::before,
::after {
box-sizing: content-box;
}
/* Controls and tables are laid out as border-box, which is what the theme
assumes when it sizes them. */
button,
select,
table,
input[type="button"],
input[type="reset"],
input[type="submit"] {
box-sizing: border-box;
}
/* modern-normalize puts a system font stack on `html`. Nothing visible
inherits it -- `body` below covers the whole document -- but state the
theme's own stack so there is a single font declaration for the page. */
html {
font-family: arial, sans-serif;
}
/* modern-normalize makes form controls inherit the page font. The theme sizes
them as the unstyled controls they were: Arial at the control default. */
button,
input,
optgroup,
select,
textarea {
font-family: Arial, sans-serif;
font-size: 13.3333px;
line-height: normal;
}
+47 -30
View File
@@ -1,43 +1,60 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import fs from 'node:fs';
import path from 'node:path';
import react from '@vitejs/plugin-react';
import { defineConfig, type Plugin } from 'vite';
import sirv from 'sirv';
import type { Connect, Plugin, ResolvedConfig } from 'vite';
import { defineConfig } from 'vite';
// public/ also holds symlinks to the archive storage (modules/, uploads/). The
// dev server serves them, but the build must not copy gigabytes of frozen data
// that the web server maps in directly. readdir reports a symlink as neither a
// file nor a directory, so walking public/ emits the real files and skips them.
const copyPublicFiles = (dir: string): Plugin => ({
name: 'copy-public-files',
generateBundle() {
const walk = (from: string) => {
for (const entry of readdirSync(from, { withFileTypes: true })) {
const path = join(from, entry.name);
if (entry.isFile()) {
this.emitFile({ type: 'asset', fileName: relative(dir, path), source: readFileSync(path) });
} else if (entry.isDirectory()) {
walk(path);
}
}
};
walk(dir);
},
});
/**
* `static-contents/` holds the archived site data (`modules/`, `uploads/`).
* It sits next to the repository and is served from the site root next to
* `public/`, but it is deliberately kept out of the bundle: it runs to
* gigabytes of frozen data that the web server maps in directly. The dev and
* preview servers mount it so that both behave like production.
*/
const staticContents = (): Plugin => {
let dir: string;
const mount = (middlewares: { use: (fn: Connect.NextHandleFunction) => void }, dev: boolean) => {
if (fs.existsSync(dir)) {
// maxAge 0 makes sirv send `Cache-Control: public, max-age=0`, so the
// browser revalidates against the ETag instead of falling back to
// heuristic caching. Without it a re-dump, or another site served
// from the same localhost port, is answered from a stale cache.
middlewares.use(sirv(dir, { dev, etag: true, maxAge: 0 }));
}
};
return {
name: 'bsi-ni:static-contents',
configResolved(config: ResolvedConfig) {
dir = path.resolve(config.root, '../static-contents');
},
configureServer(server) {
mount(server.middlewares, true);
},
configurePreviewServer(server) {
mount(server.middlewares, false);
},
};
};
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), copyPublicFiles('public')],
plugins: [react(), staticContents()],
optimizeDeps: { exclude: ['fs'] },
build: {
copyPublicDir: false,
sourcemap: true,
rollupOptions: {
rolldownOptions: {
external: ['fs'],
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
return 'vendor';
}
// react and react-dom are over 200 kB together, which would
// push a single dependency chunk close to the 500 kB warning
// threshold. They also change far less often than the rest,
// so split them off and let the two cache independently.
codeSplitting: {
groups: [
{ name: 'vendor-react', test: /node_modules\/(react|react-dom|scheduler)\// },
{ name: 'vendor-misc', test: /node_modules\// },
],
},
},
},