cockpitEnvPlugin.js 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import { existsSync } from 'node:fs'
  2. import { readFile } from 'node:fs/promises'
  3. import { resolve } from 'node:path'
  4. /** 生产构建只替换连接配置,保留运行时 JSON 中的 GIS/模型参数。 */
  5. export function productionRuntimeConfig(raw, env) {
  6. const keys = ['VITE_COCKPIT_DATA_SOURCE', 'VITE_COCKPIT_API_BASE_URL', 'VITE_COCKPIT_WS_URL']
  7. for (const key of keys) {
  8. if (typeof env[key] !== 'string' || !env[key].trim()) throw new Error('缺少生产环境变量:' + key)
  9. }
  10. const imageBase = typeof env.VITE_APP_MINIO_BASE_URL === 'string' && env.VITE_APP_MINIO_BASE_URL.trim()
  11. ? env.VITE_APP_MINIO_BASE_URL.trim() : '/minio/pipe/'
  12. try {
  13. const imageUrl = new URL(imageBase, 'http://runtime-image-config.invalid/')
  14. const publicPath = /^https?:\/\//i.test(imageBase) || (imageBase.startsWith('/') && !imageBase.startsWith('//'))
  15. if (!publicPath || !['http:', 'https:'].includes(imageUrl.protocol) || imageUrl.username || imageUrl.password || imageUrl.search || imageUrl.hash) throw new Error()
  16. } catch {
  17. throw new Error('生产图片访问基址必须是无凭据或签名参数的公开地址或同源路径')
  18. }
  19. return {
  20. ...raw,
  21. dataSource: env.VITE_COCKPIT_DATA_SOURCE,
  22. api: { ...raw.api, baseUrl: env.VITE_COCKPIT_API_BASE_URL },
  23. ws: { ...raw.ws, url: env.VITE_COCKPIT_WS_URL },
  24. images: { baseUrl: imageBase },
  25. }
  26. }
  27. /** 生产配置来自可复现的部署模板,绝不复制本机 public/cockpit/config.json。 */
  28. export default function cockpitEnvPlugin() {
  29. let config
  30. const readTemplate = async () => productionRuntimeConfig(
  31. JSON.parse(await readFile(resolve(config.root, 'deploy/cockpit.config.json'), 'utf8')),
  32. config.env,
  33. )
  34. return {
  35. name: 'cockpit-environment-config',
  36. configResolved(resolved) { config = resolved },
  37. configureServer(server) {
  38. // 干净检出也有完整地图配置;已有本机 JSON 时仍遵循原覆盖规则。
  39. server.middlewares.use('/cockpit/config.json', async (req, res, next) => {
  40. if (existsSync(resolve(config.publicDir, 'cockpit/config.json'))) return next()
  41. try {
  42. const raw = await readTemplate()
  43. res.setHeader('Content-Type', 'application/json; charset=utf-8')
  44. res.setHeader('Cache-Control', 'no-cache')
  45. res.end(JSON.stringify(raw))
  46. } catch (error) { next(error) }
  47. })
  48. },
  49. async generateBundle() {
  50. this.emitFile({
  51. type: 'asset',
  52. fileName: 'cockpit/config.json',
  53. source: JSON.stringify(await readTemplate(), null, 2) + '\n',
  54. })
  55. },
  56. }
  57. }