get-npmignore.js 5.21 KB
Newer Older
liang ce committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
/**
 * @author Toru Nagashima
 * See LICENSE file in root directory for full license.
 */
"use strict"

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")

const cache = new Cache()
const SLASH_AT_BEGIN_AND_END = /^!?\/+|^!|\/+$/gu
const PARENT_RELATIVE_PATH = /^\.\./u
const NEVER_IGNORED = /^(?:readme\.[^.]*|(?:licen[cs]e|changes|changelog|history)(?:\.[^.]*)?)$/iu

/**
 * 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 isAncestorFiles(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()
    const igN = ignore()
    let hasN = false

    for (const file of files) {
        if (typeof file === "string" && file) {
            const body = file.replace(SLASH_AT_BEGIN_AND_END, "")
            if (file.startsWith("!")) {
                igN.add(`${body}`)
                igN.add(`${body}/**`)
                hasN = true
            } else {
                ig.add(`/${body}`)
                ig.add(`/${body}/**`)
            }
        }
    }

    return hasN
        ? or(ig.createFilter(), not(igN.createFilter()))
        : 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())
}

/**
 * Gets an object to check whether a given path should be ignored or not.
 * 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: isAncestorFiles }

    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(isAncestorFiles, filesIgnore, npmignoreIgnore)
            )
        } else if (filesIgnore) {
            retv.match = and(
                filterNeverIgnoredFiles(p),
                or(isAncestorFiles, filesIgnore)
            )
        } else if (npmignoreIgnore) {
            retv.match = and(
                filterNeverIgnoredFiles(p),
                or(isAncestorFiles, npmignoreIgnore)
            )
        }

        cache.set(p.filePath, retv)
    }

    return retv
}