var Ignore = require('fstream-ignore') var inherits = require('inherits') var path = require('path') var fs = require('fs') module.exports = Packer inherits(Packer, Ignore) function Packer (props) { if (!(this instanceof Packer)) { return new Packer(props) } if (typeof props === 'string') { props = { path: props } } props.ignoreFiles = props.ignoreFiles || [ '.npmignore', '.gitignore', 'package.json' ] Ignore.call(this, props) this.bundled = props.bundled this.bundleLinks = props.bundleLinks this.package = props.package // only do the magic bundling stuff for the node_modules folder that // lives right next to a package.json file. this.bundleMagic = this.parent && this.parent.packageRoot && this.basename === 'node_modules' // in a node_modules folder, resolve symbolic links to // bundled dependencies when creating the package. props.follow = this.follow = this.bundleMagic // console.error("follow?", this.path, props.follow) if (this === this.root || this.parent && this.parent.bundleMagic && this.basename.charAt(0) !== '.') { this.readBundledLinks() } this.on('entryStat', function (entry, props) { // files should *always* get into tarballs // in a user-writable state, even if they're // being installed from some wackey vm-mounted // read-only filesystem. entry.mode = props.mode = props.mode | parseInt('0200', 8) }) } Packer.prototype.readBundledLinks = function () { if (this._paused) { this.once('resume', this.addIgnoreFiles) return } this.pause() fs.readdir(this.path + '/node_modules', function (er, list) { // no harm if there's no bundle var l = list && list.length if (er || l === 0) return this.resume() var errState = null var then = function then (er) { if (errState) return if (er) { errState = er return this.resume() } if (--l === 0) return this.resume() }.bind(this) list.forEach(function (pkg) { if (pkg.charAt(0) === '.') return then() var pd = this.path + '/node_modules/' + pkg fs.realpath(pd, function (er, rp) { if (er) return then() this.bundleLinks = this.bundleLinks || {} this.bundleLinks[pkg] = rp then() }.bind(this)) }, this) }.bind(this)) } Packer.prototype.applyIgnores = function (entry, partial, entryObj) { if (!entryObj || entryObj.type !== 'Directory') { // package.json files can never be ignored. if (entry === 'package.json') return true // readme files should never be ignored. if (entry.match(/^readme(\.[^\.]*)$/i)) return true // license files should never be ignored. if (entry.match(/^(license|licence)(\.[^\.]*)?$/i)) return true // copyright notice files should never be ignored. if (entry.match(/^(notice)(\.[^\.]*)?$/i)) return true // changelogs should never be ignored. if (entry.match(/^(changes|changelog|history)(\.[^\.]*)?$/i)) return true } // special rules. see below. if (entry === 'node_modules' && this.packageRoot) return true // package.json main file should never be ignored. var mainFile = this.package && this.package.main if (mainFile && path.resolve(this.path, entry) === path.resolve(this.path, mainFile)) return true // some files are *never* allowed under any circumstances // (VCS folders, native build cruft, npm cruft, regular cruft) if (entry === '.git' || entry === 'CVS' || entry === '.svn' || entry === '.hg' || entry === '.lock-wscript' || entry.match(/^\.wafpickle-[0-9]+$/) || (this.parent && this.parent.packageRoot && this.basename === 'build' && entry === 'config.gypi') || entry === 'npm-debug.log' || entry === '.npmrc' || entry.match(/^\..*\.swp$/) || entry === '.DS_Store' || entry.match(/^\._/) ) { return false } // in a node_modules folder, we only include bundled dependencies // also, prevent packages in node_modules from being affected // by rules set in the containing package, so that // bundles don't get busted. // Also, once in a bundle, everything is installed as-is // To prevent infinite cycles in the case of cyclic deps that are // linked with npm link, even in a bundle, deps are only bundled // if they're not already present at a higher level. if (this.bundleMagic) { // bubbling up. stop here and allow anything the bundled pkg allows if (entry.indexOf('/') !== -1) return true // never include the .bin. It's typically full of platform-specific // stuff like symlinks and .cmd files anyway. if (entry === '.bin') return false // the package root. var p = this.parent // the package before this one. var pp = p && p.parent // if this entry has already been bundled, and is a symlink, // and it is the *same* symlink as this one, then exclude it. if (pp && pp.bundleLinks && this.bundleLinks && pp.bundleLinks[entry] && pp.bundleLinks[entry] === this.bundleLinks[entry]) { return false } // since it's *not* a symbolic link, if we're *already* in a bundle, // then we should include everything. if (pp && pp.package && pp.basename === 'node_modules') { return true } // only include it at this point if it's a bundleDependency var bd = this.package && this.package.bundleDependencies if (bd && !Array.isArray(bd)) { throw new Error(this.package.name + '\'s `bundledDependencies` should ' + 'be an array') } var shouldBundle = bd && bd.indexOf(entry) !== -1 // if we're not going to bundle it, then it doesn't count as a bundleLink // if (this.bundleLinks && !shouldBundle) delete this.bundleLinks[entry] return shouldBundle } // if (this.bundled) return true return Ignore.prototype.applyIgnores.call(this, entry, partial, entryObj) } Packer.prototype.addIgnoreFiles = function () { var entries = this.entries // if there's a .npmignore, then we do *not* want to // read the .gitignore. if (entries.indexOf('.npmignore') !== -1) { var i = entries.indexOf('.gitignore') if (i !== -1) { entries.splice(i, 1) } } this.entries = entries Ignore.prototype.addIgnoreFiles.call(this) } Packer.prototype.readRules = function (buf, e) { if (e !== 'package.json') { return Ignore.prototype.readRules.call(this, buf, e) } buf = buf.toString().trim() if (buf.length === 0) return [] try { var p = this.package = JSON.parse(buf) } catch (er) { // just pretend it's a normal old file, not magic at all. return [] } if (this === this.root) { this.bundleLinks = this.bundleLinks || {} this.bundleLinks[p.name] = this._path } this.packageRoot = true this.emit('package', p) // make bundle deps predictable if (p.bundledDependencies && !p.bundleDependencies) { p.bundleDependencies = p.bundledDependencies delete p.bundledDependencies } if (!p.files || !Array.isArray(p.files)) return [] // ignore everything except what's in the files array. return ['*'].concat(p.files.map(function (f) { return '!' + f })).concat(p.files.map(function (f) { return '!' + f.replace(/\/+$/, '') + '/**' })) } Packer.prototype.getChildProps = function (stat) { var props = Ignore.prototype.getChildProps.call(this, stat) props.package = this.package props.bundled = this.bundled && this.bundled.slice(0) props.bundleLinks = this.bundleLinks && Object.create(this.bundleLinks) // Directories have to be read as Packers // otherwise fstream.Reader will create a DirReader instead. if (stat.isDirectory()) { props.type = this.constructor } // only follow symbolic links directly in the node_modules folder. props.follow = false return props } var order = [ 'package.json', '.npmignore', '.gitignore', /^README(\.md)?$/, 'LICENCE', 'LICENSE', /\.js$/ ] Packer.prototype.sort = function (a, b) { for (var i = 0, l = order.length; i < l; i++) { var o = order[i] if (typeof o === 'string') { if (a === o) return -1 if (b === o) return 1 } else { if (a.match(o)) return -1 if (b.match(o)) return 1 } } // deps go in the back if (a === 'node_modules') return 1 if (b === 'node_modules') return -1 return Ignore.prototype.sort.call(this, a, b) } Packer.prototype.emitEntry = function (entry) { if (this._paused) { this.once('resume', this.emitEntry.bind(this, entry)) return } // if there is a .gitignore, then we're going to // rename it to .npmignore in the output. if (entry.basename === '.gitignore') { entry.basename = '.npmignore' entry.path = path.resolve(entry.dirname, entry.basename) } // all *.gyp files are renamed to binding.gyp for node-gyp // but only when they are in the same folder as a package.json file. if (entry.basename.match(/\.gyp$/) && this.entries.indexOf('package.json') !== -1) { entry.basename = 'binding.gyp' entry.path = path.resolve(entry.dirname, entry.basename) } // skip over symbolic links if (entry.type === 'SymbolicLink') { entry.abort() return } if (entry.type !== 'Directory') { // make it so that the folder in the tarball is named "package" var h = path.dirname((entry.root || entry).path) var t = entry.path.substr(h.length + 1).replace(/^[^\/\\]+/, 'package') var p = h + '/' + t entry.path = p entry.dirname = path.dirname(p) return Ignore.prototype.emitEntry.call(this, entry) } // we don't want empty directories to show up in package // tarballs. // don't emit entry events for dirs, but still walk through // and read them. This means that we need to proxy up their // entry events so that those entries won't be missed, since // .pipe() doesn't do anythign special with "child" events, on // with "entry" events. var me = this entry.on('entry', function (e) { if (e.parent === entry) { e.parent = me me.emit('entry', e) } }) entry.on('package', this.emit.bind(this, 'package')) }