presta
Version:
Hyper minimal framework for the modern web.
227 lines (189 loc) • 5.84 kB
text/typescript
import fs from 'fs-extra'
// @ts-ignore
import graph from 'watch-dependency-graph'
import chokidar from 'chokidar'
import match from 'picomatch'
import { outputLambdas } from './outputLambdas'
import * as logger from './log'
import { getFiles, isStatic, isDynamic } from './getFiles'
import { renderStaticEntries } from './renderStaticEntries'
import { timer } from './timer'
import { createConfig, removeConfigValues, getConfigFile } from './config'
import { builtStaticFiles } from './builtStaticFiles'
import { removeBuiltStaticFile } from './removeBuiltStaticFile'
import { Presta } from './types'
/*
* Wraps outputLambdas for logging
*/
function updateLambdas(inputs: string[], config: Presta) {
const time = timer()
// always write this, even if inputs = []
outputLambdas(inputs, config)
// if user actually has routes configured, give feedback
if (inputs.length) {
logger.info({
label: 'built',
message: `lambdas`,
duration: time(),
})
}
}
export async function watch(config: Presta) {
/*
* Get files that match static/dynamic patters at startup
*/
let files = getFiles(config)
let hasConfigFile = fs.existsSync(config.configFilepath)
if (!files.length) {
logger.warn({
label: 'paths',
message: 'no files configured',
})
}
/*
* Create initial dynamic entry regardless of if the user has routes, bc we
* need this file to serve 404 locally
*/
updateLambdas(files.filter(isDynamic), config)
/*
* Set up all watchers
*/
const fileWatcher = graph({ alias: { '@': config.cwd } })
const globalWatcher = chokidar.watch(config.cwd, {
ignoreInitial: true,
ignored: [config.output, config.assets],
})
/*
* On a config update, the user may have passed in a new `files` array or
* other global config required by all files, so we need to re-fetch all
* files and rebuild everything.
*/
async function handleConfigUpdate() {
files = getFiles(config)
await renderStaticEntries(files.filter(isStatic), config)
updateLambdas(files.filter(isDynamic), config)
}
/*
* On a changed file, we can just render it
*/
async function handleFileChange(file: string) {
// render just file that changed
if (isStatic(file)) {
await renderStaticEntries([file], config)
}
// update dynamic entry with ALL dynamic files
if (isDynamic(file)) {
updateLambdas(files.filter(isDynamic), config)
}
config.hooks.emitBrowserRefresh()
}
config.hooks.onBuildFile(({ file }) => {
handleFileChange(file)
})
fileWatcher.on('remove', async ([id]: string[]) => {
logger.debug({
label: 'watch',
message: `fileWatcher - removed ${id}`,
})
// remove from local hash
files.splice(files.indexOf(id), 1)
// update this regardless, not sure if [id] was dynamic or static
updateLambdas(files.filter(isDynamic), config)
// if it was config, we gotta do a restart
if (id === config.configFilepath) {
// filter out values from the config file
config = await removeConfigValues()
// reset this!
hasConfigFile = false
handleConfigUpdate()
}
;(builtStaticFiles[id] || []).forEach((file) => removeBuiltStaticFile(file, config))
})
fileWatcher.on('change', async ([id]: string[]) => {
logger.debug({
label: 'watch',
message: `fileWatcher - changed ${id}`,
})
if (id === config.configFilepath) {
// clear config file for re-require
delete require.cache[config.configFilepath]
try {
// merge in new values from config file
config = await createConfig({
config: getConfigFile(config.configFilepath),
})
handleConfigUpdate()
} catch (e) {
logger.error({
label: 'error',
error: e as Error,
})
}
} else {
handleFileChange(id)
}
})
fileWatcher.on('error', (e: Error) => {
logger.error({
label: 'error',
error: e,
})
})
/*
* globalWatcher watches the raw file globs passed to the CLI or as `files`
* in the config. If checks on add/change to see if a file should be upgraded
* to a a Presta source file, and added to the fileWatcher. It also watches
* for addition of a config file.
*/
globalWatcher.on('all', async (event, file) => {
// ignore events handled by wdg, or any directory events
if (!/add|change/.test(event) || !fs.existsSync(file) || fs.lstatSync(file).isDirectory()) return
// if a file change matches any pages globs
if (match(config.files)(file) && !files.includes(file)) {
logger.debug({
label: 'watch',
message: `globalWatcher - add ${file}`,
})
files.push(file)
fileWatcher.add(file)
handleFileChange(file)
}
// if file matches config file and we don't already have one
if (file === config.configFilepath && !hasConfigFile) {
logger.debug({
label: 'watch',
message: `globalWatcher - add config file ${file}`,
})
fileWatcher.add(config.configFilepath)
try {
// merge in new values from config file
config = await createConfig({
config: getConfigFile(config.configFilepath),
})
hasConfigFile = true
handleConfigUpdate()
} catch (e) {
logger.error({
label: 'error',
error: e as Error,
})
}
}
})
/**
* Init watching after event subscriptions
*/
fileWatcher.add(files)
if (hasConfigFile) fileWatcher.add(config.configFilepath)
/**
* Prime files to check for errors on startup and register any plugins
*/
try {
files.map(require)
} catch (e) {
logger.error({
label: 'error',
error: e as Error,
})
}
}