diff options
Diffstat (limited to 'src/write/build-modes/static-build.js')
-rw-r--r-- | src/write/build-modes/static-build.js | 265 |
1 files changed, 175 insertions, 90 deletions
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index a355a002..b5ded04c 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -1,13 +1,7 @@ +import {cp, mkdir, stat, symlink, writeFile, unlink} from 'node:fs/promises'; import * as path from 'node:path'; -import { - copyFile, - mkdir, - stat, - symlink, - writeFile, - unlink, -} from 'node:fs/promises'; +import {rimraf} from 'rimraf'; import {quickLoadContentDependencies} from '#content-dependencies'; import {quickEvaluate} from '#content-function'; @@ -24,6 +18,7 @@ import { } from '#cli'; import { + getOrigin, getPagePathname, getURLsFrom, getURLsFromRoot, @@ -38,7 +33,7 @@ export const description = `Generates all page content in one build (according t export const config = { fileSizes: { - default: true, + default: 'perform', }, languageReloading: { @@ -46,11 +41,19 @@ export const config = { }, mediaValidation: { - default: true, + default: 'perform', + }, + + search: { + default: 'perform', }, thumbs: { - default: true, + default: 'perform', + }, + + webRoutes: { + required: true, }, }; @@ -99,23 +102,16 @@ export function getCLIOptions() { export async function go({ cliOptions, - _dataPath, - mediaPath, - mediaCachePath, queueSize, + universalUtilities, + defaultLanguage, languages, - missingImagePaths, - srcRootPath, - thumbsCache, urls, + webRoutes, wikiData, - cachebust, - developersComment: _developersComment, - getSizeOfAdditionalFile, - getSizeOfImagePath, niceShowAggregate, }) { const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; @@ -148,20 +144,17 @@ export async function go({ await mkdir(outputPath, {recursive: true}); - await writeSymlinks({ - srcRootPath, - mediaPath, - mediaCachePath, + await writeWebRouteSymlinks({ outputPath, - urls, + webRoutes, }); - if (writeAll) { - await writeFavicon({ - mediaPath, - outputPath, - }); + await writeWebRouteCopies({ + outputPath, + webRoutes, + }); + if (writeAll) { await writeSharedFilesAndPages({ outputPath, randomLinkDataJSON: generateRandomLinkDataJSON({wikiData}), @@ -186,7 +179,7 @@ export async function go({ return null; } - const paths = []; + let paths = []; if (pageSpec.pathsTargetless) { const result = pageSpec.pathsTargetless({wikiData}); @@ -216,6 +209,9 @@ export async function go({ // TODO: Validate each pathsForTargets entry } + paths = + paths.filter(path => path.condition?.() ?? true); + return paths; }) .filter(Boolean) @@ -277,6 +273,8 @@ export async function go({ showAggregate: niceShowAggregate, }); + const commonUtilities = {...universalUtilities}; + const perLanguageFn = async (language, i, entries) => { const baseDirectory = language === defaultLanguage ? '' : language.code; @@ -305,19 +303,13 @@ export async function go({ }); const bound = bindUtilities({ + ...commonUtilities, + absoluteTo, - cachebust, - defaultLanguage, - getSizeOfAdditionalFile, - getSizeOfImagePath, language, - languages, - missingImagePaths, pagePath, - thumbsCache, - to, - urls, - wikiData, + pagePathStringFromRoot: pathname, + to: page.absoluteLinks ? absoluteTo : to, }); let pageHTML, oEmbedJSON; @@ -432,66 +424,159 @@ async function writePage({ ].filter(Boolean)); } -function writeSymlinks({ - srcRootPath, - mediaPath, - mediaCachePath, +function filterNoOrigin(route) { + return !getOrigin(route.to); +} + +function writeWebRouteSymlinks({ outputPath, - urls, + webRoutes, }) { - return progressPromiseAll('Writing site symlinks.', [ - link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'), - link(path.join(srcRootPath, 'static'), 'shared.staticRoot'), - link(mediaPath, 'media.root'), - link(mediaCachePath, 'thumb.root'), - ]); - - async function link(directory, urlKey) { - const pathname = urls.from('shared.root').toDevice(urlKey); - const file = path.join(outputPath, pathname); - - try { - await unlink(file); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; + const symlinkRoutes = + webRoutes + .filter(route => route.statically === 'symlink') + .filter(filterNoOrigin); + + const promises = + symlinkRoutes.map(async route => { + const parts = route.to.split('/'); + const parentDirectoryParts = parts.slice(0, -1); + const symlinkNamePart = parts.at(-1); + + const parentDirectory = path.join(outputPath, ...parentDirectoryParts); + const symlinkPath = path.join(parentDirectory, symlinkNamePart); + + try { + await unlink(symlinkPath); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } } - } - try { - await symlink(path.resolve(directory), file); - } catch (error) { - if (error.code === 'EPERM') { - await symlink(path.resolve(directory), file, 'junction'); - } else { - throw error; + await mkdir(parentDirectory, {recursive: true}); + + try { + await symlink(route.from, symlinkPath); + } catch (error) { + if (error.code === 'EPERM') { + await symlink(route.from, symlinkPath, 'junction'); + } else { + throw error; + } } - } - } + }); + + return progressPromiseAll(`Writing web route symlinks.`, promises); } -async function writeFavicon({ - mediaPath, +async function writeWebRouteCopies({ outputPath, + webRoutes, }) { - const faviconFile = 'favicon.ico'; + const copyRoutes = + webRoutes + .filter(route => route.statically === 'copy') + .filter(filterNoOrigin); + + const promises = + copyRoutes.map(async route => { + const permissionName = '__hsmusic-ok-for-deletion.txt'; + + const parts = route.to.split('/'); + const parentDirectoryParts = parts.slice(0, -1); + const copyNamePart = parts.at(-1); + + const parentDirectory = path.join(outputPath, ...parentDirectoryParts); + const copyPath = path.join(parentDirectory, copyNamePart); + + // We're going to do a rimraf call! This is freaking terrifying, + // so nope out on a couple important conditions. + + let needsDelete; + try { + await stat(copyPath); + needsDelete = true; + } catch (error) { + if (error.code === 'ENOENT') { + needsDelete = false; + } else { + throw error; + } + } - try { - await stat(path.join(mediaPath, faviconFile)); - } catch (error) { - return; - } + if (needsDelete) { + // First remove it directly, in case it's a symlink. + try { + await unlink(copyPath); + needsDelete = false; + } catch (error) { + // EPERM is POSIX, but libuv may or may not flat-out just raise + // the system error (which is ostensibly EISDIR on Linux). + // https://github.com/nodejs/node-v0.x-archive/issues/5791 + // https://man7.org/linux/man-pages/man2/unlink.2.html + // + // Both of these indidcate "a directory, probably" and we'll + // still check for the deletion permission file where we expect + // it before actually touching anything. + if (error.code !== 'EPERM' && error.code !== 'EISDIR') { + throw error; + } + } + } - try { - await copyFile( - path.join(mediaPath, faviconFile), - path.join(outputPath, faviconFile)); - } catch (error) { - logWarn`Failed to copy favicon! ${error.message}`; - return; - } + if (needsDelete) { + // Then check that the deletion permission file exists + // where we expect it. + try { + await stat(path.join(copyPath, permissionName)); + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Couldn't find ${permissionName} in ${copyPath} - please delete or move away this folder manually`); + } else { + throw error; + } + } + + // And *then* actually delete that directory. + await rimraf(copyPath); + } - logInfo`Copied favicon to site root.`; + // Actually copy the source path where it's wanted. + await cp(route.from, copyPath, {recursive: true}); + + // And certify that it's OK to delete this path, next time around. + await writeFile(path.join(copyPath, permissionName), + `The presence of this file (by its name, not its contents)\n` + + `indicates hsmusic may delete everything contained in this\n` + + `directory (the one which directly contains this file, *not*\n` + + `any further-up parent directories).\n` + + `\n` + + `If you make edits, or add any files, they will be deleted or\n` + + `overwritten the next time you run the build.\n` + + `\n` + + `If you delete *this* file, hsmusic will error during the next\n` + + `build, and will ask that you delete the containing directory\n` + + `yourself.\n`); + }); + + const results = + await Promise.allSettled(promises); + + const errors = + results + .filter(({status}) => status === 'rejected') + .map(({reason}) => reason) + .map(err => + (err.message.startsWith(`Couldn't find`) + ? err.message + : err)); + + if (empty(errors)) { + logInfo`Wrote web route copies.`; + } else { + throw new AggregateError(errors, `Errors copying internal files ("web routes")`); + } } async function writeSharedFilesAndPages({ |