// Native const { resolve } = require("path"); // Packages const flatten = require("arr-flatten"); const unique = require("array-unique"); const ignore = require("ignore"); const _glob = require("glob"); const { stat, readdir, readFile } = require("fs-promise"); // Ours const IGNORED = require("./ignored"); /** * Returns a list of files in the given * directory that are subject to be * synchronized for npm. * * @param {String} full path to directory * @param {String} contents of `package.json` to avoid lookup * @param {Object} options: * - `limit` {Number|null} byte limit * - `debug` {Boolean} warn upon ignore * @return {Array} comprehensive list of paths to sync */ async function npm( path, pkg, nowConfig = null, { limit = null, hasNowJson = false, debug = false } = {} ) { const whitelist = (nowConfig && nowConfig.files) || pkg.files; // the package.json `files` whitelist still // honors ignores: https://docs.npmjs.com/files/package.json#files const search_ = whitelist || ["."]; // convert all filenames into absolute paths const search = Array.prototype.concat.apply( [], await Promise.all( search_.map(file => glob(file, { cwd: path, absolute: true, dot: true })) ) ); // always include the "main" file if (pkg.main) { search.push(require.resolve(resolve(path, pkg.main), "may-exclude")); // pkg: may-exclude suppresses warnings } // compile list of ignored patterns and files const npmIgnore = await maybeRead(resolve(path, ".npmignore"), null); const gitIgnore = npmIgnore === null ? await maybeRead(resolve(path, ".gitignore")) : null; const filter = ignore() .add( IGNORED + "\n" + clearRelative(npmIgnore === null ? gitIgnore : npmIgnore) ) .createFilter(); const prefixLength = path.length + 1; // the package.json `files` whitelist still // honors npmignores: https://docs.npmjs.com/files/package.json#files // but we don't ignore if the user is explicitly listing files // under the now namespace, or using files in combination with gitignore const overrideIgnores = (pkg.now && pkg.now.files) || (gitIgnore !== null && pkg.files); const accepts = overrideIgnores ? () => true : file => { const relativePath = file.substr(prefixLength); if (relativePath === "") { return true; } const accepted = filter(relativePath); if (!accepted && debug) { console.log('> [debug] ignoring "%s"', file); } return accepted; }; // locate files if (debug) { console.time(`> [debug] locating files ${path}`); } const files = await explode(search, { accepts, limit, debug }); if (debug) { console.timeEnd(`> [debug] locating files ${path}`); } // always include manifest as npm does not allow ignoring it // source: https://docs.npmjs.com/files/package.json#files files.push(asAbsolute("package.json", path)); if (hasNowJson) { files.push(asAbsolute("now.json", path)); } // get files return unique(files); } /** * Transform relative paths into absolutes, * and maintains absolutes as such. * * @param {String} maybe relative path * @param {String} parent full path */ const asAbsolute = function(path, parent) { if (path[0] === "/") { return path; } return resolve(parent, path); }; /** * Returns a list of files in the given * directory that are subject to be * sent to docker as build context. * * @param {String} full path to directory * @param {String} contents of `Dockerfile` * @param {Object} options: * - `limit` {Number|null} byte limit * - `debug` {Boolean} warn upon ignore * @return {Array} comprehensive list of paths to sync */ async function docker( path, nowConfig = null, { limit = null, hasNowJson = false, debug = false } = {} ) { const whitelist = nowConfig && nowConfig.files; // base search path // the now.json `files` whitelist still // honors ignores: https://docs.npmjs.com/files/package.json#files const search_ = whitelist || ["."]; // convert all filenames into absolute paths const search = search_.map(file => asAbsolute(file, path)); // compile list of ignored patterns and files const dockerIgnore = await maybeRead(resolve(path, ".dockerignore"), null); const filter = ignore() .add( IGNORED + "\n" + clearRelative( dockerIgnore === null ? await maybeRead(resolve(path, ".gitignore")) : dockerIgnore ) ) .createFilter(); const prefixLength = path.length + 1; const accepts = function(file) { const relativePath = file.substr(prefixLength); if (relativePath === "") { return true; } const accepted = filter(relativePath); if (!accepted && debug) { console.log('> [debug] ignoring "%s"', file); } return accepted; }; // locate files if (debug) { console.time(`> [debug] locating files ${path}`); } const files = await explode(search, { accepts, limit, debug }); if (debug) { console.timeEnd(`> [debug] locating files ${path}`); } // always include manifest as npm does not allow ignoring it // source: https://docs.npmjs.com/files/package.json#files files.push(asAbsolute("Dockerfile", path)); if (hasNowJson) { files.push(asAbsolute("now.json", path)); } // get files return unique(files); } const glob = async function(pattern, options) { return new Promise((resolve, reject) => { _glob(pattern, options, (error, files) => { if (error) { reject(error); } else { resolve(files); } }); }); }; /** * Explodes directories into a full list of files. * Eg: * in: ['/a.js', '/b'] * out: ['/a.js', '/b/c.js', '/b/d.js'] * * @param {Array} of {String}s representing paths * @param {Array} of ignored {String}s. * @param {Object} options: * - `limit` {Number|null} byte limit * - `debug` {Boolean} warn upon ignore * @return {Array} of {String}s of full paths */ async function explode(paths, { accepts, debug }) { const list = async file => { let path = file; let s; if (!accepts(file)) { return null; } try { s = await stat(path); } catch (e) { // in case the file comes from `files` or `main` // and it wasn't specified with `.js` by the user path = file + ".js"; try { s = await stat(path); } catch (e2) { if (debug) { console.log('> [debug] ignoring invalid file "%s"', file); } return null; } } if (s.isDirectory()) { const all = await readdir(file); return many(all.map(subdir => asAbsolute(subdir, file))); } else if (!s.isFile()) { if (debug) { console.log('> [debug] ignoring special file "%s"', file); } return null; } return path; }; const many = async all => { return await Promise.all( all.map(async file => { return await list(file); }) ); }; return flatten(await many(paths)).filter(v => v !== null); } /** * Returns the contents of a file if it exists. * * @return {String} results or `''` */ const maybeRead = async function(path, default_ = "") { try { return await readFile(path, "utf8"); } catch (err) { return default_; } }; /** * Remove leading `./` from the beginning of ignores * because our parser doesn't like them :| */ const clearRelative = function(str) { return str.replace(/(\n|^)\.\//g, "$1"); }; module.exports = { npm, docker };