Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0433f49ddd
|
||
|
|
c850376571
|
||
|
|
8c1a6e7d2b
|
||
|
|
a6bb5bb672
|
||
|
|
792dbac92f
|
||
|
|
4ee6597410
|
||
|
|
28424287fd
|
||
|
|
65c543bad1
|
||
|
|
710b8885e3
|
@@ -23,7 +23,3 @@ coverage
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Converted XOOPS data, symlinked in from the archive storage
|
||||
/public/modules
|
||||
/public/uploads
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
+60
-11
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
@import "normalize.css";
|
||||
+2
-1
@@ -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');
|
||||
|
||||
@@ -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
@@ -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\// },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user