@keg-hub/parse-config
Version:
Utils to allow loading non-javascript files into a node environment
199 lines (175 loc) • 5.7 kB
JavaScript
const { execTemplate } = require('../template')
const { throwError, throwNoFile } = require('../error')
const { limbo, noOp, noOpObj, deepMerge, isStr } = require('@keg-hub/jsutils')
const {
existsSync,
promises:fs,
readFileSync,
} = require('fs')
const defLoaderArgs = {
error: true,
fill: true,
data: noOpObj,
format: 'object',
}
const pathExists = async (path) => {
try {
await fs.access(path)
return true
}
catch (err) {
return false
}
}
/**
* Catches EFBBBF (UTF-8 BOM) because the buffer-to-string
* conversion translates it to FEFF (UTF-16 BOM)
* @function
* @param {string} content - Content of the loaded env file
*
* @returns {string} - stripped string
*/
const stripBom = content =>
content.charCodeAt(0) === 0xfeff ? content.slice(1) : content
/**
* Checks it the passed in location exists on th local file system
* @type {function}
* @throws
* @param {string} location - Path to the file
* @param {boolean} error - If an error should be thrown when file does not exist
* @param {string} type - Type of file that's being loaded
*
* @returns {boolean} - True if the file exists
*/
const checkExists = async (location, error = true, type) => {
const [ err, exists ] = await limbo(pathExists(location))
return exists
? true
: error
? throwNoFile(location, err ? err.stack : `Could not load ${type} file!`)
: false
}
/**
* TODO: add caching of loaded files
* Should be for all file types based on location
* When getContentSync || getContent is called
* The response should be cached based on the format
* Then return the cached version if it exists
*/
/**
* Gets the content of a file from the passed in location synchronously
* @type {function}
* @throws
* @param {string} location - Path to the file
* @param {boolean} error - If an error should be thrown when file does not exist
* @param {string} type - Type of file that's being loaded
*
* @returns {string} - Loaded file content
*/
const getContentSync = (location, error = true, type) => {
return existsSync(location)
? readFileSync(location, 'utf8')
: error
? throwNoFile(location, `Could not load ${type} file!`)
: null
}
/**
* Gets the content of a file from the passed in location
* @type {function}
* @throws
* @param {string} location - Path to the file
* @param {boolean} error - If an error should be thrown when file does not exist
* @param {string} type - Type of file that's being loaded
*
* @returns {string} - Loaded file content
*/
const getContent = async (location, error = true, type) => {
const exists = await checkExists(location, error, type)
if (!exists) return null
// Get the content of the file
const [ err, content ] = await limbo(fs.readFile(location, 'utf8'))
return !err
? content
: error
? throwError(location, `Could not load ${type} file!`)
: null
}
/**
* Removes a file from the local file system
* @function
* @throws
* @param {Array} location - Path to the file
*
* @returns {boolean} - If the file could be removed
*/
const removeFile = async (location, type) => {
!isStr(location) &&
throwError(
`Remove ${type} file requires a file location, instead got: ${location}`
)
const [err] = await limbo(fs.rm(location))
return err ? throwError(err) : true
}
/**
* Loads multiple files from an array of passed in files paths
* Then merges them all together
* @function
* @param {Array} files - Array of files paths to load
* @param {function} loader - callback to load the file, should return an object
*
* @returns {Object} - Merged files as an Object
*/
const mergeFiles = async ({ files, loader = noOp, ...args }) => {
const loaded = await Promise.all(
await files.reduce(async (toResolve, file) => {
const loaded = await toResolve
const loadedContent =
isStr(file) && (await loader({ location: file, ...args }))
loadedContent && loaded.push(loadedContent)
return loaded
}, Promise.resolve([]))
)
return deepMerge(...loaded)
}
/**
* Parses the env content to replaces any template values from the data object
* Then converts it into a JS Object with the `env.safeLoad` call
* @function
* @param {Object} [args.data={}] - Data to file the file with, if it's a template
* @param {string} [args.format] - Type that should be returned ( string || Object )
* @param {boolean} [args.fill=true] - Should the content be treated as a template
* @param {RegEx} [args.pattern] - Pattern to match against template values
* @param {string} [content] - Text content to be filled
* @param {function} loader - Callback function to parse the content after it's filled
*
* @returns {Object} - Parse ENV file as a JS Object
*/
const loadTemplate = (args, content, loader) => {
const { data = noOpObj, fill = true, format, pattern } = args
if (!content) return format === 'string' ? '' : {}
const cleaned = stripBom(content)
const template = fill ? execTemplate(cleaned, data, pattern) : cleaned
// Treat it as a template and try to fill it fill === true
return format === 'string' ? template : loader(template)
}
/**
* Ensures args is an object for file loading methods
* If args is a string, will set it as the location
* Sets other defaults where needed
* @function
* @param {string|Object} args - Path to a file or args object
*
* @returns {Object} - Args converted into an object if needed
*/
const resolveArgs = args => {
return deepMerge(defLoaderArgs, !isStr(args) ? args : { location: args })
}
module.exports = {
getContent,
getContentSync,
loadTemplate,
mergeFiles,
removeFile,
resolveArgs,
stripBom,
}