@react-synth/synth 0.0.7-alpha → 0.2.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,187 +1,361 @@
1
- # React Synth šŸŽ¹
2
-
3
- This is a fun little experiment showing that React API can be used outside of
4
- browser environment to render... music instead of HTML.
5
-
6
- Should you use it? I don't know, you are an adult.
7
-
8
- ## How It Works
9
-
10
- Write music using React components:
11
-
12
- ```tsx
13
- // song.tsx
14
- import {
15
- Chord,
16
- Loop,
17
- Note,
18
- Sample,
19
- Sequence,
20
- Synth,
21
- Track,
22
- } from "@react-synth/synth";
23
-
24
- export default function MySong() {
25
- return (
26
- <Track bpm={120}>
27
- {/* Simple kick drum pattern */}
28
- <Loop id="kick" interval={1}>
29
- <Sample name="bd_haus" amp={2} />
30
- </Loop>
31
-
32
- {/* Melody arpeggio with prophet synth */}
33
- <Loop id="melody" interval={2}>
34
- <Synth type="prophet">
35
- <Sequence interval={0.25}>
36
- <Note note="C4" />
37
- <Note note="E4" />
38
- <Note note="G4" />
39
- <Note note="C5" />
40
- </Sequence>
41
- </Synth>
42
- </Loop>
43
-
44
- {/* Chord progression */}
45
- <Loop id="pads" interval={4}>
46
- <Synth type="hollow">
47
- <Chord notes="Am7" release={4} amp={0.5} />
48
- </Synth>
49
- </Loop>
50
- </Track>
51
- );
52
- }
53
- ```
54
-
55
- Then run it with:
56
-
57
- ```bash
58
- npx react-synth song.tsx
59
- ```
60
-
61
- ## Components
62
-
63
- ### `<Track bpm={number}>`
64
-
65
- Root component that sets the tempo and provides audio context.
66
-
67
- ### `<Loop id={string} interval={number}>`
68
-
69
- Repeats its children every `interval` beats.
70
-
71
- ### `<Synth type={string}>`
72
-
73
- Defines the synthesizer for child `Note` and `Chord` components.
74
-
75
- Available presets: `"sine"`, `"saw"`, `"square"`, `"tri"`, `"prophet"`,
76
- `"hollow"`, `"dark_ambience"`, `"bass"`, `"pluck"`
77
-
78
- ### `<Note>`
79
-
80
- Plays a single note with ADSR envelope.
81
-
82
- - `note` - Note name ("C4", "A#3") or frequency in Hz
83
- - `amp` - Volume 0-1 (default: 0.3)
84
- - `attack`, `decay`, `sustain`, `release` - ADSR envelope in beats
85
- - `oscillator` - Override synth oscillator type
86
- - `filter` - Override filter settings `{ cutoff, resonance, type }`
87
- - `voices` - Override voice settings `{ count, detune, spread }`
88
-
89
- ### `<Chord notes={string | array}>`
90
-
91
- Plays multiple notes simultaneously.
92
-
93
- - `notes` - Chord name ("Cmaj7", "Am:4") or array of notes (["C4", "E4", "G4"])
94
- - Same ADSR and synth overrides as `<Note>`
95
-
96
- ### `<Sample name={string}>`
97
-
98
- Plays an audio sample file.
99
-
100
- - `name` - Sample name (without extension)
101
- - `amp` - Volume
102
- - `cutoff` - Filter cutoff (MIDI note number)
103
- - `rate` - Playback rate
104
- - `pan` - Stereo pan (-1 to 1)
105
-
106
- ### `<Sequence interval={number}>`
107
-
108
- Plays children one after another with `interval` beats between each.
109
-
110
- ## Installation
111
-
112
- ```bash
113
- npm install @react-synth/synth
114
- ```
115
-
116
- ## CLI Usage
117
-
118
- ```bash
119
- # Run with hot reload
120
- npx react-synth song.tsx
121
-
122
- # Or during development
123
- npm run dev song.tsx
124
- ```
125
-
126
- ## Prerequisites
127
-
128
- - Node.js v18+
129
-
130
- ## Development
131
-
132
- ```bash
133
- # Install dependencies
134
- npm install
135
-
136
- # Build the library
137
- npm run build
138
-
139
- # Run type checking
140
- npm run typecheck
141
-
142
- # Live coding with hot reload
143
- npm run dev examples/simple/simple.tsx
144
- ```
145
-
146
- ## Project Structure
147
-
148
- ```
149
- react-synth/
150
- ā”œā”€ā”€ src/
151
- │ ā”œā”€ā”€ index.ts # Main exports
152
- │ ā”œā”€ā”€ audio/
153
- │ │ ā”œā”€ā”€ scheduler.ts # Beat scheduling
154
- │ │ └── sampleLoader.ts # Audio sample loading
155
- │ ā”œā”€ā”€ components/
156
- │ │ ā”œā”€ā”€ Track.tsx # Root component
157
- │ │ ā”œā”€ā”€ Loop.tsx # Looping
158
- │ │ ā”œā”€ā”€ Note/ # Oscillator notes
159
- │ │ ā”œā”€ā”€ Chord.tsx # Chord playback
160
- │ │ ā”œā”€ā”€ Sample/ # Sample playback
161
- │ │ ā”œā”€ā”€ Synth/ # Synth presets
162
- │ │ └── Sequence.tsx # Sequential playback
163
- │ ā”œā”€ā”€ types/
164
- │ │ └── music.ts # Type definitions
165
- │ └── utils/
166
- │ ā”œā”€ā”€ envelope.ts # ADSR envelope
167
- │ ā”œā”€ā”€ line.ts # Value interpolation
168
- │ └── notes.ts # Note/chord utilities
169
- ā”œā”€ā”€ cli/
170
- │ └── cli.ts # CLI entry point (includes React + JSDOM setup)
171
- ā”œā”€ā”€ examples/
172
- │ └── simple/
173
- │ └── simple.tsx # Demo song
174
- ā”œā”€ā”€ dist/ # Build output
175
- ā”œā”€ā”€ package.json
176
- ā”œā”€ā”€ tsconfig.json
177
- └── vite.config.ts
178
- ```
179
-
180
- ## Inspired By
181
-
182
- - [Sonic Pi](https://sonic-pi.net/) - The original live coding synth
183
- - React's declarative component model
184
-
185
- ## License
186
-
187
- MIT
1
+ # React Synth šŸŽ¹
2
+
3
+ This is a fun little experiment showing that React API can be used outside of
4
+ browser environment to render... music instead of HTML.
5
+
6
+ Should you use it? I don't know, you are an adult.
7
+
8
+ ## How It Works
9
+
10
+ Init new repository and install react-synth and its dependencies:
11
+
12
+ ```bash
13
+ npm init
14
+ npm i @react-synth/synth react
15
+ npm i -D @types/react
16
+ ```
17
+
18
+ Then create new `.tsx` file. React-synth requires created file to have default
19
+ export with ReactNode. For example, you can paste below code:
20
+
21
+ ```tsx
22
+ // song.tsx
23
+ import React from "react";
24
+ import {
25
+ Chord,
26
+ Loop,
27
+ Note,
28
+ Sample,
29
+ Sequence,
30
+ Synth,
31
+ Track,
32
+ } from "@react-synth/synth";
33
+
34
+ export default function MySong() {
35
+ return (
36
+ <Track bpm={120}>
37
+ {/* Simple kick drum pattern */}
38
+ <Loop id="kick" interval={1}>
39
+ <Sample name="bd_haus" amp={2} />
40
+ </Loop>
41
+
42
+ {/* Melody arpeggio with prophet synth */}
43
+ <Loop id="melody" interval={2}>
44
+ <Synth type="prophet">
45
+ <Sequence interval={0.25}>
46
+ <Note note="C4" />
47
+ <Note note="E4" />
48
+ <Note note="G4" />
49
+ <Note note="C5" />
50
+ </Sequence>
51
+ </Synth>
52
+ </Loop>
53
+
54
+ {/* Chord progression */}
55
+ <Loop id="pads" interval={4}>
56
+ <Synth type="hollow">
57
+ <Chord notes="Am7" release={4} amp={0.5} />
58
+ </Synth>
59
+ </Loop>
60
+ </Track>
61
+ );
62
+ }
63
+ ```
64
+
65
+ Then run it with:
66
+
67
+ ```bash
68
+ npx react-synth song.tsx
69
+ ```
70
+
71
+ Now any change made to the code will cause hot reload without disruption.
72
+
73
+ ## Components
74
+
75
+ ### `<Track>`
76
+
77
+ Root component that sets the tempo and provides audio context. All other
78
+ components must be nested inside a Track.
79
+
80
+ | Prop | Type | Default | Description |
81
+ | ---------- | ----------- | ------- | --------------------------------------- |
82
+ | `bpm` | `number` | — | **Required.** Tempo in beats per minute |
83
+ | `children` | `ReactNode` | — | Child components to render |
84
+
85
+ ```tsx
86
+ <Track bpm={120}>
87
+ {/* Your music components here */}
88
+ </Track>;
89
+ ```
90
+
91
+ ---
92
+
93
+ ### `<Loop>`
94
+
95
+ Repeats its children at a specified beat interval. Use this for drum patterns,
96
+ repeating melodies, or any cyclical musical phrase.
97
+
98
+ | Prop | Type | Default | Description |
99
+ | ---------- | ----------- | ------- | --------------------------------------------------- |
100
+ | `id` | `string` | — | **Required.** Unique identifier for this loop |
101
+ | `interval` | `number` | — | **Required.** Interval in beats between repetitions |
102
+ | `children` | `ReactNode` | — | Content to play on each loop iteration |
103
+
104
+ ```tsx
105
+ {/* Kick drum every beat */}
106
+ <Loop id="kick" interval={1}>
107
+ <Sample name="bd_haus" />
108
+ </Loop>;
109
+
110
+ {/* Chord progression every 4 beats */}
111
+ <Loop id="chords" interval={4}>
112
+ <Chord notes="Am7" />
113
+ </Loop>;
114
+ ```
115
+
116
+ ---
117
+
118
+ ### `<Sequence>`
119
+
120
+ Plays children one after another with a specified interval between each. Each
121
+ child is played at its index position Ɨ interval beats.
122
+
123
+ | Prop | Type | Default | Description |
124
+ | ---------- | ----------- | ------- | --------------------------------------------- |
125
+ | `interval` | `number` | — | **Required.** Time between each step in beats |
126
+ | `children` | `ReactNode` | — | Steps to play in order |
127
+
128
+ ```tsx
129
+ {/* Arpeggio: C-E-G-C played as 16th notes */}
130
+ <Sequence interval={0.25}>
131
+ <Note note="C4" />
132
+ <Note note="E4" />
133
+ <Note note="G4" />
134
+ <Note note="C5" />
135
+ </Sequence>;
136
+
137
+ {/* Drum pattern */}
138
+ <Sequence interval={0.5}>
139
+ <Sample name="bd_haus" />
140
+ <Sample name="drum_snare_soft" />
141
+ <Sample name="bd_haus" />
142
+ <Sample name="drum_snare_soft" />
143
+ </Sequence>;
144
+ ```
145
+
146
+ ---
147
+
148
+ ### `<Synth>`
149
+
150
+ Defines the synthesizer configuration for child `Note` and `Chord` components.
151
+ Provides preset sounds and allows customization of oscillator, filter, and voice
152
+ parameters.
153
+
154
+ | Prop | Type | Default | Description |
155
+ | ------------ | ----------------------- | ------- | --------------------------------------------------------------------- |
156
+ | `type` | `SynthType` | — | **Required.** Synth preset name (see below) |
157
+ | `oscillator` | `OscillatorType` | — | Override oscillator: `"sine"`, `"square"`, `"sawtooth"`, `"triangle"` |
158
+ | `filter` | `Partial<FilterConfig>` | — | Override filter settings |
159
+ | `voices` | `Partial<VoiceConfig>` | — | Override voice/unison settings |
160
+ | `children` | `ReactNode` | — | Note/Chord components to apply this synth to |
161
+
162
+ **Available Synth Presets:**
163
+
164
+ | Preset | Description |
165
+ | ----------------- | ---------------------------------------------------- |
166
+ | `"sine"` | Pure sine wave - clean, fundamental tone |
167
+ | `"saw"` | Sawtooth wave - bright, harmonically rich |
168
+ | `"square"` | Square wave - hollow, clarinet-like |
169
+ | `"tri"` | Triangle wave - soft, flute-like |
170
+ | `"prophet"` | Prophet-5 inspired - warm with detuned voices |
171
+ | `"hollow"` | Ethereal pad - filtered square, atmospheric |
172
+ | `"dark_ambience"` | Deep atmospheric pad - low-passed with many voices |
173
+ | `"bass"` | Punchy bass - filtered sawtooth |
174
+ | `"pluck"` | Bright plucked string - square wave with high cutoff |
175
+
176
+ **Filter Config:**
177
+
178
+ | Property | Type | Description |
179
+ | ----------- | ------------------ | --------------------------------------------- |
180
+ | `type` | `FilterType` | `"lowpass"`, `"highpass"`, `"bandpass"`, etc. |
181
+ | `cutoff` | `number` or `Line` | Cutoff frequency in Hz, or a Line pattern |
182
+ | `resonance` | `number` | Filter resonance/Q factor |
183
+
184
+ **Voice Config:**
185
+
186
+ | Property | Type | Description |
187
+ | -------- | -------- | ------------------------------------ |
188
+ | `count` | `number` | Number of oscillator voices (unison) |
189
+ | `detune` | `number` | Detune spread in cents |
190
+ | `spread` | `number` | Stereo spread 0-1 |
191
+
192
+ ```tsx
193
+ {/* Using a preset */}
194
+ <Synth type="prophet">
195
+ <Note note="C4" />
196
+ </Synth>;
197
+
198
+ {/* Overriding filter settings */}
199
+ <Synth type="saw" filter={{ cutoff: 2000, resonance: 8 }}>
200
+ <Chord notes="Am7" />
201
+ </Synth>;
202
+
203
+ {/* Thick unison lead */}
204
+ <Synth type="saw" voices={{ count: 4, detune: 15, spread: 0.8 }}>
205
+ <Note note="A4" />
206
+ </Synth>;
207
+ ```
208
+
209
+ ---
210
+
211
+ ### `<Note>`
212
+
213
+ Plays a single note using Web Audio oscillators with ADSR envelope. Inherits
214
+ synth settings from parent `<Synth>` component, or uses defaults.
215
+
216
+ | Prop | Type | Default | Description |
217
+ | --------------- | ----------------------- | --------------- | ------------------------------------------------------------ |
218
+ | `note` | `string` or `number` | — | **Required.** Note name (`"C4"`, `"A#3"`) or frequency in Hz |
219
+ | `amp` | `number` | `0.3` | Amplitude/volume 0-1 |
220
+ | `attack` | `number` | `0` | Attack time in beats |
221
+ | `attack_level` | `number` | `1` | Peak level at end of attack (multiplied by amp) |
222
+ | `decay` | `number` | `0` | Decay time in beats |
223
+ | `decay_level` | `number` | `sustain_level` | Level at end of decay |
224
+ | `sustain` | `number` | `0` | Sustain time in beats |
225
+ | `sustain_level` | `number` | `1` | Sustained level (multiplied by amp) |
226
+ | `release` | `number` | `1` | Release time in beats |
227
+ | `oscillator` | `OscillatorType` | — | Override synth oscillator type |
228
+ | `filter` | `Partial<FilterConfig>` | — | Override filter settings |
229
+ | `voices` | `Partial<VoiceConfig>` | — | Override voice settings |
230
+
231
+ **Note Duration:** Total duration = `attack` + `decay` + `sustain` + `release`
232
+
233
+ ```tsx
234
+ {/* Simple note */}
235
+ <Note note="A4" />;
236
+
237
+ {/* Note with envelope */}
238
+ <Note note="C4" amp={0.5} attack={0.1} sustain={0.5} release={0.3} />;
239
+
240
+ {/* Note with frequency in Hz */}
241
+ <Note note={440} />;
242
+
243
+ {/* Note with filter override */}
244
+ <Note note="E4" filter={{ cutoff: 800, resonance: 5 }} />;
245
+ ```
246
+
247
+ ---
248
+
249
+ ### `<Chord>`
250
+
251
+ Plays multiple notes simultaneously. Supports chord name notation (powered by
252
+ Tonal) or explicit note arrays.
253
+
254
+ | Prop | Type | Default | Description |
255
+ | --------------- | -------------------------------- | --------------- | ------------------------------------------ |
256
+ | `notes` | `string` or `(string\|number)[]` | — | **Required.** Chord name or array of notes |
257
+ | `amp` | `number` | `0.3` | Amplitude/volume 0-1 |
258
+ | `attack` | `number` | `0` | Attack time in beats |
259
+ | `attack_level` | `number` | `1` | Peak level at end of attack |
260
+ | `decay` | `number` | `0` | Decay time in beats |
261
+ | `decay_level` | `number` | `sustain_level` | Level at end of decay |
262
+ | `sustain` | `number` | `0` | Sustain time in beats |
263
+ | `sustain_level` | `number` | `1` | Sustained level |
264
+ | `release` | `number` | `1` | Release time in beats |
265
+ | `oscillator` | `OscillatorType` | — | Override synth oscillator type |
266
+ | `filter` | `Partial<FilterConfig>` | — | Override filter settings |
267
+ | `voices` | `Partial<VoiceConfig>` | — | Override voice settings |
268
+
269
+ **Chord Name Format:**
270
+
271
+ - Basic: `"C"`, `"Am"`, `"F#m"`, `"Bb"`
272
+ - Extended: `"Cmaj7"`, `"Dm7"`, `"G7"`, `"Am9"`
273
+ - With octave: `"Cmaj7:4"` (plays in octave 4, default is octave 3)
274
+ - Slash chords: `"Dm7/F"`, `"C/E"`
275
+
276
+ ```tsx
277
+ {/* Chord name */}
278
+ <Chord notes="Am7" />;
279
+
280
+ {/* Chord in specific octave */}
281
+ <Chord notes="Cmaj7:4" />;
282
+
283
+ {/* Note array */}
284
+ <Chord notes={["C4", "E4", "G4"]} />;
285
+
286
+ {/* Frequency array */}
287
+ <Chord notes={[261.63, 329.63, 392.00]} />;
288
+
289
+ {/* With envelope for pad sound */}
290
+ <Chord notes="Em7" amp={0.4} attack={0.5} sustain={2} release={1} />;
291
+ ```
292
+
293
+ ---
294
+
295
+ ### `<Sample>`
296
+
297
+ Plays an audio sample file with optional effects. Samples are loaded from a
298
+ built-in sample library.
299
+
300
+ | Prop | Type | Default | Description |
301
+ | -------- | ------------------ | ------- | -------------------------------------------------- |
302
+ | `name` | `SampleName` | — | **Required.** Sample name (without file extension) |
303
+ | `amp` | `number` | `1` | Amplitude/volume multiplier |
304
+ | `cutoff` | `number` or `Line` | `20000` | Lowpass filter cutoff (MIDI note or Hz) |
305
+ | `rate` | `number` | `1` | Playback rate multiplier |
306
+ | `pan` | `number` | `0` | Stereo pan position: -1 (left) to 1 (right) |
307
+
308
+ **Cutoff Values (MIDI note → Hz):**
309
+
310
+ - `60` → ~262 Hz (C4)
311
+ - `80` → ~831 Hz
312
+ - `100` → ~2637 Hz
313
+ - `110` → ~4699 Hz
314
+ - `130` → ~20kHz (no filtering)
315
+
316
+ ```tsx
317
+ {/* Basic kick drum */}
318
+ <Sample name="bd_haus" />;
319
+
320
+ {/* Louder with filter */}
321
+ <Sample name="bd_haus" amp={2} cutoff={100} />;
322
+
323
+ {/* Hi-hat with pitch shift and panning */}
324
+ <Sample name="drum_cymbal_closed" rate={1.2} pan={0.3} />;
325
+
326
+ {/* Snare with lowpass filter */}
327
+ <Sample name="drum_snare_soft" cutoff={80} amp={0.8} />;
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Line Patterns
333
+
334
+ The `cutoff` prop on `<Note>`, `<Chord>`, and `<Sample>` components supports
335
+ Line patterns for dynamic filter sweeps within a `<Sequence>`:
336
+
337
+ ```tsx
338
+ <Sequence interval={0.25}>
339
+ {/* Filter sweeps from 60 to 120 over 8 steps */}
340
+ <Note note="C4" filter={{ cutoff: { from: 60, to: 120, steps: 8 } }} />
341
+ <Note note="E4" filter={{ cutoff: { from: 60, to: 120, steps: 8 } }} />
342
+ <Note note="G4" filter={{ cutoff: { from: 60, to: 120, steps: 8 } }} />
343
+ {/* ... */}
344
+ </Sequence>;
345
+ ```
346
+
347
+ | Property | Type | Description |
348
+ | -------- | --------- | --------------------------------- |
349
+ | `from` | `number` | Starting cutoff value (MIDI note) |
350
+ | `to` | `number` | Ending cutoff value (MIDI note) |
351
+ | `steps` | `number` | Number of interpolation steps |
352
+ | `mirror` | `boolean` | If true, goes up then back down |
353
+ | `step` | `number` | Manual step index override |
354
+
355
+ ## Inspired By
356
+
357
+ - [Sonic Pi](https://sonic-pi.net/) - The original live coding synth
358
+
359
+ ## License
360
+
361
+ MIT
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","sources":["../cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { resolve } from \"path\";\r\nimport {\r\n createServer,\r\n type ModuleNode,\r\n normalizePath,\r\n type ViteDevServer,\r\n} from \"vite\";\r\nimport { JSDOM } from \"jsdom\";\r\nimport { type ComponentType, createElement } from \"react\";\r\nimport { createRoot, type Root } from \"react-dom/client\";\r\n\r\n// Create fake DOM for React\r\nconst dom = new JSDOM('<!DOCTYPE html><div id=\"root\"></div>');\r\n\r\n// Polyfill globals that React needs\r\n(globalThis as any).document = dom.window.document;\r\n(globalThis as any).window = dom.window;\r\n\r\nlet root: Root | null = null;\r\n\r\nfunction renderSong(SongComponent: ComponentType): void {\r\n const container = document.getElementById(\"root\")!;\r\n\r\n if (!root) {\r\n root = createRoot(container);\r\n }\r\n\r\n root.render(createElement(SongComponent));\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const filePath = process.argv[2];\r\n\r\n if (!filePath) {\r\n console.log(`\r\nšŸŽ¹ React Synth - Live coding music with React\r\n\r\nUsage: \r\n npx react-synth <song.tsx> Run a song with hot reload\r\n`);\r\n process.exit(0);\r\n }\r\n\r\n // Use normalizePath for consistent forward slashes (Vite uses forward slashes internally)\r\n const absolutePath = normalizePath(resolve(process.cwd(), filePath));\r\n console.log(`šŸŽµ Loading ${filePath}...`);\r\n\r\n const vite: ViteDevServer = await createServer({\r\n configFile: false,\r\n root: process.cwd(),\r\n server: {\r\n middlewareMode: true,\r\n hmr: true,\r\n },\r\n appType: \"custom\",\r\n optimizeDeps: {\r\n exclude: [\"@react-synth/synth\", \"node-web-audio-api\"],\r\n },\r\n ssr: {\r\n external: [\"node-web-audio-api\", \"jsdom\"],\r\n noExternal: [\"@react-synth/synth\", \"tonal\"],\r\n },\r\n esbuild: {\r\n jsx: \"automatic\",\r\n },\r\n logLevel: \"info\",\r\n });\r\n\r\n let currentModuleUrl: string | null = null;\r\n\r\n async function loadSong(): Promise<void> {\r\n const url = `${absolutePath}?t=${Date.now()}`;\r\n\r\n // Invalidate the previous module if it exists\r\n if (currentModuleUrl) {\r\n const mod = await vite.moduleGraph.getModuleByUrl(currentModuleUrl);\r\n if (mod) {\r\n invalidateModuleAndImporters(mod);\r\n }\r\n }\r\n\r\n currentModuleUrl = url;\r\n const module = await vite.ssrLoadModule(url);\r\n\r\n // Auto-render the default export if it's a component\r\n if (module.default && typeof module.default === \"function\") {\r\n renderSong(module.default);\r\n } else {\r\n console.warn(\r\n \"āš ļø No default export found. Song file should export a React component as default.\",\r\n );\r\n }\r\n }\r\n\r\n // Recursively invalidate a module and all its importers\r\n function invalidateModuleAndImporters(mod: ModuleNode): void {\r\n vite.moduleGraph.invalidateModule(mod);\r\n for (const importer of mod.importers) {\r\n invalidateModuleAndImporters(importer);\r\n }\r\n }\r\n\r\n // Collect all dependencies (imports) of a module recursively\r\n function collectDependencies(\r\n mod: ModuleNode,\r\n visited: Set<string> = new Set(),\r\n ): Set<string> {\r\n if (mod.file) {\r\n visited.add(mod.file);\r\n }\r\n\r\n for (const imported of mod.ssrImportedModules) {\r\n if (imported.file && !visited.has(imported.file)) {\r\n collectDependencies(imported, visited);\r\n }\r\n }\r\n\r\n return visited;\r\n }\r\n\r\n // Check if a file is a dependency of the song module\r\n function isDependencyOfSong(changedFile: string): boolean {\r\n const songMod = vite.moduleGraph.getModulesByFile(absolutePath);\r\n if (!songMod || songMod.size === 0) {\r\n return false;\r\n }\r\n\r\n for (const mod of songMod) {\r\n const deps = collectDependencies(mod);\r\n if (deps.has(changedFile)) {\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n await loadSong();\r\n console.log(\"šŸŽ¹ Playing track\");\r\n console.log(\" Watching for changes...\");\r\n\r\n let debounce: ReturnType<typeof setTimeout> | undefined;\r\n\r\n vite.watcher.on(\"change\", async (changedPath) => {\r\n // Normalize to forward slashes to match Vite's module graph\r\n const normalizedChanged = normalizePath(resolve(changedPath));\r\n\r\n // Reload if the song file itself changed, or any of its dependencies\r\n if (!isDependencyOfSong(normalizedChanged)) {\r\n return;\r\n }\r\n\r\n clearTimeout(debounce);\r\n debounce = setTimeout(async () => {\r\n try {\r\n await loadSong();\r\n console.log(`āœ… Hot reloaded (${changedPath})`);\r\n } catch (e) {\r\n console.error(\"āŒ Error:\", e instanceof Error ? e.message : String(e));\r\n }\r\n }, 50);\r\n });\r\n\r\n process.on(\"SIGINT\", async () => {\r\n console.log(\"\\nšŸ‘‹ Stopping...\");\r\n await vite.close();\r\n process.exit(0);\r\n });\r\n}\r\n\r\nmain().catch(console.error);\r\n"],"names":["JSDOM","createRoot","createElement","normalizePath","resolve","vite","createServer","module"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","sources":["../cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve } from \"path\";\nimport {\n createServer,\n type ModuleNode,\n normalizePath,\n type ViteDevServer,\n} from \"vite\";\nimport { JSDOM } from \"jsdom\";\nimport { type ComponentType, createElement } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\n// Create fake DOM for React\nconst dom = new JSDOM('<!DOCTYPE html><div id=\"root\"></div>');\n\n// Polyfill globals that React needs\n(globalThis as any).document = dom.window.document;\n(globalThis as any).window = dom.window;\n\nlet root: Root | null = null;\n\nfunction renderSong(SongComponent: ComponentType): void {\n const container = document.getElementById(\"root\")!;\n\n if (!root) {\n root = createRoot(container);\n }\n\n root.render(createElement(SongComponent));\n}\n\nasync function main(): Promise<void> {\n const filePath = process.argv[2];\n\n if (!filePath) {\n console.log(`\nšŸŽ¹ React Synth - Live coding music with React\n\nUsage: \n npx react-synth <song.tsx> Run a song with hot reload\n`);\n process.exit(0);\n }\n\n // Use normalizePath for consistent forward slashes (Vite uses forward slashes internally)\n const absolutePath = normalizePath(resolve(process.cwd(), filePath));\n console.log(`šŸŽµ Loading ${filePath}...`);\n\n const vite: ViteDevServer = await createServer({\n configFile: false,\n root: process.cwd(),\n server: {\n middlewareMode: true,\n hmr: true,\n },\n appType: \"custom\",\n optimizeDeps: {\n exclude: [\"@react-synth/synth\", \"node-web-audio-api\"],\n },\n ssr: {\n external: [\"node-web-audio-api\", \"jsdom\"],\n noExternal: [\"@react-synth/synth\", \"tonal\"],\n },\n esbuild: {\n jsx: \"automatic\",\n },\n logLevel: \"info\",\n });\n\n let currentModuleUrl: string | null = null;\n\n async function loadSong(): Promise<void> {\n const url = `${absolutePath}?t=${Date.now()}`;\n\n // Invalidate the previous module if it exists\n if (currentModuleUrl) {\n const mod = await vite.moduleGraph.getModuleByUrl(currentModuleUrl);\n if (mod) {\n invalidateModuleAndImporters(mod);\n }\n }\n\n currentModuleUrl = url;\n const module = await vite.ssrLoadModule(url);\n\n // Auto-render the default export if it's a component\n if (module.default && typeof module.default === \"function\") {\n renderSong(module.default);\n } else {\n console.warn(\n \"āš ļø No default export found. Song file should export a React component as default.\",\n );\n }\n }\n\n // Recursively invalidate a module and all its importers\n function invalidateModuleAndImporters(mod: ModuleNode): void {\n vite.moduleGraph.invalidateModule(mod);\n for (const importer of mod.importers) {\n invalidateModuleAndImporters(importer);\n }\n }\n\n // Collect all dependencies (imports) of a module recursively\n function collectDependencies(\n mod: ModuleNode,\n visited: Set<string> = new Set(),\n ): Set<string> {\n if (mod.file) {\n visited.add(mod.file);\n }\n\n for (const imported of mod.ssrImportedModules) {\n if (imported.file && !visited.has(imported.file)) {\n collectDependencies(imported, visited);\n }\n }\n\n return visited;\n }\n\n // Check if a file is a dependency of the song module\n function isDependencyOfSong(changedFile: string): boolean {\n const songMod = vite.moduleGraph.getModulesByFile(absolutePath);\n if (!songMod || songMod.size === 0) {\n return false;\n }\n\n for (const mod of songMod) {\n const deps = collectDependencies(mod);\n if (deps.has(changedFile)) {\n return true;\n }\n }\n\n return false;\n }\n\n await loadSong();\n console.log(\"šŸŽ¹ Playing track\");\n console.log(\" Watching for changes...\");\n\n let debounce: ReturnType<typeof setTimeout> | undefined;\n\n vite.watcher.on(\"change\", async (changedPath) => {\n // Normalize to forward slashes to match Vite's module graph\n const normalizedChanged = normalizePath(resolve(changedPath));\n\n // Reload if the song file itself changed, or any of its dependencies\n if (!isDependencyOfSong(normalizedChanged)) {\n return;\n }\n\n clearTimeout(debounce);\n debounce = setTimeout(async () => {\n try {\n await loadSong();\n console.log(`āœ… Hot reloaded (${changedPath})`);\n } catch (e) {\n console.error(\"āŒ Error:\", e instanceof Error ? e.message : String(e));\n }\n }, 50);\n });\n\n process.on(\"SIGINT\", async () => {\n console.log(\"\\nšŸ‘‹ Stopping...\");\n await vite.close();\n process.exit(0);\n });\n}\n\nmain().catch(console.error);\n"],"names":["JSDOM","createRoot","createElement","normalizePath","resolve","vite","createServer","module"],"mappings":"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"}
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sources":["../cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { resolve } from \"path\";\r\nimport {\r\n createServer,\r\n type ModuleNode,\r\n normalizePath,\r\n type ViteDevServer,\r\n} from \"vite\";\r\nimport { JSDOM } from \"jsdom\";\r\nimport { type ComponentType, createElement } from \"react\";\r\nimport { createRoot, type Root } from \"react-dom/client\";\r\n\r\n// Create fake DOM for React\r\nconst dom = new JSDOM('<!DOCTYPE html><div id=\"root\"></div>');\r\n\r\n// Polyfill globals that React needs\r\n(globalThis as any).document = dom.window.document;\r\n(globalThis as any).window = dom.window;\r\n\r\nlet root: Root | null = null;\r\n\r\nfunction renderSong(SongComponent: ComponentType): void {\r\n const container = document.getElementById(\"root\")!;\r\n\r\n if (!root) {\r\n root = createRoot(container);\r\n }\r\n\r\n root.render(createElement(SongComponent));\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const filePath = process.argv[2];\r\n\r\n if (!filePath) {\r\n console.log(`\r\nšŸŽ¹ React Synth - Live coding music with React\r\n\r\nUsage: \r\n npx react-synth <song.tsx> Run a song with hot reload\r\n`);\r\n process.exit(0);\r\n }\r\n\r\n // Use normalizePath for consistent forward slashes (Vite uses forward slashes internally)\r\n const absolutePath = normalizePath(resolve(process.cwd(), filePath));\r\n console.log(`šŸŽµ Loading ${filePath}...`);\r\n\r\n const vite: ViteDevServer = await createServer({\r\n configFile: false,\r\n root: process.cwd(),\r\n server: {\r\n middlewareMode: true,\r\n hmr: true,\r\n },\r\n appType: \"custom\",\r\n optimizeDeps: {\r\n exclude: [\"@react-synth/synth\", \"node-web-audio-api\"],\r\n },\r\n ssr: {\r\n external: [\"node-web-audio-api\", \"jsdom\"],\r\n noExternal: [\"@react-synth/synth\", \"tonal\"],\r\n },\r\n esbuild: {\r\n jsx: \"automatic\",\r\n },\r\n logLevel: \"info\",\r\n });\r\n\r\n let currentModuleUrl: string | null = null;\r\n\r\n async function loadSong(): Promise<void> {\r\n const url = `${absolutePath}?t=${Date.now()}`;\r\n\r\n // Invalidate the previous module if it exists\r\n if (currentModuleUrl) {\r\n const mod = await vite.moduleGraph.getModuleByUrl(currentModuleUrl);\r\n if (mod) {\r\n invalidateModuleAndImporters(mod);\r\n }\r\n }\r\n\r\n currentModuleUrl = url;\r\n const module = await vite.ssrLoadModule(url);\r\n\r\n // Auto-render the default export if it's a component\r\n if (module.default && typeof module.default === \"function\") {\r\n renderSong(module.default);\r\n } else {\r\n console.warn(\r\n \"āš ļø No default export found. Song file should export a React component as default.\",\r\n );\r\n }\r\n }\r\n\r\n // Recursively invalidate a module and all its importers\r\n function invalidateModuleAndImporters(mod: ModuleNode): void {\r\n vite.moduleGraph.invalidateModule(mod);\r\n for (const importer of mod.importers) {\r\n invalidateModuleAndImporters(importer);\r\n }\r\n }\r\n\r\n // Collect all dependencies (imports) of a module recursively\r\n function collectDependencies(\r\n mod: ModuleNode,\r\n visited: Set<string> = new Set(),\r\n ): Set<string> {\r\n if (mod.file) {\r\n visited.add(mod.file);\r\n }\r\n\r\n for (const imported of mod.ssrImportedModules) {\r\n if (imported.file && !visited.has(imported.file)) {\r\n collectDependencies(imported, visited);\r\n }\r\n }\r\n\r\n return visited;\r\n }\r\n\r\n // Check if a file is a dependency of the song module\r\n function isDependencyOfSong(changedFile: string): boolean {\r\n const songMod = vite.moduleGraph.getModulesByFile(absolutePath);\r\n if (!songMod || songMod.size === 0) {\r\n return false;\r\n }\r\n\r\n for (const mod of songMod) {\r\n const deps = collectDependencies(mod);\r\n if (deps.has(changedFile)) {\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n await loadSong();\r\n console.log(\"šŸŽ¹ Playing track\");\r\n console.log(\" Watching for changes...\");\r\n\r\n let debounce: ReturnType<typeof setTimeout> | undefined;\r\n\r\n vite.watcher.on(\"change\", async (changedPath) => {\r\n // Normalize to forward slashes to match Vite's module graph\r\n const normalizedChanged = normalizePath(resolve(changedPath));\r\n\r\n // Reload if the song file itself changed, or any of its dependencies\r\n if (!isDependencyOfSong(normalizedChanged)) {\r\n return;\r\n }\r\n\r\n clearTimeout(debounce);\r\n debounce = setTimeout(async () => {\r\n try {\r\n await loadSong();\r\n console.log(`āœ… Hot reloaded (${changedPath})`);\r\n } catch (e) {\r\n console.error(\"āŒ Error:\", e instanceof Error ? e.message : String(e));\r\n }\r\n }, 50);\r\n });\r\n\r\n process.on(\"SIGINT\", async () => {\r\n console.log(\"\\nšŸ‘‹ Stopping...\");\r\n await vite.close();\r\n process.exit(0);\r\n });\r\n}\r\n\r\nmain().catch(console.error);\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sources":["../cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve } from \"path\";\nimport {\n createServer,\n type ModuleNode,\n normalizePath,\n type ViteDevServer,\n} from \"vite\";\nimport { JSDOM } from \"jsdom\";\nimport { type ComponentType, createElement } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\n// Create fake DOM for React\nconst dom = new JSDOM('<!DOCTYPE html><div id=\"root\"></div>');\n\n// Polyfill globals that React needs\n(globalThis as any).document = dom.window.document;\n(globalThis as any).window = dom.window;\n\nlet root: Root | null = null;\n\nfunction renderSong(SongComponent: ComponentType): void {\n const container = document.getElementById(\"root\")!;\n\n if (!root) {\n root = createRoot(container);\n }\n\n root.render(createElement(SongComponent));\n}\n\nasync function main(): Promise<void> {\n const filePath = process.argv[2];\n\n if (!filePath) {\n console.log(`\nšŸŽ¹ React Synth - Live coding music with React\n\nUsage: \n npx react-synth <song.tsx> Run a song with hot reload\n`);\n process.exit(0);\n }\n\n // Use normalizePath for consistent forward slashes (Vite uses forward slashes internally)\n const absolutePath = normalizePath(resolve(process.cwd(), filePath));\n console.log(`šŸŽµ Loading ${filePath}...`);\n\n const vite: ViteDevServer = await createServer({\n configFile: false,\n root: process.cwd(),\n server: {\n middlewareMode: true,\n hmr: true,\n },\n appType: \"custom\",\n optimizeDeps: {\n exclude: [\"@react-synth/synth\", \"node-web-audio-api\"],\n },\n ssr: {\n external: [\"node-web-audio-api\", \"jsdom\"],\n noExternal: [\"@react-synth/synth\", \"tonal\"],\n },\n esbuild: {\n jsx: \"automatic\",\n },\n logLevel: \"info\",\n });\n\n let currentModuleUrl: string | null = null;\n\n async function loadSong(): Promise<void> {\n const url = `${absolutePath}?t=${Date.now()}`;\n\n // Invalidate the previous module if it exists\n if (currentModuleUrl) {\n const mod = await vite.moduleGraph.getModuleByUrl(currentModuleUrl);\n if (mod) {\n invalidateModuleAndImporters(mod);\n }\n }\n\n currentModuleUrl = url;\n const module = await vite.ssrLoadModule(url);\n\n // Auto-render the default export if it's a component\n if (module.default && typeof module.default === \"function\") {\n renderSong(module.default);\n } else {\n console.warn(\n \"āš ļø No default export found. Song file should export a React component as default.\",\n );\n }\n }\n\n // Recursively invalidate a module and all its importers\n function invalidateModuleAndImporters(mod: ModuleNode): void {\n vite.moduleGraph.invalidateModule(mod);\n for (const importer of mod.importers) {\n invalidateModuleAndImporters(importer);\n }\n }\n\n // Collect all dependencies (imports) of a module recursively\n function collectDependencies(\n mod: ModuleNode,\n visited: Set<string> = new Set(),\n ): Set<string> {\n if (mod.file) {\n visited.add(mod.file);\n }\n\n for (const imported of mod.ssrImportedModules) {\n if (imported.file && !visited.has(imported.file)) {\n collectDependencies(imported, visited);\n }\n }\n\n return visited;\n }\n\n // Check if a file is a dependency of the song module\n function isDependencyOfSong(changedFile: string): boolean {\n const songMod = vite.moduleGraph.getModulesByFile(absolutePath);\n if (!songMod || songMod.size === 0) {\n return false;\n }\n\n for (const mod of songMod) {\n const deps = collectDependencies(mod);\n if (deps.has(changedFile)) {\n return true;\n }\n }\n\n return false;\n }\n\n await loadSong();\n console.log(\"šŸŽ¹ Playing track\");\n console.log(\" Watching for changes...\");\n\n let debounce: ReturnType<typeof setTimeout> | undefined;\n\n vite.watcher.on(\"change\", async (changedPath) => {\n // Normalize to forward slashes to match Vite's module graph\n const normalizedChanged = normalizePath(resolve(changedPath));\n\n // Reload if the song file itself changed, or any of its dependencies\n if (!isDependencyOfSong(normalizedChanged)) {\n return;\n }\n\n clearTimeout(debounce);\n debounce = setTimeout(async () => {\n try {\n await loadSong();\n console.log(`āœ… Hot reloaded (${changedPath})`);\n } catch (e) {\n console.error(\"āŒ Error:\", e instanceof Error ? e.message : String(e));\n }\n }, 50);\n });\n\n process.on(\"SIGINT\", async () => {\n console.log(\"\\nšŸ‘‹ Stopping...\");\n await vite.close();\n process.exit(0);\n });\n}\n\nmain().catch(console.error);\n"],"names":[],"mappings":"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"}