@amritk/mjst 0.7.6 → 0.7.8

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.
@@ -1,45 +0,0 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
- import { tmpdir } from 'node:os'
3
- import { join } from 'node:path'
4
- import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5
-
6
- import { detectHelpersMode } from './detect-helpers-mode'
7
-
8
- describe('detect-helpers-mode', () => {
9
- let tmpRoot: string
10
-
11
- beforeAll(async () => {
12
- tmpRoot = await mkdtemp(join(tmpdir(), 'mjst-detect-'))
13
- })
14
-
15
- afterAll(async () => {
16
- await rm(tmpRoot, { recursive: true, force: true })
17
- })
18
-
19
- it("returns 'package' when @amritk/helpers is resolvable from outDir", async () => {
20
- const outDir = join(tmpRoot, 'with-helpers/out')
21
- const helpersPkgDir = join(tmpRoot, 'with-helpers/node_modules/@amritk/helpers')
22
- await mkdir(outDir, { recursive: true })
23
- await mkdir(helpersPkgDir, { recursive: true })
24
- await writeFile(join(helpersPkgDir, 'is-object.js'), 'export const isObject = () => true\n', 'utf-8')
25
- await writeFile(
26
- join(helpersPkgDir, 'package.json'),
27
- JSON.stringify({
28
- name: '@amritk/helpers',
29
- version: '0.0.0',
30
- type: 'module',
31
- exports: { './is-object': './is-object.js' },
32
- }),
33
- 'utf-8',
34
- )
35
-
36
- expect(detectHelpersMode(outDir)).toBe('package')
37
- })
38
-
39
- it("returns 'embedded' when @amritk/helpers cannot be resolved from outDir", async () => {
40
- const outDir = join(tmpRoot, 'no-helpers/out')
41
- await mkdir(outDir, { recursive: true })
42
-
43
- expect(detectHelpersMode(outDir)).toBe('embedded')
44
- })
45
- })
@@ -1,21 +0,0 @@
1
- import { createRequire } from 'node:module'
2
- import { resolve } from 'node:path'
3
-
4
- /**
5
- * Detects whether `@amritk/helpers` is resolvable from the consumer's output
6
- * directory using standard Node module resolution (walks up `node_modules`).
7
- *
8
- * Returns `'package'` when generated parsers can safely
9
- * `import { ... } from '@amritk/helpers/...'` at runtime, or `'embedded'`
10
- * when the helper sources need to be shipped alongside the generated files
11
- * for the output to be self-contained.
12
- */
13
- export const detectHelpersMode = (outputDir: string): 'package' | 'embedded' => {
14
- const req = createRequire(resolve(outputDir, 'noop.js'))
15
- try {
16
- req.resolve('@amritk/helpers/is-object')
17
- return 'package'
18
- } catch {
19
- return 'embedded'
20
- }
21
- }
@@ -1,78 +0,0 @@
1
- import { writeFile } from 'node:fs/promises'
2
- import { tmpdir } from 'node:os'
3
- import { join } from 'node:path'
4
- import { describe, expect, it } from 'vitest'
5
-
6
- import { loadConfig } from './load-config'
7
-
8
- describe('load-config', () => {
9
- it('loads schema and outDir from a JSON config file', async () => {
10
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
11
- await writeFile(configPath, JSON.stringify({ schema: 'my-schema.json', outDir: 'output' }))
12
-
13
- const result = await loadConfig(configPath)
14
-
15
- expect(result).toEqual({
16
- schema: 'my-schema.json',
17
- outDir: 'output',
18
- })
19
- })
20
-
21
- it('returns partial config when only some keys are present', async () => {
22
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
23
- await writeFile(configPath, JSON.stringify({ schema: 'my-schema.json' }))
24
-
25
- const result = await loadConfig(configPath)
26
-
27
- expect(result).toEqual({
28
- schema: 'my-schema.json',
29
- })
30
- })
31
-
32
- it('ignores non-string values for known keys', async () => {
33
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
34
- await writeFile(configPath, JSON.stringify({ schema: 123, outDir: true }))
35
-
36
- const result = await loadConfig(configPath)
37
-
38
- expect(result).toEqual({})
39
- })
40
-
41
- it('throws for non-object config files', async () => {
42
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
43
- await writeFile(configPath, JSON.stringify([1, 2, 3]))
44
-
45
- await expect(loadConfig(configPath)).rejects.toThrow('Config file must be a JSON object')
46
- })
47
-
48
- it('throws for missing config files', async () => {
49
- await expect(loadConfig('/nonexistent/config.json')).rejects.toThrow()
50
- })
51
-
52
- it('loads strict boolean from config file', async () => {
53
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
54
- await writeFile(configPath, JSON.stringify({ schema: 's.json', outDir: 'o', strict: true }))
55
-
56
- const result = await loadConfig(configPath)
57
-
58
- expect(result).toEqual({ schema: 's.json', outDir: 'o', strict: true })
59
- })
60
-
61
- it('loads schemaDir from a config file', async () => {
62
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
63
- await writeFile(configPath, JSON.stringify({ schemaDir: './schemas', outDir: 'output' }))
64
-
65
- const result = await loadConfig(configPath)
66
-
67
- expect(result).toEqual({ schemaDir: './schemas', outDir: 'output' })
68
- })
69
-
70
- it('ignores non-boolean strict value', async () => {
71
- const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
72
- await writeFile(configPath, JSON.stringify({ schema: 's.json', strict: 'yes' }))
73
-
74
- const result = await loadConfig(configPath)
75
-
76
- expect(result).toEqual({ schema: 's.json' })
77
- })
78
- })
@@ -1,40 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { resolve } from 'node:path'
3
- import type { SourceFormat } from '@amritk/adapters/source-format'
4
-
5
- import type { CliConfig } from './cli-config'
6
-
7
- const SOURCE_FORMATS: readonly string[] = ['json', 'typebox', 'zod', 'valibot', 'effect']
8
-
9
- const isSourceFormat = (value: unknown): value is SourceFormat =>
10
- typeof value === 'string' && SOURCE_FORMATS.includes(value)
11
-
12
- /**
13
- * Loads a JSON config file and returns the relevant CLI config properties.
14
- * The config file should have the same keys as the CLI flags (schema, outDir).
15
- */
16
- export const loadConfig = async (configPath: string): Promise<Partial<CliConfig>> => {
17
- const absolutePath = resolve(configPath)
18
- const raw = await readFile(absolutePath, 'utf-8')
19
- const parsed: unknown = JSON.parse(raw)
20
-
21
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
22
- throw new Error(`Config file must be a JSON object: ${configPath}`)
23
- }
24
-
25
- const obj = parsed as Record<string, unknown>
26
-
27
- return {
28
- ...(typeof obj['schema'] === 'string' && { schema: obj['schema'] }),
29
- ...(typeof obj['schemaDir'] === 'string' && { schemaDir: obj['schemaDir'] }),
30
- ...(typeof obj['outDir'] === 'string' && { outDir: obj['outDir'] }),
31
- ...(typeof obj['outFile'] === 'string' && { outFile: obj['outFile'] }),
32
- ...(isSourceFormat(obj['input']) && { input: obj['input'] }),
33
- ...(typeof obj['export'] === 'string' && { export: obj['export'] }),
34
- ...(typeof obj['typesOnly'] === 'boolean' && { typesOnly: obj['typesOnly'] }),
35
- ...(typeof obj['build'] === 'boolean' && { build: obj['build'] }),
36
- ...(typeof obj['logWarnings'] === 'boolean' && { logWarnings: obj['logWarnings'] }),
37
- ...(typeof obj['strict'] === 'boolean' && { strict: obj['strict'] }),
38
- ...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
39
- }
40
- }
@@ -1,49 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2
- import { tmpdir } from 'node:os'
3
- import { join } from 'node:path'
4
- import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5
-
6
- import { loadSchemaModule } from './load-schema-module'
7
-
8
- describe('loadSchemaModule', () => {
9
- let dir: string
10
-
11
- beforeAll(async () => {
12
- dir = await mkdtemp(join(tmpdir(), 'mjst-load-module-'))
13
- })
14
-
15
- afterAll(async () => {
16
- await rm(dir, { recursive: true, force: true })
17
- })
18
-
19
- const write = async (name: string, contents: string): Promise<string> => {
20
- const path = join(dir, name)
21
- await writeFile(path, contents, 'utf-8')
22
- return path
23
- }
24
-
25
- it('returns the default export when no export name is given', async () => {
26
- const path = await write('default.mjs', 'export default { type: "string" }')
27
- expect(await loadSchemaModule(path)).toEqual({ type: 'string' })
28
- })
29
-
30
- it('returns the sole named export when there is no default', async () => {
31
- const path = await write('single.mjs', 'export const Schema = { type: "number" }')
32
- expect(await loadSchemaModule(path)).toEqual({ type: 'number' })
33
- })
34
-
35
- it('returns the requested named export', async () => {
36
- const path = await write('named.mjs', 'export const A = { type: "boolean" }; export const B = { type: "string" }')
37
- expect(await loadSchemaModule(path, 'B')).toEqual({ type: 'string' })
38
- })
39
-
40
- it('throws when the requested export is missing', async () => {
41
- const path = await write('missing.mjs', 'export const A = { type: "boolean" }')
42
- await expect(loadSchemaModule(path, 'Nope')).rejects.toThrow(/no export named 'Nope'/)
43
- })
44
-
45
- it('throws when multiple exports are ambiguous', async () => {
46
- const path = await write('ambiguous.mjs', 'export const A = {}; export const B = {}')
47
- await expect(loadSchemaModule(path)).rejects.toThrow(/Specify which one to use with --export/)
48
- })
49
- })
@@ -1,52 +0,0 @@
1
- import { pathToFileURL } from 'node:url'
2
-
3
- /**
4
- * Loads a schema authored as a JS/TS module and returns the exported schema value.
5
- *
6
- * Export selection, in order of preference:
7
- * 1. The export named by `exportName`, when provided (`--export <name>`).
8
- * 2. The module's default export.
9
- * 3. The sole named export, when there is exactly one.
10
- *
11
- * Anything else is ambiguous and throws, so the user can disambiguate explicitly
12
- * rather than have mjst guess wrong.
13
- *
14
- * Importing a `.ts` module requires a TypeScript-capable runtime (Bun, or Node
15
- * with a loader such as `tsx`). We surface a friendly hint when the import fails
16
- * for a `.ts` path so the cause is not a cryptic module-resolution error.
17
- */
18
- export const loadSchemaModule = async (modulePath: string, exportName?: string): Promise<unknown> => {
19
- let mod: Record<string, unknown>
20
-
21
- try {
22
- mod = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>
23
- } catch (error) {
24
- const hint = modulePath.endsWith('.ts')
25
- ? ' Importing a .ts schema requires a TypeScript-capable runtime — run mjst via `bunx`, or install `tsx` and run Node with `--import tsx`.'
26
- : ''
27
- throw new Error(`Failed to load schema module at ${modulePath}.${hint}\n${String(error)}`)
28
- }
29
-
30
- if (exportName) {
31
- if (!(exportName in mod)) {
32
- throw new Error(`Schema module ${modulePath} has no export named '${exportName}'.`)
33
- }
34
- return mod[exportName]
35
- }
36
-
37
- if ('default' in mod && mod['default'] !== undefined) {
38
- return mod['default']
39
- }
40
-
41
- const namedKeys = Object.keys(mod).filter((key) => key !== 'default')
42
-
43
- if (namedKeys.length === 1) {
44
- const key = namedKeys[0]
45
- if (key) return mod[key]
46
- }
47
-
48
- throw new Error(
49
- `Schema module ${modulePath} exports ${namedKeys.length} values (${namedKeys.join(', ') || 'none'}). ` +
50
- `Specify which one to use with --export <name>.`,
51
- )
52
- }
@@ -1,303 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { parseCliArgs } from './parse-cli-args'
4
-
5
- describe('parse-cli-args', () => {
6
- it('parses --schema and --outDir with space-separated values', () => {
7
- const result = parseCliArgs(['--schema', 'path/to/schema.json', '--outDir', 'dist'])
8
-
9
- expect(result).toEqual({
10
- schema: 'path/to/schema.json',
11
- outDir: 'dist',
12
- })
13
- })
14
-
15
- it('parses --schema and --outDir with equals syntax', () => {
16
- const result = parseCliArgs(['--schema=path/to/schema.json', '--outDir=dist'])
17
-
18
- expect(result).toEqual({
19
- schema: 'path/to/schema.json',
20
- outDir: 'dist',
21
- })
22
- })
23
-
24
- it('returns empty object when no flags are provided', () => {
25
- const result = parseCliArgs([])
26
-
27
- expect(result).toEqual({})
28
- })
29
-
30
- it('ignores unknown flags', () => {
31
- const result = parseCliArgs(['--unknown', 'value', '--schema', 'schema.json'])
32
-
33
- expect(result).toEqual({
34
- schema: 'schema.json',
35
- })
36
- })
37
-
38
- it('handles only --schema without --outDir', () => {
39
- const result = parseCliArgs(['--schema', 'schema.json'])
40
-
41
- expect(result).toEqual({
42
- schema: 'schema.json',
43
- })
44
- })
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
-
66
- it('handles only --outDir without --schema', () => {
67
- const result = parseCliArgs(['--outDir', 'output'])
68
-
69
- expect(result).toEqual({
70
- outDir: 'output',
71
- })
72
- })
73
-
74
- it('does not treat a flag as a value for the previous flag', () => {
75
- const result = parseCliArgs(['--schema', '--outDir', 'output'])
76
-
77
- expect(result).toEqual({
78
- outDir: 'output',
79
- })
80
- })
81
-
82
- it('handles equals syntax with empty value', () => {
83
- const result = parseCliArgs(['--schema=', '--outDir=dist'])
84
-
85
- expect(result).toEqual({
86
- schema: '',
87
- outDir: 'dist',
88
- })
89
- })
90
-
91
- it('parses --types-only boolean flag', () => {
92
- const result = parseCliArgs(['--types-only'])
93
-
94
- expect(result).toEqual({
95
- typesOnly: true,
96
- })
97
- })
98
-
99
- it('parses --types-only=true with equals syntax', () => {
100
- const result = parseCliArgs(['--types-only=true'])
101
-
102
- expect(result).toEqual({
103
- typesOnly: true,
104
- })
105
- })
106
-
107
- it('parses --types-only=false with equals syntax', () => {
108
- const result = parseCliArgs(['--types-only=false'])
109
-
110
- expect(result).toEqual({
111
- typesOnly: false,
112
- })
113
- })
114
-
115
- it('parses --types-only alongside other flags', () => {
116
- const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--types-only'])
117
-
118
- expect(result).toEqual({
119
- schema: 'schema.json',
120
- outDir: 'dist',
121
- typesOnly: true,
122
- })
123
- })
124
-
125
- it('parses --build boolean flag', () => {
126
- const result = parseCliArgs(['--build'])
127
-
128
- expect(result).toEqual({
129
- build: true,
130
- })
131
- })
132
-
133
- it('parses --build=true with equals syntax', () => {
134
- const result = parseCliArgs(['--build=true'])
135
-
136
- expect(result).toEqual({
137
- build: true,
138
- })
139
- })
140
-
141
- it('parses --build=false with equals syntax', () => {
142
- const result = parseCliArgs(['--build=false'])
143
-
144
- expect(result).toEqual({
145
- build: false,
146
- })
147
- })
148
-
149
- it('parses --build alongside other flags', () => {
150
- const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--build'])
151
-
152
- expect(result).toEqual({
153
- schema: 'schema.json',
154
- outDir: 'dist',
155
- build: true,
156
- })
157
- })
158
-
159
- it('parses --log-warnings boolean flag', () => {
160
- const result = parseCliArgs(['--log-warnings'])
161
-
162
- expect(result).toEqual({
163
- logWarnings: true,
164
- })
165
- })
166
-
167
- it('parses --log-warnings=true with equals syntax', () => {
168
- const result = parseCliArgs(['--log-warnings=true'])
169
-
170
- expect(result).toEqual({
171
- logWarnings: true,
172
- })
173
- })
174
-
175
- it('parses --log-warnings=false with equals syntax', () => {
176
- const result = parseCliArgs(['--log-warnings=false'])
177
-
178
- expect(result).toEqual({
179
- logWarnings: false,
180
- })
181
- })
182
-
183
- it('parses --log-warnings alongside other flags', () => {
184
- const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--log-warnings'])
185
-
186
- expect(result).toEqual({
187
- schema: 'schema.json',
188
- outDir: 'dist',
189
- logWarnings: true,
190
- })
191
- })
192
-
193
- it('parses --strict boolean flag', () => {
194
- const result = parseCliArgs(['--strict'])
195
-
196
- expect(result).toEqual({
197
- strict: true,
198
- })
199
- })
200
-
201
- it('parses --strict=true with equals syntax', () => {
202
- const result = parseCliArgs(['--strict=true'])
203
-
204
- expect(result).toEqual({
205
- strict: true,
206
- })
207
- })
208
-
209
- it('parses --strict=false with equals syntax', () => {
210
- const result = parseCliArgs(['--strict=false'])
211
-
212
- expect(result).toEqual({
213
- strict: false,
214
- })
215
- })
216
-
217
- it('parses --strict alongside other flags', () => {
218
- const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--strict'])
219
-
220
- expect(result).toEqual({
221
- schema: 'schema.json',
222
- outDir: 'dist',
223
- strict: true,
224
- })
225
- })
226
-
227
- it('parses --helpers package with space-separated value', () => {
228
- const result = parseCliArgs(['--helpers', 'package'])
229
- expect(result).toEqual({ helpers: 'package' })
230
- })
231
-
232
- it('parses --helpers=embedded with equals syntax', () => {
233
- const result = parseCliArgs(['--helpers=embedded'])
234
- expect(result).toEqual({ helpers: 'embedded' })
235
- })
236
-
237
- it('throws on an invalid --helpers value instead of silently ignoring it', () => {
238
- expect(() => parseCliArgs(['--helpers', 'bogus', '--schema', 's.json'])).toThrow(/Invalid --helpers value "bogus"/)
239
- })
240
-
241
- it('throws on an invalid --helpers=foo value', () => {
242
- expect(() => parseCliArgs(['--helpers=foo', '--schema', 's.json'])).toThrow(/package, embedded/)
243
- })
244
-
245
- it('parses a valid --input value', () => {
246
- expect(parseCliArgs(['--input', 'zod'])).toEqual({ input: 'zod' })
247
- })
248
-
249
- it('throws on an invalid --input value, listing the valid formats', () => {
250
- expect(() => parseCliArgs(['--input', 'typscript'])).toThrow(
251
- /Invalid --input value "typscript".*json, typebox, zod, valibot, effect/,
252
- )
253
- })
254
-
255
- it('accepts kebab-case flag names', () => {
256
- const result = parseCliArgs(['--schema-dir', './schemas', '--out-dir', 'dist'])
257
-
258
- expect(result).toEqual({
259
- schemaDir: './schemas',
260
- outDir: 'dist',
261
- })
262
- })
263
-
264
- it('treats kebab-case and camelCase variants as equivalent', () => {
265
- const kebab = parseCliArgs(['--out-dir', 'dist', '--types-only', '--log-warnings'])
266
- const camel = parseCliArgs(['--outDir', 'dist', '--typesOnly', '--logWarnings'])
267
-
268
- expect(kebab).toEqual(camel)
269
- expect(camel).toEqual({ outDir: 'dist', typesOnly: true, logWarnings: true })
270
- })
271
-
272
- it('parses --out-file with space-separated and equals syntax', () => {
273
- expect(parseCliArgs(['--out-file', './out/schema.ts'])).toEqual({ outFile: './out/schema.ts' })
274
- expect(parseCliArgs(['--outFile=./out/schema.ts'])).toEqual({ outFile: './out/schema.ts' })
275
- })
276
-
277
- it('parses --readonly boolean flag and its equals forms', () => {
278
- expect(parseCliArgs(['--readonly'])).toEqual({ readonly: true })
279
- expect(parseCliArgs(['--readonly=true'])).toEqual({ readonly: true })
280
- expect(parseCliArgs(['--readonly=false'])).toEqual({ readonly: false })
281
- })
282
-
283
- it('parses --banner with no value as true (default message)', () => {
284
- expect(parseCliArgs(['--banner'])).toEqual({ banner: true })
285
- })
286
-
287
- it('parses --banner=true and --banner=false', () => {
288
- expect(parseCliArgs(['--banner=true'])).toEqual({ banner: true })
289
- expect(parseCliArgs(['--banner=false'])).toEqual({ banner: false })
290
- })
291
-
292
- it('parses --banner=<text> as a custom message string', () => {
293
- expect(parseCliArgs(['--banner=Do not edit this file.'])).toEqual({ banner: 'Do not edit this file.' })
294
- })
295
-
296
- it('parses --banner <text> (space-separated) as a custom message string', () => {
297
- expect(parseCliArgs(['--banner', 'Do not edit this file.'])).toEqual({ banner: 'Do not edit this file.' })
298
- })
299
-
300
- it('parses --banner followed by a flag as true (default message)', () => {
301
- expect(parseCliArgs(['--banner', '--out-dir', 'dist'])).toEqual({ banner: true, outDir: 'dist' })
302
- })
303
- })