Skip to content

Commit ce0b74f

Browse files
committed
...
1 parent 6781af5 commit ce0b74f

File tree

7 files changed

+3065
-3
lines changed

7 files changed

+3065
-3
lines changed

.github/workflows/website.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
- name: Checkout
3333
uses: actions/checkout@v4
3434
with:
35-
fetch-depth: 0 # Not needed if lastUpdated is not enabled
35+
fetch-depth: 0 # Not needed if lastUpdated is not enabled
3636
- name: Setup Node
3737
uses: actions/setup-node@v4
3838
with:
@@ -41,14 +41,30 @@ jobs:
4141
uses: actions/configure-pages@v5
4242
- name: Install dependencies
4343
run: yarn install --frozen-lockfile
44+
- name: Copy logo to docs assets
45+
run: |
46+
mkdir -p docs/assets
47+
cp ../assets/logo.png docs/assets/
4448
- name: Build with VitePress
4549
run: |
4650
yarn build
4751
touch docs/.vitepress/dist/.nojekyll
52+
- name: Ensure logo is in build output
53+
run: |
54+
mkdir -p docs/.vitepress/dist/assets
55+
cp ../assets/logo.png docs/.vitepress/dist/assets/
56+
- name: Create single HTML bundle
57+
run: yarn bundle
4858
- name: Upload artifact
4959
uses: actions/upload-pages-artifact@v3
5060
with:
5161
path: website/docs/.vitepress/dist
62+
- name: Upload single HTML artifact
63+
uses: actions/upload-artifact@v4
64+
with:
65+
name: single-html-bundle
66+
path: website/dist-single/index.html
67+
retention-days: 30
5268

5369
# Deployment job
5470
deploy:

