Skip to content

Commit 53e5ce6

Browse files
committed
feat(vite-plugin-warpdrive): new Vite plugin for uploading large assets to S3 compatible storages
1 parent 35bf4ec commit 53e5ce6

File tree

16 files changed

+908
-83
lines changed

16 files changed

+908
-83
lines changed

apps/stage-tamagotchi/electron.vite.config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk'
1515
import { templateCompilerOptions } from '@tresjs/core'
1616
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
1717

18+
const stageUIAssetsRoot = resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'assets'))
19+
const sharedCacheDir = resolve(join(import.meta.dirname, '..', '..', '.cache'))
20+
1821
export default defineConfig({
1922
main: {
2023
plugins: [externalizeDepsPlugin()],
@@ -130,10 +133,10 @@ export default defineConfig({
130133
}),
131134

132135
DownloadLive2DSDK(),
133-
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'),
134-
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'assets/live2d/models'),
135-
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'assets/vrm/models/AvatarSample-A'),
136-
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'assets/vrm/models/AvatarSample-B'),
136+
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'live2d/models', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
137+
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'live2d/models', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
138+
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'vrm/models/AvatarSample-A', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
139+
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'vrm/models/AvatarSample-B', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
137140
],
138141
},
139142
})

apps/stage-tamagotchi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
"@proj-airi/lobe-icons": "^1.0.14",
136136
"@proj-airi/stage-shared": "workspace:^",
137137
"@proj-airi/ui-transitions": "workspace:^",
138-
"@proj-airi/unplugin-fetch": "^0.1.7",
138+
"@proj-airi/unplugin-fetch": "^0.2.0",
139139
"@proj-airi/unplugin-live2d-sdk": "^0.1.6",
140140
"@shikijs/markdown-it": "^3.15.0",
141141
"@types/culori": "^4.0.1",

apps/stage-web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@
105105
"@iconify-json/vscode-icons": "^1.2.34",
106106
"@intlify/unplugin-vue-i18n": "^11.0.1",
107107
"@proj-airi/lobe-icons": "^1.0.14",
108-
"@proj-airi/unplugin-fetch": "^0.1.7",
108+
"@proj-airi/unplugin-fetch": "^0.2.0",
109109
"@proj-airi/unplugin-live2d-sdk": "^0.1.6",
110+
"@proj-airi/vite-plugin-warpdrive": "workspace:*",
110111
"@shikijs/markdown-it": "^3.15.0",
111112
"@types/audioworklet": "^0.0.91",
112113
"@types/culori": "^4.0.1",

apps/stage-web/vite.config.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ import Layouts from 'vite-plugin-vue-layouts'
1313

1414
import { Download } from '@proj-airi/unplugin-fetch/vite'
1515
import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk/vite'
16+
import { createS3Provider, WarpDrivePlugin } from '@proj-airi/vite-plugin-warpdrive'
1617
import { templateCompilerOptions } from '@tresjs/core'
1718
import { LFS, SpaceCard } from 'hfup/vite'
1819
import { defineConfig } from 'vite'
1920
import { VitePWA } from 'vite-plugin-pwa'
2021

22+
const stageUIAssetsRoot = resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'assets'))
23+
const sharedCacheDir = resolve(join(import.meta.dirname, '..', '..', '.cache'))
24+
2125
export default defineConfig({
2226
optimizeDeps: {
2327
exclude: [
@@ -63,6 +67,7 @@ export default defineConfig({
6367
],
6468
},
6569
},
70+
6671
plugins: [
6772
Info(),
6873

@@ -153,10 +158,10 @@ export default defineConfig({
153158
VueDevTools(),
154159

155160
DownloadLive2DSDK(),
156-
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'),
157-
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'assets/live2d/models'),
158-
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'assets/vrm/models/AvatarSample-A'),
159-
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'assets/vrm/models/AvatarSample-B'),
161+
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'live2d/models', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
162+
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'live2d/models', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
163+
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'vrm/models/AvatarSample-A', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
164+
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'vrm/models/AvatarSample-B', { parentDir: stageUIAssetsRoot, cacheDir: sharedCacheDir }),
160165

161166
// HuggingFace Spaces
162167
LFS({ root: cwd(), extraGlobs: ['*.vrm', '*.vrma', '*.hdr', '*.cmo3', '*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.bmp', '*.ttf'] }),
@@ -175,5 +180,47 @@ export default defineConfig({
175180
],
176181
short_description: 'AI driven VTuber & Companion, supports Live2D and VRM.',
177182
}),
183+
184+
// For the following example assets:
185+
//
186+
// dist/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm 21,596.01 kB │ gzip: 5,121.95 kB
187+
// dist/assets/XiaolaiSC-Regular-SNWuh554.ttf 22,183.94 kB
188+
// dist/assets/cjkFonts_allseto_v1.11-ByBdljxl.ttf 31,337.14 kB
189+
// dist/assets/duckdb-coi-CSr8FQO4.wasm 32,320.49 kB │ gzip: 7,194.65 kB
190+
// dist/assets/duckdb-eh-BJOC5S4x.wasm 32,604.02 kB │ gzip: 7,133.37 kB
191+
// dist/assets/duckdb-mvp-8HYqhb4i.wasm 37,345.64 kB │ gzip: 8,099.69 kB
192+
//
193+
// they are too large to be able to put into deployments like Cloudflare Workers or Pages,
194+
// we need to upload them to external storage and use renderBuiltUrl to rewrite their URLs.
195+
...((!env.S3_ENDPOINT || !env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY)
196+
? []
197+
: [
198+
WarpDrivePlugin({
199+
prefix: 'proj-airi/stage-web/',
200+
include: [/\.wasm$/i, /\.ttf$/i, /\.vrm$/i, /\.zip$/i], // in existing assets, wasm, ttf, vrm files are the largest ones
201+
manifest: true,
202+
contentTypeBy: (filename: string) => {
203+
if (filename.endsWith('.wasm')) {
204+
return 'application/wasm'
205+
}
206+
if (filename.endsWith('.ttf')) {
207+
return 'font/ttf'
208+
}
209+
if (filename.endsWith('.vrm')) {
210+
return 'application/octet-stream'
211+
}
212+
if (filename.endsWith('.zip')) {
213+
return 'application/zip'
214+
}
215+
},
216+
provider: createS3Provider({
217+
endpoint: env.S3_ENDPOINT,
218+
accessKeyId: env.S3_ACCESS_KEY_ID,
219+
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
220+
region: env.S3_REGION,
221+
publicBaseUrl: env.WARP_DRIVE_PUBLIC_BASE ?? env.S3_ENDPOINT,
222+
}),
223+
}),
224+
]),
178225
],
179226
})

