Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

Commit

Permalink
feat(ts compilation): improve support for incremental and composite p…
Browse files Browse the repository at this point in the history
…rojects

If you are building a project that has project references, rather than having to rebuild and
typecheck your files, ncdc will make use of the tyepscript build API. It will build any files that
have not yet been built and then skip the additional typecheck.

fix #337
  • Loading branch information
tamj0rd2 authored and probot-auto-merge[bot] committed Jul 30, 2020
1 parent be8e40b commit aac45a3
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 193 deletions.
9 changes: 8 additions & 1 deletion src/commands/generate/command.ts
Expand Up @@ -5,6 +5,7 @@ import createHandler, { GenerateArgs } from './handler'
import { getConfigTypes } from './config'
import { SchemaGenerator } from '~schema'
import { generate } from './generate'
import TsHelpers from '~schema/ts-helpers'

const builder = (yargs: Argv): Argv<GenerateArgs> =>
yargs
Expand Down Expand Up @@ -33,7 +34,13 @@ export default function createGenerateCommand(getCommonDeps: GetRootDeps): Comma
generate,
getConfigTypes,
getSchemaGenerator: (tsconfigPath, force) => {
const generator = new SchemaGenerator(tsconfigPath, force, reportMetric, logger)
const tsHelpers = new TsHelpers(reportMetric, logger)
const generator = new SchemaGenerator(
tsHelpers.createProgram(tsconfigPath, !force),
force,
reportMetric,
logger,
)
generator.init()
return generator
},
Expand Down
10 changes: 9 additions & 1 deletion src/commands/serve/index.ts
Expand Up @@ -9,6 +9,7 @@ import Ajv from 'ajv'
import { FsSchemaLoader, WatchingSchemaGenerator } from '~schema'
import { SchemaGenerator } from '~schema'
import createServerLogger from './server/server-logger'
import TsHelpers from '~schema/ts-helpers'

