wrapup
Version:
wraps up node packages for web development
239 lines (197 loc) • 7.72 kB
JavaScript
;
var prime = require('prime')
var Emitter = require('prime/emitter')
var fs = require("fs")
var esprima = require('esprima')
var async = require('async')
var util = require('./util')
var Module = require('./module')
var errors = require('./errors')
var NativeError = errors.NativeError
var NotInPathError = errors.NotInPathError
var ResolveError = errors.ResolveError
var JavaScriptError = errors.JavaScriptError
var Scanner = prime({
inherits: Emitter,
constructor: function(resolver, storage){
this.withResolver(resolver).withStorage(storage)
this.options = {}
this.index = 0
this.uids = {}
this.transforms = []
},
withResolver: function(resolver){
this.resolver = resolver
return this
},
withStorage: function(storage){
this.storage = storage
return this
},
addTransform: function(transform){
this.transforms.push(transform)
return this
},
set: function(option, value){
this.options[option] = value
return this
},
emitSync: function(name){
var args = Array.prototype.slice.call(arguments)
args.push(Emitter.EMIT_SYNC)
this.emit.apply(this, args)
},
scan: function(what, from, callback){
var resolver = this.resolver
var storage = this.storage
var modulefull = resolver.resolve(what, from)
var self = this
if (modulefull == null){
this.emitSync("warn", new ResolveError(what, from))
return callback()
}
if (modulefull === true){
this.emitSync("warn", new NativeError(what, from))
return callback()
}
var inPath = this.options.inPath
if (inPath && !util.inPath(inPath, modulefull)){
this.emitSync("warn", new NotInPathError(what, from, inPath))
return callback()
}
// check if we already have this module and it's still valid
var module = storage.get(modulefull)
if (module && !module.invalid) return callback(null, module)
if (module && module.invalid){
// make the module valid again, it will be reparsed now
module.invalid = false
module.err = false
} else {
// only create a new module object if it didn't exist yet
module = new Module(modulefull, this.uids[modulefull] || (this.index++).toString(36))
// ensure the file is always associated with the same uid
this.uids[modulefull] = module.uid
}
storage.put(module)
var transforms = []
// from filename to code
transforms.push(function(module, callback){
fs.readFile(module.full, 'utf-8', function(err, src){
if (err && err.code == 'ENOENT'){
// It is possible that .resolve still resolves the file
// correctly, but that the file actually doesn't exist anymore.
// Then still fire the "warn" event that the file cannot be
// resolved
self.emitSync("warn", new ResolveError(what, from))
}
module.src = src
callback(err, module)
})
})
function pushTransform(type){
return function(transform){
if (typeof transform != 'object') return
transforms.push(function(module, callback){
var trsfrm = transform[type]
if (!trsfrm) callback(null, module)
else trsfrm(module, callback)
})
}
}
// some code transformations
this.transforms.forEach(pushTransform('src'))
// stream transforms compatible with browserify
function pushStreamTransform(transform){
if (typeof transform != 'function') return
transforms.push(function(module, callback){
var stream = transform(module.full)
var src = ''
stream.on('data', function(buf){
src += buf
})
stream.on('end', function(){
module.src = src
callback(null, module)
})
stream.write(module.src)
stream.end()
})
}
this.transforms.forEach(pushStreamTransform)
// from code to AST
transforms.push(function(module, callback){
// parse the code with esprima to see if there are any errors and
// so we can walk through the AST to find require()s
try {
module.ast = esprima.parse(module.src, {
loc: true,
source: util.relative(module.full)
})
} catch (e){
// add to the object so it will be watched for newer versions
module.err = true
var err = new JavaScriptError(modulefull, from, e.lineNumber, e.column)
self.emitSync("warn", err)
callback(err)
return
}
callback(null, module)
})
// some AST transformations
this.transforms.forEach(pushTransform('ast'))
transforms.push(function(module, callback){
self.findRequires(module, function(err){
if (err) return callback(err)
module.ready = true
self.emitSync('scan', module)
callback(null, module)
})
})
async.reduce(transforms, module, function(memo, item, callback){
item(memo, callback)
}, function(err, module){
if (module && module.err) return
callback(err, module)
})
},
findRequires: function(module, callback){
var self = this
var ast = module.ast
var sourcemap = this.options.sourcemap && ast.loc && !ast.loc.source
var source = sourcemap && util.relative(module.full)
util.astWalker(ast, function(ast, parent, key, path){
// temporary fix until https://github.com/ariya/esprima/pull/148
// is pulled and released.
if (ast && sourcemap && key == 'loc'){
ast.source = source
return true
}
if (ast &&
(ast.type == "CallExpression" || ast.type == "NewExpression") &&
ast.callee.name == "require"){
module.requires.push({
path: path,
ast: ast,
argument: ast['arguments'].length == 1 && ast['arguments'][0].value,
i: module.requires.length})
return true
}
})
async.forEach(module.requires, function(require, callback){
var dep = require.argument
// note that deps also contains "null" values if the module could
// not be resolved. This way an output prime can determine to
// remove the require, or do something else with it.
if (!dep){
module.dependencies[require.i] = null
callback()
} else self.scan(dep, module.full, function(err, mod){
if (err) return callback(err)
module.dependencies[require.i] = mod ? mod : null
if (mod) mod.dependents.push(module)
callback()
})
}, callback)
}
})
module.exports = Scanner