Problem
js/hyperaudio-lite.js is currently a single classic script — it declares class HyperaudioLite at the top level, which becomes a global when loaded via <script src=...>. That works for the example HTML files but is awkward for everyone else:
- Modern frontend projects (Vite, webpack, Rollup, etc.) want
import { HyperaudioLite } from 'hyperaudio-lite', which requires an export.
- Node-based tooling (tests, server-side rendering, build scripts) wants
const { HyperaudioLite } = require('hyperaudio-lite'), which requires module.exports.
- Trying to add
export { HyperaudioLite }; to the current file makes it a syntactically-invalid classic script — browsers loading it with <script src=...> would throw a SyntaxError.
Today, downstream consumers vendor the file and patch in the missing export/require line by hand. This is annoying and easy to get out of sync when Hyperaudio Lite updates.
Proposed direction (zero-build, low-maintenance)
Ship three sibling files in js/:
hyperaudio-lite.js — current classic-script form, unchanged. Works in <script src=...>.
hyperaudio-lite.mjs — same code + export { HyperaudioLite }; at the bottom. Works with ESM import.
hyperaudio-lite.cjs — same code + module.exports = { HyperaudioLite }; at the bottom. Works with require.
Plus an exports map in package.json so consumers automatically get the right file:
{
"exports": {
".": {
"import": "./js/hyperaudio-lite.mjs",
"require": "./js/hyperaudio-lite.cjs",
"browser": "./js/hyperaudio-lite.js",
"default": "./js/hyperaudio-lite.mjs"
}
},
"main": "./js/hyperaudio-lite.cjs",
"module": "./js/hyperaudio-lite.mjs"
}
Keeping the three files in sync
Two options:
a. Convention. Treat hyperaudio-lite.js as the source of truth. A small npm script (npm run sync-modules) appends the export/require line into .mjs / .cjs. Run it as a prepublishOnly and document in CONTRIBUTING.
b. Tooling. Author in ESM, use tsup / esbuild / unbuild to emit all three (plus an IIFE for the classic-script case) into dist/. ~10 lines of config, no source-level changes.
(a) preserves the project's "no build step" ethos. (b) is the modern standard for npm-published libraries. Either is fine — (a) probably fits this project better given its size.
Why not just one universal-shim file
It's possible to write a single file that supports classic-script and CJS via a runtime sniff:
class HyperaudioLite { /* ... */ }
if (typeof module !== 'undefined' && module.exports) {
module.exports = { HyperaudioLite };
} else if (typeof globalThis !== 'undefined') {
globalThis.HyperaudioLite = HyperaudioLite;
}
But ESM is not addable to that same file — top-level export makes the whole file an ES module, which is incompatible with classic-script loading. So one file can serve at most two of the three consumption modes, and the missing one is the one that arguably matters most going forward (ESM).
Migration impact
- Existing
<script src="js/hyperaudio-lite.js"> users: zero change.
- Downstream npm/bundler users: stop vendoring + patching, just
import { HyperaudioLite } from 'hyperaudio-lite'.
- Jest tests: switch the
require in __TEST__/hyperaudio-lite.test.js to point at the .cjs file (or leave alone and let the exports map resolve it).
Related
This pairs naturally with #217 (options-object constructor) — both are "make HLE pleasant to integrate" changes that could share a release window (2.5.0 or 3.0.0).
Problem
js/hyperaudio-lite.jsis currently a single classic script — it declaresclass HyperaudioLiteat the top level, which becomes a global when loaded via<script src=...>. That works for the example HTML files but is awkward for everyone else:import { HyperaudioLite } from 'hyperaudio-lite', which requires anexport.const { HyperaudioLite } = require('hyperaudio-lite'), which requiresmodule.exports.export { HyperaudioLite };to the current file makes it a syntactically-invalid classic script — browsers loading it with<script src=...>would throw a SyntaxError.Today, downstream consumers vendor the file and patch in the missing export/require line by hand. This is annoying and easy to get out of sync when Hyperaudio Lite updates.
Proposed direction (zero-build, low-maintenance)
Ship three sibling files in
js/:hyperaudio-lite.js— current classic-script form, unchanged. Works in<script src=...>.hyperaudio-lite.mjs— same code +export { HyperaudioLite };at the bottom. Works with ESMimport.hyperaudio-lite.cjs— same code +module.exports = { HyperaudioLite };at the bottom. Works withrequire.Plus an
exportsmap inpackage.jsonso consumers automatically get the right file:{ "exports": { ".": { "import": "./js/hyperaudio-lite.mjs", "require": "./js/hyperaudio-lite.cjs", "browser": "./js/hyperaudio-lite.js", "default": "./js/hyperaudio-lite.mjs" } }, "main": "./js/hyperaudio-lite.cjs", "module": "./js/hyperaudio-lite.mjs" }Keeping the three files in sync
Two options:
a. Convention. Treat
hyperaudio-lite.jsas the source of truth. A small npm script (npm run sync-modules) appends the export/require line into.mjs/.cjs. Run it as aprepublishOnlyand document inCONTRIBUTING.b. Tooling. Author in ESM, use
tsup/esbuild/unbuildto emit all three (plus an IIFE for the classic-script case) intodist/. ~10 lines of config, no source-level changes.(a) preserves the project's "no build step" ethos. (b) is the modern standard for npm-published libraries. Either is fine — (a) probably fits this project better given its size.
Why not just one universal-shim file
It's possible to write a single file that supports classic-script and CJS via a runtime sniff:
But ESM is not addable to that same file — top-level
exportmakes the whole file an ES module, which is incompatible with classic-script loading. So one file can serve at most two of the three consumption modes, and the missing one is the one that arguably matters most going forward (ESM).Migration impact
<script src="js/hyperaudio-lite.js">users: zero change.import { HyperaudioLite } from 'hyperaudio-lite'.requirein__TEST__/hyperaudio-lite.test.jsto point at the.cjsfile (or leave alone and let theexportsmap resolve it).Related
This pairs naturally with #217 (options-object constructor) — both are "make HLE pleasant to integrate" changes that could share a release window (2.5.0 or 3.0.0).