module-walker
Version:
JavaScript module traverser
420 lines (344 loc) • 10 kB
JavaScript
'use strict'
const { parseDependenciesFromAST } = require('./dependency')
const traceCircular = require('./circular')
const {
matchExt,
astFromSource,
resolve
} = require('./utils')
const node_path = require('path')
const fs = require('fs')
const util = require('util')
const { EventEmitter } = require('events')
const mix = require('mix2')
const make_array = require('make-array')
const async = require('async')
const set = require('set-options')
// [ref](http://nodejs.org/api/modules.html#modules_file_modules)
const EXTS_NODE = ['.js', '.json', '.node']
const DEFAULT_WALKER_OPTIONS = {
concurrency: 50,
extensions: EXTS_NODE,
allowCyclic: true,
allowAbsoluteDependency: true,
resolve: resolve
}
// @param {Object} options
// - concurrency
module.exports = class Walker extends EventEmitter {
// @param {Object} options see walker-promise.js
constructor (entries, options, callback) {
super()
this.options = set(options, DEFAULT_WALKER_OPTIONS)
this.options.parse = this.options.parse || astFromSource
this.nodes = {}
this.callback = callback
this._init_queue()
this._walk(entries)
}
_init_queue () {
this.queue = async.queue((task, done) => {
// `filename` will always be an absolute path.
let filename = task.node.id
this._parse_file(filename, task.type, err => {
if (!err) {
return done()
}
this.queue.kill()
this.error = err
this._done()
})
}, this.options.concurrency)
this.queue.drain = () => {
this._done()
}
}
_walk (entries) {
make_array(entries).forEach(entry => {
entry = node_path.resolve(entry)
let node = this._get_node(entry)
if (node) {
return
}
this._walk_one(this._create_node(entry))
})
}
_walk_one (node, type) {
type = type || 'require'
// All ready parsed
if (~node.type.indexOf(type)) {
return
}
node.type.push(type)
if (node.foreign) {
return
}
this.queue.push({
node: node,
type: type
})
}
_done () {
this.callback(this.error || null, this.nodes)
}
_parse_file (path, type, callback) {
let node = this._get_node(path)
this._get_compiled_content(node, err => {
if (err) {
return callback(err)
}
if (!node.js || type !== 'require') {
return callback(null)
}
parseDependenciesFromAST(node.ast, node.code, this.options).then(
(data) => {
async.each(['require', 'resolve', 'async'], (type, done) => {
this._parse_dependencies_by_type(path, data[type], type, done)
}, callback)
},
(err) => {
err.message = `${path}: ${err.message}`
callback(err)
}
)
})
}
_get_compiled_content (node, callback) {
if (node.content) {
return callback(null)
}
let filename = node.id
this._read(filename, (err, content) => {
if (err) {
return callback(err)
}
this._compile(filename, content, (err, compiled) => {
if (err) {
return callback(err)
}
mix(node, compiled)
callback(null)
})
})
}
_read (path, callback) {
fs.readFile(path, (err, content) => {
if (err) {
return callback({
code: 'ERROR_READ_FILE',
message: 'Error reading module "' + path + '": ' + err.stack,
data: {
path: path,
error: err
}
})
}
callback(null, content.toString())
})
}
// Applies all compilers to process the file content
_compile (filename, content, callback) {
const matchCompiler = (compiler, compiled) => compiler.test(compiled)
const compilers = this.options.compilers
const length = compilers.length
let i = 0
const done = (err, compiled) => {
if (err) {
return callback(err)
}
// if no ast, try to generate ast
if (!compiled.ast && compiled.js) {
try {
compiled.ast = this.options.parse(compiled.code, filename)
} catch (e) {
return callback(e)
}
}
// make sure `filename`
compiled.filename = filename
let compiler
while (i < length) {
compiler = compilers[i ++]
if (matchCompiler(compiler, compiled)) {
return task(done, compiler, compiled)
}
}
callback(null, compiled)
}
const task = (done, compiler, compiled) => {
const compilerOptions = set({}, compiler.options)
if (compiled.ast) {
compilerOptions.ast = compiled.ast
}
if (compiled.map) {
compilerOptions.map = compiled.map
}
// adds `filename` to options of each compiler
compilerOptions.filename = filename
compiler.compiler(compiled.code, compilerOptions, done)
}
const node = matchExt(filename, 'node')
const json = matchExt(filename, 'json')
const js = matchExt(filename, 'js')
done(null, {
code: content,
json,
node,
js
})
}
_parse_dependencies_by_type (path, paths, type, callback) {
let node = this._get_node(path)
async.each(paths, (dep, done) => {
var origin = dep
if (dep.indexOf('/') === 0) {
var message = {
code: 'NOT_ALLOW_ABSOLUTE_PATH',
message: 'Requiring an absolute path "' + dep + '" is not allowed in "' + path + '"',
data: {
dependency: dep,
path: path
}
}
if (!this.options.allowAbsoluteDependency) {
return done(message)
} else {
this.emit('warn', message)
}
}
// if (!this._is_relative_path(dep)) {
// // we only map top level id for now
// dep = this._solve_aliased_dependency(options['as'][dep], path) || dep
// }
// package name, not a path
if (!this._is_relative_path(dep)) {
return this._deal_dependency(origin, dep, node, type, done)
}
let resolveOptions = {
basedir: node_path.dirname(path),
extensions: this.options.extensions
}
this.options.resolve(dep, resolveOptions, (err, real) => {
if (err) {
return done({
code: 'MODULE_NOT_FOUND',
message: err.message,
stack: err.stack,
data: {
path: dep
}
})
}
this._deal_dependency(origin, real, node, type, done)
})
}, callback)
}
// // #17
// // If we define an `as` field in cortex.json
// // {
// // "as": {
// // "abc": './abc.js' // ./abc.js is relative to the root directory
// // }
// // }
// // @param {String} dep path of dependency
// // @param {String} env_path the path of the current file
// _solve_aliased_dependency (dep, env_path) {
// var cwd = this.options.cwd
// if (!dep || !cwd || !this._is_relative_path(dep)) {
// return dep
// }
// dep = node_path.join(cwd, dep)
// dep = node_path.relative(node_path.dirname(env_path), dep)
// // After join and relative, dep will contains `node_path.sep` which varies from operating system,
// // so normalize it
// .replace(/\\/g, '/')
// if (!~dep.indexOf('..')) {
// // 'abc.js' -> './abc.js'
// dep = './' + dep
// }
// return dep
// }
_deal_dependency (dep, real, node, type, callback) {
node[type][dep] = real
let sub_node = this._get_node(real)
let new_node = sub_node || this._create_node(real)
this._walk_one(new_node, type)
// We only check the node if it meets the conditions below:
// 1. already exists: all new nodes are innocent.
// 2. but assigned as a dependency of anothor node
// If one of the ancestor dependents of `node` is `current`, it forms a circle.
// If newly created node, then skip checking.
if (!sub_node) {
return callback(null)
}
sub_node = new_node
var circular_trace
// node -> sub_node
if (circular_trace = traceCircular(sub_node, node, this.nodes)) {
var message = {
code: 'CYCLIC_DEPENDENCY',
message: 'Cyclic dependency found: \n' + this._print_cyclic(circular_trace),
data: {
trace: circular_trace,
path: real
}
}
if (!this.options.allowCyclic) {
return callback(message)
} else {
this.emit('warn', message)
}
}
callback(null)
}
_get_node (path) {
return this.nodes[path]
}
// Creates the node by id if not exists.
// No fault tolerance for the sake of private method
// @param {string} id
// - `path` must be absolute path if is a relative module
// - package name for foreign module
_create_node (id) {
return this.nodes[id] = {
id: id,
foreign: this._is_foreign(id),
type: [],
require: {},
resolve: {},
async: {}
}
}
_is_foreign (path) {
return !this._is_absolute_path(path)
}
_is_absolute_path (path) {
return node_path.resolve(path) === path.replace(/[\/\\]+$/, '')
}
_is_relative_path (path) {
// Actually, this method is called after the parser.js,
// and all paths are parsed from require(foo),
// so `foo` will never be affected by windows,
// so we should not use `'.' + node_path.sep` to test these paths
return path === '.'
|| path === '..'
|| path.indexOf('./') === 0
|| path.indexOf('../') === 0
}
// 1. <path>
// 2. <path>
//
_print_cyclic (trace) {
var list = trace.map(function (node, index) {
return index + 1 + ': ' + node.id
})
list.pop()
var flow = trace.map(function (node, index) {
++ index
return index === 1 || index === trace.length
? '[1]'
: index
})
return list.join('\n') + '\n\n' + flow.join(' -> ')
}
}