@ranger1/dx 0.1.8 → 0.1.10
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/README.md +23 -8
- package/lib/backend-package.js +4 -11
- package/lib/cli/dx-cli.js +4 -11
- package/lib/env-policy.js +5 -3
- package/lib/env.js +0 -39
- package/lib/exec.js +1 -7
- package/lib/validate-env.js +2 -180
- package/lib/vercel-deploy.js +3 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
必须全局安装,并始终使用最新版本:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pnpm add -g @ranger1/dx
|
|
12
|
+
pnpm add -g @ranger1/dx@latest
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
安装后即可在任意目录使用:
|
|
@@ -19,11 +19,10 @@ dx --help
|
|
|
19
19
|
dx status
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
升级到最新版本:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
pnpm
|
|
26
|
-
pnpm exec dx --help
|
|
25
|
+
pnpm update -g @ranger1/dx
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
## 使用条件(必须满足)
|
|
@@ -32,7 +31,7 @@ pnpm exec dx --help
|
|
|
32
31
|
- 包管理器:pnpm(dx 内部会调用 `pnpm`)
|
|
33
32
|
- 构建系统:Nx(dx 默认命令配置里大量使用 `npx nx ...`)
|
|
34
33
|
- 环境加载:建议项目依赖 `dotenv-cli`(dx 会用 `pnpm exec dotenv ...` 包裹命令来注入 `.env.*`)
|
|
35
|
-
-
|
|
34
|
+
- 项目结构:推荐按 `apps/backend` / `apps/front` / `apps/admin-front` 这类布局组织;如有自定义目录结构,请通过 `dx/config/commands.json` 适配
|
|
36
35
|
|
|
37
36
|
如果你的 monorepo 不完全一致,也能用:关键是你在 `dx/config/commands.json` 里把命令写成适配你项目的形式。
|
|
38
37
|
|
|
@@ -123,7 +122,7 @@ DX_CONFIG_DIR=/path/to/your-repo/dx/config dx status
|
|
|
123
122
|
|
|
124
123
|
target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.json` 里的 `app` 通过 `env-policy.jsonc.appToTarget` 映射到某个 target。
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
注:`env-policy.jsonc` 为必需配置;未提供时 dx 将直接报错。
|
|
127
126
|
|
|
128
127
|
## 示例工程
|
|
129
128
|
|
|
@@ -152,6 +151,22 @@ dx lint
|
|
|
152
151
|
dx test e2e backend
|
|
153
152
|
```
|
|
154
153
|
|
|
154
|
+
## deploy 行为说明
|
|
155
|
+
|
|
156
|
+
从 `0.1.9` 起,`dx deploy <target>` 不再在 dx 内部硬编码执行任何 `nx build`/`sdk build` 等前置步骤。
|
|
157
|
+
|
|
158
|
+
- 需要的前置构建(例如 `shared`、`api-contracts`、OpenAPI 导出、后端构建等)应由项目自己的 Nx 依赖图(`dependsOn`/项目依赖)或 Vercel 的 `buildCommand` 负责。
|
|
159
|
+
- 这样 dx deploy 不会强依赖 `apps/sdk` 等目录结构,更容易适配不同 monorepo。
|
|
160
|
+
|
|
161
|
+
## 依赖关系约定
|
|
162
|
+
|
|
163
|
+
dx 不负责管理「工程之间的构建依赖关系」。如果多个工程之间存在依赖(例如 `front/admin` 依赖 `shared` 或 `api-contracts`),必须由 Nx 的依赖图来表达并自动拉起:
|
|
164
|
+
|
|
165
|
+
- 使用 Nx 的项目依赖(基于 import graph 或 `implicitDependencies`)
|
|
166
|
+
- 使用 `nx.json` 的 `targetDefaults.dependsOn` / `targetDependencies`
|
|
167
|
+
|
|
168
|
+
dx 只会执行你在 `dx/config/commands.json` 中配置的命令,不会在执行过程中额外硬编码插入依赖构建。
|
|
169
|
+
|
|
155
170
|
## 给 Nx target 注入版本信息(可选)
|
|
156
171
|
|
|
157
172
|
本包提供 `dx-with-version-env`,用于在 `nx:run-commands` 中注入版本/sha/构建时间等环境变量:
|
|
@@ -169,7 +184,7 @@ dx test e2e backend
|
|
|
169
184
|
当前版本面向 pnpm + nx 的 monorepo,默认假设:
|
|
170
185
|
|
|
171
186
|
- 使用 pnpm + nx
|
|
172
|
-
- 项目布局包含 `apps/backend`、`apps/front`、`apps/admin-front
|
|
187
|
+
- 项目布局包含 `apps/backend`、`apps/front`、`apps/admin-front`(如有差异,通过 `dx/config/commands.json` 适配)
|
|
173
188
|
- 版本注入脚本 `dx-with-version-env` 默认支持 app: `backend` / `front` / `admin`
|
|
174
189
|
|
|
175
190
|
## 发布到 npm(准备工作)
|
package/lib/backend-package.js
CHANGED
|
@@ -239,18 +239,11 @@ class BackendPackager {
|
|
|
239
239
|
async prepareEnvSnapshot() {
|
|
240
240
|
logger.info('校验并快照环境变量')
|
|
241
241
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
if (!targetId) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
'env-policy.jsonc 已启用,但缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)',
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
requiredVars = resolveTargetRequiredVars(policy, targetId, this.layerEnv)
|
|
251
|
-
} else {
|
|
252
|
-
requiredVars = envManager.getRequiredEnvVars(this.layerEnv, 'backend')
|
|
242
|
+
const targetId = resolvePolicyTargetId(policy, 'backend')
|
|
243
|
+
if (!targetId) {
|
|
244
|
+
throw new Error('缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)')
|
|
253
245
|
}
|
|
246
|
+
const requiredVars = resolveTargetRequiredVars(policy, targetId, this.layerEnv)
|
|
254
247
|
const collected = envManager.collectEnvFromLayers('backend', this.layerEnv)
|
|
255
248
|
const effectiveEnv = { ...collected, ...process.env }
|
|
256
249
|
const { valid, missing, placeholders } = envManager.validateRequiredVars(
|
package/lib/cli/dx-cli.js
CHANGED
|
@@ -221,18 +221,11 @@ class DxCli {
|
|
|
221
221
|
if (process.env.CI !== '1') {
|
|
222
222
|
const effectiveEnv = { ...process.env, ...layeredEnv }
|
|
223
223
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
if (!targetId) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
'env-policy.jsonc 已启用,但缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)',
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
233
|
-
} else {
|
|
234
|
-
requiredVars = envManager.getRequiredEnvVars(environment, 'backend')
|
|
224
|
+
const targetId = resolvePolicyTargetId(policy, 'backend')
|
|
225
|
+
if (!targetId) {
|
|
226
|
+
throw new Error('缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)')
|
|
235
227
|
}
|
|
228
|
+
const requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
236
229
|
if (requiredVars.length > 0) {
|
|
237
230
|
const { valid, missing, placeholders } = envManager.validateRequiredVars(
|
|
238
231
|
requiredVars,
|
package/lib/env-policy.js
CHANGED
|
@@ -28,9 +28,11 @@ export function loadEnvPolicy(configDir) {
|
|
|
28
28
|
|
|
29
29
|
const policyPath = join(configDir, DEFAULT_POLICY_PATH)
|
|
30
30
|
if (!existsSync(policyPath)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
throw new Error(
|
|
32
|
+
`缺少配置文件 ${DEFAULT_POLICY_PATH}。
|
|
33
|
+
请在目标工程根目录创建 dx/config/${DEFAULT_POLICY_PATH}(或用 DX_CONFIG_DIR / --config-dir 指定配置目录)。
|
|
34
|
+
当前配置目录: ${configDir}`,
|
|
35
|
+
)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
let parsed
|
package/lib/env.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import stripJsonComments from 'strip-json-comments'
|
|
4
3
|
|
|
5
4
|
function resolveProjectRoot() {
|
|
6
5
|
return process.env.DX_PROJECT_ROOT || process.cwd()
|
|
@@ -16,7 +15,6 @@ export class EnvManager {
|
|
|
16
15
|
this.projectRoot = resolveProjectRoot()
|
|
17
16
|
this.configDir = resolveConfigDir()
|
|
18
17
|
this.envLayers = this.loadEnvLayers()
|
|
19
|
-
this.requiredEnvConfig = null
|
|
20
18
|
this.latestEnvWarnings = []
|
|
21
19
|
|
|
22
20
|
// APP_ENV → NODE_ENV 映射(用于运行时行为和工具链,如 Nx/Next)
|
|
@@ -103,43 +101,6 @@ export class EnvManager {
|
|
|
103
101
|
return layers.map(layer => layer.replace('{app}', app))
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
loadRequiredEnvConfig() {
|
|
107
|
-
if (this.requiredEnvConfig) return this.requiredEnvConfig
|
|
108
|
-
const configPath = join(this.configDir, 'required-env.jsonc')
|
|
109
|
-
if (!existsSync(configPath)) {
|
|
110
|
-
this.requiredEnvConfig = { _common: [] }
|
|
111
|
-
return this.requiredEnvConfig
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const raw = readFileSync(configPath, 'utf8')
|
|
116
|
-
const sanitized = stripJsonComments(raw)
|
|
117
|
-
this.requiredEnvConfig = JSON.parse(sanitized || '{}') || { _common: [] }
|
|
118
|
-
} catch (error) {
|
|
119
|
-
throw new Error(`无法解析 required-env.jsonc: ${error.message}`)
|
|
120
|
-
}
|
|
121
|
-
return this.requiredEnvConfig
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getRequiredEnvVars(environment, appType = null) {
|
|
125
|
-
const config = this.loadRequiredEnvConfig()
|
|
126
|
-
const base = Array.isArray(config._common) ? config._common : []
|
|
127
|
-
const envSpecific = Array.isArray(config[environment]) ? config[environment] : []
|
|
128
|
-
|
|
129
|
-
// 按应用类型添加对应的环境变量组
|
|
130
|
-
let appSpecific = []
|
|
131
|
-
if (appType) {
|
|
132
|
-
const appTypes = Array.isArray(appType) ? appType : [appType]
|
|
133
|
-
for (const type of appTypes) {
|
|
134
|
-
if (Array.isArray(config[type])) {
|
|
135
|
-
appSpecific = appSpecific.concat(config[type])
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return Array.from(new Set([...base, ...envSpecific, ...appSpecific]))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
104
|
// 构建dotenv命令参数
|
|
144
105
|
buildEnvFlags(app, environment) {
|
|
145
106
|
return this.getResolvedEnvLayers(app, environment)
|
package/lib/exec.js
CHANGED
|
@@ -80,7 +80,7 @@ export class ExecManager {
|
|
|
80
80
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
81
81
|
let requiredVars = []
|
|
82
82
|
|
|
83
|
-
if (!isCI &&
|
|
83
|
+
if (!isCI && app) {
|
|
84
84
|
const targetId = resolvePolicyTargetId(policy, app)
|
|
85
85
|
if (!targetId) {
|
|
86
86
|
throw new Error(
|
|
@@ -88,12 +88,6 @@ export class ExecManager {
|
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
91
|
-
} else if (!policy) {
|
|
92
|
-
// Backward-compatible behavior
|
|
93
|
-
// CI 环境跳过后端环境变量校验(CI 中 build backend 只生成 OpenAPI,不需要数据库连接)
|
|
94
|
-
// 根据 app 参数确定需要检查的环境变量组
|
|
95
|
-
const appType = isCI ? null : (app === 'backend' ? 'backend' : app ? 'frontend' : null)
|
|
96
|
-
requiredVars = envManager.getRequiredEnvVars(environment, appType)
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
if (requiredVars.length > 0) {
|
package/lib/validate-env.js
CHANGED
|
@@ -16,21 +16,7 @@ const EXTRA_ENV_IGNORED_DIRS = new Set([
|
|
|
16
16
|
'.schaltwerk',
|
|
17
17
|
])
|
|
18
18
|
const EXTRA_ENV_ALLOWED_PATHS = new Set(['docker/.env', 'docker/.env.example'])
|
|
19
|
-
const LOCAL_ALLOWLIST_CONFIG = join(CONFIG_DIR, 'local-env-allowlist.jsonc')
|
|
20
|
-
const EXEMPTED_KEYS_CONFIG = join(CONFIG_DIR, 'exempted-keys.jsonc')
|
|
21
19
|
const ENV_EXAMPLE_FILE = join(ROOT_DIR, '.env.example')
|
|
22
|
-
const PLACEHOLDER_TOKEN = '__SET_IN_env.local__'
|
|
23
|
-
|
|
24
|
-
const LOCAL_ENV_FILES = [
|
|
25
|
-
'.env.development.local',
|
|
26
|
-
'.env.production.local',
|
|
27
|
-
'.env.test.local',
|
|
28
|
-
'.env.e2e.local',
|
|
29
|
-
'.env.staging.local',
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
let cachedAllowlist = null
|
|
33
|
-
let cachedExemptedKeys = null
|
|
34
20
|
|
|
35
21
|
export function validateEnvironment() {
|
|
36
22
|
if (!process.env.NODE_ENV) {
|
|
@@ -47,15 +33,8 @@ export function validateEnvironment() {
|
|
|
47
33
|
enforceRootOnlyEnvFiles(policy)
|
|
48
34
|
enforceGlobalLocalFileProhibited()
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
enforceEnvExamplePolicy(policy)
|
|
53
|
-
} else {
|
|
54
|
-
// Backward-compatible behavior
|
|
55
|
-
enforceLocalSecretWhitelist()
|
|
56
|
-
enforceNonLocalSecretPlaceholders()
|
|
57
|
-
enforceEnvExampleSecrets()
|
|
58
|
-
}
|
|
36
|
+
enforceSecretPolicy(policy)
|
|
37
|
+
enforceEnvExamplePolicy(policy)
|
|
59
38
|
|
|
60
39
|
return { nodeEnv: process.env.NODE_ENV, appEnv: process.env.APP_ENV }
|
|
61
40
|
}
|
|
@@ -114,36 +93,6 @@ function enforceRootOnlyEnvFiles(policy) {
|
|
|
114
93
|
}
|
|
115
94
|
}
|
|
116
95
|
|
|
117
|
-
function enforceLocalSecretWhitelist() {
|
|
118
|
-
const allowlist = loadLocalAllowlist()
|
|
119
|
-
const errors = []
|
|
120
|
-
|
|
121
|
-
for (const file of LOCAL_ENV_FILES) {
|
|
122
|
-
const fullPath = join(ROOT_DIR, file)
|
|
123
|
-
if (!existsSync(fullPath)) continue
|
|
124
|
-
|
|
125
|
-
const entries = parseEnvFile(fullPath)
|
|
126
|
-
const invalidKeys = []
|
|
127
|
-
|
|
128
|
-
for (const key of entries.keys()) {
|
|
129
|
-
if (!allowlist.has(key)) {
|
|
130
|
-
invalidKeys.push(key)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (invalidKeys.length > 0) {
|
|
135
|
-
errors.push(`${file}: ${invalidKeys.join(', ')}`)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (errors.length > 0) {
|
|
140
|
-
const message = errors.join('\n')
|
|
141
|
-
throw new Error(
|
|
142
|
-
`检测到 *.local 文件包含非白名单键:\n${message}\n请将这些键迁移到对应的 .env.<env> 文件,仅保留白名单内的机密信息在 *.local 中。`,
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
96
|
function enforceGlobalLocalFileProhibited() {
|
|
148
97
|
const legacyLocal = join(ROOT_DIR, '.env.local')
|
|
149
98
|
if (existsSync(legacyLocal)) {
|
|
@@ -320,80 +269,6 @@ function findOverlaps(namedSets) {
|
|
|
320
269
|
return overlaps
|
|
321
270
|
}
|
|
322
271
|
|
|
323
|
-
function enforceNonLocalSecretPlaceholders() {
|
|
324
|
-
const allowlist = loadLocalAllowlist()
|
|
325
|
-
const exemptedKeys = loadExemptedKeys()
|
|
326
|
-
const violations = []
|
|
327
|
-
|
|
328
|
-
for (const file of listRootEnvFiles()) {
|
|
329
|
-
if (file === '.env.example') continue
|
|
330
|
-
if (file.includes('.local')) continue
|
|
331
|
-
|
|
332
|
-
const fullPath = join(ROOT_DIR, file)
|
|
333
|
-
if (!existsSync(fullPath)) continue
|
|
334
|
-
|
|
335
|
-
const entries = parseEnvFile(fullPath)
|
|
336
|
-
|
|
337
|
-
for (const [key, value] of entries.entries()) {
|
|
338
|
-
if (!allowlist.has(key)) continue
|
|
339
|
-
if (exemptedKeys.has(key)) continue // 豁免的键允许使用非占位符值
|
|
340
|
-
|
|
341
|
-
const normalized = value.trim()
|
|
342
|
-
if (normalized !== PLACEHOLDER_TOKEN) {
|
|
343
|
-
violations.push(`${file}: ${key}`)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (violations.length > 0) {
|
|
349
|
-
const message = violations.join('\n')
|
|
350
|
-
throw new Error(
|
|
351
|
-
`检测到非 *.local 文件包含敏感键但未使用占位符 ${PLACEHOLDER_TOKEN}:\n${message}\n` +
|
|
352
|
-
`请仅在 .env.<env>.local 系列中设置真实值,并在其他文件中使用占位符。`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function enforceEnvExampleSecrets() {
|
|
358
|
-
if (!existsSync(ENV_EXAMPLE_FILE)) return
|
|
359
|
-
|
|
360
|
-
const allowlist = loadLocalAllowlist()
|
|
361
|
-
const entries = parseEnvFile(ENV_EXAMPLE_FILE)
|
|
362
|
-
const disallowedKeys = []
|
|
363
|
-
const invalidPlaceholders = []
|
|
364
|
-
|
|
365
|
-
for (const [key, value] of entries.entries()) {
|
|
366
|
-
const normalized = value.trim()
|
|
367
|
-
|
|
368
|
-
if (!allowlist.has(key)) {
|
|
369
|
-
disallowedKeys.push(key)
|
|
370
|
-
continue
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (normalized !== PLACEHOLDER_TOKEN) {
|
|
374
|
-
invalidPlaceholders.push(key)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (disallowedKeys.length > 0) {
|
|
379
|
-
throw new Error(
|
|
380
|
-
`.env.example 仅允许包含 scripts/config/local-env-allowlist.jsonc 中的键,检测到非法键: ${disallowedKeys.join(', ')}`,
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (invalidPlaceholders.length > 0) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`.env.example 中以下键未使用占位符 ${PLACEHOLDER_TOKEN}: ${invalidPlaceholders.join(', ')}`,
|
|
387
|
-
)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function listRootEnvFiles() {
|
|
392
|
-
return readdirSync(ROOT_DIR, { withFileTypes: true })
|
|
393
|
-
.filter(entry => entry.isFile() && entry.name.startsWith('.env'))
|
|
394
|
-
.map(entry => entry.name)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
272
|
function parseEnvFile(filePath) {
|
|
398
273
|
const content = readFileSync(filePath, 'utf8')
|
|
399
274
|
const map = new Map()
|
|
@@ -415,57 +290,4 @@ function parseEnvFile(filePath) {
|
|
|
415
290
|
return map
|
|
416
291
|
}
|
|
417
292
|
|
|
418
|
-
function loadLocalAllowlist() {
|
|
419
|
-
if (cachedAllowlist) return cachedAllowlist
|
|
420
|
-
|
|
421
|
-
if (!existsSync(LOCAL_ALLOWLIST_CONFIG)) {
|
|
422
|
-
throw new Error('缺少配置文件 scripts/config/local-env-allowlist.jsonc,请补充后重试')
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const raw = readFileSync(LOCAL_ALLOWLIST_CONFIG, 'utf8')
|
|
426
|
-
const sanitized = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
427
|
-
const parsed = JSON.parse(sanitized || '{}') || {}
|
|
428
|
-
|
|
429
|
-
const allowedValues = Array.isArray(parsed.allowed) ? parsed.allowed : []
|
|
430
|
-
if (allowedValues.length === 0) {
|
|
431
|
-
throw new Error('local-env-allowlist.jsonc 中的 allowed 不能为空,请至少保留一个键')
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const invalid = allowedValues.filter(
|
|
435
|
-
value => typeof value !== 'string' || value.trim().length === 0,
|
|
436
|
-
)
|
|
437
|
-
if (invalid.length > 0) {
|
|
438
|
-
throw new Error('local-env-allowlist.jsonc.allowed 中存在非法键,请使用非空字符串')
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
cachedAllowlist = new Set(allowedValues)
|
|
442
|
-
return cachedAllowlist
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function loadExemptedKeys() {
|
|
446
|
-
if (cachedExemptedKeys) return cachedExemptedKeys
|
|
447
|
-
|
|
448
|
-
// 如果豁免配置文件不存在,返回空集合(可选配置)
|
|
449
|
-
if (!existsSync(EXEMPTED_KEYS_CONFIG)) {
|
|
450
|
-
cachedExemptedKeys = new Set()
|
|
451
|
-
return cachedExemptedKeys
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const raw = readFileSync(EXEMPTED_KEYS_CONFIG, 'utf8')
|
|
455
|
-
const sanitized = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
456
|
-
const parsed = JSON.parse(sanitized || '{}') || {}
|
|
457
|
-
|
|
458
|
-
const exemptedValues = Array.isArray(parsed.exempted) ? parsed.exempted : []
|
|
459
|
-
|
|
460
|
-
const invalid = exemptedValues.filter(
|
|
461
|
-
value => typeof value !== 'string' || value.trim().length === 0,
|
|
462
|
-
)
|
|
463
|
-
if (invalid.length > 0) {
|
|
464
|
-
throw new Error('exempted-keys.jsonc.exempted 中存在非法键,请使用非空字符串')
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
cachedExemptedKeys = new Set(exemptedValues)
|
|
468
|
-
return cachedExemptedKeys
|
|
469
|
-
}
|
|
470
|
-
|
|
471
293
|
export default { validateEnvironment }
|
package/lib/vercel-deploy.js
CHANGED
|
@@ -79,35 +79,9 @@ export async function deployToVercel(target, options = {}) {
|
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
logger.step('编译 backend...')
|
|
86
|
-
const backendConfig = environment === 'production' || environment === 'staging'
|
|
87
|
-
? 'production'
|
|
88
|
-
: 'development'
|
|
89
|
-
await execManager.executeCommand(`npx nx build backend --configuration=${backendConfig}`, {
|
|
90
|
-
app: 'backend',
|
|
91
|
-
flags: {
|
|
92
|
-
...(environment === 'production' ? { prod: true } : {}),
|
|
93
|
-
...(environment === 'staging' ? { staging: true } : {}),
|
|
94
|
-
...(environment === 'development' ? { dev: true } : {}),
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
logger.success('backend 编译成功')
|
|
98
|
-
|
|
99
|
-
logger.step('编译 sdk...')
|
|
100
|
-
const { runSdkBuild } = await import('./sdk-build.js')
|
|
101
|
-
const sdkArgs = environment === 'production' ? [] : ['dev']
|
|
102
|
-
await runSdkBuild(sdkArgs)
|
|
103
|
-
logger.success('sdk 编译成功')
|
|
104
|
-
} catch (error) {
|
|
105
|
-
const message = error?.message || String(error)
|
|
106
|
-
logger.error(`编译失败: ${message}`)
|
|
107
|
-
logger.error('部署已终止,请先修复编译错误')
|
|
108
|
-
process.exitCode = 1
|
|
109
|
-
return
|
|
110
|
-
}
|
|
82
|
+
// deploy 不再硬编码任何 Nx 构建步骤。
|
|
83
|
+
// - 前置构建/生成(shared/contracts/backend 等)应由项目自己的 Nx 依赖图或 Vercel buildCommand 负责。
|
|
84
|
+
// - 这样 dx deploy 能兼容不同 monorepo 布局(不强依赖 apps/sdk 等目录)。
|
|
111
85
|
|
|
112
86
|
// 映射环境标识:development -> dev, staging -> staging, production -> prod
|
|
113
87
|
const envMap = {
|