Skip to content

Commit a0257d8

Browse files
add cli directory support (using globs) (#238)
* Add directory support * updates * reset vscode settings * fixes on fixes * complete glob and dir support * add error check * remove ?. * use old node friendly methods * bump @types/node * actually add the package.json this time * resolve pr comments * add pathTransform test * fix util test * remove return and add documentation
1 parent cd7990e commit a0257d8

File tree

15 files changed

+646
-33
lines changed

15 files changed

+646
-33
lines changed

README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ See [server demo](example) and [browser demo](https://github.com/bcherny/json-sc
9494
| $refOptions | object | `{}` | [$RefParser](https://github.com/BigstickCarpet/json-schema-ref-parser) Options, used when resolving `$ref`s |
9595
## CLI
9696

97-
A simple CLI utility is provided with this package.
97+
A CLI utility is provided with this package.
9898

9999
```sh
100100
cat foo.json | json2ts > foo.d.ts
@@ -117,6 +117,71 @@ json2ts -i foo.json -o foo.d.ts --unreachableDefinitions
117117
json2ts -i foo.json -o foo.d.ts --style.singleQuote --no-style.semi
118118
```
119119

120+
The CLI supports directory of definitions as well. It supports directory paths, glob patterns, and output directories.
121+
122+
Example 1: Directory of type definitions to an output directory
123+
124+
Input Directory
125+
```
126+
schemas /
127+
| a.json
128+
| b.json
129+
```
130+
131+
```sh
132+
json2ts -i schemas/ -o types/
133+
```
134+
135+
Output Directory
136+
```
137+
types /
138+
| a.d.ts
139+
| b.d.ts
140+
```
141+
142+
Example 2: Directory to pipe out
143+
Input Directory
144+
```
145+
schemas /
146+
| a.json
147+
| b.json
148+
```
149+
150+
```sh
151+
json2ts -i schemas/
152+
```
153+
154+
Example 3: Nested input directory mapped to nested output
155+
Input Directory
156+
```
157+
schemas /
158+
foo /
159+
| a.json
160+
bar /
161+
| b.json
162+
fuzz /
163+
c.json
164+
buzz /
165+
d.json
166+
```
167+
168+
```sh
169+
json2ts -i schemas/ -o types/
170+
```
171+
172+
Output Directory
173+
```
174+
types /
175+
foo /
176+
| a.d.ts
177+
bar /
178+
| b.d.ts
179+
fuzz /
180+
c.d.ts
181+
buzz /
182+
d.d.ts
183+
```
184+
120185
## Tests
121186

122187
`npm test`

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,29 @@
4545
},
4646
"homepage": "https://github.com/bcherny/json-schema-to-typescript#readme",
4747
"dependencies": {
48+
"@types/is-glob": "^4.0.1",
4849
"@types/json-schema": "^7.0.3",
49-
"@types/node": ">=4.5.0",
50+
"@types/mkdirp": "^0.5.2",
5051
"@types/prettier": "^1.16.1",
5152
"cli-color": "^1.4.0",
53+
"glob": "^7.1.4",
54+
"is-glob": "^4.0.1",
5255
"json-schema-ref-parser": "^6.1.0",
5356
"json-stringify-safe": "^5.0.1",
5457
"lodash": "^4.17.11",
5558
"minimist": "^1.2.0",
59+
"mkdirp": "^0.5.1",
5660
"mz": "^2.7.0",
5761
"prettier": "^1.19.1",
5862
"stdin": "0.0.1"
5963
},
6064
"devDependencies": {
6165
"@types/cli-color": "^0.3.29",
66+
"@types/glob": "^7.1.1",
6267
"@types/lodash": "^4.14.121",
6368
"@types/minimist": "^1.2.0",
6469
"@types/mz": "0.0.32",
70+
"@types/node": "^12.12.29",
6571
"@typescript-eslint/eslint-plugin": "^2.9.0",
6672
"@typescript-eslint/parser": "^2.9.0",
6773
"ava": "^1.2.1",

src/cli.ts

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
#!/usr/bin/env node
22

33
import {whiteBright} from 'cli-color'
4-
import {JSONSchema4} from 'json-schema'
54
import minimist = require('minimist')
6-
import {readFile, writeFile} from 'mz/fs'
7-
import {resolve} from 'path'
5+
import {readFile, writeFile, existsSync, lstatSync, readdirSync} from 'mz/fs'
6+
import * as _mkdirp from 'mkdirp'
7+
import * as _glob from 'glob'
8+
import isGlob = require('is-glob')
9+
import {promisify} from 'util'
10+
import {join, resolve, dirname, basename} from 'path'
811
import stdin = require('stdin')
912
import {compile, Options} from './index'
13+
import {pathTransform} from './utils'
14+
15+
// Promisify mkdirp & glob
16+
const mkdirp = (path: string): Promise<_mkdirp.Made> =>
17+
new Promise((res, rej) => {
18+
_mkdirp(path, (err, made) => {
19+
if (err) rej(err)
20+
else res(made === null ? undefined : made)
21+
})
22+
})
23+
24+
const glob = promisify(_glob)
1025

1126
main(
1227
minimist(process.argv.slice(2), {
@@ -25,35 +40,103 @@ async function main(argv: minimist.ParsedArgs) {
2540
}
2641

2742
const argIn: string = argv._[0] || argv.input
28-
const argOut: string = argv._[1] || argv.output
43+
const argOut: string | undefined = argv._[1] || argv.output // the output can be omitted so this can be undefined
44+
45+
const ISGLOB = isGlob(argIn)
46+
const ISDIR = isDir(argIn)
47+
48+
if ((ISGLOB || ISDIR) && argOut && argOut.includes('.d.ts')) {
49+
throw new ReferenceError(
50+
`You have specified a single file ${argOut} output for a multi file input ${argIn}. This feature is not yet supported, refer to issue #272 (https://github.com/bcherny/json-schema-to-typescript/issues/272)`
51+
)
52+
}
2953

3054
try {
31-
const schema: JSONSchema4 = JSON.parse(await readInput(argIn))
32-
const ts = await compile(schema, argIn, argv as Partial<Options>)
33-
await writeOutput(ts, argOut)
55+
// Process input as either glob, directory, or single file
56+
if (ISGLOB) {
57+
await processGlob(argIn, argOut, argv as Partial<Options>)
58+
} else if (ISDIR) {
59+
await processDir(argIn, argOut, argv as Partial<Options>)
60+
} else {
61+
await processFile(argIn, argOut, argv as Partial<Options>)
62+
}
3463
} catch (e) {
3564
console.error(whiteBright.bgRedBright('error'), e)
3665
process.exit(1)
3766
}
3867
}
3968

40-
function readInput(argIn?: string) {
41-
if (!argIn) {
42-
return new Promise(stdin)
69+
// check if path is an existing directory
70+
function isDir(path: string): boolean {
71+
return existsSync(path) && lstatSync(path).isDirectory()
72+
}
73+
74+
async function processGlob(argIn: string, argOut: string | undefined, argv: Partial<Options>) {
75+
const files = await glob(argIn) // execute glob pattern match
76+
77+
if (files.length === 0) {
78+
throw ReferenceError(
79+
`You passed a glob pattern "${argIn}", but there are no files that match that pattern in ${process.cwd()}`
80+
)
4381
}
44-
return readFile(resolve(process.cwd(), argIn), 'utf-8')
82+
// create output directory if it does not exist
83+
if (argOut && !existsSync(argOut)) {
84+
await mkdirp(argOut)
85+
}
86+
87+
Promise.all(
88+
files.map(file => {
89+
const outPath = argOut && `${argOut}/${basename(file, '.json')}.d.ts`
90+
processFile(file, outPath, argv)
91+
})
92+
)
4593
}
4694

47-
function writeOutput(ts: string, argOut: string): Promise<void> {
95+
async function processDir(argIn: string, argOut: string | undefined, argv: Partial<Options>) {
96+
const files = getPaths(argIn)
97+
98+
Promise.all(
99+
files.map(file => {
100+
if (!argOut) {
101+
processFile(file, argOut, argv)
102+
} else {
103+
let outPath = pathTransform(argOut, file)
104+
if (!isDir(dirname(outPath))) {
105+
_mkdirp.sync(dirname(outPath))
106+
}
107+
outPath = outPath.replace('.json', '.d.ts')
108+
processFile(file, outPath, argv)
109+
}
110+
})
111+
)
112+
}
113+
114+
async function processFile(argIn: string, argOut: string | undefined, argv: Partial<Options>): Promise<void> {
115+
const schema = JSON.parse(await readInput(argIn))
116+
const ts = await compile(schema, argIn, argv)
117+
48118
if (!argOut) {
49-
try {
50-
process.stdout.write(ts)
51-
return Promise.resolve()
52-
} catch (err) {
53-
return Promise.reject(err)
54-
}
119+
process.stdout.write(ts)
120+
} else {
121+
return await writeFile(argOut, ts)
122+
}
123+
}
124+
125+
function getPaths(path: string, paths: string[] = []) {
126+
if (existsSync(path) && lstatSync(path).isDirectory()) {
127+
readdirSync(resolve(path)).forEach(item => getPaths(join(path, item), paths))
128+
} else {
129+
paths.push(path)
55130
}
56-
return writeFile(argOut, ts)
131+
132+
return paths
133+
}
134+
135+
function readInput(argIn?: string) {
136+
if (!argIn) {
137+
return new Promise(stdin)
138+
}
139+
return readFile(resolve(process.cwd(), argIn), 'utf-8')
57140
}
58141

59142
function printHelp() {

src/utils.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {whiteBright} from 'cli-color'
22
import {deburr, isPlainObject, mapValues, trim, upperFirst} from 'lodash'
3-
import {basename, extname} from 'path'
3+
import {basename, extname, join} from 'path'
44
import {JSONSchema} from './types/JSONSchema'
55

66
// TODO: pull out into a separate package
@@ -221,3 +221,26 @@ export function escapeBlockComment(schema: JSONSchema) {
221221
}
222222
}
223223
}
224+
225+
/*
226+
the following logic determines the out path by comparing the in path to the users specified out path.
227+
For example, if input directory MultiSchema looks like:
228+
MultiSchema/foo/a.json
229+
MultiSchema/bar/fuzz/c.json
230+
MultiSchema/bar/d.json
231+
And the user wants the outputs to be in MultiSchema/Out, then this code will be able to map the inner directories foo, bar, and fuzz into the intended Out directory like so:
232+
MultiSchema/Out/foo/a.json
233+
MultiSchema/Out/bar/fuzz/c.json
234+
MultiSchema/Out/bar/d.json
235+
*/
236+
export function pathTransform(o: string, i: string): string {
237+
const outPathList = o.split('/')
238+
const inPathList = i.split('/')
239+
240+
const intersection = outPathList.filter(x => inPathList.includes(x))
241+
const symmetricDifference = outPathList
242+
.filter(x => !inPathList.includes(x))
243+
.concat(inPathList.filter(x => !outPathList.includes(x)))
244+
245+
return join(...intersection, ...symmetricDifference)
246+
}

0 commit comments

Comments
 (0)