|
| 1 | +#!/usr/bin/env node |
| 2 | +// Post-build step: emit a CommonJS build alongside the ESM one. |
| 3 | +// |
| 4 | +// Emscripten is configured with -sEXPORT_ES6=1 -sMODULARIZE=1, so the only |
| 5 | +// glue it produces is an ES module (dist/wasm/librosa_wasm.mjs). CommonJS |
| 6 | +// consumers (Electron main, Ableton Extension Host, plain `require()` in Node) |
| 7 | +// cannot load that. Rather than ask emscripten to emit a second flavour (which |
| 8 | +// would need a second, slow wasm link), we transform the existing ESM glue into |
| 9 | +// a `.cjs` with a handful of pure-text substitutions, then write a CJS wrapper |
| 10 | +// that mirrors src/index.ts. This runs after `build:wasm` + `build:ts`, needs no |
| 11 | +// Emscripten toolchain, and is therefore safe to run in CI on the npm publish job. |
| 12 | + |
| 13 | +import { readFileSync, writeFileSync } from "node:fs"; |
| 14 | +import { fileURLToPath } from "node:url"; |
| 15 | +import { dirname, resolve } from "node:path"; |
| 16 | + |
| 17 | +const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); |
| 18 | + |
| 19 | +/** Apply a substitution, failing loudly if the expected text is missing. */ |
| 20 | +function substitute(source, find, replace, label) { |
| 21 | + if (typeof find === "string") { |
| 22 | + if (!source.includes(find)) { |
| 23 | + throw new Error( |
| 24 | + `make-cjs: expected to find ${label} but it was not present — ` + |
| 25 | + `the emscripten glue likely changed; update scripts/make-cjs.mjs.` |
| 26 | + ); |
| 27 | + } |
| 28 | + return source.split(find).join(replace); |
| 29 | + } |
| 30 | + if (!find.test(source)) { |
| 31 | + throw new Error( |
| 32 | + `make-cjs: expected to match ${label} but nothing matched — ` + |
| 33 | + `the emscripten glue likely changed; update scripts/make-cjs.mjs.` |
| 34 | + ); |
| 35 | + } |
| 36 | + return source.replace(find, replace); |
| 37 | +} |
| 38 | + |
| 39 | +// --- 1. Convert the emscripten ESM glue to CommonJS ------------------------- |
| 40 | +// |
| 41 | +// The transform must tolerate different emscripten releases, which emit the |
| 42 | +// Node `require` shim two different ways: |
| 43 | +// * 3.1.x: a top-level static `import{createRequire}from"module";` followed by |
| 44 | +// `var require=createRequire(import.meta.url);` |
| 45 | +// * 4.0.x: an inline `const{createRequire}=await import("module");` + |
| 46 | +// `var require=createRequire(import.meta.url)` inside the Node branch |
| 47 | +// Both forms are removed (a .cjs already has a real `require`), then every |
| 48 | +// `import.meta.url` and the `export default` are rewritten. A post-conversion |
| 49 | +// check fails loudly if any ES-module construct survives, so a future emscripten |
| 50 | +// upgrade can't silently produce a broken `.cjs`. |
| 51 | +{ |
| 52 | + const mjsPath = resolve(pkgRoot, "dist/wasm/librosa_wasm.mjs"); |
| 53 | + let glue = readFileSync(mjsPath, "utf8"); |
| 54 | + |
| 55 | + // A file: URL for the .cjs itself, so `new URL("librosa_wasm.wasm", …)` still |
| 56 | + // resolves the sibling wasm binary at runtime. Named without an "import" |
| 57 | + // substring so the leftover-ESM check below stays unambiguous. |
| 58 | + const banner = |
| 59 | + `const __cjsScriptUrl = require("url").pathToFileURL(__filename).href;\n`; |
| 60 | + |
| 61 | + // Drop the createRequire shim in either form, plus the `require` assignment it |
| 62 | + // feeds. These are best-effort removals; the assertions below verify the net |
| 63 | + // result regardless of which (if any) matched. |
| 64 | + glue = glue |
| 65 | + .replace(/import\s*\{\s*createRequire\s*\}\s*from\s*["']module["']\s*;?/g, "") |
| 66 | + .replace(/const\s*\{\s*createRequire\s*\}\s*=\s*await\s+import\(\s*["']module["']\s*\)\s*;?/g, "") |
| 67 | + .replace(/\b(?:var|const|let)\s+require\s*=\s*createRequire\(\s*import\.meta\.url\s*\)\s*;?/g, ""); |
| 68 | + |
| 69 | + // Every remaining `import.meta.url` -> our CJS stand-in. |
| 70 | + glue = substitute(glue, /import\.meta\.url/g, "__cjsScriptUrl", "import.meta.url"); |
| 71 | + |
| 72 | + // `export default <name>;` -> `module.exports = <name>;` |
| 73 | + glue = substitute(glue, /export\s+default\s+([A-Za-z0-9_$]+)\s*;?/, "module.exports = $1;", "the default export"); |
| 74 | + |
| 75 | + // Fail loudly if any ESM construct survived the conversion. |
| 76 | + const leftovers = [ |
| 77 | + [/\bcreateRequire\b/, "a stray createRequire reference"], |
| 78 | + [/import\.meta/, "a stray import.meta"], |
| 79 | + [/\bexport\s+(?:default|\{|\*|const|function|class)/, "a stray ESM export"], |
| 80 | + [/(?:^|[;\n}])\s*import\s*[{*"'A-Za-z]/, "a stray static ESM import"] |
| 81 | + ]; |
| 82 | + for (const [re, label] of leftovers) { |
| 83 | + if (re.test(glue)) { |
| 84 | + throw new Error( |
| 85 | + `make-cjs: ${label} survived ESM->CJS conversion — ` + |
| 86 | + `the emscripten glue changed; update scripts/make-cjs.mjs.` |
| 87 | + ); |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + writeFileSync(resolve(pkgRoot, "dist/wasm/librosa_wasm.cjs"), banner + glue); |
| 92 | +} |
| 93 | + |
| 94 | +// --- 2. Emit a CommonJS wrapper mirroring the compiled ESM entry ------------ |
| 95 | +// |
| 96 | +// The wrapper logic (arity table + Proxy) is identical; only the module syntax |
| 97 | +// differs. We transform the already-compiled dist/src/index.js so the table |
| 98 | +// stays single-sourced from src/index.ts. |
| 99 | +{ |
| 100 | + const esmPath = resolve(pkgRoot, "dist/src/index.js"); |
| 101 | + let wrapper = readFileSync(esmPath, "utf8"); |
| 102 | + |
| 103 | + wrapper = substitute( |
| 104 | + wrapper, |
| 105 | + `import createModule from "../wasm/librosa_wasm.mjs";`, |
| 106 | + `"use strict";\nconst createModule = require("../wasm/librosa_wasm.cjs");`, |
| 107 | + "the wasm module import" |
| 108 | + ); |
| 109 | + |
| 110 | + wrapper = substitute( |
| 111 | + wrapper, |
| 112 | + `export async function createLibrosa`, |
| 113 | + `async function createLibrosa`, |
| 114 | + "the createLibrosa export" |
| 115 | + ); |
| 116 | + |
| 117 | + wrapper = substitute( |
| 118 | + wrapper, |
| 119 | + `export default createLibrosa;`, |
| 120 | + `module.exports = createLibrosa;\nmodule.exports.createLibrosa = createLibrosa;\nmodule.exports.default = createLibrosa;`, |
| 121 | + "the default export" |
| 122 | + ); |
| 123 | + |
| 124 | + // The sourcemap belongs to the ESM build; drop the now-wrong reference. |
| 125 | + wrapper = wrapper.replace(/\n?\/\/# sourceMappingURL=index\.js\.map\s*$/, "\n"); |
| 126 | + |
| 127 | + writeFileSync(resolve(pkgRoot, "dist/src/index.cjs"), wrapper); |
| 128 | +} |
| 129 | + |
| 130 | +// --- 3. Emit type declarations matching the CJS `module.exports` ------------- |
| 131 | +// |
| 132 | +// The ESM `index.d.ts` describes a default + named export. The CJS runtime sets |
| 133 | +// `module.exports = createLibrosa` (a callable), so the .d.cts must use |
| 134 | +// `export =` with a merged namespace; otherwise `import x = require(pkg)` under |
| 135 | +// node16/nodenext would type the require as a non-callable namespace object. |
| 136 | +// The re-exported type names are derived from types.d.ts so this stays in sync. |
| 137 | +{ |
| 138 | + const typesDts = readFileSync(resolve(pkgRoot, "dist/src/types.d.ts"), "utf8"); |
| 139 | + const typeNames = [ |
| 140 | + ...typesDts.matchAll(/^export\s+(?:declare\s+)?(?:type|interface|class|enum)\s+([A-Za-z0-9_]+)/gm) |
| 141 | + ].map((m) => m[1]); |
| 142 | + if (typeNames.length === 0) { |
| 143 | + throw new Error("make-cjs: found no exported type names in types.d.ts"); |
| 144 | + } |
| 145 | + |
| 146 | + const indent = (names) => names.map((n) => ` ${n}`).join(",\n"); |
| 147 | + const dcts = |
| 148 | + `import type {\n${indent(typeNames)}\n} from "./types.js";\n` + |
| 149 | + `declare function createLibrosa(options?: CreateLibrosaOptions): Promise<Librosa>;\n` + |
| 150 | + `declare namespace createLibrosa {\n` + |
| 151 | + ` export {\n` + |
| 152 | + ` createLibrosa,\n` + |
| 153 | + ` createLibrosa as default,\n` + |
| 154 | + `${typeNames.map((n) => ` ${n}`).join(",\n")}\n` + |
| 155 | + ` };\n` + |
| 156 | + `}\n` + |
| 157 | + `export = createLibrosa;\n`; |
| 158 | + |
| 159 | + writeFileSync(resolve(pkgRoot, "dist/src/index.d.cts"), dcts); |
| 160 | +} |
| 161 | + |
| 162 | +console.log("make-cjs: wrote dist/wasm/librosa_wasm.cjs, dist/src/index.cjs, dist/src/index.d.cts"); |
0 commit comments