roosevelt
Version:
🧸 MVC web framework for Node.js designed to make Express easier to use.
751 lines (728 loc) • 27.9 kB
JavaScript
// check user config against default roosevelt configuration
const fs = require('fs-extra')
require('@colors/colors')
if (module.parent) {
module.exports = {
audit: configAudit
}
} else {
configAudit()
}
async function configAudit (appDir) {
const path = require('path')
const Logger = require('roosevelt-logger')
const logger = new Logger()
const defaultScripts = require('../defaults/scripts')
const defaultScriptKeys = Object.keys(defaultScripts)
let pkg
let cfg
let errors
let processEnv
if (!appDir) {
if (fs.pathExistsSync(path.join(process.cwd(), 'node_modules')) === false) {
processEnv = process.cwd()
} else {
processEnv = undefined
}
appDir = processEnv
}
try {
// check for existence of package.json and rooseveltConfig within
pkg = require(path.join(appDir, 'package.json'))
if (!pkg.rooseveltConfig) {
throw new Error('rooseveltConfig not found!')
}
} catch {
pkg = {}
}
try {
// check for existence of rooseveltConfig.json or roosevelt.config.json
if (fs.existsSync(path.join(appDir, 'rooseveltConfig.json'))) cfg = require(path.join(appDir, 'rooseveltConfig.json'))
else if (fs.existsSync(path.join(appDir, 'roosevelt.config.json'))) cfg = require(path.join(appDir, 'roosevelt.config.json'))
} catch (e) {
// skip audit if package config was also not found
if (!pkg.rooseveltConfig) {
return
}
}
logger.info('📋', 'Starting rooseveltConfig audit...')
if (cfg) {
logger.info('📋', 'Scanning rooseveltConfig.json...')
// audit config file if it exists
audit(cfg, 'file')
}
if (pkg.rooseveltConfig) {
logger.info('📋', 'Scanning package.json...')
// audit package.json if it exists
audit(pkg, 'package')
}
/**
* Audit a given Roosevelt config
* @param {object} config - configuration object to test
* @param {string} source - source of configuration (package/config file)
*/
function audit (config, source) {
let userConfig
let userConfigKeys
let userScripts
// make deep copies of configuration object
if (source === 'package') {
userConfig = JSON.parse(JSON.stringify(config.rooseveltConfig))
userConfigKeys = Object.keys(userConfig)
userScripts = config.scripts || {}
} else if (source === 'file') {
userConfig = JSON.parse(JSON.stringify(config))
userConfigKeys = Object.keys(userConfig)
}
/**
* To check for extra params, call `foundExtra()` in the default case
* Auditing an object requires a `switch`, cases for each param, and to push/pop from `keyStack`
* Check types by calling `checkTypes` and passing it a list of possible types it should be
*/
const keyStack = []
for (const key of userConfigKeys) {
const userParam = userConfig[key]
keyStack.push(key)
switch (key) {
case 'hostPublic':
checkTypes(userParam, key, ['boolean'])
break
case 'clientViews': {
checkTypes(userParam, key, ['object'])
const clientViewParam = userParam || {}
for (const clientViewKey of Object.keys(clientViewParam)) {
keyStack.push(clientViewKey)
switch (clientViewKey) {
case 'allowlist':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['null', 'object'])
break
case 'blocklist':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['null', 'object'])
break
case 'output':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['string'])
break
case 'minify':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['boolean'])
break
case 'minifyOptions':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['null', 'object'])
break
case 'exposeAll':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['boolean'])
break
case 'defaultBundle':
checkTypes(clientViewParam[clientViewKey], clientViewKey, ['string'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(clientViewKey)
}
break
}
case 'clientControllers': {
checkTypes(userParam, key, ['object'])
const clientControllerParam = userParam || {}
for (const clientControllerKey of Object.keys(clientControllerParam)) {
keyStack.push(clientControllerKey)
switch (clientControllerKey) {
case 'allowlist':
checkTypes(clientControllerParam[clientControllerKey], clientControllerKey, ['null', 'object'])
break
case 'blocklist':
checkTypes(clientControllerParam[clientControllerKey], clientControllerKey, ['null', 'object'])
break
case 'output':
checkTypes(clientControllerParam[clientControllerKey], clientControllerKey, ['string'])
break
case 'exposeAll':
checkTypes(clientControllerParam[clientControllerKey], clientControllerKey, ['boolean'])
break
case 'defaultBundle':
checkTypes(clientControllerParam[clientControllerKey], clientControllerKey, ['string'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(clientControllerKey)
}
break
}
case 'controllersPath':
checkTypes(userParam, key, ['string'])
break
case 'enableCLIFlags':
checkTypes(userParam, key, ['boolean'])
break
case 'favicon':
checkTypes(userParam, key, ['null', 'string'])
break
case 'makeBuildArtifacts':
checkTypes(userParam, key, ['boolean', 'string'])
break
case 'https': {
checkTypes(userParam, key, ['boolean', 'object'])
const httpsParam = userParam || {}
for (const httpsKey of Object.keys(httpsParam)) {
keyStack.push(httpsKey)
switch (httpsKey) {
case 'enable':
checkTypes(httpsParam[httpsKey], httpsKey, ['boolean'])
break
case 'force':
checkTypes(httpsParam[httpsKey], httpsKey, ['boolean'])
break
case 'autoCert':
checkTypes(httpsParam[httpsKey], httpsKey, ['boolean'])
break
case 'port':
checkTypes(httpsParam[httpsKey], httpsKey, ['string', 'number'])
break
case 'authInfoPath':
checkTypes(httpsParam[httpsKey], httpsKey, ['null', 'object'])
break
case 'passphrase':
checkTypes(httpsParam[httpsKey], httpsKey, ['null', 'string'])
break
case 'caCert':
checkTypes(httpsParam[httpsKey], httpsKey, ['null', 'string'])
break
case 'requestCert':
checkTypes(httpsParam[httpsKey], httpsKey, ['null', 'boolean'])
break
case 'rejectUnauthorized':
checkTypes(httpsParam[httpsKey], httpsKey, ['null', 'boolean'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(httpsKey)
}
break
}
case 'localhostOnly':
checkTypes(userParam, key, ['boolean'])
break
case 'minify':
checkTypes(userParam, key, ['boolean'])
break
case 'mode':
checkTypes(userParam, key, ['string'])
break
case 'modelsPath':
checkTypes(userParam, key, ['string'])
break
case 'secretsPath':
checkTypes(userParam, key, ['string'])
break
case 'formidable':
checkTypes(userParam, key, ['boolean', 'object'])
break
case 'port':
checkTypes(userParam, key, ['number', 'string'])
break
case 'publicFolder':
checkTypes(userParam, key, ['string'])
break
case 'routePrefix':
checkTypes(userParam, key, ['null', 'string'])
break
case 'shutdownTimeout':
checkTypes(userParam, key, ['number'])
break
case 'staticsRoot':
checkTypes(userParam, key, ['string'])
break
case 'symlinks':
checkTypes(userParam, key, ['array'])
break
case 'prodSourceMaps':
checkTypes(userParam, key, ['boolean'])
break
case 'versionedPublic':
checkTypes(userParam, key, ['boolean'])
break
case 'viewEngine':
checkTypes(userParam, key, ['array', 'null', 'string'])
break
case 'viewsPath':
checkTypes(userParam, key, ['string'])
break
case 'preprocessedViewsPath':
checkTypes(userParam, key, ['boolean', 'string'])
break
case 'preprocessedStaticsPath':
checkTypes(userParam, key, ['boolean', 'string'])
break
case 'helmet':
checkTypes(userParam, key, ['object'])
break
case 'csrfProtection':
checkTypes(userParam, key, ['boolean', 'object'])
break
case 'expressSessionStore':
checkTypes(userParam, key, ['object'])
break
case 'expressSession':
checkTypes(userParam, key, ['boolean', 'object'])
break
case 'bodyParser': {
checkTypes(userParam, key, ['object'])
const bodyParserParam = userParam || {}
for (const bodyParserKey of Object.keys(bodyParserParam)) {
keyStack.push(bodyParserKey)
switch (bodyParserKey) {
case 'urlEncoded':
checkTypes(bodyParserParam[bodyParserKey], bodyParserKey, ['object'])
break
case 'json':
checkTypes(bodyParserParam[bodyParserKey], bodyParserKey, ['object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(bodyParserKey)
}
break
}
case 'html': {
checkTypes(userParam, key, ['object'])
const htmlParam = userParam || {}
for (const htmlKey of Object.keys(htmlParam)) {
keyStack.push(htmlKey)
switch (htmlKey) {
case 'sourcePath':
checkTypes(htmlParam[htmlKey], htmlKey, ['string'])
break
case 'allowlist':
checkTypes(htmlParam[htmlKey], htmlKey, ['null', 'array'])
break
case 'blocklist':
checkTypes(htmlParam[htmlKey], htmlKey, ['null', 'array'])
break
case 'models':
checkTypes(htmlParam[htmlKey], htmlKey, ['object'])
break
case 'output':
checkTypes(htmlParam[htmlKey], htmlKey, ['string', 'null'])
break
case 'minifier': {
checkTypes(htmlParam[htmlKey], htmlKey, ['object'])
const htmlMinParam = htmlParam[htmlKey] || {}
for (const htmlMinKey of Object.keys(htmlMinParam)) {
keyStack.push(htmlMinKey)
switch (htmlMinKey) {
case 'enable':
checkTypes(htmlMinParam[htmlMinKey], htmlMinKey, ['boolean'])
break
case 'exceptionRoutes':
checkTypes(htmlMinParam[htmlMinKey], htmlMinKey, ['array', 'boolean', 'string'])
break
case 'options':
checkTypes(htmlMinParam[htmlMinKey], htmlMinKey, ['object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(htmlMinKey)
}
break
}
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(htmlKey)
}
break
}
case 'css': {
checkTypes(userParam, key, ['object'])
const cssParam = userParam || {}
for (const cssKey of Object.keys(cssParam)) {
keyStack.push(cssKey)
switch (cssKey) {
case 'sourcePath':
checkTypes(cssParam[cssKey], cssKey, ['string'])
break
case 'compiler': {
checkTypes(cssParam[cssKey], cssKey, ['null', 'object', 'string'])
const cssCompilerParam = cssParam[cssKey] || {}
for (const cssCompilerKey of Object.keys(cssCompilerParam)) {
keyStack.push(cssCompilerKey)
switch (cssCompilerKey) {
case 'enable':
checkTypes(cssCompilerParam[cssCompilerKey], cssCompilerKey, ['boolean'])
break
case 'module':
checkTypes(cssCompilerParam[cssCompilerKey], cssCompilerKey, ['string'])
break
case 'options':
checkTypes(cssCompilerParam[cssCompilerKey], cssCompilerKey, ['object', 'null'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(cssCompilerKey)
}
break
}
case 'minifier': {
checkTypes(cssParam[cssKey], cssKey, ['null', 'object'])
const cssMinifierParam = cssParam[cssKey] || {}
for (const minifierKey of Object.keys(cssMinifierParam)) {
keyStack.push(minifierKey)
switch (minifierKey) {
case 'enable':
checkTypes(cssMinifierParam[minifierKey], minifierKey, ['boolean'])
break
case 'options':
checkTypes(cssMinifierParam[minifierKey], minifierKey, ['object', 'null'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(minifierKey)
}
break
}
case 'allowlist':
checkTypes(cssParam[cssKey], cssKey, ['array', 'null'])
break
case 'output':
checkTypes(cssParam[cssKey], cssKey, ['string'])
break
case 'versionFile':
checkTypes(cssParam[cssKey], cssKey, ['null', 'object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(cssKey)
}
break
}
case 'errorPages': {
checkTypes(userParam, key, ['object'])
const errorPagesParam = userParam || {}
for (const errorPagesKey of Object.keys(errorPagesParam)) {
keyStack.push(errorPagesKey)
switch (errorPagesKey) {
case 'notFound':
checkTypes(errorPagesParam[errorPagesKey], errorPagesKey, ['string'])
break
case 'forbidden':
checkTypes(errorPagesParam[errorPagesKey], errorPagesKey, ['string'])
break
case 'internalServerError':
checkTypes(errorPagesParam[errorPagesKey], errorPagesKey, ['string'])
break
case 'serviceUnavailable':
checkTypes(errorPagesParam[errorPagesKey], errorPagesKey, ['string'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(errorPagesKey)
}
break
}
case 'frontendReload': {
checkTypes(userParam, key, ['object'])
const reloadParam = userParam || {}
for (const reloadKey of Object.keys(reloadParam)) {
keyStack.push(reloadKey)
switch (reloadKey) {
case 'enable':
checkTypes(reloadParam[reloadKey], reloadKey, ['boolean'])
break
case 'exceptionRoutes':
checkTypes(reloadParam[reloadKey], reloadKey, ['array'])
break
case 'expressBrowserReloadParams':
checkTypes(reloadParam[reloadKey], reloadKey, ['object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(reloadKey)
}
break
}
case 'minifyHtmlAttributes': {
checkTypes(userParam, key, ['object'])
const minifyHtmlAttributesParam = userParam || {}
for (const minifyHtmlAttributesKey of Object.keys(minifyHtmlAttributesParam)) {
keyStack.push(minifyHtmlAttributesKey)
switch (minifyHtmlAttributesKey) {
case 'enable':
checkTypes(minifyHtmlAttributesParam[minifyHtmlAttributesKey], minifyHtmlAttributesKey, ['boolean'])
break
case 'minifyHtmlAttributesParams':
checkTypes(minifyHtmlAttributesParam[minifyHtmlAttributesKey], minifyHtmlAttributesKey, ['object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(minifyHtmlAttributesKey)
}
break
}
case 'htmlValidator': {
checkTypes(userParam, key, ['object'])
const validatorParam = userParam || {}
for (const validatorKey of Object.keys(validatorParam)) {
keyStack.push(validatorKey)
switch (validatorKey) {
case 'enable':
checkTypes(validatorParam[validatorKey], validatorKey, ['boolean'])
break
case 'exceptions': {
checkTypes(validatorParam[validatorKey], validatorKey, ['object'])
break
}
case 'validatorConfig':
checkTypes(validatorParam[validatorKey], validatorKey, ['object'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(validatorKey)
}
break
}
case 'logging': {
checkTypes(userParam, key, ['object'])
const loggingParam = userParam || {}
for (const loggingKey of Object.keys(loggingParam)) {
keyStack.push(loggingKey)
switch (loggingKey) {
case 'methods': {
checkTypes(loggingParam[loggingKey], loggingKey, ['object'])
const methodsParam = loggingParam[loggingKey] || {}
for (const methodsKey of Object.keys(methodsParam)) {
keyStack.push(methodsKey)
switch (methodsKey) {
case 'http':
checkTypes(methodsParam[methodsKey], methodsKey, ['boolean'])
break
case 'info':
checkTypes(methodsParam[methodsKey], methodsKey, ['boolean'])
break
case 'warn':
checkTypes(methodsParam[methodsKey], methodsKey, ['boolean'])
break
case 'error':
checkTypes(methodsParam[methodsKey], methodsKey, ['boolean'])
break
case 'verbose':
checkTypes(methodsParam[methodsKey], methodsKey, ['boolean'])
break
default:
// ignore extra custom log types in the methods object
}
keyStack.pop(methodsKey)
}
break
}
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(loggingKey)
}
break
}
case 'js': {
checkTypes(userParam, key, ['object'])
const jsParam = userParam || {}
for (const jsKey of Object.keys(jsParam)) {
keyStack.push(jsKey)
switch (jsKey) {
case 'sourcePath':
checkTypes(jsParam[jsKey], jsKey, ['string'])
break
case 'webpack': {
checkTypes(jsParam[jsKey], jsKey, ['object'])
const bundlerParam = jsParam[jsKey] || {}
for (const bundlerKey of Object.keys(bundlerParam)) {
keyStack.push(bundlerKey)
switch (bundlerKey) {
case 'enable':
checkTypes(bundlerParam[bundlerKey], bundlerKey, ['boolean'])
break
case 'bundles':
checkTypes(bundlerParam[bundlerKey], bundlerKey, ['array'])
break
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(bundlerKey)
}
break
}
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(jsKey)
}
break
}
default:
foundExtra(['rooseveltConfig', ...keyStack])
}
keyStack.pop(key)
}
if (userScripts) {
let hasProdScript = false
let hasDevScript = false
for (const script of defaultScriptKeys) {
switch (script) {
case 'start':
case 'production':
case 'prod':
case 'p':
if (userScripts[script] !== undefined) {
hasProdScript = true
}
break
case 'development':
case 'dev':
case 'd':
if (userScripts[script] !== undefined) {
hasDevScript = true
}
break
case 'audit-config':
checkMissingScript(userScripts, script)
checkOutdatedScript(userScripts, script)
break
case 'a':
break
case 'generate-certs':
checkMissingScript(userScripts, script)
checkOutdatedScript(userScripts, script)
break
case 'c':
break
case 'standard':
break
case 'stylelint':
break
case 'lint':
case 'l':
break
case 'test':
if (userScripts[script] === undefined) {
logger.warn(` Missing recommended script "${script}"!`)
errors = true
}
break
}
}
// output warning for missing start script
if (!hasProdScript) {
logger.warn(' Missing production run script(s).')
errors = true
}
if (!hasDevScript) {
logger.warn(' Missing development run script(s).')
errors = true
}
}
if (errors) {
logger.error('Issues have been detected in your Roosevelt config, please consult https://github.com/rooseveltframework/roosevelt#configure-your-app-with-parameters for details on each param.'.bold)
logger.error('Or see https://github.com/rooseveltframework/roosevelt/blob/master/lib/defaults/config.json for the latest sample Roosevelt config.'.bold)
} else {
logger.info('✅', 'rooseveltConfig audit completed with no errors found.'.green)
}
// detect if roosevelt is being updated from a pre 0.16 version
if (source === 'package' && pkg.rooseveltConfig.cleanTimer) {
logger.error('Your Roosevelt config was created under an old version of Roosevelt. Because of breaking API changes you may need to remove previous build artifacts that were generated by the previous version of Roosevelt such as most commonly: statics/.build and statics/js/.bundled. You can remove this warning by upgrading your Roosevelt config to the latest version of Roosevelt. Be sure to update your nodemonConfig and .gitignore as well. See https://github.com/rooseveltframework/roosevelt/blob/master/lib/defaults/config.json for the latest default config.'.bold)
}
}
/**
* Logs an error to the console displaying info about the extra param that was found
*
* @param {Array} keyList - list of keys from the keyStack
*/
function foundExtra (keyList) {
logger.error('⚠️', ` Extra param "${keyList.pop()}" found in ${keyList.join('.')}, this can be removed.`)
errors = true
}
/**
* Type checker for config params - matches a list of types against a given param
*
* @param param - the value to be type checked against
* @param key - the name of the param
* @param types - the types to check against the param
*/
function checkTypes (param, key, types) {
if (param !== undefined) {
let isCorrectType = false
for (let i = 0; i < types.length; i++) {
switch (types[i]) {
case 'array':
if (param instanceof Array) {
isCorrectType = true
}
break
case 'boolean':
if (typeof param === 'boolean') {
isCorrectType = true
}
break
case 'null':
if (param === null) {
isCorrectType = true
}
break
case 'number':
if (typeof param === 'number') {
isCorrectType = true
}
break
case 'object':
if (param instanceof Object) {
isCorrectType = true
}
break
case 'string':
if (typeof param === 'string') {
isCorrectType = true
}
break
}
}
if (isCorrectType === false) {
logger.error('⚠️', ` The type of param "${key}" should be one of the supported types: ${types.join(', ')}`)
errors = true
}
}
}
/**
* Checks user's scripts if the default script exists
*
* @param scripts - user's scripts
* @param scriptKey - script key to access script from user's scripts
*/
function checkMissingScript (scripts, scriptKey) {
if (scripts[scriptKey] === undefined) {
logger.error('⚠️', ` Missing script "${scriptKey}"!`)
errors = true
}
}
/**
* Checks user's scripts for outdated script
*
* @param scripts - user's scripts
* @param scriptKey - script key to access script from user's scripts
*/
function checkOutdatedScript (scripts, scriptKey) {
if (scripts !== undefined && scripts[scriptKey] !== undefined) {
if (scripts[scriptKey].includes('roosevelt') && scripts[scriptKey] !== defaultScripts[scriptKey].value) {
logger.error('⚠️', ` Detected outdated script "${scriptKey}". Update contents to "${defaultScripts[scriptKey].value}" to restore functionality.`)
errors = true
}
}
}
}