website/bundle-simple.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
8+
console.log('🚀 Starting bundle process...');
9+
10+
// 路径配置
11+
const distDir = path.join(__dirname, 'docs/.vitepress/dist');
12+
const htmlFile = path.join(distDir, 'index.html');
13+
const outputDir = path.join(__dirname, 'dist-single');
14+
const outputFile = path.join(outputDir, 'index.html');
15+
16+
// 确保输出目录存在
17+
if (!fs.existsSync(outputDir)) {
18+
fs.mkdirSync(outputDir, { recursive: true });
19+
}
20+
21+
// 读取 HTML 内容
22+
let html = fs.readFileSync(htmlFile, 'utf-8');
23+
console.log('📖 Reading HTML file...');
24+
25+
// 内联CSS文件
26+
console.log('🎨 Inlining CSS...');
27+
const cssRegex = /<link[^>]*rel=['"](preload )?stylesheet['"][^>]*href=['"]([^'"]*\.css)['"][^>]*>/g;
28+
let cssMatch;
29+
let cssCount = 0;
30+
while ((cssMatch = cssRegex.exec(html)) !== null) {
31+
const cssPath = cssMatch[2];
32+
const fullCssPath = path.join(distDir, cssPath.startsWith('/') ? cssPath.slice(1) : cssPath);
33+
34+
if (fs.existsSync(fullCssPath)) {
35+
const cssContent = fs.readFileSync(fullCssPath, 'utf-8');
36+
const styleTag = `<style>${cssContent}</style>`;
37+
html = html.replace(cssMatch[0], styleTag);
38+
cssCount++;
39+
}
40+
}
41+
console.log(`✅ Inlined ${cssCount} CSS files`);
42+
43+
// 内联JavaScript文件
44+
console.log('⚡ Inlining JavaScript...');
45+
const jsRegex = /<script[^>]*src=['"]([^'"]*\.js)['"][^>]*><\/script>/g;
46+
let jsMatch;
47+
let jsCount = 0;
48+
while ((jsMatch = jsRegex.exec(html)) !== null) {
49+
const jsPath = jsMatch[1];
50+
const fullJsPath = path.join(distDir, jsPath.startsWith('/') ? jsPath.slice(1) : jsPath);
51+
52+
if (fs.existsSync(fullJsPath)) {
53+
const jsContent = fs.readFileSync(fullJsPath, 'utf-8');
54+
const scriptTag = `<script>${jsContent}</script>`;
55+
html = html.replace(jsMatch[0], scriptTag);
56+
jsCount++;
57+
}
58+
}
59+
console.log(`✅ Inlined ${jsCount} JavaScript files`);
60+
61+
// 内联图片
62+
console.log('🖼️ Inlining images...');
63+
const imgRegex = /src=['"]\/assets\/([^'"]+\.(png|jpg|jpeg|gif|svg|ico))['"]|href=['"]\/assets\/([^'"]+\.(png|jpg|jpeg|gif|svg|ico))['"]|url\(['"]?\/assets\/([^'"]*\.(png|jpg|jpeg|gif|svg|ico))['"]?\)/g;
64+
let imgMatch;
65+
let imgCount = 0;
66+
while ((imgMatch = imgRegex.exec(html)) !== null) {
67+
const assetPath = imgMatch[1] || imgMatch[3] || imgMatch[5];
68+
if (assetPath) {
69+
const fullAssetPath = path.join(distDir, 'assets', assetPath);
70+
if (fs.existsSync(fullAssetPath)) {
71+
try {
72+
const assetContent = fs.readFileSync(fullAssetPath);
73+
const ext = path.extname(assetPath).toLowerCase();
74+
const mimeType = {
75+
'.png': 'image/png',
76+
'.jpg': 'image/jpeg',
77+
'.jpeg': 'image/jpeg',
78+
'.gif': 'image/gif',
79+
'.svg': 'image/svg+xml',
80+
'.ico': 'image/x-icon'
81+
}[ext] || 'image/png';
82+
83+
const base64 = assetContent.toString('base64');
84+
const dataUri = `data:${mimeType};base64,${base64}`;
85+
html = html.replace(imgMatch[0], imgMatch[0].replace(`/assets/${assetPath}`, dataUri));
86+
imgCount++;
87+
} catch (e) {
88+
console.warn(`⚠️ Could not encode ${assetPath}: ${e.message}`);
89+
}
90+
}
91+
}
92+
}
93+
console.log(`✅ Inlined ${imgCount} images`);
94+
95+
// 内联字体
96+
console.log('🔤 Inlining fonts...');
97+
const fontRegex = /url\(['"]?\/assets\/([^'"]*\.(woff2?|ttf|eot|otf))['"]?\)/g;
98+
let fontMatch;
99+
let fontCount = 0;
100+
while ((fontMatch = fontRegex.exec(html)) !== null) {
101+
const fontPath = fontMatch[1];
102+
const fullFontPath = path.join(distDir, 'assets', fontPath);
103+
104+
if (fs.existsSync(fullFontPath)) {
105+
try {
106+
const fontContent = fs.readFileSync(fullFontPath);
107+
const ext = path.extname(fontPath).toLowerCase();
108+
const mimeType = {
109+
'.woff': 'font/woff',
110+
'.woff2': 'font/woff2',
111+
'.ttf': 'font/ttf',
112+
'.otf': 'font/otf',
113+
'.eot': 'application/vnd.ms-fontobject'
114+
}[ext] || 'font/woff2';
115+
116+
const base64 = fontContent.toString('base64');
117+
const dataUri = `data:${mimeType};base64,${base64}`;
118+
html = html.replace(fontMatch[0], `url(${dataUri})`);
119+
fontCount++;
120+
} catch (e) {
121+
console.warn(`⚠️ Could not encode font ${fontPath}: ${e.message}`);
122+
}
123+
}
124+
}
125+
console.log(`✅ Inlined ${fontCount} fonts`);
126+
127+
// 写入输出文件
128+
fs.writeFileSync(outputFile, html, 'utf-8');
129+
130+
const fileSizeKB = Math.round(fs.statSync(outputFile).size / 1024);
131+
console.log(`\n🎉 Bundle created successfully!`);
132+
console.log(`📄 Output: ${outputFile}`);
133+
console.log(`📦 File size: ${fileSizeKB} KB`);

website/bundle.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { defineConfig } from 'vite'
2+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs'
3+
import { join, extname, relative, dirname } from 'path'
4+
import { createHash } from 'crypto'
5+
6+
// 递归复制VitePress构建输出并创建单文件HTML
7+
function createSingleHTMLBundle() {
8+
return {
9+
name: 'create-single-html',
10+
apply: 'build',
11+
closeBundle() {
12+
const vitepressDistPath = 'docs/.vitepress/dist'
13+
const outputPath = 'dist-single'
14+
15+
if (!existsSync(vitepressDistPath)) {
16+
console.error('❌ VitePress build output not found. Please run "npm run build" first.')
17+
return
18+
}
19+
20+
try {
21+
// 读取主HTML文件
22+
const indexPath = join(vitepressDistPath, 'index.html')
23+
if (!existsSync(indexPath)) {
24+
console.error('❌ index.html not found in VitePress output')
25+
return
26+
}
27+
28+
let htmlContent = readFileSync(indexPath, 'utf-8')
29+
30+
// 内联所有CSS文件
31+
htmlContent = inlineAssets(htmlContent, vitepressDistPath, '.css', 'style')
32+
33+
// 内联所有JavaScript文件
34+
htmlContent = inlineAssets(htmlContent, vitepressDistPath, '.js', 'script')
35+
36+
// 内联所有图片和字体文件
37+
htmlContent = inlineImageAssets(htmlContent, vitepressDistPath)
38+
htmlContent = inlineFontAssets(htmlContent, vitepressDistPath)
39+
40+
// 确保输出目录存在
41+
if (!existsSync(outputPath)) {
42+
require('fs').mkdirSync(outputPath, { recursive: true })
43+
}
44+
45+
// 写入单文件HTML
46+
const outputFile = join(outputPath, 'index.html')
47+
writeFileSync(outputFile, htmlContent, 'utf-8')
48+
49+
console.log('✅ Successfully created single HTML file:', outputFile)
50+
console.log(`📦 File size: ${(htmlContent.length / 1024).toFixed(2)} KB`)
51+
52+
} catch (error) {
53+
console.error('❌ Error creating single HTML bundle:', error.message)
54+
}
55+
}
56+
}
57+
}
58+
59+
// 内联CSS和JS资产
60+
function inlineAssets(htmlContent, basePath, extension, tagType) {
61+
const assetRegex = extension === '.css'
62+
? /<link[^>]*rel="stylesheet"[^>]*href="([^"]+\.css)"[^>]*>/gi
63+
: /<script[^>]*src="([^"]+\.js)"[^>]*><\/script>/gi
64+
65+
return htmlContent.replace(assetRegex, (match, assetPath) => {
66+
try {
67+
// 处理相对路径
68+
const fullPath = assetPath.startsWith('/')
69+
? join(basePath, assetPath.slice(1))
70+
: join(basePath, assetPath)
71+
72+
if (existsSync(fullPath)) {
73+
const content = readFileSync(fullPath, 'utf-8')
74+
return tagType === 'style'
75+
? `<style>${content}</style>`
76+
: `<script>${content}</script>`
77+
} else {
78+
console.warn(`⚠️ Asset not found: ${fullPath}`)
79+
return match
80+
}
81+
} catch (error) {
82+
console.warn(`⚠️ Error inlining ${assetPath}:`, error.message)
83+
return match
84+
}
85+
})
86+
}
87+
88+
// 内联图片资产
89+
function inlineImageAssets(htmlContent, basePath) {
90+
const imageRegex = /<img[^>]*src="([^"]+\.(png|jpg|jpeg|gif|svg|ico))"[^>]*>/gi
91+
92+
return htmlContent.replace(imageRegex, (match, imagePath) => {
93+
try {
94+
const fullPath = imagePath.startsWith('/')
95+
? join(basePath, imagePath.slice(1))
96+
: join(basePath, imagePath)
97+
98+
if (existsSync(fullPath)) {
99+
const imageBuffer = readFileSync(fullPath)
100+
const base64 = imageBuffer.toString('base64')
101+
const ext = extname(imagePath).slice(1).toLowerCase()
102+
const mimeType = getMimeType(ext)
103+
const dataUri = `data:${mimeType};base64,${base64}`
104+
105+
return match.replace(imagePath, dataUri)
106+
} else {
107+
console.warn(`⚠️ Image not found: ${fullPath}`)
108+
return match
109+
}
110+
} catch (error) {
111+
console.warn(`⚠️ Error inlining image ${imagePath}:`, error.message)
112+
return match
113+
}
114+
})
115+
}
116+
117+
// 内联字体资产
118+
function inlineFontAssets(htmlContent, basePath) {
119+
const fontRegex = /url\(['"]?([^'"]*\.(woff2?|ttf|eot|otf))['"]?\)/gi
120+
121+
return htmlContent.replace(fontRegex, (match, fontPath) => {
122+
try {
123+
const fullPath = fontPath.startsWith('/')
124+
? join(basePath, fontPath.slice(1))
125+
: join(basePath, fontPath)
126+
127+
if (existsSync(fullPath)) {
128+
const fontBuffer = readFileSync(fullPath)
129+
const base64 = fontBuffer.toString('base64')
130+
const ext = extname(fontPath).slice(1).toLowerCase()
131+
const mimeType = getFontMimeType(ext)
132+
const dataUri = `data:${mimeType};base64,${base64}`
133+
134+
return `url(${dataUri})`
135+
} else {
136+
console.warn(`⚠️ Font not found: ${fullPath}`)
137+
return match
138+
}
139+
} catch (error) {
140+
console.warn(`⚠️ Error inlining font ${fontPath}:`, error.message)
141+
return match
142+
}
143+
})
144+
}
145+
146+
// 获取MIME类型
147+
function getMimeType(ext) {
148+
const mimeTypes = {
149+
'png': 'image/png',
150+
'jpg': 'image/jpeg',
151+
'jpeg': 'image/jpeg',
152+
'gif': 'image/gif',
153+
'svg': 'image/svg+xml',
154+
'ico': 'image/x-icon',
155+
'webp': 'image/webp'
156+
}
157+
return mimeTypes[ext] || 'application/octet-stream'
158+
}
159+
160+
// 获取字体MIME类型
161+
function getFontMimeType(ext) {
162+
const fontMimeTypes = {
163+
'woff': 'font/woff',
164+
'woff2': 'font/woff2',
165+
'ttf': 'font/ttf',
166+
'otf': 'font/otf',
167+
'eot': 'application/vnd.ms-fontobject'
168+
}
169+
return fontMimeTypes[ext] || 'application/octet-stream'
170+
}
171+
172+
export default defineConfig({
173+
plugins: [createSingleHTMLBundle()],
174+
build: {
175+
// 这个配置主要是为了让Vite能够运行我们的插件
176+
// 实际的输入来自VitePress的构建输出
177+
rollupOptions: {
178+
input: 'bundle.js', // 虚拟入口点
179+
external: ['fs', 'path', 'crypto'] // Node.js内置模块
180+
}
181+
}
182+
})

website/dist-single/index.html

Lines changed: 27 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)