apps/stage-web/wrangler.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ name = "airi"
22
compatibility_date = "2025-04-01"
33
preview_urls = true
44

5-
[build]
6-
command = "pnpm -F @proj-airi/stage-web run build && pnpm -F @proj-airi/docs run build:base && mv ./docs/.vitepress/dist ./apps/stage-web/dist/docs && cp ./apps/stage-web/dist/docs/sitemap.xml ./apps/stage-web/dist/sitemap.xml && pnpm -F @proj-airi/stage-ui run story:build && mv ./packages/stage-ui/.histoire/dist ./apps/stage-web/dist/ui"
7-
85
[assets]
96
directory = "./dist"
107
not_found_handling = "single-page-application"

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ words:
289289
- vtuber
290290
- vueuse
291291
- waapi
292+
- warpdrive
292293
- Warudo
293294
- wavefile
294295
- webgpu

packages/stage-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
"@iconify-json/vscode-icons": "^1.2.34",
157157
"@moeru/std": "catalog:",
158158
"@proj-airi/lobe-icons": "^1.0.14",
159+
"@proj-airi/vite-plugin-warpdrive": "workspace:*",
159160
"@types/audioworklet": "^0.0.91",
160161
"@types/culori": "^4.0.1",
161162
"@types/three": "^0.181.0",

packages/stage-ui/vite.config.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { Plugin } from 'vite'
22

33
import { join, resolve } from 'node:path'
4+
import { env } from 'node:process'
45

56
import Vue from '@vitejs/plugin-vue'
67
import Unocss from 'unocss/vite'
78
import Yaml from 'unplugin-yaml/vite'
89
import Inspect from 'vite-plugin-inspect'
910

11+
import { createS3Provider, WarpDrivePlugin } from '@proj-airi/vite-plugin-warpdrive'
1012
import { defineConfig } from 'vite'
1113

