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+ ? / < l i n k [ ^ > ] * r e l = " s t y l e s h e e t " [ ^ > ] * h r e f = " ( [ ^ " ] + \. c s s ) " [ ^ > ] * > / gi
63+ : / < s c r i p t [ ^ > ] * s r c = " ( [ ^ " ] + \. j s ) " [ ^ > ] * > < \/ s c r i p t > / 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 = / < i m g [ ^ > ] * s r c = " ( [ ^ " ] + \. ( p n g | j p g | j p e g | g i f | s v g | i c o ) ) " [ ^ > ] * > / 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 = / u r l \( [ ' " ] ? ( [ ^ ' " ] * \. ( w o f f 2 ? | t t f | e o t | o t f ) ) [ ' " ] ? \) / 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+ } )
0 commit comments