@ranger1/dx 0.1.52 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/commands/deploy.js +5 -1
- package/lib/vercel-deploy.js +261 -80
- package/package.json +1 -1
|
@@ -101,5 +101,9 @@ export async function handleDeploy(cli, args) {
|
|
|
101
101
|
? parseTelegramWebhookFlags(cli.args)
|
|
102
102
|
: null
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
const strictContext = process.env.DX_VERCEL_STRICT_CONTEXT != null
|
|
105
|
+
? !['0', 'false', 'no'].includes(String(process.env.DX_VERCEL_STRICT_CONTEXT).toLowerCase())
|
|
106
|
+
: true
|
|
107
|
+
|
|
108
|
+
await deployToVercel(normalizedTarget, { environment, telegramWebhook, strictContext })
|
|
105
109
|
}
|
package/lib/vercel-deploy.js
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { execSync, spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
|
2
3
|
import { join } from 'node:path'
|
|
3
4
|
import { envManager } from './env.js'
|
|
4
5
|
import { logger } from './logger.js'
|
|
5
6
|
|
|
6
7
|
const ALLOWED_ENVIRONMENTS = ['development', 'staging', 'production']
|
|
8
|
+
const VALID_TARGETS = ['front', 'admin', 'telegram-bot']
|
|
9
|
+
|
|
10
|
+
const TARGET_CONFIGS = {
|
|
11
|
+
front: {
|
|
12
|
+
configFile: 'vercel.front.json',
|
|
13
|
+
projectIdEnvVar: 'VERCEL_PROJECT_ID_FRONT',
|
|
14
|
+
},
|
|
15
|
+
admin: {
|
|
16
|
+
configFile: 'vercel.admin.json',
|
|
17
|
+
projectIdEnvVar: 'VERCEL_PROJECT_ID_ADMIN',
|
|
18
|
+
},
|
|
19
|
+
'telegram-bot': {
|
|
20
|
+
configFile: 'vercel.telegram-bot.json',
|
|
21
|
+
projectIdEnvVar: 'VERCEL_PROJECT_ID_TELEGRAM_BOT',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EXPLICIT_ENV_VARS = ['APP_ENV', 'NODE_ENV']
|
|
26
|
+
const PUBLIC_ENV_PREFIXES = ['NEXT_PUBLIC_', 'VITE_']
|
|
27
|
+
|
|
28
|
+
const APP_ENV_MAP = {
|
|
29
|
+
development: 'dev',
|
|
30
|
+
staging: 'staging',
|
|
31
|
+
production: 'prod',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VERCEL_PROJECT_LINK_PATH = '.vercel/project.json'
|
|
35
|
+
|
|
36
|
+
function getTargetConfig(target) {
|
|
37
|
+
return TARGET_CONFIGS[target]
|
|
38
|
+
}
|
|
7
39
|
|
|
8
40
|
function collectErrorText(err) {
|
|
9
41
|
const parts = []
|
|
@@ -22,6 +54,122 @@ function isMissingFilesError(err) {
|
|
|
22
54
|
)
|
|
23
55
|
}
|
|
24
56
|
|
|
57
|
+
function listMissingVarKeys(targetConfigs, token, orgId) {
|
|
58
|
+
const missing = []
|
|
59
|
+
|
|
60
|
+
if (!token || envManager.isPlaceholderEnvValue(token)) {
|
|
61
|
+
missing.push('VERCEL_TOKEN')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!orgId || envManager.isPlaceholderEnvValue(orgId)) {
|
|
65
|
+
missing.push('VERCEL_ORG_ID')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const config of targetConfigs) {
|
|
69
|
+
const projectId = process.env[config.projectIdEnvVar]
|
|
70
|
+
if (!projectId || envManager.isPlaceholderEnvValue(projectId)) {
|
|
71
|
+
missing.push(config.projectIdEnvVar)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [...new Set(missing)]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listMissingConfigs(targetConfigs, projectRoot) {
|
|
79
|
+
const missing = []
|
|
80
|
+
|
|
81
|
+
for (const config of targetConfigs) {
|
|
82
|
+
const configPath = join(projectRoot, config.configFile)
|
|
83
|
+
if (!existsSync(configPath)) {
|
|
84
|
+
missing.push(config.configFile)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return missing
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function appendTargetArgs(baseArgs, { cwd, orgId, projectId, explicitEnvArgs = [] }) {
|
|
92
|
+
const args = [...baseArgs]
|
|
93
|
+
|
|
94
|
+
if (explicitEnvArgs.length > 0) {
|
|
95
|
+
args.push(...explicitEnvArgs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (cwd) {
|
|
99
|
+
args.push('--cwd', cwd)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (orgId) {
|
|
103
|
+
args.push('--scope', orgId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (projectId) {
|
|
107
|
+
args.push('--project', projectId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return args
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectExplicitVercelEnvArgs(sourceEnv = {}) {
|
|
114
|
+
const includeKeys = new Set(EXPLICIT_ENV_VARS)
|
|
115
|
+
|
|
116
|
+
Object.keys(sourceEnv).forEach(name => {
|
|
117
|
+
if (PUBLIC_ENV_PREFIXES.some(prefix => name.startsWith(prefix))) {
|
|
118
|
+
includeKeys.add(name)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return [...includeKeys]
|
|
123
|
+
.filter(name => {
|
|
124
|
+
const value = sourceEnv[name]
|
|
125
|
+
return !(
|
|
126
|
+
value === undefined ||
|
|
127
|
+
value === null ||
|
|
128
|
+
envManager.isPlaceholderEnvValue(value)
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
.sort()
|
|
132
|
+
.flatMap(name => ['--env', `${name}=${String(sourceEnv[name])}`])
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function maskIdentifier(value) {
|
|
136
|
+
const raw = String(value || '').trim()
|
|
137
|
+
if (raw.length <= 10) return raw || '-'
|
|
138
|
+
return `${raw.slice(0, 6)}...${raw.slice(-4)}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readLinkedProjectContext(projectRoot) {
|
|
142
|
+
const path = join(projectRoot, VERCEL_PROJECT_LINK_PATH)
|
|
143
|
+
if (!existsSync(path)) {
|
|
144
|
+
return { exists: false, path, orgId: null, projectId: null, parseError: null }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const content = readFileSync(path, 'utf8')
|
|
149
|
+
const parsed = JSON.parse(content)
|
|
150
|
+
return {
|
|
151
|
+
exists: true,
|
|
152
|
+
path,
|
|
153
|
+
orgId: parsed?.orgId || null,
|
|
154
|
+
projectId: parsed?.projectId || null,
|
|
155
|
+
parseError: null,
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
exists: true,
|
|
160
|
+
path,
|
|
161
|
+
orgId: null,
|
|
162
|
+
projectId: null,
|
|
163
|
+
parseError: error,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function clearLinkedProjectContext(projectRoot) {
|
|
169
|
+
const path = join(projectRoot, VERCEL_PROJECT_LINK_PATH)
|
|
170
|
+
rmSync(path, { force: true })
|
|
171
|
+
}
|
|
172
|
+
|
|
25
173
|
async function runVercel(args, options = {}) {
|
|
26
174
|
const { env, cwd } = options
|
|
27
175
|
const MAX_CAPTURE = 20000
|
|
@@ -101,7 +249,12 @@ export async function deployPrebuiltWithFallback(options) {
|
|
|
101
249
|
}
|
|
102
250
|
|
|
103
251
|
export async function deployToVercel(target, options = {}) {
|
|
104
|
-
const {
|
|
252
|
+
const {
|
|
253
|
+
environment = 'staging',
|
|
254
|
+
telegramWebhook = null,
|
|
255
|
+
strictContext = true,
|
|
256
|
+
run = runVercel,
|
|
257
|
+
} = options
|
|
105
258
|
|
|
106
259
|
// 校验环境参数
|
|
107
260
|
if (!ALLOWED_ENVIRONMENTS.includes(environment)) {
|
|
@@ -111,18 +264,13 @@ export async function deployToVercel(target, options = {}) {
|
|
|
111
264
|
return
|
|
112
265
|
}
|
|
113
266
|
|
|
114
|
-
const token = process.env.VERCEL_TOKEN
|
|
115
|
-
const orgId = process.env.VERCEL_ORG_ID
|
|
116
|
-
const projectIdFront = process.env.VERCEL_PROJECT_ID_FRONT
|
|
117
|
-
const projectIdAdmin = process.env.VERCEL_PROJECT_ID_ADMIN
|
|
118
|
-
const projectIdTelegramBot = process.env.VERCEL_PROJECT_ID_TELEGRAM_BOT
|
|
119
|
-
|
|
120
267
|
const normalizedTarget = String(target || '').toLowerCase()
|
|
121
268
|
const targets = normalizedTarget === 'all' ? ['front', 'admin'] : [normalizedTarget]
|
|
269
|
+
const projectRoot = process.cwd()
|
|
122
270
|
|
|
123
271
|
// 校验目标有效性
|
|
124
272
|
for (const t of targets) {
|
|
125
|
-
if (!
|
|
273
|
+
if (!VALID_TARGETS.includes(t)) {
|
|
126
274
|
logger.error(`不支持的部署目标: ${t}`)
|
|
127
275
|
logger.info('可用目标: front, admin, telegram-bot, all')
|
|
128
276
|
process.exitCode = 1
|
|
@@ -130,47 +278,43 @@ export async function deployToVercel(target, options = {}) {
|
|
|
130
278
|
}
|
|
131
279
|
}
|
|
132
280
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
if (!token || envManager.isPlaceholderEnvValue(token)) {
|
|
137
|
-
missingVars.push('VERCEL_TOKEN')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!orgId || envManager.isPlaceholderEnvValue(orgId)) {
|
|
141
|
-
missingVars.push('VERCEL_ORG_ID')
|
|
142
|
-
}
|
|
281
|
+
const targetConfigs = targets.map(getTargetConfig)
|
|
282
|
+
const token = process.env.VERCEL_TOKEN
|
|
283
|
+
const orgId = process.env.VERCEL_ORG_ID
|
|
143
284
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
missingVars.push('VERCEL_PROJECT_ID_FRONT')
|
|
147
|
-
}
|
|
285
|
+
const missingVars = listMissingVarKeys(targetConfigs, token, orgId)
|
|
286
|
+
const missingConfigFiles = listMissingConfigs(targetConfigs, projectRoot)
|
|
148
287
|
|
|
149
|
-
if (
|
|
150
|
-
missingVars.
|
|
151
|
-
|
|
288
|
+
if (missingVars.length > 0 || missingConfigFiles.length > 0) {
|
|
289
|
+
if (missingVars.length > 0) {
|
|
290
|
+
logger.error('缺少以下 Vercel 环境变量:')
|
|
291
|
+
missingVars.forEach(v => {
|
|
292
|
+
logger.error(` - ${v}`)
|
|
293
|
+
})
|
|
294
|
+
logger.info('')
|
|
295
|
+
logger.info('请在 .env.<env>.local 中配置这些变量,例如:')
|
|
296
|
+
logger.info(' VERCEL_TOKEN=<your-vercel-token>')
|
|
297
|
+
logger.info(' VERCEL_ORG_ID=team_xxx')
|
|
298
|
+
logger.info(' VERCEL_PROJECT_ID_FRONT=prj_xxx')
|
|
299
|
+
logger.info(' VERCEL_PROJECT_ID_ADMIN=prj_xxx')
|
|
300
|
+
logger.info(' VERCEL_PROJECT_ID_TELEGRAM_BOT=prj_xxx')
|
|
301
|
+
logger.info('')
|
|
302
|
+
logger.info('获取方式:')
|
|
303
|
+
logger.info(' 1. VERCEL_TOKEN: vercel login 后查看 ~/Library/Application Support/com.vercel.cli/auth.json')
|
|
304
|
+
logger.info(' 2. PROJECT_ID: vercel project ls --scope <org> 或通过 Vercel Dashboard 获取')
|
|
305
|
+
logger.info('')
|
|
306
|
+
logger.info('提示:部署命令会显式校验 --scope 与 --project,避免环境漂移。')
|
|
307
|
+
}
|
|
152
308
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
309
|
+
if (missingConfigFiles.length > 0) {
|
|
310
|
+
logger.error('缺少以下 Vercel 配置文件:')
|
|
311
|
+
missingConfigFiles.forEach(name => {
|
|
312
|
+
logger.error(` - ${name}`)
|
|
313
|
+
})
|
|
314
|
+
logger.info('')
|
|
315
|
+
logger.info('请确认同级目录存在对应的 vercel.*.json 文件。')
|
|
316
|
+
}
|
|
156
317
|
|
|
157
|
-
// 如果有缺失变量,统一报错并退出
|
|
158
|
-
if (missingVars.length > 0) {
|
|
159
|
-
logger.error('缺少以下 Vercel 环境变量:')
|
|
160
|
-
missingVars.forEach(v => {
|
|
161
|
-
logger.error(` - ${v}`)
|
|
162
|
-
})
|
|
163
|
-
logger.info('')
|
|
164
|
-
logger.info('请在 .env.<env>.local 中配置这些变量,例如:')
|
|
165
|
-
logger.info(' VERCEL_TOKEN=<your-vercel-token>')
|
|
166
|
-
logger.info(' VERCEL_ORG_ID=team_xxx')
|
|
167
|
-
logger.info(' VERCEL_PROJECT_ID_FRONT=prj_xxx')
|
|
168
|
-
logger.info(' VERCEL_PROJECT_ID_ADMIN=prj_xxx')
|
|
169
|
-
logger.info(' VERCEL_PROJECT_ID_TELEGRAM_BOT=prj_xxx')
|
|
170
|
-
logger.info('')
|
|
171
|
-
logger.info('获取方式:')
|
|
172
|
-
logger.info(' 1. VERCEL_TOKEN: vercel login 后查看 ~/Library/Application Support/com.vercel.cli/auth.json')
|
|
173
|
-
logger.info(' 2. PROJECT_ID: vercel project ls --scope <org> 或通过 Vercel Dashboard 获取')
|
|
174
318
|
process.exitCode = 1
|
|
175
319
|
return
|
|
176
320
|
}
|
|
@@ -180,40 +324,64 @@ export async function deployToVercel(target, options = {}) {
|
|
|
180
324
|
// - 这样 dx deploy 能兼容不同 monorepo 布局(不强依赖 apps/sdk 等目录)。
|
|
181
325
|
|
|
182
326
|
// 映射环境标识:development -> dev, staging -> staging, production -> prod
|
|
183
|
-
const
|
|
184
|
-
development: 'dev',
|
|
185
|
-
staging: 'staging',
|
|
186
|
-
production: 'prod',
|
|
187
|
-
}
|
|
188
|
-
const buildEnv = envMap[environment]
|
|
327
|
+
const buildEnv = APP_ENV_MAP[environment]
|
|
189
328
|
|
|
190
329
|
for (const t of targets) {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
330
|
+
const targetConfig = getTargetConfig(t)
|
|
331
|
+
const projectId = process.env[targetConfig.projectIdEnvVar]
|
|
332
|
+
const configFile = targetConfig.configFile
|
|
333
|
+
const configPath = join(projectRoot, configFile)
|
|
334
|
+
const linkedContext = readLinkedProjectContext(projectRoot)
|
|
335
|
+
|
|
336
|
+
const linkedMismatch =
|
|
337
|
+
linkedContext.exists &&
|
|
338
|
+
linkedContext.parseError == null &&
|
|
339
|
+
((linkedContext.orgId && linkedContext.orgId !== orgId) ||
|
|
340
|
+
(linkedContext.projectId && linkedContext.projectId !== projectId))
|
|
341
|
+
|
|
342
|
+
if (linkedContext.exists && linkedContext.parseError) {
|
|
343
|
+
logger.warn(
|
|
344
|
+
`检测到 ${VERCEL_PROJECT_LINK_PATH} 但解析失败: ${linkedContext.parseError.message}`,
|
|
345
|
+
)
|
|
346
|
+
if (strictContext) {
|
|
347
|
+
logger.error('strictContext 已开启,已阻止继续部署以避免回退污染')
|
|
348
|
+
process.exitCode = 1
|
|
349
|
+
return
|
|
350
|
+
}
|
|
196
351
|
}
|
|
197
352
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
353
|
+
if (linkedMismatch) {
|
|
354
|
+
logger.error('检测到 .vercel 链接冲突')
|
|
355
|
+
logger.error(` 当前目标: org=${maskIdentifier(orgId)} project=${maskIdentifier(projectId)}`)
|
|
356
|
+
logger.error(
|
|
357
|
+
` 本地链接: org=${maskIdentifier(linkedContext.orgId)} project=${maskIdentifier(linkedContext.projectId)}`,
|
|
358
|
+
)
|
|
359
|
+
if (strictContext) {
|
|
360
|
+
logger.error('strictContext 已开启,已阻止部署(请清理 .vercel 或修正环境变量)')
|
|
361
|
+
process.exitCode = 1
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
logger.warn('strictContext 已关闭,继续执行(可能存在误部署风险)')
|
|
203
365
|
}
|
|
204
366
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
367
|
+
logger.info(
|
|
368
|
+
`[deploy-context] env=${environment} target=${t} strict=${strictContext ? 1 : 0} org=${maskIdentifier(orgId)} project=${maskIdentifier(projectId)} linked=${linkedContext.exists ? 'yes' : 'no'} token=env`,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
const explicitEnvArgs = collectExplicitVercelEnvArgs({
|
|
372
|
+
...process.env,
|
|
373
|
+
APP_ENV: buildEnv,
|
|
374
|
+
NODE_ENV: envManager.mapAppEnvToNodeEnv(environment),
|
|
375
|
+
VERCEL_ORG_ID: orgId,
|
|
376
|
+
VERCEL_PROJECT_ID: projectId,
|
|
377
|
+
})
|
|
208
378
|
|
|
209
379
|
const envVars = {
|
|
210
380
|
...process.env,
|
|
211
381
|
VERCEL_PROJECT_ID: projectId,
|
|
212
382
|
APP_ENV: buildEnv,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (orgId) {
|
|
216
|
-
envVars.VERCEL_ORG_ID = orgId
|
|
383
|
+
NODE_ENV: envManager.mapAppEnvToNodeEnv(environment),
|
|
384
|
+
VERCEL_ORG_ID: orgId,
|
|
217
385
|
}
|
|
218
386
|
|
|
219
387
|
// 不通过 CLI args 传递 token,避免出现在错误信息/日志中
|
|
@@ -235,39 +403,52 @@ export async function deployToVercel(target, options = {}) {
|
|
|
235
403
|
}
|
|
236
404
|
|
|
237
405
|
try {
|
|
406
|
+
if (strictContext && process.env.DX_VERCEL_KEEP_LINK !== '1') {
|
|
407
|
+
clearLinkedProjectContext(projectRoot)
|
|
408
|
+
}
|
|
409
|
+
|
|
238
410
|
// 第一步:本地构建
|
|
239
411
|
logger.step(`本地构建 ${t} (${environment})`)
|
|
240
|
-
const buildArgs =
|
|
412
|
+
const buildArgs = appendTargetArgs(
|
|
413
|
+
['build', '--local-config', configPath, '--yes'],
|
|
414
|
+
{
|
|
415
|
+
cwd: projectRoot,
|
|
416
|
+
orgId,
|
|
417
|
+
projectId,
|
|
418
|
+
explicitEnvArgs,
|
|
419
|
+
},
|
|
420
|
+
)
|
|
241
421
|
|
|
242
422
|
// staging 和 production 环境需要 --prod 标志,确保构建产物与部署环境匹配
|
|
243
423
|
if (environment === 'staging' || environment === 'production') {
|
|
244
424
|
buildArgs.push('--prod')
|
|
245
425
|
}
|
|
246
426
|
|
|
247
|
-
|
|
248
|
-
buildArgs.push('--scope', orgId)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
await runVercel(buildArgs, { env: envVars, cwd: process.cwd() })
|
|
427
|
+
await run(buildArgs, { env: envVars, cwd: projectRoot })
|
|
252
428
|
logger.success(`${t} 本地构建成功`)
|
|
253
429
|
|
|
254
430
|
// 第二步:上传预构建产物
|
|
255
431
|
logger.step(`部署 ${t} 到 Vercel (${environment})`)
|
|
256
|
-
const baseDeployArgs =
|
|
432
|
+
const baseDeployArgs = appendTargetArgs(
|
|
433
|
+
['deploy', '--prebuilt', '--local-config', configPath, '--yes'],
|
|
434
|
+
{
|
|
435
|
+
cwd: projectRoot,
|
|
436
|
+
orgId,
|
|
437
|
+
projectId,
|
|
438
|
+
explicitEnvArgs,
|
|
439
|
+
},
|
|
440
|
+
)
|
|
257
441
|
|
|
258
442
|
// staging 和 production 环境都添加 --prod 标志以绑定固定域名
|
|
259
443
|
if (environment === 'staging' || environment === 'production') {
|
|
260
444
|
baseDeployArgs.push('--prod')
|
|
261
445
|
}
|
|
262
446
|
|
|
263
|
-
if (orgId) {
|
|
264
|
-
baseDeployArgs.push('--scope', orgId)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
447
|
const deployResult = await deployPrebuiltWithFallback({
|
|
268
448
|
baseArgs: baseDeployArgs,
|
|
269
449
|
env: envVars,
|
|
270
|
-
cwd:
|
|
450
|
+
cwd: projectRoot,
|
|
451
|
+
run,
|
|
271
452
|
})
|
|
272
453
|
|
|
273
454
|
const deployOutput = [deployResult?.result?.stdout, deployResult?.result?.stderr]
|