ai-coding
2026-08-19 2fec9b032fb2aa4e600ba0c63ab4b974dd191e14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import type { ProxyOptions, UserConfig } from 'vite'
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
 
/** 服务配置接口 */
interface ServiceConfig {
  [key: string]: string
}
 
/** 服务环境类型 */
type ServiceEnvType = string
 
/** 完整的服务配置类型 */
interface FullServiceConfig {
  [key: ServiceEnvType]: ServiceConfig
}
 
/** 代理项接口 */
interface ProxyItem {
  /** 代理路径 */
  path: string
  /** 原始地址 */
  rawPath: string
}
 
/** 代理地址映射接口 */
interface ProxyMapping {
  [serviceName: string]: ProxyItem
}
 
/** 插件选项接口 */
export interface ServiceProxyPluginOptions {
  /** 服务配置对象(必填) */
  serviceConfig: FullServiceConfig
  /** 代理路径前缀(可选,默认为 'proxy-') */
  proxyPrefix?: string
  /** 是否启用代理配置 */
  enableProxy?: boolean
  /** 环境变量名(可选,默认为 '__URL_MAP__') */
  envName?: string
  /** d.ts 类型文件生成路径(可选,如果传入路径则在该路径生成 d.ts 类型文件) */
  dts?: string
}
 
export default function createServiceProxyPlugin(options: ServiceProxyPluginOptions) {
  const {
    serviceConfig,
    proxyPrefix = 'proxy-',
    enableProxy = true,
    envName = '__URL_MAP__',
    dts,
  } = options
 
  return {
    name: 'vite-auto-proxy',
    config(config: UserConfig, { mode, command }: { mode: string, command: 'build' | 'serve' }) {
      // 只在开发环境(serve命令)时生成代理配置
      const isDev = command === 'serve'
 
      // 在非开发环境也注入空的代理映射,避免运行时错误
      if (!config.define) {
        config.define = {}
      }
 
      if (!enableProxy || !isDev) {
        const rawMapping: ProxyMapping = {}
        const envConfig = serviceConfig[mode]
 
        if (envConfig) {
          Object.entries(envConfig).forEach(([serviceName, serviceUrl]) => {
            rawMapping[serviceName] = {
              path: serviceUrl,
              rawPath: serviceUrl,
            }
          })
          console.warn(`[auto-proxy] 已加载 ${Object.keys(envConfig).length} 个服务地址`)
        }
        else {
          console.warn(`[auto-proxy] 未找到环境 "${mode}" 的配置`)
        }
 
        config.define[envName] = JSON.stringify(rawMapping)
 
        // 生成 d.ts 类型文件(如果指定了路径)
        if (dts) {
          generateDtsFile(rawMapping, dts, envName)
        }
        return
      }
 
      console.warn(`[auto-proxy] 已加载${mode}模式 ${Object.keys(serviceConfig[mode]).length} 个服务地址`)
 
      const { proxyConfig, proxyMapping } = generateProxyFromServiceConfig(serviceConfig, mode, proxyPrefix)
 
      Object.entries(proxyMapping).forEach(([serviceName, proxyItem]) => {
        console.warn(`[auto-proxy] 服务: ${serviceName} | 代理地址: ${proxyItem.path} | 实际地址: ${proxyItem.rawPath}`)
      })
 
      if (proxyConfig && Object.keys(proxyConfig).length > 0) {
        // 确保 server 对象存在
        if (!config.server) {
          config.server = {}
        }
 
        // 合并代理配置
        config.server.proxy = {
          ...config.server.proxy,
          ...proxyConfig,
        }
        config.define[envName] = JSON.stringify(proxyMapping)
        console.warn(`[auto-proxy] 代理映射已注入到 ${envName}`)
 
        // 生成 d.ts 类型文件(如果指定了路径)
        if (dts) {
          generateDtsFile(proxyMapping, dts, envName)
        }
      }
      else {
        console.warn(`[auto-proxy] 未生成任何代理配置`)
        config.define[envName] = JSON.stringify({})
 
        // 生成空的 d.ts 类型文件(如果指定了路径)
        if (dts) {
          generateDtsFile({}, dts, envName)
        }
      }
    },
  }
}
 
function generateProxyFromServiceConfig(
  serviceConfig: FullServiceConfig,
  mode: ServiceEnvType,
  proxyPrefix: string,
): { proxyConfig: Record<string, ProxyOptions>, proxyMapping: ProxyMapping } {
  try {
    // 获取当前环境的配置
    const envConfig = serviceConfig[mode]
    if (!envConfig) {
      console.warn(`[auto-proxy] 未找到环境 "${mode}" 的配置,使用 development 配置`)
      const defaultConfig = serviceConfig.development
      if (!defaultConfig) {
        console.error(`[auto-proxy] 也未找到 development 配置`)
        return { proxyConfig: {}, proxyMapping: {} }
      }
      return generateProxyFromConfig(defaultConfig, proxyPrefix)
    }
 
    return generateProxyFromConfig(envConfig, proxyPrefix)
  }
  catch (error) {
    console.error(`[auto-proxy] 生成代理配置失败:`, (error as Error).message)
    return { proxyConfig: {}, proxyMapping: {} }
  }
}
 
function generateProxyFromConfig(
  envConfig: ServiceConfig,
  proxyPrefix: string,
): { proxyConfig: Record<string, ProxyOptions>, proxyMapping: ProxyMapping } {
  const proxyConfig: Record<string, ProxyOptions> = {}
  const proxyMapping: ProxyMapping = {}
 
  Object.entries(envConfig).forEach(([serviceName, serviceUrl]) => {
    if (typeof serviceUrl === 'string' && serviceUrl.trim()) {
      const proxyPath = `/${proxyPrefix}${serviceName}`
 
      const isWs = serviceUrl.startsWith('ws://') || serviceUrl.startsWith('wss://')
      // 生成代理配置
      proxyConfig[proxyPath] = {
        target: serviceUrl,
        changeOrigin: true,
        ws: isWs,
        rewrite: (path: string): string => path.replace(new RegExp(`^/${proxyPrefix}${serviceName}`), ''),
      }
 
      // 生成代理映射
      proxyMapping[serviceName] = {
        path: proxyPath,
        rawPath: serviceUrl,
      }
    }
  })
 
  return { proxyConfig, proxyMapping }
}
 
function generateDtsFile(
  mapping: ProxyMapping,
  outputPath: string,
  envName: string,
) {
  try {
    const serviceNames = Object.keys(mapping).map(name => `'${name}'`).join(' | ')
    const serviceNameType = serviceNames || 'never'
 
    const dtsContent = `/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by auto-proxy
// biome-ignore lint: disable
export {}
 
type serviceName = ${serviceNameType}
 
declare global {
  const ${envName}: {
    [K in serviceName]: {
      path: string
      rawPath: string
    }
  }
}
`
 
    const dir = dirname(outputPath)
    if (dir) {
      mkdirSync(dir, { recursive: true })
    }
    writeFileSync(outputPath, dtsContent, 'utf-8')
  }
  catch (error) {
    console.error(`[auto-proxy] 生成 d.ts 文件失败:`, (error as Error).message)
  }
}