« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rwxr-xr-xsrc/upd8.js343
-rw-r--r--src/write/build-modes/live-dev-server.js10
-rw-r--r--src/write/build-modes/static-build.js10
3 files changed, 264 insertions, 99 deletions
diff --git a/src/upd8.js b/src/upd8.js
index ebb278b2..6105565c 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -145,11 +145,15 @@ async function main() {
     precacheAllData:
       {...defaultStepStatus, name: `precache nearly all data`},
 
+    // TODO: This should be split into load/watch steps.
     loadInternalDefaultLanguage:
       {...defaultStepStatus, name: `load internal default language`},
 
     loadLanguageFiles:
-      {...defaultStepStatus, name: `load custom language files`},
+      {...defaultStepStatus, name: `statically load custom language files`},
+
+    watchLanguageFiles:
+      {...defaultStepStatus, name: `watch custom language files`},
 
     initializeDefaultLanguage:
       {...defaultStepStatus, name: `initialize default language`},
@@ -462,13 +466,8 @@ async function main() {
   const mediaPath = cliOptions['media-path'] || process.env.HSMUSIC_MEDIA;
   const langPath = cliOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset!
 
-  const migrateThumbs = cliOptions['migrate-thumbs'] ?? false;
-  const skipThumbs = cliOptions['skip-thumbs'] ?? false;
   const thumbsOnly = cliOptions['thumbs-only'] ?? false;
-  const skipReferenceValidation = cliOptions['skip-reference-validation'] ?? false;
-  const noBuild = cliOptions['no-build'] ?? false;
   const noInput = cliOptions['no-input'] ?? false;
-  let noLanguageReloading = cliOptions['no-language-reloading'] ?? null; // Will get default later.
 
   showStepStatusSummary = cliOptions['show-step-summary'] ?? false;
 
@@ -509,34 +508,164 @@ async function main() {
     });
   }
 
