vite.config.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { defineConfig } from 'vite';
  2. import vue from '@vitejs/plugin-vue';
  3. import path from 'node:path';
  4. // Output goes to web/dist/ at the repo root so the Go binary can embed it
  5. // via embed.FS without reaching outside the web/ tree.
  6. const outDir = path.resolve(__dirname, '../web/dist');
  7. // In production the Go binary serves /panel/<route> from web/dist/<route>.html.
  8. // In dev the Vue app lives at /index.html, /settings.html, ... while AppSidebar
  9. // links use the production-style /panel/<route> URLs. Map each migrated route
  10. // to its Vite entry so the sidebar works without relying on the Go backend
  11. // for already-ported pages.
  12. const MIGRATED_ROUTES = {
  13. '/panel': '/index.html',
  14. '/panel/': '/index.html',
  15. '/panel/settings': '/settings.html',
  16. '/panel/settings/': '/settings.html',
  17. '/panel/inbounds': '/inbounds.html',
  18. '/panel/inbounds/': '/inbounds.html',
  19. '/panel/xray': '/xray.html',
  20. '/panel/xray/': '/xray.html',
  21. '/panel/nodes': '/nodes.html',
  22. '/panel/nodes/': '/nodes.html',
  23. };
  24. // Build a proxy config that suppresses ECONNREFUSED noise when the Go
  25. // backend isn't running locally. Real errors (timeouts, 5xx, etc.) still
  26. // surface in the Vite log.
  27. function makeBackendProxy(target, patterns) {
  28. const config = {};
  29. for (const pattern of patterns) {
  30. config[pattern] = {
  31. target,
  32. changeOrigin: true,
  33. // Returning a path from bypass tells Vite to serve that file from
  34. // its own dev server instead of forwarding the request — used here
  35. // to short-circuit /panel/<route> for pages we've already migrated.
  36. //
  37. // Only GETs get bypassed: the xray page reuses its page URL
  38. // (`POST /panel/xray/`) for data, so a method-blind bypass would
  39. // hand HTML back to fetch calls and break the page in dev.
  40. bypass(req) {
  41. if (req.method !== 'GET') return undefined;
  42. const url = req.url.split('?')[0];
  43. if (Object.prototype.hasOwnProperty.call(MIGRATED_ROUTES, url)) {
  44. return MIGRATED_ROUTES[url];
  45. }
  46. return undefined;
  47. },
  48. configure(proxy) {
  49. let warned = false;
  50. proxy.on('error', (err, req) => {
  51. // Node wraps connection failures in an AggregateError when DNS
  52. // returns multiple addresses (e.g. ::1 + 127.0.0.1) and all
  53. // refuse — the code lands on the inner errors, not the outer.
  54. const codes = new Set();
  55. if (err && err.code) codes.add(err.code);
  56. if (err && Array.isArray(err.errors)) {
  57. for (const inner of err.errors) {
  58. if (inner && inner.code) codes.add(inner.code);
  59. }
  60. }
  61. const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET');
  62. if (offline) {
  63. // Print a single friendly hint the first time, then stay quiet.
  64. if (!warned) {
  65. warned = true;
  66. // eslint-disable-next-line no-console
  67. console.warn(
  68. `[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`,
  69. );
  70. }
  71. return;
  72. }
  73. // eslint-disable-next-line no-console
  74. console.error('[proxy]', err);
  75. });
  76. },
  77. };
  78. }
  79. return config;
  80. }
  81. export default defineConfig({
  82. plugins: [vue()],
  83. resolve: {
  84. alias: {
  85. '@': path.resolve(__dirname, 'src'),
  86. },
  87. },
  88. build: {
  89. outDir,
  90. emptyOutDir: true,
  91. sourcemap: true,
  92. target: 'es2020',
  93. // ant-design-vue is intentionally bundled as one chunk (its
  94. // components share internals — splitting it breaks Modal/Form/
  95. // Select interop). Minified it lands ~1.4MB but gzips to ~410kB,
  96. // so the actual transfer is fine and caches across every page.
  97. // Bump the warning past that ceiling so the build stays quiet.
  98. chunkSizeWarningLimit: 1500,
  99. // Multiple HTML entries — one per legacy page we migrate.
  100. // As pages get ported in later phases, add their entrypoints here.
  101. rollupOptions: {
  102. input: {
  103. index: path.resolve(__dirname, 'index.html'),
  104. login: path.resolve(__dirname, 'login.html'),
  105. settings: path.resolve(__dirname, 'settings.html'),
  106. inbounds: path.resolve(__dirname, 'inbounds.html'),
  107. xray: path.resolve(__dirname, 'xray.html'),
  108. nodes: path.resolve(__dirname, 'nodes.html'),
  109. subpage: path.resolve(__dirname, 'subpage.html'),
  110. },
  111. output: {
  112. // Split vendor deps into stable chunks so each page only pulls
  113. // what it needs and the browser caches them across versions.
  114. // Without this, ant-design-vue + vue + icons all end up in one
  115. // 1.6MB blob attached to whichever page consumed them first.
  116. manualChunks(id) {
  117. if (!id.includes('node_modules')) return undefined;
  118. if (id.includes('ant-design-vue')) return 'vendor-antd';
  119. if (id.includes('@ant-design/icons-vue')) return 'vendor-icons';
  120. if (id.includes('vue-i18n')) return 'vendor-i18n';
  121. if (
  122. id.includes('/node_modules/vue/')
  123. || id.includes('/node_modules/@vue/')
  124. ) return 'vendor-vue';
  125. if (id.includes('dayjs')) return 'vendor-dayjs';
  126. if (id.includes('qrious')) return 'vendor-qrious';
  127. if (id.includes('axios')) return 'vendor-axios';
  128. // The persian datepicker pulls in moment + moment-jalaali; bundle
  129. // the trio together so unrelated pages don't pay the cost.
  130. if (
  131. id.includes('vue3-persian-datetime-picker')
  132. || id.includes('moment-jalaali')
  133. || id.includes('jalaali-js')
  134. || id.includes('/node_modules/moment/')
  135. ) return 'vendor-jalali';
  136. return 'vendor';
  137. },
  138. },
  139. },
  140. },
  141. server: {
  142. port: 5173,
  143. strictPort: true,
  144. proxy: {
  145. ...makeBackendProxy('http://localhost:2053', [
  146. // Patterns are anchored regex so /login.html and /index.html
  147. // (which Vite serves itself) are NOT forwarded — only the bare
  148. // backend paths and their sub-routes.
  149. '^/(login|logout|getTwoFactorEnable|csrf-token)$',
  150. '^/(panel|server)(/|$)',
  151. ]),
  152. // The panel mounts the live-update WebSocket at /ws (basePath +
  153. // "/ws"). Vite needs `ws: true` to forward the HTTP Upgrade to the
  154. // Go backend; without it the dev server would 404 the upgrade and
  155. // the page falls back to the no-data state.
  156. '/ws': {
  157. target: 'ws://localhost:2053',
  158. ws: true,
  159. changeOrigin: true,
  160. },
  161. },
  162. },
  163. });