Skip to content

Commit

Permalink
BREAKING: Generate output to a single file
Browse files Browse the repository at this point in the history
  • Loading branch information
akheron committed Mar 18, 2022
1 parent aec7dc7 commit ca74a2f
Show file tree
Hide file tree
Showing 7 changed files with 684 additions and 692 deletions.
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,15 @@ import * as swaggerUi from 'swagger-ui-express'
import { prefix } from 'typera-openapi'

import myRoutes from './my-routes'
import myRouteDefs from './my-routes.openapi'
import openapi from './openapi'

const openapiDoc: OpenAPIV3.Document = {
openapi: '3.0.0',
info: {
title: 'My cool API',
version: '0.1.0',
},
paths: {
...prefix('/api', myRouteDefs.paths),
},
...openapi,
}

const app = express()
Expand All @@ -102,9 +100,6 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiDoc))
app.listen(3000)
```

The `prefix` function is used to move OpenAPI path definitions to a different
prefix, because the `myRoutes` are served from the `/api` prefix.

## CLI

```
Expand Down
78 changes: 30 additions & 48 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#!/usr/bin/env node
import * as fs from 'fs'
import * as path from 'path'
import { OpenAPIV3 } from 'openapi-types'
import * as ts from 'typescript'
import * as yargs from 'yargs'

import { generate } from '.'
import { GenerateResult, generate } from '.'
import { Logger } from './context'
import { runPrettier } from './prettify'

Expand All @@ -22,37 +20,39 @@ const parseArgs = () =>
type: 'boolean',
default: false,
})
.option('format', {
description: 'Output file format',
choices: ['ts' as const, 'json' as const],
default: 'ts' as Format,
.option('outfile', {
alias: 'o',
description: 'Output file. Must end in `.ts` or `.json`.',
default: 'openapi.ts',
coerce: (arg: string): [string, Format] => {
if (arg.endsWith('.ts')) return [arg, 'ts']
if (arg.endsWith('.json')) return [arg, 'json']
throw new Error('outfile must end in `.ts` or `.json`')
},
})
.option('tsconfig', {
description: 'Which tsconfig.json to use',
default: 'tsconfig.json',
})
.option('prettify', {
alias: 'p',
description: 'Apply prettier to output files',
description: 'Apply prettier to the output file',
type: 'boolean',
default: false,
})
.option('check', {
alias: 'c',
description:
'Exit with an error if output files are not up-to-date (useful for CI)',
'Exit with an error if the output file is not up-to-date (useful for CI)',
type: 'boolean',
default: false,
}).argv

const outputFileName = (sourceFileName: string, ext: string): string =>
sourceFileName.slice(0, -path.extname(sourceFileName).length) + ext

const main = async () => {
const main = async (): Promise<number> => {
const args = parseArgs()

const sourceFiles = args._.map((x) => path.resolve(x.toString()))
const ext = `.openapi.${args.format}`
const [outfile, format] = args.outfile

const compilerOptions = readCompilerOptions(args.tsconfig)
if (!compilerOptions) process.exit(1)
Expand All @@ -61,32 +61,18 @@ const main = async () => {
console.log(`Compiler options: ${JSON.stringify(compilerOptions, null, 2)}`)
}

const results = generate(sourceFiles, compilerOptions, {
log,
}).map((result) => ({
...result,
outputFileName: outputFileName(result.fileName, ext),
}))

let success = true
for (const { outputFileName, paths, components } of results) {
let content =
args.format === 'ts'
? tsString(paths, components)
: jsonString(paths, components)
if (args.prettify) {
content = await runPrettier(outputFileName, content)
}
if (args.check) {
if (!checkOutput(outputFileName, content)) success = false
} else {
writeOutput(outputFileName, content)
}
const result = generate(sourceFiles, compilerOptions, { log })
let content = format === 'ts' ? tsString(result) : jsonString(result)
if (args.prettify) {
content = await runPrettier(outfile, content)
}

if (!success) {
process.exit(1)
if (args.check) {
if (!checkOutput(outfile, content)) return 1
} else {
writeOutput(outfile, content)
}

return 0
}

const readCompilerOptions = (
Expand Down Expand Up @@ -136,22 +122,18 @@ const writeOutput = (fileName: string, content: string): void => {
fs.writeFileSync(fileName, content)
}

const tsString = (
paths: OpenAPIV3.PathsObject,
components: OpenAPIV3.ComponentsObject
): string => `\
const tsString = (result: GenerateResult): string => `\
import { OpenAPIV3 } from 'openapi-types'
const spec: { paths: OpenAPIV3.PathsObject, components: OpenAPIV3.ComponentsObject } = ${JSON.stringify(
{ paths, components }
result
)};
export default spec;
`

const jsonString = (
paths: OpenAPIV3.PathsObject,
components: OpenAPIV3.ComponentsObject
): string => JSON.stringify({ paths, components })
const jsonString = (result: GenerateResult): string => JSON.stringify(result)

main()
main().then((status) => {
if (status !== 0) process.exit(status)
})
22 changes: 9 additions & 13 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ interface GenerateOptions {
log: Logger
}

interface Result {
fileName: string
export interface GenerateResult {
paths: OpenAPIV3.PathsObject
components: OpenAPIV3.ComponentsObject
}
Expand All @@ -37,35 +36,32 @@ export const generate = (
fileNames: string[],
compilerOptions: ts.CompilerOptions,
options?: GenerateOptions
): Result[] => {
): GenerateResult => {
const log = options?.log || (() => undefined)
const program = ts.createProgram(fileNames, compilerOptions)
const checker = program.getTypeChecker()

const result: Result[] = []
const components = new Components()
let paths: OpenAPIV3.PathsObject = {}

for (const sourceFile of program.getSourceFiles()) {
if (!fileNames.includes(sourceFile.fileName)) continue
if (sourceFile.isDeclarationFile) continue

ts.forEachChild(sourceFile, (node) => {
const components = new Components()
const paths = visitTopLevelNode(
const newPaths = visitTopLevelNode(
context(checker, sourceFile, log, node),
components,
node
)
if (paths) {
result.push({
fileName: sourceFile.fileName,
paths,
components: components.build(),
})
if (newPaths) {
// TODO: What if a route is defined multiple times?
paths = { ...paths, ...newPaths }
}
})
}

return result
return { paths, components: components.build() }
}

const visitTopLevelNode = (
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { generate } from './generate'
export { GenerateResult, generate } from './generate'
export { LogLevel } from './context'
import { OpenAPIV3 } from 'openapi-types'

Expand Down
Loading

0 comments on commit ca74a2f

Please sign in to comment.