citizen
Version:
Node.js MVC web application framework. Includes routing, serving, caching, session management, and other helpful tools.
288 lines (252 loc) • 10.1 kB
JavaScript
// application event hooks
// node
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
import path from 'node:path'
import { createRequire } from 'node:module'
// citizen
import cache from '../cache.js'
import helpers from '../helpers.js'
// third-party
import chokidar from 'chokidar'
const start = async () => {
// If file logging is enabled and the specified log directory doesn't exist, create it.
if ( CTZN.config.citizen.logs.access || CTZN.config.citizen.logs.error || CTZN.config.citizen.logs.debug ) {
try {
await fsPromises.stat(CTZN.config.citizen.directories.logs)
} catch ( err ) {
switch ( err.code ) {
case 'ENOENT':
try {
await fsPromises.mkdir(CTZN.config.citizen.directories.logs)
console.log('\nCreating log file directory:\n')
console.log(' ' + CTZN.config.citizen.directories.logs + '\n')
} catch ( err ) {
console.log('\ncitizen attempted to create a log file directory, but encountered the following error. Try creating the directory manually and starting the app again.\n\n')
console.log(err)
console.log('\n\nExiting...\n\n')
process.exit()
}
break
default:
console.log('\ncitizen encountered the following error when trying to read the log file directory:\n\n')
console.log(err)
console.log('\n\nExiting...\n\n')
process.exit()
}
}
}
// If max log file size is specified, watch files and rename when they hit their limit.
if ( CTZN.config.citizen.logs.maxFileSize ) {
chokidar.watch(CTZN.config.citizen.directories.logs + '/**.log', CTZN.config.citizen.logs.watcher).on('change', file => {
fs.stat(file, (err, stats) => {
if ( stats.size >= CTZN.config.citizen.logs.maxFileSize * 1024 ) {
fs.rename(file, file.replace('.log', '-' + stats.mtimeMs + '.log'), err => {
if ( err ) {
helpers.log({
type : 'error:server',
label : 'There was a problem archiving the following log file: ' + file,
content : err
})
}
})
}
})
})
}
// Enable hot module reloading in development mode
if ( CTZN.config.citizen.mode === 'development' ) {
const require = createRequire(import.meta.url)
console.log('\nStarting watcher for hot module replacement:\n')
// Hooks
console.log(' ' + CTZN.config.citizen.directories.controllers + '/hooks')
const hookWatch = chokidar.watch(CTZN.config.citizen.directories.controllers + '/hooks', CTZN.config.citizen.development.watcher)
hookWatch.on('change', async file => {
try {
let extension = path.extname(file),
fileName = path.basename(file, extension),
cacheBuster = Date.now()
cache.clear()
helpers.log({
label: 'Hook reinitialized: ' + fileName,
content: file
})
if ( CTZN.sessions && CTZN.config.citizen.development.watcher.killSession ) {
CTZN.sessions = {}
}
delete require.cache[require.resolve(file)]
CTZN.controllers.hooks[fileName] = await import(file + '?v=' + cacheBuster)
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following module: \n\n' + file,
content: err
})
}
})
// Route controllers
console.log(' ' + CTZN.config.citizen.directories.controllers + '/routes')
const controllerWatch = chokidar.watch(CTZN.config.citizen.directories.controllers + '/routes', CTZN.config.citizen.development.watcher)
controllerWatch.on('change', async file => {
try {
let extension = path.extname(file),
fileName = path.basename(file, extension),
cacheBuster = Date.now()
cache.clear()
if ( CTZN.sessions && CTZN.config.citizen.development.watcher.killSession ) {
CTZN.sessions = {}
}
delete require.cache[require.resolve(file)]
CTZN.controllers.routes[fileName] = await import(file + '?v=' + cacheBuster)
helpers.log({
label: 'Controller reinitialized: ' + fileName,
content: file
})
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following controller: \n\n' + file,
content: err
})
}
})
// Helpers
console.log(' ' + CTZN.config.citizen.directories.helpers)
const helperWatch = chokidar.watch(CTZN.config.citizen.directories.helpers, CTZN.config.citizen.development.watcher)
helperWatch.on('change', async file => {
try {
let extension = path.extname(file),
fileName = path.basename(file, extension),
cacheBuster = Date.now()
cache.clear()
helpers.log({
label: 'Helper reinitialized: ' + fileName,
content: file
})
if ( CTZN.sessions && CTZN.config.citizen.development.watcher.killSession ) {
CTZN.sessions = {}
}
delete require.cache[require.resolve(file)]
CTZN.helpers[fileName] = await import(file + '?v=' + cacheBuster)
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following module: \n\n' + file,
content: err
})
}
})
// Models
console.log(' ' + CTZN.config.citizen.directories.models)
const modelWatch = chokidar.watch(CTZN.config.citizen.directories.models, CTZN.config.citizen.development.watcher)
modelWatch.on('change', async file => {
try {
let extension = path.extname(file),
fileName = path.basename(file, extension),
cacheBuster = Date.now()
cache.clear()
if ( CTZN.sessions && CTZN.config.citizen.development.watcher.killSession ) {
CTZN.sessions = {}
}
delete require.cache[require.resolve(file)]
CTZN.models[fileName] = await import(file + '?v=' + cacheBuster)
helpers.log({
label: 'Model reinitialized: ' + fileName,
content: file
})
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following model: \n\n' + file,
content: err
})
}
})
// Views
console.log(' ' + CTZN.config.citizen.directories.views)
const viewWatch = chokidar.watch(CTZN.config.citizen.directories.views, CTZN.config.citizen.development.watcher)
viewWatch.on('change', async file => {
try {
let pattern = file.replace(/.+\/views\/([A-Za-z0-9-_+]+)\/[A-Za-z0-9-_+]+\.[A-Za-z]+$/g, '$1'),
extension = path.extname(file),
fileName = path.basename(file, extension)
cache.clear()
// If there's no matching pattern, it's a view file in the views directory.
if ( pattern === file ) {
CTZN.views[fileName] = {
path: file
}
} else {
CTZN.views[pattern][fileName] = {
path: file
}
}
if ( CTZN.config.citizen.templateEngine !== 'templateLiterals' ) {
let consolidate = await import('@ladjs/consolidate')
try {
await consolidate[CTZN.config.citizen.templateEngine](CTZN.views[fileName]?.path || CTZN.views[pattern][fileName].path, { cache: false })
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following view: \n\n' + fileName,
content: err
})
}
}
helpers.log({
label: 'View reinitialized: ' + fileName,
content: file
})
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following view: \n\n' + file,
content: err
})
}
})
// User modules
CTZN.config.citizen.development.watcher.custom.forEach(item => {
console.log(' ' + CTZN.config.citizen.directories.app + item.watch)
const customWatch = chokidar.watch(CTZN.config.citizen.directories.app + item.watch, CTZN.config.citizen.development.watcher)
customWatch.on('change', async file => {
try {
let extension = path.extname(file),
fileName = path.basename(file, extension),
cacheBuster = Date.now()
cache.clear()
helpers.log({
label: 'Module reinitialized: ' + fileName,
content: file
})
delete require.cache[require.resolve(file)]
if ( CTZN.sessions && CTZN.config.citizen.development.watcher.killSession ) {
CTZN.sessions = {}
}
delete require.cache[require.resolve(file)]
if ( item.assign ) {
let app = eval('global.' + item.assign)
app[fileName] = await import(file + '?v=' + cacheBuster)
}
} catch (err) {
helpers.log({
label: 'There was a problem reinitializing the following module: \n\n' + file,
content: err
})
}
})
})
// Static assets
if ( CTZN.config.citizen.cache.static.enabled ) {
const staticWatch = chokidar.watch(CTZN.config.citizen.directories.web, CTZN.config.citizen.development.watcher)
staticWatch.on('change', async file => {
cache.clear({ file: file })
})
}
console.log('')
}
// Fire the app's application start hook
if ( CTZN.controllers.hooks.application?.start ) {
await CTZN.controllers.hooks.application.start(CTZN.config)
}
}
const error = async (err, params, request, response, context) => {
if ( CTZN.controllers.hooks.application?.error ) {
await CTZN.controllers.hooks.application.error(err, params, request, response, context)
}
}
export default { start, error }