« get me outta code hell

data steps: experimental live JS reload infrastructure - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-03-18 20:15:37 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-03-18 20:15:37 -0300
commitc6e1a0b6fb9314186a46cf1352a8685e8aa5fe8d (patch)
tree54d9ba89a99882d635c5504262eccf31fc8f2147 /src
parent4f0d935f1dec0cece23ac661b02486f095b5ee94 (diff)
data steps: experimental live JS reload infrastructure
Diffstat (limited to 'src')
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js71
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbedDescription.js48
-rw-r--r--src/content/dependencies/generateAlbumStylesheet.js59
-rw-r--r--src/content/dependencies/index.js108
-rw-r--r--src/misc-templates.js60
-rw-r--r--src/page/album.js130
6 files changed, 293 insertions, 183 deletions
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',