Skip to content

feat: multiple files to single output file #338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {compile, Options} from './index'
import {pathTransform, error} from './utils'

main(
minimist(process.argv.slice(2), {
minimist<Partial<Options>>(process.argv.slice(2), {
alias: {
help: ['h'],
input: ['i'],
Expand All @@ -20,7 +20,7 @@ main(
})
)

async function main(argv: minimist.ParsedArgs) {
async function main(argv: minimist.ParsedArgs & Partial<Options>) {
if (argv.help) {
printHelp()
process.exit(0)
Expand All @@ -32,20 +32,24 @@ async function main(argv: minimist.ParsedArgs) {
const ISGLOB = isGlob(argIn)
const ISDIR = isDir(argIn)

if ((ISGLOB || ISDIR) && argOut && argOut.includes('.d.ts')) {
throw new ReferenceError(
`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)`
)
}

try {
if ((ISGLOB || ISDIR) && argOut && argOut.includes('.d.ts')) {
const files = ISGLOB ? await glob(argIn) : getPaths(argIn)
await processAllToOne(files, argOut, argv)
return
}
if ((ISGLOB || ISDIR) && argOut === undefined) {
// writing to stdout, set usedName so piping it to a file doesn't have duplicate names.
// will leave duplicate banner comment to user though, we now support output to file where banner is taken care of.
argv.usedNames = new Set<string>()
}
// Process input as either glob, directory, or single file
if (ISGLOB) {
await processGlob(argIn, argOut, argv as Partial<Options>)
await processGlob(argIn, argOut, argv)
} else if (ISDIR) {
await processDir(argIn, argOut, argv as Partial<Options>)
await processDir(argIn, argOut, argv)
} else {
const result = await processFile(argIn, argv as Partial<Options>)
const result = await processFile(argIn, argv)
outputResult(result, argOut)
}
} catch (e) {
Expand Down Expand Up @@ -103,6 +107,24 @@ async function processDir(argIn: string, argOut: string | undefined, argv: Parti
)
}

async function processAllToOne(files: string[], outputFile: string, argv: Partial<Options>) {
// we will read all files in parallel but will do the processing in series
// because we need to ensure no race conditions with usedNames.
const usedNames = new Set<string>()
const schemas = await Promise.all(files.map(async file => [file, JSON.parse(await readInput(file))] as const))
let first = true
let wholeText = ''
for (const [filename, schema] of schemas) {
// override banner for each subsequent files so the banner only appears at the top.
const omitBanner = first ? {} : {bannerComment: `////////////`}
// again, we don't parallelize this to ensure no race conditions with usedNames.
const text = await compile(schema, filename, {usedNames, ...argv, ...omitBanner})
wholeText += (first ? '' : '\n') + text
first = false
}
outputResult(wholeText, outputFile)
}

async function outputResult(result: string, outputPath: string | undefined): Promise<void> {
if (!outputPath) {
process.stdout.write(result)
Expand Down Expand Up @@ -165,6 +187,8 @@ Boolean values can be set to false using the 'no-' prefix.
Output unknown type instead of any type
--unreachableDefinitions
Generates code for definitions that aren't referenced by the schema
--onlyExportMain
exports only the main schema(s), any definitions are kept internal.
`
)
}
28 changes: 15 additions & 13 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export function generate(ast: AST, options = DEFAULT_OPTIONS): string {
options.bannerComment,
declareNamedTypes(ast, options, ast.standaloneName!),
declareNamedInterfaces(ast, options, ast.standaloneName!),
declareEnums(ast, options)
declareEnums(ast, options, undefined, ast)
]
.filter(Boolean)
.join('\n\n') + '\n'
) // trailing newline
}

function declareEnums(ast: AST, options: Options, processed = new Set<AST>()): string {
function declareEnums(ast: AST, options: Options, processed = new Set<AST>(), rootAst?: AST): string {
if (processed.has(ast)) {
return ''
}
Expand All @@ -38,7 +38,7 @@ function declareEnums(ast: AST, options: Options, processed = new Set<AST>()): s

switch (ast.type) {
case 'ENUM':
type = generateStandaloneEnum(ast, options) + '\n'
type = generateStandaloneEnum(ast, options, ast === rootAst) + '\n'
break
case 'ARRAY':
return declareEnums(ast.params, options, processed)
Expand Down Expand Up @@ -77,7 +77,7 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string,
type = [
hasStandaloneName(ast) &&
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
generateStandaloneInterface(ast, options),
generateStandaloneInterface(ast, options, ast.standaloneName === rootASTName),
getSuperTypesAndParams(ast)
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed))
.filter(Boolean)
Expand Down Expand Up @@ -116,7 +116,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
case 'ARRAY':
type = [
declareNamedTypes(ast.params, options, rootASTName, processed),
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined
hasStandaloneName(ast) ? generateStandaloneType(ast, options, ast.standaloneName === rootASTName) : undefined
]
.filter(Boolean)
.join('\n')
Expand All @@ -138,7 +138,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
case 'TUPLE':
case 'UNION':
type = [
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
hasStandaloneName(ast) ? generateStandaloneType(ast, options, ast.standaloneName === rootASTName) : undefined,
ast.params
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
.filter(Boolean)
Expand All @@ -152,7 +152,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
break
default:
if (hasStandaloneName(ast)) {
type = generateStandaloneType(ast, options)
type = generateStandaloneType(ast, options, ast.standaloneName === rootASTName)
}
}

Expand Down Expand Up @@ -324,10 +324,10 @@ function generateComment(comment: string): string {
return ['/**', ...comment.split('\n').map(_ => ' * ' + _), ' */'].join('\n')
}

function generateStandaloneEnum(ast: TEnum, options: Options): string {
function generateStandaloneEnum(ast: TEnum, options: Options, isMainSchema: boolean): string {
return (
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') +
'export ' +
(isMainSchema || !options.onlyExportMain ? 'export ' : '') +
(options.enableConstEnums ? 'const ' : '') +
`enum ${toSafeString(ast.standaloneName)} {` +
'\n' +
Expand All @@ -337,21 +337,23 @@ function generateStandaloneEnum(ast: TEnum, options: Options): string {
)
}

function generateStandaloneInterface(ast: TNamedInterface, options: Options): string {
function generateStandaloneInterface(ast: TNamedInterface, options: Options, isMainSchema: boolean): string {
return (
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') +
`export interface ${toSafeString(ast.standaloneName)} ` +
(isMainSchema || !options.onlyExportMain ? 'export ' : '') +
`interface ${toSafeString(ast.standaloneName)} ` +
(ast.superTypes.length > 0
? `extends ${ast.superTypes.map(superType => toSafeString(superType.standaloneName)).join(', ')} `
: '') +
generateInterface(ast, options)
)
}

function generateStandaloneType(ast: ASTWithStandaloneName, options: Options): string {
function generateStandaloneType(ast: ASTWithStandaloneName, options: Options, isMainSchema: boolean): string {
return (
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') +
`export type ${toSafeString(ast.standaloneName)} = ${generateType(
(isMainSchema || !options.onlyExportMain ? 'export ' : '') +
`type ${toSafeString(ast.standaloneName)} = ${generateType(
omit<AST>(ast, 'standaloneName') as AST /* TODO */,
options
)}`
Expand Down
15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ export interface Options {
* [$RefParser](https://github.com/BigstickCarpet/json-schema-ref-parser) Options, used when resolving `$ref`s
*/
$refOptions: $RefOptions
/**
* specifies reserved interface or type names that should not be used.
* All types and interfaces generated by the call to compile will add to this set
* Used if putting multiple schemas in the same file, this ensures definition names never conflict.
*/
usedNames: Set<string> | undefined
/**
* when set to true only the interface / type / enum for the main schema is exported
* any other types such as definitions are defined but not exported.
*/
onlyExportMain: boolean
}

export const DEFAULT_OPTIONS: Options = {
Expand All @@ -88,7 +99,9 @@ export const DEFAULT_OPTIONS: Options = {
useTabs: false
},
unreachableDefinitions: false,
unknownAny: true
unknownAny: true,
usedNames: undefined,
onlyExportMain: false
}

export function compileFromFile(filename: string, options: Partial<Options> = DEFAULT_OPTIONS): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function parse(
keyName?: string,
isSchema = true,
processed: Processed = new Map<JSONSchema | JSONSchema4Type, AST>(),
usedNames = new Set<string>()
usedNames = options.usedNames ?? new Set<string>()
): AST {
// If we've seen this node before, return it.
if (processed.has(schema)) {
Expand Down
31 changes: 31 additions & 0 deletions test/__snapshots__/test/test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10704,3 +10704,34 @@ Generated by [AVA](https://avajs.dev).
[k: string]: unknown;␊
}␊
`

## files in (-i), single file out

> Snapshot 1

`/* tslint:disable */␊
/**␊
* This file was automatically generated by json-schema-to-typescript.␊
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊
* and run json-schema-to-typescript to regenerate this file.␊
*/␊
export interface ASchema {␊
f: Value;␊
g?: Value;␊
}␊
interface Value {␊
a?: string;␊
b?: string;␊
}␊
////////////␊
type Value1 = number;␊
export interface BSchema {␊
x?: string;␊
y: Value1;␊
[k: string]: unknown;␊
}␊
`
Binary file modified test/__snapshots__/test/test.ts.snap
Binary file not shown.
30 changes: 30 additions & 0 deletions test/resources/MultiSchema3/a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"title": "A schema",
"type": "object",
"definitions": {
"value": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
}
},
"additionalProperties": false
}
},
"properties": {
"f": {
"$ref": "#/definitions/value"
},
"g": {
"$ref": "#/definitions/value"
}
},
"additionalProperties": false,
"required": [
"f"
]
}
13 changes: 13 additions & 0 deletions test/resources/MultiSchema3/b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "B schema",
"type": "object",
"definitions": {
"value": {"type": "integer"}
},
"properties": {
"x": {"type": "string"},
"y": {"$ref": "#/definitions/value"}
},
"additionalProperties": true,
"required": ["y"]
}
7 changes: 7 additions & 0 deletions test/testCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ export function run() {
})
rimraf.sync('./test/resources/MultiSchema2/out')
})

test('files in (-i), single file out', t => {
const file = './test/resources/MultiSchema3/out.d.ts'
execSync(`node dist/src/cli.js -i './test/resources/MultiSchema3/' -o ${file} --onlyExportMain=true`)
t.snapshot(readFileSync(file, 'utf-8'))
unlinkSync(file)
})
}

function getPaths(path: string, paths: string[] = []) {
Expand Down