/** * @author Toru Nagashima * @copyright 2016 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ "use strict" //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const fs = require("fs") const path = require("path") const ignore = require("ignore") const Cache = require("./cache") const exists = require("./exists") const getPackageJson = require("./get-package-json") //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const cache = new Cache() const SLASH_AT_BEGIN_AND_END = /^!?\/+|^!|\/+$/g const PARENT_RELATIVE_PATH = /^\.\./ const NEVER_IGNORED = /^(?:readme\.[^.]*|(?:licen[cs]e|changes|changelog|history)(?:\.[^.]*)?)$/i /** * Checks whether or not a given file name is a relative path to a ancestor * directory. * * @param {string} filePath - A file name to check. * @returns {boolean} `true` if the file name is a relative path to a ancestor * directory. */ function notAncestorFiles(filePath) { return PARENT_RELATIVE_PATH.test(filePath) } /** * @param {function} f - A function. * @param {function} g - A function. * @returns {function} A logical-and function of `f` and `g`. */ function and(f, g) { return (filePath) => f(filePath) && g(filePath) } /** * @param {function} f - A function. * @param {function} g - A function. * @param {function|null} h - A function. * @returns {function} A logical-or function of `f`, `g`, and `h`. */ function or(f, g, h) { return (filePath) => f(filePath) || g(filePath) || (h && h(filePath)) } /** * @param {function} f - A function. * @returns {function} A logical-not function of `f`. */ function not(f) { return (filePath) => !f(filePath) } /** * Creates a function which checks whether or not a given file is ignoreable. * * @param {object} p - An object of package.json. * @returns {function} A function which checks whether or not a given file is ignoreable. */ function filterNeverIgnoredFiles(p) { const basedir = path.dirname(p.filePath) const mainFilePath = (typeof p.main === "string") ? path.join(basedir, p.main) : null return (filePath) => ( path.join(basedir, filePath) !== mainFilePath && filePath !== "package.json" && !NEVER_IGNORED.test(path.relative(basedir, filePath)) ) } /** * Creates a function which checks whether or not a given file should be ignored. * * @param {string[]|null} files - File names of whitelist. * @returns {function|null} A function which checks whether or not a given file should be ignored. */ function parseWhiteList(files) { if (!files || !Array.isArray(files)) { return null } const ig = ignore() ig.add("*") for (const file of files) { if (typeof file === "string" && file) { const prefix = file.startsWith("!") ? "" : "!" const body = file.replace(SLASH_AT_BEGIN_AND_END, "") ig.add(`${prefix}/${body}`) ig.add(`${prefix}/${body}/**`) } } return not(ig.createFilter()) } /** * Creates a function which checks whether or not a given file should be ignored. * * @param {string} basedir - The directory path "package.json" exists. * @param {boolean} filesFieldExists - `true` if `files` field of `package.json` exists. * @returns {function|null} A function which checks whether or not a given file should be ignored. */ function parseNpmignore(basedir, filesFieldExists) { let filePath = path.join(basedir, ".npmignore") if (!exists(filePath)) { if (filesFieldExists) { return null } filePath = path.join(basedir, ".gitignore") if (!exists(filePath)) { return null } } const ig = ignore() ig.add(fs.readFileSync(filePath, "utf8")) return not(ig.createFilter()) } //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * Gets an object to check whether or not a given path should be ignored. * The object is created from: * * - `files` field of `package.json` * - `.npmignore` * * @param {string} startPath - A file path to lookup. * @returns {object} * An object to check whther or not a given path should be ignored. * The object has a method `match`. * `match` returns `true` if a given file path should be ignored. */ module.exports = function getNpmignore(startPath) { const retv = {match: notAncestorFiles} const p = getPackageJson(startPath) if (p) { const data = cache.get(p.filePath) if (data) { return data } const filesIgnore = parseWhiteList(p.files) const npmignoreIgnore = parseNpmignore(path.dirname(p.filePath), Boolean(filesIgnore)) if (filesIgnore && npmignoreIgnore) { retv.match = and(filterNeverIgnoredFiles(p), or(notAncestorFiles, filesIgnore, npmignoreIgnore)) } else if (filesIgnore) { retv.match = and(filterNeverIgnoredFiles(p), or(notAncestorFiles, filesIgnore)) } else if (npmignoreIgnore) { retv.match = and(filterNeverIgnoredFiles(p), or(notAncestorFiles, npmignoreIgnore)) } cache.set(p.filePath, retv) } return retv }