From 594e8dd46f9e6cc74c680536a1d820eef27133f0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Jan 2023 21:07:16 -0400 Subject: most essential behavior for live-dev-server --- src/misc-templates.js | 43 ++--- src/util/urls.js | 56 +++--- src/write/bind-utilities.js | 1 + src/write/build-modes/live-dev-server.js | 287 +++++++++++++++++++++++++++++-- src/write/build-modes/static-build.js | 28 +-- src/write/page-template.js | 21 ++- 6 files changed, 355 insertions(+), 81 deletions(-) diff --git a/src/misc-templates.js b/src/misc-templates.js index 9a1bbf50..bccb8831 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -971,46 +971,37 @@ function unbound_generateStickyHeadingContainer({ function unbound_getFooterLocalizationLinks(pathname, { html, + defaultLanguage, language, + languages, to, - paths, - defaultLanguage, - languages, + pageSubKey, + urlArgs, }) { - const {urlPath} = paths; - const keySuffix = urlPath[0].replace(/^localized\./, '.'); - const toArgs = urlPath.slice(1); - const links = Object.entries(languages) .filter(([code, language]) => code !== 'default' && !language.hidden) .map(([code, language]) => language) .sort(({name: a}, {name: b}) => (a < b ? -1 : a > b ? 1 : 0)) .map((language) => - html.tag( - 'span', - html.tag( - 'a', + html.tag('span', + html.tag('a', { href: language === defaultLanguage - ? to('localizedDefaultLanguage' + keySuffix, ...toArgs) + ? to( + 'localizedDefaultLanguage.' + pageSubKey, + ...urlArgs) : to( - 'localizedWithBaseDirectory' + keySuffix, - language.code, - ...toArgs - ), + 'localizedWithBaseDirectory.' + pageSubKey, + language.code, ...urlArgs), }, - language.name - ) - ) - ); - - return html.tag( - 'div', - {class: 'footer-localization-links'}, - language.$('misc.uiLanguage', {languages: links.join('\n')}) - ); + language.name))); + + return html.tag('div', {class: 'footer-localization-links'}, + language.$('misc.uiLanguage', { + languages: links.join('\n'), + })); } // Exports diff --git a/src/util/urls.js b/src/util/urls.js index f05f134b..69ff1d7e 100644 --- a/src/util/urls.js +++ b/src/util/urls.js @@ -142,11 +142,11 @@ export function getURLsFrom({ baseDirectory, pageSubKey, - paths, + subdirectoryPrefix, }) { return (targetFullKey, ...args) => { const [groupKey, subKey] = targetFullKey.split('.'); - let path = paths.subdirectoryPrefix; + let path = subdirectoryPrefix; let from; let to; @@ -184,32 +184,47 @@ export function getURLsFrom({ }; } -export function getPagePaths({ - outputPath, +export function getPagePathname({ + baseDirectory, + fullKey, + urlArgs, urls, +}) { + const [groupKey, subKey] = fullKey.split('.'); + return (groupKey === 'localized' && baseDirectory + ? urls + .from('shared.root') + .toDevice( + 'localizedWithBaseDirectory.' + subKey, + baseDirectory, + ...urlArgs) + : urls + .from('shared.root') + .toDevice(fullKey, ...urlArgs)); +} + +// Needed for the rare path arguments which themselves contains one or more +// slashes, e.g. for listings, with arguments like 'albums/by-name'. +export function getPageSubdirectoryPrefix({urlArgs}) { + return '../'.repeat(urlArgs.join('/').split('/').length - 1); +} + +export function getPagePaths({ baseDirectory, fullKey, + outputPath, urlArgs, + urls, }) { const [groupKey, subKey] = fullKey.split('.'); - const pathname = - groupKey === 'localized' && baseDirectory - ? urls - .from('shared.root') - .toDevice( - 'localizedWithBaseDirectory.' + subKey, - baseDirectory, - ...urlArgs) - : urls - .from('shared.root') - .toDevice(fullKey, ...urlArgs); - - // Needed for the rare path arguments which themselves contains one or more - // slashes, e.g. for listings, with arguments like 'albums/by-name'. - const subdirectoryPrefix = - '../'.repeat(urlArgs.join('/').split('/').length - 1); + const pathname = getPagePathname({ + baseDirectory, + fullKey, + urlArgs, + urls, + }); const outputDirectory = path.join(outputPath, pathname); @@ -224,6 +239,5 @@ export function getPagePaths({ output, pathname, - subdirectoryPrefix, }; } diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 6632ba30..1c4dd282 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -61,6 +61,7 @@ export function bindUtilities({ const bound = {}; bound.html = html; + bound.language = language; bound.img = bindOpts(img, { [bindOpts.bindIndex]: 0, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index c3094712..80badeb9 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,34 +1,55 @@ +import * as http from 'http'; +import {createReadStream} from 'fs'; +import {stat} from 'fs/promises'; +import * as path from 'path'; +import {pipeline} from 'stream/promises' + +import {bindUtilities} from '../bind-utilities.js'; + +import {serializeThings} from '../../data/serialize.js'; + import * as pageSpecs from '../../page/index.js'; +import {logInfo, progressCallAll} from '../../util/cli.js'; +import {withEntries} from '../../util/sugar.js'; + +import { + getPagePathname, + getPageSubdirectoryPrefix, + getURLsFrom, +} from '../../util/urls.js'; + import { - logInfo, - progressCallAll, -} from '../../util/cli.js'; + generateDocumentHTML, + generateGlobalWikiDataJSON, + generateRedirectHTML, +} from '../page-template.js'; export function getCLIOptions() { - // Stub. return {}; } export async function go({ _cliOptions, _dataPath, - _mediaPath, + mediaPath, _queueSize, - _defaultLanguage, - _languages, - _srcRootPath, - _urls, + defaultLanguage, + languages, + srcRootPath, + urls, _urlSpec, wikiData, - _cachebust, - _developersComment, - _getSizeOfAdditionalFile, + cachebust, + developersComment, + getSizeOfAdditionalFile, }) { + const port = 8002; + let targetSpecPairs = getPageSpecsWithTargets({wikiData}); - const writes = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, + const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, targetSpecPairs.map(({ pageSpec, target, @@ -38,7 +59,245 @@ export async function go({ ? pageSpec.writeTargetless({wikiData}) : pageSpec.write(target, {wikiData}))).flat(); - logInfo`Will be serving a total of ${writes.length} pages.`; + logInfo`Will be serving a total of ${pages.length} pages.`; + + const urlToPageMap = Object.fromEntries(pages + .filter(page => page.type === 'page' || page.type === 'redirect') + .flatMap(page => { + let servePath; + if (page.type === 'page') + servePath = page.path; + else if (page.type === 'redirect') + servePath = page.fromPath; + + const fullKey = 'localized.' + servePath[0]; + const urlArgs = servePath.slice(1); + + return Object.values(languages).map(language => { + const baseDirectory = + language === defaultLanguage ? '' : language.code; + + const pathname = getPagePathname({ + baseDirectory, + fullKey, + urlArgs, + urls, + }); + + return [pathname, { + baseDirectory, + language, + page, + servePath, + }]; + }); + })); + + const server = http.createServer(async (request, response) => { + const contentTypeHTML = {'Content-Type': 'text/html; charset=utf-8'}; + const contentTypeJSON = {'Content-Type': 'text/json; charset=utf-8'}; + const contentTypePlain = {'Content-Type': 'text/plain; charset=utf-8'}; + + const requestTime = new Date().toLocaleDateString('en-US', {hour: '2-digit', minute: '2-digit', second: '2-digit'}); + const requestHead = `${requestTime} - ${request.socket.remoteAddress}`; + + let url; + try { + url = new URL(request.url, `http://${request.headers.host}`); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end('Failed to parse request URL\n'); + return; + } + + const {pathname} = url; + + // Specialized routes + + if (pathname === '/data.json') { + response.writeHead(200, contentTypeJSON); + response.end(generateGlobalWikiDataJSON({ + serializeThings, + wikiData, + })); + return; + } + + const { + area: localFileArea, + path: localFilePath + } = pathname.match(/^\/(?static|media)\/(?.*)/)?.groups ?? {}; + + if (localFileArea) { + // Not security tested, man, this is a dev server!! + const safePath = path.resolve('/', localFilePath).replace(/^\//, ''); + + let localDirectory; + if (localFileArea === 'static') { + localDirectory = path.join(srcRootPath, 'static'); + } else if (localFileArea === 'media') { + localDirectory = mediaPath; + } + + const filePath = path.resolve(localDirectory, safePath); + + try { + await stat(filePath); + } catch (error) { + if (error.code === 'ENOENT') { + response.writeHead(404, contentTypePlain); + response.end(`No ${localFileArea} file 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}`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + } + return; + } + + try { + response.writeHead(200); // Sorry, no MIME type for now + await pipeline( + createReadStream(filePath), + response); + console.log(`${requestHead} [200] ${pathname}`); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end(`Failed during file-to-response pipeline`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + return; + } + } + + // Other routes determined by page and URL specs + + // URL to page map expects trailing slash but no leading slash. + const pathnameKey = pathname.replace(/^\//, '') + (pathname.endsWith('/') ? '' : '/'); + + if (!Object.hasOwn(urlToPageMap, pathnameKey)) { + response.writeHead(404, contentTypePlain); + response.end(`No page found for: ${pathnameKey}\n`); + console.log(`${requestHead} [404] ${pathname}`); + return; + } + + const { + baseDirectory, + language, + page, + servePath, + } = urlToPageMap[pathnameKey]; + + const to = getURLsFrom({ + urls, + baseDirectory, + pageSubKey: servePath[0], + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: servePath.slice(1), + }), + }); + + const absoluteTo = (targetFullKey, ...args) => { + const [groupKey, subKey] = targetFullKey.split('.'); + const from = urls.from('shared.root'); + return ( + '/' + + (groupKey === 'localized' && baseDirectory + ? from.to( + 'localizedWithBaseDirectory.' + subKey, + baseDirectory, + ...args + ) + : from.to(targetFullKey, ...args)) + ); + }; + + try { + const pageSubKey = servePath[0]; + const urlArgs = servePath.slice(1); + + if (page.type === 'redirect') { + response.writeHead(301, contentTypeHTML); + + const target = to('localized.' + page.toPath[0], ...page.toPath.slice(1)); + const redirectHTML = generateRedirectHTML(page.title, target, {language}); + + response.end(redirectHTML); + + console.log(`${requestHead} [301] (redirect) ${pathname}`); + return; + } + + response.writeHead(200, contentTypeHTML); + + const localizedPathnames = withEntries(languages, entries => entries + .filter(([key, language]) => key !== 'default' && !language.hidden) + .map(([_key, language]) => [ + language.code, + getPagePathname({ + baseDirectory: + (language === defaultLanguage + ? '' + : language.code), + fullKey: 'localized.' + pageSubKey, + urlArgs, + urls, + }), + ])); + + const bound = bindUtilities({ + language, + to, + wikiData, + }); + + const pageInfo = page.page({ + ...bound, + + absoluteTo, + relativeTo: to, + to, + urls, + + getSizeOfAdditionalFile, + }); + + const pageHTML = generateDocumentHTML(pageInfo, { + cachebust, + defaultLanguage, + developersComment, + getThemeString: bound.getThemeString, + language, + languages, + localizedPathnames, + oEmbedJSONHref: null, // No oEmbed support for live dev server + pageSubKey, + pathname, + urlArgs, + to, + transformMultiline: bound.transformMultiline, + wikiData, + }); + + console.log(`${requestHead} [200] ${pathname}`); + response.end(pageHTML); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end(`Error generating page, view server log for details\n`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + } + }); + + server.listen(port); + logInfo`${'All done!'} Listening at ${`http://0.0.0.0:${port}/`}`; + + // Just keep going... forever!!! + await new Promise(() => {}); return true; } diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index b3700c43..1544a122 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -16,7 +16,6 @@ import * as pageSpecs from '../../page/index.js'; import link from '../../util/link.js'; import {empty, queue, withEntries} from '../../util/sugar.js'; -import {getPagePaths, getURLsFrom} from '../../util/urls.js'; import { logError, @@ -26,6 +25,13 @@ import { progressPromiseAll, } from '../../util/cli.js'; +import { + getPagePathname, + getPagePaths, + getPageSubdirectoryPrefix, + getURLsFrom, +} from '../../util/urls.js'; + const pageFlags = Object.keys(pageSpecs); export function getCLIOptions() { @@ -263,20 +269,18 @@ export async function go({ const pageSubKey = path[0]; const urlArgs = path.slice(1); - const localizedPaths = withEntries(languages, entries => entries + const localizedPathnames = withEntries(languages, entries => entries .filter(([key, language]) => key !== 'default' && !language.hidden) .map(([_key, language]) => [ language.code, - getPagePaths({ - outputPath, - urls, - + getPagePathname({ baseDirectory: (language === defaultLanguage ? '' : language.code), fullKey: 'localized.' + pageSubKey, urlArgs, + urls, }), ])); @@ -293,7 +297,9 @@ export async function go({ urls, baseDirectory, pageSubKey, - paths, + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: page.path.slice(1), + }), }); const absoluteTo = (targetFullKey, ...args) => { @@ -320,8 +326,6 @@ export async function go({ const pageInfo = page({ ...bound, - language, - absoluteTo, relativeTo: to, to, @@ -350,7 +354,7 @@ export async function go({ getThemeString: bound.getThemeString, language, languages, - localizedPaths, + localizedPathnames, oEmbedJSONHref, paths, to, @@ -382,7 +386,9 @@ export async function go({ urls, baseDirectory, pageSubKey: fromPath[0], - paths: from, + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: fromPath.slice(1), + }), }); const target = to('localized.' + toPath[0], ...toPath.slice(1)); diff --git a/src/write/page-template.js b/src/write/page-template.js index efbc5795..88d81c23 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -53,11 +53,13 @@ export function generateDocumentHTML(pageInfo, { getThemeString, language, languages, - localizedPaths, - paths, + localizedPathnames, oEmbedJSONHref, + pageSubKey, + pathname, to, transformMultiline, + urlArgs, wikiData, }) { const {wikiInfo} = wikiData; @@ -125,11 +127,11 @@ export function generateDocumentHTML(pageInfo, { : null; const canonical = wikiInfo.canonicalBase - ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname) + ? wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname) : ''; const localizedCanonical = wikiInfo.canonicalBase - ? Object.entries(localizedPaths).map(([code, {pathname}]) => ({ + ? Object.entries(localizedPathnames).map(([code, pathname]) => ({ lang: code, href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname), })) @@ -162,13 +164,14 @@ export function generateDocumentHTML(pageInfo, { }, footer.content), - getFooterLocalizationLinks(paths.pathname, { + getFooterLocalizationLinks(pathname, { defaultLanguage, html, language, languages, - paths, + pageSubKey, to, + urlArgs, }), ]); @@ -264,7 +267,7 @@ export function generateDocumentHTML(pageInfo, { ? to(...cur.path) : cur.href ? (() => { - logWarn`Using legacy href format nav link in ${paths.pathname}`; + logWarn`Using legacy href format nav link in ${pathname}`; return cur.href; })() : null, @@ -447,9 +450,9 @@ export function generateDocumentHTML(pageInfo, { { lang: language.intlCode, 'data-language-code': language.code, - 'data-url-key': paths.urlPath[0], + 'data-url-key': 'localized.' + pageSubKey, ...Object.fromEntries( - paths.urlPath.slice(1).map((v, i) => [['data-url-value' + i], v]) + urlArgs.map((v, i) => [['data-url-value' + i], v]) ), 'data-rebase-localized': to('localized.root'), 'data-rebase-shared': to('shared.root'), -- cgit 1.3.0-6-gf8a5