11import { defineConfig } from 'vite' ;
22import vue from '@vitejs/plugin-vue' ;
3+ import fs from 'node:fs' ;
34import path from 'node:path' ;
5+ import { DatabaseSync } from 'node:sqlite' ;
46
5- // Output goes to web/dist/ at the repo root so the Go binary can embed it
6- // via embed.FS without reaching outside the web/ tree.
77const outDir = path . resolve ( __dirname , '../web/dist' ) ;
8+ const BACKEND_TARGET = 'http://localhost:2053' ;
89
9- // In production the Go binary serves /panel/<route> from web/dist/<route>.html.
10- // In dev the Vue app lives at /index.html, /settings.html, ... while AppSidebar
11- // links use the production-style /panel/<route> URLs. Map each migrated route
12- // to its Vite entry so the sidebar works without relying on the Go backend
13- // for already-ported pages.
14- const MIGRATED_ROUTES = {
15- '/panel' : '/index.html' ,
16- '/panel/' : '/index.html' ,
17- '/panel/settings' : '/settings.html' ,
18- '/panel/settings/' : '/settings.html' ,
19- '/panel/inbounds' : '/inbounds.html' ,
20- '/panel/inbounds/' : '/inbounds.html' ,
21- '/panel/xray' : '/xray.html' ,
22- '/panel/xray/' : '/xray.html' ,
23- '/panel/nodes' : '/nodes.html' ,
24- '/panel/nodes/' : '/nodes.html' ,
10+ function resolveDBPath ( ) {
11+ const envFolder = process . env . XUI_DB_FOLDER ;
12+ if ( envFolder ) return path . join ( envFolder , 'x-ui.db' ) ;
13+ const repoDB = path . resolve ( __dirname , '..' , 'x-ui.db' ) ;
14+ if ( fs . existsSync ( repoDB ) ) return repoDB ;
15+ return '/etc/x-ui/x-ui.db' ;
16+ }
17+
18+ const BASE_MIGRATED_ROUTES = {
19+ 'panel' : '/index.html' ,
20+ 'panel/' : '/index.html' ,
21+ 'panel/settings' : '/settings.html' ,
22+ 'panel/settings/' : '/settings.html' ,
23+ 'panel/inbounds' : '/inbounds.html' ,
24+ 'panel/inbounds/' : '/inbounds.html' ,
25+ 'panel/xray' : '/xray.html' ,
26+ 'panel/xray/' : '/xray.html' ,
27+ 'panel/nodes' : '/nodes.html' ,
28+ 'panel/nodes/' : '/nodes.html' ,
2529} ;
2630
27- // Build a proxy config that suppresses ECONNREFUSED noise when the Go
28- // backend isn't running locally. Real errors (timeouts, 5xx, etc.) still
29- // surface in the Vite log.
30- function makeBackendProxy ( target , patterns ) {
31- const config = { } ;
32- for ( const pattern of patterns ) {
33- config [ pattern ] = {
34- target,
35- changeOrigin : true ,
36- // Returning a path from bypass tells Vite to serve that file from
37- // its own dev server instead of forwarding the request — used here
38- // to short-circuit /panel/<route> for pages we've already migrated.
39- //
40- // Only GETs get bypassed: the xray page reuses its page URL
41- // (`POST /panel/xray/`) for data, so a method-blind bypass would
42- // hand HTML back to fetch calls and break the page in dev.
43- bypass ( req ) {
44- if ( req . method !== 'GET' ) return undefined ;
45- const url = req . url . split ( '?' ) [ 0 ] ;
46- if ( Object . prototype . hasOwnProperty . call ( MIGRATED_ROUTES , url ) ) {
47- return MIGRATED_ROUTES [ url ] ;
48- }
49- return undefined ;
50- } ,
51- configure ( proxy ) {
52- let warned = false ;
53- proxy . on ( 'error' , ( err , req ) => {
54- // Node wraps connection failures in an AggregateError when DNS
55- // returns multiple addresses (e.g. ::1 + 127.0.0.1) and all
56- // refuse — the code lands on the inner errors, not the outer.
57- const codes = new Set ( ) ;
58- if ( err && err . code ) codes . add ( err . code ) ;
59- if ( err && Array . isArray ( err . errors ) ) {
60- for ( const inner of err . errors ) {
61- if ( inner && inner . code ) codes . add ( inner . code ) ;
62- }
31+ let cachedBasePath = '/' ;
32+
33+ function readBasePathFromDB ( ) {
34+ const dbPath = resolveDBPath ( ) ;
35+ let db ;
36+ try {
37+ db = new DatabaseSync ( dbPath , { readOnly : true } ) ;
38+ } catch ( _e ) {
39+ return '/' ;
40+ }
41+ try {
42+ const row = db . prepare ( 'SELECT value FROM settings WHERE key = ?' ) . get ( 'webBasePath' ) ;
43+ let value = row && typeof row . value === 'string' ? row . value : '/' ;
44+ if ( ! value . startsWith ( '/' ) ) value = '/' + value ;
45+ if ( ! value . endsWith ( '/' ) ) value += '/' ;
46+ return value ;
47+ } catch ( _e ) {
48+ return '/' ;
49+ } finally {
50+ db . close ( ) ;
51+ }
52+ }
53+
54+ function refreshBasePath ( ) {
55+ cachedBasePath = readBasePathFromDB ( ) ;
56+ return cachedBasePath ;
57+ }
58+
59+ // `apply: 'serve'` keeps the injection out of `vite build` — dist.go
60+ // already injects __X_UI_BASE_PATH__ at runtime in production.
61+ function injectBasePathPlugin ( ) {
62+ return {
63+ name : 'xui-inject-base-path' ,
64+ apply : 'serve' ,
65+ transformIndexHtml ( html ) {
66+ const basePath = refreshBasePath ( ) ;
67+ const escaped = basePath . replace ( / \\ / g, '\\\\' ) . replace ( / " / g, '\\"' ) ;
68+ const tag = `<script>window.__X_UI_BASE_PATH__="${ escaped } ";</script>` ;
69+ return html . replace ( '</head>' , `${ tag } </head>` ) ;
70+ } ,
71+ } ;
72+ }
73+
74+ function bypassMigratedRoute ( req ) {
75+ if ( req . method !== 'GET' ) return undefined ;
76+ const url = req . url . split ( '?' ) [ 0 ] ;
77+
78+ for ( const [ key , value ] of Object . entries ( BASE_MIGRATED_ROUTES ) ) {
79+ if ( url === '/' + key ) return value ;
80+ }
81+
82+ const m = url . match ( / ^ \/ [ ^ / ] + \/ ( .+ ) $ / ) ;
83+ if ( m ) {
84+ const stripped = m [ 1 ] ;
85+ if ( stripped in BASE_MIGRATED_ROUTES ) return BASE_MIGRATED_ROUTES [ stripped ] ;
86+ }
87+
88+ if ( url === '/' || / ^ \/ [ ^ / ] + \/ $ / . test ( url ) ) return '/login.html' ;
89+
90+ return undefined ;
91+ }
92+
93+ function rewriteToBackend ( p ) {
94+ if ( cachedBasePath === '/' || p . startsWith ( cachedBasePath ) ) return p ;
95+ return cachedBasePath + p . replace ( / ^ \/ / , '' ) ;
96+ }
97+
98+ function makeBackendProxy ( target ) {
99+ return {
100+ target,
101+ changeOrigin : true ,
102+ rewrite : rewriteToBackend ,
103+ bypass : bypassMigratedRoute ,
104+ configure ( proxy ) {
105+ let warned = false ;
106+ proxy . on ( 'error' , ( err , req ) => {
107+ const codes = new Set ( ) ;
108+ if ( err && err . code ) codes . add ( err . code ) ;
109+ if ( err && Array . isArray ( err . errors ) ) {
110+ for ( const inner of err . errors ) {
111+ if ( inner && inner . code ) codes . add ( inner . code ) ;
63112 }
64- const offline = codes . has ( 'ECONNREFUSED' ) || codes . has ( 'ECONNRESET' ) ;
65- if ( offline ) {
66- // Print a single friendly hint the first time, then stay quiet.
67- if ( ! warned ) {
68- warned = true ;
69- // eslint-disable-next-line no-console
70- console . warn (
71- `[proxy] backend ${ target } is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${ req ?. url || 'requests' } .` ,
72- ) ;
73- }
74- return ;
113+ }
114+ const offline = codes . has ( 'ECONNREFUSED' ) || codes . has ( 'ECONNRESET' ) ;
115+ if ( offline ) {
116+ if ( ! warned ) {
117+ warned = true ;
118+ // eslint-disable-next-line no-console
119+ console . warn (
120+ `[proxy] backend ${ target } is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${ req ?. url || 'requests' } .` ,
121+ ) ;
75122 }
76- // eslint-disable-next-line no-console
77- console . error ( '[proxy]' , err ) ;
78- } ) ;
79- } ,
80- } ;
81- }
82- return config ;
123+ return ;
124+ }
125+ // eslint-disable-next-line no-console
126+ console . error ( '[proxy]' , err ) ;
127+ } ) ;
128+ } ,
129+ } ;
83130}
84131
85132export default defineConfig ( {
86- plugins : [ vue ( ) ] ,
133+ plugins : [ vue ( ) , injectBasePathPlugin ( ) ] ,
87134 resolve : {
88135 alias : {
89136 '@' : path . resolve ( __dirname , 'src' ) ,
@@ -94,14 +141,7 @@ export default defineConfig({
94141 emptyOutDir : true ,
95142 sourcemap : true ,
96143 target : 'es2020' ,
97- // ant-design-vue is intentionally bundled as one chunk (its
98- // components share internals — splitting it breaks Modal/Form/
99- // Select interop). Minified it lands ~1.4MB but gzips to ~410kB,
100- // so the actual transfer is fine and caches across every page.
101- // Bump the warning past that ceiling so the build stays quiet.
102144 chunkSizeWarningLimit : 1500 ,
103- // Multiple HTML entries — one per legacy page we migrate.
104- // As pages get ported in later phases, add their entrypoints here.
105145 rollupOptions : {
106146 input : {
107147 index : path . resolve ( __dirname , 'index.html' ) ,
@@ -113,10 +153,6 @@ export default defineConfig({
113153 subpage : path . resolve ( __dirname , 'subpage.html' ) ,
114154 } ,
115155 output : {
116- // Split vendor deps into stable chunks so each page only pulls
117- // what it needs and the browser caches them across versions.
118- // Without this, ant-design-vue + vue + icons all end up in one
119- // 1.6MB blob attached to whichever page consumed them first.
120156 manualChunks ( id ) {
121157 if ( ! id . includes ( 'node_modules' ) ) return undefined ;
122158 if ( id . includes ( 'ant-design-vue' ) ) return 'vendor-antd' ;
@@ -129,8 +165,6 @@ export default defineConfig({
129165 if ( id . includes ( 'dayjs' ) ) return 'vendor-dayjs' ;
130166 if ( id . includes ( 'qrious' ) ) return 'vendor-qrious' ;
131167 if ( id . includes ( 'axios' ) ) return 'vendor-axios' ;
132- // The persian datepicker pulls in moment + moment-jalaali; bundle
133- // the trio together so unrelated pages don't pay the cost.
134168 if (
135169 id . includes ( 'vue3-persian-datetime-picker' )
136170 || id . includes ( 'moment-jalaali' )
@@ -146,21 +180,14 @@ export default defineConfig({
146180 port : 5173 ,
147181 strictPort : true ,
148182 proxy : {
149- ...makeBackendProxy ( 'http://localhost:2053' , [
150- // Patterns are anchored regex so /login.html and /index.html
151- // (which Vite serves itself) are NOT forwarded — only the bare
152- // backend paths and their sub-routes.
153- '^/(login|logout|getTwoFactorEnable|csrf-token)$' ,
154- '^/(panel|server)(/|$)' ,
155- ] ) ,
156- // The panel mounts the live-update WebSocket at /ws (basePath +
157- // "/ws"). Vite needs `ws: true` to forward the HTTP Upgrade to the
158- // Go backend; without it the dev server would 404 the upgrade and
159- // the page falls back to the no-data state.
160- '/ws' : {
183+ '^/(?:[^/]+/)?(login|logout|getTwoFactorEnable|csrf-token|panel|server)(?:/|$)' : makeBackendProxy ( BACKEND_TARGET ) ,
184+ '^/$' : makeBackendProxy ( BACKEND_TARGET ) ,
185+ '^/[^/]+/$' : makeBackendProxy ( BACKEND_TARGET ) ,
186+ '^/(?:[^/]+/)?ws$' : {
161187 target : 'ws://localhost:2053' ,
162188 ws : true ,
163189 changeOrigin : true ,
190+ rewrite : rewriteToBackend ,
164191 } ,
165192 } ,
166193 } ,
0 commit comments