@ranger1/dx 0.1.0

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.
@@ -0,0 +1,664 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 后端部署包构建脚本
5
+ *
6
+ * 功能概览:
7
+ * 1. 根据当前环境构建 NestJS 后端 dist 产物
8
+ * 2. 收集 Prisma schema/migrations、运行时配置等运行所需文件
9
+ * 3. 安装并裁剪生产依赖,生成可直接部署的目录结构
10
+ * 4. 打包为 backend-<version>-<sha>.tar.gz,提供 bin/start.sh 启动脚本
11
+ */
12
+
13
+ import { tmpdir } from 'node:os'
14
+ import { mkdir, rm, writeFile, chmod, stat } from 'node:fs/promises'
15
+ import { existsSync, cpSync, readFileSync, readdirSync, statSync } from 'node:fs'
16
+ import { resolve, join, dirname, relative } from 'node:path'
17
+ import crypto from 'node:crypto'
18
+ import * as child_process from 'node:child_process'
19
+ import { parse as parseYaml } from 'yaml'
20
+ import { logger } from './logger.js'
21
+ import { execManager } from './exec.js'
22
+ import { envManager } from './env.js'
23
+
24
+ class BackendPackager {
25
+ constructor(options = {}) {
26
+ this.projectRoot = process.env.DX_PROJECT_ROOT || process.cwd()
27
+ this.backendRoot = join(this.projectRoot, 'apps/backend')
28
+ this.distRoot = join(this.projectRoot, 'dist/backend')
29
+ this.prismaSrc = join(this.backendRoot, 'prisma')
30
+ this.targetEnv = options.environment || process.env.APP_ENV || 'development'
31
+ this.skipBuild = Boolean(options.skipBuild)
32
+ this.disableCleanup = Boolean(options.keepWorkdir)
33
+ this.layerEnv = envManager.mapAppEnvToLayerEnv(this.targetEnv)
34
+ const { version, packageManager, pnpmConfig, nxVersion } = this.resolveRootMetadata()
35
+ this.repoVersion = version
36
+ this.packageManager = packageManager
37
+ this.rootPnpmConfig = pnpmConfig
38
+ this.rootNxVersion = nxVersion
39
+ this.gitSha = this.runGitCommand('git rev-parse HEAD') || 'unknown'
40
+ this.gitShortSha = this.gitSha === 'unknown' ? 'unknown' : this.gitSha.slice(0, 7)
41
+ this.buildTimestamp = new Date().toISOString()
42
+ this.envSlug = this.createEnvironmentSlug(this.targetEnv)
43
+ this.artifactBase = `backend-${this.repoVersion}-${this.envSlug}-${this.gitShortSha}`
44
+ this.artifactFile = `${this.artifactBase}.tar.gz`
45
+ this.tmpRoot = null
46
+ this.outputRoot = null
47
+ this.outputAppDir = null
48
+ this.nodeVersionConstraint = this.resolveNodeConstraint()
49
+ this.envSnapshot = {}
50
+ this.buildConfiguration = 'skipped'
51
+ this.pnpmVersion = this.resolvePnpmVersion()
52
+ this.workspacePackagesInfo = null
53
+ this.copiedWorkspacePackages = new Set()
54
+ }
55
+
56
+ resolveRootMetadata() {
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(join(this.projectRoot, 'package.json'), 'utf8'))
59
+ const version = typeof pkg.version === 'string' ? pkg.version : '0.0.0'
60
+ const packageManager = pkg.packageManager || 'pnpm'
61
+ const pnpmConfig = pkg.pnpm ? structuredClone(pkg.pnpm) : undefined
62
+ const nxVersion = pkg.devDependencies?.nx || pkg.dependencies?.nx || null
63
+ return { version, packageManager, pnpmConfig, nxVersion }
64
+ } catch (error) {
65
+ logger.warn(`读取根 package.json 失败: ${error.message}`)
66
+ return {
67
+ version: '0.0.0',
68
+ packageManager: 'pnpm',
69
+ pnpmConfig: undefined,
70
+ nxVersion: null,
71
+ }
72
+ }
73
+ }
74
+
75
+ resolvePnpmVersion() {
76
+ try {
77
+ return child_process
78
+ .execSync('pnpm --version', { cwd: this.projectRoot, encoding: 'utf8' })
79
+ .trim()
80
+ } catch (error) {
81
+ logger.warn(`无法获取 pnpm 版本信息: ${error.message}`)
82
+ return null
83
+ }
84
+ }
85
+
86
+ loadWorkspacePackageInfo() {
87
+ if (this.workspacePackagesInfo) return this.workspacePackagesInfo
88
+
89
+ const map = new Map()
90
+ try {
91
+ const workspaceFile = join(this.projectRoot, 'pnpm-workspace.yaml')
92
+ let patterns = ['apps/*', 'packages/*']
93
+ if (existsSync(workspaceFile)) {
94
+ const raw = readFileSync(workspaceFile, 'utf8')
95
+ const parsed = parseYaml(raw)
96
+ if (parsed?.packages && Array.isArray(parsed.packages)) patterns = parsed.packages
97
+ }
98
+
99
+ const collectFromDir = dir => {
100
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return
101
+ const entries = readdirSync(dir, { withFileTypes: true })
102
+ for (const entry of entries) {
103
+ if (!entry.isDirectory()) continue
104
+ const child = join(dir, entry.name)
105
+ const pkgPath = join(child, 'package.json')
106
+ if (!existsSync(pkgPath)) continue
107
+ try {
108
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
109
+ if (pkg?.name && pkg?.version) {
110
+ map.set(pkg.name, {
111
+ version: pkg.version,
112
+ path: relative(this.projectRoot, child),
113
+ })
114
+ }
115
+ } catch {}
116
+ }
117
+ }
118
+
119
+ patterns.forEach(pattern => {
120
+ if (!pattern) return
121
+ const normalized = pattern.replace(/\\/g, '/').trim()
122
+ if (normalized.endsWith('/*')) {
123
+ const base = normalized.slice(0, -2)
124
+ collectFromDir(join(this.projectRoot, base))
125
+ } else {
126
+ const absDir = join(this.projectRoot, normalized)
127
+ const pkgPath = join(absDir, 'package.json')
128
+ if (!existsSync(pkgPath)) return
129
+ try {
130
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
131
+ if (pkg?.name && pkg?.version) {
132
+ map.set(pkg.name, {
133
+ version: pkg.version,
134
+ path: relative(this.projectRoot, absDir),
135
+ })
136
+ }
137
+ } catch {}
138
+ }
139
+ })
140
+ } catch (error) {
141
+ logger.warn(`解析 pnpm workspace 失败: ${error.message}`)
142
+ }
143
+
144
+ this.workspacePackagesInfo = map
145
+ return map
146
+ }
147
+
148
+ createEnvironmentSlug(env) {
149
+ const raw = String(env || 'unknown').toLowerCase()
150
+ const normalized = raw
151
+ .replace(/[^a-z0-9_-]/g, '-')
152
+ .replace(/-{2,}/g, '-')
153
+ .replace(/^-|-$/g, '')
154
+ return normalized || 'unknown'
155
+ }
156
+
157
+ async copyWorkspacePackage(name, info) {
158
+ if (this.copiedWorkspacePackages.has(name)) return
159
+ this.copiedWorkspacePackages.add(name)
160
+
161
+ const sourceDir = join(this.projectRoot, info.path)
162
+ if (!existsSync(sourceDir)) {
163
+ logger.warn(`未找到 workspace 包路径: ${sourceDir}`)
164
+ return
165
+ }
166
+
167
+ try {
168
+ await execManager.executeCommand(`pnpm --filter ${name} build`, {
169
+ cwd: this.projectRoot,
170
+ skipEnvValidation: true,
171
+ })
172
+ } catch (error) {
173
+ logger.warn(`workspace 包 ${name} 构建失败: ${error.message}`)
174
+ }
175
+
176
+ const targetDir = join(this.outputAppDir, 'dist', info.path)
177
+ await mkdir(targetDir, { recursive: true })
178
+
179
+ const packageJsonPath = join(sourceDir, 'package.json')
180
+ if (existsSync(packageJsonPath)) {
181
+ cpSync(packageJsonPath, join(targetDir, 'package.json'))
182
+ }
183
+
184
+ const distDir = join(sourceDir, 'dist')
185
+ if (existsSync(distDir)) {
186
+ cpSync(distDir, join(targetDir, 'dist'), { recursive: true })
187
+ }
188
+ }
189
+
190
+ resolveNodeConstraint() {
191
+ try {
192
+ const pkg = JSON.parse(readFileSync(join(this.projectRoot, 'package.json'), 'utf8'))
193
+ const engines = pkg.engines || {}
194
+ const nodeConstraint = engines.node || '>=20.11.0'
195
+ return String(nodeConstraint)
196
+ } catch {
197
+ return '>=20.11.0'
198
+ }
199
+ }
200
+
201
+ runGitCommand(command) {
202
+ try {
203
+ return child_process.execSync(command, { cwd: this.projectRoot, encoding: 'utf8' }).trim()
204
+ } catch {
205
+ return null
206
+ }
207
+ }
208
+
209
+ async run() {
210
+ logger.step(`后端部署包构建 (${this.targetEnv})`)
211
+
212
+ try {
213
+ envManager.syncEnvironments(this.targetEnv)
214
+ await this.prepareEnvSnapshot()
215
+ await this.prepareWorkdir()
216
+ if (!this.skipBuild) await this.buildBackend()
217
+ await this.ensureDistArtifacts()
218
+ await this.stageRuntimeFiles()
219
+ await this.installProductionDependencies()
220
+ await this.writeManifest()
221
+ await this.createArchive()
222
+ logger.success(`部署包已生成: ${this.getArtifactPath()}`)
223
+ } catch (error) {
224
+ if (envManager.latestEnvWarnings?.length) {
225
+ envManager.latestEnvWarnings.forEach(message => logger.warn(message))
226
+ }
227
+ logger.error(`后端打包失败: ${error.message}`)
228
+ throw error
229
+ } finally {
230
+ await this.cleanup()
231
+ }
232
+ }
233
+
234
+ async prepareEnvSnapshot() {
235
+ logger.info('校验并快照环境变量')
236
+ const requiredVars = envManager.getRequiredEnvVars(this.layerEnv, 'backend')
237
+ const collected = envManager.collectEnvFromLayers('backend', this.layerEnv)
238
+ const effectiveEnv = { ...collected, ...process.env }
239
+ const { valid, missing, placeholders } = envManager.validateRequiredVars(
240
+ requiredVars,
241
+ effectiveEnv,
242
+ )
243
+ if (!valid) {
244
+ const problems = []
245
+ if (missing.length > 0) problems.push(`缺少必填环境变量: ${missing.join(', ')}`)
246
+ if (placeholders.length > 0) problems.push(`以下变量仍为占位值: ${placeholders.join(', ')}`)
247
+ const message = problems.length > 0 ? problems.join('\n') : '环境变量校验未通过'
248
+ throw new Error(message)
249
+ }
250
+ const snapshotKeys = new Set([
251
+ ...Object.keys(collected),
252
+ ...requiredVars,
253
+ 'APP_ENV',
254
+ 'NODE_ENV',
255
+ ])
256
+
257
+ const snapshot = {}
258
+ snapshotKeys.forEach(key => {
259
+ const value = effectiveEnv[key]
260
+ if (value !== undefined && value !== null) {
261
+ snapshot[key] = String(value)
262
+ }
263
+ })
264
+
265
+ snapshot.APP_ENV = this.targetEnv
266
+ snapshot.NODE_ENV = envManager.mapAppEnvToNodeEnv(this.targetEnv)
267
+
268
+ this.envSnapshot = this.stripUndefined(snapshot)
269
+ this.envSnapshot.APP_VERSION = this.repoVersion
270
+ this.envSnapshot.BUILD_GIT_SHA = this.gitSha
271
+ this.envSnapshot.BUILD_TIME = this.buildTimestamp
272
+ }
273
+
274
+ stripUndefined(record) {
275
+ const result = {}
276
+ for (const [key, value] of Object.entries(record)) {
277
+ if (value === undefined || value === null) continue
278
+ result[key] = String(value)
279
+ }
280
+ return result
281
+ }
282
+
283
+ async prepareWorkdir() {
284
+ const randomSuffix = crypto.randomBytes(6).toString('hex')
285
+ const tmpBase = join(tmpdir(), `backend-package-${randomSuffix}`)
286
+ await mkdir(tmpBase, { recursive: true })
287
+ this.tmpRoot = tmpBase
288
+ this.outputRoot = join(tmpBase, this.artifactBase)
289
+ this.outputAppDir = join(this.outputRoot, 'backend')
290
+ await mkdir(this.outputAppDir, { recursive: true })
291
+ }
292
+
293
+ async buildBackend() {
294
+ logger.step('构建后端产物')
295
+ const configuration = ['production', 'staging'].includes(this.targetEnv)
296
+ ? 'production'
297
+ : 'development'
298
+ this.buildConfiguration = configuration
299
+ await execManager.executeCommand(`npx nx build backend --configuration=${configuration}`, {
300
+ app: 'backend',
301
+ })
302
+ }
303
+
304
+ async ensureDistArtifacts() {
305
+ const expectedMain = join(this.distRoot, 'apps/backend/src/main.js')
306
+ try {
307
+ await stat(expectedMain)
308
+ } catch (error) {
309
+ throw new Error(
310
+ `缺少编译产物 ${relative(this.projectRoot, expectedMain)},请检查 build 步骤。`,
311
+ )
312
+ }
313
+ }
314
+
315
+ async stageRuntimeFiles() {
316
+ logger.step('收集运行所需文件')
317
+
318
+ // dist
319
+ await this.copyDistTree()
320
+
321
+ // prisma schema/migrations
322
+ if (existsSync(this.prismaSrc)) {
323
+ cpSync(this.prismaSrc, join(this.outputAppDir, 'prisma'), { recursive: true })
324
+ }
325
+
326
+ // package.json
327
+ const distPackagePath = join(this.distRoot, 'package.json')
328
+ const distPackage = JSON.parse(readFileSync(distPackagePath, 'utf8'))
329
+ const backendPackage = JSON.parse(readFileSync(join(this.backendRoot, 'package.json'), 'utf8'))
330
+
331
+ const runtimeDeps = { ...(distPackage.dependencies || {}) }
332
+ const workspacePackages = this.loadWorkspacePackageInfo()
333
+
334
+ for (const [name, version] of Object.entries(backendPackage.dependencies || {})) {
335
+ if (typeof version !== 'string') continue
336
+ if (version.startsWith('workspace:')) {
337
+ const info = workspacePackages.get(name)
338
+ if (info) {
339
+ await this.copyWorkspacePackage(name, info)
340
+ const relPath = join('dist', info.path).replace(/\\/g, '/')
341
+ runtimeDeps[name] = `file:./${relPath}`
342
+ } else {
343
+ logger.warn(`未能解析 workspace 依赖 ${name},请检查工作区配置`)
344
+ }
345
+ continue
346
+ }
347
+ runtimeDeps[name] = version
348
+ }
349
+ if (backendPackage.devDependencies?.prisma) {
350
+ runtimeDeps.prisma = backendPackage.devDependencies.prisma
351
+ }
352
+
353
+ const devOnlyPackages = new Set([
354
+ 'husky',
355
+ 'lint-staged',
356
+ '@nestjs/cli',
357
+ 'ts-node',
358
+ 'ts-jest',
359
+ 'supertest',
360
+ ])
361
+
362
+ const sanitizedDeps = Object.fromEntries(
363
+ Object.entries(runtimeDeps)
364
+ .filter(([name]) => !name.startsWith('@types/') && !devOnlyPackages.has(name))
365
+ .sort(([a], [b]) => a.localeCompare(b)),
366
+ )
367
+
368
+ const packageJson = {
369
+ name: backendPackage.name || distPackage.name || '@ai/backend',
370
+ version: this.repoVersion,
371
+ private: true,
372
+ type: 'commonjs',
373
+ dependencies: sanitizedDeps,
374
+ scripts: {
375
+ start: 'node dist/apps/backend/src/main.js',
376
+ 'prisma:migrate': 'prisma migrate deploy',
377
+ },
378
+ engines: { node: this.nodeVersionConstraint },
379
+ ...(this.rootPnpmConfig ? { pnpm: this.rootPnpmConfig } : {}),
380
+ }
381
+
382
+ await writeFile(
383
+ join(this.outputAppDir, 'package.json'),
384
+ `${JSON.stringify(packageJson, null, 2)}\n`,
385
+ 'utf8',
386
+ )
387
+
388
+ const rootLockfile = join(this.projectRoot, 'pnpm-lock.yaml')
389
+ if (existsSync(rootLockfile)) {
390
+ cpSync(rootLockfile, join(this.outputAppDir, 'pnpm-lock.yaml'))
391
+ }
392
+
393
+ await this.writeRuntimeEnv()
394
+ await this.writeStartScript()
395
+ await this.writeHealthcheckScript()
396
+ await this.writeDeployReadme()
397
+ }
398
+
399
+ async copyDistTree() {
400
+ const targetDist = join(this.outputAppDir, 'dist')
401
+ await mkdir(targetDist, { recursive: true })
402
+ cpSync(this.distRoot, targetDist, {
403
+ recursive: true,
404
+ filter: (source, destination) => {
405
+ const rel = relative(this.distRoot, source)
406
+ if (!rel || rel === '' || rel === 'package.json') return rel !== 'package.json'
407
+ return true
408
+ },
409
+ })
410
+ }
411
+
412
+ async writeRuntimeEnv() {
413
+ const configDir = join(this.outputAppDir, 'config')
414
+ await mkdir(configDir, { recursive: true })
415
+ const lines = Object.entries(this.envSnapshot)
416
+ .sort(([a], [b]) => a.localeCompare(b))
417
+ .map(([key, value]) => `${key}=${this.escapeEnvValue(value)}`)
418
+ await writeFile(join(configDir, '.env.runtime'), `${lines.join('\n')}\n`, 'utf8')
419
+ }
420
+
421
+ escapeEnvValue(value) {
422
+ if (value === '') return "''"
423
+ if (/[^\w\-./:@]/.test(value)) {
424
+ return `'${value.replace(/'/g, "'\\''")}'`
425
+ }
426
+ return value
427
+ }
428
+
429
+ async writeStartScript() {
430
+ const binDir = join(this.outputAppDir, 'bin')
431
+ await mkdir(binDir, { recursive: true })
432
+ const scriptPath = join(binDir, 'start.sh')
433
+ const script = `#!/usr/bin/env bash
434
+ set -euo pipefail
435
+
436
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
437
+ APP_ROOT="\${SCRIPT_DIR%/bin}"
438
+ ENV_FILE="$APP_ROOT/config/.env.runtime"
439
+ NODE_BIN="$(command -v node || true)"
440
+ REQUIRED_NODE_RAW="${this.nodeVersionConstraint}"
441
+
442
+ if [[ -z "$NODE_BIN" ]]; then
443
+ echo "❌ 未找到 node 命令,请先安装 Node.js (${this.nodeVersionConstraint})" >&2
444
+ exit 1
445
+ fi
446
+
447
+ trim_constraint() {
448
+ local raw="$1"
449
+ echo "\${raw#>=}"
450
+ }
451
+
452
+ version_ge() {
453
+ local current="$1"
454
+ local required="$2"
455
+ local IFS=.
456
+ read -r c1 c2 c3 <<<"\${current//v/}"
457
+ read -r r1 r2 r3 <<<"\${required}"
458
+ c2=\${c2:-0}; c3=\${c3:-0}
459
+ r2=\${r2:-0}; r3=\${r3:-0}
460
+ if (( c1 > r1 )); then return 0; fi
461
+ if (( c1 < r1 )); then return 1; fi
462
+ if (( c2 > r2 )); then return 0; fi
463
+ if (( c2 < r2 )); then return 1; fi
464
+ if (( c3 >= r3 )); then return 0; fi
465
+ return 1
466
+ }
467
+
468
+ CURRENT_NODE_VERSION="$(node -v 2>/dev/null || true)"
469
+ REQUIRED_NODE_VERSION="$(trim_constraint "$REQUIRED_NODE_RAW")"
470
+
471
+ if [[ -z "$CURRENT_NODE_VERSION" ]]; then
472
+ echo "❌ 无法检测到 Node.js 版本,请确认已正确安装" >&2
473
+ exit 1
474
+ fi
475
+
476
+ if ! version_ge "$CURRENT_NODE_VERSION" "$REQUIRED_NODE_VERSION"; then
477
+ echo "❌ 当前 Node.js 版本 $CURRENT_NODE_VERSION 不满足要求 (>= $REQUIRED_NODE_VERSION)" >&2
478
+ exit 1
479
+ fi
480
+
481
+ if [[ ! -f "$ENV_FILE" ]]; then
482
+ echo "❌ 缺少运行时环境文件 $ENV_FILE" >&2
483
+ exit 1
484
+ fi
485
+
486
+ set -a
487
+ source "$ENV_FILE"
488
+ set +a
489
+
490
+ export NODE_ENV="${envManager.mapAppEnvToNodeEnv(this.targetEnv)}"
491
+ export APP_ENV="${this.targetEnv}"
492
+
493
+ echo "🚀 执行 Prisma 数据库迁移"
494
+ npx --yes prisma migrate deploy --schema "$APP_ROOT/prisma/schema" >/dev/null
495
+
496
+ echo "✅ 数据库迁移完成,启动后端服务"
497
+ exec node "$APP_ROOT/dist/apps/backend/src/main.js"
498
+ `
499
+
500
+ await writeFile(scriptPath, script, 'utf8')
501
+ await chmod(scriptPath, 0o755)
502
+ }
503
+
504
+ async writeHealthcheckScript() {
505
+ const binDir = join(this.outputAppDir, 'bin')
506
+ await mkdir(binDir, { recursive: true })
507
+ const scriptPath = join(binDir, 'healthcheck.sh')
508
+ const port = this.envSnapshot.PORT || '3000'
509
+ const apiPrefix = (this.envSnapshot.API_PREFIX || 'api/v1').replace(/^\//, '')
510
+ const script = `#!/usr/bin/env bash
511
+ set -euo pipefail
512
+
513
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
514
+ APP_ROOT="\${SCRIPT_DIR%/bin}"
515
+ ENV_FILE="$APP_ROOT/config/.env.runtime"
516
+
517
+ if [[ ! -f "$ENV_FILE" ]]; then
518
+ echo "❌ 缺少运行时环境文件 $ENV_FILE" >&2
519
+ exit 1
520
+ fi
521
+
522
+ set -a
523
+ source "$ENV_FILE"
524
+ set +a
525
+
526
+ BASE_URL="http://localhost:${port}"
527
+ ENDPOINT="${apiPrefix}/health"
528
+
529
+ curl -sf --connect-timeout 2 --max-time 3 "\${BASE_URL%/}/\${ENDPOINT#/}"
530
+ `
531
+ await writeFile(scriptPath, script, 'utf8')
532
+ await chmod(scriptPath, 0o755)
533
+ }
534
+
535
+ async writeDeployReadme() {
536
+ const content = [
537
+ '# 后端部署包',
538
+ '',
539
+ '## 目录结构',
540
+ '',
541
+ '- bin/start.sh: 启动脚本(自动加载环境变量、执行 migrate deploy、启动服务)',
542
+ '- config/.env.runtime: 打包时固化的环境变量',
543
+ '- dist/: NestJS 编译后的 JavaScript 产物',
544
+ '- node_modules/: 生产依赖',
545
+ '- prisma/: Prisma schema 与 migrations',
546
+ '- manifest.json: 元数据记录(版本、git、环境)',
547
+ '',
548
+ '## 使用步骤',
549
+ '',
550
+ `1. 解压 ${this.artifactFile} 后进入目录:`,
551
+ ` tar -xzf ${this.artifactFile}`,
552
+ ' cd backend',
553
+ '',
554
+ '2. 如需覆盖环境变量,可编辑 config/.env.runtime。',
555
+ '',
556
+ '3. 执行 bin/start.sh 启动服务。脚本会检查 Node 版本、执行 prisma migrate deploy 并启动后端。',
557
+ '',
558
+ '4. (可选)使用进程管理工具(如 pm2/systemd)托管 bin/start.sh。',
559
+ '',
560
+ '## 注意事项',
561
+ '',
562
+ '- 包内已包含生产依赖,无需再执行 pnpm install。',
563
+ '- 如需更新环境变量,请重新执行打包命令或手动维护 config/.env.runtime。',
564
+ '- 启动脚本需在具备数据库与 Redis 连通性的环境下执行。',
565
+ '',
566
+ ].join('\n')
567
+ await writeFile(join(this.outputAppDir, 'README_DEPLOY.md'), content, 'utf8')
568
+ }
569
+
570
+ async installProductionDependencies() {
571
+ logger.step('安装生产依赖')
572
+ const installEnv = { ...process.env }
573
+ installEnv.NX_SKIP_NX_CACHE = 'true'
574
+ const installBaseCmd =
575
+ 'pnpm install --prod --config.node-linker=hoisted --config.package-import-method=copy'
576
+
577
+ await execManager.executeCommand(`${installBaseCmd} --lockfile-only`, {
578
+ cwd: this.outputAppDir,
579
+ skipEnvValidation: true,
580
+ env: installEnv,
581
+ })
582
+
583
+ await execManager.executeCommand(`${installBaseCmd} --frozen-lockfile`, {
584
+ cwd: this.outputAppDir,
585
+ skipEnvValidation: true,
586
+ env: installEnv,
587
+ })
588
+ }
589
+
590
+ async writeManifest() {
591
+ const manifest = {
592
+ app: '@ai/backend',
593
+ version: this.repoVersion,
594
+ gitSha: this.gitSha,
595
+ gitShortSha: this.gitShortSha,
596
+ environment: this.targetEnv,
597
+ buildTime: this.buildTimestamp,
598
+ node: {
599
+ required: this.nodeVersionConstraint,
600
+ runtime: process.version,
601
+ },
602
+ packageManager: this.packageManager,
603
+ tooling: {
604
+ nx: this.rootNxVersion || 'unknown',
605
+ pnpm: this.pnpmVersion || 'unknown',
606
+ },
607
+ build: {
608
+ configuration: this.buildConfiguration || 'unknown',
609
+ },
610
+ }
611
+ await writeFile(
612
+ join(this.outputAppDir, 'manifest.json'),
613
+ `${JSON.stringify(manifest, null, 2)}\n`,
614
+ 'utf8',
615
+ )
616
+ }
617
+
618
+ async createArchive() {
619
+ const artifactPath = this.getArtifactPath()
620
+ await mkdir(dirname(artifactPath), { recursive: true })
621
+ const tarCommand = `tar -czf "${artifactPath}" -C "${this.outputRoot}" backend`
622
+ await execManager.executeCommand(tarCommand, {
623
+ skipEnvValidation: true,
624
+ })
625
+ }
626
+
627
+ getArtifactPath() {
628
+ return join(this.projectRoot, 'dist', 'backend', this.artifactFile)
629
+ }
630
+
631
+ async cleanup() {
632
+ if (this.disableCleanup || !this.tmpRoot) return
633
+ try {
634
+ await rm(this.tmpRoot, { recursive: true, force: true })
635
+ } catch (error) {
636
+ logger.warn(`清理临时目录失败: ${error.message}`)
637
+ }
638
+ }
639
+ }
640
+
641
+ function parseArgs(argv) {
642
+ const options = {}
643
+ for (const arg of argv) {
644
+ if (arg === '--skip-build') options.skipBuild = true
645
+ else if (arg === '--keep-workdir') options.keepWorkdir = true
646
+ else if (arg.startsWith('--env=')) options.environment = arg.slice('--env='.length)
647
+ }
648
+ return options
649
+ }
650
+
651
+ export async function runBackendPackage(argv = []) {
652
+ const options = parseArgs(Array.isArray(argv) ? argv : [])
653
+ const packager = new BackendPackager(options)
654
+ await packager.run()
655
+ }
656
+
657
+ async function main() {
658
+ await runBackendPackage(process.argv.slice(2))
659
+ }
660
+
661
+ main().catch(error => {
662
+ logger.error(`后端部署包构建异常: ${error.message}`)
663
+ process.exit(1)
664
+ })
@@ -0,0 +1,19 @@
1
+ export function getCleanArgs(args = []) {
2
+ const result = []
3
+ let afterDoubleDash = false
4
+ for (const arg of args) {
5
+ if (arg === '--') {
6
+ afterDoubleDash = true
7
+ break
8
+ }
9
+ if (arg.startsWith('-')) continue
10
+ result.push(arg)
11
+ }
12
+ return result
13
+ }
14
+
15
+ export function getPassthroughArgs(args = []) {
16
+ const doubleDashIndex = args.indexOf('--')
17
+ if (doubleDashIndex === -1) return []
18
+ return args.slice(doubleDashIndex + 1)
19
+ }