@ranger1/dx 0.1.96 → 0.1.98

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/dx-cli.js CHANGED
@@ -13,10 +13,10 @@ import {
13
13
  import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
14
14
  import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
15
15
  import { showHelp, showCommandHelp } from './help.js'
16
+ import { buildStrictHelpValidationContext, validateHelpConfig } from './help-schema.js'
16
17
  import { getPackageVersion } from '../version.js'
17
18
  import {
18
19
  handleHelp,
19
- handleDev,
20
20
  handleBuild,
21
21
  handleTest,
22
22
  handleLint,
@@ -51,7 +51,6 @@ class DxCli {
51
51
  this.envCache = null
52
52
  this.commandHandlers = {
53
53
  help: args => handleHelp(this, args),
54
- dev: args => handleDev(this, args),
55
54
  start: args => handleStart(this, args),
56
55
  build: args => handleBuild(this, args),
57
56
  test: args => handleTest(this, args),
@@ -70,6 +69,7 @@ class DxCli {
70
69
  }
71
70
 
72
71
  this.flagDefinitions = FLAG_DEFINITIONS
72
+ this.validateLoadedHelpConfig()
73
73
  }
74
74
 
75
75
  // 加载命令配置
@@ -84,6 +84,10 @@ class DxCli {
84
84
  }
85
85
  }
86
86
 
87
+ validateLoadedHelpConfig() {
88
+ validateHelpConfig(this.commands, buildStrictHelpValidationContext(this))
89
+ }
90
+
87
91
  // 检测并安装依赖
88
92
  async ensureDependencies() {
89
93
  const nodeModulesPath = join(process.cwd(), 'node_modules')
@@ -160,8 +164,17 @@ class DxCli {
160
164
 
161
165
  // 检测并生成 Prisma Client
162
166
  async ensurePrismaClient() {
167
+ // 仅对真正安装了 @prisma/client 的项目执行检查。
168
+ // 依据「node_modules/@prisma/client/package.json 是否存在」判断项目是否使用 Prisma,
169
+ // 避免在纯文档站、纯前端等未引入 Prisma 的项目上误触发生成流程,
170
+ // 进而报出形如「未找到 db.generate 命令配置」的误导性错误。
171
+ const prismaClientDir = join(process.cwd(), 'node_modules', '@prisma', 'client')
172
+ if (!existsSync(join(prismaClientDir, 'package.json'))) {
173
+ return
174
+ }
175
+
163
176
  // pnpm 结构下检测 @prisma/client 生成的 default.js 文件
164
- const prismaClientPath = join(process.cwd(), 'node_modules', '@prisma', 'client', 'default.js')
177
+ const prismaClientPath = join(prismaClientDir, 'default.js')
165
178
 
166
179
  if (!existsSync(prismaClientPath)) {
167
180
  const environment = this.determineEnvironment()
@@ -172,22 +185,9 @@ class DxCli {
172
185
  throw new Error('未找到 db.generate 命令配置,请检查 dx/config/commands.json')
173
186
  }
174
187
 
175
- const envKey = this.normalizeEnvKey(environment)
176
- const execFlags = { ...this.flags }
177
- ;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
178
- key => {
179
- delete execFlags[key]
180
- },
181
- )
182
- if (envKey === 'prod') execFlags.prod = true
183
- else if (envKey === 'dev') execFlags.dev = true
184
- else if (envKey === 'test') execFlags.test = true
185
- else if (envKey === 'e2e') execFlags.e2e = true
186
- else if (envKey === 'staging') execFlags.staging = true
187
-
188
188
  await execManager.executeCommand(generateConfig.command, {
189
189
  app: generateConfig.app || 'backend',
190
- flags: execFlags,
190
+ flags: this.createExecutionFlags(environment),
191
191
  // Prisma generate 不应卡在环境变量校验上
192
192
  skipEnvValidation: true,
193
193
  })
@@ -286,9 +286,9 @@ class DxCli {
286
286
  // 显示帮助
287
287
  if (this.flags.help || !this.command) {
288
288
  if (this.flags.help && this.command && this.command !== 'help') {
289
- showCommandHelp(this.command)
289
+ showCommandHelp(this.command, this)
290
290
  } else {
291
- showHelp()
291
+ showHelp(this)
292
292
  }
293
293
  return
294
294
  }
@@ -298,7 +298,7 @@ class DxCli {
298
298
  // Fail fast for unknown commands before dependency/env startup checks.
299
299
  if (this.command && !this.commandHandlers[this.command]) {
300
300
  logger.error(`未知命令: ${this.command}`)
301
- showHelp()
301
+ showHelp(this)
302
302
  process.exit(1)
303
303
  }
304
304
 
@@ -346,7 +346,7 @@ class DxCli {
346
346
  const [, ...subArgs] = cleanArgs
347
347
 
348
348
  if (!command) {
349
- showHelp()
349
+ showHelp(this)
350
350
  return
351
351
  }
352
352
 
@@ -504,14 +504,14 @@ class DxCli {
504
504
  case 'worktree': {
505
505
  if (positionalArgs.length === 0) return
506
506
  const action = positionalArgs[0]
507
- if (['del', 'delete', 'rm'].includes(action)) {
507
+ if (action === 'del') {
508
508
  return
509
509
  }
510
- if (['make'].includes(action)) {
510
+ if (action === 'make') {
511
511
  ensureMax(3)
512
512
  break
513
513
  }
514
- if (['list', 'ls', 'clean', 'prune'].includes(action)) {
514
+ if (action === 'list' || action === 'clean') {
515
515
  ensureMax(1)
516
516
  break
517
517
  }
@@ -550,11 +550,8 @@ class DxCli {
550
550
  const value = String(token).toLowerCase()
551
551
  return (
552
552
  value === 'dev' ||
553
- value === 'development' ||
554
553
  value === 'prod' ||
555
- value === 'production' ||
556
554
  value === 'staging' ||
557
- value === 'stage' ||
558
555
  value === 'test' ||
559
556
  value === 'e2e'
560
557
  )
@@ -570,18 +567,19 @@ class DxCli {
570
567
  if (suggestion) {
571
568
  logger.info(`建议命令: ${suggestion}`)
572
569
  } else if (normalizedFlag) {
573
- logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
570
+ logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
574
571
  }
575
572
  logger.info('未显式指定环境时将默认使用 --dev。')
576
573
  process.exit(1)
577
574
  }
578
575
 
579
576
  getEnvironmentFlagExample(token) {
580
- const key = this.normalizeEnvKey(token)
581
- switch (key) {
577
+ switch (String(token || '').toLowerCase()) {
582
578
  case 'dev':
579
+ case 'development':
583
580
  return '--dev'
584
581
  case 'prod':
582
+ case 'production':
585
583
  return '--prod'
586
584
  case 'staging':
587
585
  return '--staging'
@@ -634,13 +632,13 @@ class DxCli {
634
632
  }
635
633
 
636
634
  validateStartPositionals(positionalArgs) {
637
- const service = positionalArgs[0] || 'dev'
635
+ const service = positionalArgs[0] || 'development'
638
636
  const environment = this.determineEnvironment()
639
637
  const envKey = this.normalizeEnvKey(environment)
640
638
 
641
- if (service === 'dev' && envKey !== 'dev') {
639
+ if (service === 'development' && envKey !== 'development') {
642
640
  logger.error('dx start 在未指定服务时仅允许使用开发环境')
643
- logger.info(`示例: ${this.invocation} start all --dev`)
641
+ logger.info(`示例: ${this.invocation} start --dev`)
644
642
  logger.info(`示例: ${this.invocation} start backend --prod`)
645
643
  process.exit(1)
646
644
  }
@@ -649,7 +647,7 @@ class DxCli {
649
647
  if (!startConfig) return
650
648
 
651
649
  // 对 start 下的单层 command 配置,默认视为开发态目标,仅允许 --dev。
652
- if (startConfig.command && envKey !== 'dev') {
650
+ if (startConfig.command && envKey !== 'development') {
653
651
  logger.error(`启动目标 ${service} 仅支持开发环境`)
654
652
  logger.info(`示例: ${this.invocation} start ${service} --dev`)
655
653
  process.exit(1)
@@ -669,13 +667,13 @@ class DxCli {
669
667
  const buildConfig = this.commands?.build?.[target]
670
668
  if (!buildConfig || buildConfig.command) return
671
669
 
672
- const supportsCurrentEnv = Boolean(buildConfig[envKey] || (envKey === 'staging' && buildConfig.prod))
670
+ const supportsCurrentEnv = Boolean(buildConfig[envKey])
673
671
  if (supportsCurrentEnv) return
674
672
 
675
673
  const envFlag = this.getEnvironmentFlagExample(envKey) || `--${envKey}`
676
674
  logger.error(`构建目标 ${target} 不支持 ${envFlag} 环境`)
677
675
  logger.info('显式传入环境标志时,必须是该 target 实际支持的环境。')
678
- const available = ['dev', 'staging', 'prod', 'test', 'e2e']
676
+ const available = ['development', 'staging', 'production', 'test', 'e2e']
679
677
  .filter(key => key in buildConfig)
680
678
  .map(key => this.getEnvironmentFlagExample(key) || `--${key}`)
681
679
  if (available.length > 0) {
@@ -688,19 +686,6 @@ class DxCli {
688
686
  process.exit(1)
689
687
  }
690
688
 
691
- reportDevCommandRemoved(args) {
692
- const target = args?.[0]
693
- logger.error('`dx dev` 命令已移除,统一使用 `dx start`。')
694
- if (target) {
695
- logger.info(`请执行: ${this.invocation} start ${target} --dev`)
696
- } else {
697
- logger.info(`示例: ${this.invocation} start backend --dev`)
698
- logger.info(` ${this.invocation} start front --dev`)
699
- logger.info(` ${this.invocation} start admin --dev`)
700
- }
701
- process.exit(1)
702
- }
703
-
704
689
  reportExtraPositionals(command, extras) {
705
690
  const list = extras.join(', ')
706
691
  if (command === '全局') {
@@ -772,7 +757,7 @@ class DxCli {
772
757
  continue
773
758
  }
774
759
  commands.push({
775
- command: this.applySdkOfflineFlag(config.command),
760
+ command: config.command,
776
761
  options: {
777
762
  app: config.app,
778
763
  ports: config.ports,
@@ -820,22 +805,11 @@ class DxCli {
820
805
  if (environment && config) {
821
806
  const envKey = this.normalizeEnvKey(environment)
822
807
  if (config[envKey]) config = config[envKey]
823
- else if (envKey === 'staging' && config.prod) config = config.prod
824
808
  }
825
809
 
826
810
  return config
827
811
  }
828
812
 
829
- // SDK 构建命令当前不再暴露 --online/--offline 模式,保留该方法仅为兼容旧调用
830
- applySdkModeFlags(command) {
831
- return command
832
- }
833
-
834
- // 向后兼容的别名
835
- applySdkOfflineFlag(command) {
836
- return command
837
- }
838
-
839
813
  collectStartPorts(service, startConfig, envKey) {
840
814
  const portSet = new Set()
841
815
 
@@ -845,15 +819,6 @@ class DxCli {
845
819
  })
846
820
  }
847
821
 
848
- if (envKey === 'dev') {
849
- const legacyConfig = this.commands.dev?.[service]
850
- if (legacyConfig && Array.isArray(legacyConfig.ports)) {
851
- legacyConfig.ports.forEach(port => {
852
- this.addPortToSet(portSet, port)
853
- })
854
- }
855
- }
856
-
857
822
  return Array.from(portSet)
858
823
  }
859
824
 
@@ -936,28 +901,6 @@ class DxCli {
936
901
  }
937
902
 
938
903
  const rawCommand = String(config.command).trim()
939
- // backward compat: old commands.json referenced scripts/lib/*.js in the project
940
- if (rawCommand.startsWith('node scripts/lib/sdk-build.js')) {
941
- const argsText = rawCommand.replace(/^node\s+scripts\/lib\/sdk-build\.js\s*/g, '')
942
- const args = argsText ? argsText.split(/\s+/).filter(Boolean) : []
943
- await withTempEnv(async () => {
944
- const { runSdkBuild } = await import('../sdk-build.js')
945
- await runSdkBuild(args)
946
- })
947
- return
948
- }
949
-
950
- if (rawCommand.startsWith('node scripts/lib/backend-package.js')) {
951
- const argsText = rawCommand.replace(/^node\s+scripts\/lib\/backend-package\.js\s*/g, '')
952
- const args = argsText ? argsText.split(/\s+/).filter(Boolean) : []
953
- await withTempEnv(async () => {
954
- const { runBackendPackage } = await import('../backend-package.js')
955
- await runBackendPackage(args)
956
- })
957
- return
958
- }
959
-
960
- const command = this.applySdkOfflineFlag(rawCommand)
961
904
 
962
905
  const options = {
963
906
  app: config.app,
@@ -969,7 +912,7 @@ class DxCli {
969
912
  forcePortCleanup: Boolean(config.forcePortCleanup),
970
913
  }
971
914
 
972
- await execManager.executeCommand(command, options)
915
+ await execManager.executeCommand(rawCommand, options)
973
916
  }
974
917
 
975
918
  // 确定环境
@@ -977,17 +920,14 @@ class DxCli {
977
920
  return envManager.detectEnvironment(this.flags)
978
921
  }
979
922
 
980
- // 规范化环境键到命令配置使用的命名(dev/prod/test/e2e)
923
+ // 规范化环境键到命令配置使用的命名(development/staging/production/test/e2e)
981
924
  normalizeEnvKey(env) {
982
925
  switch (String(env || '').toLowerCase()) {
983
926
  case 'development':
984
- case 'dev':
985
- return 'dev'
927
+ return 'development'
986
928
  case 'production':
987
- case 'prod':
988
- return 'prod'
929
+ return 'production'
989
930
  case 'staging':
990
- case 'stage':
991
931
  return 'staging'
992
932
  case 'test':
993
933
  return 'test'
@@ -998,6 +938,23 @@ class DxCli {
998
938
  }
999
939
  }
1000
940
 
941
+ createExecutionFlags(environment) {
942
+ const envKey = this.normalizeEnvKey(environment)
943
+ const execFlags = { ...this.flags }
944
+
945
+ ;['dev', 'prod', 'staging', 'test', 'e2e'].forEach(key => {
946
+ delete execFlags[key]
947
+ })
948
+
949
+ if (envKey === 'production') execFlags.prod = true
950
+ else if (envKey === 'development') execFlags.dev = true
951
+ else if (envKey === 'test') execFlags.test = true
952
+ else if (envKey === 'e2e') execFlags.e2e = true
953
+ else if (envKey === 'staging') execFlags.staging = true
954
+
955
+ return execFlags
956
+ }
957
+
1001
958
  }
1002
959
 
1003
960
  export { DxCli }
package/lib/cli/flags.js CHANGED
@@ -1,11 +1,8 @@
1
1
  export const FLAG_DEFINITIONS = {
2
2
  _global: [
3
3
  { flag: '--dev' },
4
- { flag: '--development' },
5
4
  { flag: '--prod' },
6
- { flag: '--production' },
7
5
  { flag: '--staging' },
8
- { flag: '--stage' },
9
6
  { flag: '--test' },
10
7
  { flag: '--e2e' },
11
8
  { flag: '--no-env-check' },
@@ -59,15 +56,12 @@ export function parseFlags(args = []) {
59
56
  if (!flag.startsWith('-')) continue
60
57
  switch (flag) {
61
58
  case '--dev':
62
- case '--development':
63
59
  flags.dev = true
64
60
  break
65
61
  case '--prod':
66
- case '--production':
67
62
  flags.prod = true
68
63
  break
69
64
  case '--staging':
70
- case '--stage':
71
65
  flags.staging = true
72
66
  break
73
67
  case '--test':
@@ -0,0 +1,217 @@
1
+ const ENVIRONMENT_KEYS = new Set(['development', 'production', 'staging', 'test', 'e2e'])
2
+ const COMMAND_LIST_HIDDEN = new Set(['help'])
3
+ const META_KEYS = new Set(['help', 'description', 'args', 'interactive', 'dangerous'])
4
+ const INTERNAL_CONFIG_KEYS = new Set([
5
+ 'services',
6
+ 'urls',
7
+ 'preflight',
8
+ 'ecosystemConfig',
9
+ 'pm2Bin',
10
+ 'backendDeploy',
11
+ 'artifactDeploy',
12
+ 'telegramWebhook',
13
+ ])
14
+
15
+ export function buildHelpRuntimeContext(cli = {}) {
16
+ return {
17
+ registeredCommands: getRegisteredCommands(cli),
18
+ knownFlags: getKnownFlags(cli),
19
+ }
20
+ }
21
+
22
+ export function getRegisteredCommands(cli = {}) {
23
+ const handlers = cli?.commandHandlers
24
+ if (!handlers || typeof handlers !== 'object') return []
25
+
26
+ return Object.keys(handlers).filter(name => !COMMAND_LIST_HIDDEN.has(name))
27
+ }
28
+
29
+ export function getKnownFlags(cli = {}) {
30
+ const definitions = cli?.flagDefinitions
31
+ const knownFlags = new Map()
32
+
33
+ if (!definitions || typeof definitions !== 'object') return knownFlags
34
+
35
+ for (const entries of Object.values(definitions)) {
36
+ if (!Array.isArray(entries)) continue
37
+ for (const entry of entries) {
38
+ if (!entry?.flag) continue
39
+ knownFlags.set(entry.flag, {
40
+ expectsValue: Boolean(entry.expectsValue),
41
+ })
42
+ }
43
+ }
44
+
45
+ return knownFlags
46
+ }
47
+
48
+ export function classifyCommandNode(node = {}) {
49
+ if (node?.help?.nodeType) return node.help.nodeType
50
+ if (looksLikeInternalConfigBag(node)) return 'internal-config-bag'
51
+ if (node?.command || node?.internal) return 'target-leaf'
52
+ if (node?.concurrent || node?.sequential) return 'orchestration-node'
53
+ if (looksLikeEnvContainer(node)) return 'env-container'
54
+ if (looksLikeCategoryNode(node)) return 'category-node'
55
+ return 'unknown-node'
56
+ }
57
+
58
+ export function isVisibleHelpNode(name, node, nodeType = classifyCommandNode(node)) {
59
+ void name
60
+
61
+ if (node?.help?.expose === false) return false
62
+ if (nodeType === 'internal-config-bag') return false
63
+ if (nodeType === 'category-node') return false
64
+ if (nodeType === 'orchestration-node') return node?.help?.expose === true
65
+ return true
66
+ }
67
+
68
+ export function getGlobalHelpModel(commands = {}, context = {}) {
69
+ const registeredCommands = Array.isArray(context?.registeredCommands)
70
+ ? context.registeredCommands
71
+ : []
72
+
73
+ return {
74
+ summary: commands?.help?.summary ?? '',
75
+ commands: registeredCommands.map(name => getCommandHelpModel(commands, name, context)),
76
+ globalOptions: normalizeArray(commands?.help?.globalOptions),
77
+ examples: normalizeArray(commands?.help?.examples),
78
+ }
79
+ }
80
+
81
+ export function getCommandHelpModel(commands = {}, commandName, context = {}) {
82
+ const commandConfig = commands?.[commandName] ?? {}
83
+ const commandHelp = getCommandHelpConfig(commands, commandName)
84
+
85
+ return {
86
+ name: commandName,
87
+ summary: resolveSummary(commandConfig, commandHelp),
88
+ usage: resolveUsage(commandName, commandHelp, commandConfig),
89
+ args: normalizeArray(commandHelp?.args),
90
+ notes: normalizeArray(commandHelp?.notes),
91
+ examples: normalizeArray(commandHelp?.examples),
92
+ options: normalizeArray(commandHelp?.options),
93
+ targets: resolveVisibleTargets(commands, commandName, commandConfig, context),
94
+ }
95
+ }
96
+
97
+ export function resolveSummary(node = {}, help = null) {
98
+ return help?.summary || node?.help?.summary || node?.description || ''
99
+ }
100
+
101
+ function resolveUsage(commandName, commandHelp = {}, commandConfig = {}) {
102
+ return commandHelp?.usage || generateUsage(commandName, commandConfig)
103
+ }
104
+
105
+ function generateUsage(commandName, commandConfig = {}) {
106
+ switch (commandName) {
107
+ case 'start':
108
+ return 'dx start <service> [环境标志]'
109
+ case 'build':
110
+ case 'package':
111
+ return `dx ${commandName} <target> [环境标志]`
112
+ case 'db':
113
+ return 'dx db <action> [name] [环境标志]'
114
+ case 'test':
115
+ return 'dx test [type] <target> [path]'
116
+ case 'deploy':
117
+ case 'clean':
118
+ case 'cache':
119
+ return `dx ${commandName} <target>`
120
+ case 'help':
121
+ return 'dx help [command]'
122
+ case 'worktree':
123
+ return 'dx worktree [action] [args...]'
124
+ case 'lint':
125
+ case 'status':
126
+ return `dx ${commandName}`
127
+ default:
128
+ return hasVisibleTargets(commandConfig) ? `dx ${commandName} <target>` : `dx ${commandName}`
129
+ }
130
+ }
131
+
132
+ function hasVisibleTargets(commandConfig) {
133
+ if (!isPlainObject(commandConfig)) return false
134
+
135
+ return Object.entries(commandConfig).some(([name, node]) => {
136
+ if (name === 'help') return false
137
+ return isVisibleHelpNode(name, node)
138
+ })
139
+ }
140
+
141
+ function resolveVisibleTargets(commands, commandName, commandConfig, context) {
142
+ void context
143
+
144
+ if (!isPlainObject(commandConfig)) return []
145
+
146
+ return Object.entries(commandConfig)
147
+ .filter(([name]) => name !== 'help')
148
+ .map(([name, node]) => {
149
+ const nodeType = classifyCommandNode(node)
150
+ if (!isVisibleHelpNode(name, node, nodeType)) return null
151
+
152
+ const targetHelp = getTargetHelpConfig(commands, commandName, name)
153
+
154
+ return {
155
+ name,
156
+ nodeType,
157
+ summary: resolveSummary(node, targetHelp),
158
+ notes: normalizeArray(targetHelp?.notes),
159
+ options: normalizeArray(targetHelp?.options),
160
+ examples: normalizeArray(targetHelp?.examples),
161
+ }
162
+ })
163
+ .filter(Boolean)
164
+ }
165
+
166
+ function getCommandHelpConfig(commands, commandName) {
167
+ const config = commands?.help?.commands?.[commandName]
168
+ return isPlainObject(config) ? config : {}
169
+ }
170
+
171
+ function getTargetHelpConfig(commands, commandName, targetName) {
172
+ const config = commands?.help?.targets?.[commandName]?.[targetName]
173
+ return isPlainObject(config) ? config : {}
174
+ }
175
+
176
+ function looksLikeInternalConfigBag(node) {
177
+ if (!isPlainObject(node)) return false
178
+ if (node.command || node.internal || node.concurrent || node.sequential) return false
179
+ if (looksLikeEnvContainer(node)) return false
180
+
181
+ const visibleKeys = getVisibleKeys(node)
182
+ if (visibleKeys.length === 0) return false
183
+
184
+ return visibleKeys.some(key => INTERNAL_CONFIG_KEYS.has(key))
185
+ }
186
+
187
+ function looksLikeEnvContainer(node) {
188
+ if (!isPlainObject(node)) return false
189
+ const visibleKeys = getVisibleKeys(node)
190
+ if (visibleKeys.length === 0) return false
191
+
192
+ const envKeys = visibleKeys.filter(key => ENVIRONMENT_KEYS.has(key))
193
+ return envKeys.length > 0 && envKeys.length === visibleKeys.length
194
+ }
195
+
196
+ function looksLikeCategoryNode(node) {
197
+ if (!isPlainObject(node)) return false
198
+ if (node.command || node.internal || node.concurrent || node.sequential) return false
199
+ if (looksLikeEnvContainer(node) || looksLikeInternalConfigBag(node)) return false
200
+
201
+ const visibleKeys = getVisibleKeys(node)
202
+ if (visibleKeys.length === 0) return false
203
+
204
+ return visibleKeys.every(key => isPlainObject(node[key]))
205
+ }
206
+
207
+ function getVisibleKeys(node) {
208
+ return Object.keys(node).filter(key => !META_KEYS.has(key))
209
+ }
210
+
211
+ function normalizeArray(value) {
212
+ return Array.isArray(value) ? value : []
213
+ }
214
+
215
+ function isPlainObject(value) {
216
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
217
+ }