anvilsign in

collin/strudel-claude

main / node-tui / loader-hooks.ts
1// Module-resolution patches that stand in for what a Vite build normally does
2// for Strudel. Registered by boot.ts.
3//
4// IMPORTANT: registerHooks() only takes effect for modules resolved *after* it
5// runs, and an ESM graph is fully linked before any of it evaluates. A static
6// `import '@strudel/core'` anywhere in the same graph as this file would
7// therefore be linked before the hooks exist. That's why boot.ts imports the
8// app with a dynamic `import()` — it defers resolution until after this module
9// has evaluated. Don't turn that into a static import.
10import { registerHooks } from 'node:module';
11import { fileURLToPath } from 'node:url';
12
13const WORKLET_SUFFIX = '?audioworklet';
14
15// @strudel/core imports `SalatRepl` from @kabelsalat/web to support kabelsalat
16// DSP graphs. That package ships a browser IIFE bundle, not ESM — Node's
17// CJS named-export detection can't see through it, so linking @strudel/core
18// fails outright with "does not provide an export named 'SalatRepl'".
19//
20// We don't use kabelsalat: it's reached only via the `kabel`/mondo code path,
21// which is a separate DSP language from the patterns we evaluate. Rather than
22// wire up a browser bundle we can't run anyway, hand core a stub that satisfies
23// the import and throws only if a pattern actually tries to use it.
24//
25// The constructor must succeed — repl() instantiates SalatRepl eagerly at setup
26// and only calls evaluate() from its compileKabel() path. Throwing from the
27// constructor takes the whole REPL down at boot.
28const KABELSALAT_STUB = `
29export class SalatRepl {
30 constructor() {}
31 evaluate() {
32 throw new Error('[node-tui] kabelsalat DSP is not available in the Node runtime');
33 }
34}
35export default { SalatRepl };
36`;
37
38registerHooks({
39 resolve(specifier, context, next) {
40 if (specifier === '@kabelsalat/web') {
41 return { url: 'node-tui-stub:kabelsalat', shortCircuit: true };
42 }
43 if (specifier.endsWith(WORKLET_SUFFIX)) {
44 const resolved = next(specifier.slice(0, -WORKLET_SUFFIX.length), context);
45 return { ...resolved, url: resolved.url + WORKLET_SUFFIX, shortCircuit: true };
46 }
47 return next(specifier, context);
48 },
49
50 load(url, context, next) {
51 if (url === 'node-tui-stub:kabelsalat') {
52 return { format: 'module', shortCircuit: true, source: KABELSALAT_STUB };
53 }
54 // superdough's *source* does `import workletsUrl from './worklets.mjs?audioworklet'`,
55 // a Vite-only specifier. Resolve it to the module's path on disk, which
56 // node-web-audio-api's addModule() accepts directly.
57 //
58 // (The published dist bundle inlines the worklets as a data: URL instead and
59 // never hits this path — but superdough resolves to source in some setups,
60 // and this costs nothing.)
61 if (url.endsWith(WORKLET_SUFFIX)) {
62 const path = fileURLToPath(url.slice(0, -WORKLET_SUFFIX.length));
63 return {
64 format: 'module',
65 shortCircuit: true,
66 source: `export default ${JSON.stringify(path)};`,
67 };
68 }
69 return next(url, context);
70 },
71});