-  // Prepare not-applicable steps before anything else.
+  if (cliOptions['no-build']) {
+    logInfo`Won't generate any site or page files this run (--no-build passed).`;
 
-  if (skipThumbs) {
-    Object.assign(stepStatusSummary.generateThumbnails, {
+    Object.assign(stepStatusSummary.performBuild, {
       status: STATUS_NOT_APPLICABLE,
-      annotation: `provided --skip-thumbs`,
+      annotation: `--no-build provided`,
     });
   } else {
-    Object.assign(stepStatusSummary.loadThumbnailCache, {
-      status: STATUS_NOT_APPLICABLE,
-      annotation: `using cache from thumbnail generation`,
+    if (usingDefaultBuildMode) {
+      logInfo`No build mode specified, will use default: ${selectedBuildModeFlag}`;
+    } else {
+      logInfo`Will use specified build mode: ${selectedBuildModeFlag}`;
+    }
+  }
+
+  // Finish setting up defaults by combining information from all options.
+
+  const _fallbackStep = (stepKey, {
+    default: defaultValue,
+
+    cli: {
+      flag: cliFlag = null,
+      negate: cliFlagNegates = false,
+      warn: cliFlagWarning = null,
+    } = {},
+
+    buildConfig: buildConfigKey,
+  }) => {
+    const {[buildConfigKey]: buildConfig} = selectedBuildMode.config;
+    const {[stepKey]: step} = stepStatusSummary;
+
+    if (cliFlag && cliOptions[cliFlag]) {
+      const cliPart = `--` + cliFlag;
+      const modePart = `--` + selectedBuildModeFlag;
+      if (buildConfig?.applicable === false) {
+        if (cliFlagNegates) {
+          logWarn`${cliPart} provided, but ${modePart} already skips this step`;
+          logWarn`Redundant option ${cliPart}`;
+        } else {
+          logWarn`${cliPart} provided, but this step isn't applicable for ${modePart}`;
+          logWarn`Ignoring option ${cliPart}`;
+        }
+      } else if (buildConfig?.required === true) {
+        if (cliFlagNegates) {
+          logWarn`${cliPart} provided, but ${modePart} requires this step`;
+          logWarn`Ignoring option ${cliPart}`;
+        } else {
+          logWarn`${cliPart} provided, but ${modePart} already requires this step`;
+          logWarn`Redundant option ${cliPart}`;
+        }
+      } else {
+        if (cliFlagNegates) {
+          step.status = STATUS_NOT_APPLICABLE;
+          step.annotation = `--${cliFlag} provided`;
+        }
+        if (cliFlagWarning) {
+          for (const line of cliFlagWarning.split('\n')) {
+            logWarn(line);
+          }
+        }
+      }
+    }
+
+    if (buildConfig?.applicable === false) {
+      step.status = STATUS_NOT_APPLICABLE;
+      step.annotation = `N/A for --${selectedBuildModeFlag}`;
+      return;
+    }
+
+    if (buildConfig?.default === 'skip') {
+      step.status = STATUS_NOT_APPLICABLE;
+      step.annotation = `default for --${selectedBuildModeFlag}`;
+      return;
+    }
+
+    switch (defaultValue) {
+      case 'skip':
+        step.status = STATUS_NOT_APPLICABLE;
+        if (cliFlag && !cliFlagNegates) {
+          step.annotation = `--${cliFlag} not provided`;
+        }
+        break;
+
+      case 'perform':
+        break;
+
+      default:
+        throw new Error(`Invalid default step status ${defaultValue}`);
+    }
+  };
+
+  {
+    let errored = false;
+
+    const fallbackStep = (stepKey, options) => {
+      try {
+        _fallbackStep(stepKey, options);
+      } catch (error) {
+        logError`Error determining fallback for step ${stepKey}`;
+        showAggregate(error);
+        errored = true;
+      }
+    };
+
+    fallbackStep('filterReferenceErrors', {
+      default: 'perform',
+      buildConfig: null,
+      cli: {
+        flag: 'skip-reference-validation',
+        negate: true,
+        warn:
+          `Skipping reference validation. If any reference errors are present\n` +
+          `in data, they will be silently passed along to the build.`,
+      }
+    });
+
+    fallbackStep('generateThumbnails', {
+      default: 'perform',
+      buildConfig: 'thumbs',
+      cli: {
+        flag: 'skip-thumbs',
+        negate: true,
+      },
+    });
+
+    fallbackStep('migrateThumbnails', {
+      default: 'skip',
+      buildConfig: null,
+      cli: {
+        flag: 'migrate-thumbs',
+      },
     });
+
+    fallbackStep('watchLanguageFiles', {
+      default: 'perform',
+      buildConfig: 'languageReloading',
+      cli: {
+        flag: 'no-language-reloading',
+        negate: true,
+      },
+    });
+
+    if (errored) {
+      return false;
+    }
   }
 
-  if (!migrateThumbs) {
-    Object.assign(stepStatusSummary.migrateThumbnails, {
+  if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.loadThumbnailCache, {
       status: STATUS_NOT_APPLICABLE,
-      annotation: `--migrate-thumbs not provided`,
+      annotation: `using cache from thumbnail generation`,
     });
   }
 
-  if (skipReferenceValidation) {
-    logWarn`Skipping reference validation. If any reference errors are present`;
-    logWarn`in data, they will be silently passed along to the build.`;
-
-    Object.assign(stepStatusSummary.filterReferenceErrors, {
+  if (stepStatusSummary.watchLanguageFiles.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.loadLanguageFiles, {
       status: STATUS_NOT_APPLICABLE,
-      annotation: `--skip-reference-validation provided`,
+      annotation: `watching for changes instead`,
     });
   }
 
@@ -576,28 +705,14 @@ async function main() {
       status: STATUS_NOT_APPLICABLE,
       annotation: `neither --lang-path nor HSMUSIC_LANG provided`,
     });
-  }
-
-  if (noBuild) {
-    logInfo`Won't generate any site or page files this run (--no-build passed).`;
 
-    Object.assign(stepStatusSummary.performBuild, {
+    Object.assign(stepStatusSummary.watchLanguageFiles, {
       status: STATUS_NOT_APPLICABLE,
-      annotation: `--no-build provided`,
+      annotation: `neither --lang-path nor HSMUSIC_LANG provided`,
     });
-  } else if (usingDefaultBuildMode) {
-    logInfo`No build mode specified, will use default: ${selectedBuildModeFlag}`;
-  } else {
-    logInfo`Will use specified build mode: ${selectedBuildModeFlag}`;
   }
 
-  noLanguageReloading ??=
-    ({
-      'static-build': true,
-      'live-dev-server': false,
-    })[selectedBuildModeFlag];
-
-  if (skipThumbs && thumbsOnly) {
+  if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_APPLICABLE && thumbsOnly) {
     logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
     return false;
   }
@@ -613,7 +728,7 @@ async function main() {
       providedMediaCachePath:
         cliOptions['media-cache-path'] || process.env.HSMUSIC_MEDIA_CACHE,
       disallowDoubling:
-        migrateThumbs,
+        stepStatusSummary.migrateThumbnails.status === STATUS_NOT_STARTED,
     });
 
   if (!mediaCachePath) {
@@ -654,7 +769,7 @@ async function main() {
     timeEnd: Date.now(),
   });
 
-  if (migrateThumbs) {
+  if (stepStatusSummary.migrateThumbnails.status === STATUS_NOT_STARTED) {
     Object.assign(stepStatusSummary.migrateThumbnails, {
       status: STATUS_STARTED_NOT_DONE,
       timeStart: Date.now(),
@@ -695,9 +810,16 @@ async function main() {
     });
   };
 
+  if (
+    stepStatusSummary.loadThumbnailCache.status === STATUS_NOT_STARTED &&
+    stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED
+  ) {
+    throw new Error(`Unable to continue with both loadThumbnailCache and generateThumbnails`);
+  }
+
   let thumbsCache;
 
-  if (skipThumbs) {
+  if (stepStatusSummary.loadThumbnailCache.status === STATUS_NOT_STARTED) {
     Object.assign(stepStatusSummary.loadThumbnailCache, {
       status: STATUS_STARTED_NOT_DONE,
       timeStart: Date.now(),
@@ -749,7 +871,7 @@ async function main() {
     });
 
     logInfo`Skipping thumbnail generation.`;
-  } else {
+  } else if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED) {
     Object.assign(stepStatusSummary.generateThumbnails, {
       status: STATUS_STARTED_NOT_DONE,
       timeStart: Date.now(),
@@ -788,6 +910,8 @@ async function main() {
     }
 
     thumbsCache = result.cache;
+  } else {
+    thumbsCache = {};
   }
 
   if (showInvalidPropertyAccesses) {
@@ -1016,7 +1140,7 @@ async function main() {
   // Filter out any reference errors throughout the data, warning about them
   // too.
 
-  if (!skipReferenceValidation) {
+  if (stepStatusSummary.filterReferenceErrors.status === STATUS_NOT_STARTED) {
     Object.assign(stepStatusSummary.filterReferenceErrors, {
       status: STATUS_STARTED_NOT_DONE,
       timeStart: Date.now(),
@@ -1087,7 +1211,7 @@ async function main() {
     });
   }
 
-  if (noBuild) {
+  if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) {
     displayCompositeCacheAnalysis();
 
     if (precacheMode === 'all') {
@@ -1095,6 +1219,9 @@ async function main() {
     }
   }
 
+  const languageReloading =
+    stepStatusSummary.watchLanguageFiles.status === STATUS_NOT_STARTED;
+
   Object.assign(stepStatusSummary.loadInternalDefaultLanguage, {
     status: STATUS_STARTED_NOT_DONE,
     timeStart: Date.now(),
@@ -1105,16 +1232,7 @@ async function main() {
 
   let errorLoadingInternalDefaultLanguage = false;
 
-  if (noLanguageReloading) {
-    internalDefaultLanguageWatcher = null;
-
-    try {
-      internalDefaultLanguage = await processLanguageFile(internalDefaultStringsFile);
-    } catch (error) {
-      niceShowAggregate(error);
-      errorLoadingInternalDefaultLanguage = true;
-    }
-  } else {
+  if (languageReloading) {
     internalDefaultLanguageWatcher = watchLanguageFile(internalDefaultStringsFile);
 
     try {
@@ -1144,6 +1262,15 @@ async function main() {
       // watchLanguageFile.
       errorLoadingInternalDefaultLanguage = true;
     }
+  } else {
+    internalDefaultLanguageWatcher = null;
+
+    try {
+      internalDefaultLanguage = await processLanguageFile(internalDefaultStringsFile);
+    } catch (error) {
+      niceShowAggregate(error);
+      errorLoadingInternalDefaultLanguage = true;
+    }
   }
 
   if (errorLoadingInternalDefaultLanguage) {
@@ -1159,7 +1286,7 @@ async function main() {
     return false;
   }
 
-  if (!noLanguageReloading) {
+  if (languageReloading) {
     // Bypass node.js special-case handling for uncaught error events
     internalDefaultLanguageWatcher.on('error', () => {});
   }
@@ -1173,10 +1300,17 @@ async function main() {
   let languages;
 
   if (langPath) {
-    Object.assign(stepStatusSummary.loadLanguageFiles, {
-      status: STATUS_STARTED_NOT_DONE,
-      timeStart: Date.now(),
-    });
+    if (languageReloading) {
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
+    } else {
+      Object.assign(stepStatusSummary.loadLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
+    }
 
     const languageDataFiles = await traverse(langPath, {
       filterFile: name =>
@@ -1187,23 +1321,12 @@ async function main() {
 
     let errorLoadingCustomLanguages = false;
 
-    if (noLanguageReloading) {
-      languages = {};
-
-      const results =
-        await Promise.allSettled(
-          languageDataFiles
-            .map(file => processLanguageFile(file)));
+    if (languageReloading) watchCustomLanguages: {
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
 
-      for (const {status, value: language, reason: error} of results) {
-        if (status === 'rejected') {
-          errorLoadingCustomLanguages = true;
-          niceShowAggregate(error);
-        } else {
-          languages[language.code] = language;
-        }
-      }
-    } else watchCustomLanguages: {
       customLanguageWatchers =
         languageDataFiles.map(file => {
           const watcher = watchLanguageFile(file);
@@ -1248,6 +1371,12 @@ async function main() {
             watcher.close();
           }
 
+          Object.assign(stepStatusSummary.watchLanguageFiles, {
+            status: STATUS_FATAL_ERROR,
+            annotation: `see log for details`,
+            timeEnd: Date.now(),
+          });
+
           errorLoadingCustomLanguages = true;
           break watchCustomLanguages;
         }
@@ -1273,31 +1402,47 @@ async function main() {
         Object.fromEntries(
           customLanguageWatchers
             .map(({language}) => [language.code, language]));
+
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    } else {
+      languages = {};
+
+      const results =
+        await Promise.allSettled(
+          languageDataFiles
+            .map(file => processLanguageFile(file)));
+
+      for (const {status, value: language, reason: error} of results) {
+        if (status === 'rejected') {
+          errorLoadingCustomLanguages = true;
+          niceShowAggregate(error);
+        } else {
+          languages[language.code] = language;
+        }
+      }
+
+      if (errorLoadingCustomLanguages) {
+        Object.assign(stepStatusSummary.loadLanguageFiles, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `see log for details`,
+          timeEnd: Date.now(),
+        });
+      } else {
+        Object.assign(stepStatusSummary.loadLanguageFiles, {
+          status: STATUS_DONE_CLEAN,
+          timeEnd: Date.now(),
+        });
+      }
     }
 
     if (errorLoadingCustomLanguages) {
       logError`Failed to load language files. Please investigate these, or don't provide`;
       logError`--lang-path (or HSMUSIC_LANG) and build again.`;
-
-      Object.assign(stepStatusSummary.loadLanguageFiles, {
-        status: STATUS_FATAL_ERROR,
-        annotation: `see log for details`,
-        timeEnd: Date.now(),
-      });
-
       return false;
     }
-
-    Object.assign(stepStatusSummary.loadLanguageFiles, {
-      status: STATUS_DONE_CLEAN,
-      timeEnd: Date.now(),
-        annotation:
-        (noLanguageReloading
-          ? (selectedBuildModeFlag === 'static-build'
-              ? `loaded statically, default for --static-build`
-              : `loaded statically, --no-language-reloading provided`)
-          : `watching for changes`),
-    });
   } else {
     languages = {};
   }
@@ -1336,7 +1481,7 @@ async function main() {
     finalDefaultLanguage = customDefaultLanguage;
     finalDefaultLanguageAnnotation = `using wiki-specified custom default language`;
 
-    if (!noLanguageReloading) {
+    if (languageReloading) {
       finalDefaultLanguageWatcher =
         customLanguageWatchers
           .find(({language}) => language === customDefaultLanguage);
@@ -1347,7 +1492,7 @@ async function main() {
     finalDefaultLanguage = customDefaultLanguage;
     finalDefaultLanguageAnnotation = `using inferred custom default language`;
 
-    if (!noLanguageReloading) {
+    if (languageReloading) {
       finalDefaultLanguageWatcher =
         customLanguageWatchers
           .find(({language}) => language === customDefaultLanguage);
@@ -1358,7 +1503,7 @@ async function main() {
     finalDefaultLanguage = internalDefaultLanguage;
     finalDefaultLanguageAnnotation = `no custom default language specified`;
 
-    if (!noLanguageReloading) {
+    if (languageReloading) {
       finalDefaultLanguageWatcher = internalDefaultLanguageWatcher;
     }
   }
@@ -1387,7 +1532,7 @@ async function main() {
 
   inheritStringsFromDefaultLanguage();
 
-  if (!noLanguageReloading) {
+  if (languageReloading) {
     if (finalDefaultLanguage !== internalDefaultLanguage) {
       internalDefaultLanguageWatcher.on('update', () => {
         inheritStringsFromInternalLanguage();
@@ -1540,7 +1685,7 @@ async function main() {
     });
   }
 
-  if (noBuild) {
+  if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) {
     return true;
   }
 
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 8160c8f9..ecd1d954 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -25,6 +25,16 @@ 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 config = {
+  languageReloading: {
+    default: true,
+  },
+
+  thumbs: {
+    default: true,
+  },
+};
+
 function inspect(value, opts = {}) {
   return nodeInspect(value, {colors: ENABLE_COLOR, ...opts});
 }
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index f292fde0..bed24332 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -37,6 +37,16 @@ const pageFlags = Object.keys(pageSpecs);
 
 export const description = `Generates all page content in one build (according to the contents of data files at build time) and writes them to disk, preparing the output folder for upload and serving by any static web host\n\nIntended for any production or public-facing release of a wiki; serviceable for local development, but can be a bit unwieldy and time/CPU-expensive`;
 
+export const config = {
+  languageReloading: {
+    applicable: false,
+  },
+
+  thumbs: {
+    default: true,
+  },
+};
+
 export function getCLIOptions() {
   return {
     // This is the output directory. It's the one you'll upload online with