rockstar-strudel 1.0.0

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/PLAN.md ADDED
@@ -0,0 +1,227 @@
1
+ # Plan: shipping `rockstar-strudel` from a RockstarLang/rockstar fork
2
+
3
+ This document describes every step needed to wire the existing Starship WASM
4
+ engine up to the `rockstar` template-tag module in this repo so that
5
+ strudel.cc users can simply write:
6
+
7
+ ```js
8
+ import { init, rockstar } from 'https://esm.sh/rockstar-strudel'
9
+ await init('https://<your-username>.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js')
10
+ const data = await rockstar`Shout 42`
11
+ // data === [42]
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Background: why anything needs to change
17
+
18
+ The Starship engine is already built and running at
19
+ `https://codewithrockstar.com/wasm/`. The issue is that browsers enforce the
20
+ **Same-Origin Policy**: a page on `strudel.cc` cannot load a JS/WASM module
21
+ from `codewithrockstar.com` unless that server explicitly opts in via a CORS
22
+ header (`Access-Control-Allow-Origin: *`).
23
+
24
+ `codewithrockstar.com` uses a **custom domain**, which means CORS headers must
25
+ be configured at the CDN/DNS level (e.g. Cloudflare) by the site owner — a
26
+ change that can't be made via a GitHub PR to the repo.
27
+
28
+ However, **GitHub Pages on `*.github.io` domains serves all static assets with
29
+ `Access-Control-Allow-Origin: *` built in**, at no extra configuration cost.
30
+ This means a fork of the repo deployed to GitHub Pages at its default
31
+ `<you>.github.io/rockstar` URL has CORS working immediately, with no CDN
32
+ setup needed.
33
+
34
+ The complete build pipeline (`.NET` WASM compile → Jekyll site build →
35
+ GitHub Pages deploy) is already automated in the repo's GitHub Actions
36
+ workflows, so enabling it on a fork is a matter of enabling Pages in the
37
+ fork's settings.
38
+
39
+ ---
40
+
41
+ ## Option A — Fork and host on GitHub Pages (unblocked today)
42
+
43
+ This is the fastest path. No CDN, no Cloudflare, no extra accounts needed.
44
+
45
+ ### Step 1 — Fork `RockstarLang/rockstar`
46
+
47
+ Fork it on GitHub (keep it **public** so the free GitHub Pages tier is
48
+ available).
49
+
50
+ ### Step 2 — Enable GitHub Pages on the fork
51
+
52
+ 1. Go to your fork → **Settings → Pages**
53
+ 2. Under *Build and deployment*, set Source to **GitHub Actions**
54
+ (not a branch — the workflow handles deployment itself)
55
+ 3. Save.
56
+
57
+ ### Step 3 — Trigger the first build
58
+
59
+ The build pipeline is three chained workflows:
60
+
61
+ ```
62
+ build-rockstar-2.0
63
+ └─► release-rockstar-engine
64
+ └─► build-and-deploy-codewithrockstar.com → GitHub Pages
65
+ ```
66
+
67
+ Trigger the first one manually:
68
+ - Go to **Actions → build-rockstar-2.0 → Run workflow** (pick `main`)
69
+
70
+ This will:
71
+ 1. Build the Starship .NET engine and run its tests
72
+ 2. Compile the WASM with `dotnet publish Starship/Rockstar.Wasm -c Release`
73
+ 3. Copy the WASM into the Jekyll site and deploy it to GitHub Pages
74
+
75
+ After a few minutes your site will be live at:
76
+ ```
77
+ https://<your-username>.github.io/rockstar/
78
+ ```
79
+
80
+ And the WASM loader will be at:
81
+ ```
82
+ https://<your-username>.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js
83
+ ```
84
+
85
+ ### Step 4 — Point `rockstar-strudel` at your fork
86
+
87
+ Update `DEFAULT_DOTNET_URL` in `src/index.js`:
88
+
89
+ ```js
90
+ const DEFAULT_DOTNET_URL =
91
+ 'https://<your-username>.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js';
92
+ ```
93
+
94
+ Or leave the default pointing at `codewithrockstar.com` and let users pass
95
+ their own URL via `init()`:
96
+
97
+ ```js
98
+ await init('https://<your-username>.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js')
99
+ ```
100
+
101
+ ### Step 5 — Publish the npm package
102
+
103
+ ```bash
104
+ cd rockstar-strudel
105
+ npm publish --access public
106
+ ```
107
+
108
+ Users can then import from `https://esm.sh/rockstar-strudel`.
109
+
110
+ ---
111
+
112
+ ## Option B — PR / issue to `RockstarLang/rockstar` (long-term fix)
113
+
114
+ A PR to the repo **cannot** fix CORS for the custom domain by itself — that
115
+ change must be made in the Cloudflare (or equivalent CDN) dashboard by the
116
+ site owner. The most useful thing you can do is:
117
+
118
+ 1. **Open an issue** explaining the strudel.cc use case and asking them to add
119
+ `Access-Control-Allow-Origin: *` to the `/wasm/` path in their CDN config.
120
+ A single Cloudflare Transform Rule would fix it permanently for all users.
121
+
122
+ 2. Optionally include a **PR that adds a `_headers` file** as a signal of
123
+ intent (it has no effect on a custom domain, but documents the desired
124
+ config):
125
+
126
+ ```
127
+ # codewithrockstar.com/_headers
128
+ /wasm/*
129
+ Access-Control-Allow-Origin: *
130
+ ```
131
+
132
+ Once the upstream site adds the header, update `DEFAULT_DOTNET_URL` back to
133
+ `https://codewithrockstar.com/wasm/wwwroot/_framework/dotnet.js` so users
134
+ get the canonical URL by default.
135
+
136
+ ---
137
+
138
+ ## Summary
139
+
140
+ | Path | Works today? | Effort |
141
+ |---|---|---|
142
+ | Fork → GitHub Pages (Option A) | ✅ Yes, ~20 min | Fork + enable Pages + trigger build |
143
+ | PR/issue to upstream (Option B) | ⏳ Depends on maintainer | Low effort, uncertain timeline |
144
+
145
+ **Do both**: use Option A to unblock yourself right now, open an upstream
146
+ issue (Option B) so the permanent fix lands in `codewithrockstar.com`.
147
+
148
+ ---
149
+
150
+ ## Local smoke-test (optional but recommended)
151
+
152
+ To verify `src/index.js` against a local WASM build before publishing:
153
+
154
+ ### Build the WASM locally
155
+
156
+ You need the **.NET 9 SDK** (`dotnet --version` should show `9.x`).
157
+
158
+ ```bash
159
+ cd Starship
160
+ dotnet workload install wasm-tools
161
+ dotnet publish Rockstar.Wasm -c Release -o ../wasm-publish
162
+ ```
163
+
164
+ The output in `../wasm-publish/wwwroot/` contains:
165
+
166
+ ```
167
+ _framework/
168
+ dotnet.js ← the JS loader
169
+ dotnet.native.js
170
+ dotnet.runtime.js
171
+ dotnet.wasm ← the .NET runtime (~7 MB, AOT-compiled in Release)
172
+ Rockstar.Wasm.wasm ← the Rockstar engine
173
+ ```
174
+
175
+ ### Run the integration test
176
+
177
+ Create a minimal HTML file in the same directory as the WASM:
178
+
179
+ ```html
180
+ <!-- wasm-publish/wwwroot/test.html -->
181
+ <script type="module">
182
+ import { init, rockstar } from '/path/to/rockstar-strudel/src/index.js'
183
+
184
+ // localhost is in the allowed URL list, so no allowlist update needed
185
+ await init('http://localhost:8080/_framework/dotnet.js')
186
+
187
+ const result = await rockstar`
188
+ My heart is 123
189
+ Let your love be 456
190
+ Put 789 into the night
191
+ Shout my heart. Scream your love. Whisper the night.
192
+ `
193
+ console.assert(JSON.stringify(result) === '[123,456,789]',
194
+ 'Expected [123,456,789], got ' + JSON.stringify(result))
195
+ document.body.textContent = JSON.stringify(result)
196
+ </script>
197
+ ```
198
+
199
+ Serve from localhost (same origin as the WASM avoids CORS entirely):
200
+
201
+ ```bash
202
+ cd wasm-publish/wwwroot
203
+ npx serve -p 8080
204
+ # open http://localhost:8080/test.html
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Summary checklist
210
+
211
+ - [ ] Fork `RockstarLang/rockstar` on GitHub (keep public)
212
+ - [ ] Fork Settings → Pages → Source: **GitHub Actions**
213
+ - [ ] Actions → `build-rockstar-2.0` → **Run workflow** (triggers the full chain)
214
+ - [ ] Wait for Pages deployment; note your URL: `https://<you>.github.io/rockstar/`
215
+ - [ ] Update `DEFAULT_DOTNET_URL` in `src/index.js` to your fork's WASM URL
216
+ (or let users pass it to `init()`)
217
+ - [ ] `npm publish --access public` the `rockstar-strudel` package
218
+ - [ ] Verify in strudel.cc:
219
+ ```js
220
+ import { init, rockstar } from 'https://esm.sh/rockstar-strudel'
221
+ await init('https://<you>.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js')
222
+ const data = await rockstar`Shout 42` // [42]
223
+ ```
224
+ - [ ] Open an issue on `RockstarLang/rockstar` asking them to add
225
+ `Access-Control-Allow-Origin: *` to `/wasm/` at their CDN level, so
226
+ the default URL can eventually point back at `codewithrockstar.com`
227
+
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # rockstar-strudel
2
+
3
+ Run [Rockstar](https://codewithrockstar.com) programs from the
4
+ [strudel.cc](https://strudel.cc) live-coding REPL (or any browser-based JS
5
+ environment) via a simple template-tag function.
6
+
7
+ ```js
8
+ const data = await rockstar`
9
+ My heart is 123
10
+ Let your love be 456
11
+ Put 789 into the night
12
+ Shout my heart. Scream your love. Whisper the night.
13
+ `
14
+ // data === [123, 456, 789]
15
+ ```
16
+
17
+ Every value printed by `Say` / `Shout` / `Scream` / `Whisper` becomes one
18
+ element of the returned array. Values that parse as finite numbers are
19
+ returned as JS `number`; everything else is returned as a `string`.
20
+
21
+ ---
22
+
23
+ ## Using it in strudel.cc
24
+
25
+ ```js
26
+ import { init, rockstar } from 'https://esm.sh/rockstar-strudel'
27
+
28
+ // Pre-warm the WASM engine while other code loads (optional but recommended)
29
+ await init()
30
+
31
+ // Run a Rockstar program
32
+ const notes = await rockstar`
33
+ Tommy was 60
34
+ Build Tommy up, up, up, up
35
+ Shout Tommy
36
+ Build Tommy up
37
+ Shout Tommy
38
+ Build Tommy up, up
39
+ Shout Tommy
40
+ `
41
+ // notes === [64, 65, 67]
42
+
43
+ note(notes).sound("piano").slow(2)
44
+ ```
45
+
46
+ ### Template interpolations
47
+
48
+ JavaScript values can be spliced into the source, letting you parameterise
49
+ programs from strudel patterns:
50
+
51
+ ```js
52
+ const root = 60
53
+ const data = await rockstar`
54
+ Tommy was ${root}
55
+ Build Tommy up, up, up, up
56
+ Shout Tommy
57
+ `
58
+ // data === [64]
59
+ ```
60
+
61
+ ---
62
+
63
+ ## API
64
+
65
+ ### `rockstar(strings, ...values)` → `Promise<Array<number|string>>`
66
+
67
+ Tagged-template function. Runs the Rockstar source code and resolves with an
68
+ array of every printed value.
69
+
70
+ ### `init([dotnetUrl])` → `Promise<void>`
71
+
72
+ Pre-loads the WASM engine. Optionally accepts a custom `dotnet.js` URL (see
73
+ [PLAN.md](PLAN.md) for hosting your own copy with CORS headers).
74
+
75
+ ### `buildSource(strings, ...values)` → `string`
76
+
77
+ Pure helper that reconstructs the full source string from a tagged-template
78
+ call. Exported for testing.
79
+
80
+ ### `coerce(line)` → `number | string | undefined`
81
+
82
+ Pure helper that converts a raw WASM callback line to a typed JS value.
83
+ Exported for testing.
84
+
85
+ ---
86
+
87
+ ## How it works
88
+
89
+ The Rockstar **Starship** engine is a .NET 9 application compiled to
90
+ WebAssembly. The built WASM is hosted at
91
+ `https://codewithrockstar.com/wasm/`. This package dynamically imports
92
+ `dotnet.js` from that URL, initialises the runtime, and calls
93
+ `RockstarRunner.Run(source, outputCallback, stdin, args)`, which is
94
+ `[JSExport]`'d from C#.
95
+
96
+ Each call to `Say`/`Shout`/`Scream`/`Whisper` in the Rockstar program triggers
97
+ the callback with the printed string (plus a trailing newline added by
98
+ `WasmIO.WriteLine` in C#). The tag strips whitespace and coerces numeric
99
+ strings to `number`.
100
+
101
+ ---
102
+
103
+ ## CORS requirement
104
+
105
+ `codewithrockstar.com` must serve its `/wasm/` assets with the header
106
+
107
+ ```
108
+ Access-Control-Allow-Origin: *
109
+ ```
110
+
111
+ Without this, browsers will block the cross-origin `import()` of `dotnet.js`.
112
+ See [PLAN.md](PLAN.md) for the exact steps needed in the rockstar fork.
113
+
114
+ ---
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ npm test # run the 19 unit tests (pure JS logic, no WASM required)
120
+ ```
121
+
122
+ Integration testing (actual Rockstar program execution) requires a browser
123
+ environment where the WASM can load; see [PLAN.md](PLAN.md) for details.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "rockstar-strudel",
3
+ "version": "1.0.0",
4
+ "description": "Run Rockstar lang programs via the Starship WASM engine, returning output as a JS array. Designed for use in the strudel.cc live-coding REPL.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stretchyboy/rockstar-strudel.git"
16
+ },
17
+ "keywords": [
18
+ "rockstar",
19
+ "strudel",
20
+ "live-coding",
21
+ "music",
22
+ "wasm",
23
+ "template-tag"
24
+ ],
25
+ "author": "",
26
+ "license": "AGPL-3.0",
27
+ "bugs": {
28
+ "url": "https://github.com/stretchyboy/rockstar-strudel/issues"
29
+ },
30
+ "homepage": "https://github.com/stretchyboy/rockstar-strudel#readme"
31
+ }
package/src/index.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * rockstar-strudel
3
+ *
4
+ * Runs Rockstar lang programs via the Starship WebAssembly engine and returns
5
+ * each printed value as an element of a JavaScript array, for use in the
6
+ * strudel.cc live-coding REPL.
7
+ *
8
+ * Quick start in strudel.cc
9
+ * ─────────────────────────
10
+ * import { rockstar } from 'https://esm.sh/rockstar-strudel'
11
+ *
12
+ * const data = await rockstar`
13
+ * My heart is 123
14
+ * Let your love be 456
15
+ * Put 789 into the night
16
+ * Shout my heart. Scream your love. Whisper the night.
17
+ * `
18
+ * // data === [123, 456, 789]
19
+ *
20
+ * CORS requirement
21
+ * ────────────────
22
+ * The WASM engine is loaded from https://codewithrockstar.com/wasm/.
23
+ * That origin must serve its WASM assets with
24
+ * Access-Control-Allow-Origin: *
25
+ * See PLAN.md for instructions on enabling this in a rockstar fork.
26
+ */
27
+
28
+ /** Default URL of the .NET WASM loader published by codewithrockstar.com. */
29
+ const DEFAULT_DOTNET_URL =
30
+ 'https://codewithrockstar.com/wasm/wwwroot/_framework/dotnet.js';
31
+
32
+ /**
33
+ * URL prefixes that are considered safe for loading the dotnet.js WASM
34
+ * runtime. Calls to `init()` with a URL that does not start with one of
35
+ * these prefixes (or match the github.io pattern) are rejected to prevent
36
+ * loading arbitrary remote code.
37
+ * Extend this list if you host the WASM yourself on a trusted CDN.
38
+ */
39
+ export const ALLOWED_URL_PREFIXES = [
40
+ 'https://codewithrockstar.com/',
41
+ 'https://cdn.jsdelivr.net/',
42
+ 'https://unpkg.com/',
43
+ 'http://localhost:',
44
+ 'http://127.0.0.1:',
45
+ ];
46
+
47
+ /** github.io subdomains (e.g. username.github.io) are also trusted. */
48
+ export const GITHUB_IO_PATTERN = /^https:\/\/[^.]+\.github\.io\//;
49
+
50
+ /**
51
+ * Returns true if the given dotnetUrl is trusted for WASM loading.
52
+ * Exported for testing.
53
+ * @param {string} url
54
+ * @returns {boolean}
55
+ */
56
+ export function isTrustedUrl(url) {
57
+ return (
58
+ ALLOWED_URL_PREFIXES.some((prefix) => url.startsWith(prefix)) ||
59
+ GITHUB_IO_PATTERN.test(url)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * The single cached promise that resolves to the RockstarRunner export object.
65
+ * Kept at module scope so the WASM runtime is initialised at most once per
66
+ * page/worker load.
67
+ * @type {Promise<object> | null}
68
+ */
69
+ let _runnerPromise = null;
70
+
71
+ /**
72
+ * Pre-load the Rockstar WASM engine.
73
+ *
74
+ * Called automatically on the first use of the `rockstar` tag, but you can
75
+ * call it earlier to eliminate the cold-start delay on the first program run.
76
+ *
77
+ * @param {string} [dotnetUrl]
78
+ * Override the dotnet.js loader URL.
79
+ * Useful when you host the WASM assets yourself (e.g. after following the
80
+ * steps in PLAN.md to add CORS headers to your own rockstar fork deployment).
81
+ * Defaults to DEFAULT_DOTNET_URL.
82
+ * @returns {Promise<void>}
83
+ */
84
+ export async function init(dotnetUrl) {
85
+ if (!_runnerPromise) {
86
+ _runnerPromise = _loadRunner(dotnetUrl ?? DEFAULT_DOTNET_URL);
87
+ }
88
+ await _runnerPromise;
89
+ }
90
+
91
+ /**
92
+ * Internal: dynamically import the dotnet.js WASM loader, initialise the
93
+ * .NET runtime, and return the JSExport'd RockstarRunner object.
94
+ *
95
+ * dotnet.js resolves all sibling WASM/assembly blobs relative to its own URL,
96
+ * so as long as the hosting origin sets CORS headers the runtime loads without
97
+ * any additional configuration.
98
+ *
99
+ * @param {string} dotnetUrl
100
+ * @returns {Promise<object>} Resolves to `exports.Rockstar.Wasm.RockstarRunner`
101
+ */
102
+ async function _loadRunner(dotnetUrl) {
103
+ if (!isTrustedUrl(dotnetUrl)) {
104
+ throw new Error(
105
+ `Untrusted dotnet.js URL: "${dotnetUrl}". ` +
106
+ `Must start with one of: ${ALLOWED_URL_PREFIXES.join(', ')} ` +
107
+ `or be a *.github.io URL. ` +
108
+ `Add your CDN prefix to ALLOWED_URL_PREFIXES in src/index.js if needed.`
109
+ );
110
+ }
111
+ // eslint-disable-next-line no-eval -- dynamic import from a runtime URL
112
+ const { dotnet } = await import(/* webpackIgnore: true */ dotnetUrl);
113
+ const { getAssemblyExports, getConfig } = await dotnet
114
+ .withDiagnosticTracing(false)
115
+ .create();
116
+ const config = getConfig();
117
+ const exports = await getAssemblyExports(config.mainAssemblyName);
118
+ return exports.Rockstar.Wasm.RockstarRunner;
119
+ }
120
+
121
+ /**
122
+ * Return the cached runner promise, initialising with the default URL if
123
+ * `init()` has not been called yet.
124
+ * @returns {Promise<object>}
125
+ */
126
+ function _runner() {
127
+ if (!_runnerPromise) _runnerPromise = _loadRunner(DEFAULT_DOTNET_URL);
128
+ return _runnerPromise;
129
+ }
130
+
131
+ /**
132
+ * Coerce a single raw output line (as delivered by the WASM callback) to a
133
+ * typed JavaScript value.
134
+ *
135
+ * - Trailing `\r\n` / `\n` (added by `WasmIO.WriteLine` in C#) is stripped.
136
+ * - Blank lines after stripping are ignored (returns `undefined`).
137
+ * - Finite numbers are returned as JS `number`.
138
+ * - Everything else is returned as a `string`.
139
+ *
140
+ * @param {string} line Raw callback argument from the WASM engine.
141
+ * @returns {number | string | undefined}
142
+ */
143
+ export function coerce(line) {
144
+ const trimmed = line.trimEnd();
145
+ if (trimmed === '') return undefined;
146
+ const num = Number(trimmed);
147
+ return Number.isFinite(num) ? num : trimmed;
148
+ }
149
+
150
+ /**
151
+ * Build the full Rockstar source string from a tagged-template call's parts.
152
+ *
153
+ * Template interpolations are stringified and spliced in, so you can
154
+ * parameterise programs:
155
+ *
156
+ * const n = 10;
157
+ * await rockstar`Tommy was ${n}\nShout Tommy`
158
+ * // equivalent to running: Tommy was 10 \n Shout Tommy
159
+ *
160
+ * @param {TemplateStringsArray} strings
161
+ * @param {...*} values
162
+ * @returns {string}
163
+ */
164
+ export function buildSource(strings, ...values) {
165
+ return strings.reduce((acc, str, i) => {
166
+ const interpolated = values[i - 1];
167
+ return acc + (interpolated !== undefined ? String(interpolated) : '') + str;
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Template tag that executes a Rockstar program and returns an array of every
173
+ * value printed by the program (via `Say` / `Shout` / `Scream` / `Whisper`).
174
+ *
175
+ * Values that parse as finite numbers are returned as JS `number`; everything
176
+ * else is returned as a `string`.
177
+ *
178
+ * The WASM engine is loaded lazily on the first call and cached for subsequent
179
+ * calls. You can call `init()` first to pre-warm the engine if desired.
180
+ *
181
+ * @example
182
+ * const data = await rockstar`
183
+ * My heart is 123
184
+ * Let your love be 456
185
+ * Put 789 into the night
186
+ * Shout my heart. Scream your love. Whisper the night.
187
+ * `
188
+ * // data === [123, 456, 789]
189
+ *
190
+ * @param {TemplateStringsArray} strings
191
+ * @param {...*} values
192
+ * @returns {Promise<Array<number|string>>}
193
+ */
194
+ export async function rockstar(strings, ...values) {
195
+ const code = buildSource(strings, ...values);
196
+ const runner = await _runner();
197
+ const outputs = [];
198
+
199
+ await runner.Run(
200
+ code,
201
+ (line) => {
202
+ const value = coerce(line);
203
+ if (value !== undefined) outputs.push(value);
204
+ },
205
+ /* stdin */ '',
206
+ /* args */ ''
207
+ );
208
+
209
+ return outputs;
210
+ }