@react-synth/synth 0.0.6-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 +187 -0
- package/dist/cli.cjs +127 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +126 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1702 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +388 -0
- package/dist/index.js +1701 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/src/samples/bd_haus.flac +0 -0
- package/src/samples/drum_cymbal_closed.flac +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
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
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const vite = require("vite");
|
|
5
|
+
const jsdom = require("jsdom");
|
|
6
|
+
const React = require("react");
|
|
7
|
+
const client = require("react-dom/client");
|
|
8
|
+
const dom = new jsdom.JSDOM('<!DOCTYPE html><div id="root"></div>');
|
|
9
|
+
globalThis.document = dom.window.document;
|
|
10
|
+
globalThis.window = dom.window;
|
|
11
|
+
let root = null;
|
|
12
|
+
function renderSong(SongComponent) {
|
|
13
|
+
const container = document.getElementById("root");
|
|
14
|
+
if (!root) {
|
|
15
|
+
root = client.createRoot(container);
|
|
16
|
+
}
|
|
17
|
+
root.render(React.createElement(SongComponent));
|
|
18
|
+
}
|
|
19
|
+
async function main() {
|
|
20
|
+
const filePath = process.argv[2];
|
|
21
|
+
if (!filePath) {
|
|
22
|
+
console.log(`
|
|
23
|
+
š¹ React Synth - Live coding music with React
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
npx react-synth <song.tsx> Run a song with hot reload
|
|
27
|
+
`);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
const absolutePath = vite.normalizePath(path.resolve(process.cwd(), filePath));
|
|
31
|
+
console.log(`šµ Loading ${filePath}...`);
|
|
32
|
+
const vite$1 = await vite.createServer({
|
|
33
|
+
configFile: false,
|
|
34
|
+
root: process.cwd(),
|
|
35
|
+
server: {
|
|
36
|
+
middlewareMode: true,
|
|
37
|
+
hmr: true
|
|
38
|
+
},
|
|
39
|
+
appType: "custom",
|
|
40
|
+
optimizeDeps: {
|
|
41
|
+
exclude: ["@react-synth/synth", "node-web-audio-api"]
|
|
42
|
+
},
|
|
43
|
+
ssr: {
|
|
44
|
+
external: ["node-web-audio-api", "jsdom"],
|
|
45
|
+
noExternal: ["@react-synth/synth", "tonal"]
|
|
46
|
+
},
|
|
47
|
+
esbuild: {
|
|
48
|
+
jsx: "automatic"
|
|
49
|
+
},
|
|
50
|
+
logLevel: "info"
|
|
51
|
+
});
|
|
52
|
+
let currentModuleUrl = null;
|
|
53
|
+
async function loadSong() {
|
|
54
|
+
const url = `${absolutePath}?t=${Date.now()}`;
|
|
55
|
+
if (currentModuleUrl) {
|
|
56
|
+
const mod = await vite$1.moduleGraph.getModuleByUrl(currentModuleUrl);
|
|
57
|
+
if (mod) {
|
|
58
|
+
invalidateModuleAndImporters(mod);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
currentModuleUrl = url;
|
|
62
|
+
const module2 = await vite$1.ssrLoadModule(url);
|
|
63
|
+
if (module2.default && typeof module2.default === "function") {
|
|
64
|
+
renderSong(module2.default);
|
|
65
|
+
} else {
|
|
66
|
+
console.warn(
|
|
67
|
+
"ā ļø No default export found. Song file should export a React component as default."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function invalidateModuleAndImporters(mod) {
|
|
72
|
+
vite$1.moduleGraph.invalidateModule(mod);
|
|
73
|
+
for (const importer of mod.importers) {
|
|
74
|
+
invalidateModuleAndImporters(importer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function collectDependencies(mod, visited = /* @__PURE__ */ new Set()) {
|
|
78
|
+
if (mod.file) {
|
|
79
|
+
visited.add(mod.file);
|
|
80
|
+
}
|
|
81
|
+
for (const imported of mod.ssrImportedModules) {
|
|
82
|
+
if (imported.file && !visited.has(imported.file)) {
|
|
83
|
+
collectDependencies(imported, visited);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return visited;
|
|
87
|
+
}
|
|
88
|
+
function isDependencyOfSong(changedFile) {
|
|
89
|
+
const songMod = vite$1.moduleGraph.getModulesByFile(absolutePath);
|
|
90
|
+
if (!songMod || songMod.size === 0) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
for (const mod of songMod) {
|
|
94
|
+
const deps = collectDependencies(mod);
|
|
95
|
+
if (deps.has(changedFile)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
await loadSong();
|
|
102
|
+
console.log("š¹ Playing track");
|
|
103
|
+
console.log(" Watching for changes...");
|
|
104
|
+
let debounce;
|
|
105
|
+
vite$1.watcher.on("change", async (changedPath) => {
|
|
106
|
+
const normalizedChanged = vite.normalizePath(path.resolve(changedPath));
|
|
107
|
+
if (!isDependencyOfSong(normalizedChanged)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
clearTimeout(debounce);
|
|
111
|
+
debounce = setTimeout(async () => {
|
|
112
|
+
try {
|
|
113
|
+
await loadSong();
|
|
114
|
+
console.log(`ā
Hot reloaded (${changedPath})`);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error("ā Error:", e instanceof Error ? e.message : String(e));
|
|
117
|
+
}
|
|
118
|
+
}, 50);
|
|
119
|
+
});
|
|
120
|
+
process.on("SIGINT", async () => {
|
|
121
|
+
console.log("\nš Stopping...");
|
|
122
|
+
await vite$1.close();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
main().catch(console.error);
|
|
127
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +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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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { }
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { normalizePath, createServer } from "vite";
|
|
4
|
+
import { JSDOM } from "jsdom";
|
|
5
|
+
import { createElement } from "react";
|
|
6
|
+
import { createRoot } from "react-dom/client";
|
|
7
|
+
const dom = new JSDOM('<!DOCTYPE html><div id="root"></div>');
|
|
8
|
+
globalThis.document = dom.window.document;
|
|
9
|
+
globalThis.window = dom.window;
|
|
10
|
+
let root = null;
|
|
11
|
+
function renderSong(SongComponent) {
|
|
12
|
+
const container = document.getElementById("root");
|
|
13
|
+
if (!root) {
|
|
14
|
+
root = createRoot(container);
|
|
15
|
+
}
|
|
16
|
+
root.render(createElement(SongComponent));
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
const filePath = process.argv[2];
|
|
20
|
+
if (!filePath) {
|
|
21
|
+
console.log(`
|
|
22
|
+
š¹ React Synth - Live coding music with React
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
npx react-synth <song.tsx> Run a song with hot reload
|
|
26
|
+
`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
const absolutePath = normalizePath(resolve(process.cwd(), filePath));
|
|
30
|
+
console.log(`šµ Loading ${filePath}...`);
|
|
31
|
+
const vite = await createServer({
|
|
32
|
+
configFile: false,
|
|
33
|
+
root: process.cwd(),
|
|
34
|
+
server: {
|
|
35
|
+
middlewareMode: true,
|
|
36
|
+
hmr: true
|
|
37
|
+
},
|
|
38
|
+
appType: "custom",
|
|
39
|
+
optimizeDeps: {
|
|
40
|
+
exclude: ["@react-synth/synth", "node-web-audio-api"]
|
|
41
|
+
},
|
|
42
|
+
ssr: {
|
|
43
|
+
external: ["node-web-audio-api", "jsdom"],
|
|
44
|
+
noExternal: ["@react-synth/synth", "tonal"]
|
|
45
|
+
},
|
|
46
|
+
esbuild: {
|
|
47
|
+
jsx: "automatic"
|
|
48
|
+
},
|
|
49
|
+
logLevel: "info"
|
|
50
|
+
});
|
|
51
|
+
let currentModuleUrl = null;
|
|
52
|
+
async function loadSong() {
|
|
53
|
+
const url = `${absolutePath}?t=${Date.now()}`;
|
|
54
|
+
if (currentModuleUrl) {
|
|
55
|
+
const mod = await vite.moduleGraph.getModuleByUrl(currentModuleUrl);
|
|
56
|
+
if (mod) {
|
|
57
|
+
invalidateModuleAndImporters(mod);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
currentModuleUrl = url;
|
|
61
|
+
const module = await vite.ssrLoadModule(url);
|
|
62
|
+
if (module.default && typeof module.default === "function") {
|
|
63
|
+
renderSong(module.default);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(
|
|
66
|
+
"ā ļø No default export found. Song file should export a React component as default."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function invalidateModuleAndImporters(mod) {
|
|
71
|
+
vite.moduleGraph.invalidateModule(mod);
|
|
72
|
+
for (const importer of mod.importers) {
|
|
73
|
+
invalidateModuleAndImporters(importer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function collectDependencies(mod, visited = /* @__PURE__ */ new Set()) {
|
|
77
|
+
if (mod.file) {
|
|
78
|
+
visited.add(mod.file);
|
|
79
|
+
}
|
|
80
|
+
for (const imported of mod.ssrImportedModules) {
|
|
81
|
+
if (imported.file && !visited.has(imported.file)) {
|
|
82
|
+
collectDependencies(imported, visited);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return visited;
|
|
86
|
+
}
|
|
87
|
+
function isDependencyOfSong(changedFile) {
|
|
88
|
+
const songMod = vite.moduleGraph.getModulesByFile(absolutePath);
|
|
89
|
+
if (!songMod || songMod.size === 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
for (const mod of songMod) {
|
|
93
|
+
const deps = collectDependencies(mod);
|
|
94
|
+
if (deps.has(changedFile)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
await loadSong();
|
|
101
|
+
console.log("š¹ Playing track");
|
|
102
|
+
console.log(" Watching for changes...");
|
|
103
|
+
let debounce;
|
|
104
|
+
vite.watcher.on("change", async (changedPath) => {
|
|
105
|
+
const normalizedChanged = normalizePath(resolve(changedPath));
|
|
106
|
+
if (!isDependencyOfSong(normalizedChanged)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
clearTimeout(debounce);
|
|
110
|
+
debounce = setTimeout(async () => {
|
|
111
|
+
try {
|
|
112
|
+
await loadSong();
|
|
113
|
+
console.log(`ā
Hot reloaded (${changedPath})`);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error("ā Error:", e instanceof Error ? e.message : String(e));
|
|
116
|
+
}
|
|
117
|
+
}, 50);
|
|
118
|
+
});
|
|
119
|
+
process.on("SIGINT", async () => {
|
|
120
|
+
console.log("\nš Stopping...");
|
|
121
|
+
await vite.close();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
main().catch(console.error);
|
|
126
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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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"}
|