« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/write/build-modes
diff options
context:
space:
mode:
Diffstat (limited to 'src/write/build-modes')
-rw-r--r--src/write/build-modes/live-dev-server.js169
1 files changed, 147 insertions, 22 deletions
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 6dfa7d71..10b40cf0 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -11,7 +11,7 @@ import {serializeThings} from '../../data/serialize.js';
 import * as pageSpecs from '../../page/index.js';
 
 import {logInfo, logWarn, progressCallAll} from '../../util/cli.js';
-
+import {empty} from '../../util/sugar.js';
 import {
   getPagePathname,
   getPagePathnameAcrossLanguages,
@@ -20,11 +20,21 @@ import {
 } from '../../util/urls.js';
 
 import {
-  generateDocumentHTML,
   generateGlobalWikiDataJSON,
   generateRedirectHTML,
 } from '../page-template.js';
 
+import {
+  watchContentDependencies,
+} from '../../content/dependencies/index.js';
+
+import {
+  fillRelationsLayoutFromSlotResults,
+  flattenRelationsTree,
+  getRelationsTree,
+  getNeededContentDependencyNames,
+} from '../../content-function.js';
+
 const defaultHost = '0.0.0.0';
 const defaultPort = 8002;
 
@@ -64,20 +74,35 @@ export async function go({
   developersComment,
   getSizeOfAdditionalFile,
   getSizeOfImageFile,
+  niceShowAggregate,
 }) {
+  const showError = (error) => {
+    if (error instanceof AggregateError && niceShowAggregate) {
+      niceShowAggregate(error);
+    } else {
+      console.error(error);
+    }
+  };
+
   const host = cliOptions['host'] ?? defaultHost;
   const port = parseInt(cliOptions['port'] ?? defaultPort);
 
+  const contentDependenciesWatcher = await watchContentDependencies();
+  const {contentDependencies: allContentDependencies} = contentDependenciesWatcher;
+
+  contentDependenciesWatcher.on('error', () => {});
+  await new Promise(resolve => contentDependenciesWatcher.once('ready', resolve));
+
   let targetSpecPairs = getPageSpecsWithTargets({wikiData});
   const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`,
-    targetSpecPairs.map(({
+    targetSpecPairs.flatMap(({
       pageSpec,
       target,
       targetless,
     }) => () =>
       targetless
-        ? pageSpec.writeTargetless({wikiData})
-        : pageSpec.write(target, {wikiData}))).flat();
+        ? [pageSpec.writeTargetless({wikiData})]
+        : pageSpec.pathsForTarget(target))).flat();
 
   logInfo`Will be serving a total of ${pages.length} pages.`;
 
@@ -143,7 +168,7 @@ export async function go({
         response.writeHead(500, contentTypeJSON);
         response.end({error: `Internal error serializing wiki JSON`});
         console.error(`${requestHead} [500] /data.json`);
-        console.error(error);
+        showError(error);
       }
       return;
     }
@@ -186,7 +211,7 @@ export async function go({
           response.writeHead(500, contentTypePlain);
           response.end(`Internal error accessing ${localFileArea} file for: ${safePath}`);
           console.error(`${requestHead} [500] ${pathname}`);
-          console.error(error);
+          showError(error);
         }
         return;
       }
@@ -239,7 +264,7 @@ export async function go({
         response.writeHead(500, contentTypePlain);
         response.end(`Failed during file-to-response pipeline`);
         console.error(`${requestHead} [500] ${pathname}`);
-        console.error(error);
+        showError(error);
       }
       return;
     }
@@ -305,8 +330,6 @@ export async function go({
         return;
       }
 
-      response.writeHead(200, contentTypeHTML);
-
       const localizedPathnames = getPagePathnameAcrossLanguages({
         defaultLanguage,
         languages,
@@ -314,37 +337,139 @@ export async function go({
         urls,
       });
 
+      const {name, args} = page.contentFunction;
+
       const bound = bindUtilities({
         absoluteTo,
+        cachebust,
         defaultLanguage,
         getSizeOfAdditionalFile,
         getSizeOfImageFile,
         language,
         languages,
+        pagePath: servePath,
         to,
         urls,
         wikiData,
       });
 
-      const pageInfo = page.page(bound);
-
-      const pageHTML = generateDocumentHTML(pageInfo, {
+      const allExtraDependencies = {
         ...bound,
-        cachebust,
-        developersComment,
-        localizedPathnames,
-        oEmbedJSONHref: null, // No oEmbed support for live dev server
-        pagePath: servePath,
-        pathname,
-      });
+
+        appendIndexHTML: false,
+        transformMultiline: text => `<p>${text}</p>`,
+      };
+
+      // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM)
+
+      const treeInfo = getRelationsTree(allContentDependencies, name, wikiData, ...args);
+      const flatTreeInfo = flattenRelationsTree(treeInfo);
+      const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
+
+      const neededContentDependencyNames =
+        getNeededContentDependencyNames(allContentDependencies, name);
+
+      // Content functions aren't recursive, so by following the set above
+      // sequentually, we will always provide fulfilled content functions as the
+      // dependencies for later content functions.
+      const fulfilledContentDependencies = {};
+      for (const name of neededContentDependencyNames) {
+        const unfulfilledContentFunction = allContentDependencies[name];
+        if (!unfulfilledContentFunction) continue;
+
+        const {contentDependencies, extraDependencies} = unfulfilledContentFunction;
+
+        if (empty(contentDependencies) && empty(extraDependencies)) {
+          fulfilledContentDependencies[name] = unfulfilledContentFunction;
+          continue;
+        }
+
+        const fulfillments = {};
+
+        for (const dependencyName of contentDependencies ?? []) {
+          if (dependencyName in fulfilledContentDependencies) {
+            fulfillments[dependencyName] =
+              fulfilledContentDependencies[dependencyName];
+          }
+        }
+
+        for (const dependencyName of extraDependencies ?? []) {
+          if (dependencyName in allExtraDependencies) {
+            fulfillments[dependencyName] =
+              allExtraDependencies[dependencyName];
+          }
+        }
+
+        fulfilledContentDependencies[name] =
+          unfulfilledContentFunction.fulfill(fulfillments);
+      }
+
+      // There might still be unfulfilled content functions if dependencies weren't
+      // provided as part of allContentDependencies or allExtraDependencies.
+      // Catch and report these early, together in an aggregate error.
+      const unfulfilledErrors = [];
+      const unfulfilledNames = [];
+      for (const name of neededContentDependencyNames) {
+        const contentFunction = fulfilledContentDependencies[name];
+        if (!contentFunction) continue;
+        if (!contentFunction.fulfilled) {
+          try {
+            contentFunction();
+          } catch (error) {
+            error.message = `(${name}) ${error.message}`;
+            unfulfilledErrors.push(error);
+            unfulfilledNames.push(name);
+          }
+        }
+      }
+
+      if (!empty(unfulfilledErrors)) {
+        throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled (${unfulfilledNames.join(', ')})`);
+      }
+
+      const slotResults = {};
+
+      function runContentFunction({name, args, relations: flatRelations}) {
+        const contentFunction = fulfilledContentDependencies[name];
+
+        if (!contentFunction) {
+          throw new Error(`Content function ${name} unfulfilled or not listed`);
+        }
+
+        const sprawl =
+          contentFunction.sprawl?.(allExtraDependencies.wikiData, ...args);
+
+        const relations =
+          fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, flatRelations);
+
+        const data =
+          (sprawl
+            ? contentFunction.data?.(sprawl, ...args)
+            : contentFunction.data?.(...args));
+
+        const generateArgs = [data, relations].filter(Boolean);
+
+        return contentFunction(...generateArgs);
+      }
+
+      for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) {
+        slotResults[slot] = runContentFunction(flatRelationSlots[slot]);
+      }
+
+      const topLevelResult = runContentFunction(root);
+
+      // END PASTE
+
+      const pageHTML = topLevelResult.toString();
 
       console.log(`${requestHead} [200] ${pathname}`);
+      response.writeHead(200, contentTypeHTML);
       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);
+      showError(error);
     }
   });
 
@@ -360,7 +485,7 @@ export async function go({
       }, 10_000);
     } else {
       console.error(`Server error detected (code: ${error.code})`);
-      console.error(error);
+      showError(error);
     }
   });