vite.config.js 5.8 KB

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