Skip to content

Commit

Permalink
feat: support incremental builds
Browse files Browse the repository at this point in the history
closes #5
  • Loading branch information
BenShelton committed Oct 8, 2021
1 parent 15f587e commit 8785d97
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ report.*.json

# Test files
tests/coverage
tests/fixtures/*.tsbuildinfo
4 changes: 3 additions & 1 deletion src/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import * as ts from 'typescript'
const scriptRegex = /<script.*>([\s\S]*)<\/script>/

export function createHost(options: ts.CompilerOptions): ts.CompilerHost {
const host = ts.createCompilerHost(options)
const host = options.incremental
? ts.createIncrementalCompilerHost(options)
: ts.createCompilerHost(options)
host.fileExists = (filename: string): boolean => {
// remove the .ts extension that TS will try to append when resolving
const actualFilename = filename.replace('.vue.ts', '.vue')
Expand Down
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ export async function tsc(opts: Options): Promise<void> {
// .vue is not a supported extension, so we fake it
// this is removed later during resolution
const rootNames = fileNames.map((f) => f.replace(vueFileRegex, '.vue.ts'))
const program = ts.createProgram({ options, rootNames, host })
const program = options.incremental
? ts.createIncrementalProgram({ options, rootNames, host })
: ts.createProgram({ options, rootNames, host })
const emitResult = program.emit()

const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics)
const diagnostics = [
...program.getConfigFileParsingDiagnostics(),
...program.getSyntacticDiagnostics(),
...program.getOptionsDiagnostics(),
...program.getSemanticDiagnostics(),
]

const allDiagnostics = diagnostics.concat(emitResult.diagnostics)

allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
Expand Down
1 change: 1 addition & 0 deletions tests/args.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Options, parseArgs } from '../src/args'
const cwd = resolve(__dirname, '..')

describe('args', () => {
// Tests
describe('parseArgs', () => {
test('uses default values', () => {
expect(parseArgs({})).toEqual({
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/tsconfig.clean.incremental.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"rootDir": "src-clean",
"strict": true,
"noEmit": true,
"incremental": true,
"tsBuildInfoFile": "clean.tsbuildinfo"
},
"include": [
"src-clean/**/*.ts",
"src-clean/**/*.vue"
]
}
15 changes: 15 additions & 0 deletions tests/fixtures/tsconfig.error-ts.incremental.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"rootDir": "src-error-ts",
"strict": true,
"noEmit": true,
"incremental": true,
"tsBuildInfoFile": "error-ts.tsbuildinfo"
},
"include": [
"src-error-ts/**/*.vue"
]
}
98 changes: 91 additions & 7 deletions tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { join } from 'path'
import { readFile, rm } from 'fs/promises'
import { join, resolve } from 'path'
import { tsc } from '../src'

const fixturesDir = join('tests', 'fixtures')

describe('index', () => {
// Setup
let consoleSpy: jest.SpyInstance
beforeAll(() => {
consoleSpy = jest.spyOn(console, 'log').mockReturnValue()
Expand All @@ -15,19 +17,45 @@ describe('index', () => {
consoleSpy.mockRestore()
})

/**
* Assertions for checking the `src-error-ts` directory.
*/
function errorTSExpectations(err: Error) {
expect(err.message).toBe('Type Check returned errors, see above')
expect(consoleSpy).toBeCalledTimes(1)
expect(consoleSpy.mock.calls[0][1]).toContain(
"/tests/fixtures/src-error-ts/messages.ts (1,14): Type 'string' is not assignable to type 'number'."
)
}

/**
* @param buildInfo The path to the `.tsbuildinfo` file.
* @param strings Asserts each of the provided strings appears in the buildInfo file.
*/
async function checkBuildFile(buildInfo: string, strings: string[]) {
const buildFile = await readFile(buildInfo, 'utf-8')
for (const str of strings) {
expect(buildFile).toContain(str)
}
}

// Tests
describe('tsc', () => {
test('succeeds if no errors', async () => {
expect.assertions(1)
try {
await tsc({
root: fixturesDir,
tsconfig: 'tsconfig.clean.json',
})
expect(consoleSpy).not.toBeCalled()
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
})

test('throws errors in vue files', async () => {
expect.hasAssertions()
try {
await tsc({
root: fixturesDir,
Expand All @@ -38,58 +66,114 @@ describe('index', () => {
expect(err.message).toBe('Type Check returned errors, see above')
expect(consoleSpy).toBeCalledTimes(1)
expect(consoleSpy.mock.calls[0][1]).toContain(
"App.vue (14,7): Type 'string' is not assignable to type 'number'."
"/tests/fixtures/src-error-vue/App.vue (14,7): Type 'string' is not assignable to type 'number'."
)
}
})

test('throws errors in imported files', async () => {
expect.hasAssertions()
try {
await tsc({
root: fixturesDir,
tsconfig: 'tsconfig.error-ts.json',
})
expect('Should not have passed').toBeFalsy()
} catch (err) {
expect(err.message).toBe('Type Check returned errors, see above')
expect(consoleSpy).toBeCalledTimes(1)
expect(consoleSpy.mock.calls[0][1]).toContain(
"messages.ts (1,14): Type 'string' is not assignable to type 'number'."
)
errorTSExpectations(err)
}
})

test('respects exclude option', async () => {
expect.assertions(1)
try {
await tsc({
root: fixturesDir,
tsconfig: 'tsconfig.ignore.exclude.json',
})
expect(consoleSpy).not.toBeCalled()
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
})

test('respects files option', async () => {
expect.assertions(1)
try {
await tsc({
root: fixturesDir,
tsconfig: 'tsconfig.ignore.files.json',
})
expect(consoleSpy).not.toBeCalled()
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
})

test('respects include option', async () => {
expect.assertions(1)
try {
await tsc({
root: fixturesDir,
tsconfig: 'tsconfig.ignore.include.json',
})
expect(consoleSpy).not.toBeCalled()
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
})

test('performs incremental builds (clean)', async () => {
expect.hasAssertions()
function runTSC() {
return tsc({
root: fixturesDir,
tsconfig: 'tsconfig.clean.incremental.json',
})
}
const buildInfo = resolve(fixturesDir, 'clean.tsbuildinfo')
await rm(buildInfo, { force: true })
try {
await runTSC()
await checkBuildFile(buildInfo, ['app.vue', 'other.vue', 'messages.ts'])

// check that the build info file is able to be read, this should take much less time
try {
await runTSC()
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
} catch (err) {
expect(consoleSpy).not.toBeCalled()
}
}, 20000)

test('performs incremental builds (error)', async () => {
expect.hasAssertions()
function runTSC() {
return tsc({
root: fixturesDir,
tsconfig: 'tsconfig.error-ts.incremental.json',
})
}
const buildInfo = resolve(fixturesDir, 'error-ts.tsbuildinfo')
await rm(buildInfo, { force: true })
try {
await runTSC()
expect('Should not have passed').toBeFalsy()
} catch (err) {
errorTSExpectations(err)
await checkBuildFile(buildInfo, ['app.vue', 'messages.ts'])

// check that the build info file is able to be read, this should take much less time
try {
consoleSpy.mockClear()
await runTSC()
expect('Should not have passed').toBeFalsy()
} catch (err) {
errorTSExpectations(err)
}
}
}, 20000)
})
})

0 comments on commit 8785d97

Please sign in to comment.