@weotro/dx 0.1.19 → 0.1.21
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 +51 -0
- package/bin/dx.js +10 -1
- package/lib/cli/dx-cli.js +29 -3
- package/lib/cli/help-model.js +13 -1
- package/lib/exec.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,57 @@ DX_CONFIG_DIR=/path/to/your-repo/dx/config dx status
|
|
|
103
103
|
|
|
104
104
|
### 1) dx/config/commands.json
|
|
105
105
|
|
|
106
|
+
#### 自定义命令参数透传
|
|
107
|
+
|
|
108
|
+
在选中的可执行叶节点声明 `allowPassthrough: true`,然后用第一个独立的 `--`
|
|
109
|
+
分开 DX 选项和子命令参数:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
dx release plan --prod -- --brands zh --targets backend --ref v0.2.205
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
最小 `commands.json`(脚本由目标项目提供):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"release": {
|
|
120
|
+
"plan": {
|
|
121
|
+
"production": {
|
|
122
|
+
"command": "node scripts/release/cli.mjs plan --environment production",
|
|
123
|
+
"allowPassthrough": true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
字段契约:
|
|
131
|
+
|
|
132
|
+
| 字段 | 类型 / 默认值 | 作用 |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `allowPassthrough` | boolean / `false` | 只有严格的 `true` 开启透传;必须放在环境选择后的 `command` 叶节点,父节点不继承 |
|
|
135
|
+
| `command` | string | 现有 Bash 命令;用户参数逐项转义后追加在末尾,固定参数在前 |
|
|
136
|
+
|
|
137
|
+
适用于通用配置处理器执行的自定义命令,包括 `release plan/run` 这类内置命令族的扩展 action,
|
|
138
|
+
以及任意非内置顶层命令。`concurrent`、`sequential`、`internal` 配置暂不支持透传,
|
|
139
|
+
即使声明 `allowPassthrough: true` 也会拒绝非空参数;不会向多个子任务广播。
|
|
140
|
+
内置 `test/db/env` 等仍使用各自已有的参数契约,此字段不会覆盖内置处理器。
|
|
141
|
+
|
|
142
|
+
迁移时,为确实需要接收参数的叶节点增加该字段,并将业务参数移到 `--` 后。
|
|
143
|
+
未开启或不支持透传的配置收到非空子参数时会非零退出,不再静默丢弃后执行默认命令。
|
|
144
|
+
没有子参数或仅有末尾 `--` 时保留旧行为;省略分隔符的未知选项继续报错。
|
|
145
|
+
|
|
146
|
+
第一个 `--` 后的顺序、重复项、空字符串、特殊字符和第二个 `--` 均保留。
|
|
147
|
+
其中的 `--prod`、`-Y`、`--help`、`--version`、`--config-dir` 不影响 DX 自身。
|
|
148
|
+
危险操作仍须通过原确认流程;子命令退出失败时 DX 非零退出。
|
|
149
|
+
DX 不校验业务参数含义,拼错的 key、品牌、目标、ref 由子脚本严格校验。
|
|
150
|
+
|
|
151
|
+
建议 `command` 只调用一个脚本;包含管道、多个语句或末尾注释的 shell 文本应先移入脚本,
|
|
152
|
+
确保追加参数由明确的入口接收。不要在命令中自行拼接用户输入。
|
|
153
|
+
|
|
154
|
+
回归验证:`pnpm test --runInBand test/configured-passthrough.test.js`,测试使用真实
|
|
155
|
+
`bin/dx.js` 和临时 probe,校验实际子进程 argv、拒绝时未启动子进程及失败退出。
|
|
156
|
+
|
|
106
157
|
这是核心文件,定义了 dx 各命令要执行的 shell 命令,也承载帮助信息配置。
|
|
107
158
|
|
|
108
159
|
它支持:
|
package/bin/dx.js
CHANGED
|
@@ -8,7 +8,9 @@ function parseConfigDir(argv) {
|
|
|
8
8
|
const envValue = process.env.DX_CONFIG_DIR
|
|
9
9
|
if (envValue) return String(envValue)
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const raw = Array.isArray(argv) ? argv : []
|
|
12
|
+
const separator = raw.indexOf('--')
|
|
13
|
+
const args = separator === -1 ? raw : raw.slice(0, separator)
|
|
12
14
|
const idx = args.indexOf('--config-dir')
|
|
13
15
|
if (idx !== -1 && idx + 1 < args.length) return String(args[idx + 1])
|
|
14
16
|
for (const token of args) {
|
|
@@ -22,7 +24,14 @@ function stripConfigDirArgs(argv) {
|
|
|
22
24
|
const out = []
|
|
23
25
|
for (let i = 0; i < args.length; i++) {
|
|
24
26
|
const token = args[i]
|
|
27
|
+
if (token === '--') {
|
|
28
|
+
out.push(...args.slice(i))
|
|
29
|
+
break
|
|
30
|
+
}
|
|
25
31
|
if (token === '--config-dir') {
|
|
32
|
+
if (args[i + 1] === undefined || args[i + 1] === '--') {
|
|
33
|
+
throw new Error('--config-dir 需要在 -- 分隔符前提供配置目录')
|
|
34
|
+
}
|
|
26
35
|
i++
|
|
27
36
|
continue
|
|
28
37
|
}
|
package/lib/cli/dx-cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveTargetRequiredVars,
|
|
12
12
|
} from '../env-policy.js'
|
|
13
13
|
import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
|
|
14
|
-
import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
|
|
14
|
+
import { getCleanArgs, getCleanArgsWithConsumedValues, getPassthroughArgs } from './args.js'
|
|
15
15
|
import { showHelp, showCommandHelp } from './help.js'
|
|
16
16
|
import { buildStrictHelpValidationContext, validateHelpConfig } from './help-schema.js'
|
|
17
17
|
import { appendNxVerboseFlag } from './nx-command.js'
|
|
@@ -189,12 +189,14 @@ class DxCli {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
registerConfiguredCommandHandlers() {
|
|
192
|
+
this.configuredCommandNames = new Set()
|
|
192
193
|
for (const [commandName, config] of Object.entries(this.commands || {})) {
|
|
193
194
|
if (commandName === 'help') continue
|
|
194
195
|
if (this.commandHandlers[commandName]) continue
|
|
195
196
|
if (!this.isExecutableConfigTree(config)) continue
|
|
196
197
|
|
|
197
198
|
this.commandHandlers[commandName] = args => this.handleConfiguredCommand(commandName, args)
|
|
199
|
+
this.configuredCommandNames.add(commandName)
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -220,6 +222,9 @@ class DxCli {
|
|
|
220
222
|
return
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
const passthroughArgs = getPassthroughArgs(this.args)
|
|
226
|
+
this.validateConfiguredPassthrough(config, passthroughArgs)
|
|
227
|
+
|
|
223
228
|
if (config.dangerous) {
|
|
224
229
|
const operation = config.description || [commandName, ...args].join(' ')
|
|
225
230
|
const confirmed = await confirmManager.confirmDangerous(operation, environment, this.flags.Y)
|
|
@@ -234,7 +239,17 @@ class DxCli {
|
|
|
234
239
|
} else if (config.sequential && Array.isArray(config.commands)) {
|
|
235
240
|
await this.handleSequentialCommands(config.commands, environment)
|
|
236
241
|
} else {
|
|
237
|
-
await this.executeCommand(config)
|
|
242
|
+
await this.executeCommand(config, undefined, passthroughArgs)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
validateConfiguredPassthrough(config, args) {
|
|
247
|
+
if (args.length === 0) return
|
|
248
|
+
if (config?.internal || config?.concurrent || config?.sequential || !config?.command) {
|
|
249
|
+
throw new Error('此命令类型不支持 -- 参数透传;仅普通 command 叶节点支持,请移除额外参数或改用独立脚本命令。')
|
|
250
|
+
}
|
|
251
|
+
if (config.allowPassthrough !== true) {
|
|
252
|
+
throw new Error('此命令未启用参数透传;请在选中的 command 叶节点设置 allowPassthrough: true,并使用 dx <命令路径> [环境标志] -- <子命令参数>。')
|
|
238
253
|
}
|
|
239
254
|
}
|
|
240
255
|
|
|
@@ -389,6 +404,14 @@ class DxCli {
|
|
|
389
404
|
|
|
390
405
|
this.validateInputs()
|
|
391
406
|
|
|
407
|
+
// Reject unsupported custom argv before dependency installation or startup hooks.
|
|
408
|
+
if (this.configuredCommandNames.has(this.command)) {
|
|
409
|
+
const consumed = this.validateFlags(this.command, this.getAllowedFlags(this.command))
|
|
410
|
+
const [, ...args] = getCleanArgsWithConsumedValues(this.args, consumed)
|
|
411
|
+
const config = this.resolveConfiguredCommand(this.command, args, this.determineEnvironment())
|
|
412
|
+
this.validateConfiguredPassthrough(config, getPassthroughArgs(this.args))
|
|
413
|
+
}
|
|
414
|
+
|
|
392
415
|
// Fail fast for unknown commands before dependency/env startup checks.
|
|
393
416
|
if (this.command && !this.commandHandlers[this.command]) {
|
|
394
417
|
logger.error(`未知命令: ${this.command}`)
|
|
@@ -810,6 +833,7 @@ class DxCli {
|
|
|
810
833
|
|
|
811
834
|
reportUnknownFlag(command, flag, allowedFlags) {
|
|
812
835
|
logger.error(`检测到未识别的选项: ${flag}`)
|
|
836
|
+
logger.info('自定义命令参数请放在 -- 后,并在 command 叶节点设置 allowPassthrough: true。')
|
|
813
837
|
const supported = Array.from(allowedFlags.keys())
|
|
814
838
|
if (supported.length > 0) {
|
|
815
839
|
logger.info(`支持的选项: ${supported.join(', ')}`)
|
|
@@ -937,7 +961,8 @@ class DxCli {
|
|
|
937
961
|
}
|
|
938
962
|
|
|
939
963
|
// 执行单个命令
|
|
940
|
-
async executeCommand(config, overrideFlags) {
|
|
964
|
+
async executeCommand(config, overrideFlags, passthroughArgs = []) {
|
|
965
|
+
this.validateConfiguredPassthrough(config, passthroughArgs)
|
|
941
966
|
if (!config) {
|
|
942
967
|
logger.error('无效的命令配置')
|
|
943
968
|
return
|
|
@@ -1022,6 +1047,7 @@ class DxCli {
|
|
|
1022
1047
|
env: config.env || {},
|
|
1023
1048
|
skipEnvValidation: Boolean(config.skipEnvValidation),
|
|
1024
1049
|
forcePortCleanup: Boolean(config.forcePortCleanup),
|
|
1050
|
+
passthroughArgs,
|
|
1025
1051
|
}
|
|
1026
1052
|
|
|
1027
1053
|
await execManager.executeCommand(rawCommand, options)
|
package/lib/cli/help-model.js
CHANGED
|
@@ -84,19 +84,31 @@ export function getGlobalHelpModel(commands = {}, context = {}) {
|
|
|
84
84
|
export function getCommandHelpModel(commands = {}, commandName, context = {}) {
|
|
85
85
|
const commandConfig = commands?.[commandName] ?? {}
|
|
86
86
|
const commandHelp = getCommandHelpConfig(commands, commandName)
|
|
87
|
+
const notes = [...normalizeArray(commandHelp?.notes)]
|
|
88
|
+
if (hasPassthroughCommand(commandConfig)) {
|
|
89
|
+
notes.push(`自定义 command 叶节点设置 allowPassthrough: true 后可透传参数:dx ${commandName} <命令路径> [环境标志] -- <子命令参数>;未开启或组合/internal 命令收到额外参数会报错。`)
|
|
90
|
+
}
|
|
87
91
|
|
|
88
92
|
return {
|
|
89
93
|
name: commandName,
|
|
90
94
|
summary: resolveSummary(commandConfig, commandHelp) || BUILTIN_SUMMARIES[commandName] || '',
|
|
91
95
|
usage: resolveUsage(commandName, commandHelp, commandConfig),
|
|
92
96
|
args: normalizeArray(commandHelp?.args),
|
|
93
|
-
notes
|
|
97
|
+
notes,
|
|
94
98
|
examples: normalizeArray(commandHelp?.examples),
|
|
95
99
|
options: normalizeArray(commandHelp?.options),
|
|
96
100
|
targets: resolveVisibleTargets(commands, commandName, commandConfig, context),
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
function hasPassthroughCommand(node) {
|
|
105
|
+
if (!isPlainObject(node)) return false
|
|
106
|
+
if (node.command || node.internal || node.concurrent || node.sequential) {
|
|
107
|
+
return Boolean(node.command && node.allowPassthrough === true && !node.internal && !node.concurrent && !node.sequential)
|
|
108
|
+
}
|
|
109
|
+
return Object.entries(node).some(([key, value]) => key !== 'help' && hasPassthroughCommand(value))
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
export function resolveSummary(node = {}, help = null) {
|
|
101
113
|
return help?.summary || node?.help?.summary || node?.description || ''
|
|
102
114
|
}
|
package/lib/exec.js
CHANGED
|
@@ -66,6 +66,7 @@ export class ExecManager {
|
|
|
66
66
|
ports = [],
|
|
67
67
|
skipEnvValidation = false,
|
|
68
68
|
forcePortCleanup = false,
|
|
69
|
+
passthroughArgs = [],
|
|
69
70
|
} = options
|
|
70
71
|
|
|
71
72
|
// 在执行前同步环境,确保 Nx/构建工具拿到规范的 NODE_ENV
|
|
@@ -186,7 +187,12 @@ export class ExecManager {
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
// Quote for the bash executor, after command classification so child data
|
|
191
|
+
// cannot change environment selection or trigger port-cleanup heuristics.
|
|
192
|
+
const executionCommand = passthroughArgs.length > 0
|
|
193
|
+
? `${fullCommand} ${passthroughArgs.map(arg => `'${arg.replace(/'/g, "'\\''")}'`).join(' ')}`
|
|
194
|
+
: fullCommand
|
|
195
|
+
logger.command(executionCommand)
|
|
190
196
|
|
|
191
197
|
// 执行命令(可能重试)
|
|
192
198
|
let lastError = null
|
|
@@ -253,7 +259,7 @@ export class ExecManager {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
const result = await this.spawnCommand(
|
|
262
|
+
const result = await this.spawnCommand(executionCommand, {
|
|
257
263
|
cwd: cwd || process.cwd(),
|
|
258
264
|
stdio,
|
|
259
265
|
env: sanitizeChildEnv({
|