vite.config.js 5.8 KB

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