|
import * as ts from 'typescript' |
|
import * as Bundler from 'parcel-bundler' |
|
import * as express from 'express' |
|
import { createProxyMiddleware, Options as ProxyOptions } from 'http-proxy-middleware' |
|
import { merge, Observable, partition, Subscriber } from 'rxjs' |
|
import { buffer, bufferCount, filter, map, share, skipWhile, tap, timestamp } from 'rxjs/operators' |
|
|
|
//============================================================ |
|
|
|
process.env.NODE_ENV = 'development' |
|
|
|
const protocol = 'http' |
|
const entryPoint = 'src/index.html' |
|
const port = Number(process.env.PORT || 1234) |
|
|
|
const proxies: Record<string, ProxyOptions> = { |
|
'/api': { |
|
target: `${protocol}://localhost:3000`, |
|
logLevel: 'warn', |
|
}, |
|
} |
|
|
|
const parcelOptions = { |
|
autoInstall: false, |
|
open: true, // this does not work |
|
} as Bundler.ParcelOptions |
|
|
|
//============================================================ |
|
|
|
class BundlerEvent {} |
|
|
|
class BundlerBuildStartEvent extends BundlerEvent {} |
|
|
|
class BundlerBuildEndEvent extends BundlerEvent {} |
|
|
|
type WatcherCallback = () => void |
|
|
|
class CompilerEvent {} |
|
|
|
class CompilerStartEvent extends CompilerEvent {} |
|
|
|
class CompilerEndEvent extends CompilerEvent {} |
|
|
|
const bundler$ = new Observable((subscriber: Subscriber<BundlerEvent>) => { |
|
const bundler = new Bundler(entryPoint, parcelOptions) |
|
|
|
bundler.on('buildStart', () => subscriber.next(new BundlerBuildStartEvent())) |
|
bundler.on('buildEnd', () => subscriber.next(new BundlerBuildEndEvent())) |
|
|
|
const app = express() |
|
|
|
Object.keys(proxies).forEach(path => app.use(path, createProxyMiddleware(proxies[path]))) |
|
|
|
app.use(bundler.middleware()) |
|
|
|
app.listen(port) |
|
}).pipe(share()) |
|
|
|
const tsc$ = new Observable((subscriber: Subscriber<WatcherCallback | CompilerEvent>) => { |
|
const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json') |
|
if (!configPath) { |
|
throw new Error("Could not find a valid 'tsconfig.json'.") |
|
} |
|
|
|
const formatHost: ts.FormatDiagnosticsHost = { |
|
getCanonicalFileName: path => path, |
|
getCurrentDirectory: ts.sys.getCurrentDirectory, |
|
getNewLine: () => ts.sys.newLine, |
|
} |
|
|
|
const host = ts.createWatchCompilerHost( |
|
configPath, |
|
{ preserveWatchOutput: true, pretty: true }, |
|
ts.sys, |
|
ts.createSemanticDiagnosticsBuilderProgram, |
|
diagnostic => console.error(ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost)), |
|
ts.createBuilderStatusReporter(ts.sys, true), |
|
{ synchronousWatchDirectory: false }, |
|
) |
|
|
|
// Delay watcher callbacks until we decide when to notify the compiler |
|
const origWatchFile = host.watchFile |
|
host.watchFile = function(path, callback, pollingInterval, options) { |
|
const newCb = (fileName, eventKind) => subscriber.next(() => callback(fileName, eventKind)) |
|
return origWatchFile.call(this, path, newCb, pollingInterval, options) |
|
} |
|
|
|
const origWatchDirectory = host.watchDirectory |
|
host.watchDirectory = function(path, callback, recursive, options) { |
|
const newCb = fileName => subscriber.next(() => callback(fileName)) |
|
return origWatchDirectory.call(this, path, newCb, recursive, options) |
|
} |
|
|
|
// Emit start and end events for time measuring purpose |
|
const origCreateProgram = host.createProgram |
|
host.createProgram = function(...args) { |
|
subscriber.next(new CompilerStartEvent()) |
|
return origCreateProgram.call(this, ...args) |
|
} |
|
|
|
const origAfterProgramCreate = host.afterProgramCreate |
|
host.afterProgramCreate = function(...args) { |
|
subscriber.next(new CompilerEndEvent()) |
|
return origAfterProgramCreate?.call(this, ...args) |
|
} |
|
|
|
ts.createWatchProgram(host) |
|
}).pipe(share()) |
|
|
|
const buildStart$ = bundler$.pipe(filter<BundlerBuildStartEvent>(x => x instanceof BundlerBuildStartEvent)) |
|
const buildEnd$ = bundler$.pipe(filter<BundlerBuildEndEvent>(x => x instanceof BundlerBuildEndEvent)) |
|
|
|
const [compiler$, watcher$] = partition(tsc$, x => x instanceof CompilerEvent) as [ |
|
Observable<CompilerEvent>, |
|
Observable<WatcherCallback>, |
|
] |
|
|
|
merge( |
|
buildStart$.pipe(tap(() => console.clear())), |
|
buildEnd$.pipe(tap(() => console.log(`Server running at ${protocol}://localhost:${port}`))), |
|
watcher$.pipe( |
|
// Buffer all watcher callbacks until a build end event occurred, |
|
// indicating Parcel finished its job so we have free CPU cycles to do type checking |
|
buffer(buildEnd$), |
|
// Apply all buffered watcher callbacks to notify tsc |
|
tap((cbs: WatcherCallback[]) => cbs.forEach(cb => cb())), |
|
), |
|
compiler$.pipe( |
|
tap(x => x instanceof CompilerStartEvent && console.log("We're about to type checking...")), |
|
// Make sure the stream comes in the order like [start, end, start, ...] instead of [end, start, end, ...] |
|
skipWhile(x => x instanceof CompilerEndEvent), |
|
timestamp(), |
|
bufferCount(2), |
|
map(([start, end]) => (end.timestamp - start.timestamp) / 1000), |
|
tap(cost => console.log('Type checking in', cost, 'sec.')), |
|
), |
|
).subscribe() |