From c6e1a0b6fb9314186a46cf1352a8685e8aa5fe8d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Mar 2023 20:15:37 -0300 Subject: data steps: experimental live JS reload infrastructure --- .../dependencies/generateAlbumSocialEmbed.js | 71 +++++++++++ .../generateAlbumSocialEmbedDescription.js | 48 ++++++++ .../dependencies/generateAlbumStylesheet.js | 59 ++++++++++ src/content/dependencies/index.js | 108 +++++++++++++++++ src/misc-templates.js | 60 ---------- src/page/album.js | 130 ++------------------- 6 files changed, 293 insertions(+), 183 deletions(-) create mode 100644 src/content/dependencies/generateAlbumSocialEmbed.js create mode 100644 src/content/dependencies/generateAlbumSocialEmbedDescription.js create mode 100644 src/content/dependencies/generateAlbumStylesheet.js create mode 100644 src/content/dependencies/index.js diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js new file mode 100644 index 00000000..699b3d26 --- /dev/null +++ b/src/content/dependencies/generateAlbumSocialEmbed.js @@ -0,0 +1,71 @@ +export default { + contentDependencies: [ + 'generateSocialEmbedDescription', + ], + + extraDependencies: [ + 'absoluteTo', + 'language', + 'to', + 'urls', + ], + + data(album, { + generateSocialEmbedDescription, + }) { + const data = {}; + + data.descriptionData = generateSocialEmbedDescription.data(album); + + data.hasHeading = !empty(album.groups); + + if (data.hasHeading) { + const firstGroup = album.groups[0]; + data.headingGroupName = firstGroup.directory; + data.headingGroupDirectory = firstGroup.directory; + } + + data.albumName = album.name; + data.albumColor = album.color; + + return data; + }, + + generate(data, { + generateSocialEmbedDescription, + + absoluteTo, + language, + to, + urls, + }) { + const socialEmbed = {}; + + if (data.hasHeading) { + socialEmbed.heading = + language.$('albumPage.socialEmbed.heading', { + group: data.headingGroupName, + }); + + socialEmbed.headingLink = + absoluteTo('localized.album', data.headingGroupDirectory); + } else { + socialEmbed.heading = ''; + socialEmbed.headingLink = null; + } + + socialEmbed.title = + language.$('albumPage.socialEmbed.title', { + album: data.albumName, + }); + + socialEmbed.description = generateSocialEmbedDescription(data.descriptionData); + + socialEmbed.image = + '/' + getAlbumCover(album, {to: urls.from('shared.root').to}); + + socialEmbed.color = data.albumColor; + + return socialEmbed; + }, +}; diff --git a/src/content/dependencies/generateAlbumSocialEmbedDescription.js b/src/content/dependencies/generateAlbumSocialEmbedDescription.js new file mode 100644 index 00000000..2bb62596 --- /dev/null +++ b/src/content/dependencies/generateAlbumSocialEmbedDescription.js @@ -0,0 +1,48 @@ +export default { + extraDependencies: ['language'], + + data(album) { + const data = {}; + + const duration = getTotalDuration(album); + + data.hasDuration = duration > 0; + data.hasTracks = album.tracks.length > 0; + data.hasDate = !!album.date; + data.hasAny = (data.hasDuration || data.hasTracks || data.hasDuration); + + if (!data.hasAny) + return data; + + if (data.hasDuration) + data.duration = duration; + + if (data.hasTracks) + data.tracks = album.tracks.length; + + if (data.hasDate) + data.date = album.date; + + return data; + }, + + generate(data, { + language, + }) { + return language.formatString( + 'albumPage.socialEmbed.body' + [ + data.hasDuration && '.withDuration', + data.hasTracks && '.withTracks', + data.hasDate && '.withReleaseDate', + ].filter(Boolean).join(''), + + Object.fromEntries([ + data.hasDuration && + ['duration', language.formatDuration(data.duration)], + data.hasTracks && + ['tracks', language.countTracks(data.tracks, {unit: true})], + data.hasDate && + ['date', language.formatDate(data.date)], + ].filter(Boolean))); + }, +}; diff --git a/src/content/dependencies/generateAlbumStylesheet.js b/src/content/dependencies/generateAlbumStylesheet.js new file mode 100644 index 00000000..575f7d59 --- /dev/null +++ b/src/content/dependencies/generateAlbumStylesheet.js @@ -0,0 +1,59 @@ +export default { + extraDependencies: [ + 'to', + ], + + data: function(album) { + const data = {}; + + data.hasWallpaper = !empty(album.wallpaperArtistContribs); + data.hasBanner = !empty(album.bannerArtistContribs); + + if (data.hasWallpaper) { + data.hasWallpaperStyle = !!album.wallpaperStyle; + data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; + data.wallpaperStyle = album.wallpaperStyle; + } + + if (data.hasBanner) { + data.hasBannerStyle = !!album.bannerStyle; + data.bannerStyle = album.bannerStyle; + } + + return data; + }, + + generate(data, {to}) { + const wallpaperPart = + (data.hasWallpaper + ? [ + `body::before {`, + ` background-image: url("${to(...data.wallpaperPath)}");`, + ...(data.hasWallpaperStyle + ? data.wallpaperStyle + .split('\n') + .map(line => ` ${line}`) + : []), + `}`, + ] + : []); + + const bannerPart = + (data.hasBannerStyle + ? [ + `#banner img {`, + ...data.bannerStyle + .split('\n') + .map(line => ` ${line}`), + `}`, + ] + : []); + + return [ + ...wallpaperPart, + ...bannerPart, + ] + .filter(Boolean) + .join('\n'); + }, +}; diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js new file mode 100644 index 00000000..5cd116d4 --- /dev/null +++ b/src/content/dependencies/index.js @@ -0,0 +1,108 @@ +import chokidar from 'chokidar'; +import EventEmitter from 'events'; +import * as path from 'path'; +import {fileURLToPath} from 'url'; + +import contentFunction from '../../content-function.js'; +import {color, logWarn} from '../../util/cli.js'; +import {annotateFunction} from '../../util/sugar.js'; + +export function watchContentDependencies() { + const events = new EventEmitter(); + const contentDependencies = {}; + + Object.assign(events, { + contentDependencies, + }); + + // Watch adjacent files + const metaPath = fileURLToPath(import.meta.url); + const metaDirname = path.dirname(metaPath); + const watcher = chokidar.watch(metaDirname); + + watcher.on('all', (event, filePath) => { + if (!['add', 'change'].includes(event)) return; + if (filePath === metaPath) return; + handlePathUpdated(filePath); + }); + + watcher.on('unlink', (filePath) => { + if (filePath === metaPath) { + console.error(`Yeowzers content dependencies just got nuked.`); + return; + } + handlePathRemoved(filePath); + }) + + return events; + + function getFunctionName(filePath) { + const shortPath = path.basename(filePath); + const functionName = shortPath.slice(0, -path.extname(shortPath).length); + return functionName; + } + + async function handlePathRemoved(filePath) { + const functionName = getFunctionName(filePath); + delete contentDependencies[functionName]; + } + + async function handlePathUpdated(filePath) { + const functionName = getFunctionName(filePath); + let error = null; + + main: { + let spec; + try { + spec = (await import(`${filePath}?${Date.now()}`)).default; + } catch (caughtError) { + error = caughtError; + error.message = `Error importing: ${error.message}`; + break main; + } + + try { + if (typeof spec.data === 'function') { + annotateFunction(spec.data, {name: functionName, description: 'data'}); + } + + if (typeof spec.generate === 'function') { + annotateFunction(spec.generate, {name: functionName}); + } + } catch (caughtError) { + error = caughtError; + error.message = `Error annotating functions: ${error.message}`; + break main; + } + + let fn; + try { + fn = contentFunction(spec); + } catch (caughtError) { + error = caughtError; + error.message = `Error loading spec: ${error.message}`; + break main; + } + + contentDependencies[functionName] = fn; + } + + if (!error) { + return true; + } + + if (contentDependencies[functionName]) { + logWarn`Failed to import ${functionName} - using existing version`; + } else { + logWarn`Failed to import ${functionName} - no prior version loaded`; + } + + if (typeof error === 'string') { + console.error(color.yellow(error)); + } else { + console.error(error); + } + + return false; + } +} diff --git a/src/misc-templates.js b/src/misc-templates.js index cbdedfe0..e912c121 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -341,66 +341,6 @@ function unbound_getThemeString(color, { ].join('\n'); } -export const u_generateAlbumStylesheet = contentFunction({ - extraDependencies: [ - 'to', - ], - - data: function(album) { - const data = {}; - - data.hasWallpaper = !empty(album.wallpaperArtistContribs); - data.hasBanner = !empty(album.bannerArtistContribs); - - if (data.hasWallpaper) { - data.hasWallpaperStyle = !!album.wallpaperStyle; - data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; - data.wallpaperStyle = album.wallpaperStyle; - } - - if (data.hasBanner) { - data.hasBannerStyle = !!album.bannerStyle; - data.bannerStyle = album.bannerStyle; - } - - return data; - }, - - generate: function generateAlbumStylesheet(data, {to}) { - const wallpaperPart = - (data.hasWallpaper - ? [ - `body::before {`, - ` background-image: url("${to(...data.wallpaperPath)}");`, - ...(data.hasWallpaperStyle - ? data.wallpaperStyle - .split('\n') - .map(line => ` ${line}`) - : []), - `}`, - ] - : []); - - const bannerPart = - (data.hasBannerStyle - ? [ - `#banner img {`, - ...data.bannerStyle - .split('\n') - .map(line => ` ${line}`), - `}`, - ] - : []); - - return [ - ...wallpaperPart, - ...bannerPart, - ] - .filter(Boolean) - .join('\n'); - }, -}); - // Divided track lists function unbound_generateTrackListDividedByGroups(tracks, { diff --git a/src/page/album.js b/src/page/album.js index ab1e1b2f..4cf9fd99 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -26,6 +26,11 @@ export function targets({wikiData}) { } export const dataSteps = { + contentDependencies: [ + 'generateAlbumSocialEmbed', + 'generateAlbumStylesheet', + ], + computePathsForTarget(data, album) { data.hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt); data.hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary); @@ -66,8 +71,8 @@ export const dataSteps = { // TODO: We can't use content-unfulfilled functions here. // But how do we express that these need to be fulfilled // from within data steps? - data.socialEmbedData = u_generateAlbumSocialEmbed.data(album); - data.stylesheetData = u_generateAlbumStylesheet.data(album); + data.socialEmbedData = data.dependencies.generateAlbumSocialEmbed.data(album); + data.stylesheetData = data.dependencies.generateAlbumStylesheet.data(album); data.name = album.name; data.color = album.color; @@ -145,127 +150,6 @@ export const dataSteps = { }, }; -const u_generateAlbumSocialEmbedDescription = contentFunction({ - extraDependencies: ['language'], - - data: function(album) { - const data = {}; - - const duration = getTotalDuration(album); - - data.hasDuration = duration > 0; - data.hasTracks = album.tracks.length > 0; - data.hasDate = !!album.date; - data.hasAny = (data.hasDuration || data.hasTracks || data.hasDuration); - - if (!data.hasAny) - return data; - - if (data.hasDuration) - data.duration = duration; - - if (data.hasTracks) - data.tracks = album.tracks.length; - - if (data.hasDate) - data.date = album.date; - - return data; - }, - - generate: function generateAlbumSocialEmbedDescription(data, { - language, - }) { - return language.formatString( - 'albumPage.socialEmbed.body' + [ - data.hasDuration && '.withDuration', - data.hasTracks && '.withTracks', - data.hasDate && '.withReleaseDate', - ].filter(Boolean).join(''), - - Object.fromEntries([ - data.hasDuration && - ['duration', language.formatDuration(data.duration)], - data.hasTracks && - ['tracks', language.countTracks(data.tracks, {unit: true})], - data.hasDate && - ['date', language.formatDate(data.date)], - ].filter(Boolean))); - }, -}); - -const u_generateAlbumSocialEmbed = contentFunction({ - contentDependencies: [ - 'generateSocialEmbedDescription', - ], - - extraDependencies: [ - 'absoluteTo', - 'language', - 'to', - 'urls', - ], - - data: function(album, { - generateSocialEmbedDescription, - }) { - const data = {}; - - data.descriptionData = generateSocialEmbedDescription.data(album); - - data.hasHeading = !empty(album.groups); - - if (data.hasHeading) { - const firstGroup = album.groups[0]; - data.headingGroupName = firstGroup.directory; - data.headingGroupDirectory = firstGroup.directory; - } - - data.albumName = album.name; - data.albumColor = album.color; - - return data; - }, - - generate: function generateAlbumSocialEmbed(data, { - generateSocialEmbedDescription, - - absoluteTo, - language, - to, - urls, - }) { - const socialEmbed = {}; - - if (data.hasHeading) { - socialEmbed.heading = - language.$('albumPage.socialEmbed.heading', { - group: data.headingGroupName, - }); - - socialEmbed.headingLink = - absoluteTo('localized.album', data.headingGroupDirectory); - } else { - socialEmbed.heading = ''; - socialEmbed.headingLink = null; - } - - socialEmbed.title = - language.$('albumPage.socialEmbed.title', { - album: data.albumName, - }); - - socialEmbed.description = generateSocialEmbedDescription(data.descriptionData); - - socialEmbed.image = - '/' + getAlbumCover(album, {to: urls.from('shared.root').to}); - - socialEmbed.color = data.albumColor; - - return socialEmbed; - }, -}); - const u_generateTrackListItem = contentFunction({ contentDependencies: [ 'generateContributionLinks', -- cgit 1.3.0-6-gf8a5