1
0

vite.config.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. // es-toolkit's `./compat/*` exports map only declares a CJS condition, so deep
  72. // imports like `es-toolkit/compat/get` resolve to a CJS shim. That shim uses a
  73. // `require_X.Y` pattern that Vite's optimizer and Rolldown both mishandle
  74. // (TypeError: require_isUnsafeProperty is not a function). The ESM build at
  75. // `dist/compat/<category>/<name>.mjs` is fine but only carries a named export,
  76. // while consumers like recharts use default imports — so emit a virtual module
  77. // that re-exports the named symbol as default.
  78. const ES_TOOLKIT_COMPAT_DIRS = ['array', 'function', 'math', 'object', 'predicate', 'string', 'util'];
  79. const ES_TOOLKIT_SHIM_PREFIX = '\0es-toolkit-compat:';
  80. function findEsToolkitCompatMjs(name) {
  81. for (const sub of ES_TOOLKIT_COMPAT_DIRS) {
  82. const candidate = path.resolve(__dirname, 'node_modules/es-toolkit/dist/compat', sub, `${name}.mjs`);
  83. if (fs.existsSync(candidate)) return candidate;
  84. }
  85. return null;
  86. }
  87. function esToolkitCompatEsmResolver() {
  88. return {
  89. name: 'es-toolkit-compat-esm',
  90. enforce: 'pre',
  91. resolveId(id) {
  92. const m = id.match(/^es-toolkit\/compat\/(.+)$/);
  93. if (!m) return null;
  94. if (!findEsToolkitCompatMjs(m[1])) return null;
  95. return ES_TOOLKIT_SHIM_PREFIX + m[1];
  96. },
  97. load(id) {
  98. if (!id.startsWith(ES_TOOLKIT_SHIM_PREFIX)) return null;
  99. const name = id.slice(ES_TOOLKIT_SHIM_PREFIX.length);
  100. const target = findEsToolkitCompatMjs(name);
  101. if (!target) return null;
  102. const url = target.replace(/\\/g, '/');
  103. return `import { ${name} } from ${JSON.stringify(url)};\nexport { ${name} };\nexport default ${name};\n`;
  104. },
  105. };
  106. }
  107. function bypassMigratedRoute(req) {
  108. if (req.method !== 'GET') return undefined;
  109. const url = req.url.split('?')[0];
  110. const basePath = refreshBasePath();
  111. if (url === basePath) return '/login.html';
  112. if (url.startsWith(basePath)) {
  113. const stripped = url.slice(basePath.length);
  114. for (const prefix of PANEL_API_PREFIXES) {
  115. if (prefix.endsWith('/')) {
  116. if (stripped.startsWith(prefix)) return undefined;
  117. } else if (stripped === prefix || stripped.startsWith(prefix + '/')) {
  118. return undefined;
  119. }
  120. }
  121. if (stripped === 'panel' || stripped === 'panel/' || stripped.startsWith('panel/')) {
  122. return '/index.html';
  123. }
  124. }
  125. return undefined;
  126. }
  127. function rewriteToBackend(p) {
  128. if (cachedBasePath === '/' || p.startsWith(cachedBasePath)) return p;
  129. return cachedBasePath + p.replace(/^\//, '');
  130. }
  131. function makeBackendProxy(target) {
  132. return {
  133. target,
  134. changeOrigin: true,
  135. rewrite: rewriteToBackend,
  136. bypass: bypassMigratedRoute,
  137. configure(proxy) {
  138. let warned = false;
  139. proxy.on('error', (err, req) => {
  140. const codes = new Set();
  141. if (err && err.code) codes.add(err.code);
  142. if (err && Array.isArray(err.errors)) {
  143. for (const inner of err.errors) {
  144. if (inner && inner.code) codes.add(inner.code);
  145. }
  146. }
  147. const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET');
  148. if (offline) {
  149. if (!warned) {
  150. warned = true;
  151. // eslint-disable-next-line no-console
  152. console.warn(
  153. `[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`,
  154. );
  155. }
  156. return;
  157. }
  158. // eslint-disable-next-line no-console
  159. console.error('[proxy]', err);
  160. });
  161. },
  162. };
  163. }
  164. export default defineConfig({
  165. plugins: [esToolkitCompatEsmResolver(), react(), injectBasePathPlugin()],
  166. resolve: {
  167. alias: {
  168. '@': path.resolve(__dirname, 'src'),
  169. },
  170. },
  171. optimizeDeps: {
  172. rolldownOptions: {
  173. plugins: [esToolkitCompatEsmResolver()],
  174. },
  175. },
  176. experimental: {
  177. renderBuiltUrl(filename, { hostType }) {
  178. if (hostType === 'js') {
  179. return {
  180. runtime: `((window.X_UI_BASE_PATH||'/')+${JSON.stringify(filename)})`,
  181. };
  182. }
  183. return undefined;
  184. },
  185. },
  186. build: {
  187. outDir,
  188. emptyOutDir: true,
  189. sourcemap: true,
  190. target: 'es2020',
  191. chunkSizeWarningLimit: 1500,
  192. rollupOptions: {
  193. input: {
  194. index: path.resolve(__dirname, 'index.html'),
  195. login: path.resolve(__dirname, 'login.html'),
  196. subpage: path.resolve(__dirname, 'subpage.html'),
  197. },
  198. output: {
  199. manualChunks(id) {
  200. if (!id.includes('node_modules')) return undefined;
  201. if (id.includes('/node_modules/antd/')) return 'vendor-antd';
  202. if (id.includes('/@ant-design/icons/') || id.includes('/@ant-design/icons-svg/')) return 'vendor-icons';
  203. if (
  204. id.includes('/node_modules/@rc-component/')
  205. || id.includes('/node_modules/rc-')
  206. || id.includes('/@ant-design/cssinjs')
  207. || id.includes('/@ant-design/colors')
  208. || id.includes('/@ant-design/fast-color')
  209. || id.includes('/@ant-design/react-slick')
  210. || id.includes('/@ctrl/tinycolor')
  211. ) return 'vendor-antd';
  212. if (
  213. id.includes('/node_modules/react-i18next/')
  214. || id.includes('/node_modules/i18next/')
  215. ) return 'vendor-i18next';
  216. if (
  217. id.includes('/node_modules/react/')
  218. || id.includes('/node_modules/react-dom/')
  219. || id.includes('/node_modules/scheduler/')
  220. ) return 'vendor-react';
  221. if (
  222. id.includes('/node_modules/codemirror/')
  223. || id.includes('/node_modules/@codemirror/')
  224. || id.includes('/node_modules/@lezer/')
  225. ) return 'vendor-codemirror';
  226. if (id.includes('/node_modules/persian-calendar-suite/')) return 'vendor-jalali';
  227. if (id.includes('/node_modules/otpauth/')) return 'vendor-otpauth';
  228. if (id.includes('/node_modules/@tanstack/')) return 'vendor-tanstack';
  229. if (id.includes('/node_modules/react-router')) return 'vendor-router';
  230. if (
  231. id.includes('/node_modules/swagger-ui-react/')
  232. || id.includes('/node_modules/swagger-ui/')
  233. || id.includes('/node_modules/swagger-client/')
  234. ) return 'vendor-swagger';
  235. if (
  236. id.includes('/node_modules/recharts/')
  237. || id.includes('/node_modules/victory-vendor/')
  238. || id.includes('/node_modules/d3-')
  239. ) return 'vendor-recharts';
  240. if (id.includes('dayjs')) return 'vendor-dayjs';
  241. if (id.includes('axios')) return 'vendor-axios';
  242. return 'vendor';
  243. },
  244. },
  245. },
  246. },
  247. server: {
  248. port: 5173,
  249. strictPort: true,
  250. proxy: {
  251. '^/(?:[^/]+/)?(login|logout|getTwoFactorEnable|csrf-token|panel|server)(?:/|$)': makeBackendProxy(BACKEND_TARGET),
  252. '^/$': makeBackendProxy(BACKEND_TARGET),
  253. '^/[^/]+/$': makeBackendProxy(BACKEND_TARGET),
  254. '^/(?:[^/]+/)?ws$': {
  255. target: 'ws://localhost:2053',
  256. ws: true,
  257. changeOrigin: true,
  258. rewrite: rewriteToBackend,
  259. },
  260. },
  261. },
  262. });