193 lines
4.6 KiB
JavaScript
193 lines
4.6 KiB
JavaScript
const fs = require('fs')
|
|
const { promisify } = require('util')
|
|
const { resolve, relative, normalize, dirname } = require('path')
|
|
const stat = promisify(fs.stat)
|
|
const readFile = promisify(fs.readFile)
|
|
const writeFile = promisify(fs.writeFile)
|
|
const mkdir = promisify(fs.mkdir)
|
|
const fsreaddir = promisify(fs.readdir)
|
|
const chmod = promisify(fs.chmod)
|
|
const rmrf = require('rimraf')
|
|
const micromatch = require('micromatch')
|
|
|
|
/**
|
|
* Type-checkers
|
|
*/
|
|
function isBoolean(b) {
|
|
return typeof b === 'boolean'
|
|
}
|
|
function isNumber(n) {
|
|
return typeof n === 'number' && !Number.isNaN(n)
|
|
}
|
|
function isObject(o) {
|
|
return o !== null && typeof o === 'object'
|
|
}
|
|
function isString(s) {
|
|
return typeof s === 'string'
|
|
}
|
|
function isUndefined(u) {
|
|
return typeof u === 'undefined'
|
|
}
|
|
function isFunction(f) {
|
|
return typeof f === 'function'
|
|
}
|
|
|
|
function match(input, patterns, options) {
|
|
input = input || Object.keys(this._files)
|
|
if (!(input && input.length)) return []
|
|
options = Object.assign({ dot: true }, options || {}, {
|
|
// required to convert forward to backslashes on Windows and match the file keys properly
|
|
format: normalize
|
|
})
|
|
return micromatch(input, patterns, options).sort()
|
|
}
|
|
|
|
function writeStream(path) {
|
|
return fs.createWriteStream(path, 'utf-8')
|
|
}
|
|
/**
|
|
* Recursively remove a directory
|
|
* @param {string} p
|
|
* @returns Promise
|
|
*/
|
|
function rm(p) {
|
|
return new Promise(function (resolve, reject) {
|
|
/* istanbul ignore else */
|
|
if (Object.prototype.hasOwnProperty.call(fs, 'rm')) {
|
|
fs.rm(
|
|
p,
|
|
{
|
|
recursive: true,
|
|
force: true
|
|
},
|
|
(err) => {
|
|
if (err) reject(err)
|
|
else resolve()
|
|
}
|
|
)
|
|
} else {
|
|
// Node 14.14- compat
|
|
rmrf(p, { glob: { dot: true } }, (err) => {
|
|
if (err) reject(err)
|
|
else resolve()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Recursive readdir with support for ignores
|
|
* @private
|
|
* @param {String} dir
|
|
* @param {Array<String|Function>} ignores
|
|
*/
|
|
function readdir(dir, ignores) {
|
|
if (Array.isArray(ignores)) {
|
|
ignores = {
|
|
str: ignores.filter(isString),
|
|
fn: ignores.filter(isFunction)
|
|
}
|
|
}
|
|
|
|
if (isString(dir)) {
|
|
dir = {
|
|
current: dir,
|
|
root: dir
|
|
}
|
|
}
|
|
const result = fsreaddir(dir.current)
|
|
.then((children) => {
|
|
const filtered = []
|
|
|
|
children.forEach((child) => {
|
|
const res = resolve(dir.current, child)
|
|
const rel = relative(dir.root, res)
|
|
|
|
if (!match(rel, ignores.str).length) {
|
|
filtered.push(
|
|
stat(res).then((stat) => {
|
|
// it would be better to put this check together with the previous if,
|
|
// but that would break backwards-compatibility with Metalsmith 2.3.0
|
|
// as the stat object needs to be passed to ignore fns
|
|
if (ignores.fn.some((fn) => fn(rel, stat))) {
|
|
return null
|
|
}
|
|
if (stat.isDirectory()) {
|
|
return readdir({ current: res, root: dir.root }, ignores)
|
|
}
|
|
return res
|
|
})
|
|
)
|
|
}
|
|
})
|
|
return Promise.all(filtered)
|
|
})
|
|
.then((files) => {
|
|
const result = files
|
|
// @TODO: this is a catch-all & can probably be finetuned with some more tests
|
|
.filter((file) => !(file instanceof Error) && file !== null)
|
|
.reduce((all, file) => all.concat(file), [])
|
|
.sort()
|
|
return result
|
|
})
|
|
.catch((err) => {
|
|
throw err
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Run `fn` in parallel on #`concurrency` number of `items`, spread over #`items / concurrency` sequential batches
|
|
* @private
|
|
* @param {() => Promise} fn
|
|
* @param {*[]} items
|
|
* @param {number} concurrency
|
|
* @returns {Promise<*[]>}
|
|
*/
|
|
function batchAsync(fn, items, concurrency) {
|
|
let batches = Promise.resolve([])
|
|
items = [...items]
|
|
|
|
while (items.length) {
|
|
const slice = items.splice(0, concurrency)
|
|
batches = batches.then((previousBatch) => {
|
|
return Promise.all(slice.map((...args) => fn(...args))).then((currentBatch) => [
|
|
...previousBatch,
|
|
...currentBatch
|
|
])
|
|
})
|
|
}
|
|
|
|
return batches
|
|
}
|
|
|
|
/**
|
|
* Output a file and create any non-existing directories in the process
|
|
* @private
|
|
**/
|
|
function outputFile(file, data, mode) {
|
|
return mkdir(dirname(file), { recursive: true })
|
|
.then(() => writeFile(file, data))
|
|
.then(() => (mode ? chmod(file, mode) : Promise.resolve()))
|
|
}
|
|
|
|
const helpers = {
|
|
isBoolean,
|
|
isNumber,
|
|
isString,
|
|
isObject,
|
|
isUndefined,
|
|
isFunction,
|
|
match,
|
|
rm,
|
|
readdir,
|
|
outputFile,
|
|
stat,
|
|
readFile,
|
|
batchAsync,
|
|
writeStream
|
|
}
|
|
|
|
module.exports = helpers
|