1214
// For Histoire
@@ -47,5 +49,44 @@ export default defineConfig({
4749
// TODO: Type wrong for `unplugin-yaml` in Histoire required
4850
// Vite version, wait until Histoire updates to support Vite 7
4951
Inspect() as Plugin,
52+
53+
// For the following example assets:
54+
//
55+
// dist/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm 21,596.01 kB │ gzip: 5,121.95 kB
56+
// dist/assets/XiaolaiSC-Regular-SNWuh554.ttf 22,183.94 kB
57+
// dist/assets/cjkFonts_allseto_v1.11-ByBdljxl.ttf 31,337.14 kB
58+
//
59+
// they are too large to be able to put into deployments like Cloudflare Workers or Pages,
60+
// we need to upload them to external storage and use renderBuiltUrl to rewrite their URLs.
61+
...((!env.S3_ENDPOINT || !env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY)
62+
? []
63+
: [
64+
WarpDrivePlugin({
65+
prefix: 'proj-airi/stage-ui/',
66+
include: [/\.wasm$/i, /\.ttf$/i, /\.vrm$/i, /\.zip$/i], // in existing assets, wasm, ttf, vrm files are the largest ones
67+
manifest: true,
68+
contentTypeBy: (filename: string) => {
69+
if (filename.endsWith('.wasm')) {
70+
return 'application/wasm'
71+
}
72+
if (filename.endsWith('.ttf')) {
73+
return 'font/ttf'
74+
}
75+
if (filename.endsWith('.vrm')) {
76+
return 'application/octet-stream'
77+
}
78+
if (filename.endsWith('.zip')) {
79+
return 'application/zip'
80+
}
81+
},
82+
provider: createS3Provider({
83+
endpoint: env.S3_ENDPOINT,
84+
accessKeyId: env.S3_ACCESS_KEY_ID,
85+
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
86+
region: env.S3_REGION,
87+
publicBaseUrl: env.WARP_DRIVE_PUBLIC_BASE ?? env.S3_ENDPOINT,
88+
}),
89+
}),
90+
]),
5091
],
5192
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# @proj-airi/vite-plugin-warpdrive
2+
3+
Vite plugin that rewrites selected build assets (large WASM/TTF/VRM, etc.) to remote object storage and uploads them after build. It uses Vite's `renderBuiltUrl` hook so the generated bundles reference the remote URL while keeping the local file for upload.
4+
5+
## Why
6+
7+
- Keep HTML/JS bundles lean while serving heavy assets (WASM, fonts, models) from object storage/CDN.
8+
- Simple provider abstraction; ships with an S3-compatible implementation via [`s3mini`](https://github.com/good-lly/s3mini).
9+
- Emits an optional manifest (`remote-assets.manifest.json`) that maps built filenames to remote URLs plus hostId/hostType for debugging.
10+
11+
## Install
12+
13+
```bash
14+
pnpm add -D @proj-airi/vite-plugin-warpdrive
15+
```
16+
17+
## Usage
18+
19+
```ts
20+
import { createS3Provider, WarpDrivePlugin } from '@proj-airi/vite-plugin-warpdrive'
21+
// vite.config.ts
22+
import { defineConfig } from 'vite'
23+
24+
export default defineConfig({
25+
plugins: [
26+
WarpDrivePlugin({
27+
prefix: 'remote-assets', // optional path prefix in the bucket
28+
include: [/\.wasm$/i, /\.ttf$/i, /\.vrm$/i], // which assets to rewrite/upload
29+
// includeBy: (file, ctx) => ctx.hostId?.includes('duckdb'),
30+
// contentType: (file) => file.endsWith('.wasm') ? 'application/wasm' : undefined,
31+
manifest: true, // emit remote-assets.manifest.json in dist
32+
provider: createS3Provider({
33+
endpoint: process.env.S3_ENDPOINT!,
34+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
35+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
36+
region: process.env.S3_REGION,
37+
publicBaseUrl: process.env.WARP_DRIVE_PUBLIC_BASE, // defaults to endpoint
38+
}),
39+
}),
40+
],
41+
})
42+
```
43+
44+
### Options
45+
46+
- `prefix`: string path prefix for uploaded keys and URLs (e.g. `remote-assets` -> `remote-assets/assets/foo.wasm`).
47+
- `include`: array of regex or predicate functions to decide which assets to rewrite/upload.
48+
- `includeBy`: optional `(filename, ctx) => boolean` for finer control (ctx has `hostId`, `hostType`).
49+
- `manifest`: when true, emits `remote-assets.manifest.json` describing fileName/key/url/hostId/hostType/size.
50+
- `contentType`: optional `(filename) => string | undefined` resolver passed to the provider upload.
51+
- `logger`: optional logger ({ info, warn, error }) for custom logging sinks.
52+
- `provider`: any object implementing `{ getPublicUrl(key): string; upload(localPath, key, contentType?): Promise<void> }`.
53+
54+
### createS3Provider
55+
56+
Light wrapper around `s3mini`. Required fields:
57+
58+
- `endpoint`: full bucket URL (e.g. `https://s3.example.com/my-bucket`).
59+
- `accessKeyId`, `secretAccessKey`: credentials.
60+
- Optional: `region`, `requestSizeInBytes`, `requestAbortTimeout`, `publicBaseUrl` (override public URL base).
61+
62+
## How it works
63+
64+
1. `renderBuiltUrl` returns the remote URL for matching assets while remembering the key/hostId/hostType.
65+
2. In `generateBundle`, local artifacts are uploaded via the provider.
66+
3. Optional manifest is emitted for traceability/debugging.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@proj-airi/vite-plugin-warpdrive",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"private": true,
6+
"description": "Vite plugin to rewrite and upload heavy build assets to remote object storage",
7+
"author": {
8+
"name": "Moeru AI Project AIRI Team",
9+
"email": "airi@moeru.ai",
10+
"url": "https://github.com/moeru-ai"
11+
},
12+
"license": "MIT",
13+
"exports": {
14+
".": {
15+
"types": "./dist/index.d.mts",
16+
"default": "./dist/index.mjs"
17+
}
18+
},
19+
"main": "./dist/index.mjs",
20+
"types": "./dist/index.d.mts",
21+
"files": [
22+
"README.md",
23+
"dist",
24+
"package.json"
25+
],
26+
"scripts": {
27+
"build": "tsdown",
28+
"typecheck": "tsc --noEmit"
29+
},
30+
"peerDependencies": {
31+
"vite": "^5.0.0 || ^6.0.0"
32+
},
33+
"dependencies": {
34+
"rolldown": "1.0.0-beta.51",
35+
"s3mini": "^0.7.0"
36+
},
37+
"devDependencies": {
38+
"@moeru/std": "catalog:"
39+
}
40+
}

0 commit comments

Comments
 (0)