@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.
- package/README.md +1 -1
- package/dist/cli-config.d.ts +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/combine-files.d.ts +0 -1
- package/dist/detect-helpers-mode.d.ts +0 -1
- package/dist/load-config.d.ts +0 -1
- package/dist/load-schema-module.d.ts +0 -1
- package/dist/parse-cli-args.d.ts +0 -1
- package/dist/read-version.d.ts +0 -1
- package/package.json +5 -6
- package/dist/cli-config.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/combine-files.d.ts.map +0 -1
- package/dist/detect-helpers-mode.d.ts.map +0 -1
- package/dist/load-config.d.ts.map +0 -1
- package/dist/load-schema-module.d.ts.map +0 -1
- package/dist/parse-cli-args.d.ts.map +0 -1
- package/dist/read-version.d.ts.map +0 -1
- package/src/cli-config.ts +0 -84
- package/src/cli.ts +0 -371
- package/src/combine-files.test.ts +0 -49
- package/src/combine-files.ts +0 -43
- package/src/detect-helpers-mode.test.ts +0 -45
- package/src/detect-helpers-mode.ts +0 -21
- package/src/load-config.test.ts +0 -78
- package/src/load-config.ts +0 -40
- package/src/load-schema-module.test.ts +0 -49
- package/src/load-schema-module.ts +0 -52
- package/src/parse-cli-args.test.ts +0 -303
- package/src/parse-cli-args.ts +0 -180
- package/src/read-version.test.ts +0 -18
- package/src/read-version.ts +0 -14
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ npx mjst --config ./mjst.config.json
|
|
|
204
204
|
<td align="center"><code>false</code></td>
|
|
205
205
|
</tr>
|
|
206
206
|
<tr>
|
|
207
|
-
<td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values.
|
|
207
|
+
<td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. When a schema sets additionalProperties: false, undeclared keys throw too; otherwise they are still allowed.</td>
|
|
208
208
|
</tr>
|
|
209
209
|
<tr>
|
|
210
210
|
<td>🔒 <code>readonly</code></td>
|
package/dist/cli-config.d.ts
CHANGED
package/dist/cli.d.ts
CHANGED
package/dist/combine-files.d.ts
CHANGED
package/dist/load-config.d.ts
CHANGED
package/dist/parse-cli-args.d.ts
CHANGED
package/dist/read-version.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"mjst": "./dist/cli.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"dist"
|
|
31
|
-
"src"
|
|
30
|
+
"dist"
|
|
32
31
|
],
|
|
33
32
|
"publishConfig": {
|
|
34
33
|
"access": "public"
|
|
@@ -42,10 +41,10 @@
|
|
|
42
41
|
"generate-readme": "bun run ./scripts/generate-readme.ts"
|
|
43
42
|
},
|
|
44
43
|
"dependencies": {
|
|
45
|
-
"@amritk/adapters": "0.2.
|
|
46
|
-
"@amritk/generate-parsers": "0.
|
|
44
|
+
"@amritk/adapters": "0.2.10",
|
|
45
|
+
"@amritk/generate-parsers": "0.9.0",
|
|
47
46
|
"@amritk/generate-markdown": "0.2.4",
|
|
48
|
-
"@amritk/helpers": "0.
|
|
47
|
+
"@amritk/helpers": "0.10.0"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"json-schema-typed": "^8.0.1"
|
package/dist/cli-config.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-config.d.ts","sourceRoot":"","sources":["../src/cli-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAElE;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CACnC,CAAA"}
|
package/dist/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"combine-files.d.ts","sourceRoot":"","sources":["../src/combine-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAO7D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,UAAW,SAAS,aAAa,EAAE,KAAG,MAwBvE,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"detect-helpers-mode.d.ts","sourceRoot":"","sources":["../src/detect-helpers-mode.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,cAAe,MAAM,KAAG,SAAS,GAAG,UAQjE,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAO7C;;;GAGG;AACH,eAAO,MAAM,UAAU,eAAsB,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAwB/E,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"load-schema-module.d.ts","sourceRoot":"","sources":["../src/load-schema-module.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,eAAsB,MAAM,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAkC/F,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parse-cli-args.d.ts","sourceRoot":"","sources":["../src/parse-cli-args.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAgH7C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,SAAU,SAAS,MAAM,EAAE,KAAG,OAAO,CAAC,SAAS,CA0DvE,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-version.d.ts","sourceRoot":"","sources":["../src/read-version.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,MAAM,CAIlD,CAAA"}
|
package/src/cli-config.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { SourceFormat } from '@amritk/adapters/source-format'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Configuration for the CLI tool.
|
|
5
|
-
* These properties map 1:1 with CLI flags and config file keys.
|
|
6
|
-
*/
|
|
7
|
-
export type CliConfig = {
|
|
8
|
-
/** Path to the schema to process — a JSON Schema file, or a JS/TS module when `input` is set. */
|
|
9
|
-
readonly schema?: string
|
|
10
|
-
/**
|
|
11
|
-
* Path to a directory of JSON Schema files. When set, the CLI walks the directory
|
|
12
|
-
* recursively, generates parsers for every `*.json` schema it finds, and mirrors the
|
|
13
|
-
* directory layout under `outDir`. A single shared `_helpers/` directory is emitted at
|
|
14
|
-
* the root of `outDir` (in embedded mode) and every nested parser imports from it.
|
|
15
|
-
* Mutually exclusive with `schema`; when both are present `schemaDir` wins.
|
|
16
|
-
*/
|
|
17
|
-
readonly schemaDir?: string
|
|
18
|
-
/**
|
|
19
|
-
* The source format of the schema. Defaults to `'json'` (a plain JSON Schema file).
|
|
20
|
-
* Any other format loads `schema` as a module and converts it via the matching adapter.
|
|
21
|
-
*/
|
|
22
|
-
readonly input?: SourceFormat
|
|
23
|
-
/**
|
|
24
|
-
* Which export of the schema module to use when `input` is not `'json'`.
|
|
25
|
-
* Defaults to the default export, or the sole named export when there is exactly one.
|
|
26
|
-
*/
|
|
27
|
-
readonly export?: string
|
|
28
|
-
/** Output directory for generated TypeScript files. Mutually exclusive with `outFile`. */
|
|
29
|
-
readonly outDir?: string
|
|
30
|
-
/**
|
|
31
|
-
* Output everything to a single TypeScript file instead of a directory of files.
|
|
32
|
-
* Mutually exclusive with `outDir`. Currently supported only with `typesOnly`.
|
|
33
|
-
*/
|
|
34
|
-
readonly outFile?: string
|
|
35
|
-
/**
|
|
36
|
-
* When true, only generate TypeScript type definitions without parser functions.
|
|
37
|
-
* Useful when you only need the type shapes and do not need runtime validation.
|
|
38
|
-
*/
|
|
39
|
-
readonly typesOnly?: boolean
|
|
40
|
-
/**
|
|
41
|
-
* When true, compile the generated TypeScript files to .js and .d.ts output.
|
|
42
|
-
* The .ts source files are removed after compilation.
|
|
43
|
-
*/
|
|
44
|
-
readonly build?: boolean
|
|
45
|
-
/**
|
|
46
|
-
* When true, the generated parsers emit a console.warn for every input key
|
|
47
|
-
* that is not declared in the schema's properties.
|
|
48
|
-
*/
|
|
49
|
-
readonly logWarnings?: boolean
|
|
50
|
-
/**
|
|
51
|
-
* When true, the generated parsers throw on type/shape mismatches
|
|
52
|
-
* (wrong type, missing required property, enum/pattern/min/max violations)
|
|
53
|
-
* instead of coercing invalid input to default values.
|
|
54
|
-
*/
|
|
55
|
-
readonly strict?: boolean
|
|
56
|
-
/**
|
|
57
|
-
* Controls how generated parsers reference their runtime helpers.
|
|
58
|
-
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
|
59
|
-
* - `'embedded'`: ship the helper source under `outDir/_helpers/` and emit
|
|
60
|
-
* relative imports so the output directory is self-contained.
|
|
61
|
-
*
|
|
62
|
-
* When omitted, the CLI auto-detects: it picks `'package'` if `@amritk/helpers`
|
|
63
|
-
* resolves from outDir, otherwise falls back to `'embedded'`.
|
|
64
|
-
*/
|
|
65
|
-
readonly helpers?: 'package' | 'embedded'
|
|
66
|
-
/**
|
|
67
|
-
* When true, every property, array, and record in the generated type
|
|
68
|
-
* definitions is emitted as `readonly`, producing deeply immutable types.
|
|
69
|
-
*/
|
|
70
|
-
readonly readonly?: boolean
|
|
71
|
-
/**
|
|
72
|
-
* Suffix appended to every generated type name derived from a `$ref`
|
|
73
|
-
* (e.g. `'Object'` turns `Contact` into `ContactObject`). Defaults to `''`
|
|
74
|
-
* (no suffix). The root type name is used verbatim and is unaffected.
|
|
75
|
-
*/
|
|
76
|
-
readonly typeSuffix?: string
|
|
77
|
-
/**
|
|
78
|
-
* Prepends a comment header to every generated file (excluding `_helpers/`).
|
|
79
|
-
* - `true` — use the default message: "This file was auto-generated by @amritk/mjst…"
|
|
80
|
-
* - A string — use that text as the message body (wrapped in a JSDoc block).
|
|
81
|
-
* - `false` / omitted — no header.
|
|
82
|
-
*/
|
|
83
|
-
readonly banner?: boolean | string
|
|
84
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execFile } from 'node:child_process'
|
|
3
|
-
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
5
|
-
import { promisify } from 'node:util'
|
|
6
|
-
import { getAdapter } from '@amritk/adapters/get-adapter'
|
|
7
|
-
import { buildSchema } from '@amritk/generate-parsers'
|
|
8
|
-
import { deriveRootTypeName } from '@amritk/helpers/derive-root-type-name'
|
|
9
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
10
|
-
|
|
11
|
-
const execFileAsync = promisify(execFile)
|
|
12
|
-
|
|
13
|
-
import type { CliConfig } from './cli-config'
|
|
14
|
-
import { combineGeneratedFiles } from './combine-files'
|
|
15
|
-
import { detectHelpersMode } from './detect-helpers-mode'
|
|
16
|
-
import { loadConfig } from './load-config'
|
|
17
|
-
import { loadSchemaModule } from './load-schema-module'
|
|
18
|
-
import { parseCliArgs } from './parse-cli-args'
|
|
19
|
-
import { readVersion } from './read-version'
|
|
20
|
-
|
|
21
|
-
/** True when the args request the CLI version (`version`, `--version`, or `-v`). */
|
|
22
|
-
const isVersionRequest = (args: readonly string[]): boolean =>
|
|
23
|
-
args[0] === 'version' || args.includes('--version') || args.includes('-v')
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Extracts the --config flag value from process args before full parsing.
|
|
27
|
-
* We need this early so we can load the config file first, then overlay CLI flags.
|
|
28
|
-
*/
|
|
29
|
-
const extractConfigPath = (args: readonly string[]): string | undefined => {
|
|
30
|
-
for (let i = 0; i < args.length; i++) {
|
|
31
|
-
const arg = args[i]
|
|
32
|
-
|
|
33
|
-
if (arg === '--config' && args[i + 1] && !args[i + 1]?.startsWith('--')) {
|
|
34
|
-
return args[i + 1]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (arg?.startsWith('--config=')) {
|
|
38
|
-
return arg.slice('--config='.length)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return undefined
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Reads a JSON Schema off disk, or loads a module and converts it via its adapter. */
|
|
46
|
-
const loadSchema = async (config: Partial<CliConfig>, schemaPath: string): Promise<unknown> => {
|
|
47
|
-
const inputFormat = config.input ?? 'json'
|
|
48
|
-
|
|
49
|
-
if (inputFormat === 'json') {
|
|
50
|
-
return JSON.parse(await readFile(schemaPath, 'utf-8'))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.log(`Input format: ${inputFormat}`)
|
|
54
|
-
const source = await loadSchemaModule(schemaPath, config.export)
|
|
55
|
-
return getAdapter(inputFormat).toJSONSchema(source)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const DEFAULT_BANNER_TEXT =
|
|
59
|
-
'This file was auto-generated by @amritk/mjst.\n * Do not edit it manually — your changes will be overwritten on the next generation.'
|
|
60
|
-
|
|
61
|
-
const resolveBanner = (banner: boolean | string | undefined): string => {
|
|
62
|
-
if (!banner) return ''
|
|
63
|
-
const text = banner === true ? DEFAULT_BANNER_TEXT : banner
|
|
64
|
-
return `/**\n * ${text}\n */\n\n`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Writes a generated file, creating any parent directories the filename implies. */
|
|
68
|
-
const writeGeneratedFile = async (baseDir: string, filename: string, content: string): Promise<void> => {
|
|
69
|
-
const filePath = join(baseDir, filename)
|
|
70
|
-
await mkdir(dirname(filePath), { recursive: true })
|
|
71
|
-
await writeFile(filePath, content, 'utf-8')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Compiles the generated `.ts` files to `.js`/`.d.ts` with a temporary tsconfig,
|
|
76
|
-
* then removes the intermediate sources. `tsFiles` are paths relative to `outputDir`.
|
|
77
|
-
*/
|
|
78
|
-
const buildOutput = async (outputDir: string, tsFiles: readonly string[], typesOnly?: boolean): Promise<void> => {
|
|
79
|
-
// Write a minimal tsconfig so tsc can compile the generated files without
|
|
80
|
-
// inheriting settings like allowImportingTsExtensions that block emission.
|
|
81
|
-
// In 'package' mode tsc finds @amritk/helpers via the consumer's node_modules.
|
|
82
|
-
// In 'embedded' mode the helpers live under ./_helpers and need no resolution.
|
|
83
|
-
const tsconfigContent = JSON.stringify(
|
|
84
|
-
{
|
|
85
|
-
compilerOptions: {
|
|
86
|
-
target: 'ESNext',
|
|
87
|
-
module: 'ESNext',
|
|
88
|
-
moduleResolution: 'bundler',
|
|
89
|
-
declaration: true,
|
|
90
|
-
emitDeclarationOnly: typesOnly,
|
|
91
|
-
skipLibCheck: true,
|
|
92
|
-
},
|
|
93
|
-
include: tsFiles.map((file) => `./${file}`),
|
|
94
|
-
},
|
|
95
|
-
null,
|
|
96
|
-
2,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const tsconfigPath = join(outputDir, '.tsconfig-mjst-build.json')
|
|
100
|
-
await writeFile(tsconfigPath, tsconfigContent, 'utf-8')
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await execFileAsync('npx', ['tsc', '--project', tsconfigPath])
|
|
104
|
-
} catch (error) {
|
|
105
|
-
// tsc reports the actual errors on stdout; surface them so the user sees
|
|
106
|
-
// *what* failed instead of a bare "compilation failed".
|
|
107
|
-
const details = error as { stdout?: string; stderr?: string }
|
|
108
|
-
const output = [details.stdout, details.stderr].filter(Boolean).join('\n').trim()
|
|
109
|
-
throw new Error(
|
|
110
|
-
`TypeScript compilation failed. Check the generated files for errors.${output ? `\n\n${output}` : ''}`,
|
|
111
|
-
)
|
|
112
|
-
} finally {
|
|
113
|
-
await unlink(tsconfigPath)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Remove the intermediate .ts files now that .js and .d.ts have been produced
|
|
117
|
-
for (const file of tsFiles) {
|
|
118
|
-
await unlink(join(outputDir, file))
|
|
119
|
-
console.log(`Built: ${file.replace(/\.ts$/, '.js')}`)
|
|
120
|
-
console.log(`Built: ${file.replace(/\.ts$/, '.d.ts')}`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
console.log(`\nTotal files built: ${tsFiles.length * 2}`)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Recursively collects every `*.json` file under `dir`, returning absolute paths. */
|
|
127
|
-
const findJsonSchemas = async (dir: string): Promise<string[]> => {
|
|
128
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
129
|
-
const results: string[] = []
|
|
130
|
-
|
|
131
|
-
for (const entry of entries) {
|
|
132
|
-
const full = join(dir, entry.name)
|
|
133
|
-
if (entry.isDirectory()) {
|
|
134
|
-
results.push(...(await findJsonSchemas(full)))
|
|
135
|
-
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
136
|
-
results.push(full)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return results
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Generates parsers for a single schema (the original one-schema-in, one-outDir-out flow). */
|
|
144
|
-
const runSingle = async (config: Partial<CliConfig>, schemaPath: string, outputDir: string): Promise<void> => {
|
|
145
|
-
const schema = await loadSchema(config, schemaPath)
|
|
146
|
-
|
|
147
|
-
await mkdir(outputDir, { recursive: true })
|
|
148
|
-
|
|
149
|
-
const helpersMode = config.helpers ?? detectHelpersMode(outputDir)
|
|
150
|
-
console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`)
|
|
151
|
-
|
|
152
|
-
const rootTypeName = deriveRootTypeName(schema)
|
|
153
|
-
console.log(`Root type: ${rootTypeName}`)
|
|
154
|
-
|
|
155
|
-
const files = await buildSchema(
|
|
156
|
-
schema as JSONSchema,
|
|
157
|
-
rootTypeName,
|
|
158
|
-
undefined,
|
|
159
|
-
config.typesOnly,
|
|
160
|
-
config.logWarnings,
|
|
161
|
-
config.strict,
|
|
162
|
-
helpersMode,
|
|
163
|
-
undefined,
|
|
164
|
-
config.readonly,
|
|
165
|
-
config.typeSuffix,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
for (const file of files) {
|
|
169
|
-
const isHelper = file.filename.startsWith('_helpers/')
|
|
170
|
-
const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content
|
|
171
|
-
await writeGeneratedFile(outputDir, file.filename, content)
|
|
172
|
-
console.log(`Generated: ${file.filename}`)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (config.build) {
|
|
176
|
-
await buildOutput(
|
|
177
|
-
outputDir,
|
|
178
|
-
files.map((f) => f.filename),
|
|
179
|
-
config.typesOnly,
|
|
180
|
-
)
|
|
181
|
-
} else {
|
|
182
|
-
console.log(`\nTotal files generated: ${files.length}`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Generates a single self-contained file from one schema, concatenating every
|
|
188
|
-
* generated definition (and dropping the cross-file imports that are no longer
|
|
189
|
-
* needed) into `outFilePath`. Currently used for types-only output.
|
|
190
|
-
*/
|
|
191
|
-
const runSingleFile = async (config: Partial<CliConfig>, schemaPath: string, outFilePath: string): Promise<void> => {
|
|
192
|
-
const schema = await loadSchema(config, schemaPath)
|
|
193
|
-
const outputDir = dirname(outFilePath)
|
|
194
|
-
await mkdir(outputDir, { recursive: true })
|
|
195
|
-
|
|
196
|
-
const rootTypeName = deriveRootTypeName(schema)
|
|
197
|
-
console.log(`Root type: ${rootTypeName}`)
|
|
198
|
-
|
|
199
|
-
const files = await buildSchema(
|
|
200
|
-
schema as JSONSchema,
|
|
201
|
-
rootTypeName,
|
|
202
|
-
undefined,
|
|
203
|
-
config.typesOnly,
|
|
204
|
-
config.logWarnings,
|
|
205
|
-
config.strict,
|
|
206
|
-
config.helpers ?? 'package',
|
|
207
|
-
undefined,
|
|
208
|
-
config.readonly,
|
|
209
|
-
config.typeSuffix,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
const combined = combineGeneratedFiles(files)
|
|
213
|
-
await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, 'utf-8')
|
|
214
|
-
console.log(`Generated: ${relative(process.cwd(), outFilePath)}`)
|
|
215
|
-
|
|
216
|
-
if (config.build) {
|
|
217
|
-
await buildOutput(outputDir, [basename(outFilePath)], config.typesOnly)
|
|
218
|
-
} else {
|
|
219
|
-
console.log(`\nTotal files generated: 1`)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Generates parsers for every JSON Schema under `schemaDir`, mirroring the directory
|
|
225
|
-
* layout into `outDir`. Each schema's output lands in its own subdirectory (so files
|
|
226
|
-
* from different schemas never collide) and the runtime helpers are emitted once into a
|
|
227
|
-
* shared `outDir/_helpers/` that every nested parser imports from.
|
|
228
|
-
*/
|
|
229
|
-
const runRecursive = async (config: Partial<CliConfig>, schemaDir: string, outputDir: string): Promise<void> => {
|
|
230
|
-
if (config.input && config.input !== 'json') {
|
|
231
|
-
console.error("Error: --schemaDir only supports JSON Schema files; remove --input or set it to 'json'.")
|
|
232
|
-
process.exit(1)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const schemaFiles = await findJsonSchemas(schemaDir)
|
|
236
|
-
|
|
237
|
-
if (schemaFiles.length === 0) {
|
|
238
|
-
console.error(`Error: no .json schema files found under ${schemaDir}.`)
|
|
239
|
-
process.exit(1)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
await mkdir(outputDir, { recursive: true })
|
|
243
|
-
|
|
244
|
-
const helpersMode = config.helpers ?? detectHelpersMode(outputDir)
|
|
245
|
-
console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`)
|
|
246
|
-
|
|
247
|
-
// Helper sources are identical across schemas, so collect them by filename and
|
|
248
|
-
// write the deduplicated set once at the output root.
|
|
249
|
-
const sharedHelpers = new Map<string, string>()
|
|
250
|
-
const writtenTsFiles: string[] = []
|
|
251
|
-
|
|
252
|
-
for (const schemaFile of schemaFiles) {
|
|
253
|
-
const relPath = relative(schemaDir, schemaFile)
|
|
254
|
-
const relNoExt = relPath.slice(0, -'.json'.length)
|
|
255
|
-
const schemaSubDir = join(outputDir, relNoExt)
|
|
256
|
-
// Depth of the schema's output subdirectory, used to build the relative import
|
|
257
|
-
// path back to the shared `_helpers/` at the output root (../, ../../, ...).
|
|
258
|
-
const depth = relNoExt.split(sep).filter(Boolean).length
|
|
259
|
-
const helpersImportPrefix = '../'.repeat(depth)
|
|
260
|
-
|
|
261
|
-
const schema = await loadSchema(config, schemaFile)
|
|
262
|
-
const rootTypeName = deriveRootTypeName(schema)
|
|
263
|
-
console.log(`\n${relPath} → ${relNoExt}/ (root type: ${rootTypeName})`)
|
|
264
|
-
|
|
265
|
-
const files = await buildSchema(
|
|
266
|
-
schema as JSONSchema,
|
|
267
|
-
rootTypeName,
|
|
268
|
-
undefined,
|
|
269
|
-
config.typesOnly,
|
|
270
|
-
config.logWarnings,
|
|
271
|
-
config.strict,
|
|
272
|
-
helpersMode,
|
|
273
|
-
helpersImportPrefix,
|
|
274
|
-
config.readonly,
|
|
275
|
-
config.typeSuffix,
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
for (const file of files) {
|
|
279
|
-
if (file.filename.startsWith('_helpers/')) {
|
|
280
|
-
sharedHelpers.set(file.filename, file.content)
|
|
281
|
-
continue
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const content = config.banner ? resolveBanner(config.banner) + file.content : file.content
|
|
285
|
-
await writeGeneratedFile(schemaSubDir, file.filename, content)
|
|
286
|
-
const relativeFilename = join(relNoExt, file.filename)
|
|
287
|
-
writtenTsFiles.push(relativeFilename)
|
|
288
|
-
console.log(`Generated: ${relativeFilename}`)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
for (const [filename, content] of sharedHelpers) {
|
|
293
|
-
await writeGeneratedFile(outputDir, filename, content)
|
|
294
|
-
writtenTsFiles.push(filename)
|
|
295
|
-
console.log(`Generated: ${filename}`)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (config.build) {
|
|
299
|
-
await buildOutput(outputDir, writtenTsFiles, config.typesOnly)
|
|
300
|
-
} else {
|
|
301
|
-
console.log(`\nTotal files generated: ${writtenTsFiles.length}`)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const run = async (): Promise<void> => {
|
|
306
|
-
// Skip the first two args (node executable and script path)
|
|
307
|
-
const args = process.argv.slice(2)
|
|
308
|
-
|
|
309
|
-
if (isVersionRequest(args)) {
|
|
310
|
-
console.log(await readVersion())
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const configPath = extractConfigPath(args)
|
|
315
|
-
|
|
316
|
-
// Start with config file values if provided, then overlay CLI flags on top
|
|
317
|
-
const fileConfig = configPath ? await loadConfig(configPath) : {}
|
|
318
|
-
const cliConfig = parseCliArgs(args)
|
|
319
|
-
const config = { ...fileConfig, ...cliConfig }
|
|
320
|
-
|
|
321
|
-
if (config.outDir && config.outFile) {
|
|
322
|
-
console.error('Error: provide only one of --out-dir or --out-file, not both.')
|
|
323
|
-
process.exit(1)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Single-file output is the alternative to a directory of files.
|
|
327
|
-
if (config.outFile) {
|
|
328
|
-
if (!config.typesOnly) {
|
|
329
|
-
console.error('Error: --out-file currently supports only --types-only output. Add --types-only or use --out-dir.')
|
|
330
|
-
process.exit(1)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (config.schemaDir) {
|
|
334
|
-
console.error('Error: --out-file cannot be combined with --schema-dir; use --out-dir for recursive output.')
|
|
335
|
-
process.exit(1)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (!config.schema) {
|
|
339
|
-
console.error('Error: --schema is required. Provide a path to a JSON Schema.')
|
|
340
|
-
process.exit(1)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
await runSingleFile(config, resolve(config.schema), resolve(config.outFile))
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (!config.outDir) {
|
|
348
|
-
console.error('Error: --out-dir or --out-file is required. Provide an output destination for generated files.')
|
|
349
|
-
process.exit(1)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const outputDir = resolve(config.outDir)
|
|
353
|
-
|
|
354
|
-
// schemaDir activates recursive mode and takes precedence over a single schema.
|
|
355
|
-
if (config.schemaDir) {
|
|
356
|
-
await runRecursive(config, resolve(config.schemaDir), outputDir)
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!config.schema) {
|
|
361
|
-
console.error('Error: --schema (or --schemaDir) is required. Provide a path to a JSON Schema.')
|
|
362
|
-
process.exit(1)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
await runSingle(config, resolve(config.schema), outputDir)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
run().catch((error: unknown) => {
|
|
369
|
-
console.error(error instanceof Error ? error.message : error)
|
|
370
|
-
process.exit(1)
|
|
371
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { GeneratedFile } from '@amritk/generate-parsers'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { combineGeneratedFiles } from './combine-files'
|
|
5
|
-
|
|
6
|
-
describe('combineGeneratedFiles', () => {
|
|
7
|
-
it('drops relative imports and the index barrel, inlining every definition', () => {
|
|
8
|
-
const files: GeneratedFile[] = [
|
|
9
|
-
{
|
|
10
|
-
filename: 'document.ts',
|
|
11
|
-
content: "import type { Person } from './person';\n\nexport type Document = {\n owner?: Person;\n};",
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
filename: 'person.ts',
|
|
15
|
-
content: 'export type Person = {\n name: string;\n};',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
filename: 'index.ts',
|
|
19
|
-
content: "export type { Document } from './document';\nexport type { Person } from './person';\n",
|
|
20
|
-
},
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
const result = combineGeneratedFiles(files)
|
|
24
|
-
|
|
25
|
-
expect(result).not.toContain('import')
|
|
26
|
-
expect(result).not.toContain('./document')
|
|
27
|
-
expect(result).toContain('export type Document =')
|
|
28
|
-
expect(result).toContain('export type Person =')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('hoists and deduplicates external (non-relative) imports', () => {
|
|
32
|
-
const files: GeneratedFile[] = [
|
|
33
|
-
{
|
|
34
|
-
filename: 'a.ts',
|
|
35
|
-
content: "import { isObject } from '@amritk/helpers/is-object';\n\nexport const a = isObject;",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
filename: 'b.ts',
|
|
39
|
-
content: "import { isObject } from '@amritk/helpers/is-object';\n\nexport const b = isObject;",
|
|
40
|
-
},
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
const result = combineGeneratedFiles(files)
|
|
44
|
-
const importCount = result.split('\n').filter((line) => line.startsWith('import')).length
|
|
45
|
-
|
|
46
|
-
expect(importCount).toBe(1)
|
|
47
|
-
expect(result).toContain("import { isObject } from '@amritk/helpers/is-object';")
|
|
48
|
-
})
|
|
49
|
-
})
|
package/src/combine-files.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { GeneratedFile } from '@amritk/generate-parsers'
|
|
2
|
-
|
|
3
|
-
// Require whitespace after `import` so a property literally named `import` (e.g.
|
|
4
|
-
// `import?: string`) is not mistaken for an import statement.
|
|
5
|
-
const IMPORT_RE = /^import\s/
|
|
6
|
-
const RELATIVE_IMPORT_RE = /\bfrom\s+['"]\.\.?\//
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Merges the per-schema {@link GeneratedFile}s into a single self-contained
|
|
10
|
-
* TypeScript module. Each generated file imports its sibling definitions via
|
|
11
|
-
* relative paths; since everything now lives in one file those imports are
|
|
12
|
-
* dropped, while any external (package) imports are hoisted to the top and
|
|
13
|
-
* deduplicated. The `index.ts` barrel is skipped because it only re-exports the
|
|
14
|
-
* declarations that are already inlined here.
|
|
15
|
-
*
|
|
16
|
-
* Intended for types-only output, where files contain only type definitions and
|
|
17
|
-
* type-only relative imports (no runtime helpers).
|
|
18
|
-
*/
|
|
19
|
-
export const combineGeneratedFiles = (files: readonly GeneratedFile[]): string => {
|
|
20
|
-
const externalImports = new Set<string>()
|
|
21
|
-
const bodies: string[] = []
|
|
22
|
-
|
|
23
|
-
for (const file of files) {
|
|
24
|
-
if (file.filename === 'index.ts') continue
|
|
25
|
-
|
|
26
|
-
const bodyLines: string[] = []
|
|
27
|
-
for (const line of file.content.split('\n')) {
|
|
28
|
-
if (IMPORT_RE.test(line.trimStart())) {
|
|
29
|
-
// Relative imports point at sibling files that are now inlined — drop them.
|
|
30
|
-
if (RELATIVE_IMPORT_RE.test(line)) continue
|
|
31
|
-
externalImports.add(line.trim())
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
bodyLines.push(line)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const body = bodyLines.join('\n').trim()
|
|
38
|
-
if (body) bodies.push(body)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const importBlock = [...externalImports].sort().join('\n')
|
|
42
|
-
return [importBlock, bodies.join('\n\n')].filter(Boolean).join('\n\n') + '\n'
|
|
43
|
-
}
|