diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-10-29 09:26:59 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-10-29 09:26:59 -0300 |
commit | bfa1953e79a562ee675940b7acc52b5b29d22d8f (patch) | |
tree | 5c1cd2f4050c801a60f4b65b367a714ed0979759 /src/upd8.js | |
parent | c4ef4ced62d659d217873c6c48dd8038dbf765af (diff) | |
parent | 940b2cbf8b68eb0b446cca0feeb507840c486394 (diff) |
Merge branch 'preview' into listing-tweaks
Diffstat (limited to 'src/upd8.js')
-rwxr-xr-x | src/upd8.js | 607 |
1 files changed, 491 insertions, 116 deletions
diff --git a/src/upd8.js b/src/upd8.js index bfdd1c2a..27445a8e 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -32,11 +32,13 @@ // node.js and you'll 8e fine. ...Within the project root. O8viously. import {execSync} from 'node:child_process'; +import {readFile} from 'node:fs/promises'; import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; +import {displayCompositeCacheAnalysis} from '#composite'; import {processLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; @@ -46,8 +48,9 @@ import {generateURLs, urlSpec} from '#urls'; import {sortByName} from '#wiki-data'; import { - color, + colors, decorateTime, + fileIssue, logWarn, logInfo, logError, @@ -57,6 +60,7 @@ import { } from '#cli'; import genThumbs, { + CACHE_FILE as thumbsCacheFile, clearThumbs, defaultMagickThreads, isThumb, @@ -78,7 +82,7 @@ import * as buildModes from './write/build-modes/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CACHEBUST = 20; +const CACHEBUST = 22; let COMMIT; try { @@ -91,9 +95,64 @@ const BUILD_TIME = new Date(); const DEFAULT_STRINGS_FILE = 'strings-default.json'; +const STATUS_NOT_STARTED = `not started`; +const STATUS_NOT_APPLICABLE = `not applicable`; +const STATUS_STARTED_NOT_DONE = `started but not yet done`; +const STATUS_DONE_CLEAN = `done without warnings`; +const STATUS_FATAL_ERROR = `fatal error`; +const STATUS_HAS_WARNINGS = `has warnings`; + +const defaultStepStatus = {status: STATUS_NOT_STARTED, annotation: null}; + +// Defined globally for quick access outside the main() function's contents. +// This will be initialized and mutated over the course of main(). +let stepStatusSummary; +let showStepStatusSummary = false; + async function main() { Error.stackTraceLimit = Infinity; + stepStatusSummary = { + loadThumbnailCache: + {...defaultStepStatus, name: `load thumbnail cache file`}, + + generateThumbnails: + {...defaultStepStatus, name: `generate thumbnails`}, + + loadDataFiles: + {...defaultStepStatus, name: `load and process data files`}, + + linkWikiDataArrays: + {...defaultStepStatus, name: `link wiki data arrays`}, + + filterDuplicateDirectories: + {...defaultStepStatus, name: `filter duplicate directories`}, + + filterReferenceErrors: + {...defaultStepStatus, name: `filter reference errors`}, + + sortWikiDataArrays: + {...defaultStepStatus, name: `sort wiki data arrays`}, + + precacheData: + {...defaultStepStatus, name: `precache data`}, + + loadInternalDefaultLanguage: + {...defaultStepStatus, name: `load internal default language`}, + + loadLanguageFiles: + {...defaultStepStatus, name: `load custom language files`}, + + initializeDefaultLanguage: + {...defaultStepStatus, name: `initialize default language`}, + + preloadFileSizes: + {...defaultStepStatus, name: `preload file sizes`}, + + performBuild: + {...defaultStepStatus, name: `perform selected build mode`}, + }; + const defaultQueueSize = 500; const buildModeFlagOptions = ( @@ -118,7 +177,7 @@ async function main() { } else if (selectedBuildModeFlags.length > 1) { logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`; logError`Please specify a maximum of one build mode.`; - return; + return false; } else { selectedBuildModeFlag = selectedBuildModeFlags[0]; usingDefaultBuildMode = false; @@ -218,6 +277,11 @@ async function main() { type: 'flag', }, + 'show-step-summary': { + help: `Show a summary of all the top-level build steps once hsmusic exits. This is mostly useful for progammer debugging!`, + type: 'flag', + }, + 'queue-size': { help: `Process more or fewer disk files at once to optimize performance or avoid I/O errors, unlimited if set to 0 (between 500 and 700 is usually a safe range for building HSMusic on Windows machines)\nDefaults to ${defaultQueueSize}`, type: 'value', @@ -231,6 +295,12 @@ async function main() { 'magick-threads': { help: `Process more or fewer thumbnail files at once with ImageMagick when generating thumbnails. (Each ImageMagick thread may also make use of multi-core processing at its own utility.)`, + type: 'value', + validate(threads) { + if (parseInt(threads) !== parseFloat(threads)) return 'an integer'; + if (parseInt(threads) < 0) return 'a counting number or zero'; + return true; + } }, magick: {alias: 'magick-threads'}, @@ -271,7 +341,7 @@ async function main() { const indentWrap = (spaces, str) => wrap(str, {width: 60 - spaces, indent: ' '.repeat(spaces)}); const showOptions = (msg, options) => { - console.log(color.bright(msg)); + console.log(colors.bright(msg)); const entries = Object.entries(options); const sortedOptions = sortByName(entries @@ -302,13 +372,13 @@ async function main() { console.log(''); } - console.log(color.bright(` --` + name) + + console.log(colors.bright(` --` + name) + (aliases.length - ? ` (or: ${aliases.map(alias => color.bright(`--` + alias)).join(', ')})` + ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})` : '') + (descriptor.help ? '' - : color.dim(' (no help provided)'))); + : colors.dim(' (no help provided)'))); if (wrappedHelp) { console.log(wrappedHelp); @@ -328,7 +398,7 @@ async function main() { }; console.log( - color.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + + colors.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + `static wiki software cataloguing collaborative creation\n`); console.log(indentWrap(0, @@ -352,7 +422,7 @@ async function main() { })`, buildOptions); } - return; + return true; } const dataPath = cliOptions['data-path'] || process.env.HSMUSIC_DATA; @@ -364,6 +434,8 @@ async function main() { const clearThumbsFlag = cliOptions['clear-thumbs'] ?? false; const noBuild = cliOptions['no-build'] ?? false; + showStepStatusSummary = cliOptions['show-step-summary'] ?? false; + const replFlag = cliOptions['repl'] ?? false; const disableReplHistory = cliOptions['no-repl-history'] ?? false; @@ -379,19 +451,16 @@ async function main() { const magickThreads = +(cliOptions['magick-threads'] ?? defaultMagickThreads); - { - let errored = false; - const error = (cond, msg) => { - if (cond) { - console.error(`\x1b[31;1m${msg}\x1b[0m`); - errored = true; - } - }; - error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`); - error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`); - if (errored) { - return; - } + if (!dataPath) { + logError`${`Expected --data-path option or HSMUSIC_DATA to be set`}`; + } + + if (!mediaPath) { + logError`${`Expected --media-path option or HSMUSIC_MEDIA to be set`}`; + } + + if (!dataPath || !mediaPath) { + return false; } if (replFlag) { @@ -414,31 +483,103 @@ async function main() { if (skipThumbs && thumbsOnly) { logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; - return; + return false; } if (clearThumbsFlag) { - await clearThumbs(mediaPath, {queueSize}); - - logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`; - if (skipThumbs) { - logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; + const result = await clearThumbs(mediaPath, {queueSize}); + if (result.success) { + logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`; + if (skipThumbs) { + logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; + } } - return; + return true; } + let thumbsCache; + if (skipThumbs) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `provided --skip-thumbs`, + }); + + stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; + + const thumbsCachePath = path.join(mediaPath, thumbsCacheFile); + + try { + thumbsCache = JSON.parse(await readFile(thumbsCachePath)); + logInfo`Thumbnail cache file successfully read.`; + stepStatusSummary.loadThumbnailCache.status = STATUS_DONE_CLEAN; + } catch (error) { + if (error.code === 'ENOENT') { + logError`The thumbnail cache doesn't exist, and it's necessary to build` + logError`the website. Please run once without ${'--skip-thumbs'} - after` + logError`that you'll be good to go and don't need to process thumbnails` + logError`again!`; + + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_FATAL_ERROR, + annotation: `cache does not exist`, + }); + + return false; + } else { + logError`Malformed or unreadable thumbnail cache file: ${error}`; + logError`Path: ${thumbsCachePath}`; + logError`The thumbbnail cache is necessary to build the site, so you'll`; + logError`have to investigate this to get the build working. Try running`; + logError`again without ${'--skip-thumbs'}. If you can't get it working,`; + logError`you're welcome to message in the HSMusic Discord and we'll try`; + logError`to help you out with troubleshooting!`; + logError`${'https://hsmusic.wiki/discord/'}`; + + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_FATAL_ERROR, + annotation: `cache malformed or unreadable`, + }); + + return false; + } + } + logInfo`Skipping thumbnail generation.`; } else { + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_NOT_APPLICABLE, + annotation: `using cache from thumbnail generation`, + }); + + stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE; + logInfo`Begin thumbnail generation... -----+`; + const result = await genThumbs(mediaPath, { queueSize, magickThreads, quiet: !thumbsOnly, }); + logInfo`Done thumbnail generation! --------+`; - if (!result) return; - if (thumbsOnly) return; + + if (!result.success) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_FATAL_ERROR, + annotation: `view log for details`, + }); + + return false; + } + + stepStatusSummary.generateThumbnails.status = STATUS_DONE_CLEAN; + + if (thumbsOnly) { + return true; + } + + thumbsCache = result.cache; } if (noBuild) { @@ -453,14 +594,32 @@ async function main() { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } - const {aggregate: processDataAggregate, result: wikiDataResult} = - await loadAndProcessDataDocuments({dataPath}); + stepStatusSummary.loadDataFiles.status = STATUS_STARTED_NOT_DONE; + + let processDataAggregate, wikiDataResult; + + try { + ({aggregate: processDataAggregate, result: wikiDataResult} = + await loadAndProcessDataDocuments({dataPath})); + } catch (error) { + console.error(error); + + logError`There was a JavaScript error loading data files.`; + fileIssue(); + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_FATAL_ERROR, + annotation: `javascript error - view log for details`, + }); + + return false; + } Object.assign(wikiData, wikiDataResult); { const logThings = (thingDataProp, label) => - logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`; + logInfo` - ${wikiData[thingDataProp]?.length ?? colors.red('(Missing!)')} ${colors.normal(colors.dim(label))}`; try { logInfo`Loaded data and processed objects:`; logThings('albumData', 'albums'); @@ -499,84 +658,105 @@ async function main() { logWarn`still build - but all errored data will be skipped.`; logWarn`(Resolve errors for more complete output!)`; errorless = false; - } - if (errorless) { - logInfo`All data processed without any errors - nice!`; - logInfo`(This means all source files will be fully accounted for during page generation.)`; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + }); } - } - if (!wikiData.wikiInfo) { - logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; - return; - } + if (!wikiData.wikiInfo) { + logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; - let duplicateDirectoriesErrored = false; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_FATAL_ERROR, + annotation: `wiki info object not available`, + }); - function filterAndShowDuplicateDirectories() { - const aggregate = filterDuplicateDirectories(wikiData); - let errorless = true; - try { - aggregate.close(); - } catch (aggregate) { - niceShowAggregate(aggregate); - logWarn`The above duplicate directories were detected while reviewing data files.`; - logWarn`Each thing listed above will been totally excempt from this build of the site!`; - logWarn`Specify unique 'Directory' fields in data entries to resolve these.`; - logWarn`${`Note:`} This will probably result in reference errors below.`; - logWarn`${`. . .`} You should fix duplicate directories first!`; - logWarn`(Resolve errors for more complete output!)`; - duplicateDirectoriesErrored = true; - errorless = false; - } - if (errorless) { - logInfo`No duplicate directories found - nice!`; + return false; } - } - function filterAndShowReferenceErrors() { - const aggregate = filterReferenceErrors(wikiData); - let errorless = true; - try { - aggregate.close(); - } catch (error) { - niceShowAggregate(error); - logWarn`The above errors were detected while validating references in data files.`; - logWarn`If the remaining valid data is complete enough, the wiki will still build -`; - logWarn`but all errored references will be skipped.`; - if (duplicateDirectoriesErrored) { - logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`; - logWarn`${`. . .`} as they may have caused some of the errors detected above.`; - } - logWarn`(Resolve errors for more complete output!)`; - errorless = false; - } if (errorless) { - logInfo`All references validated without any errors - nice!`; - logInfo`(This means all references between things, such as leitmotif references`; - logInfo` and artist credits, will be fully accounted for during page generation.)`; + logInfo`All data files processed without any errors - nice!`; + stepStatusSummary.loadDataFiles.status = STATUS_DONE_CLEAN; } } // Link data arrays so that all essential references between objects are // complete, so properties (like dates!) are inherited where that's // appropriate. + + stepStatusSummary.linkWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + linkWikiDataArrays(wikiData); + stepStatusSummary.linkWikiDataArrays.status = STATUS_DONE_CLEAN; + // Filter out any things with duplicate directories throughout the data, // warning about them too. - filterAndShowDuplicateDirectories(); + + stepStatusSummary.filterDuplicateDirectories.status = STATUS_STARTED_NOT_DONE; + + const filterDuplicateDirectoriesAggregate = + filterDuplicateDirectories(wikiData); + + try { + filterDuplicateDirectoriesAggregate.close(); + logInfo`No duplicate directories found - nice!`; + stepStatusSummary.filterDuplicateDirectories.status = STATUS_DONE_CLEAN; + } catch (aggregate) { + niceShowAggregate(aggregate); + + logWarn`The above duplicate directories were detected while reviewing data files.`; + logWarn`Since it's impossible to automatically determine which one's directory is`; + logWarn`correct, the build can't continue. Specify unique 'Directory' fields in`; + logWarn`some or all of these data entries to resolve the errors.`; + + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_FATAL_ERROR, + annotation: `duplicate directories found`, + }); + + return false; + } // Filter out any reference errors throughout the data, warning about them // too. - filterAndShowReferenceErrors(); + + stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; + + const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); + + try { + filterReferenceErrorsAggregate.close(); + logInfo`All references validated without any errors - nice!`; + stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN; + } catch (error) { + niceShowAggregate(error); + + logWarn`The above errors were detected while validating references in data files.`; + logWarn`The wiki will still build, but these connections between data objects`; + logWarn`will be completely skipped. Resolve the errors for more complete output.`; + + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + }); + } // Sort data arrays so that they're all in order! This may use properties // which are only available after the initial linking. + + stepStatusSummary.sortWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + sortWikiDataArrays(wikiData); + stepStatusSummary.sortWikiDataArrays.status = STATUS_DONE_CLEAN; + if (precacheData) { + stepStatusSummary.precacheData.status = STATUS_STARTED_NOT_DONE; + + // TODO: Aggregate errors here, instead of just throwing. progressCallAll('Caching all data values', Object.entries(wikiData) .filter(([key]) => key !== 'listingSpec' && @@ -587,27 +767,96 @@ async function main() { [key, value]) .flatMap(([_key, things]) => things) .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); + + stepStatusSummary.precacheData.status = STATUS_DONE_CLEAN; + } else { + Object.assign(stepStatusSummary.precacheData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-data not provided`, + }); + } + + if (noBuild) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_NOT_APPLICABLE, + annotation: `--no-build provided`, + }); + + displayCompositeCacheAnalysis(); + + if (precacheData) { + return true; + } } - const internalDefaultLanguage = await processLanguageFile( - path.join(__dirname, DEFAULT_STRINGS_FILE)); + let internalDefaultLanguage; + + try { + internalDefaultLanguage = + await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); + + stepStatusSummary.loadInternalDefaultLanguage.status = STATUS_DONE_CLEAN; + } catch (error) { + console.error(error); + + logError`There was an error reading the internal language file.`; + fileIssue(); + + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_FATAL_ERROR, + annotation: `see log for details`, + }); + + return false; + } let languages; + if (langPath) { + stepStatusSummary.loadLanguageFiles.status = STATUS_STARTED_NOT_DONE; + const languageDataFiles = await traverse(langPath, { filterFile: name => path.extname(name) === '.json', pathStyle: 'device', }); - const results = await progressPromiseAll(`Reading & processing language files.`, - languageDataFiles.map((file) => processLanguageFile(file))); + let results; - languages = Object.fromEntries( - results.map((language) => [language.code, language])); + // TODO: Aggregate errors (with Promise.allSettled). + try { + results = + await progressPromiseAll(`Reading & processing language files.`, + languageDataFiles.map((file) => processLanguageFile(file))); + } catch (error) { + console.error(error); + + 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`, + }); + + return false; + } + + languages = + Object.fromEntries( + results.map((language) => [language.code, language])); + + stepStatusSummary.loadLanguageFiles.status = STATUS_DONE_CLEAN; } else { languages = {}; + + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_NOT_APPLICABLE, + annotation: `--lang-path and HSMUSIC_LANG not provided`, + }); } + stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE; + const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; let finalDefaultLanguage; @@ -616,17 +865,34 @@ async function main() { logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; finalDefaultLanguage = customDefaultLanguage; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_DONE_CLEAN, + annotation: `using wiki-specified custom default language`, + }); } else if (wikiData.wikiInfo.defaultLanguage) { logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; if (langPath) { logError`Check if an appropriate file exists in ${langPath}?`; } else { - logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`; + logError`Be sure to specify ${'--lang-path'} or ${'HSMUSIC_LANG'} with the path to language files.`; } - return; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_FATAL_ERROR, + annotation: `wiki specifies default language whose file is not available`, + }); + + return false; } else { languages[internalDefaultLanguage.code] = internalDefaultLanguage; finalDefaultLanguage = internalDefaultLanguage; + stepStatusSummary.initializeDefaultLanguage.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_DONE_CLEAN, + annotation: `no custom default language specified`, + }); } for (const language of Object.values(languages)) { @@ -641,7 +907,8 @@ async function main() { const urls = generateURLs(urlSpec); - await verifyImagePaths(mediaPath, {urls, wikiData}); + const {missing: missingImagePaths} = + await verifyImagePaths(mediaPath, {urls, wikiData}); const fileSizePreloader = new FileSizePreloader(); @@ -704,7 +971,9 @@ async function main() { }; const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); - const getSizeOfImageFile = getSizeOfMediaFileHelper(imageFilePaths); + const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths); + + stepStatusSummary.preloadFileSizes.status = STATUS_STARTED_NOT_DONE; logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; @@ -716,9 +985,23 @@ async function main() { fileSizePreloader.loadPaths(...imageFilePaths.map((path) => path.device)); await fileSizePreloader.waitUntilDoneLoading(); - logInfo`Done preloading filesizes!`; + if (fileSizePreloader.hasErrored) { + logWarn`Some media files couldn't be read for preloading filesizes.`; + logWarn`This means the wiki won't display file sizes for these files.`; + logWarn`Investigate missing or unreadable files to get that fixed!`; - if (noBuild) return; + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_HAS_WARNINGS, + annotation: `see log for details`, + }); + } else { + logInfo`Done preloading filesizes without any errors - nice!`; + stepStatusSummary.preloadFileSizes.status = STATUS_DONE_CLEAN; + } + + if (noBuild) { + return true; + } const developersComment = `<!--\n` + [ @@ -750,25 +1033,58 @@ async function main() { .map(line => ` ` + line) .join('\n') + `\n-->`; - return selectedBuildMode.go({ - cliOptions, - dataPath, - mediaPath, - queueSize, - srcRootPath: __dirname, - - defaultLanguage: finalDefaultLanguage, - languages, - wikiData, - urls, - urlSpec, - - cachebust: '?' + CACHEBUST, - developersComment, - getSizeOfAdditionalFile, - getSizeOfImageFile, - niceShowAggregate, - }); + stepStatusSummary.performBuild.status = STATUS_STARTED_NOT_DONE; + + let buildModeResult; + + try { + buildModeResult = await selectedBuildMode.go({ + cliOptions, + dataPath, + mediaPath, + queueSize, + srcRootPath: __dirname, + + defaultLanguage: finalDefaultLanguage, + languages, + missingImagePaths, + thumbsCache, + urls, + urlSpec, + wikiData, + + cachebust: '?' + CACHEBUST, + developersComment, + getSizeOfAdditionalFile, + getSizeOfImagePath, + niceShowAggregate, + }); + } catch (error) { + console.error(error); + + logError`There was a JavaScript error performing the build.`; + fileIssue(); + + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_FATAL_ERROR, + message: `javascript error - view log for details`, + }); + + return false; + } + + if (buildModeResult !== true) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_HAS_WARNINGS, + message: `may not have completed - view log for details`, + }); + + return false; + } + + stepStatusSummary.performBuild.status = STATUS_DONE_CLEAN; + + return true; } // TODO: isMain detection isn't consistent across platforms here @@ -787,6 +1103,65 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } } + if (showStepStatusSummary) { + console.error(colors.bright(`Step summary:`)); + + const longestNameLength = + Math.max(... + Object.values(stepStatusSummary) + .map(({name}) => name.length)); + + const anyStepsNotClean = + Object.values(stepStatusSummary) + .some(({status}) => + status === STATUS_HAS_WARNINGS || + status === STATUS_FATAL_ERROR || + status === STATUS_STARTED_NOT_DONE); + + for (const {name, status, annotation} of Object.values(stepStatusSummary)) { + let message = `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + if (annotation) { + message += ` (${annotation})`; + } + + switch (status) { + case STATUS_DONE_CLEAN: + console.error(colors.green(message)); + break; + + case STATUS_NOT_STARTED: + case STATUS_NOT_APPLICABLE: + console.error(colors.dim(message)); + break; + + case STATUS_HAS_WARNINGS: + case STATUS_STARTED_NOT_DONE: + console.error(colors.yellow(message)); + break; + + case STATUS_FATAL_ERROR: + console.error(colors.red(message)); + break; + + default: + console.error(message); + break; + } + } + + if (result === true) { + if (anyStepsNotClean) { + console.error(colors.bright(`Final output is true, but some steps aren't clean.`)); + process.exit(1); + return; + } else { + console.error(colors.bright(`Final output is true and all steps are clean.`)); + } + } else { + console.error(colors.bright(`Final output is not true (${result}).`)); + } + } + if (result !== true) { process.exit(1); return; |