diff options
Diffstat (limited to 'src/write/build-modes/live-dev-server.js')
-rw-r--r-- | src/write/build-modes/live-dev-server.js | 203 |
1 files changed, 132 insertions, 71 deletions
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index b018bc1c..ecb9df21 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,11 +1,14 @@ import {spawn} from 'node:child_process'; import * as http from 'node:http'; import {open, stat} from 'node:fs/promises'; +import * as os from 'node:os'; import * as path from 'node:path'; import {pipeline} from 'node:stream/promises'; import {inspect as nodeInspect} from 'node:util'; -import {ENABLE_COLOR, colors, logInfo, logWarn, progressCallAll} from '#cli'; +import {openAggregate} from '#aggregate'; +import {ENABLE_COLOR, colors, fileIssue, logInfo, logWarn, progressCallAll} + from '#cli'; import {watchContentDependencies} from '#content-dependencies'; import {quickEvaluate} from '#content-function'; import * as html from '#html'; @@ -91,21 +94,49 @@ export function getCLIOptions() { }; } +const getContentType = extname => ({ + // BRB covering all my bases + 'aac': 'audio/aac', + 'bmp': 'image/bmp', + 'css': 'text/css', + 'csv': 'text/csv', + 'gif': 'image/gif', + 'ico': 'image/vnd.microsoft.icon', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'js': 'text/javascript', + 'mjs': 'text/javascript', + 'mp3': 'audio/mpeg', + 'mp4': 'video/mp4', + 'oga': 'audio/ogg', + 'ogg': 'audio/ogg', + 'ogv': 'video/ogg', + 'opus': 'audio/opus', + 'png': 'image/png', + 'pdf': 'application/pdf', + 'svg': 'image/svg+xml', + 'ttf': 'font/ttf', + 'txt': 'text/plain', + 'wav': 'audio/wav', + 'weba': 'audio/webm', + 'webm': 'video/webm', + 'woff': 'font/woff', + 'woff2': 'font/woff2', + 'xml': 'application/xml', + 'zip': 'application/zip', +})[extname]; + export async function go({ cliOptions, + universalUtilities, + defaultLanguage, languages, - missingImagePaths, - srcRootPath, - thumbsCache, urls, webRoutes, wikiData, - developersComment: _developersComment, - getSizeOfAdditionalFile, - getSizeOfImagePath, niceShowAggregate, }) { const showError = (error) => { @@ -135,21 +166,49 @@ export async function go({ contentDependenciesWatcher.on('error', () => {}); await new Promise(resolve => contentDependenciesWatcher.once('ready', resolve)); + const commonUtilities = {...universalUtilities}; + + const pathAggregate = openAggregate({message: `Errors computing page paths`}); + let targetSpecPairs = getPageSpecsWithTargets({wikiData}); - const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, + const pages = progressCallAll(`Computing page paths for ${targetSpecPairs.length} targets.`, targetSpecPairs.flatMap(({ pageSpec, target, targetless, }) => () => { - if (targetless) { - const result = pageSpec.pathsTargetless({wikiData}); - return Array.isArray(result) ? result : [result]; - } else { - return pageSpec.pathsForTarget(target); + try { + if (targetless) { + const result = pageSpec.pathsTargetless({wikiData}); + return Array.isArray(result) ? result : [result]; + } else { + return pageSpec.pathsForTarget(target); + } + } catch (caughtError) { + if (targetless) { + pathAggregate.push(new Error( + `Failed to compute targetless paths for ` + + inspect(pageSpec, {compact: true}), + {cause: caughtError})); + } else { + pathAggregate.push(new Error( + `Failed to compute paths for ` + + inspect(target), + {cause: caughtError})); + } + return []; } })).flat(); + try { + pathAggregate.close(); + } catch (error) { + niceShowAggregate(error); + logWarn`Failed to compute page paths for some targets.`; + logWarn`This means some pages that normally exist will be 404s.`; + fileIssue(); + } + logInfo`Will be serving a total of ${pages.length} pages.`; const urlToPageMap = Object.fromEntries(pages @@ -266,38 +325,7 @@ export async function go({ } const extname = path.extname(safePath).slice(1).toLowerCase(); - - const contentType = { - // BRB covering all my bases - 'aac': 'audio/aac', - 'bmp': 'image/bmp', - 'css': 'text/css', - 'csv': 'text/csv', - 'gif': 'image/gif', - 'ico': 'image/vnd.microsoft.icon', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'js': 'text/javascript', - 'mjs': 'text/javascript', - 'mp3': 'audio/mpeg', - 'mp4': 'video/mp4', - 'oga': 'audio/ogg', - 'ogg': 'audio/ogg', - 'ogv': 'video/ogg', - 'opus': 'audio/opus', - 'png': 'image/png', - 'pdf': 'application/pdf', - 'svg': 'image/svg+xml', - 'ttf': 'font/ttf', - 'txt': 'text/plain', - 'wav': 'audio/wav', - 'weba': 'audio/webm', - 'webm': 'video/webm', - 'woff': 'font/woff', - 'woff2': 'font/woff2', - 'xml': 'application/xml', - 'zip': 'application/zip', - }[extname]; + const contentType = getContentType(extname); let fd, size; try { @@ -322,7 +350,16 @@ export async function go({ 'Content-Length': size, }); - await pipeline(fd.createReadStream(), response); + try { + await pipeline(fd.createReadStream(), response); + } catch (error) { + if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') { + // Connection was dropped, this is OK. + return; + } else { + throw error; + } + } if (loudResponses) console.log(`${requestHead} [200] ${pathname} (${colors.magenta(`web route`)})`); @@ -331,10 +368,33 @@ export async function go({ // Other routes determined by page and URL specs + const startTiming = () => { + if (!showTimings) { + return () => ''; + } + + const timeStart = Date.now(); + + return () => { + const timeEnd = Date.now(); + const timeDelta = timeEnd - timeStart; + + if (timeDelta > 100) { + return `${(timeDelta / 1000).toFixed(2)}s`; + } else { + return `${timeDelta}ms`; + } + }; + }; + // URL to page map expects trailing slash but no leading slash. const pathnameKey = pathname.replace(/^\//, '') + (pathname.endsWith('/') ? '' : '/'); - if (!Object.hasOwn(urlToPageMap, pathnameKey)) { + const is404 = + !Object.hasOwn(urlToPageMap, pathnameKey) || + !(urlToPageMap[pathnameKey].page.condition?.() ?? true); + + if (is404) { response.writeHead(404, contentTypePlain); response.end(`No page found for: ${pathnameKey}\n`); if (loudResponses) console.log(`${requestHead} [404] ${pathname} (no page)`); @@ -394,21 +454,16 @@ export async function go({ return; } - const timeStart = Date.now(); + const timing = startTiming(); const bound = bindUtilities({ + ...commonUtilities, + absoluteTo, - defaultLanguage, - getSizeOfAdditionalFile, - getSizeOfImagePath, language, - languages, - missingImagePaths, pagePath: servePath, - thumbsCache, - to, - urls, - wikiData, + pagePathStringFromRoot: pathname.replace(/^\//, ''), + to: page.absoluteLinks ? absoluteTo : to, }); const topLevelResult = @@ -422,18 +477,10 @@ export async function go({ const {pageHTML} = html.resolve(topLevelResult); - const timeEnd = Date.now(); - const timeDelta = timeEnd - timeStart; - - if (showTimings) { - const timeString = - (timeDelta > 100 - ? `${(timeDelta / 1000).toFixed(2)}s` - : `${timeDelta}ms`); - - console.log(`${requestHead} [200, ${timeString}] ${pathname} (${colors.blue(`page`)})`); - } else if (loudResponses) { - console.log(`${requestHead} [200] ${pathname} (${colors.blue(`page`)})`); + const timeString = timing(); + const status = (timeString ? `200 ${timeString}` : `200`); + if (showTimings || loudResponses) { + console.log(`${requestHead} [${status}] ${pathname} (${colors.blue(`page`)})`); } response.writeHead(200, contentTypeHTML); @@ -446,7 +493,13 @@ export async function go({ } }); - const address = `http://${host}:${port}/`; + const addresses = + (host === '0.0.0.0' + ? [`http://localhost:${port}/`, + `http://${os.hostname()}:${port}/`] + : host === '127.0.0.1' + ? [`http://localhost:${port}/`] + : [`http://${host}:${port}/`]); server.on('error', error => { if (error.code === 'EADDRINUSE') { @@ -463,7 +516,15 @@ export async function go({ }); server.on('listening', () => { - logInfo`${'All done!'} Listening at: ${address}`; + if (addresses.length === 1) { + logInfo`${'All done!'} Listening at: ${addresses[0]}`; + } else { + logInfo`${`All done!`} Listening at:`; + for (const address of addresses) { + logInfo`- ${address}`; + } + } + logInfo`Press ^C here (control+C) to stop the server and exit.`; if (showTimings && loudResponses) { logInfo`Printing all HTTP responses, plus page generation timings.`; |