diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/data/things/sorting-rule.js | 109 | ||||
-rwxr-xr-x | src/upd8.js | 401 |
2 files changed, 337 insertions, 173 deletions
diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule.js index 18da452e..0ed7fb0f 100644 --- a/src/data/things/sorting-rule.js +++ b/src/data/things/sorting-rule.js @@ -6,7 +6,7 @@ import * as path from 'node:path'; import {input} from '#composite'; import {chunkByProperties, compareArrays, unique} from '#sugar'; import Thing from '#thing'; -import {isObject, isStringNonEmpty, strictArrayOf} from '#validators'; +import {isObject, isStringNonEmpty, anyOf, strictArrayOf} from '#validators'; import { compareCaseLessSensitive, @@ -24,6 +24,17 @@ import { import {flag} from '#composite/wiki-properties'; +function isSelectFollowingEntry(value) { + isObject(value); + + const {length} = Object.keys(value); + if (length !== 1) { + throw new Error(`Expected object with 1 key, got ${length}`); + } + + return true; +} + export class SortingRule extends Thing { static [Thing.friendlyName] = `Sorting Rule`; @@ -61,7 +72,7 @@ export class SortingRule extends Thing { save: (results) => ({sortingRules: results}), }); - check({wikiData}) { + check(opts) { return this.constructor.check(this, opts); } @@ -69,19 +80,22 @@ export class SortingRule extends Thing { return this.constructor.apply(this, opts); } - static check() { - throw new Error(`Not implemented`); + static check(rule, opts) { + const result = this.apply(rule, {...opts, dry: true}); + if (!result) return true; + if (!result.changed) return true; + return false; } - static async apply() { + static async apply(_rule, _opts) { throw new Error(`Not implemented`); } - static async* applyAll() { + static async* applyAll(_rules, _opts) { throw new Error(`Not implemented`); } - static async* go({dataPath, wikiData}) { + static async* go({dataPath, wikiData, dry}) { const rules = wikiData.sortingRules; const constructors = unique(rules.map(rule => rule.constructor)); @@ -90,7 +104,7 @@ export class SortingRule extends Thing { rules .filter(rule => rule.active) .filter(rule => rule.constructor === constructor), - {dataPath, wikiData}); + {dataPath, wikiData, dry}); } } } @@ -186,16 +200,17 @@ export class DocumentSortingRule extends ThingSortingRule { flags: {update: true, expose: true}, update: { - validate(value) { - isObject(value); - - const {length} = Object.keys(value); - if (length !== 1) { - throw new Error(`Expected object with 1 key, got ${length}`); - } + validate: + anyOf( + isSelectFollowingEntry, + strictArrayOf(isSelectFollowingEntry)), + }, - return true; - }, + compute: { + transform: value => + (Array.isArray(value) + ? value + : [value]), }, }, @@ -220,24 +235,17 @@ export class DocumentSortingRule extends ThingSortingRule { ], }); - static check(rule, {wikiData}) { + static async apply(rule, {wikiData, dataPath, dry}) { const oldLayout = getThingLayoutForFilename(rule.filename, wikiData); - if (!oldLayout) return; + if (!oldLayout) return null; const newLayout = rule.#processLayout(oldLayout); const oldOrder = flattenThingLayoutToDocumentOrder(oldLayout); const newOrder = flattenThingLayoutToDocumentOrder(newLayout); + const changed = compareArrays(oldOrder, newOrder); - return compareArrays(oldOrder, newOrder); - } - - static async apply(rule, {wikiData, dataPath}) { - const oldLayout = getThingLayoutForFilename(rule.filename, wikiData); - if (!oldLayout) return; - - const newLayout = rule.#processLayout(oldLayout); - const newOrder = flattenThingLayoutToDocumentOrder(newLayout); + if (dry) return {changed}; const realPath = path.join( @@ -248,9 +256,11 @@ export class DocumentSortingRule extends ThingSortingRule { const newSourceText = reorderDocumentsInYAMLSourceText(oldSourceText, newOrder); await writeFile(realPath, newSourceText); + + return {changed}; } - static async* applyAll(rules, {wikiData, dataPath}) { + static async* applyAll(rules, {wikiData, dataPath, dry}) { rules = rules .slice() @@ -281,6 +291,7 @@ export class DocumentSortingRule extends ThingSortingRule { } if (!anyChanged) continue; + if (dry) continue; const newLayout = currLayout; const newOrder = flattenThingLayoutToDocumentOrder(newLayout); @@ -317,31 +328,33 @@ export class DocumentSortingRule extends ThingSortingRule { } if (this.selectDocumentsFollowing) { - const [field, value] = Object.entries(this.selectDocumentsFollowing)[0]; + for (const entry of this.selectDocumentsFollowing) { + const [field, value] = Object.entries(entry)[0]; - const after = - sortable.findIndex(thing => - thing[Thing.yamlSourceDocument][field] === value); + const after = + sortable.findIndex(thing => + thing[Thing.yamlSourceDocument][field] === value); - const different = - after + - sortable - .slice(after) - .findIndex(thing => - Object.hasOwn(thing[Thing.yamlSourceDocument], field) && - thing[Thing.yamlSourceDocument][field] !== value); + const different = + after + + sortable + .slice(after) + .findIndex(thing => + Object.hasOwn(thing[Thing.yamlSourceDocument], field) && + thing[Thing.yamlSourceDocument][field] !== value); - const before = - (different === -1 - ? sortable.length - : different); + const before = + (different === -1 + ? sortable.length + : different); - const subsortable = - sortable.slice(after + 1, before); + const subsortable = + sortable.slice(after + 1, before); - this.sort(subsortable); + this.sort(subsortable); - sortable.splice(after + 1, before - after - 1, ...subsortable); + sortable.splice(after + 1, before - after - 1, ...subsortable); + } } else if (this.selectDocumentsUnder) { const field = this.selectDocumentsUnder; diff --git a/src/upd8.js b/src/upd8.js index a3840424..9b347e61 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -51,6 +51,7 @@ import {isMain, traverse} from '#node-utils'; import {bindReverse} from '#reverse'; import {writeSearchData} from '#search'; import {sortByName} from '#sort'; +import thingConstructors from '#things'; import {identifyAllWebRoutes} from '#web-routes'; import { @@ -133,8 +134,8 @@ 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; -let showStepMemoryInSummary = false; +let shouldShowStepStatusSummary = false; +let shouldShowStepMemoryInSummary = false; async function main() { Error.stackTraceLimit = Infinity; @@ -190,6 +191,13 @@ async function main() { {...defaultStepStatus, name: `precache nearly all data`, for: ['build']}, + sortWikiDataSourceFiles: + {...defaultStepStatus, name: `apply sorting rules to wiki data files`, + for: ['build']}, + + checkWikiDataSourceFileSorting: + {...defaultStepStatus, name: `check sorting rules against wiki data files`}, + loadURLFiles: {...defaultStepStatus, name: `load internal & custom url spec files`, for: ['build']}, @@ -270,6 +278,35 @@ async function main() { })); let selectedBuildModeFlag; + let sortInAdditionToBuild = false; + + // As an exception, --sort can be combined with another build mode. + if (selectedBuildModeFlags.length >= 2 && selectedBuildModeFlags.includes('sort')) { + sortInAdditionToBuild = true; + selectedBuildModeFlags.splice(selectedBuildModeFlags.indexOf('sort'), 1); + } + + if (sortInAdditionToBuild) { + Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { + status: STATUS_NOT_STARTED, + annotation: `--sort provided with another build mode`, + }); + + Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { + status: STATUS_NOT_APPLICABLE, + annotation: `--sort provided, dry run not applicable`, + }); + } else { + Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { + status: STATUS_NOT_APPLICABLE, + annotation: `--sort not provided, dry run only`, + }); + + Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { + status: STATUS_NOT_STARTED, + annotation: `--sort not provided, dry run applicable`, + }); + } if (empty(selectedBuildModeFlags)) { // No build mode selected. This is not a valid state for building the wiki, @@ -525,8 +562,8 @@ async function main() { ...buildOptions, }); - showStepStatusSummary = cliOptions['show-step-summary'] ?? false; - showStepMemoryInSummary = cliOptions['show-step-memory'] ?? false; + shouldShowStepStatusSummary = cliOptions['show-step-summary'] ?? false; + shouldShowStepMemoryInSummary = cliOptions['show-step-memory'] ?? false; if (cliOptions['help']) { console.log( @@ -1846,6 +1883,77 @@ async function main() { }); } + if (stepStatusSummary.sortWikiDataSourceFiles.status === STATUS_NOT_STARTED) { + Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + const {SortingRule} = thingConstructors; + const results = + await Array.fromAsync(SortingRule.go({dataPath, wikiData})); + + if (results.some(result => result.changed)) { + logInfo`Updated data files to satisfy sorting.`; + logInfo`Restarting automatically, since that's now needed!`; + + Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { + status: STATUS_DONE_CLEAN, + annotation: `changes cueing restart`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + + return 'restart'; + } else { + logInfo`All sorting rules are satisfied. Nice!`; + paragraph = false; + + Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { + status: STATUS_DONE_CLEAN, + annotation: `no changes needed`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + } + } else if (stepStatusSummary.checkWikiDataSourceFileSorting.status === STATUS_NOT_STARTED) { + Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + const {SortingRule} = thingConstructors; + const results = + await Array.fromAsync(SortingRule.go({dataPath, wikiData, dry: true})); + + const needed = results.filter(result => result.changed); + + if (empty(needed)) { + logInfo`All sorting rules are satisfied. Nice!`; + paragraph = false; + + Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + } else { + logWarn`Some of this wiki's sorting rules currently aren't satisfied:`; + for (const {rule} of needed) { + logWarn`- ${rule.message}`; + } + logWarn`Run ${'hsmusic --sort'} to automatically update data files.`; + paragraph = false; + + Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { + status: STATUS_HAS_WARNINGS, + annotation: `not all rules satisfied`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + } + } + if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) { displayCompositeCacheAnalysis(); @@ -3094,141 +3202,67 @@ async function main() { if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmusic') { (async () => { let result; + let numRestarts = 0; 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'; + while (true) { + 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 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 = - Math.max(... - Object.values(stepStatusSummary) - .map(({name}) => name.length)); - - const stepsNotClean = - Object.values(stepStatusSummary) - .map(({status}) => - status === STATUS_HAS_WARNINGS || - status === STATUS_FATAL_ERROR || - status === STATUS_STARTED_NOT_DONE); - - const anyStepsNotClean = - stepsNotClean.includes(true); - - 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 (result === 'restart') { + console.log(''); - if (typeof timeStart !== 'number' || typeof timeEnd !== 'number') { - return 'unknown'; + if (shouldShowStepStatusSummary) { + if (numRestarts >= 1) { + console.error(colors.bright(`Step summary since latest restart:`)); + } else { + console.error(colors.bright(`Step summary before restart:`)); } - return formatDuration(timeEnd - timeStart); - }); - - const longestDurationLength = - Math.max(...stepDurations.map(duration => duration.length)); - - const stepMemories = - stepDetails.map(({memory}) => - (memory - ? Math.round(memory["heapUsed"] / 1024 / 1024) + 'MB' - : '-')); - - const longestMemoryLength = - Math.max(...stepMemories.map(memory => memory.length)); - - for (let index = 0; index < stepDetails.length; index++) { - const {name, status, annotation} = stepDetails[index]; - const duration = stepDurations[index]; - const memory = stepMemories[index]; - - let message = - (stepsNotClean[index] - ? `!! ` - : ` - `); - - message += `(${duration} `.padStart(longestDurationLength + 2, ' '); - - if (showStepMemoryInSummary) { - message += ` ${memory})`.padStart(longestMemoryLength + 2, ' '); + showStepStatusSummary(); + console.log(''); } - message += ` `; - message += `${name}: `.padEnd(longestNameLength + 4, '.'); - message += ` `; - message += status; - - if (annotation) { - message += ` (${annotation})`; + if (numRestarts > 5) { + logError`A restart was cued, but we've restarted a bunch already.`; + logError`Exiting because this is probably a bug!`; + console.log(''); + break; + } else { + console.log(''); + logInfo`A restart was cued. This is probably normal, and required`; + logInfo`to load updated data files. Restarting automatically now!`; + console.log(''); + numRestarts++; } + } else { + break; + } + } - 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; + if (shouldShowStepStatusSummary) { + if (numRestarts >= 1) { + console.error(colors.bright(`Step summary after final restart:`)); + } else { + console.error(colors.bright(`Step summary:`)); + } - case STATUS_FATAL_ERROR: - console.error(colors.red(message)); - break; + const {anyStepsNotClean} = + showStepStatusSummary(); - default: - console.error(message); - break; - } - } + const totalTimeEnd = Date.now(); + const totalDuration = formatDuration(totalTimeEnd - totalTimeStart); console.error(colors.bright(`Done in ${totalDuration}.`)); @@ -3257,3 +3291,120 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus process.exit(0); })(); } + +function 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`; +} + +function showStepStatusSummary() { + const longestNameLength = + Math.max(... + Object.values(stepStatusSummary) + .map(({name}) => name.length)); + + const stepsNotClean = + Object.values(stepStatusSummary) + .map(({status}) => + status === STATUS_HAS_WARNINGS || + status === STATUS_FATAL_ERROR || + status === STATUS_STARTED_NOT_DONE); + + const anyStepsNotClean = + stepsNotClean.includes(true); + + 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)); + + const stepMemories = + stepDetails.map(({memory}) => + (memory + ? Math.round(memory["heapUsed"] / 1024 / 1024) + 'MB' + : '-')); + + const longestMemoryLength = + Math.max(...stepMemories.map(memory => memory.length)); + + for (let index = 0; index < stepDetails.length; index++) { + const {name, status, annotation} = stepDetails[index]; + const duration = stepDurations[index]; + const memory = stepMemories[index]; + + let message = + (stepsNotClean[index] + ? `!! ` + : ` - `); + + message += `(${duration} `.padStart(longestDurationLength + 2, ' '); + + if (shouldShowStepMemoryInSummary) { + message += ` ${memory})`.padStart(longestMemoryLength + 2, ' '); + } + + message += ` `; + message += `${name}: `.padEnd(longestNameLength + 4, '.'); + message += ` `; + message += 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; + } + } + + return {anyStepsNotClean}; +} |