vite.config.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import { defineConfig } from 'vite';
  2. import react from '@vitejs/plugin-react';
  3. import fs from 'node:fs';
  4. import path from 'node:path';
  5. import { DatabaseSync } from 'node:sqlite';
  6. const outDir = path.resolve(__dirname, '../web/dist');
  7. const BACKEND_TARGET = 'http://localhost:2053';
  8. function resolveDBPath() {
  9. const envFolder = process.env.XUI_DB_FOLDER;
  10. if (envFolder) {
  11. const abs = path.isAbsolute(envFolder)
  12. ? envFolder
  13. : path.resolve(__dirname, '..', envFolder);
  14. return path.join(abs, 'x-ui.db');
  15. }
  16. const repoSubDB = path.resolve(__dirname, '..', 'x-ui', 'x-ui.db');
  17. if (fs.existsSync(repoSubDB)) return repoSubDB;
  18. const repoDB = path.resolve(__dirname, '..', 'x-ui.db');
  19. if (fs.existsSync(repoDB)) return repoDB;
  20. return '/etc/x-ui/x-ui.db';
  21. }
  22. const BASE_MIGRATED_ROUTES = {
  23. 'panel': '/index.html',
  24. 'panel/': '/index.html',
  25. 'panel/settings': '/settings.html',
  26. 'panel/settings/': '/settings.html',
  27. 'panel/inbounds': '/inbounds.html',
  28. 'panel/inbounds/': '/inbounds.html',
  29. 'panel/clients': '/clients.html',
  30. 'panel/clients/': '/clients.html',
  31. 'panel/xray': '/xray.html',
  32. 'panel/xray/': '/xray.html',
  33. 'panel/nodes': '/nodes.html',
  34. 'panel/nodes/': '/nodes.html',
  35. 'panel/api-docs': '/api-docs.html',
  36. 'panel/api-docs/': '/api-docs.html',
  37. };
  38. let cachedBasePath = '/';
  39. function readBasePathFromDB() {
  40. const dbPath = resolveDBPath();
  41. let db;
  42. try {
  43. db = new DatabaseSync(dbPath, { readOnly: true });
  44. } catch (_e) {
  45. return '/';
  46. }
  47. try {
  48. const row = db.prepare('SELECT value FROM settings WHERE key = ?').get('webBasePath');
  49. let value = row && typeof row.value === 'string' ? row.value : '/';
  50. if (!value.startsWith('/')) value = '/' + value;
  51. if (!value.endsWith('/')) value += '/';
  52. return value;
  53. } catch (_e) {
  54. return '/';
  55. } finally {
  56. db.close();
  57. }
  58. }
  59. function refreshBasePath() {
  60. cachedBasePath = readBasePathFromDB();
  61. return cachedBasePath;
  62. }
  63. function readPanelVersion() {
  64. try {
  65. const versionFile = path.resolve(__dirname, '..', 'config', 'version');
  66. return fs.readFileSync(versionFile, 'utf8').trim();
  67. } catch (_e) {
  68. return '';
  69. }
  70. }
  71. // `apply: 'serve'` keeps the injection out of `vite build` — dist.go
  72. // already injects webBasePath and version at runtime in production.
  73. function injectBasePathPlugin() {
  74. return {
  75. name: 'xui-inject-base-path',
  76. apply: 'serve',
  77. transformIndexHtml(html) {
  78. const basePath = refreshBasePath();
  79. const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  80. const version = readPanelVersion().replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  81. const tag = `<script>window.X_UI_BASE_PATH="${escaped}";window.X_UI_CUR_VER="${version}";</script>`;
  82. return html.replace('</head>', `${tag}</head>`);
  83. },
  84. };
  85. }
  86. function bypassMigratedRoute(req) {
  87. if (req.method !== 'GET') return undefined;
  88. const url = req.url.split('?')[0];
  89. const basePath = refreshBasePath();
  90. if (url === basePath) return '/login.html';
  91. if (url.startsWith(basePath)) {
  92. const stripped = url.slice(basePath.length);
  93. if (stripped in BASE_MIGRATED_ROUTES) return BASE_MIGRATED_ROUTES[stripped];
  94. }
  95. return undefined;
  96. }
  97. function rewriteToBackend(p) {
  98. if (cachedBasePath === '/' || p.startsWith(cachedBasePath)) return p;
  99. return cachedBasePath + p.replace(/^\//, '');
  100. }
  101. function makeBackendProxy(target) {
  102. return {
  103. target,
  104. changeOrigin: true,
  105. rewrite: rewriteToBackend,
  106. bypass: bypassMigratedRoute,
  107. configure(proxy) {
  108. let warned = false;
  109. proxy.on('error', (err, req) => {
  110. const codes = new Set();
  111. if (err && err.code) codes.add(err.code);
  112. if (err && Array.isArray(err.errors)) {
  113. for (const inner of err.errors) {
  114. if (inner && inner.code) codes.add(inner.code);
  115. }
  116. }
  117. const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET');
  118. if (offline) {
  119. if (!warned) {
  120. warned = true;
  121. // eslint-disable-next-line no-console
  122. console.warn(
  123. `[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`,
  124. );
  125. }
  126. return;
  127. }
  128. // eslint-disable-next-line no-console
  129. console.error('[proxy]', err);
  130. });
  131. },
  132. };
  133. }
  134. export default defineConfig({
  135. plugins: [react(), injectBasePathPlugin()],
  136. resolve: {
  137. alias: {
  138. '@': path.resolve(__dirname, 'src'),
  139. },
  140. },
  141. build: {
  142. outDir,
  143. emptyOutDir: true,
  144. sourcemap: true,
  145. target: 'es2020',
  146. chunkSizeWarningLimit: 1500,
  147. rollupOptions: {
  148. input: {
  149. index: path.resolve(__dirname, 'index.html'),
  150. login: path.resolve(__dirname, 'login.html'),
  151. settings: path.resolve(__dirname, 'settings.html'),
  152. inbounds: path.resolve(__dirname, 'inbounds.html'),
  153. clients: path.resolve(__dirname, 'clients.html'),
  154. xray: path.resolve(__dirname, 'xray.html'),
  155. nodes: path.resolve(__dirname, 'nodes.html'),
  156. apiDocs: path.resolve(__dirname, 'api-docs.html'),
  157. subpage: path.resolve(__dirname, 'subpage.html'),
  158. },
  159. output: {
  160. manualChunks(id) {
  161. if (!id.includes('node_modules')) return undefined;
  162. if (id.includes('/node_modules/antd/')) return 'vendor-antd';
  163. if (id.includes('/@ant-design/icons/')) return 'vendor-icons';
  164. if (
  165. id.includes('/node_modules/react-i18next/')
  166. || id.includes('/node_modules/i18next/')
  167. ) return 'vendor-i18next';
  168. if (
  169. id.includes('/node_modules/react/')
  170. || id.includes('/node_modules/react-dom/')
  171. || id.includes('/node_modules/scheduler/')
  172. ) return 'vendor-react';
  173. if (id.includes('dayjs')) return 'vendor-dayjs';
  174. if (id.includes('axios')) return 'vendor-axios';
  175. return 'vendor';
  176. },
  177. },
  178. },
  179. },
  180. server: {
  181. port: 5173,
  182. strictPort: true,
  183. proxy: {
  184. '^/(?:[^/]+/)?(login|logout|getTwoFactorEnable|csrf-token|panel|server)(?:/|$)': makeBackendProxy(BACKEND_TARGET),
  185. '^/$': makeBackendProxy(BACKEND_TARGET),
  186. '^/[^/]+/$': makeBackendProxy(BACKEND_TARGET),
  187. '^/(?:[^/]+/)?ws$': {
  188. target: 'ws://localhost:2053',
  189. ws: true,
  190. changeOrigin: true,
  191. rewrite: rewriteToBackend,
  192. },
  193. },
  194. },
  195. });