diff options
-rwxr-xr-x | src/upd8.js | 508 | ||||
-rw-r--r-- | src/write/build-modes/live-dev-server.js | 16 |
2 files changed, 418 insertions, 106 deletions
diff --git a/src/upd8.js b/src/upd8.js index 23182778..408ad884 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -132,6 +132,9 @@ async function main() { linkWikiDataArrays: {...defaultStepStatus, name: `link wiki data arrays`}, + precacheCommonData: + {...defaultStepStatus, name: `precache common data`}, + filterDuplicateDirectories: {...defaultStepStatus, name: `filter duplicate directories`}, @@ -141,8 +144,8 @@ async function main() { sortWikiDataArrays: {...defaultStepStatus, name: `sort wiki data arrays`}, - precacheData: - {...defaultStepStatus, name: `precache data`}, + precacheAllData: + {...defaultStepStatus, name: `precache nearly all data`}, loadInternalDefaultLanguage: {...defaultStepStatus, name: `load internal default language`}, @@ -255,6 +258,11 @@ async function main() { type: 'flag', }, + 'skip-reference-validation': { + help: `Skips checking and reporting reference errors, which speeds up the build but may silently allow erroneous data to pass through`, + type: 'flag', + }, + // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e // kinda a pain to run every time, since it does necessit8te reading // every media file at run time. Pass this to skip it. @@ -327,18 +335,18 @@ async function main() { type: 'flag', }, - // Compute ALL data properties before moving on to building. This ensures - // writes are processed at a stable speed (since they don't have to perform - // any additional data computation besides what is done for the page - // itself), but it'll also take a long while for the initial caching to - // complete. This shouldn't have any overall difference on efficiency as - // it's the same amount of processing being done regardless; the option is - // mostly present for optimization testing (i.e. if you want to focus on - // efficiency of data calculation or write generation separately instead of - // mixed together). - 'precache-data': { - help: `Compute all runtime-cached values for wiki data objects before proceeding to site build (optimizes rate of content generation/serving, but waits a lot longer before build actually starts, and may compute data which is never required for this build)`, - type: 'flag', + 'precache-mode': { + help: + `Change the way certain runtime-computed values are preemptively evaluated and cached\n\n` + + `common: Preemptively compute certain properties which are needed for basic data loading and site generation\n\n` + + `all: Compute every visible data property, optimizing rate of content generation, but causing a long stall before the build actually starts\n\n` + + `none: Don't preemptively compute any values - strictly the most efficient, but may result in unpredictably "lopsided" performance for individual steps of loading data and building the site\n\n` + + `Defaults to 'common'`, + type: 'value', + validate(value) { + if (['common', 'all', 'none'].includes(value)) return true; + return 'common, all, or none'; + }, }, }; @@ -447,6 +455,7 @@ async function main() { 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; showStepStatusSummary = cliOptions['show-step-summary'] ?? false; @@ -456,7 +465,7 @@ async function main() { const showAggregateTraces = cliOptions['show-traces'] ?? false; - const precacheData = cliOptions['precache-data'] ?? false; + const precacheMode = cliOptions['precache-mode'] ?? 'common'; const showInvalidPropertyAccesses = cliOptions['show-invalid-property-accesses'] ?? false; // Makes writing nicer on the CPU and file I/O parts of the OS, with a @@ -488,7 +497,91 @@ async function main() { }); } - stepStatusSummary.determineMediaCachePath.status = STATUS_STARTED_NOT_DONE; + // Prepare not-applicable steps before anything else. + + if (skipThumbs) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `provided --skip-thumbs`, + }); + } else { + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_NOT_APPLICABLE, + annotation: `using cache from thumbnail generation`, + }); + } + + if (!migrateThumbs) { + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `--migrate-thumbs not provided`, + }); + } + + 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, { + status: STATUS_NOT_APPLICABLE, + annotation: `--skip-reference-validation provided`, + }); + } + + switch (precacheMode) { + case 'common': + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is common, not all`, + }); + + break; + + case 'all': + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is all, not common`, + }); + + break; + + case 'none': + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is none`, + }); + + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is none`, + }); + + break; + } + + if (!langPath) { + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_NOT_APPLICABLE, + annotation: `neither --lang-path nor HSMUSIC_LANG provided`, + }); + } + + if (noBuild) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_NOT_APPLICABLE, + annotation: `--no-build provided`, + }); + } + + if (skipThumbs && thumbsOnly) { + logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; + return false; + } + + Object.assign(stepStatusSummary.determineMediaCachePath, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const {mediaCachePath, annotation: mediaCachePathAnnotation} = await determineMediaCachePath({ @@ -523,6 +616,7 @@ async function main() { Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_FATAL_ERROR, annotation: mediaCachePathAnnotation, + timeEnd: Date.now(), }); return false; @@ -533,10 +627,14 @@ async function main() { Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_DONE_CLEAN, annotation: mediaCachePathAnnotation, + timeEnd: Date.now(), }); if (migrateThumbs) { - stepStatusSummary.migrateThumbnails.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const result = await migrateThumbsIntoDedicatedCacheDirectory({ mediaPath, @@ -548,20 +646,21 @@ async function main() { Object.assign(stepStatusSummary.migrateThumbnails, { status: STATUS_FATAL_ERROR, annotation: `view log for details`, + timeEnd: Date.now(), }); + return false; } - stepStatusSummary.migrateThumbnails.status = STATUS_DONE_CLEAN; - logInfo`Good to go! Run hsmusic again without ${'--migrate-thumbs'} to start`; logInfo`using the migrated media cache.`; - return true; - } else { + Object.assign(stepStatusSummary.migrateThumbnails, { - status: STATUS_NOT_APPLICABLE, - annotation: `--migrate-thumbs not provided`, + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), }); + + return true; } const niceShowAggregate = (error, ...opts) => { @@ -572,27 +671,18 @@ async function main() { }); }; - if (skipThumbs && thumbsOnly) { - logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; - return false; - } - let thumbsCache; if (skipThumbs) { - Object.assign(stepStatusSummary.generateThumbnails, { - status: STATUS_NOT_APPLICABLE, - annotation: `provided --skip-thumbs`, + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), }); - stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; - const thumbsCachePath = path.join(mediaCachePath, 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` @@ -603,6 +693,7 @@ async function main() { Object.assign(stepStatusSummary.loadThumbnailCache, { status: STATUS_FATAL_ERROR, annotation: `cache does not exist`, + timeEnd: Date.now(), }); return false; @@ -619,20 +710,26 @@ async function main() { Object.assign(stepStatusSummary.loadThumbnailCache, { status: STATUS_FATAL_ERROR, annotation: `cache malformed or unreadable`, + timeEnd: Date.now(), }); return false; } } - logInfo`Skipping thumbnail generation.`; - } else { + logInfo`Thumbnail cache file successfully read.`; + Object.assign(stepStatusSummary.loadThumbnailCache, { - status: STATUS_NOT_APPLICABLE, - annotation: `using cache from thumbnail generation`, + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), }); - stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE; + logInfo`Skipping thumbnail generation.`; + } else { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); logInfo`Begin thumbnail generation... -----+`; @@ -651,12 +748,16 @@ async function main() { Object.assign(stepStatusSummary.generateThumbnails, { status: STATUS_FATAL_ERROR, annotation: `view log for details`, + timeEnd: Date.now(), }); return false; } - stepStatusSummary.generateThumbnails.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); if (thumbsOnly) { return true; @@ -677,7 +778,10 @@ async function main() { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } - stepStatusSummary.loadDataFiles.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); let processDataAggregate, wikiDataResult; @@ -693,6 +797,7 @@ async function main() { Object.assign(stepStatusSummary.loadDataFiles, { status: STATUS_FATAL_ERROR, annotation: `javascript error - view log for details`, + timeEnd: Date.now(), }); return false; @@ -737,15 +842,7 @@ async function main() { } catch (error) { niceShowAggregate(error); logWarn`The above errors were detected while processing data files.`; - logWarn`If the remaining valid data is complete enough, the wiki will`; - logWarn`still build - but all errored data will be skipped.`; - logWarn`(Resolve errors for more complete output!)`; errorless = false; - - Object.assign(stepStatusSummary.loadDataFiles, { - status: STATUS_HAS_WARNINGS, - annotation: `view log for details`, - }); } if (!wikiData.wikiInfo) { @@ -754,6 +851,7 @@ async function main() { Object.assign(stepStatusSummary.loadDataFiles, { status: STATUS_FATAL_ERROR, annotation: `wiki info object not available`, + timeEnd: Date.now(), }); return false; @@ -761,7 +859,21 @@ async function main() { if (errorless) { logInfo`All data files processed without any errors - nice!`; - stepStatusSummary.loadDataFiles.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + } else { + logWarn`If the remaining valid data is complete enough, the wiki will`; + logWarn`still build - but all errored data will be skipped.`; + logWarn`(Resolve errors for more complete output!)`; + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + timeEnd: Date.now(), + }); } } @@ -769,16 +881,93 @@ async function main() { // complete, so properties (like dates!) are inherited where that's // appropriate. - stepStatusSummary.linkWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.linkWikiDataArrays, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); linkWikiDataArrays(wikiData); - stepStatusSummary.linkWikiDataArrays.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.linkWikiDataArrays, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + + if (precacheMode === 'common') { + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + const commonDataMap = { + albumData: new Set([ + // Needed for sorting + 'date', 'tracks', + // Needed for computing page paths + 'commentary', + ]), + + artTagData: new Set([ + // Needed for computing page paths + 'isContentWarning', + ]), + + artistAliasData: new Set([ + // Needed for computing page paths + 'aliasedArtist', + ]), + + flashData: new Set([ + // Needed for sorting + 'act', 'date', + ]), + + flashActData: new Set([ + // Needed for sorting + 'flashes', + ]), + + groupData: new Set([ + // Needed for computing page paths + 'albums', + ]), + + listingSpec: new Set([ + // Needed for computing page paths + 'contentFunction', 'featureFlag', + ]), + + trackData: new Set([ + // Needed for sorting + 'album', 'date', + // Needed for computing page paths + 'commentary', + ]), + }; + + for (const [wikiDataKey, properties] of Object.entries(commonDataMap)) { + const thingData = wikiData[wikiDataKey]; + const allProperties = new Set(['name', 'directory', ...properties]); + for (const thing of thingData) { + for (const property of allProperties) { + void thing[property]; + } + } + } + + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + } // Filter out any things with duplicate directories throughout the data, // warning about them too. - stepStatusSummary.filterDuplicateDirectories.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const filterDuplicateDirectoriesAggregate = filterDuplicateDirectories(wikiData); @@ -786,7 +975,11 @@ async function main() { try { filterDuplicateDirectoriesAggregate.close(); logInfo`No duplicate directories found - nice!`; - stepStatusSummary.filterDuplicateDirectories.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } catch (aggregate) { niceShowAggregate(aggregate); @@ -798,6 +991,7 @@ async function main() { Object.assign(stepStatusSummary.filterDuplicateDirectories, { status: STATUS_FATAL_ERROR, annotation: `duplicate directories found`, + timeEnd: Date.now(), }); return false; @@ -806,38 +1000,58 @@ async function main() { // Filter out any reference errors throughout the data, warning about them // too. - stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; + if (!skipReferenceValidation) { + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); - const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); + 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); + try { + filterReferenceErrorsAggregate.close(); - 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.`; + logInfo`All references validated without any errors - nice!`; - Object.assign(stepStatusSummary.filterReferenceErrors, { - status: STATUS_HAS_WARNINGS, - annotation: `view log for details`, - }); + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + } 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`, + timeEnd: Date.now(), + }); + } } // 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; + Object.assign(stepStatusSummary.sortWikiDataArrays, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); sortWikiDataArrays(wikiData); - stepStatusSummary.sortWikiDataArrays.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.sortWikiDataArrays, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); - if (precacheData) { - stepStatusSummary.precacheData.status = STATUS_STARTED_NOT_DONE; + if (precacheMode === 'all') { + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); // TODO: Aggregate errors here, instead of just throwing. progressCallAll('Caching all data values', Object.entries(wikiData) @@ -851,34 +1065,35 @@ async function main() { .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`, + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), }); } if (noBuild) { - Object.assign(stepStatusSummary.performBuild, { - status: STATUS_NOT_APPLICABLE, - annotation: `--no-build provided`, - }); - displayCompositeCacheAnalysis(); - if (precacheData) { + if (precacheMode === 'all') { return true; } } + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + let internalDefaultLanguage; try { internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); - stepStatusSummary.loadInternalDefaultLanguage.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } catch (error) { console.error(error); @@ -888,6 +1103,7 @@ async function main() { Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { status: STATUS_FATAL_ERROR, annotation: `see log for details`, + timeEnd: Date.now(), }); return false; @@ -896,7 +1112,10 @@ async function main() { let languages; if (langPath) { - stepStatusSummary.loadLanguageFiles.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const languageDataFiles = await traverse(langPath, { filterFile: name => path.extname(name) === '.json', @@ -919,6 +1138,7 @@ async function main() { Object.assign(stepStatusSummary.loadLanguageFiles, { status: STATUS_FATAL_ERROR, annotation: `see log for details`, + timeEnd: Date.now(), }); return false; @@ -928,17 +1148,18 @@ async function main() { 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: `neither --lang-path nor HSMUSIC_LANG provided`, + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), }); + } else { + languages = {}; } - stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; @@ -952,6 +1173,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_DONE_CLEAN, annotation: `using wiki-specified custom default language`, + timeEnd: Date.now(), }); } else if (wikiData.wikiInfo.defaultLanguage) { logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; @@ -964,6 +1186,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_FATAL_ERROR, annotation: `wiki specifies default language whose file is not available`, + timeEnd: Date.now(), }); return false; @@ -975,6 +1198,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_DONE_CLEAN, annotation: `no custom default language specified`, + timeEnd: Date.now(), }); } @@ -990,30 +1214,44 @@ async function main() { const urls = generateURLs(urlSpec); - stepStatusSummary.verifyImagePaths.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const {missing: missingImagePaths, misplaced: misplacedImagePaths} = await verifyImagePaths(mediaPath, {urls, wikiData}); if (empty(missingImagePaths) && empty(misplacedImagePaths)) { - stepStatusSummary.verifyImagePaths.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } else if (empty(missingImagePaths)) { Object.assign(stepStatusSummary.verifyImagePaths, { status: STATUS_HAS_WARNINGS, annotation: `misplaced images detected`, + timeEnd: Date.now(), }); } else if (empty(misplacedImagePaths)) { Object.assign(stepStatusSummary.verifyImagePaths, { status: STATUS_HAS_WARNINGS, annotation: `missing images detected`, + timeEnd: Date.now(), }); } else { Object.assign(stepStatusSummary.verifyImagePaths, { - status :STATUS_HAS_WARNINGS, + status: STATUS_HAS_WARNINGS, annotation: `missing and misplaced images detected`, + timeEnd: Date.now(), }); } + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + const fileSizePreloader = new FileSizePreloader(); // File sizes of additional files need to be precalculated before we can @@ -1077,8 +1315,6 @@ async function main() { const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths); - stepStatusSummary.preloadFileSizes.status = STATUS_STARTED_NOT_DONE; - logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; fileSizePreloader.loadPaths(...additionalFilePaths.map((path) => path.device)); @@ -1097,10 +1333,15 @@ async function main() { Object.assign(stepStatusSummary.preloadFileSizes, { status: STATUS_HAS_WARNINGS, annotation: `see log for details`, + timeEnd: Date.now(), }); } else { logInfo`Done preloading filesizes without any errors - nice!`; - stepStatusSummary.preloadFileSizes.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } if (noBuild) { @@ -1137,7 +1378,10 @@ async function main() { .map(line => ` ` + line) .join('\n') + `\n-->`; - stepStatusSummary.performBuild.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); let buildModeResult; @@ -1173,6 +1417,7 @@ async function main() { Object.assign(stepStatusSummary.performBuild, { status: STATUS_FATAL_ERROR, message: `javascript error - view log for details`, + timeEnd: Date.now(), }); return false; @@ -1182,12 +1427,16 @@ async function main() { Object.assign(stepStatusSummary.performBuild, { status: STATUS_HAS_WARNINGS, annotation: `may not have completed - view log for details`, + timeEnd: Date.now(), }); return false; } - stepStatusSummary.performBuild.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); return true; } @@ -1198,17 +1447,43 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus (async () => { let result; + const totalTimeStart = Date.now(); + try { result = await main(); } catch (error) { if (error instanceof AggregateError) { showAggregate(error); + } else if (error.cause) { + console.error(error); + showAggregate(error); } else { console.error(error); } } + const totalTimeEnd = Date.now(); + + const formatDuration = timeDelta => { + const seconds = timeDelta / 1000; + + if (seconds > 90) { + const modSeconds = Math.floor(seconds % 60); + const minutes = Math.floor(seconds - seconds % 60) / 60; + return `${minutes}m${modSeconds}s`; + } + + if (seconds < 0.1) { + return 'instant'; + } + + const precision = (seconds > 1 ? 3 : 2); + return `${seconds.toPrecision(precision)}s`; + }; + if (showStepStatusSummary) { + const totalDuration = formatDuration(totalTimeEnd - totalTimeStart); + console.error(colors.bright(`Step summary:`)); const longestNameLength = @@ -1228,15 +1503,40 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus const stepDetails = Object.values(stepStatusSummary); + const stepDurations = + stepDetails.map(({status, timeStart, timeEnd}) => { + if ( + status === STATUS_NOT_APPLICABLE || + status === STATUS_NOT_STARTED || + status === STATUS_STARTED_NOT_DONE + ) { + return '-'; + } + + if (typeof timeStart !== 'number' || typeof timeEnd !== 'number') { + return 'unknown'; + } + + return formatDuration(timeEnd - timeStart); + }); + + const longestDurationLength = + Math.max(...stepDurations.map(duration => duration.length)); + for (let index = 0; index < stepDetails.length; index++) { const {name, status, annotation} = stepDetails[index]; + const duration = stepDurations[index]; let message = (stepsNotClean[index] ? `!! ` : ` - `); - message += `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + message += `(${duration})`.padStart(longestDurationLength + 2, ' '); + message += ` `; + message += `${name}: `.padEnd(longestNameLength + 4, '.'); + message += ` `; + message += status; if (annotation) { message += ` (${annotation})`; @@ -1267,6 +1567,8 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } } + console.error(colors.bright(`Done in ${totalDuration}.`)); + if (result === true) { if (anyStepsNotClean) { console.error(colors.bright(`Final output is true, but some steps aren't clean.`)); diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 47d59f95..ab6ceecb 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -44,6 +44,11 @@ export function getCLIOptions() { help: `Enables outputting [200] and [404] responses in the server log, which are suppressed by default`, type: 'flag', }, + + 'skip-serving': { + help: `Causes the build to exit when it would start serving over HTTP instead\n\nMainly useful for testing performance`, + type: 'flag', + }, }; } @@ -78,6 +83,7 @@ export async function go({ const host = cliOptions['host'] ?? defaultHost; const port = parseInt(cliOptions['port'] ?? defaultPort); const loudResponses = cliOptions['loud-responses'] ?? false; + const skipServing = cliOptions['skip-serving'] ?? false; const contentDependenciesWatcher = await watchContentDependencies(); const {contentDependencies} = contentDependenciesWatcher; @@ -396,10 +402,14 @@ export async function go({ } }); - server.listen(port, host); + if (skipServing) { + logInfo`Ready to serve! But --skip-serving was passed, so all done.`; + } else { + server.listen(port, host); - // Just keep going... forever!!! - await new Promise(() => {}); + // Just keep going... forever!!! + await new Promise(() => {}); + } return true; } |