@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.
@@ -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
  })
@@ -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
- * Supports --schema, --outDir, and --types-only flags with either `--flag value` or `--flag=value` syntax.
10
- * Only returns the flags that were explicitly provided so we can layer them on top of config file values.
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
- // Use a mutable local type for building, then return as Partial<CliConfig>
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
- if (arg.startsWith('--') && arg.includes('=')) {
43
- const equalsIndex = arg.indexOf('=')
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
- if (key === 'schema') {
48
- config.schema = value
49
- } else if (key === 'outDir') {
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
- // Handle --flag value syntax
74
- if (arg === '--schema') {
75
- const value = args[i + 1]
138
+ const key = toCamelCase(arg.slice(2))
76
139
 
77
- if (value && !value.startsWith('--')) {
78
- config.schema = value
79
- i++
80
- }
81
- } else if (arg === '--outDir') {
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
- if (value && !value.startsWith('--')) {
92
- const parsed = parseInputValue(value)
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.export = value
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
+ }