vite.config.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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 PANEL_API_PREFIXES = ['panel/api/', 'panel/setting/', 'panel/xray/', 'panel/csrf-token'];
  23. let cachedBasePath = '/';
  24. function readBasePathFromDB() {
  25. const dbPath = resolveDBPath();
  26. let db;
  27. try {
  28. db = new DatabaseSync(dbPath, { readOnly: true });
  29. } catch (_e) {
  30. return '/';
  31. }
  32. try {
  33. const row = db.prepare('SELECT value FROM settings WHERE key = ?').get('webBasePath');
  34. let value = row && typeof row.value === 'string' ? row.value : '/';
  35. if (!value.startsWith('/')) value = '/' + value;
  36. if (!value.endsWith('/')) value += '/';
  37. return value;
  38. } catch (_e) {
  39. return '/';
  40. } finally {
  41. db.close();
  42. }
  43. }
  44. function refreshBasePath() {
  45. cachedBasePath = readBasePathFromDB();
  46. return cachedBasePath;
  47. }
  48. function readPanelVersion() {
  49. try {
  50. const versionFile = path.resolve(__dirname, '..', 'config', 'version');
  51. return fs.readFileSync(versionFile, 'utf8').trim();
  52. } catch (_e) {
  53. return '';
  54. }
  55. }
  56. // `apply: 'serve'` keeps the injection out of `vite build` — dist.go
  57. // already injects webBasePath and version at runtime in production.
  58. function injectBasePathPlugin() {
  59. return {
  60. name: 'xui-inject-base-path',
  61. apply: 'serve',
  62. transformIndexHtml(html) {
  63. const basePath = refreshBasePath();
  64. const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  65. const version = readPanelVersion().replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  66. const tag = `<script>window.X_UI_BASE_PATH="${escaped}";window.X_UI_CUR_VER="${version}";</script>`;
  67. return html.replace('</head>', `${tag}</head>`);
  68. },
  69. };
  70. }
  71. function bypassMigratedRoute(req) {
  72. if (req.method !== 'GET') return undefined;
  73. const url = req.url.split('?')[0];
  74. const basePath = refreshBasePath();
  75. if (url === basePath) return '/login.html';
  76. if (url.startsWith(basePath)) {
  77. const stripped = url.slice(basePath.length);
  78. for (const prefix of PANEL_API_PREFIXES) {
  79. if (stripped === prefix.replace(/\/$/, '') || stripped.startsWith(prefix)) {
  80. return undefined;
  81. }
  82. }
  83. if (stripped === 'panel' || stripped === 'panel/' || stripped.startsWith('panel/')) {
  84. return '/index.html';
  85. }
  86. }
  87. return undefined;
  88. }
  89. function rewriteToBackend(p) {
  90. if (cachedBasePath === '/' || p.startsWith(cachedBasePath)) return p;
  91. return cachedBasePath + p.replace(/^\//, '');
  92. }
  93. function makeBackendProxy(target) {
  94. return {
  95. target,
  96. changeOrigin: true,
  97. rewrite: rewriteToBackend,
  98. bypass: bypassMigratedRoute,
  99. configure(proxy) {
  100. let warned = false;
  101. proxy.on('error', (err, req) => {
  102. const codes = new Set();
  103. if (err && err.code) codes.add(err.code);
  104. if (err && Array.isArray(err.errors)) {
  105. for (const inner of err.errors) {
  106. if (inner && inner.code) codes.add(inner.code);
  107. }
  108. }
  109. const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET');
  110. if (offline) {
  111. if (!warned) {
  112. warned = true;
  113. // eslint-disable-next-line no-console
  114. console.warn(
  115. `[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`,
  116. );
  117. }
  118. return;
  119. }
  120. // eslint-disable-next-line no-console
  121. console.error('[proxy]', err);
  122. });
  123. },
  124. };
  125. }
  126. export default defineConfig({
  127. plugins: [react(), injectBasePathPlugin()],
  128. resolve: {
  129. alias: {
  130. '@': path.resolve(__dirname, 'src'),
  131. },
  132. },
  133. experimental: {
  134. renderBuiltUrl(filename, { hostType }) {
  135. if (hostType === 'js') {
  136. return {
  137. runtime: `((window.X_UI_BASE_PATH||'/')+${JSON.stringify(filename)})`,
  138. };
  139. }
  140. return undefined;
  141. },
  142. },
  143. build: {
  144. outDir,
  145. emptyOutDir: true,
  146. sourcemap: true,
  147. target: 'es2020',
  148. chunkSizeWarningLimit: 1500,
  149. rollupOptions: {
  150. input: {
  151. index: path.resolve(__dirname, 'index.html'),
  152. login: path.resolve(__dirname, 'login.html'),
  153. subpage: path.resolve(__dirname, 'subpage.html'),
  154. },
  155. output: {
  156. manualChunks(id) {
  157. if (!id.includes('node_modules')) return undefined;
  158. if (id.includes('/node_modules/antd/')) return 'vendor-antd';
  159. if (id.includes('/@ant-design/icons/') || id.includes('/@ant-design/icons-svg/')) return 'vendor-icons';
  160. if (
  161. id.includes('/node_modules/@rc-component/')
  162. || id.includes('/node_modules/rc-')
  163. || id.includes('/@ant-design/cssinjs')
  164. || id.includes('/@ant-design/colors')
  165. || id.includes('/@ant-design/fast-color')
  166. || id.includes('/@ant-design/react-slick')
  167. || id.includes('/@ctrl/tinycolor')
  168. ) return 'vendor-antd';
  169. if (
  170. id.includes('/node_modules/react-i18next/')
  171. || id.includes('/node_modules/i18next/')
  172. ) return 'vendor-i18next';
  173. if (
  174. id.includes('/node_modules/react/')
  175. || id.includes('/node_modules/react-dom/')
  176. || id.includes('/node_modules/scheduler/')
  177. ) return 'vendor-react';
  178. if (
  179. id.includes('/node_modules/codemirror/')
  180. || id.includes('/node_modules/@codemirror/')
  181. || id.includes('/node_modules/@lezer/')
  182. ) return 'vendor-codemirror';
  183. if (id.includes('/node_modules/persian-calendar-suite/')) return 'vendor-jalali';
  184. if (id.includes('/node_modules/otpauth/')) return 'vendor-otpauth';
  185. if (id.includes('/node_modules/@tanstack/')) return 'vendor-tanstack';
  186. if (id.includes('/node_modules/react-router')) return 'vendor-router';
  187. if (
  188. id.includes('/node_modules/swagger-ui-react/')
  189. || id.includes('/node_modules/swagger-ui/')
  190. || id.includes('/node_modules/swagger-client/')
  191. ) return 'vendor-swagger';
  192. if (id.includes('dayjs')) return 'vendor-dayjs';
  193. if (id.includes('axios')) return 'vendor-axios';
  194. return 'vendor';
  195. },
  196. },
  197. },
  198. },
  199. server: {
  200. port: 5173,
  201. strictPort: true,
  202. proxy: {
  203. '^/(?:[^/]+/)?(login|logout|getTwoFactorEnable|csrf-token|panel|server)(?:/|$)': makeBackendProxy(BACKEND_TARGET),
  204. '^/$': makeBackendProxy(BACKEND_TARGET),
  205. '^/[^/]+/$': makeBackendProxy(BACKEND_TARGET),
  206. '^/(?:[^/]+/)?ws$': {
  207. target: 'ws://localhost:2053',
  208. ws: true,
  209. changeOrigin: true,
  210. rewrite: rewriteToBackend,
  211. },
  212. },
  213. },
  214. });