diff --git a/src/backend/config-loader.ts b/src/backend/config-loader.ts index 075d01c..ae37b5c 100644 --- a/src/backend/config-loader.ts +++ b/src/backend/config-loader.ts @@ -18,9 +18,7 @@ export class ConfigurationLoader { let options = configCache.get(cwd) if(!options) { - const configPath = await findUp('tsconfig.json', { - cwd: dirname(path) - }) + const configPath = await findUp('tsconfig.json', {cwd}) const tsconfig = configPath && commentsJson.parse(await readFile(configPath)) const transpilerOptions = { diff --git a/src/backend/reporter.ts b/src/backend/reporter.ts index 026d450..59a7bee 100644 --- a/src/backend/reporter.ts +++ b/src/backend/reporter.ts @@ -1,8 +1,8 @@ -import {TypeCheckingResult} from '../interfaces' +import {TypeCheckResult} from '../interfaces' import {formatDiagnostic} from './format' export function reportDiagnostics( - {semanticDiagnostics, syntacticDiagnostics}: TypeCheckingResult + {semanticDiagnostics, syntacticDiagnostics}: TypeCheckResult ): void { const frame = formatDiagnostic(syntacticDiagnostics.concat(semanticDiagnostics), process.cwd()) diff --git a/src/backend/service/host.ts b/src/backend/service/host.ts index d2ad9af..320ae44 100644 --- a/src/backend/service/host.ts +++ b/src/backend/service/host.ts @@ -6,7 +6,6 @@ export class LanguageServiceHost implements ts.LanguageServiceHost { public readDirectory = ts.sys.readDirectory private readonly fileNames: string[] - private readonly fileCache = new Map() private readonly files: {[k: string]: {version: number}} = {} private readonly options: ts.CompilerOptions @@ -40,12 +39,6 @@ export class LanguageServiceHost implements ts.LanguageServiceHost { } public getScriptSnapshot(fileName: string) { - /*const cached = this.fileCache.get(fileName) - - if(cached) { - return ts.ScriptSnapshot.fromString(cached) - }*/ - if(!ts.sys.fileExists(fileName)) { return } @@ -53,8 +46,6 @@ export class LanguageServiceHost implements ts.LanguageServiceHost { const content = ts.sys.readFile(fileName) if(content) { - this.fileCache.set(fileName, content) - return ts.ScriptSnapshot.fromString(content) } } diff --git a/src/backend/service/index.ts b/src/backend/service/index.ts index c8987c1..ab833e9 100644 --- a/src/backend/service/index.ts +++ b/src/backend/service/index.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript' import {LanguageServiceHost} from './host' -import {TypeCheckingResult} from '../../interfaces' +import {TypeCheckResult} from '../../interfaces' export class LanguageService { private readonly service: ts.LanguageService @@ -13,7 +13,7 @@ export class LanguageService { this.service = ts.createLanguageService(this.host, ts.createDocumentRegistry()) } - public parse(path: string): TypeCheckingResult { + public parse(path: string): TypeCheckResult { const {service} = this this.host.invalidate(path) diff --git a/src/backend/transpiler.ts b/src/backend/transpiler.ts index b131645..21729e0 100644 --- a/src/backend/transpiler.ts +++ b/src/backend/transpiler.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript' -import {TranspilationResult} from '../interfaces' +import {TranspileResult} from '../interfaces' export class Transpiler { private readonly compilerOptions: ts.CompilerOptions @@ -9,7 +9,7 @@ export class Transpiler { this.compilerOptions = options.options } - public transpile(code: string, fileName: string): TranspilationResult { + public transpile(code: string, fileName: string): TranspileResult { const {compilerOptions} = this const {outputText: js, sourceMapText: sourceMap} = ts.transpileModule(code, {compilerOptions, fileName}) diff --git a/src/frontend/injector/master.ts b/src/frontend/injector/master.ts index 31d8dd8..c460b5e 100644 --- a/src/frontend/injector/master.ts +++ b/src/frontend/injector/master.ts @@ -1,11 +1,9 @@ +import {ChildProcess, fork} from 'child_process' + import WorkerFarm = require('parcel-bundler/src/WorkerFarm') -import {ConfigurationLoader} from '../../backend/config-loader' -import {reportDiagnostics} from '../../backend/reporter' -import {LanguageService} from '../../backend/service' import {InjectedMessage} from '../../interfaces' - -let serviceConfig: ConfigurationLoader|null = null +import {typeCheck} from '../process/checker' /** * This function patch the WorkerFrame properties to interecpt @@ -19,10 +17,11 @@ export function inject() { } } +// We create a proxy layer between the worker and the master const injectedReceive = ((receive: () => void) => function(this: any, data: InjectedMessage) { if(data && data.__parcelTypeScript) { - receiveMessage(data) + receiveMessage(data, true) } else { return receive.call(this, data) @@ -30,7 +29,9 @@ const injectedReceive = ((receive: () => void) => } )(WorkerFarm.prototype.receive) -export async function receiveMessage(data: InjectedMessage) { +let forkedProcess: ChildProcess|null = null + +export async function receiveMessage(data: InjectedMessage, fromWorker: boolean) { if(!data || !data.__parcelTypeScript) { throw new Error('Unknown Parcel TypeScript input message') } @@ -38,17 +39,17 @@ export async function receiveMessage(data: InjectedMessage) { const {__parcelTypeScript: message} = data if(message.type === 'check-file') { - const {file} = message + // If we are in a worker (in watch mode) + if(fromWorker) { + if(forkedProcess === null) { + forkedProcess = fork(require.resolve('../process')) + } - // TODO: move this to a background thread - if(serviceConfig === null) { - serviceConfig = new ConfigurationLoader(file, config => new LanguageService(config)) + forkedProcess.send(message) + } + else { + typeCheck(message.file) } - - const service = await serviceConfig.wait() - const result = service.parse(file) - - reportDiagnostics(result) } else { throw new Error(`Unknown Parcel TypeScript message "${message}"`) diff --git a/src/frontend/injector/worker.ts b/src/frontend/injector/worker.ts index be082dc..a5b2d1f 100644 --- a/src/frontend/injector/worker.ts +++ b/src/frontend/injector/worker.ts @@ -19,6 +19,6 @@ function dispatchMessage(message: InjectedMessage) { // else we are in the main thread // this may happen on the first build else { - receiveMessage(message) + receiveMessage(message, false) } } diff --git a/src/frontend/process/checker.ts b/src/frontend/process/checker.ts new file mode 100644 index 0000000..bb35ae8 --- /dev/null +++ b/src/frontend/process/checker.ts @@ -0,0 +1,21 @@ +import {ConfigurationLoader} from '../../backend/config-loader' +import {reportDiagnostics} from '../../backend/reporter' +import {LanguageService} from '../../backend/service' + +let serviceConfig: ConfigurationLoader|null = null + +export async function typeCheck(...files: string[]) { + if(files.length === 0) { + return + } + + if(serviceConfig === null) { + serviceConfig = new ConfigurationLoader(files[0], config => new LanguageService(config)) + } + + const service = await serviceConfig.wait() + + files.forEach(file => + reportDiagnostics(service.parse(file)) + ) +} diff --git a/src/frontend/process/index.ts b/src/frontend/process/index.ts new file mode 100644 index 0000000..1cac492 --- /dev/null +++ b/src/frontend/process/index.ts @@ -0,0 +1,9 @@ +import {CheckFileMessage} from '../../interfaces' +import {Batcher} from '../../utils/batcher' + +import {typeCheck} from './checker' + +// The batcher will batch all check queries each 50 milliseconds. +const batcher = new Batcher(messages => typeCheck(...messages.map(({file}) => file)), 50) + +process.on('message', (message: CheckFileMessage) => batcher.emit(message)) diff --git a/src/interfaces.ts b/src/interfaces.ts index d680a3f..efb2be9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,27 +1,27 @@ import {Diagnostic} from 'typescript' -export interface Message { - __parcelTypeScript: U & { - type: T - } +export type Message = U & { + type: T } export type CheckFileMessage = Message<'check-file', { file: string }> -export type InjectedMessage = CheckFileMessage +export interface InjectedMessage { + __parcelTypeScript: CheckFileMessage +} -export interface TranspilationResult { +export interface TranspileResult { sources: { js: string sourceMap?: string } } -export interface TypeCheckingResult { +export interface TypeCheckResult { syntacticDiagnostics: Diagnostic[] semanticDiagnostics: Diagnostic[] - transpile(): TranspilationResult + transpile(): TranspileResult } diff --git a/src/utils/batcher.ts b/src/utils/batcher.ts new file mode 100644 index 0000000..5129add --- /dev/null +++ b/src/utils/batcher.ts @@ -0,0 +1,37 @@ +export class Batcher { + private timeout: NodeJS.Timer|null = null + private readonly queue: T[] = [] + + constructor( + private readonly work: (data: T[]) => void, + private readonly delay: number = 0 + ) {} + + public emit(data: T) { + this.queue.push(data) + + if(this.timeout === null) { + this.timeout = setTimeout(this.batch, this.delay) + } + } + + public clear(): void { + if(this.timeout != null) { + clearTimeout(this.timeout) + } + + this.drain() + } + + private drain(): T[] { + return this.queue.splice(0) + } + + private readonly batch = () => { + this.timeout = null + + if(this.queue.length > 0) { + this.work(this.drain()) + } + } +}