Home Reference Source

src/index.js

import {copyAsync, emptyDirAsync, mkdirsAsync, removeAsync} from 'fs-extra-promise'
import {createServer} from 'http-server'
import watch from 'node-watch'
import {basename, join, relative} from 'path'
import Dependencies from './private/Dependencies'
import {exists, fileKind, traverseTree} from './private/fs-util'
import Options from './private/Options'
import Paths from './private/Paths'
import Transformers from './private/Transformers'
import {promiseDone} from './private/util'
import Logger from './private/Logger'

export default class Dum {
	/**
	Creates a new Dum instance for the given package.
	This loads options from the `package.json` there.
	*/
	static async new(packageDir) {
		const
			opts = await Options.new(packageDir),
			transformers = new Transformers(opts),
			logger = new Logger(opts),
			dependencies = new Dependencies
		return new Dum({opts, transformers, logger, dependencies})
	}

	/** @private */
	constructor(params) {
		Object.assign(this, params)
	}

	/** Transforms `inDir` to `outDir` and copies `bower.inDir` to `bower.outDir`. */
	async build() {
		const {inDir, outDir, packageDir, bower} = this.opts

		this.logger.logBuild(inDir, outDir)
		await emptyDirAsync(outDir)
		const tree = this._writeSubtree('')

		if (bower) {
			const bowerDir = join(packageDir, bower.inDir)
			const fullOutPath = join(outDir, bower.outDir)
			this.logger.logBower(bower.inDir, bower.outDir)
			await Promise.all([tree, copyAsync(bowerDir, join(packageDir, fullOutPath))])
		} else
			await tree
	}

	/**
	Serve the contents of the `outDir`.
	Unlike `dum serve`, this does *not* build and watch.
	@return An [HttpServer](https://github.com/indexzero/http-server)
	*/
	async serve() {
		await this.watch()
		const {outDir, port} = this.opts
		this.logger.logServe(outDir, port)
		return createServer({root: outDir}).listen(port)
	}

	/**
	Continually build in response to changes to the `inDir`.

	There's currently no way to turn off the watching.
	If you know of a better watch module tell me!
	*/
	async watch() {
		await this.build()
		const {inDir, outDir} = this.opts
		this.logger.logWatch(inDir, outDir)
		watch(inDir, fullInPath => {
			const relInPath = relative(inDir, fullInPath)
			if (this._shouldBuildFile(relInPath))
				promiseDone(this._handleWatched(this._paths(relInPath)))
			else
				promiseDone(Promise.all(
					(for (path of this.dependencies.getDependers(relInPath))
						this._handleDepender(this._paths(path), relInPath))))
		})
	}

	_writeSubtree(relInDir) {
		const paths = _ =>
			this._paths(join(relInDir, _))
		return traverseTree(join(this.opts.inDir, relInDir), {
			filter: _ =>
				this._shouldBuildFile(_),
			traverseDir: _ =>
				// This makes sure empty dirs get copied over too.
				mkdirsAsync(paths(_).fullOutPath),
			traverseFile: _ =>
				this._writeSingle(paths(_))
		})
	}

	async _writeSingle(paths) {
		let dependencies
		try {
			dependencies = await this.transformers.transform(paths)
		} catch (error) {
			this.logger.logError(error)
		}
		if (dependencies !== undefined && dependencies.length > 0) {
			const relDependencies = (for (_ of dependencies) relative(this.opts.inDir, _))
			this.dependencies.addDepender(paths.relInPath, relDependencies)
		}
	}

	async _handleWatched(paths) {
		const kind = await fileKind(paths.fullInPath)
		switch (kind) {
			case 'none':
				this.logger.logDelete(paths)
				this.dependencies.deleteDepender(paths.relInPath)
				removeAsync(paths.fullOutPath)
				break
			case 'directory':
				this.logger.logWrite(paths)
				await this._writeSubtree(paths.relInPath)
				break
			case 'file':
				this.logger.logWrite(paths)
				await this._writeSingle(paths)
				break
			default: throw new Error(kind)
		}
	}

	async _handleDepender(paths, dependencyPath) {
		// Need an existence check because it could have been deleted in the meantime.
		if (await exists(paths.fullInPath)) {
			this.logger.logDependency(paths, dependencyPath)
			await this._writeSingle(paths)
		}
	}

	_shouldBuildFile(inFilePath) {
		return !basename(inFilePath).startsWith('_')
	}

	_paths(relInPath) {
		return new Paths(this.transformers, this.opts.inDir, this.opts.outDir, relInPath)
	}
}