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)
}
}