« get me outta code hell

most essential behavior for live-dev-server - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/write
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-01-09 21:07:16 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-01-09 21:07:16 -0400
commit594e8dd46f9e6cc74c680536a1d820eef27133f0 (patch)
treeb4dfeda6e32dfb09df6d3c6c52cccf88bf824005 /src/write
parent154c050db72ebf06d4514326c696ab43fb3f8dc8 (diff)
most essential behavior for live-dev-server
Diffstat (limited to 'src/write')
-rw-r--r--src/write/bind-utilities.js1
-rw-r--r--src/write/build-modes/live-dev-server.js287
-rw-r--r--src/write/build-modes/static-build.js28
-rw-r--r--src/write/page-template.js21
4 files changed, 303 insertions, 34 deletions
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(/^\/(?<area>static|media)\/(?<path>.*)/)?.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'),