« 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/build-modes/live-dev-server.js107
-rw-r--r--src/write/build-modes/repl.js2
-rw-r--r--src/write/build-modes/static-build.js81
3 files changed, 103 insertions, 87 deletions
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index a2a84a8..03ef604 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -1,10 +1,11 @@
 import {spawn} from 'node:child_process';
 import * as http from 'node:http';
-import {readFile, stat} from 'node:fs/promises';
+import {open, stat} from 'node:fs/promises';
 import * as path from 'node:path';
+import {pipeline} from 'node:stream/promises';
 import {inspect as nodeInspect} from 'node:util';
 
-import {ENABLE_COLOR, logInfo, logWarn, progressCallAll} from '#cli';
+import {ENABLE_COLOR, colors, logInfo, logWarn, progressCallAll} from '#cli';
 import {watchContentDependencies} from '#content-dependencies';
 import {quickEvaluate} from '#content-function';
 import * as html from '#html';
@@ -27,19 +28,23 @@ export const description = `Hosts a local HTTP server which generates page conte
 
 export const config = {
   fileSizes: {
-    default: true,
+    default: 'perform',
   },
 
   languageReloading: {
-    default: true,
+    default: 'perform',
   },
 
   mediaValidation: {
-    default: true,
+    default: 'perform',
   },
 
   thumbs: {
-    default: true,
+    default: 'perform',
+  },
+
+  webRoutes: {
+    required: true,
   },
 };
 
@@ -88,9 +93,6 @@ export function getCLIOptions() {
 
 export async function go({
   cliOptions,
-  _dataPath,
-  mediaPath,
-  mediaCachePath,
 
   defaultLanguage,
   languages,
@@ -98,6 +100,7 @@ export async function go({
   srcRootPath,
   thumbsCache,
   urls,
+  webRoutes,
   wikiData,
 
   cachebust,
@@ -211,7 +214,7 @@ export async function go({
 
         response.writeHead(200, contentTypeJSON);
         response.end(json);
-        if (loudResponses) console.log(`${requestHead} [200] ${pathname}`);
+        if (loudResponses) console.log(`${requestHead} [200] ${pathname} (${colors.yellow(`special`)})`);
       } catch (error) {
         response.writeHead(500, contentTypeJSON);
         response.end(`Internal error serializing wiki JSON`);
@@ -221,30 +224,27 @@ export async function go({
       return;
     }
 
-    const {
-      area: localFileArea,
-      path: localFilePath
-    } = pathname.match(/^\/(?<area>static|util|media|thumb)\/(?<path>.*)/)?.groups ?? {};
+    const matchedWebRoute =
+      webRoutes
+        .find(({to}) => pathname.startsWith('/' + to));
+
+    if (matchedWebRoute) {
+      const localFilePath = pathname.slice(1 + matchedWebRoute.to.length);
 
-    if (localFileArea) {
       // Not security tested, man, this is a dev server!!
-      const safePath = path.posix.resolve('/', localFilePath).replace(/^\//, '');
-
-      let localDirectory;
-      if (localFileArea === 'static' || localFileArea === 'util') {
-        localDirectory = path.join(srcRootPath, localFileArea);
-      } else if (localFileArea === 'media') {
-        localDirectory = mediaPath;
-      } else if (localFileArea === 'thumb') {
-        localDirectory = mediaCachePath;
-      }
+      const safePath =
+        path.posix
+          .resolve('/', localFilePath)
+          .replace(/^\//, '');
+
+      const localDirectory = matchedWebRoute.from;
 
       let filePath;
       try {
         filePath = path.resolve(localDirectory, decodeURI(safePath.split('/').join(path.sep)));
       } catch (error) {
         response.writeHead(404, contentTypePlain);
-        response.end(`No ${localFileArea} file found for: ${safePath}`);
+        response.end(`File not found for: ${safePath}`);
         console.log(`${requestHead} [404] ${pathname}`);
         console.log(`Failed to decode request pathname`);
       }
@@ -254,12 +254,12 @@ export async function go({
       } catch (error) {
         if (error.code === 'ENOENT') {
           response.writeHead(404, contentTypePlain);
-          response.end(`No ${localFileArea} file found for: ${safePath}`);
+          response.end(`File not 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}`);
+          response.end(`Internal error accessing file for: ${safePath}`);
           console.error(`${requestHead} [500] ${pathname}`);
           showError(error);
         }
@@ -300,21 +300,33 @@ export async function go({
         'zip': 'application/zip',
       }[extname];
 
+      let fd, size;
       try {
-        const {size} = await stat(filePath);
-        const buffer = await readFile(filePath)
-        response.writeHead(200, contentType ? {
-          'Content-Type': contentType,
-          'Content-Length': size,
-        } : {});
-        response.end(buffer);
-        if (loudResponses) console.log(`${requestHead} [200] ${pathname}`);
+        ({size} = await stat(filePath));
+        fd = await open(filePath);
       } catch (error) {
-        response.writeHead(500, contentTypePlain);
-        response.end(`Failed during file-to-response pipeline`);
-        console.error(`${requestHead} [500] ${pathname}`);
-        showError(error);
+        if (error.code === 'EISDIR') {
+          response.writeHead(404, contentTypePlain);
+          response.end(`File not found for: ${safePath}`);
+          console.error(`${requestHead} [404] ${pathname} (is directory)`);
+        } else {
+          response.writeHead(500, contentTypePlain);
+          response.end(`Failed during file-to-response pipeline`);
+          console.error(`${requestHead} [500] ${pathname}`);
+          showError(error);
+        }
+        return;
       }
+
+      response.writeHead(200, {
+        ...contentType ? {'Content-Type': contentType} : {},
+        'Content-Length': size,
+      });
+
+      await pipeline(fd.createReadStream(), response);
+
+      if (loudResponses) console.log(`${requestHead} [200] ${pathname} (${colors.magenta(`web route`)})`);
+
       return;
     }
 
@@ -421,9 +433,9 @@ export async function go({
             ? `${(timeDelta / 1000).toFixed(2)}s`
             : `${timeDelta}ms`);
 
-        console.log(`${requestHead} [200, ${timeString}] ${pathname}`);
+        console.log(`${requestHead} [200, ${timeString}] ${pathname} (${colors.blue(`page`)})`);
       } else if (loudResponses) {
-        console.log(`${requestHead} [200] ${pathname}`);
+        console.log(`${requestHead} [200] ${pathname} (${colors.blue(`page`)})`);
       }
 
       response.writeHead(200, contentTypeHTML);
@@ -474,10 +486,15 @@ export async function go({
   if (skipServing) {
     logInfo`Ready to serve! But --skip-serving was passed, so all done.`;
   } else {
-    server.listen(port, host);
+    process.on('SIGINT', () => {
+      process.stdout.write('\n');
+      server.close();
+    });
 
-    // Just keep going... forever!!!
-    await new Promise(() => {});
+    await new Promise(resolve => {
+      server.listen(port, host);
+      server.on('close', () => resolve());
+    });
   }
 
   return true;
diff --git a/src/write/build-modes/repl.js b/src/write/build-modes/repl.js
index 2098559..b300e8e 100644
--- a/src/write/build-modes/repl.js
+++ b/src/write/build-modes/repl.js
@@ -6,7 +6,7 @@ export const config = {
   },
 
   languageReloading: {
-    default: true,
+    default: 'perform',
   },
 
   mediaValidation: {
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index a355a00..68cf094 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -38,7 +38,7 @@ export const description = `Generates all page content in one build (according t
 
 export const config = {
   fileSizes: {
-    default: true,
+    default: 'perform',
   },
 
   languageReloading: {
@@ -46,11 +46,15 @@ export const config = {
   },
 
   mediaValidation: {
-    default: true,
+    default: 'perform',
   },
 
   thumbs: {
-    default: true,
+    default: 'perform',
+  },
+
+  webRoutes: {
+    required: true,
   },
 };
 
@@ -99,9 +103,7 @@ export function getCLIOptions() {
 
 export async function go({
   cliOptions,
-  _dataPath,
   mediaPath,
-  mediaCachePath,
   queueSize,
 
   defaultLanguage,
@@ -110,6 +112,7 @@ export async function go({
   srcRootPath,
   thumbsCache,
   urls,
+  webRoutes,
   wikiData,
 
   cachebust,
@@ -148,12 +151,9 @@ export async function go({
 
   await mkdir(outputPath, {recursive: true});
 
-  await writeSymlinks({
-    srcRootPath,
-    mediaPath,
-    mediaCachePath,
+  await writeWebRouteSymlinks({
     outputPath,
-    urls,
+    webRoutes,
   });
 
   if (writeAll) {
@@ -432,42 +432,41 @@ async function writePage({
   ].filter(Boolean));
 }
 
-function writeSymlinks({
-  srcRootPath,
-  mediaPath,
-  mediaCachePath,
+function writeWebRouteSymlinks({
   outputPath,
-  urls,
+  webRoutes,
 }) {
-  return progressPromiseAll('Writing site symlinks.', [
-    link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'),
-    link(path.join(srcRootPath, 'static'), 'shared.staticRoot'),
-    link(mediaPath, 'media.root'),
-    link(mediaCachePath, 'thumb.root'),
-  ]);
-
-  async function link(directory, urlKey) {
-    const pathname = urls.from('shared.root').toDevice(urlKey);
-    const file = path.join(outputPath, pathname);
-
-    try {
-      await unlink(file);
-    } catch (error) {
-      if (error.code !== 'ENOENT') {
-        throw error;
+  const promises =
+    webRoutes.map(async route => {
+      const parts = route.to.split('/');
+      const parentDirectoryParts = parts.slice(0, -1);
+      const symlinkNamePart = parts.at(-1);
+
+      const parentDirectory = path.join(outputPath, ...parentDirectoryParts);
+      const symlinkPath = path.join(parentDirectory, symlinkNamePart);
+
+      try {
+        await unlink(symlinkPath);
+      } catch (error) {
+        if (error.code !== 'ENOENT') {
+          throw error;
+        }
       }
-    }
 
-    try {
-      await symlink(path.resolve(directory), file);
-    } catch (error) {
-      if (error.code === 'EPERM') {
-        await symlink(path.resolve(directory), file, 'junction');
-      } else {
-        throw error;
+      await mkdir(parentDirectory, {recursive: true});
+
+      try {
+        await symlink(route.from, symlinkPath);
+      } catch (error) {
+        if (error.code === 'EPERM') {
+          await symlink(route.from, symlinkPath, 'junction');
+        } else {
+          throw error;
+        }
       }
-    }
-  }
+    });
+
+  return progressPromiseAll(`Writing web route symlinks.`, promises);
 }
 
 async function writeFavicon({