@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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +86 -0
- package/lib/backend-package.js +664 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands/core.js +233 -0
- package/lib/cli/commands/db.js +239 -0
- package/lib/cli/commands/deploy.js +76 -0
- package/lib/cli/commands/export.js +34 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/stack.js +451 -0
- package/lib/cli/commands/start.js +83 -0
- package/lib/cli/commands/worktree.js +149 -0
- package/lib/cli/dx-cli.js +864 -0
- package/lib/cli/flags.js +96 -0
- package/lib/cli/help.js +209 -0
- package/lib/cli/index.js +4 -0
- package/lib/confirm.js +213 -0
- package/lib/env.js +296 -0
- package/lib/exec.js +643 -0
- package/lib/logger.js +188 -0
- package/lib/run-with-version-env.js +173 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +134 -0
- package/lib/validate-env.js +284 -0
- package/lib/vercel-deploy.js +237 -0
- package/lib/worktree.js +1032 -0
- package/package.json +34 -0
package/lib/env.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
function resolveProjectRoot() {
|
|
5
|
+
return process.env.DX_PROJECT_ROOT || process.cwd()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveConfigDir() {
|
|
9
|
+
const projectRoot = resolveProjectRoot()
|
|
10
|
+
return process.env.DX_CONFIG_DIR || join(projectRoot, 'dx', 'config')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EnvManager {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.projectRoot = resolveProjectRoot()
|
|
16
|
+
this.configDir = resolveConfigDir()
|
|
17
|
+
this.envLayers = this.loadEnvLayers()
|
|
18
|
+
this.requiredEnvConfig = null
|
|
19
|
+
this.latestEnvWarnings = []
|
|
20
|
+
|
|
21
|
+
// APP_ENV → NODE_ENV 映射(用于运行时行为和工具链,如 Nx/Next)
|
|
22
|
+
// 注意:'e2e' 在 dotenv 层使用独立层(.env.e2e),但在 NODE_ENV 上归并为 'test'
|
|
23
|
+
this.APP_TO_NODE_ENV = {
|
|
24
|
+
local: 'development',
|
|
25
|
+
dev: 'development',
|
|
26
|
+
development: 'development',
|
|
27
|
+
staging: 'production',
|
|
28
|
+
production: 'production',
|
|
29
|
+
e2e: 'test',
|
|
30
|
+
test: 'test',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 加载环境层级配置
|
|
35
|
+
loadEnvLayers() {
|
|
36
|
+
try {
|
|
37
|
+
const configPath = join(this.configDir, 'env-layers.json')
|
|
38
|
+
return JSON.parse(readFileSync(configPath, 'utf8'))
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// 使用默认配置(按环境 → 全局本地 → 环境本地 的优先级)
|
|
41
|
+
return {
|
|
42
|
+
development: ['.env.development', '.env.development.local'],
|
|
43
|
+
staging: ['.env.staging', '.env.staging.local'],
|
|
44
|
+
production: ['.env.production', '.env.production.local'],
|
|
45
|
+
test: ['.env.test', '.env.test.local'],
|
|
46
|
+
e2e: ['.env.e2e', '.env.e2e.local'],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 将 APP_ENV 规范化为 dotenv 层(development/production/test/e2e)
|
|
52
|
+
mapAppEnvToLayerEnv(appEnv) {
|
|
53
|
+
const env = String(appEnv || '').toLowerCase()
|
|
54
|
+
if (env === 'e2e') return 'e2e'
|
|
55
|
+
if (env === 'staging' || env === 'stage') return 'staging'
|
|
56
|
+
if (env === 'production' || env === 'prod') return 'production'
|
|
57
|
+
if (env === 'test') return 'test'
|
|
58
|
+
return 'development'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 将 APP_ENV 规范化为 NODE_ENV(development/production/test)
|
|
62
|
+
mapAppEnvToNodeEnv(appEnv) {
|
|
63
|
+
const env = String(appEnv || '').toLowerCase()
|
|
64
|
+
return this.APP_TO_NODE_ENV[env] || 'development'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 同步 APP_ENV 与 NODE_ENV(不改变现有 APP_ENV;仅在缺失或需规范化时设置 NODE_ENV)
|
|
68
|
+
syncEnvironments(appEnv) {
|
|
69
|
+
const app = String(appEnv || process.env.APP_ENV || '').toLowerCase()
|
|
70
|
+
if (app) {
|
|
71
|
+
const node = this.mapAppEnvToNodeEnv(app)
|
|
72
|
+
process.env.APP_ENV = app
|
|
73
|
+
process.env.NODE_ENV = node
|
|
74
|
+
return { appEnv: app, nodeEnv: node }
|
|
75
|
+
}
|
|
76
|
+
// 若没有 APP_ENV,仍保证 NODE_ENV 有合理默认值
|
|
77
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
|
|
78
|
+
return { appEnv: process.env.APP_ENV, nodeEnv: process.env.NODE_ENV }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 检测当前环境(用于选择 dotenv 层,如 .env.production/.env.e2e)
|
|
82
|
+
detectEnvironment(flags = {}) {
|
|
83
|
+
if (flags.prod || flags.production) return 'production'
|
|
84
|
+
if (flags.staging) return 'staging'
|
|
85
|
+
if (flags.dev || flags.development) return 'development'
|
|
86
|
+
if (flags.test) return 'test'
|
|
87
|
+
if (flags.e2e) return 'e2e'
|
|
88
|
+
|
|
89
|
+
// 优先基于 APP_ENV 选择 dotenv 层
|
|
90
|
+
if (process.env.APP_ENV) {
|
|
91
|
+
return this.mapAppEnvToLayerEnv(process.env.APP_ENV)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 回退到 NODE_ENV
|
|
95
|
+
return process.env.NODE_ENV || 'development'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 获取解析后的 dotenv 层级路径
|
|
99
|
+
getResolvedEnvLayers(app, environment) {
|
|
100
|
+
const layers = this.envLayers[environment] || []
|
|
101
|
+
if (!app) return layers
|
|
102
|
+
return layers.map(layer => layer.replace('{app}', app))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
loadRequiredEnvConfig() {
|
|
106
|
+
if (this.requiredEnvConfig) return this.requiredEnvConfig
|
|
107
|
+
const configPath = join(this.configDir, 'required-env.jsonc')
|
|
108
|
+
if (!existsSync(configPath)) {
|
|
109
|
+
this.requiredEnvConfig = { _common: [] }
|
|
110
|
+
return this.requiredEnvConfig
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const raw = readFileSync(configPath, 'utf8')
|
|
115
|
+
const sanitized = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
116
|
+
this.requiredEnvConfig = JSON.parse(sanitized || '{}') || { _common: [] }
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`无法解析 required-env.jsonc: ${error.message}`)
|
|
119
|
+
}
|
|
120
|
+
return this.requiredEnvConfig
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getRequiredEnvVars(environment, appType = null) {
|
|
124
|
+
const config = this.loadRequiredEnvConfig()
|
|
125
|
+
const base = Array.isArray(config._common) ? config._common : []
|
|
126
|
+
const envSpecific = Array.isArray(config[environment]) ? config[environment] : []
|
|
127
|
+
|
|
128
|
+
// 按应用类型添加对应的环境变量组
|
|
129
|
+
let appSpecific = []
|
|
130
|
+
if (appType) {
|
|
131
|
+
const appTypes = Array.isArray(appType) ? appType : [appType]
|
|
132
|
+
for (const type of appTypes) {
|
|
133
|
+
if (Array.isArray(config[type])) {
|
|
134
|
+
appSpecific = appSpecific.concat(config[type])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(new Set([...base, ...envSpecific, ...appSpecific]))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 构建dotenv命令参数
|
|
143
|
+
buildEnvFlags(app, environment) {
|
|
144
|
+
return this.getResolvedEnvLayers(app, environment)
|
|
145
|
+
.map(layer => `-e ${layer}`)
|
|
146
|
+
.join(' ')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 检查必需环境变量
|
|
150
|
+
validateRequiredVars(requiredVars = [], sourceEnv = process.env) {
|
|
151
|
+
const missing = []
|
|
152
|
+
const placeholders = []
|
|
153
|
+
|
|
154
|
+
requiredVars.forEach(varName => {
|
|
155
|
+
const value = sourceEnv[varName]
|
|
156
|
+
if (value === undefined || value === null) {
|
|
157
|
+
missing.push(varName)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.isPlaceholderEnvValue(value)) {
|
|
162
|
+
placeholders.push(varName)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if (missing.length > 0 || placeholders.length > 0) {
|
|
167
|
+
return { valid: false, missing, placeholders }
|
|
168
|
+
}
|
|
169
|
+
return { valid: true, missing: [], placeholders: [] }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 判断环境变量值是否缺失或仅为占位内容
|
|
173
|
+
isMissingEnvValue(value) {
|
|
174
|
+
if (value === undefined || value === null) return true
|
|
175
|
+
return this.isPlaceholderEnvValue(value)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 占位符判定:空串/空格/包裹引号但内容为空/null/undefined
|
|
179
|
+
isPlaceholderEnvValue(value) {
|
|
180
|
+
const stringValue = String(value)
|
|
181
|
+
const trimmed = stringValue.trim()
|
|
182
|
+
if (trimmed.length === 0) return true
|
|
183
|
+
|
|
184
|
+
let unwrapped = trimmed
|
|
185
|
+
const firstChar = trimmed[0]
|
|
186
|
+
const lastChar = trimmed[trimmed.length - 1]
|
|
187
|
+
const isQuotedPair =
|
|
188
|
+
(firstChar === '"' || firstChar === "'" || firstChar === '`') && firstChar === lastChar
|
|
189
|
+
if (trimmed.length >= 2 && isQuotedPair) {
|
|
190
|
+
unwrapped = trimmed.slice(1, -1).trim()
|
|
191
|
+
if (unwrapped.length === 0) return true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (unwrapped.includes('__SET_IN_env.local__')) return true
|
|
195
|
+
|
|
196
|
+
const normalized = unwrapped.toLowerCase()
|
|
197
|
+
return normalized === 'null' || normalized === 'undefined'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
collectEnvFromLayers(app, environment) {
|
|
201
|
+
const layers = this.getResolvedEnvLayers(app, environment)
|
|
202
|
+
const result = {}
|
|
203
|
+
const warnings = []
|
|
204
|
+
|
|
205
|
+
const interpolate = value =>
|
|
206
|
+
value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
207
|
+
const key = name.trim()
|
|
208
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) return result[key]
|
|
209
|
+
if (process.env[key] !== undefined) return process.env[key]
|
|
210
|
+
return ''
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
layers.forEach(layer => {
|
|
214
|
+
const filePath = join(this.projectRoot, layer)
|
|
215
|
+
if (!existsSync(filePath)) return
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const content = readFileSync(filePath, 'utf8')
|
|
219
|
+
const lines = content.split(/\r?\n/)
|
|
220
|
+
for (const rawLine of lines) {
|
|
221
|
+
const line = rawLine.trim()
|
|
222
|
+
if (!line || line.startsWith('#')) continue
|
|
223
|
+
const eqIdx = line.indexOf('=')
|
|
224
|
+
if (eqIdx <= 0) continue
|
|
225
|
+
const key = line.slice(0, eqIdx).trim()
|
|
226
|
+
if (!key) continue
|
|
227
|
+
let value = line.slice(eqIdx + 1)
|
|
228
|
+
|
|
229
|
+
// 移除行末注释(仅当值未被引号包裹时处理)
|
|
230
|
+
let commentIndex = -1
|
|
231
|
+
if (!/^\s*['"`]/.test(value)) {
|
|
232
|
+
commentIndex = value.indexOf(' #')
|
|
233
|
+
if (commentIndex !== -1) {
|
|
234
|
+
value = value.slice(0, commentIndex)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
value = value.trim()
|
|
239
|
+
const isSingleQuoted = value.startsWith("'") && value.endsWith("'")
|
|
240
|
+
const isDoubleQuoted = value.startsWith('"') && value.endsWith('"')
|
|
241
|
+
const isBacktickQuoted = value.startsWith('`') && value.endsWith('`')
|
|
242
|
+
|
|
243
|
+
if (isSingleQuoted || isDoubleQuoted || isBacktickQuoted) {
|
|
244
|
+
value = value.slice(1, -1)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!isSingleQuoted) {
|
|
248
|
+
value = interpolate(value)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const previous = result[key]
|
|
252
|
+
const previousIsNonEmpty = previous !== undefined && String(previous).trim().length > 0
|
|
253
|
+
if (previousIsNonEmpty && value.trim().length === 0) {
|
|
254
|
+
warnings.push(`环境文件 ${layer} 将 ${key} 覆盖为空值,请确认层级顺序是否正确`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
result[key] = value
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new Error(`读取环境文件失败 (${filePath}): ${error.message}`)
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
this.latestEnvWarnings = warnings
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 智能错误修复建议
|
|
269
|
+
suggestFixes(missing, environment) {
|
|
270
|
+
const env = environment || this.detectEnvironment()
|
|
271
|
+
return missing.map(varName => ({
|
|
272
|
+
var: varName,
|
|
273
|
+
suggestion: `请检查以下文件中的 ${varName} 配置:`,
|
|
274
|
+
files: [`.env.${env}`, `.env.${env}.local`],
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 获取环境描述
|
|
279
|
+
getEnvironmentDescription(environment) {
|
|
280
|
+
const descriptions = {
|
|
281
|
+
development: '开发环境',
|
|
282
|
+
staging: '预发环境',
|
|
283
|
+
production: '生产环境',
|
|
284
|
+
test: '测试环境',
|
|
285
|
+
e2e: 'E2E测试环境',
|
|
286
|
+
}
|
|
287
|
+
return descriptions[environment] || environment
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 检查危险操作环境
|
|
291
|
+
isDangerousEnvironment(environment) {
|
|
292
|
+
return environment === 'production'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const envManager = new EnvManager()
|