diff options
Diffstat (limited to 'src/write')
-rw-r--r-- | src/write/bind-utilities.js | 2 | ||||
-rw-r--r-- | src/write/build-modes/live-dev-server.js | 102 | ||||
-rw-r--r-- | src/write/build-modes/repl.js | 8 | ||||
-rw-r--r-- | src/write/build-modes/static-build.js | 87 |
4 files changed, 107 insertions, 92 deletions
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 3d4ecc7a..8dd08dba 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -19,7 +19,6 @@ import { export function bindUtilities({ absoluteTo, - cachebust, defaultLanguage, getSizeOfAdditionalFile, getSizeOfImagePath, @@ -36,7 +35,6 @@ export function bindUtilities({ Object.assign(bound, { absoluteTo, - cachebust, defaultLanguage, getSizeOfAdditionalFile, getSizeOfImagePath, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 24e18320..b018bc1c 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,10 +1,11 @@ import {spawn} from 'node:child_process'; import * as http from 'node:http'; -import {readFile, stat} from 'node:fs/promises'; +import {open, stat} from 'node:fs/promises'; import * as path from 'node:path'; +import {pipeline} from 'node:stream/promises'; import {inspect as nodeInspect} from 'node:util'; -import {ENABLE_COLOR, logInfo, logWarn, progressCallAll} from '#cli'; +import {ENABLE_COLOR, colors, logInfo, logWarn, progressCallAll} from '#cli'; import {watchContentDependencies} from '#content-dependencies'; import {quickEvaluate} from '#content-function'; import * as html from '#html'; @@ -23,23 +24,27 @@ import {generateRandomLinkDataJSON, generateRedirectHTML} from '../common-templa const defaultHost = '0.0.0.0'; const defaultPort = 8002; -export const description = `Hosts a local HTTP server which generates page content as it is requested, instead of all at once; reacts to changes in data files, so new reloads will be up-to-date with on-disk YAML data (<- not implemented yet, check back soon!)\n\nIntended for local development ONLY; this custom HTTP server is NOT rigorously tested and almost certainly has security flaws`; +export const description = `Hosts a local HTTP server which generates page content as it is requested, instead of all at once\n\nIntended for local development ONLY; this custom HTTP server is NOT rigorously tested and almost certainly has security flaws`; export const config = { fileSizes: { - default: true, + default: 'perform', }, languageReloading: { - default: true, + default: 'perform', }, mediaValidation: { - default: true, + default: 'perform', }, thumbs: { - default: true, + default: 'perform', + }, + + webRoutes: { + required: true, }, }; @@ -88,9 +93,6 @@ export function getCLIOptions() { export async function go({ cliOptions, - _dataPath, - mediaPath, - mediaCachePath, defaultLanguage, languages, @@ -98,9 +100,9 @@ export async function go({ srcRootPath, thumbsCache, urls, + webRoutes, wikiData, - cachebust, developersComment: _developersComment, getSizeOfAdditionalFile, getSizeOfImagePath, @@ -211,7 +213,7 @@ export async function go({ response.writeHead(200, contentTypeJSON); response.end(json); - if (loudResponses) console.log(`${requestHead} [200] ${pathname}`); + if (loudResponses) console.log(`${requestHead} [200] ${pathname} (${colors.yellow(`special`)})`); } catch (error) { response.writeHead(500, contentTypeJSON); response.end(`Internal error serializing wiki JSON`); @@ -221,30 +223,27 @@ export async function go({ return; } - const { - area: localFileArea, - path: localFilePath - } = pathname.match(/^\/(?<area>static|util|media|thumb)\/(?<path>.*)/)?.groups ?? {}; + const matchedWebRoute = + webRoutes + .find(({to}) => pathname.startsWith('/' + to)); + + if (matchedWebRoute) { + const localFilePath = pathname.slice(1 + matchedWebRoute.to.length); - if (localFileArea) { // Not security tested, man, this is a dev server!! - const safePath = path.posix.resolve('/', localFilePath).replace(/^\//, ''); - - let localDirectory; - if (localFileArea === 'static' || localFileArea === 'util') { - localDirectory = path.join(srcRootPath, localFileArea); - } else if (localFileArea === 'media') { - localDirectory = mediaPath; - } else if (localFileArea === 'thumb') { - localDirectory = mediaCachePath; - } + const safePath = + path.posix + .resolve('/', localFilePath) + .replace(/^\//, ''); + + const localDirectory = matchedWebRoute.from; let filePath; try { filePath = path.resolve(localDirectory, decodeURI(safePath.split('/').join(path.sep))); } catch (error) { response.writeHead(404, contentTypePlain); - response.end(`No ${localFileArea} file found for: ${safePath}`); + response.end(`File not found for: ${safePath}`); console.log(`${requestHead} [404] ${pathname}`); console.log(`Failed to decode request pathname`); } @@ -254,12 +253,12 @@ export async function go({ } catch (error) { if (error.code === 'ENOENT') { response.writeHead(404, contentTypePlain); - response.end(`No ${localFileArea} file found for: ${safePath}`); + response.end(`File not found for: ${safePath}`); console.log(`${requestHead} [404] ${pathname}`); console.log(`ENOENT for stat: ${filePath}`); } else { response.writeHead(500, contentTypePlain); - response.end(`Internal error accessing ${localFileArea} file for: ${safePath}`); + response.end(`Internal error accessing file for: ${safePath}`); console.error(`${requestHead} [500] ${pathname}`); showError(error); } @@ -300,21 +299,33 @@ export async function go({ 'zip': 'application/zip', }[extname]; + let fd, size; try { - const {size} = await stat(filePath); - const buffer = await readFile(filePath) - response.writeHead(200, contentType ? { - 'Content-Type': contentType, - 'Content-Length': size, - } : {}); - response.end(buffer); - if (loudResponses) console.log(`${requestHead} [200] ${pathname}`); + ({size} = await stat(filePath)); + fd = await open(filePath); } catch (error) { - response.writeHead(500, contentTypePlain); - response.end(`Failed during file-to-response pipeline`); - console.error(`${requestHead} [500] ${pathname}`); - showError(error); + if (error.code === 'EISDIR') { + response.writeHead(404, contentTypePlain); + response.end(`File not found for: ${safePath}`); + console.error(`${requestHead} [404] ${pathname} (is directory)`); + } else { + response.writeHead(500, contentTypePlain); + response.end(`Failed during file-to-response pipeline`); + console.error(`${requestHead} [500] ${pathname}`); + showError(error); + } + return; } + + response.writeHead(200, { + ...contentType ? {'Content-Type': contentType} : {}, + 'Content-Length': size, + }); + + await pipeline(fd.createReadStream(), response); + + if (loudResponses) console.log(`${requestHead} [200] ${pathname} (${colors.magenta(`web route`)})`); + return; } @@ -326,7 +337,7 @@ export async function go({ if (!Object.hasOwn(urlToPageMap, pathnameKey)) { response.writeHead(404, contentTypePlain); response.end(`No page found for: ${pathnameKey}\n`); - if (loudResponses) console.log(`${requestHead} [404] ${pathname}`); + if (loudResponses) console.log(`${requestHead} [404] ${pathname} (no page)`); return; } @@ -387,7 +398,6 @@ export async function go({ const bound = bindUtilities({ absoluteTo, - cachebust, defaultLanguage, getSizeOfAdditionalFile, getSizeOfImagePath, @@ -421,9 +431,9 @@ export async function go({ ? `${(timeDelta / 1000).toFixed(2)}s` : `${timeDelta}ms`); - console.log(`${requestHead} [200, ${timeString}] ${pathname}`); + console.log(`${requestHead} [200, ${timeString}] ${pathname} (${colors.blue(`page`)})`); } else if (loudResponses) { - console.log(`${requestHead} [200] ${pathname}`); + console.log(`${requestHead} [200] ${pathname} (${colors.blue(`page`)})`); } response.writeHead(200, contentTypeHTML); diff --git a/src/write/build-modes/repl.js b/src/write/build-modes/repl.js index 20985595..faba8a34 100644 --- a/src/write/build-modes/repl.js +++ b/src/write/build-modes/repl.js @@ -6,13 +6,17 @@ export const config = { }, languageReloading: { - default: true, + default: 'perform', }, mediaValidation: { default: 'skip', }, + search: { + default: 'skip', + }, + thumbs: { applicable: false, }, @@ -51,6 +55,7 @@ export async function getContextAssignments({ missingImagePaths, thumbsCache, urls, + webRoutes, wikiData, getSizeOfAdditionalFile, @@ -78,6 +83,7 @@ export async function getContextAssignments({ missingImagePaths, thumbsCache, urls, + webRoutes, wikiData, ...wikiData, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index a355a002..1ab0604e 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -38,7 +38,7 @@ export const description = `Generates all page content in one build (according t export const config = { fileSizes: { - default: true, + default: 'perform', }, languageReloading: { @@ -46,11 +46,19 @@ export const config = { }, mediaValidation: { - default: true, + default: 'perform', + }, + + search: { + default: 'perform', }, thumbs: { - default: true, + default: 'perform', + }, + + webRoutes: { + required: true, }, }; @@ -99,9 +107,7 @@ export function getCLIOptions() { export async function go({ cliOptions, - _dataPath, mediaPath, - mediaCachePath, queueSize, defaultLanguage, @@ -110,9 +116,9 @@ export async function go({ srcRootPath, thumbsCache, urls, + webRoutes, wikiData, - cachebust, developersComment: _developersComment, getSizeOfAdditionalFile, getSizeOfImagePath, @@ -148,12 +154,9 @@ export async function go({ await mkdir(outputPath, {recursive: true}); - await writeSymlinks({ - srcRootPath, - mediaPath, - mediaCachePath, + await writeWebRouteSymlinks({ outputPath, - urls, + webRoutes, }); if (writeAll) { @@ -306,7 +309,6 @@ export async function go({ const bound = bindUtilities({ absoluteTo, - cachebust, defaultLanguage, getSizeOfAdditionalFile, getSizeOfImagePath, @@ -432,42 +434,41 @@ async function writePage({ ].filter(Boolean)); } -function writeSymlinks({ - srcRootPath, - mediaPath, - mediaCachePath, +function writeWebRouteSymlinks({ outputPath, - urls, + webRoutes, }) { - return progressPromiseAll('Writing site symlinks.', [ - link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'), - link(path.join(srcRootPath, 'static'), 'shared.staticRoot'), - link(mediaPath, 'media.root'), - link(mediaCachePath, 'thumb.root'), - ]); - - async function link(directory, urlKey) { - const pathname = urls.from('shared.root').toDevice(urlKey); - const file = path.join(outputPath, pathname); - - try { - await unlink(file); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; + const promises = + webRoutes.map(async route => { + const parts = route.to.split('/'); + const parentDirectoryParts = parts.slice(0, -1); + const symlinkNamePart = parts.at(-1); + + const parentDirectory = path.join(outputPath, ...parentDirectoryParts); + const symlinkPath = path.join(parentDirectory, symlinkNamePart); + + try { + await unlink(symlinkPath); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } } - } - try { - await symlink(path.resolve(directory), file); - } catch (error) { - if (error.code === 'EPERM') { - await symlink(path.resolve(directory), file, 'junction'); - } else { - throw error; + await mkdir(parentDirectory, {recursive: true}); + + try { + await symlink(route.from, symlinkPath); + } catch (error) { + if (error.code === 'EPERM') { + await symlink(route.from, symlinkPath, 'junction'); + } else { + throw error; + } } - } - } + }); + + return progressPromiseAll(`Writing web route symlinks.`, promises); } async function writeFavicon({ |