« get me outta code hell

infra: collect things with wildcard exports - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2026-01-26 13:01:53 -0400
committer(quasar) nebula <qznebula@protonmail.com>2026-01-26 13:02:24 -0400
commitc4a2bd0e7b29abc201d40b7cdae7815a508f8681 (patch)
treeae69def20dcf2d4d41a52c96b7f34fc4a4135720 /src/data
parent828c2994803959be7aff218c98f90bad97d7964c (diff)
infra: collect things with wildcard exports
Removes checking for duplicate class names. I think that's meant
to be an error in ES6 modules (i.e. because two exports would be
sharing the same name), but Node.js seemingly is fine with it,
so just be careful for now.
Diffstat (limited to 'src/data')
-rw-r--r--src/data/things/index.js295
-rw-r--r--src/data/things/init.js208
2 files changed, 228 insertions, 275 deletions
diff --git a/src/data/things/index.js b/src/data/things/index.js
index 35cd8cf2..bf3df9a7 100644
--- a/src/data/things/index.js
+++ b/src/data/things/index.js
@@ -1,275 +1,20 @@
-import * as path from 'node:path';
-import {fileURLToPath} from 'node:url';
-
-import {openAggregate, showAggregate} from '#aggregate';
-import CacheableObject from '#cacheable-object';
-import {logError} from '#cli';
-import {compositeFrom} from '#composite';
-import * as serialize from '#serialize';
-import {empty} from '#sugar';
-import Thing from '#thing';
-
-import * as additionalFileClasses from './additional-file.js';
-import * as additionalNameClasses from './additional-name.js';
-import * as albumClasses from './album.js';
-import * as artTagClasses from './art-tag.js';
-import * as artistClasses from './artist.js';
-import * as artworkClasses from './artwork.js';
-import * as contentClasses from './content.js';
-import * as contributionClasses from './contribution.js';
-import * as flashClasses from './flash.js';
-import * as groupClasses from './group.js';
-import * as homepageLayoutClasses from './homepage-layout.js';
-import * as languageClasses from './language.js';
-import * as musicVideoClasses from './music-video.js';
-import * as newsEntryClasses from './news-entry.js';
-import * as sortingRuleClasses from './sorting-rule.js';
-import * as staticPageClasses from './static-page.js';
-import * as trackClasses from './track.js';
-import * as wikiInfoClasses from './wiki-info.js';
-
-const allClassLists = {
-  'additional-file.js': additionalFileClasses,
-  'additional-name.js': additionalNameClasses,
-  'album.js': albumClasses,
-  'art-tag.js': artTagClasses,
-  'artist.js': artistClasses,
-  'artwork.js': artworkClasses,
-  'content.js': contentClasses,
-  'contribution.js': contributionClasses,
-  'flash.js': flashClasses,
-  'group.js': groupClasses,
-  'homepage-layout.js': homepageLayoutClasses,
-  'language.js': languageClasses,
-  'music-video.js': musicVideoClasses,
-  'news-entry.js': newsEntryClasses,
-  'sorting-rule.js': sortingRuleClasses,
-  'static-page.js': staticPageClasses,
-  'track.js': trackClasses,
-  'wiki-info.js': wikiInfoClasses,
-};
-
-let allClasses = Object.create(null);
-
-// src/data/things/index.js -> src/
-const __dirname = path.dirname(
-  path.resolve(
-    fileURLToPath(import.meta.url),
-    '../..'));
-
-function niceShowAggregate(error, ...opts) {
-  showAggregate(error, {
-    pathToFileURL: (f) => path.relative(__dirname, fileURLToPath(f)),
-    showClasses: false,
-    ...opts,
-  });
-}
-
-function errorDuplicateClassNames() {
-  const locationDict = Object.create(null);
-
-  for (const [location, classes] of Object.entries(allClassLists)) {
-    for (const className of Object.keys(classes)) {
-      if (className in locationDict) {
-        locationDict[className].push(location);
-      } else {
-        locationDict[className] = [location];
-      }
-    }
-  }
-
-  let success = true;
-
-  for (const [className, locations] of Object.entries(locationDict)) {
-    if (locations.length === 1) {
-      continue;
-    }
-
-    logError`Thing class name ${`"${className}"`} is defined more than once: ${locations.join(', ')}`;
-    success = false;
-  }
-
-  return success;
-}
-
-function flattenClassLists() {
-  let remaining = [];
-  for (const classes of Object.values(allClassLists)) {
-    for (const constructor of Object.values(classes)) {
-      if (typeof constructor !== 'function') continue;
-      if (!(constructor.prototype instanceof Thing)) continue;
-      remaining.push(constructor);
-    }
-  }
-
-  let sorted = [];
-  while (true) {
-    if (sorted[0]) {
-      const superclass = Object.getPrototypeOf(sorted[0]);
-      if (superclass !== Thing) {
-        if (sorted.includes(superclass)) {
-          sorted.unshift(...sorted.splice(sorted.indexOf(superclass), 1));
-        } else {
-          sorted.unshift(superclass);
-        }
-        continue;
-      }
-    }
-
-    if (!empty(remaining)) {
-      sorted.unshift(remaining.shift());
-    } else {
-      break;
-    }
-  }
-
-  for (const constructor of sorted) {
-    allClasses[constructor.name] = constructor;
-  }
-}
-
-function descriptorAggregateHelper({
-  showFailedClasses,
-  message,
-  op,
-}) {
-  const failureSymbol = Symbol();
-  const aggregate = openAggregate({
-    message,
-    returnOnFail: failureSymbol,
-  });
-
-  const failedClasses = [];
-
-  for (const [name, constructor] of Object.entries(allClasses)) {
-    const result = aggregate.call(op, constructor);
-
-    if (result === failureSymbol) {
-      failedClasses.push(name);
-    }
-  }
-
-  try {
-    aggregate.close();
-    return true;
-  } catch (error) {
-    niceShowAggregate(error);
-    showFailedClasses(failedClasses);
-
-    /*
-    if (error.errors) {
-      for (const sub of error.errors) {
-        console.error(sub);
-      }
-    }
-    */
-
-    return false;
-  }
-}
-
-function evaluatePropertyDescriptors() {
-  const opts = {...allClasses};
-
-  return descriptorAggregateHelper({
-    message: `Errors evaluating Thing class property descriptors`,
-
-    op(constructor) {
-      if (!constructor[Thing.getPropertyDescriptors]) {
-        throw new Error(`Missing [Thing.getPropertyDescriptors] function`);
-      }
-
-      const results = constructor[Thing.getPropertyDescriptors](opts);
-
-      for (const [key, value] of Object.entries(results)) {
-        if (Array.isArray(value)) {
-          results[key] = compositeFrom({
-            annotation: `${constructor.name}.${key}`,
-            compose: false,
-            steps: value,
-          });
-        } else if (value.toResolvedComposition) {
-          results[key] = compositeFrom(value.toResolvedComposition());
-        }
-      }
-
-      constructor[CacheableObject.propertyDescriptors] =
-        Object.create(constructor[CacheableObject.propertyDescriptors] ?? null);
-
-      Object.assign(constructor[CacheableObject.propertyDescriptors], results);
-    },
-
-    showFailedClasses(failedClasses) {
-      logError`Failed to evaluate property descriptors for classes: ${failedClasses.join(', ')}`;
-    },
-  });
-}
-
-function evaluateSerializeDescriptors() {
-  const opts = {...allClasses, serialize};
-
-  return descriptorAggregateHelper({
-    message: `Errors evaluating Thing class serialize descriptors`,
-
-    op(constructor) {
-      if (!constructor[Thing.getSerializeDescriptors]) {
-        return;
-      }
-
-      constructor[serialize.serializeDescriptors] =
-        constructor[Thing.getSerializeDescriptors](opts);
-    },
-
-    showFailedClasses(failedClasses) {
-      logError`Failed to evaluate serialize descriptors for classes: ${failedClasses.join(', ')}`;
-    },
-  });
-}
-
-function finalizeYamlDocumentSpecs() {
-  return descriptorAggregateHelper({
-    message: `Errors finalizing Thing YAML document specs`,
-
-    op(constructor) {
-      const superclass = Object.getPrototypeOf(constructor);
-      if (
-        constructor[Thing.yamlDocumentSpec] &&
-        superclass[Thing.yamlDocumentSpec]
-      ) {
-        constructor[Thing.yamlDocumentSpec] =
-          Thing.extendDocumentSpec(superclass, constructor[Thing.yamlDocumentSpec]);
-      }
-    },
-
-    showFailedClasses(failedClasses) {
-      logError`Failed to finalize YAML document specs for classes: ${failedClasses.join(', ')}`;
-    },
-  });
-}
-
-function finalizeCacheableObjectPrototypes() {
-  return descriptorAggregateHelper({
-    message: `Errors finalizing Thing class prototypes`,
-
-    op(constructor) {
-      constructor.finalizeCacheableObjectPrototype();
-    },
-
-    showFailedClasses(failedClasses) {
-      logError`Failed to finalize cacheable object prototypes for classes: ${failedClasses.join(', ')}`;
-    },
-  });
-}
-
-if (!errorDuplicateClassNames()) process.exit(1);
-
-flattenClassLists();
-
-if (!evaluatePropertyDescriptors()) process.exit(1);
-if (!evaluateSerializeDescriptors()) process.exit(1);
-if (!finalizeYamlDocumentSpecs()) process.exit(1);
-if (!finalizeCacheableObjectPrototypes()) process.exit(1);
-
-Object.assign(allClasses, {Thing});
-
-export default allClasses;
+// Not actually the entry point for #things - that's init.js in this folder.
+
+export * from './additional-file.js';
+export * from './additional-name.js';
+export * from './album.js';
+export * from './art-tag.js';
+export * from './artist.js';
+export * from './artwork.js';
+export * from './content.js';
+export * from './contribution.js';
+export * from './flash.js';
+export * from './group.js';
+export * from './homepage-layout.js';
+export * from './language.js';
+export * from './music-video.js';
+export * from './news-entry.js';
+export * from './sorting-rule.js';
+export * from './static-page.js';
+export * from './track.js';
+export * from './wiki-info.js';
diff --git a/src/data/things/init.js b/src/data/things/init.js
new file mode 100644
index 00000000..e705f626
--- /dev/null
+++ b/src/data/things/init.js
@@ -0,0 +1,208 @@
+// This is the actual entry point for #things.
+
+import * as path from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+import {openAggregate, showAggregate} from '#aggregate';
+import CacheableObject from '#cacheable-object';
+import {logError} from '#cli';
+import {compositeFrom} from '#composite';
+import * as serialize from '#serialize';
+import {empty} from '#sugar';
+import Thing from '#thing';
+
+import * as indexExports from './index.js';
+
+const thingConstructors = Object.create(null);
+
+// src/data/things/index.js -> src/
+const __dirname = path.dirname(
+  path.resolve(
+    fileURLToPath(import.meta.url),
+    '../..'));
+
+function niceShowAggregate(error, ...opts) {
+  showAggregate(error, {
+    pathToFileURL: (f) => path.relative(__dirname, fileURLToPath(f)),
+    showClasses: false,
+    ...opts,
+  });
+}
+
+function sortThingConstructors() {
+  let remaining = [];
+  for (const constructor of Object.values(indexExports)) {
+    if (typeof constructor !== 'function') continue;
+    if (!(constructor.prototype instanceof Thing)) continue;
+    remaining.push(constructor);
+  }
+
+  let sorted = [];
+  while (true) {
+    if (sorted[0]) {
+      const superclass = Object.getPrototypeOf(sorted[0]);
+      if (superclass !== Thing) {
+        if (sorted.includes(superclass)) {
+          sorted.unshift(...sorted.splice(sorted.indexOf(superclass), 1));
+        } else {
+          sorted.unshift(superclass);
+        }
+        continue;
+      }
+    }
+
+    if (!empty(remaining)) {
+      sorted.unshift(remaining.shift());
+    } else {
+      break;
+    }
+  }
+
+  for (const constructor of sorted) {
+    thingConstructors[constructor.name] = constructor;
+  }
+}
+
+function descriptorAggregateHelper({
+  showFailedClasses,
+  message,
+  op,
+}) {
+  const failureSymbol = Symbol();
+  const aggregate = openAggregate({
+    message,
+    returnOnFail: failureSymbol,
+  });
+
+  const failedClasses = [];
+
+  for (const [name, constructor] of Object.entries(thingConstructors)) {
+    const result = aggregate.call(op, constructor);
+
+    if (result === failureSymbol) {
+      failedClasses.push(name);
+    }
+  }
+
+  try {
+    aggregate.close();
+    return true;
+  } catch (error) {
+    niceShowAggregate(error);
+    showFailedClasses(failedClasses);
+
+    /*
+    if (error.errors) {
+      for (const sub of error.errors) {
+        console.error(sub);
+      }
+    }
+    */
+
+    return false;
+  }
+}
+
+function evaluatePropertyDescriptors() {
+  const opts = {...thingConstructors};
+
+  return descriptorAggregateHelper({
+    message: `Errors evaluating Thing class property descriptors`,
+
+    op(constructor) {
+      if (!constructor[Thing.getPropertyDescriptors]) {
+        throw new Error(`Missing [Thing.getPropertyDescriptors] function`);
+      }
+
+      const results = constructor[Thing.getPropertyDescriptors](opts);
+
+      for (const [key, value] of Object.entries(results)) {
+        if (Array.isArray(value)) {
+          results[key] = compositeFrom({
+            annotation: `${constructor.name}.${key}`,
+            compose: false,
+            steps: value,
+          });
+        } else if (value.toResolvedComposition) {
+          results[key] = compositeFrom(value.toResolvedComposition());
+        }
+      }
+
+      constructor[CacheableObject.propertyDescriptors] =
+        Object.create(constructor[CacheableObject.propertyDescriptors] ?? null);
+
+      Object.assign(constructor[CacheableObject.propertyDescriptors], results);
+    },
+
+    showFailedClasses(failedClasses) {
+      logError`Failed to evaluate property descriptors for classes: ${failedClasses.join(', ')}`;
+    },
+  });
+}
+
+function evaluateSerializeDescriptors() {
+  const opts = {...thingConstructors, serialize};
+
+  return descriptorAggregateHelper({
+    message: `Errors evaluating Thing class serialize descriptors`,
+
+    op(constructor) {
+      if (!constructor[Thing.getSerializeDescriptors]) {
+        return;
+      }
+
+      constructor[serialize.serializeDescriptors] =
+        constructor[Thing.getSerializeDescriptors](opts);
+    },
+
+    showFailedClasses(failedClasses) {
+      logError`Failed to evaluate serialize descriptors for classes: ${failedClasses.join(', ')}`;
+    },
+  });
+}
+
+function finalizeYamlDocumentSpecs() {
+  return descriptorAggregateHelper({
+    message: `Errors finalizing Thing YAML document specs`,
+
+    op(constructor) {
+      const superclass = Object.getPrototypeOf(constructor);
+      if (
+        constructor[Thing.yamlDocumentSpec] &&
+        superclass[Thing.yamlDocumentSpec]
+      ) {
+        constructor[Thing.yamlDocumentSpec] =
+          Thing.extendDocumentSpec(superclass, constructor[Thing.yamlDocumentSpec]);
+      }
+    },
+
+    showFailedClasses(failedClasses) {
+      logError`Failed to finalize YAML document specs for classes: ${failedClasses.join(', ')}`;
+    },
+  });
+}
+
+function finalizeCacheableObjectPrototypes() {
+  return descriptorAggregateHelper({
+    message: `Errors finalizing Thing class prototypes`,
+
+    op(constructor) {
+      constructor.finalizeCacheableObjectPrototype();
+    },
+
+    showFailedClasses(failedClasses) {
+      logError`Failed to finalize cacheable object prototypes for classes: ${failedClasses.join(', ')}`;
+    },
+  });
+}
+
+sortThingConstructors();
+
+if (!evaluatePropertyDescriptors()) process.exit(1);
+if (!evaluateSerializeDescriptors()) process.exit(1);
+if (!finalizeYamlDocumentSpecs()) process.exit(1);
+if (!finalizeCacheableObjectPrototypes()) process.exit(1);
+
+Object.assign(thingConstructors, {Thing});
+
+export default thingConstructors;