UNPKG

tsconfig.js

Version:

Polyfill for tsconfig.js, because TypeScript does not natively support JS config files

161 lines (126 loc) 4.21 kB
const EventEmitter = require('events') const { DepGraph } = require('dependency-graph') const fs = require('fs-extra') const extractDependencies = require('./src/extract-dependencies') const make = require('./src/make') const watch = require('./src/watcher') const withLogger = require('./src/with-logger') const { ERROR, READY, CREATE_TARGET, CREATE_DEPENDENCY, UPDATE_TARGET, UPDATE_DEPENDENCY, DELETE_TARGET, DELETE_DEPENDENCY, } = require('./src/events') module.exports = withLogger(tsconfigWatch) function tsconfigWatch (options) { const { log } = options const external = new EventEmitter() const handleError = (file) => error => { file ? log.error(`Error while processing ‘${file}’:`) : log.error(`Error from file watcher:`) log.errorError(error) external.emit(ERROR, error) } const watcher = watch(options, true) watcher.on(ERROR, handleError()) watcher.on(READY, () => external.emit(READY)) external.close = () => watcher.close() const dependenciesMap = new DepGraph() let _queue = Promise.resolve() const queue = (action) => { _queue = _queue.then(action) } watcher.on(CREATE_TARGET, file => queue(() => ( add(file, true) .then(() => build(file, options)) .catch(handleError(file)) ))) watcher.on(UPDATE_TARGET, file => queue(() => ( build(file, options) .catch(handleError(file)) ))) watcher.on(DELETE_TARGET, file => queue(() => ( build(file, options) .then(() => remove(file, true)) .catch(handleError(file)) ))) watcher.on(CREATE_DEPENDENCY, file => queue(() => ( add(file) .then(() => build(file, options)) .catch(handleError(file)) ))) watcher.on(UPDATE_DEPENDENCY, file => queue(() => ( build(file, options) .catch(handleError(file)) ))) watcher.on(DELETE_DEPENDENCY, file => queue(() => ( build(file, options) .then(() => remove(file)) .catch(handleError(file)) ))) return external // Event Handlers async function add(filepath, buildable) { const data = { buildable, } dependenciesMap.addNode(filepath, data) // in case the node has already been added as a dependency if (buildable) { // override data dependenciesMap.setNodeData(filepath, data) // don't watch redundantly watcher.clearDependency(filepath) } } async function remove(filepath, buildable) { if (buildable) { await fs.remove(`${filepath}on`) } const dependencies = dependenciesMap.dependenciesOf(filepath) dependenciesMap.removeNode(filepath) return Promise.all( dependencies .filter(dependency => dependenciesMap.dependantsOf(dependency).length === 0) .filter(dependency => !dependenciesMap.getNodeData(dependency).buildable) .map(dependency => remove(dependency)) ) } // Helpers async function build (filepath, options) { return Promise.all( [filepath] .concat(dependenciesMap.dependantsOf(filepath)) .map(fp => delete require.cache[fp] && fp) .filter(fp => dependenciesMap.getNodeData(fp).buildable) .filter(fp => fs.existsSync(fp)) .map(fp => make(fp, options).then(updateDependencies(fp))) ) } async function updateDependencies (filepath) { const directDependencies = extractDependencies(filepath) || [] const transitiveDependencies = dependenciesMap.dependenciesOf(filepath) const extraDependencies = transitiveDependencies.filter(d => !directDependencies.includes(d)) extraDependencies.forEach(dependency => { // does not touch deep dependencies, therefore clears obsolete direct dependencies dependenciesMap.removeDependency(filepath, dependency) if (dependenciesMap.dependantsOf(dependency).length === 0) { watcher.clearDependency(dependency) if (!dependenciesMap.getNodeData(dependency).buildable) { dependenciesMap.removeNode(dependency) } } }) const newDependecies = directDependencies.filter(d => !transitiveDependencies.includes(d)) return Promise.all(newDependecies.map(async dependency => { if (dependenciesMap.hasNode(dependency)) { dependenciesMap.addDependency(filepath, dependency) return } add(dependency) dependenciesMap.addDependency(filepath, dependency) watcher.addDependency(dependency) return updateDependencies(dependency) })) } }