roosevelt
Version:
🧸 MVC web framework for Node.js designed to make Express easier to use.
580 lines (544 loc) • 17.1 kB
JavaScript
// reads user supplied params from roosevelt constructor or from the app's package.json and configures the app
require('@colors/colors')
const path = require('path')
const sourceConfigs = require('source-configs')
const defaults = require('./defaults/config')
const template = require('./tools/templateLiteralRenderer')
module.exports = (params, appSchema) => {
const appDir = params.appDir
// determine if app has a package.json
let pkg
try {
pkg = require(path.join(appDir, 'package.json'))
} catch {
pkg = {}
}
// determine if app has a config file
let configFile
try {
configFile = require(path.join(appDir, 'rooseveltConfig.json'))
} catch (err) {
if (err.name === 'SyntaxError') console.error('❌', err)
configFile = {}
}
// source-configs configuration
const config = {
sources: [
'command line',
'environment variable',
params,
configFile,
pkg.rooseveltConfig || {}
],
logging: false,
// handle configuration edge cases
transform: (params, flags) => {
// handle --production-mode, --prod, -p cli flags etc
if (flags.productionProxyMode || flags.prodproxy || flags.x) {
params.mode = 'production-proxy'
} else if (flags.productionMode || flags.prod || flags.p) {
params.mode = 'production'
} else if (flags.developmentMode || flags.dev || flags.d) {
// handle --development-mode, --dev, -d cli flags
params.mode = 'development'
}
// handle --build and -b cli flags
if (flags.build || flags.b) {
params.makeBuildArtifacts = 'staticsOnly'
}
// handle --webpack, --wp, -w cli flags
if (flags.webpack === 'verbose' || flags.wp === 'verbose' || flags.w === 'verbose') {
params.js.webpack.verbose = true
}
if (flags.webpack === 'verbose-file' || flags.wp === 'verbose-file' || flags.w === 'verbose-file') {
params.js.webpack.verbose = 'file'
}
// handle --disable-validator, --raw, -r cli flags
if (flags.disableValidator || flags.raw || flags.r) {
params.htmlValidator.enable = false
} else if (flags.enableValidator || flags.htmlValidator || flags.h) {
// handle --enable-validator, --html-validator, -h cli flags
params.htmlValidator.enable = true
}
// default mode param to production if its value is invalid
if (params.mode !== 'production-proxy' && params.mode !== 'production' && params.mode !== 'development') {
params.mode = 'production'
}
return params
}
}
/**
* parameters are sourced via the following priority:
* 1. command line arguments
* 2. environment variables
* 3. object passed to roosevelt function
* 4. rooseveltConfig in package.json
* 5. defaults (stored in lib/defaults/config.json)
*/
const schema = {
appDir: {
default: appDir
},
port: {
envVar: ['HTTP_PORT', 'NODE_PORT'],
default: defaults.port
},
mode: {
envVar: ['NODE_ENV'],
default: defaults.mode
},
enableCLIFlags: {
default: defaults.enableCLIFlags
},
makeBuildArtifacts: {
default: defaults.makeBuildArtifacts
},
localhostOnly: {
default: defaults.localhostOnly
},
logging: {
default: defaults.logging
},
minify: {
default: defaults.minify
},
expressSessionStore: {
filename: {
default: defaults.expressSessionStore.filename
},
instance: {
default: defaults.expressSessionStore.instance
},
preset: {
default: defaults.expressSessionStore.preset
},
presetOptions: {
default: defaults.expressSessionStore.presetOptions
}
},
htmlValidator: {
enable: {
default: defaults.htmlValidator.enable
},
exceptions: {
requestHeader: {
default: defaults.htmlValidator.exceptions.requestHeader
},
modelValue: {
default: defaults.htmlValidator.exceptions.modelValue
}
},
validatorConfig: {
default: defaults.htmlValidator.validatorConfig
}
},
formidable: {
default: defaults.formidable
},
helmet: {
default: defaults.helmet
},
csrfProtection: {
default: defaults.csrfProtection
},
expressSession: {
default: defaults.expressSession
},
bodyParser: {
urlEncoded: {
default: defaults.bodyParser.urlEncoded
},
json: {
default: defaults.bodyParser.json
}
},
frontendReload: {
enable: {
default: defaults.frontendReload.enable
},
exceptionRoutes: {
default: defaults.frontendReload.exceptionRoutes
},
expressBrowserReloadParams: {
default: defaults.frontendReload.expressBrowserReloadParams
}
},
shutdownTimeout: {
default: defaults.shutdownTimeout
},
https: {
enable: {
default: defaults.https.enable
},
force: {
default: defaults.https.force
},
autoCert: {
default: defaults.https.autoCert
},
port: {
envVar: ['HTTPS_PORT'],
default: defaults.https.port
},
authInfoPath: {
default: null
},
passphrase: {
default: null
},
caCert: {
default: null
},
requestCert: {
default: null
},
rejectUnauthorized: {
default: null
}
},
secretsPath: {
default: defaults.secretsPath
},
modelsPath: {
default: defaults.modelsPath
},
viewsPath: {
default: defaults.viewsPath
},
preprocessedViewsPath: {
default: defaults.preprocessedViewsPath
},
preprocessedStaticsPath: {
default: defaults.preprocessedStaticsPath
},
viewEngine: {
default: defaults.viewEngine
},
controllersPath: {
default: defaults.controllersPath
},
errorPages: {
notFound: {
default: defaults.errorPages.notFound
},
forbidden: {
default: defaults.errorPages.forbidden
},
internalServerError: {
default: defaults.errorPages.internalServerError
},
serviceUnavailable: {
default: defaults.errorPages.serviceUnavailable
}
},
routePrefix: {
default: defaults.routePrefix
},
staticsRoot: {
default: defaults.staticsRoot
},
html: {
sourcePath: {
default: defaults.html.sourcePath
},
allowlist: {
default: defaults.html.allowlist
},
blocklist: {
default: defaults.html.blocklist
},
models: {
default: defaults.html.models
},
output: {
default: defaults.html.output
},
minifier: {
enable: {
default: defaults.html.minifier.enable
},
exceptionRoutes: {
default: defaults.html.minifier.exceptionRoutes
},
options: {
default: defaults.html.minifier.options
}
}
},
css: {
sourcePath: {
default: defaults.css.sourcePath
},
compiler: {
enable: {
default: defaults.css.compiler.enable
},
module: {
default: defaults.css.compiler.module
},
options: {
default: defaults.css.compiler.options
}
},
minifier: {
enable: {
default: defaults.css.minifier.enable
},
options: {
default: defaults.css.minifier.options
}
},
allowlist: {
default: defaults.css.allowlist
},
output: {
default: defaults.css.output
},
versionFile: {
default: defaults.css.versionFile
}
},
js: {
sourcePath: {
default: defaults.js.sourcePath
},
webpack: {
enable: {
default: defaults.js.webpack.enable
},
bundles: {
default: defaults.js.webpack.bundles
},
verbose: {
default: defaults.js.webpack.verbose
}
}
},
publicFolder: {
default: defaults.publicFolder
},
favicon: {
default: defaults.favicon
},
symlinks: {
default: defaults.symlinks
},
minifyHtmlAttributes: {
enable: {
default: defaults.minifyHtmlAttributes.enable
},
minifyHtmlAttributesParams: {
default: defaults.minifyHtmlAttributes.minifyHtmlAttributesParams
}
},
prodSourceMaps: {
default: defaults.prodSourceMaps
},
versionedPublic: {
default: defaults.versionedPublic
},
hostPublic: {
default: defaults.hostPublic
},
clientViews: {
enable: {
default: defaults.clientViews.enable
},
allowlist: {
default: defaults.clientViews.allowlist
},
blocklist: {
default: defaults.clientViews.blocklist
},
output: {
default: defaults.clientViews.output
},
exposeAll: {
default: defaults.clientViews.exposeAll
},
defaultBundle: {
default: defaults.clientViews.defaultBundle
},
minify: {
default: defaults.clientViews.minify
},
minifyOptions: {
default: defaults.clientViews.minifyOptions
}
},
clientControllers: {
enable: {
default: defaults.clientControllers.enable
},
allowlist: {
default: defaults.clientControllers.allowlist
},
blocklist: {
default: defaults.clientControllers.blocklist
},
output: {
default: defaults.clientControllers.output
},
exposeAll: {
default: defaults.clientControllers.exposeAll
},
defaultBundle: {
default: defaults.clientControllers.defaultBundle
}
},
onBeforeMiddleware: {
default: {}
},
onServerInit: {
default: {}
},
onServerStart: {
default: {}
},
onAppExit: {
default: {}
},
onClientViewsProcess: {
default: {}
},
cssCompiler: {
default: {}
}
}
// if a schema is passed in, update any necessary command line flags and environment variables
if (appSchema !== undefined && appSchema.rooseveltConfig !== undefined) {
updateFlagsAndEnvVars(schema, appSchema.rooseveltConfig)
}
params = sourceConfigs(schema, config)
// set mode specific overrides
if (params.mode === 'production' || params.mode === 'production-proxy') {
process.env.NODE_ENV = 'production'
} else if (params.mode === 'development') {
process.env.NODE_ENV = 'development'
}
// resolve path params
params.staticsRoot = path.join(appDir, params.staticsRoot)
params.secretsPath = path.join(appDir, params.secretsPath)
params.modelsPath = path.join(appDir, params.modelsPath)
params.viewsPath = path.join(appDir, params.viewsPath)
if (params.preprocessedViewsPath) params.preprocessedViewsPath = path.join(appDir, params.preprocessedViewsPath)
if (params.preprocessedStaticsPath) params.preprocessedStaticsPath = path.join(appDir, params.preprocessedStaticsPath)
params.controllersPath = path.join(appDir, params.controllersPath)
params.unversionedPublic = path.join(appDir, params.publicFolder)
params.publicFolder = path.join(params.unversionedPublic, params.versionedPublic ? pkg.version || '' : '')
params.html.sourcePath = path.join(params.staticsRoot, params.html.sourcePath)
params.html.output = path.join(params.unversionedPublic, params.html.output)
params.css.sourcePath = (params?.minifyHtmlAttributes?.enable === 'development' || (params?.minifyHtmlAttributes?.enable && process.env.NODE_ENV === 'production')) ? path.join(params.preprocessedStaticsPath, params.css.sourcePath) : path.join(params.staticsRoot, params.css.sourcePath)
params.css.output = path.join(params.publicFolder, params.css.output)
params.js.sourcePath = (params?.minifyHtmlAttributes?.enable === 'development' || (params?.minifyHtmlAttributes?.enable && process.env.NODE_ENV === 'production')) ? path.join(params.preprocessedStaticsPath, params.js.sourcePath) : path.join(params.staticsRoot, params.js.sourcePath)
params.clientViews.output = path.join(params.publicFolder, params.clientViews.output)
params.clientControllers.output = path.join(params.publicFolder, params.clientControllers.output)
params.pkg = pkg
// scan for variables in params and resolve them
const configTemplateLiterals = []
let passes = 3
;(function paramVariables (paramSet, paramSource) {
paramSource = paramSource || params
// iterate over param object
if (paramSet === Object(paramSet)) {
for (const paramKey in paramSet) {
const param = paramSet[paramKey]
// recurse if param value is an object (but not a function)
if (param === Object(param) && typeof param !== 'function') {
paramVariables(param, paramSource[paramKey])
} else if (typeof param === 'string' && /\${.*}/.test(param)) {
// pushes all config variables into an array for later checks
configTemplateLiterals.push(param)
// run param through template parser
let sourceParam = template(param, params)
// normalize paths
if (sourceParam.includes(appDir)) {
sourceParam = path.normalize(sourceParam)
}
// convert number strings into numbers
if (!isNaN(sourceParam) && !isNaN(parseFloat(sourceParam))) {
sourceParam = parseInt(sourceParam)
}
// convert string booleans into booleans
if (sourceParam === 'true') sourceParam = true
if (sourceParam === 'false') sourceParam = false
// bind parsed param to config
paramSource[paramKey] = sourceParam
}
}
return configTemplateLiterals
}
// repeat this process for 3 passes
passes--
if (passes > 0) paramVariables(params)
})(params)
// set mode specific overrides
if (params.mode === 'production' || params.mode === 'production-proxy') {
// html validator is always disabled in production mode
params.htmlValidator.enable = false
} else if (params.mode === 'development') {
// minification is always disabled in development mode
params.minify = false
}
// disable https by env var
if (process.env.DISABLE_HTTPS === 'true') {
params.https.enable = false
}
// set makeBuildArtifacts by env var
if (process.env.MAKE_BUILD_ARTIFACTS) {
params.makeBuildArtifacts = process.env.MAKE_BUILD_ARTIFACTS
}
// hostPublic always true in dev mode
if (params.mode === 'development') {
params.hostPublic = true
}
// force localhostOnly and hostPublic off in production-proxy mode
if (params.mode === 'production-proxy') {
params.localhostOnly = true
params.hostPublic = false
}
// sanitize the routePrefix param
if (params.routePrefix && typeof params.routePrefix === 'string') {
params.routePrefix = params.routePrefix.trim()
// append a slash if one doesn't exist
if (!params.routePrefix.startsWith('/')) {
params.routePrefix = `/${params.routePrefix}`
}
// remove trailing slash if one exists
if (params.routePrefix.endsWith('/')) {
params.routePrefix = params.routePrefix.slice(0, -1)
}
} else {
// default it to empty string if not set
params.routePrefix = ''
}
// goes through all config variables and checks if it exists
let extract
for (const row in configTemplateLiterals) {
extract = configTemplateLiterals[row].match(/\${(.*)}/).pop()
const ex = getOrSetObjectByDotNotation(params, extract)
if (!ex) console.warn(`⚠️ Config variable ${extract} does not exist.`)
}
return params
}
// recursively sets the command line flags and environment variables of the schema based on given params.
function updateFlagsAndEnvVars (schema, params) {
for (const key in params) {
if (key === 'commandLineArg' || key === 'envVar') {
schema[key] = params[key]
} else if (schema[key] !== undefined) updateFlagsAndEnvVars(schema[key], params[key])
}
}
// gets or sets an object by dot notation, e.g. thing.nestedThing.furtherNestedThing: two arguments gets, three arguments sets
function getOrSetObjectByDotNotation (obj, dotNotation, value) {
if (!dotNotation || typeof dotNotation === 'boolean' || typeof dotNotation === 'number') return dotNotation
if (typeof dotNotation === 'string') return getOrSetObjectByDotNotation(obj, dotNotation.split('.'), value)
else if (dotNotation.length === 1 && value !== undefined) {
obj[dotNotation[0]] = value
return obj[dotNotation[0]]
} else if (dotNotation.length === 0) return obj
else if (dotNotation.length === 1) {
if (obj) return obj[dotNotation[0]]
return false
} else return getOrSetObjectByDotNotation(obj[dotNotation[0]], dotNotation.slice(1), value)
}