const builder = (yargs: Argv): Argv<ServeArgs> =>
yargs
Expand Down Expand Up @@ -45,13 +46,20 @@ export default function createServeCommand(getCommonDeps: GetRootDeps) {

if (args.schemaPath) return new TypeValidator(ajv, new FsSchemaLoader(args.schemaPath))
if (!args.watch) {
const generator = new SchemaGenerator(args.tsconfigPath, args.force, reportMetric, logger)
const tsHelpers = new TsHelpers(reportMetric, logger)
const generator = new SchemaGenerator(
tsHelpers.createProgram(args.tsconfigPath, !args.force),
args.force,
reportMetric,
logger,
)
generator.init()
return new TypeValidator(ajv, generator)
}

const watcher = new WatchingSchemaGenerator(
args.tsconfigPath,
new TsHelpers(reportMetric, logger),
logger,
reportMetric,
onReload,
Expand Down
9 changes: 8 additions & 1 deletion src/commands/test/command.ts
Expand Up @@ -9,6 +9,7 @@ import { SchemaGenerator } from '~schema'
import Ajv from 'ajv'
import { TypeValidator } from '~validation'
import { createHttpClient } from './http-client'
import TsHelpers from '~schema/ts-helpers'

const builder = (yargs: Argv): Argv<TestArgs> =>
yargs
Expand Down Expand Up @@ -51,8 +52,14 @@ export default function createTestCommand(getCommonDeps: GetRootDeps): CommandMo
createTypeValidator: () => {
const ajv = new Ajv({ verbose: true, allErrors: true })
if (schemaPath) return new TypeValidator(ajv, new FsSchemaLoader(schemaPath))
const tsHelpers = new TsHelpers(reportMetric, logger)

const schemaGenerator = new SchemaGenerator(tsconfigPath, force, reportMetric, logger)
const schemaGenerator = new SchemaGenerator(
tsHelpers.createProgram(tsconfigPath, !force),
force,
reportMetric,
logger,
)
schemaGenerator.init()
return new TypeValidator(ajv, schemaGenerator)
},
Expand Down
68 changes: 9 additions & 59 deletions src/schema/schema-generator.spec.ts
@@ -1,7 +1,6 @@
import { SchemaGenerator } from './schema-generator'
import ts from 'typescript'
import { mockObj, randomString, mockFn } from '~test-helpers'
import * as tsHelpers from './ts-helpers'
import { ReportMetric } from '~commands/shared'
import * as tsj from 'ts-json-schema-generator'
import { NoRootTypeError } from 'ts-json-schema-generator'
Expand All @@ -13,13 +12,11 @@ jest.mock('ts-json-schema-generator')
jest.mock('typescript')
jest.mock('path')
jest.mock('fs')
jest.mock('./ts-helpers')

describe('SchemaLoader', () => {
const mockedTsj = mockObj(tsj)
const mockedTsjGenerator = mockObj<tsj.SchemaGenerator>({ createSchema: jest.fn() })
const mockedTypescript = mockObj(ts)
const mockedTsHelpers = mockObj(tsHelpers)
const mockedreportMetric = mockFn<ReportMetric>()
const spyLogger = mockObj<Logger>({ verbose: jest.fn() })

Expand All @@ -35,66 +32,19 @@ describe('SchemaLoader', () => {
)
mockedTypescript.getPreEmitDiagnostics.mockReturnValue([])
mockedTsj.SchemaGenerator.mockImplementation(() => mockedTsjGenerator)
mockedTsHelpers.readTsConfig.mockReturnValue({} as ts.ParsedCommandLine)
mockedTsHelpers.formatErrorDiagnostic.mockImplementation(({ messageText }) =>
typeof messageText === 'string' ? messageText : 'poop',
)

mockedreportMetric.mockReturnValue(
mockObj<OperationResult>({ fail: jest.fn(), success: jest.fn() }),
)
})

const createSchemaGenerator = (
pathOrProgram: string | ts.Program = '',
skipTypeChecking = false,
): SchemaGenerator => new SchemaGenerator(pathOrProgram, skipTypeChecking, mockedreportMetric, spyLogger)

describe('when a tsconfig path is given', () => {
const tsconfigPath = 'tsconfig path'

it('throws when there are errors and skipTypeChecking is false', () => {
mockedTypescript.getPreEmitDiagnostics.mockReturnValue([
mockObj<ts.Diagnostic>({ messageText: 'woah' }),
])

const schemaLoader = createSchemaGenerator(tsconfigPath)

expect(() => schemaLoader.init()).toThrowError('Your typescript project has compilation errors')
expect(spyLogger.verbose).toBeCalledWith('woah')
})

it('does not throw when there are no errors and skipTypeChecking is false', () => {
mockedTypescript.getPreEmitDiagnostics.mockReturnValue([])

const schemaLoader = createSchemaGenerator(tsconfigPath)

expect(() => schemaLoader.init()).not.toThrowError()
})

it('does not typecheck if skipTypeChecking is true', () => {
mockedTypescript.getPreEmitDiagnostics.mockReturnValue([
mockObj<ts.Diagnostic>({ messageText: 'woah' }),
])

const schemaLoader = createSchemaGenerator(tsconfigPath, true)

expect(() => schemaLoader.init()).not.toThrowError()
expect(mockedTypescript.getPreEmitDiagnostics).not.toBeCalled()
})
})

it('calls read ts config with the correct args', () => {
const tsconfigPath = randomString('tsconfig path')

createSchemaGenerator(tsconfigPath).init()

expect(mockedTsHelpers.readTsConfig).toBeCalledWith(tsconfigPath)
})
const createSchemaGenerator = (skipTypeChecking = false): SchemaGenerator => {
const mockProgram = mockObj<ts.Program>({})
return new SchemaGenerator(mockProgram, skipTypeChecking, mockedreportMetric, spyLogger)
}

describe('loading schemas', () => {
it('throws if there is no generator', async () => {
const schemaGenerator = createSchemaGenerator('tsconfig path', true)
const schemaGenerator = createSchemaGenerator(true)

await expect(() => schemaGenerator.load('bananas')).rejects.toThrowError(
'This SchemaGenerator instance has not been initialised',
Expand All @@ -106,7 +56,7 @@ describe('SchemaLoader', () => {
throw new NoRootTypeError(randomString('yikes'))
})

const schemaGenerator = createSchemaGenerator('', true)
const schemaGenerator = createSchemaGenerator(true)
schemaGenerator.init()

await expect(schemaGenerator.load('lol')).rejects.toThrowError('Could not find type: lol')
Expand All @@ -117,7 +67,7 @@ describe('SchemaLoader', () => {
throw new Error(randomString('yikes'))
})

const schemaGenerator = createSchemaGenerator('', true)
const schemaGenerator = createSchemaGenerator(true)
schemaGenerator.init()

await expect(schemaGenerator.load('lol')).rejects.toThrowError(
Expand All @@ -129,7 +79,7 @@ describe('SchemaLoader', () => {
const someSchema = { $schema: 'schema stuff' }
mockedTsjGenerator.createSchema.mockReturnValue(someSchema)

const schemaLoader = createSchemaGenerator('tsconfig path', true)
const schemaLoader = createSchemaGenerator(true)
schemaLoader.init()
const schema = await schemaLoader.load('DealSchema')

Expand All @@ -141,7 +91,7 @@ describe('SchemaLoader', () => {
const someSchema2 = { $schema: 'schema stuff 2' }
mockedTsjGenerator.createSchema.mockReturnValueOnce(someSchema).mockReturnValueOnce(someSchema2)

const schemaLoader = createSchemaGenerator('tsconfig path', true)
const schemaLoader = createSchemaGenerator(true)
schemaLoader.init()
const schema1 = await schemaLoader.load('DealSchema')
const schema2 = await schemaLoader.load('DealSchema')
Expand Down
35 changes: 4 additions & 31 deletions src/schema/schema-generator.ts
@@ -1,6 +1,5 @@
import { SchemaRetriever } from './types'
import ts from 'typescript'
import { readTsConfig, formatErrorDiagnostic } from './ts-helpers'
import type { Program } from 'typescript'
import { ReportMetric } from '~commands/shared'
import {
SchemaGenerator as TsSchemaGenerator,
Expand All @@ -19,15 +18,14 @@ export class SchemaGenerator implements SchemaRetriever {
private generateJsonSchema?: JsonSchemaGenerator

constructor(
private readonly pathOrProgram: string | ts.Program,
private readonly program: Program,
private readonly skipTypeChecking: boolean,
private readonly reportMetric: ReportMetric,
private readonly logger: NcdcLogger,
) {}

public init(): void {
const program = this.getTsProgram()
this.generateJsonSchema = this.createGenerator(program)
this.generateJsonSchema = this.createGenerator(this.program)
}

public load = async (symbolName: string): Promise<JSONSchema7> => {
Expand Down Expand Up @@ -58,32 +56,7 @@ export class SchemaGenerator implements SchemaRetriever {
}
}

private getTsProgram(): ts.Program {
if (typeof this.pathOrProgram !== 'string') return this.pathOrProgram
const { success, fail } = this.reportMetric('build a typescript program')
const configFile = readTsConfig(this.pathOrProgram)

const incrementalProgram = ts.createIncrementalProgram({
rootNames: configFile.fileNames,
options: configFile.options,
projectReferences: configFile.projectReferences,
})
const program = incrementalProgram.getProgram()

if (!this.skipTypeChecking) {
const diagnostics = ts.getPreEmitDiagnostics(program)
if (diagnostics.length) {
fail()
this.logger.verbose(diagnostics.map(formatErrorDiagnostic).join('\n'))
throw new Error('Your typescript project has compilation errors. Run tsc to debug.')
}
}

success()
return program
}

private createGenerator(program: ts.Program): JsonSchemaGenerator {
private createGenerator(program: Program): JsonSchemaGenerator {
const { success } = this.reportMetric('build a schema generator')
const config: Config = { skipTypeCheck: true, expose: 'all', additionalProperties: true }
const generator = new TsSchemaGenerator(
Expand Down

0 comments on commit aac45a3

Please sign in to comment.