@ranger1/dx 0.1.96 → 0.1.97
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/commands/core.js +12 -20
- package/lib/cli/commands/db.js +22 -32
- package/lib/cli/commands/export.js +7 -2
- package/lib/cli/commands/start.js +14 -33
- package/lib/cli/commands/worktree.js +0 -4
- package/lib/cli/dx-cli.js +46 -98
- package/lib/cli/flags.js +0 -6
- package/lib/cli/help-model.js +217 -0
- package/lib/cli/help-renderer.js +135 -0
- package/lib/cli/help-schema.js +549 -0
- package/lib/cli/help.js +114 -292
- package/lib/env.js +4 -5
- package/lib/worktree.js +37 -17
- package/package.json +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { logger } from '../logger.js'
|
|
2
|
+
import { parseFlags } from './flags.js'
|
|
3
|
+
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function assertOptionalString(value, path) {
|
|
9
|
+
if (value === undefined) return
|
|
10
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
11
|
+
throw new Error(`${path} must be a non-empty string`)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertArrayOfObjects(value, path) {
|
|
16
|
+
if (value === undefined) return
|
|
17
|
+
if (!Array.isArray(value)) {
|
|
18
|
+
throw new Error(`${path} must be an array`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
value.forEach((entry, index) => {
|
|
22
|
+
if (!isPlainObject(entry)) {
|
|
23
|
+
throw new Error(`${path}[${index}] must be an object`)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assertArray(value, path) {
|
|
29
|
+
if (value === undefined) return
|
|
30
|
+
if (!Array.isArray(value)) {
|
|
31
|
+
throw new Error(`${path} must be an array`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertDescription(value, path) {
|
|
36
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
37
|
+
throw new Error(`${path} must include a description`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertFlagsExist(flags, knownFlags, path) {
|
|
42
|
+
if (!Array.isArray(flags) || flags.length === 0) {
|
|
43
|
+
throw new Error(`${path} must include at least one flag`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
flags.forEach((flag, index) => {
|
|
47
|
+
if (typeof flag !== 'string' || flag.trim() === '') {
|
|
48
|
+
throw new Error(`${path}[${index}] must be a non-empty string`)
|
|
49
|
+
}
|
|
50
|
+
if (!knownFlags.has(flag)) {
|
|
51
|
+
throw new Error(`${path}[${index}] references unknown flag: ${flag}`)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractCommandName(command) {
|
|
57
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
58
|
+
return ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tokens = command.trim().split(/\s+/)
|
|
62
|
+
|
|
63
|
+
if (tokens[0] === 'dx') {
|
|
64
|
+
return tokens[1] ?? ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return tokens[0] ?? ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertRegisteredCommand(command, registeredCommands, path) {
|
|
71
|
+
const commandName = extractCommandName(command)
|
|
72
|
+
|
|
73
|
+
if (!commandName) {
|
|
74
|
+
throw new Error(`${path} must include a command`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!registeredCommands.has(commandName)) {
|
|
78
|
+
throw new Error(`${path} references unknown command: ${commandName}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateHelpExamples(examples, path, context) {
|
|
83
|
+
const { registeredCommands, exampleValidator } = context
|
|
84
|
+
|
|
85
|
+
assertArrayOfObjects(examples, path)
|
|
86
|
+
|
|
87
|
+
examples?.forEach((example, index) => {
|
|
88
|
+
const entryPath = `${path}[${index}]`
|
|
89
|
+
assertOptionalString(example.command, `${entryPath}.command`)
|
|
90
|
+
assertDescription(example.description, `${entryPath}.description`)
|
|
91
|
+
assertRegisteredCommand(example.command, registeredCommands, `${entryPath}.command`)
|
|
92
|
+
|
|
93
|
+
const result = exampleValidator(example.command)
|
|
94
|
+
if (!result?.ok) {
|
|
95
|
+
throw new Error(result?.reason || `${entryPath}.command failed validation`)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateHelpOptions(options, path, knownFlags) {
|
|
101
|
+
assertArrayOfObjects(options, path)
|
|
102
|
+
|
|
103
|
+
options?.forEach((option, index) => {
|
|
104
|
+
const entryPath = `${path}[${index}]`
|
|
105
|
+
assertFlagsExist(option.flags, knownFlags, `${entryPath}.flags`)
|
|
106
|
+
assertDescription(option.description, `${entryPath}.description`)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateCommandHelp(commandName, help, context) {
|
|
111
|
+
const { registeredCommands, usageValidator } = context
|
|
112
|
+
|
|
113
|
+
if (!isPlainObject(help)) {
|
|
114
|
+
throw new Error(`help.commands.${commandName} must be an object`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!registeredCommands.has(commandName)) {
|
|
118
|
+
throw new Error(`help.commands.${commandName} references unknown command`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assertOptionalString(help.summary, `help.commands.${commandName}.summary`)
|
|
122
|
+
assertArray(help.notes, `help.commands.${commandName}.notes`)
|
|
123
|
+
|
|
124
|
+
help.notes?.forEach((note, index) => {
|
|
125
|
+
assertOptionalString(note, `help.commands.${commandName}.notes[${index}]`)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
validateHelpOptions(
|
|
129
|
+
help.options,
|
|
130
|
+
`help.commands.${commandName}.options`,
|
|
131
|
+
context.knownFlags,
|
|
132
|
+
)
|
|
133
|
+
validateHelpExamples(help.examples, `help.commands.${commandName}.examples`, context)
|
|
134
|
+
|
|
135
|
+
if (help.usage !== undefined) {
|
|
136
|
+
assertOptionalString(help.usage, `help.commands.${commandName}.usage`)
|
|
137
|
+
const result = usageValidator(commandName, help.usage)
|
|
138
|
+
|
|
139
|
+
if (!result?.ok) {
|
|
140
|
+
throw new Error(result?.reason || `help.commands.${commandName}.usage failed validation`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateTargetHelp(commandName, targetName, help, commands, context) {
|
|
146
|
+
const entryPath = `help.targets.${commandName}.${targetName}`
|
|
147
|
+
|
|
148
|
+
if (!isPlainObject(help)) {
|
|
149
|
+
throw new Error(`${entryPath} must be an object`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!context.registeredCommands.has(commandName) || !isPlainObject(commands?.[commandName])) {
|
|
153
|
+
throw new Error(`help.targets.${commandName} references unknown command`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!Object.prototype.hasOwnProperty.call(commands[commandName], targetName)) {
|
|
157
|
+
throw new Error(`${entryPath} references unknown target`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
assertOptionalString(help.summary, `${entryPath}.summary`)
|
|
161
|
+
assertArray(help.notes, `${entryPath}.notes`)
|
|
162
|
+
|
|
163
|
+
help.notes?.forEach((note, index) => {
|
|
164
|
+
assertOptionalString(note, `${entryPath}.notes[${index}]`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
validateHelpOptions(help.options, `${entryPath}.options`, context.knownFlags)
|
|
168
|
+
validateHelpExamples(help.examples, `${entryPath}.examples`, context)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function validateHelpConfig(commands, context = {}) {
|
|
172
|
+
const {
|
|
173
|
+
registeredCommands = [],
|
|
174
|
+
knownFlags = new Map(),
|
|
175
|
+
usageValidator = () => ({ ok: true }),
|
|
176
|
+
exampleValidator = () => ({ ok: true }),
|
|
177
|
+
} = context
|
|
178
|
+
|
|
179
|
+
if (!isPlainObject(commands)) {
|
|
180
|
+
throw new Error('commands must be an object')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const normalizedContext = {
|
|
184
|
+
registeredCommands: new Set(registeredCommands),
|
|
185
|
+
knownFlags,
|
|
186
|
+
usageValidator,
|
|
187
|
+
exampleValidator,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const help = commands.help
|
|
191
|
+
if (help === undefined) {
|
|
192
|
+
return commands
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!isPlainObject(help)) {
|
|
196
|
+
throw new Error('help must be an object')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
assertOptionalString(help.summary, 'help.summary')
|
|
200
|
+
validateHelpOptions(help.globalOptions, 'help.globalOptions', normalizedContext.knownFlags)
|
|
201
|
+
validateHelpExamples(help.examples, 'help.examples', normalizedContext)
|
|
202
|
+
|
|
203
|
+
if (help.commands !== undefined) {
|
|
204
|
+
if (!isPlainObject(help.commands)) {
|
|
205
|
+
throw new Error('help.commands must be an object')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Object.entries(help.commands).forEach(([commandName, commandHelp]) => {
|
|
209
|
+
validateCommandHelp(commandName, commandHelp, normalizedContext)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (help.targets !== undefined) {
|
|
214
|
+
if (!isPlainObject(help.targets)) {
|
|
215
|
+
throw new Error('help.targets must be an object')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Object.entries(help.targets).forEach(([commandName, targetHelp]) => {
|
|
219
|
+
if (!isPlainObject(targetHelp)) {
|
|
220
|
+
throw new Error(`help.targets.${commandName} must be an object`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Object.entries(targetHelp).forEach(([targetName, targetEntryHelp]) => {
|
|
224
|
+
validateTargetHelp(commandName, targetName, targetEntryHelp, commands, normalizedContext)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return commands
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildStrictHelpValidationContext(cli) {
|
|
233
|
+
const knownFlags = new Map()
|
|
234
|
+
for (const definitions of Object.values(cli?.flagDefinitions || {})) {
|
|
235
|
+
if (!Array.isArray(definitions)) continue
|
|
236
|
+
for (const definition of definitions) {
|
|
237
|
+
if (!definition?.flag) continue
|
|
238
|
+
knownFlags.set(definition.flag, definition)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
registeredCommands: Object.keys(cli?.commandHandlers || {}),
|
|
244
|
+
knownFlags,
|
|
245
|
+
usageValidator: (commandName, usageText) =>
|
|
246
|
+
validateUsageAgainstRuntime(commandName, usageText, cli),
|
|
247
|
+
exampleValidator: commandText => validateExampleCommandAgainstCli(commandText, cli),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function validateExampleCommandAgainstCli(commandText, cli) {
|
|
252
|
+
let tokens
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
tokens = shellLikeSplit(commandText)
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return { ok: false, reason: error.message }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (tokens[0] !== cli.invocation) {
|
|
261
|
+
return { ok: false, reason: `example must start with ${cli.invocation}` }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const commandName = tokens[1]
|
|
265
|
+
if (!commandName) {
|
|
266
|
+
return { ok: false, reason: 'example must include a top-level command' }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!cli.commandHandlers?.[commandName]) {
|
|
270
|
+
return { ok: false, reason: `unknown command: ${commandName}` }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return runCliInputValidation(cli, tokens.slice(1))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function validateUsageAgainstRuntime(commandName, usageText, cli) {
|
|
277
|
+
let tokens
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
tokens = shellLikeSplit(usageText)
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return { ok: false, reason: error.message }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (tokens[0] !== cli.invocation) {
|
|
286
|
+
return { ok: false, reason: `usage must start with ${cli.invocation}` }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (tokens[1] !== commandName) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
reason: `usage for ${commandName} must start with "${cli.invocation} ${commandName}"`,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const placeholderTokens = tokens
|
|
297
|
+
.slice(2)
|
|
298
|
+
.filter(token => isPlaceholderToken(token) && !isEnvironmentPlaceholder(token))
|
|
299
|
+
const runtimeMaxPositionals = getRuntimeMaxPositionals(commandName)
|
|
300
|
+
|
|
301
|
+
if (
|
|
302
|
+
Number.isInteger(runtimeMaxPositionals) &&
|
|
303
|
+
placeholderTokens.length > runtimeMaxPositionals
|
|
304
|
+
) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
reason: `usage for ${commandName} advertises ${placeholderTokens.length} positionals but runtime allows ${runtimeMaxPositionals}`,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (mentionsPositionalEnvironmentPlaceholder(tokens)) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
reason: `usage for ${commandName} must use environment flags instead of positional env placeholders`,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (commandName === 'start' && !tokens.includes('<service>')) {
|
|
319
|
+
return { ok: false, reason: 'usage for start must include <service>' }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { ok: true }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getRuntimeMaxPositionals(commandName) {
|
|
326
|
+
switch (commandName) {
|
|
327
|
+
case 'help':
|
|
328
|
+
return 1
|
|
329
|
+
case 'build':
|
|
330
|
+
case 'package':
|
|
331
|
+
case 'clean':
|
|
332
|
+
case 'cache':
|
|
333
|
+
return 1
|
|
334
|
+
case 'db':
|
|
335
|
+
return 2
|
|
336
|
+
case 'test':
|
|
337
|
+
return 3
|
|
338
|
+
case 'worktree':
|
|
339
|
+
return 3
|
|
340
|
+
case 'start':
|
|
341
|
+
return 1
|
|
342
|
+
case 'lint':
|
|
343
|
+
case 'status':
|
|
344
|
+
return 0
|
|
345
|
+
default:
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function runCliInputValidation(cli, args) {
|
|
351
|
+
if (typeof cli?.validateInputs !== 'function') {
|
|
352
|
+
return validateArgumentTokens(args, cli)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const exitSpy = process.exit
|
|
356
|
+
const originalLogger = {
|
|
357
|
+
error: logger.error,
|
|
358
|
+
info: logger.info,
|
|
359
|
+
}
|
|
360
|
+
const messages = []
|
|
361
|
+
const originalState = {
|
|
362
|
+
args: cli.args,
|
|
363
|
+
command: cli.command,
|
|
364
|
+
flags: cli.flags,
|
|
365
|
+
subcommand: cli.subcommand,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
process.exit = code => {
|
|
370
|
+
throw new Error(`process.exit:${code}`)
|
|
371
|
+
}
|
|
372
|
+
logger.error = message => {
|
|
373
|
+
messages.push(String(message))
|
|
374
|
+
}
|
|
375
|
+
logger.info = message => {
|
|
376
|
+
messages.push(String(message))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
cli.args = [...args]
|
|
380
|
+
cli.flags = parseFlags(cli.args)
|
|
381
|
+
cli.command = cli.args[0]
|
|
382
|
+
cli.subcommand = cli.args[1]
|
|
383
|
+
cli.validateInputs()
|
|
384
|
+
return { ok: true }
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (String(error?.message || '').startsWith('process.exit:')) {
|
|
387
|
+
return { ok: false, reason: messages.join(' | ') || error.message }
|
|
388
|
+
}
|
|
389
|
+
throw error
|
|
390
|
+
} finally {
|
|
391
|
+
process.exit = exitSpy
|
|
392
|
+
logger.error = originalLogger.error
|
|
393
|
+
logger.info = originalLogger.info
|
|
394
|
+
cli.args = originalState.args
|
|
395
|
+
cli.command = originalState.command
|
|
396
|
+
cli.flags = originalState.flags
|
|
397
|
+
cli.subcommand = originalState.subcommand
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function validateArgumentTokens(args, cli) {
|
|
402
|
+
const tokens = Array.isArray(args) ? [...args] : []
|
|
403
|
+
const commandName = tokens[0]
|
|
404
|
+
if (!commandName) {
|
|
405
|
+
return { ok: false, reason: 'example must include a top-level command' }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const definitions = cli?.flagDefinitions || {}
|
|
409
|
+
const knownFlags = new Map()
|
|
410
|
+
for (const entries of Object.values(definitions)) {
|
|
411
|
+
if (!Array.isArray(entries)) continue
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
if (!entry?.flag) continue
|
|
414
|
+
knownFlags.set(entry.flag, Boolean(entry.expectsValue))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const positionalArgs = []
|
|
419
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
420
|
+
const token = tokens[index]
|
|
421
|
+
if (token === '--') break
|
|
422
|
+
if (!token.startsWith('-')) {
|
|
423
|
+
positionalArgs.push(token)
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!knownFlags.has(token)) {
|
|
428
|
+
return { ok: false, reason: `检测到未识别的选项: ${token}` }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (knownFlags.get(token)) {
|
|
432
|
+
const next = tokens[index + 1]
|
|
433
|
+
if (next === undefined || next.startsWith('-')) {
|
|
434
|
+
return { ok: false, reason: `选项 ${token} 需要提供参数值` }
|
|
435
|
+
}
|
|
436
|
+
index += 1
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const maxByCommand = {
|
|
441
|
+
help: 1,
|
|
442
|
+
build: 1,
|
|
443
|
+
package: 1,
|
|
444
|
+
db: 2,
|
|
445
|
+
test: 3,
|
|
446
|
+
worktree: 3,
|
|
447
|
+
start: 1,
|
|
448
|
+
lint: 0,
|
|
449
|
+
clean: 1,
|
|
450
|
+
cache: 1,
|
|
451
|
+
status: 0,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const max = maxByCommand[commandName]
|
|
455
|
+
if (Number.isInteger(max) && positionalArgs.length > max) {
|
|
456
|
+
return { ok: false, reason: `命令 ${commandName} 存在未识别的额外参数: ${positionalArgs.slice(max).join(', ')}` }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return { ok: true }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function isPlaceholderToken(token) {
|
|
463
|
+
return (
|
|
464
|
+
(token.startsWith('<') && token.endsWith('>')) ||
|
|
465
|
+
(token.startsWith('[') && token.endsWith(']'))
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isEnvironmentPlaceholder(token) {
|
|
470
|
+
return token === '[环境标志]'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function mentionsPositionalEnvironmentPlaceholder(tokens) {
|
|
474
|
+
return tokens.some(token => {
|
|
475
|
+
const normalized = token.replace(/^[<[|]+|[\]>|]+$/g, '').toLowerCase()
|
|
476
|
+
return normalized === 'env' || normalized === 'environment'
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function shellLikeSplit(text) {
|
|
481
|
+
const tokens = []
|
|
482
|
+
let current = ''
|
|
483
|
+
let quote = null
|
|
484
|
+
let tokenStarted = false
|
|
485
|
+
|
|
486
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
487
|
+
const character = text[index]
|
|
488
|
+
|
|
489
|
+
if (quote) {
|
|
490
|
+
if (character === '\\') {
|
|
491
|
+
const next = text[index + 1]
|
|
492
|
+
if (next === quote || next === '\\') {
|
|
493
|
+
current += next
|
|
494
|
+
tokenStarted = true
|
|
495
|
+
index += 1
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (character === quote) {
|
|
501
|
+
quote = null
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
current += character
|
|
506
|
+
tokenStarted = true
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (character === '"' || character === "'") {
|
|
511
|
+
quote = character
|
|
512
|
+
tokenStarted = true
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (character === '\\') {
|
|
517
|
+
const next = text[index + 1]
|
|
518
|
+
if (next !== undefined) {
|
|
519
|
+
current += next
|
|
520
|
+
tokenStarted = true
|
|
521
|
+
index += 1
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (/\s/.test(character)) {
|
|
527
|
+
if (tokenStarted) {
|
|
528
|
+
tokens.push(current)
|
|
529
|
+
current = ''
|
|
530
|
+
tokenStarted = false
|
|
531
|
+
}
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
current += character
|
|
536
|
+
tokenStarted = true
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (quote) {
|
|
540
|
+
const quoteName = quote === '"' ? 'double' : 'single'
|
|
541
|
+
throw new Error(`Unclosed ${quoteName} quote in "${text}"`)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (tokenStarted) {
|
|
545
|
+
tokens.push(current)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return tokens
|
|
549
|
+
}
|