From 50509a261385ed5ce6a7099257ae8c58cb006881 Mon Sep 17 00:00:00 2001 From: Leonardo Bishop Date: Tue, 8 Aug 2023 01:21:25 +0100 Subject: Add live reloading --- app/index.ts | 8 +++- app/middlewares/blogs.ts | 28 ++++++------ app/middlewares/index.ts | 26 ++++++----- app/pages.ts | 112 +++++++++++++++++++++++++++++----------------- app/routes/blog/router.ts | 5 --- app/routes/page/router.ts | 3 -- docker-compose.yml | 1 + package-lock.json | 108 +++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + 9 files changed, 211 insertions(+), 81 deletions(-) diff --git a/app/index.ts b/app/index.ts index 664a751..7d73eb4 100644 --- a/app/index.ts +++ b/app/index.ts @@ -4,6 +4,8 @@ import * as page from './routes/page/router.js'; import * as blog from './routes/blog/router.js'; import { logger } from './logger.js' import { PageDirectory } from './pages.js'; +import { directory } from './middlewares/index.js'; +import { blogs } from './middlewares/blogs.js'; dotenv.config() @@ -16,6 +18,10 @@ app.use(express.static('static', { maxAge: '1d' })); +const pageDirectory = new PageDirectory(process.env.PAGES_DIR); +app.use(directory(pageDirectory)); +app.use(blogs(pageDirectory)); + app.use(blog.router); app.use(page.router); @@ -36,7 +42,7 @@ const exit = () => { }) } -PageDirectory.rebuild('pages'); +pageDirectory.loadFromDisk(); process.on('SIGINT', exit); process.on('SIGTERM', exit); diff --git a/app/middlewares/blogs.ts b/app/middlewares/blogs.ts index 8fd07c6..e1f5433 100644 --- a/app/middlewares/blogs.ts +++ b/app/middlewares/blogs.ts @@ -1,17 +1,19 @@ import { PageDirectory } from "../pages.js"; -export const blogs = ((req, res, next) => { - let blogs = []; - for (const page of Object.values(PageDirectory.pages)) { - if (page.route.startsWith('blog/')) { - blogs.push(page); +export const blogs = (pageDirectory: PageDirectory) => { + return ((req, res, next) => { + let blogs = []; + for (const page of Object.values(pageDirectory.pages)) { + if (page.route.startsWith('blog/')) { + blogs.push(page); + } } - } - - blogs.sort((a, b) => { - return b.metadata.date.getTime() - a.metadata.date.getTime(); + + blogs.sort((a, b) => { + return b.metadata.date.getTime() - a.metadata.date.getTime(); + }); + + res.locals.blogs = blogs; + next(); }); - - res.locals.blogs = blogs; - next(); -}); +} diff --git a/app/middlewares/index.ts b/app/middlewares/index.ts index 81431ca..b5fcd34 100644 --- a/app/middlewares/index.ts +++ b/app/middlewares/index.ts @@ -1,16 +1,18 @@ import { PageDirectory } from "../pages.js"; -export const page = ((req, res, next) => { - const path = req.originalUrl == "/" ? 'index' : req.originalUrl.substring(1); - res.locals.path = path; - - const page = PageDirectory.get(path); +export const directory = (pageDirectory: PageDirectory) => { + return ((req, res, next) => { + const path = req.originalUrl == "/" ? 'index' : req.originalUrl.substring(1); + res.locals.path = path; + + const page = pageDirectory.get(path); + + if (!page) { + next(); + return; + } - if (!page) { + res.locals.page = page; next(); - return; - } - - res.locals.page = page; - next(); -}); + }); +} diff --git a/app/pages.ts b/app/pages.ts index 3bef4a9..a7057da 100644 --- a/app/pages.ts +++ b/app/pages.ts @@ -3,9 +3,9 @@ import glob from 'glob'; import { logger } from './logger.js' import { marked } from 'marked'; import matter from 'gray-matter'; +import chokidar from 'chokidar'; export function buildPage(page: Page) { - logger.info(`Building ${page.path}`); try { const result = matter(page.raw); const metadata = result.data; @@ -19,61 +19,93 @@ export function buildPage(page: Page) { } } -export namespace PageDirectory { - export const pages: Record = {}; - export let lastBuild: number; +function loadRaw(path: string): string { + return readFileSync(`${path}`, 'utf-8'); +} + +export class PageDirectory { + private pagesPath: string; + + public pages: Record = {}; + public lastFullBuild: number; - export const rebuild = (pagePath: string): boolean => { - for (const page in pages) { - delete pages[page]; + constructor(pagesPath: string) { + this.pagesPath = pagesPath; + } + + public loadFromDisk = () => { + for (const page in this.pages) { + delete this.pages[page]; } - const localPages = glob.sync(`**/*.{md,html}`, { cwd: pagePath }) + const localPages = glob.sync(`**/*.{md,html}`, { cwd: this.pagesPath }) - // Load page content - localPages.forEach(page => { - let route = page.replace(/\.[^.]*$/,'') - let name = /[^/]*$/.exec(route)[0]; - let path = `${pagePath}/${page}` - let raw: string; - try { - raw = loadRaw(path); - } catch (e) { - logger.error(`Failed to read page ${path}: ${e.message}`); - return; - } + localPages.forEach(this.loadPage); - pages[route] = { - route: route, - name: name, - path: path, - raw: raw, - buildTime: 0, - metadata: { - title: "A Page" - } - } + this.lastFullBuild = Date.now(); + + const watcher = chokidar.watch('.', { + persistent: true, + cwd: this.pagesPath, + ignoreInitial: true, }); - // Build pages - Object.values(pages).forEach(page => buildPage(page)); + const onPageChange = (page: string) => { + logger.info(`File ${page} has been modified`); + this.loadPage(page); + } + + const onPageRemoval = (page: string) => { + logger.info(`File ${page} has been removed`); + this.removePage(page); + } + + watcher.on('add', onPageChange); + watcher.on('change', onPageChange); + watcher.on('unlink', onPageRemoval); + } + + public loadPage = (page: string): void => { + logger.info(`Building page ${page}`); + let route = page.replace(/\.[^.]*$/,'') + let name = /[^/]*$/.exec(route)[0]; + let path = `${this.pagesPath}/${page}` + let raw: string; + try { + raw = loadRaw(path); + } catch (e) { + logger.error(`Failed to read page ${path}: ${e.message}`); + return; + } - lastBuild = Date.now(); - return true; + this.pages[route] = { + route: route, + name: name, + path: path, + raw: raw, + buildTime: 0, + metadata: { + title: "A Page" + } + } + + buildPage(this.pages[route]); + } + + public removePage = (page: string): void => { + logger.info(`Unloading page ${page}`); + let route = page.replace(/\.[^.]*$/,'') + delete this.pages[route]; } - export function get(name: string): Page { - const page = pages[name]; + public get(name: string): Page { + const page = this.pages[name]; if (!page) { return undefined; } return page; } - - function loadRaw(path: string): string { - return readFileSync(`${path}`, 'utf-8'); - } } export type Page = { diff --git a/app/routes/blog/router.ts b/app/routes/blog/router.ts index bbd09d5..933946b 100644 --- a/app/routes/blog/router.ts +++ b/app/routes/blog/router.ts @@ -1,12 +1,7 @@ import express from 'express'; -import { page } from '../../middlewares/index.js'; -import { blogs } from '../../middlewares/blogs.js'; export const router = express.Router({ mergeParams: true }); -router.use('/blog/:page?', page); -router.use('/blog/:page?', blogs); - router.get('/blog/:page?', (req, res, next) => { let page = res.locals.page; let index = !page || res.locals.path === 'blog'; diff --git a/app/routes/page/router.ts b/app/routes/page/router.ts index 5c0a39b..b1b9bc1 100644 --- a/app/routes/page/router.ts +++ b/app/routes/page/router.ts @@ -1,10 +1,7 @@ import express from 'express'; -import { page } from '../../middlewares/index.js'; export const router = express.Router({ mergeParams: true }); -router.use('/:page?', page); - router.get('/:page?', (req, res, next) => { let page = res.locals.page; if (!page) { diff --git a/docker-compose.yml b/docker-compose.yml index 9572132..bfee91d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: - "3000:3000" volumes: - .env:/app/.env:ro + - .env.defaults:/app/.env.defaults:ro - ./pages:/app/pages:ro - ./static:/app/static:ro - ./views:/app/views:ro diff --git a/package-lock.json b/package-lock.json index 98807f4..46764c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@types/express": "^4.17.13", "@types/glob": "^7.2.0", "axios": "^1.4.0", + "chokidar": "^3.5.3", "dateformat": "^5.0.2", "dotenv-defaults": "^3.0.0", "ejs": "^3.1.6", @@ -576,6 +577,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -621,6 +634,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -670,7 +691,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -722,6 +742,43 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1354,7 +1411,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1485,6 +1541,19 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -1738,6 +1807,17 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -1750,7 +1830,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1759,7 +1838,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1771,7 +1849,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -2035,6 +2112,14 @@ "node": ">= 0.6" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -2181,7 +2266,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -2293,6 +2377,17 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2607,7 +2702,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index d700de3..197ee3a 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/express": "^4.17.13", "@types/glob": "^7.2.0", "axios": "^1.4.0", + "chokidar": "^3.5.3", "dateformat": "^5.0.2", "dotenv-defaults": "^3.0.0", "ejs": "^3.1.6", -- cgit v1.2.3-70-g09d2