@amritk/mjst 0.5.1 → 0.7.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/README.md +42 -4
- package/dist/cli-config.d.ts +27 -3
- package/dist/cli.js +442 -250
- package/dist/combine-files.d.ts +13 -0
- package/dist/parse-cli-args.d.ts +4 -2
- package/dist/read-version.d.ts +8 -0
- package/package.json +4 -4
- package/src/cli-config.ts +27 -3
- package/src/cli.ts +269 -67
- package/src/combine-files.test.ts +49 -0
- package/src/combine-files.ts +43 -0
- package/src/load-config.test.ts +9 -0
- package/src/load-config.ts +3 -0
- package/src/parse-cli-args.test.ts +48 -0
- package/src/parse-cli-args.ts +123 -94
- package/src/read-version.test.ts +18 -0
- package/src/read-version.ts +14 -0
|
@@ -43,6 +43,26 @@ describe('parse-cli-args', () => {
|
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
+
it('parses --schemaDir with space-separated and equals syntax', () => {
|
|
47
|
+
expect(parseCliArgs(['--schemaDir', './schemas', '--outDir', 'dist'])).toEqual({
|
|
48
|
+
schemaDir: './schemas',
|
|
49
|
+
outDir: 'dist',
|
|
50
|
+
})
|
|
51
|
+
expect(parseCliArgs(['--schemaDir=./schemas', '--outDir=dist'])).toEqual({
|
|
52
|
+
schemaDir: './schemas',
|
|
53
|
+
outDir: 'dist',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('keeps --schema and --schemaDir distinct', () => {
|
|
58
|
+
const result = parseCliArgs(['--schema', 'a.json', '--schemaDir', './schemas'])
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual({
|
|
61
|
+
schema: 'a.json',
|
|
62
|
+
schemaDir: './schemas',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
46
66
|
it('handles only --outDir without --schema', () => {
|
|
47
67
|
const result = parseCliArgs(['--outDir', 'output'])
|
|
48
68
|
|
|
@@ -223,4 +243,32 @@ describe('parse-cli-args', () => {
|
|
|
223
243
|
const result = parseCliArgs(['--helpers=foo', '--schema', 's.json'])
|
|
224
244
|
expect(result).toEqual({ schema: 's.json' })
|
|
225
245
|
})
|
|
246
|
+
|
|
247
|
+
it('accepts kebab-case flag names', () => {
|
|
248
|
+
const result = parseCliArgs(['--schema-dir', './schemas', '--out-dir', 'dist'])
|
|
249
|
+
|
|
250
|
+
expect(result).toEqual({
|
|
251
|
+
schemaDir: './schemas',
|
|
252
|
+
outDir: 'dist',
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('treats kebab-case and camelCase variants as equivalent', () => {
|
|
257
|
+
const kebab = parseCliArgs(['--out-dir', 'dist', '--types-only', '--log-warnings'])
|
|
258
|
+
const camel = parseCliArgs(['--outDir', 'dist', '--typesOnly', '--logWarnings'])
|
|
259
|
+
|
|
260
|
+
expect(kebab).toEqual(camel)
|
|
261
|
+
expect(camel).toEqual({ outDir: 'dist', typesOnly: true, logWarnings: true })
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('parses --out-file with space-separated and equals syntax', () => {
|
|
265
|
+
expect(parseCliArgs(['--out-file', './out/schema.ts'])).toEqual({ outFile: './out/schema.ts' })
|
|
266
|
+
expect(parseCliArgs(['--outFile=./out/schema.ts'])).toEqual({ outFile: './out/schema.ts' })
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('parses --readonly boolean flag and its equals forms', () => {
|
|
270
|
+
expect(parseCliArgs(['--readonly'])).toEqual({ readonly: true })
|
|
271
|
+
expect(parseCliArgs(['--readonly=true'])).toEqual({ readonly: true })
|
|
272
|
+
expect(parseCliArgs(['--readonly=false'])).toEqual({ readonly: false })
|
|
273
|
+
})
|
|
226
274
|
})
|
package/src/parse-cli-args.ts
CHANGED
|
@@ -4,123 +4,152 @@ import type { CliConfig } from './cli-config'
|
|
|
4
4
|
|
|
5
5
|
const SOURCE_FORMATS: readonly SourceFormat[] = ['json', 'typebox', 'zod', 'valibot', 'effect']
|
|
6
6
|
|
|
7
|
+
// Mutable shape used while building, returned as Partial<CliConfig>.
|
|
8
|
+
type MutableConfig = {
|
|
9
|
+
schema?: string
|
|
10
|
+
schemaDir?: string
|
|
11
|
+
outDir?: string
|
|
12
|
+
outFile?: string
|
|
13
|
+
input?: SourceFormat
|
|
14
|
+
export?: string
|
|
15
|
+
typesOnly?: boolean
|
|
16
|
+
build?: boolean
|
|
17
|
+
logWarnings?: boolean
|
|
18
|
+
strict?: boolean
|
|
19
|
+
readonly?: boolean
|
|
20
|
+
helpers?: 'package' | 'embedded'
|
|
21
|
+
typeSuffix?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Boolean flags toggle on by presence and accept `--flag=false` to opt out.
|
|
25
|
+
const BOOLEAN_KEYS = new Set<keyof MutableConfig>(['typesOnly', 'build', 'logWarnings', 'strict', 'readonly'])
|
|
26
|
+
// Value flags consume the following argument (or `--flag=value`).
|
|
27
|
+
const VALUE_KEYS = new Set<keyof MutableConfig>([
|
|
28
|
+
'schema',
|
|
29
|
+
'schemaDir',
|
|
30
|
+
'outDir',
|
|
31
|
+
'outFile',
|
|
32
|
+
'input',
|
|
33
|
+
'export',
|
|
34
|
+
'helpers',
|
|
35
|
+
'typeSuffix',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/** Normalizes a CLI flag name to its camelCase config key so both `--out-dir` and `--outDir` map to `outDir`. */
|
|
39
|
+
const toCamelCase = (key: string): string => key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
40
|
+
|
|
41
|
+
const parseHelpersValue = (value: string): 'package' | 'embedded' | undefined => {
|
|
42
|
+
if (value === 'package' || value === 'embedded') return value
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parseInputValue = (value: string): SourceFormat | undefined =>
|
|
47
|
+
(SOURCE_FORMATS as readonly string[]).includes(value) ? (value as SourceFormat) : undefined
|
|
48
|
+
|
|
49
|
+
/** Assigns a value-flag onto the config. Returns false for unknown keys. */
|
|
50
|
+
const assignValue = (config: MutableConfig, key: string, value: string): boolean => {
|
|
51
|
+
switch (key) {
|
|
52
|
+
case 'schema':
|
|
53
|
+
config.schema = value
|
|
54
|
+
return true
|
|
55
|
+
case 'schemaDir':
|
|
56
|
+
config.schemaDir = value
|
|
57
|
+
return true
|
|
58
|
+
case 'outDir':
|
|
59
|
+
config.outDir = value
|
|
60
|
+
return true
|
|
61
|
+
case 'outFile':
|
|
62
|
+
config.outFile = value
|
|
63
|
+
return true
|
|
64
|
+
case 'export':
|
|
65
|
+
config.export = value
|
|
66
|
+
return true
|
|
67
|
+
case 'typeSuffix':
|
|
68
|
+
config.typeSuffix = value
|
|
69
|
+
return true
|
|
70
|
+
case 'input': {
|
|
71
|
+
const parsed = parseInputValue(value)
|
|
72
|
+
if (parsed) config.input = parsed
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
case 'helpers': {
|
|
76
|
+
const parsed = parseHelpersValue(value)
|
|
77
|
+
if (parsed) config.helpers = parsed
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Assigns a boolean flag onto the config. Returns false for unknown keys. */
|
|
86
|
+
const assignBoolean = (config: MutableConfig, key: string, value: boolean): boolean => {
|
|
87
|
+
switch (key) {
|
|
88
|
+
case 'typesOnly':
|
|
89
|
+
config.typesOnly = value
|
|
90
|
+
return true
|
|
91
|
+
case 'build':
|
|
92
|
+
config.build = value
|
|
93
|
+
return true
|
|
94
|
+
case 'logWarnings':
|
|
95
|
+
config.logWarnings = value
|
|
96
|
+
return true
|
|
97
|
+
case 'strict':
|
|
98
|
+
config.strict = value
|
|
99
|
+
return true
|
|
100
|
+
case 'readonly':
|
|
101
|
+
config.readonly = value
|
|
102
|
+
return true
|
|
103
|
+
default:
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
7
108
|
/**
|
|
8
109
|
* Parses command-line arguments into a partial CLI config.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
110
|
+
* Every flag accepts both kebab-case and camelCase (e.g. `--out-dir` and `--outDir`)
|
|
111
|
+
* and supports either `--flag value` or `--flag=value` syntax. Boolean flags toggle
|
|
112
|
+
* on by presence and accept `--flag=false` to opt out. Only flags that were explicitly
|
|
113
|
+
* provided are returned so they can be layered on top of config file values.
|
|
11
114
|
*/
|
|
12
115
|
export const parseCliArgs = (args: readonly string[]): Partial<CliConfig> => {
|
|
13
|
-
|
|
14
|
-
const config: {
|
|
15
|
-
schema?: string
|
|
16
|
-
outDir?: string
|
|
17
|
-
input?: SourceFormat
|
|
18
|
-
export?: string
|
|
19
|
-
typesOnly?: boolean
|
|
20
|
-
build?: boolean
|
|
21
|
-
logWarnings?: boolean
|
|
22
|
-
strict?: boolean
|
|
23
|
-
helpers?: 'package' | 'embedded'
|
|
24
|
-
} = {}
|
|
25
|
-
|
|
26
|
-
const parseHelpersValue = (value: string): 'package' | 'embedded' | undefined => {
|
|
27
|
-
if (value === 'package' || value === 'embedded') return value
|
|
28
|
-
return undefined
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const parseInputValue = (value: string): SourceFormat | undefined =>
|
|
32
|
-
(SOURCE_FORMATS as readonly string[]).includes(value) ? (value as SourceFormat) : undefined
|
|
116
|
+
const config: MutableConfig = {}
|
|
33
117
|
|
|
34
118
|
for (let i = 0; i < args.length; i++) {
|
|
35
119
|
const arg = args[i]
|
|
36
120
|
|
|
37
|
-
if (!arg) {
|
|
121
|
+
if (!arg || !arg.startsWith('--')) {
|
|
38
122
|
continue
|
|
39
123
|
}
|
|
40
124
|
|
|
41
125
|
// Handle --flag=value syntax
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const key = arg.slice(2, equalsIndex)
|
|
126
|
+
const equalsIndex = arg.indexOf('=')
|
|
127
|
+
if (equalsIndex !== -1) {
|
|
128
|
+
const key = toCamelCase(arg.slice(2, equalsIndex))
|
|
45
129
|
const value = arg.slice(equalsIndex + 1)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
config.outDir = value
|
|
51
|
-
} else if (key === 'input') {
|
|
52
|
-
const parsed = parseInputValue(value)
|
|
53
|
-
if (parsed) config.input = parsed
|
|
54
|
-
} else if (key === 'export') {
|
|
55
|
-
config.export = value
|
|
56
|
-
} else if (key === 'types-only') {
|
|
57
|
-
// Accept --types-only=false to explicitly opt out, otherwise treat as true
|
|
58
|
-
config.typesOnly = value !== 'false'
|
|
59
|
-
} else if (key === 'build') {
|
|
60
|
-
config.build = value !== 'false'
|
|
61
|
-
} else if (key === 'log-warnings') {
|
|
62
|
-
config.logWarnings = value !== 'false'
|
|
63
|
-
} else if (key === 'strict') {
|
|
64
|
-
config.strict = value !== 'false'
|
|
65
|
-
} else if (key === 'helpers') {
|
|
66
|
-
const parsed = parseHelpersValue(value)
|
|
67
|
-
if (parsed) config.helpers = parsed
|
|
130
|
+
if (BOOLEAN_KEYS.has(key as keyof MutableConfig)) {
|
|
131
|
+
assignBoolean(config, key, value !== 'false')
|
|
132
|
+
} else {
|
|
133
|
+
assignValue(config, key, value)
|
|
68
134
|
}
|
|
69
|
-
|
|
70
135
|
continue
|
|
71
136
|
}
|
|
72
137
|
|
|
73
|
-
|
|
74
|
-
if (arg === '--schema') {
|
|
75
|
-
const value = args[i + 1]
|
|
138
|
+
const key = toCamelCase(arg.slice(2))
|
|
76
139
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const value = args[i + 1]
|
|
83
|
-
|
|
84
|
-
if (value && !value.startsWith('--')) {
|
|
85
|
-
config.outDir = value
|
|
86
|
-
i++
|
|
87
|
-
}
|
|
88
|
-
} else if (arg === '--input') {
|
|
89
|
-
const value = args[i + 1]
|
|
140
|
+
// Boolean flag: presence alone enables it, no value needed
|
|
141
|
+
if (BOOLEAN_KEYS.has(key as keyof MutableConfig)) {
|
|
142
|
+
assignBoolean(config, key, true)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
90
145
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (parsed) {
|
|
94
|
-
config.input = parsed
|
|
95
|
-
i++
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} else if (arg === '--export') {
|
|
146
|
+
// Value flag: consume the next argument when it is not another flag
|
|
147
|
+
if (VALUE_KEYS.has(key as keyof MutableConfig)) {
|
|
99
148
|
const value = args[i + 1]
|
|
100
|
-
|
|
101
149
|
if (value && !value.startsWith('--')) {
|
|
102
|
-
config
|
|
150
|
+
assignValue(config, key, value)
|
|
103
151
|
i++
|
|
104
152
|
}
|
|
105
|
-
} else if (arg === '--types-only') {
|
|
106
|
-
// Boolean flag: presence alone enables it, no value needed
|
|
107
|
-
config.typesOnly = true
|
|
108
|
-
} else if (arg === '--build') {
|
|
109
|
-
config.build = true
|
|
110
|
-
} else if (arg === '--log-warnings') {
|
|
111
|
-
config.logWarnings = true
|
|
112
|
-
} else if (arg === '--strict') {
|
|
113
|
-
config.strict = true
|
|
114
|
-
} else if (arg === '--helpers') {
|
|
115
|
-
const value = args[i + 1]
|
|
116
|
-
|
|
117
|
-
if (value && !value.startsWith('--')) {
|
|
118
|
-
const parsed = parseHelpersValue(value)
|
|
119
|
-
if (parsed) {
|
|
120
|
-
config.helpers = parsed
|
|
121
|
-
i++
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
153
|
}
|
|
125
154
|
}
|
|
126
155
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { readVersion } from './read-version'
|
|
5
|
+
|
|
6
|
+
describe('read-version', () => {
|
|
7
|
+
it('returns the version declared in the package.json', async () => {
|
|
8
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8')) as {
|
|
9
|
+
version: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
expect(await readVersion()).toBe(pkg.version)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns a semver-shaped string', async () => {
|
|
16
|
+
expect(await readVersion()).toMatch(/^\d+\.\d+\.\d+/)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads the CLI's own version from its `package.json`.
|
|
5
|
+
*
|
|
6
|
+
* Resolved relative to this module so it works whether the code runs from
|
|
7
|
+
* `src/` (dev) or the bundled `dist/cli.js` (published) — `package.json`
|
|
8
|
+
* sits one directory above both.
|
|
9
|
+
*/
|
|
10
|
+
export const readVersion = async (): Promise<string> => {
|
|
11
|
+
const pkgUrl = new URL('../package.json', import.meta.url)
|
|
12
|
+
const pkg = JSON.parse(await readFile(pkgUrl, 'utf-8')) as { version?: string }
|
|
13
|
+
return pkg.version ?? '0.0.0'
|
|
14
|
+
}
|