UNPKG

duo

Version:

A next-generation package manager for the front-end

1,399 lines (1,185 loc) 31.6 kB
/** * Module dependencies. */ var Emitter = require('events').EventEmitter; var clone = require('component-clone'); var stoj = require('duo-string-to-js'); var compat = require('duo-css-compat'); var parallel = require('co-parallel'); var Package = require('duo-package'); var debug = require('debug')('duo'); var filedeps = require('file-deps'); var exists = require('co-exists'); var extend = require('extend.js'); var parse = require('duo-parse'); var unyield = require('unyield'); var thunk = require('thunkify'); var cache = require('./cache'); var Pack = require('duo-pack'); var main = require('duo-main'); var mkdir = require('mkdirp'); var hasha = require('hasha'); var File = require('./file'); var Ware = require('ware'); var path = require('path'); var util = require('util'); var fs = require('co-fs'); var cp = require('cp'); var basename = path.basename; var relative = path.relative; var extname = path.extname; var dirname = path.dirname; var resolve = path.resolve; var join = path.join; var fmt = util.format; var slice = [].slice; /** * Thunks. */ mkdir = thunk(mkdir); cp = thunk(cp); /** * Export `Duo`. */ module.exports = Duo; /** * Initialize a new `Duo` package with the path to the package's `root` folder. * * @param {String} root * @return {Duo} * @api public */ function Duo(root) { if (!(this instanceof Duo)) return new Duo(root); if (!root) throw error('duo requires a root directory'); Emitter.call(this); this.root(root); this.manifest('component.json'); this.installTo('components'); this.buildTo('build'); this.copy(false); this.development(false); this.sourceMap(false); this.concurrency(50); this.update(false); this.cache(true); this.assets = []; this.mapping = {}; this.includes = {}; this.packages = {}; this.json = readJson(this.path('component.json')); // plugins this.plugins = new Ware(); this.plugins.run = thunk(this.plugins.run); // alternate plugins (used for the full build, not individual files) this.altPlugins = new Ware(); this.altPlugins.run = thunk(this.altPlugins.run); } /** * Inherit from `Emitter`. */ util.inherits(Duo, Emitter); /** * Get or set the entry file for the package. * * You can either specify a single `path` to an entry file: * * duo.entry('index.js'); * * Or you can pass a source string and a file type: * * duo.entry('var a = 0;', 'js'); * * @param {String} path or source (optional) * @param {String} type (optional) * @return {Duo|File} * @api public */ Duo.prototype.entry = function () { switch (arguments.length) { case 0: return this._entry; case 1: this._entry = this.file({ path: arguments[0], entry: true }); return this; case 2: this._entry = this.file({ raw: arguments[0], type: arguments[1], entry: true }); return this; } }; /** * Get or set the root `dir` for the package. * * @param {String} dir (optional) * @return {Duo|String} * @api public */ Duo.prototype.root = function (dir) { if (!arguments.length) return this._root; this._root = dir; return this; }; /** * Get or set the `token` to authenticate to GitHub with. * * @param {String} token (optional) * @return {Duo|String} * @api public */ Duo.prototype.token = function (token) { if (!arguments.length) return this._token; this._token = token; return this; }; /** * Get or set the manifest `name` to read settings from. * * @param {String} name * @return {Duo|String} * @api public */ Duo.prototype.manifest = function (name) { if (!arguments.length) return this._manifest; this._manifest = name; return this; }; /** * Get or set a global variable `name` to expose the entry file by. * * @param {String} name (optional) * @return {String|Duo} * @api public */ Duo.prototype.global = function (name) { if (!arguments.length) return this._global; this._global = name; return this; }; /** * Get or set standalone `name`, this will be used * to create a standalone JS (umd). * * @param {String} name * @return {String|Duo} * @api public */ Duo.prototype.standalone = function (name) { if (!arguments.length) return this.umd; this.umd = name; return this; }; /** * Get or set whether to install development dependencies. * * @param {Boolean} value (optional) * @return {Duo|Boolean} * @api public */ Duo.prototype.development = function (value) { if (!arguments.length) return this._development; this._development = value; return this; }; /** * Get or set whether to include source-maps. * * @param {Boolean|String} true, false or 'inline' * @return {Duo|Boolean|String} * @api public */ Duo.prototype.sourceMap = function (value) { if (!arguments.length) return this._sourceMap; this._sourceMap = value; return this; }; /** * Get or set whether to copy built files instead of symlinking them. * * @param {Boolean} value (optional) * @return {Duo|Boolean} * @api public */ Duo.prototype.copy = function (value) { if (!arguments.length) return this._copy; this._copy = value; return this; }; /** * Get or set the download concurrency `value`. * * @param {Number} value (optional) * @return {Duo|Number} * @api public */ Duo.prototype.concurrency = function (value) { if (!arguments.length) return this._concurrency; this._concurrency = value; return this; }; /** * Get or set the flag determining if we should always resolve dependencies. * (rather than relying on cache) * * @param {Boolean} value (optional) * @return {Duo|Boolean} * @api public */ Duo.prototype.update = function (value) { if (!arguments.length) return this._update; this._update = value; return this; }; /** * Get or set whether the cache should be used. * * @param {Boolean} value (optional) * @return {Duo|Boolean} * @api public */ Duo.prototype.cache = function (value) { if (!arguments.length) return this._cache; this._cache = value; return this; }; /** * Gets the cache object if this build is configured to use * caching. * * @return {Cache|Boolean} */ Duo.prototype.getCache = unyield(function* () { if (!this._cache) return false; if (!this._cacheInstance) { yield mkdir(this.installPath()); var inst = cache(this.installPath('duo-cache')); yield inst.initialize(); this._cacheInstance = inst; } return this._cacheInstance; }); /** * Get or set the `path` to install dependencies to. * * @param {String} path (optional) * @return {Duo|String} * @api public */ Duo.prototype.installTo = function (path) { if (!arguments.length) return this._installTo; this._installTo = path; return this; }; /** * Get or set the `path` to build source to. * * @param {String} path (optional) * @return {Duo|String} * @api public */ Duo.prototype.buildTo = function (path) { if (!arguments.length) return this._buildTo; this._buildTo = path; return this; }; /** * Use an async, sync or generator `plugin` function. * * @param {Function} plugin * @return {Duo} * @api public */ Duo.prototype.use = function (fn) { if (Array.isArray(fn)) { fn.forEach(this.use, this); } else { if (fn.alternate) { this.altPlugins.use(fn); } else { this.plugins.use(fn); } log.call(this, fn); } function log(fn) { var name = fn.name || '(anonymous)'; this.emit('plugin', name); debug('using plugin: %s', name); } return this; }; /** * Resolve `paths...` relative to the root directories. * * @param {String} paths... * @return {String} * @api public */ Duo.prototype.path = function () { var strs = slice.call(arguments); strs.unshift(this.root()); return path.join.apply(path, strs); }; /** * Resolve `paths...` relative to the build directory. * * @param {String} paths... * @return {String} * @api public */ Duo.prototype.buildPath = function () { var strs = slice.call(arguments); strs.unshift(this.buildTo()); return this.path.apply(this, strs); }; /** * Resolve `paths...` relative to the install directory. * * @param {String} paths... * @return {String} * @api public */ Duo.prototype.installPath = function () { var strs = slice.call(arguments); strs.unshift(this.installTo()); return this.path.apply(this, strs); }; /** * Destroy the mapping. (the cache used here) */ Duo.prototype.cleanCache = unyield(function* () { debug('clearing cache'); var cache = yield this.getCache(); if (cache) yield cache.clean(); }); /** * Include a runtime by `name` and `src`. * * @param {String} name * @param {String} src * @return {Duo} * @api public */ Duo.prototype.include = function (name, src, type) { this.includes[name] = { id: name, src: src, entry: false, include: true, deps: includeDeps(src, type) }; return this; }; /** * Check the includes hash for the given name * * @param {String} name * @return {Boolean} * @api public */ Duo.prototype.included = function (name) { return name in this.includes; }; /** * Write the built source to a `path`, which defaults * to the same name as the entry file's name. * * @param {String} path (optional) * @param {Function} fn (optional) * @return {Duo} * @api public */ Duo.prototype.write = unyield(function* (path) { var results = yield this.run(); var entry = this.entry(); var dir = this.buildPath(); var type = entry.type; var rel = entry.id; if (!supported(entry.type)) return this; // resolve the path path = resolve(dir, path || rel); // change the extension if the type has changed var base = basename(path, extname(path)) + '.' + type; dir = dirname(path); // update the path path = join(dir, base); // write the file yield mkdir(dir); if (results.code) { yield fs.writeFile(path, results.code); this.emit('write', base); } if (results.map) { yield fs.writeFile(path + '.map', results.map); this.emit('write', base + '.map'); } return this; }); /** * Processes the entry file and generates it's mapping, * by separating out this step, we can effectively implement * a `duo install` that will fetch dependencies. * * @returns {Object} deps mapping * @api public */ Duo.prototype.install = unyield(function* () { var entry = this.entry(); if (!entry) return {}; var rel = entry.id; var global = this.global(); // logging this.emit('running', rel); debug('running: %s', rel); // idempotent across runs this.assets = []; // add core plugins this .use(compat()) .use(stoj()); var cache = yield this.getCache(); if (cache) this.mapping = yield cache.read(); this.updateDeps = (yield this.isManifestModified()) || this.update(); // ensure that the entry exists if (!(yield entry.exists())) { throw error('cannot find entry: %s', rel); } // fetch the map of the dependency graph var deps = yield this.dependencies(entry); // add "includes" to dependency mapping deps = extend(deps, this.includes); // add global if (global) deps[rel].global = global; // write out the asset files in parallel yield this.parallel(this.assets); // write out mapping if (cache) yield cache.update(deps); // standalone? if (this.standalone()) { deps[rel].name = this.standalone(); } return deps; }); /** * Run duo on the entry file and return the built source. * * - Loads core duo plugins. * - Map out the dependency grpah. * - Add in the manually included files. * - Write the assets to the `buildTo()` path. * - Write out the map of the dependency graph to `components/duo.json`. * - Build the source by passing the `mapping` into duo-pack. * * @param {Function} fn (optional) * @return {String} * @api public */ Duo.prototype.run = unyield(function* () { var entry = this.entry(); if (!entry) return { code: '' }; // TODO: throw error instead? var rel = entry.id; var deps = yield this.install(); // pack the mapping var pack = new Pack(deps, { umd: this.standalone() }); var map = this.sourceMap(); if (map) pack.sourceMap(map); var results = pack.pack(rel); // apply full build plugins yield this.altPlugins.run(results, this.entry(), this); // logging this.emit('run', rel); debug('ran: %s', rel); return results; }); /** * Recursively transform, parse and fetch the dependencies of `file`. * Returns a flat `map` of the full dependency graph. * * - `this.mapping` is a cache to only build files that have been changed. * - `map` is a current snapshot of the dependency graph. * - `map` gets built as we recurse. * * @param {File} file * @param {Object} map * @return {Object} * @api private */ Duo.prototype.dependencies = function* (file, map) { yield file.modified(); map = map || {}; var json = clone(this.mapping[file.id] || {}); var includes = this.includes; var cache = this.cache(); var paths = []; var gens = []; // logging debug('parsing: %s', file.id); var isCached = file.mtime === json.mtime; var isIncluded = json.include; var isParsed = map[file.id]; // `file` already visited if (isParsed) { debug('%s, already parsed. ignoring', file.id); return map; } // `file` included manually already if (isIncluded) { debug('%s, included manually. ignoring', file.id); map[file.id] = json; return map; } // check if the `file` has been modified // if not, skip parsing and recurse its // dependencies immediately. if (cache && !this.updateDeps && isCached) { debug('%s: has not been modified. skip parsing', file.id); map[file.id] = json; paths = values(json.deps); gens = []; // update the file with the cached // dependency mapping file.set(json); // bundle if there is a non-supported file type if (!supported(file.type)) this.assets.push(this.bundle(file.id)); // recurse the dependency's dependencies gens = this.recurse(paths, map); yield this.parallel(gens); return map; } // load the file, applying plugin transforms yield file.load(); // not a supported file type, don't parse any farther // but include in duo.json, for symlinking or copying if (!supported(file.type)) { this.assets.push(this.bundle(file.id)); delete file.attrs.src; map[file.id] = file.json(); return map; } // parse content for dependencies var deps = file.dependencies(); var depmap = {}; // logging debug('%s deps: %j', file.id, deps); // download and resolve the dependencies deps.forEach(function (dep) { depmap[dep] = includes[dep] ? dep : this.dependency(dep, file, this.entry()); }, this); // resolve dependencies from entry files, // and remove unresolved deps depmap = compact(yield depmap); paths = values(depmap); // update the file with the resolved dependencies file.set({ deps: depmap }); map[file.id] = file.json(); // recurse the dependency's dependencies gens = this.recurse(paths, map); yield this.parallel(gens); return map; }; /** * Create generators to recurse the next layer * of dependencies in the dependency graph * * @param {Array} deps * @param {Object} map * @return {Array} * @api private */ Duo.prototype.recurse = function (deps, map) { var includes = this.includes; var gens = []; deps.forEach(function (dep) { if (includes[dep]) return; var dest = this.path(dep); var root = this.findRoot(dest); var local = dep.split(path.sep)[0] !== this.installTo(); var file = this.file({ root: root, path: dest, local: local }); gens.push(this.dependencies(file, map)); }, this); return gens; }; /** * Resolve a single dependency's path. Supports local and remote dependencies. * Fetches remote packages to components/ to resolve remote paths. * * Examples: * * 1. this.dependency('./one.js', file, entry) * => lib/one/one.js * * 2. this.dependency('component/tip', file, entry) * => components/component-tip@master/index.js * * 3. this.dependency('yields/skeleton', file, entry) * => components/yields-skeleton@master/skeleton.css * * Where: * * `file` is the current file with the dependency `dep`. * `entry` is the root file you passed into Duo. * * @param {String} dep * @param {File} file * @param {File} entry * @return {String|Boolean} * @api private */ Duo.prototype.dependency = function* (dep, file, entry) { // ignore http dependencies if (http(dep)) { debug('%s: ignoring dependency "%s"', file.id, dep); return false; } // `dep` is a local dependency var local = yield this.resolve(dep, file, this.entry()); if (local) { debug('%s: local dependency "%s"', file.id, dep); return relative(entry.root, local); } else if (isLocal(dep)) { debug('%s: local dependency "%s" not found', file.id, dep); throw error('unable to resolve local dependency ' + dep + ' from ' + file.id); } // `dep` is a remote dependency var pkg = this.package(dep, file); if (!pkg) { debug('%s: cannot resolve "%s"', file.id, dep); return false; } // add cache layer var cache = yield this.getCache(); if (cache) pkg.cache(cache); // install the dependency from github // or another remote to 'components/' yield pkg.fetch(); // resolve the remote dependency's path var path = pkg.path(parse(dep).path || 'component.json'); var pkgfile = this.file({ root: pkg.path(), path: path }); var resolved = yield this.resolve(path, pkgfile, this.entry()); // logging if (!resolved) debug('%s: cannot resolve "%s"', file.id, dep); // return resolved or false return resolved ? relative(entry.root, resolved) : false; }; /** * Resolve the full local path of `dep`. * * TODO: clean & move this function into `duo-resolve`. * * Examples: * * 1. Local dependency: * * this.resolve('./one.js', file, entry) * => lib/one/one.js * * 2. Remote dependency (after it's installed): * * this.resolve('$HOME/components/yields-skeleton@0.0.1/component.json', file, entry) * => $HOME/components/yields-skeleton@0.0.1/skeleton.css * * Where: * * `file` is the current file with the dependency `dep`. * `entry` is the root file you passed into Duo. * * @param {String} dep * @param {String} root * @return {String} path * @api private */ Duo.prototype.resolve = function* (dep, file, entry) { var isManifest = this.manifest() === basename(dep); var type = entry.type; var ext = extension(dep) === type ? '' : '.' + type; var path = resolve(dirname(file.path), dep); var isRelative = dep.slice(0, 2) === './'; var isParent = dep.slice(0, 2) === '..'; var isAbsolute = dep[0] === '/'; var ret; if (isManifest) { // `dep` is a component.json var json = readJson(resolve(file.root, dep)); var entrypoint = main(json, entry.type) || 'index.' + entry.type; ret = resolve(file.root, dirname(dep), entrypoint); } else if (isAbsolute) { // `dep` is an absolute path (relative to app or component root) var relroot = this.findRoot(file.path); ret = join(relroot, dep.replace(relroot, '')); } else if (isParent) { // `dep` is a parent path with ".." ret = path; } else if (isRelative) { // `dep` is a relative path with "./" ret = path; } else if (yield isRelativeCSS(entry, path)) { // Hack to support for CSS relative paths without "./" // Example: body { background-image: url('image.jpg'); } ret = path; } // Could not resolve if (!ret) return; // Strip any hashes, querystrings, etc from path. // fonts/glyphicons-halflings-regular.eot?#iefix ret = stripPath(ret); // does one of these file paths exist? // // ./file.{{ext}} // ./file/index.{{ext}} return yield exists([ ret + ext, join(ret, 'index' + ext), ret ]); // check filesystem for relative asset function* isRelativeCSS(entry, path) { if (entry.type !== 'css') return false; path = stripPath(path); if (!extension(path)) return false; return yield exists(path); } }; /** * Helper to create a file with `attrs`. * * @param {Object} attrs * @return {File} * @api private */ Duo.prototype.file = function (attrs) { attrs.root = attrs.root || this.root(); return new File(attrs, this); }; /** * Helper to create a package from a `dep` with `file`. * * - Finds the necessary information to fetch `dep` * - Adds in our github token * - Sets up the events * * Examples: * * 1. "component/emitter": "0.1.0" in our manifest * * var pkg = this.package('emitter', file); * pkg.slug(); => component/emitter@0.1.0 * * 2. require('component/emitter@master:index.js') * * var pkg = this.package('component/emitter@master:index.js'); * pkg.slug(); => component/emitter@master * * * @param {String} dep * @param {File} file * @return {Package|null} * @api private */ Duo.prototype.package = function (dep, file) { var manifest = readJson(join(file.root, this.manifest())); var gh = this.findDependency(dep, manifest); if (!gh) return null; var token = this.token(); // check if this package is already in flight var slug = [ gh.package, gh.ref ].join('@'); if (slug in this.packages) return this.packages[slug]; // initialize the package var pkg = this.packages[slug] = new Package(gh.package, gh.ref); pkg.directory(this.installPath()); if (token) pkg.token(token); // forward package events to duo pkg .once('resolving', this.forward('resolving')) .once('installing', this.forward('installing')) .once('resolve', this.forward('resolve')) .once('install', this.forward('install')); return pkg; }; /** * Piece together the remote repository information from the dependency `dep`. * * - Consults the manifest for additional information, when needed. * - Includes development dependencies in when `development()` is set * * Examples: * * 1. specific dependency. no `manifest`: * * this.findDependency('component/emitter', {}) * => { * package: 'component/emitter', * user: 'component', * repo: 'emitter', * ref: '*' * } * * 2. vague dependency. "component/emitter": "0.1.0" is in the `manifest`: * * this.findDependency('emitter', manifest) * => { * package: 'component/emitter', * user: 'component', * repo: 'emitter', * ref: '0.1.0' * } * * 3. specific dependency. "component/emitter": "0.1.0" is in the `manifest`: * * this.findDependency('component/emitter@1.0.0', manifest) * => { * package: 'component/emitter', * user: 'component', * repo: 'emitter' * ref: '0.1.0' * } * * @param {String} dep * @param {Object} manifest * @return {Object} * @api private */ Duo.prototype.findDependency = function (dep, manifest) { var gh = parse(dep); // We have all the information we need from `dep`. // Set the `package` key and return. if (gh.user && gh.repo && gh.ref) { gh.package = gh.user + '/' + gh.repo; return gh; } var dev = this.development(); var deps = manifest.dependencies || {}; var rext = /([\.][a-z]+)?/; // Include development dependencies if `development(true)` is set. if (dev && manifest === this.json) { deps = extend(deps, manifest.development || {}); } // Build a regexp based on if we have the `user` key or not. // If we have the `user` key, our search is more specific. var re = gh.user ? new RegExp('^' + gh.user + '[\\/:]' + gh.repo + rext.source + '$', 'i') : new RegExp('([\\/:])' + gh.repo + rext.source + '$', 'i'); // Not enough information yet. // Search through the manifest for our dependency. for (dep in deps) { if (re.test(dep) || dep.replace('/', '-') === gh.repo) { gh.package = dep; gh.ref = deps[dep]; return gh; } } // We have the `user` and `repo` keys, but not the `ref`. // Although vague, we have enough information to find our dependency. // Set `ref` to `*` and add in our `package` key. if (gh.user && gh.repo) { gh.package = gh.user + '/' + gh.repo; gh.ref = '*'; return gh; } // Not enough information to find the dependency. return null; }; /** * Given a `path`, find the root. * TODO: fix this for windows. * * - `root` is relative. It could be the project `root` or a component's `root` * * Examples: * * 1. Project root (app): * * this.findRoot('$HOME/app/lib/one/one.js') * => $HOME/app * * 2. Component root * * this.findRoot('$HOME/app/components/component-reactive@1.2.0/lib/index.js') * => $HOME/app/components/component-reactive@1.2.0 * * @param {String} path * @return {String} root * @api private */ Duo.prototype.findRoot = function (path) { var root = this.root(); while (path !== '.' && path !== root && !isSlug(basename(path))) { path = dirname(path); } if (path === '.') { throw error('could not find root for %s', path); } return path; }; /** * Copy over assets from a `path` relative to the assets path. * This will symlink unless `duo.copy(true)` has been set. * * @param {String} path * @return {Duo} */ Duo.prototype.bundle = function* (path) { var log = this.copy() ? 'copying' : 'symlinking'; var action = this.copy() ? cp : fs.symlink; var dest = this.buildPath(path); var rm = fs.unlink; // mkdir -p yield mkdir(dirname(dest)); // fullpath path = this.path(path); // try removing any existing symlink or file first try { yield rm(dest); } catch(e) { if (e.code !== 'ENOENT') throw e; } // symlink or copy the file debug('%s: %s => %s', log, path, dest); try { yield action(path, dest); } catch(e) { if (e.code !== 'EEXIST') throw e; else debug('caught: %s already exists', dest); } return this; }; /** * Forward events with a `name`, adding the Duo instance as the final argument. * * @param {String} event * @return {Function} * @api private */ Duo.prototype.forward = function (name) { var duo = this; return function () { var args = slice.call(arguments); var ctx = this; args = [name].concat(args).concat(ctx); duo.emit.apply(duo, args); return ctx; }; }; /** * Parallelize an array of generators with `concurrency`. * * @param {Array} arr * @return {Array} * @api private */ Duo.prototype.parallel = function (arr) { return parallel(arr, this.concurrency()); }; /** * Helper for hashing arbitrary input. (useful during caching, * particularly for plugins) * * Objects will be serialized into a JSON string. Then the `input` * string will be hashed using MD5. * * @param {Mixed} input * @return {String} hash */ Duo.prototype.hash = function (input) { if (typeof input !== 'string' && !Buffer.isBuffer(input)) { input = JSON.stringify(input); } return hasha(input, { algorithm: 'md5' }); }; /** * Check to see if the root manifest file has been modified. * * @return {Boolean} */ Duo.prototype.isManifestModified = function* () { // the path to check var manifest = this.path(this.manifest()); // check if we even have this manifest, bail if we don't var exists = yield fs.exists(manifest); if (!exists) return false; // try to get cache, bail if not in use var cache = yield this.getCache(); if (!cache) return false; // stat the current manifest var current = yield fs.stat(manifest); var value = current.mtime.getTime(); debug('current mtime for manifest %d', value); // retrieve what we've stored in the cache previously // update the cache with our new value regardless var key = [ 'manifest', 'modified' ]; var previous = yield cache.get(key); if (previous) debug('previous mtime for manifest: %d', previous); yield cache.put(key, value); // if no previous value, assume yes var ret = !previous || previous !== value; debug('manifest modified? %j', ret); if (ret) { // reset the manifest data internally if we determine it has been modified delete require.cache[manifest]; // FIXME! this is an internal API this.json = readJson(manifest); } return ret; }; /** * Get the extension of a `path`, without the leading period. * * @param {String} path * @return {String} ext * @api private */ function extension(path) { return extname(path).slice(1); } /** * Convenience to turn a `msg` with placeholders into a proper `Error`. * * @param {String} msg * @return {Error} * @api private */ function error(msg) { var args = slice.call(arguments, 1); msg = fmt.apply(fmt, [msg].concat(args)); return new Error(msg); } /** * Is the extension supported by Duo? * * @param {String} ext * @return {Boolean} * @api private */ function supported(ext) { return /^(js|css)$/.test(ext); } /** * Strip a querystring or hash fragment from a `path`. * * @param {String} path * @return {String} * @api private */ function stripPath(path) { return path .split('?')[0] .split('#')[0]; } /** * Check if `url` is an HTTP URL. * * @param {String} path * @param {Boolean} * @api private */ function http(url) { return url.slice(0, 4) === 'http' || url.slice(0, 3) === '://' || false; } /** * Get the values of an `obj`. * * @param {Object} obj * @return {Array} * @api private */ function values(obj) { if (!obj) return []; return Object.keys(obj).map(function (k) { return obj[k]; }); } /** * Remove empty values from an `obj`. * * @param {Object} obj * @return {Object} * @api private */ function compact(obj) { var out = {}; for (var k in obj) { if (obj[k]) out[k] = obj[k]; } return out; } /** * Safely read a JSON file by `path`, with caching. * * @param {String} path * @return {Object} * @api private */ function readJson(path) { try { return require(path); } catch(e) { if (e instanceof SyntaxError) throw jsonSyntaxError(e, path); return {}; } } /** * Processes a SyntaxError trapped by readJson and creates a more user-friendly * error message. (and adds the `fileName` property) * * @param {SyntaxError} err * @returns {SyntaxError} */ function jsonSyntaxError(err, path) { var msg = err.message.split(': ')[1]; err.message = msg; err.fileName = path; return err; } /** * Reads an include file and parses for dependencies * * TODO: Currently, an included file can **only** include other included files. * This is a lame limitation, so it should be addressed in the future. * * @param {String} src * @param {String} type * @returns {Object} */ function includeDeps(src, type) { if (!type) return {}; return filedeps(src, type).reduce(function (acc, dep) { acc[dep] = dep; return acc; }, {}); } /** * Parses a path string to see if it is a dependency slug (where a slug is the * dirname for an installed dependency) * * @param {String} str * @returns {Boolean} */ function isSlug(str) { var repo = /([A-Za-z0-9-_\.]+)/; var user = /([A-Za-z0-9-]{1,39})/; var ref = /([A-Za-z0-9-_\/\.$!#%&\(\)\+=\*]+)/; var r = new RegExp(user.source + '-' + repo.source + '@' + ref.source, 'i'); return r.test(str); } /** * Tells us whether or not the input path is a local path. (begins with . or /) * * @param {String} path * @return {Boolean} */ function isLocal(path) { return path[0] === '.' || path[0] === '/'; }