« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/write')
-rw-r--r--src/write/bind-utilities.js2
-rw-r--r--src/write/build-modes/live-dev-server.js6
-rw-r--r--src/write/build-modes/repl.js6
-rw-r--r--src/write/build-modes/static-build.js126
4 files changed, 131 insertions, 9 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 03ef6049..b018bc1c 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -24,7 +24,7 @@ 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: {
@@ -103,7 +103,6 @@ export async function go({
   webRoutes,
   wikiData,
 
-  cachebust,
   developersComment: _developersComment,
   getSizeOfAdditionalFile,
   getSizeOfImagePath,
@@ -338,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;
     }
 
@@ -399,7 +398,6 @@ export async function go({
 
       const bound = bindUtilities({
         absoluteTo,
-        cachebust,
         defaultLanguage,
         getSizeOfAdditionalFile,
         getSizeOfImagePath,
diff --git a/src/write/build-modes/repl.js b/src/write/build-modes/repl.js
index b300e8e8..faba8a34 100644
--- a/src/write/build-modes/repl.js
+++ b/src/write/build-modes/repl.js
@@ -13,6 +13,10 @@ export const config = {
     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 68cf0949..86e3da0f 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -2,6 +2,7 @@ import * as path from 'node:path';
 
 import {
   copyFile,
+  cp,
   mkdir,
   stat,
   symlink,
@@ -9,6 +10,8 @@ import {
   unlink,
 } from 'node:fs/promises';
 
+import {rimraf} from 'rimraf';
+
 import {quickLoadContentDependencies} from '#content-dependencies';
 import {quickEvaluate} from '#content-function';
 import * as html from '#html';
@@ -49,6 +52,10 @@ export const config = {
     default: 'perform',
   },
 
+  search: {
+    default: 'perform',
+  },
+
   thumbs: {
     default: 'perform',
   },
@@ -115,7 +122,6 @@ export async function go({
   webRoutes,
   wikiData,
 
-  cachebust,
   developersComment: _developersComment,
   getSizeOfAdditionalFile,
   getSizeOfImagePath,
@@ -156,6 +162,11 @@ export async function go({
     webRoutes,
   });
 
+  await writeWebRouteCopies({
+    outputPath,
+    webRoutes,
+  });
+
   if (writeAll) {
     await writeFavicon({
       mediaPath,
@@ -306,7 +317,6 @@ export async function go({
 
         const bound = bindUtilities({
           absoluteTo,
-          cachebust,
           defaultLanguage,
           getSizeOfAdditionalFile,
           getSizeOfImagePath,
@@ -436,8 +446,11 @@ function writeWebRouteSymlinks({
   outputPath,
   webRoutes,
 }) {
+  const symlinkRoutes =
+    webRoutes.filter(route => route.statically === 'symlink');
+
   const promises =
-    webRoutes.map(async route => {
+    symlinkRoutes.map(async route => {
       const parts = route.to.split('/');
       const parentDirectoryParts = parts.slice(0, -1);
       const symlinkNamePart = parts.at(-1);
@@ -469,6 +482,113 @@ function writeWebRouteSymlinks({
   return progressPromiseAll(`Writing web route symlinks.`, promises);
 }
 
+async function writeWebRouteCopies({
+  outputPath,
+  webRoutes,
+}) {
+  const copyRoutes =
+    webRoutes.filter(route => route.statically === 'copy');
+
+  const promises =
+    copyRoutes.map(async route => {
+      const permissionName = '__hsmusic-ok-for-deletion.txt';
+
+      const parts = route.to.split('/');
+      const parentDirectoryParts = parts.slice(0, -1);
+      const copyNamePart = parts.at(-1);
+
+      const parentDirectory = path.join(outputPath, ...parentDirectoryParts);
+      const copyPath = path.join(parentDirectory, copyNamePart);
+
+      // We're going to do a rimraf call! This is freaking terrifying,
+      // so nope out on a couple important conditions.
+
+      let needsDelete;
+      try {
+        await stat(copyPath);
+        needsDelete = true;
+      } catch (error) {
+        if (error.code === 'ENOENT') {
+          needsDelete = false;
+        } else {
+          throw error;
+        }
+      }
+
+      if (needsDelete) {
+        // First remove it directly, in case it's a symlink.
+        try {
+          await unlink(copyPath);
+          needsDelete = false;
+        } catch (error) {
+          // EPERM is POSIX, but libuv may or may not flat-out just raise
+          // the system error (which is ostensibly EISDIR on Linux).
+          // https://github.com/nodejs/node-v0.x-archive/issues/5791
+          // https://man7.org/linux/man-pages/man2/unlink.2.html
+          //
+          // Both of these indidcate "a directory, probably" and we'll
+          // still check for the deletion permission file where we expect
+          // it before actually touching anything.
+          if (error.code !== 'EPERM' && error.code !== 'EISDIR') {
+            throw error;
+          }
+        }
+      }
+
+      if (needsDelete) {
+        // Then check that the deletion permission file exists
+        // where we expect it.
+        try {
+          await stat(path.join(copyPath, permissionName));
+        } catch (error) {
+          if (error.code === 'ENOENT') {
+            throw new Error(`Couldn't find ${permissionName} in ${copyPath} - please delete or move away this folder manually`);
+          } else {
+            throw error;
+          }
+        }
+
+        // And *then* actually delete that directory.
+        await rimraf(copyPath);
+      }
+
+      // Actually copy the source path where it's wanted.
+      await cp(route.from, copyPath, {recursive: true});
+
+      // And certify that it's OK to delete this path, next time around.
+      await writeFile(path.join(copyPath, permissionName),
+        `The presence of this file (by its name, not its contents)\n` +
+        `indicates hsmusic may delete everything contained in this\n` +
+        `directory (the one which directly contains this file, *not*\n` +
+        `any further-up parent directories).\n` +
+        `\n` +
+        `If you make edits, or add any files, they will be deleted or\n` +
+        `overwritten the next time you run the build.\n` +
+        `\n` +
+        `If you delete *this* file, hsmusic will error during the next\n` +
+        `build, and will ask that you delete the containing directory\n` +
+        `yourself.\n`);
+    });
+
+  const results =
+    await Promise.allSettled(promises);
+
+  const errors =
+    results
+      .filter(({status}) => status === 'rejected')
+      .map(({reason}) => reason)
+      .map(err =>
+        (err.message.startsWith(`Couldn't find`)
+          ? err.message
+          : err));
+
+  if (empty(errors)) {
+    logInfo`Wrote web route copies.`;
+  } else {
+    throw new AggregateError(errors, `Errors copying internal files ("web routes")`);
+  }
+}
+
 async function writeFavicon({
   mediaPath,
   outputPath,