Make the benchmark command run on older Node
Node only strips TypeScript types by default from 22.18 / 23.6 / 24 on, and
without stripping it cannot load scripts/bench.ts far enough to even print a
useful error -- which is what the command the app hands out does on a machine
with an older Node. Enter through scripts/bench.mjs, plain JS, which re-runs
itself with --experimental-strip-types when the running Node has stripping
available but off. The npm scripts pass the flag too, so the declared engines
floor of 22.6 holds for them as well.
5 changed files+60−8
README.mdmodified+10−1View file
@@ -77,7 +77,7 @@ the app prints the command line that reproduces whatever it is currently
7777 simulating:
7878
7979 ```
80-node scripts/bench.ts --preset schnak-spots --lmax 63 --backend webgpu --steps 2000 \
80+node scripts/bench.mjs --preset schnak-spots --lmax 63 --backend webgpu --steps 2000 \
8181 --seed 1 --a 0.1 --b 0.9 --D1 0.0004 --D2 0.008 --dt 0.05
8282 ```
8383
@@ -147,6 +147,15 @@ npm run dev # local dev server
147147 npm run build # type-check + production build to dist/
148148 ```
149149
150+The `.ts` entry points under `scripts/` are run by Node directly, which strips
151+types without being asked only from Node 22.18 / 23.6 / 24 on. Everything here
152+works back to 22.6, where stripping exists but is flagged: the npm scripts pass
153+`--experimental-strip-types` themselves, and the benchmark — the one command
154+that gets copied to other machines — goes through
155+[`scripts/bench.mjs`](scripts/bench.mjs), which re-runs itself with the flag
156+when it has to. Invoking a `scripts/*.ts` file by hand on 22.6–22.17 needs the
157+flag spelled out.
158+
150159 Deployed to GitHub Pages by `.github/workflows/deploy.yml` on push to `main`.
151160
152161 ## License
package.jsonmodified+5−2View file
@@ -3,14 +3,17 @@
33 "version": "0.1.0",
44 "description": "Live reaction-diffusion (Turing patterns) on the sphere, spectral spherical-harmonic solver on WebGPU",
55 "type": "module",
6+ "engines": {
7+ "node": ">=22.6"
8+ },
69 "license": "CECILL-2.1",
710 "scripts": {
811 "dev": "vite",
912 "build": "tsc --noEmit && vite build",
10- "test:node": "node scripts/test-node.ts",
13+ "test:node": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/test-node.ts",
1114 "test:gpu": "vite build && node scripts/test-gpu.mjs",
1215 "test": "npm run test:node && npm run test:gpu",
13- "bench": "node scripts/bench.ts"
16+ "bench": "node scripts/bench.mjs"
1417 },
1518 "dependencies": {
1619 "three": "^0.183.0"
scripts/bench.mjsadded+37−0View file
@@ -0,0 +1,37 @@
1+/**
2+ * Entry point for the benchmark. The app hands this command to whoever is
3+ * running the demo, so it has to survive landing on a machine with an older
4+ * Node than this repo develops against: Node only strips TypeScript types by
5+ * default from 22.18 / 23.6 / 24 on, and without stripping it cannot load
6+ * scripts/bench.ts at all — not even far enough to print a useful error.
7+ *
8+ * So this wrapper is plain JS, and re-executes itself with the flag when the
9+ * running Node has stripping available but off (22.6 through 22.17).
10+ */
11+import { spawnSync } from 'node:child_process';
12+import { fileURLToPath } from 'node:url';
13+
14+if (process.features.typescript) {
15+ await import('./bench.ts');
16+} else {
17+ const entry = fileURLToPath(new URL('./bench.ts', import.meta.url));
18+ const result = spawnSync(
19+ process.execPath,
20+ [
21+ '--experimental-strip-types',
22+ '--disable-warning=ExperimentalWarning',
23+ entry,
24+ ...process.argv.slice(2),
25+ ],
26+ { stdio: 'inherit' },
27+ );
28+ // exit code 9 is node's "bad option": this Node predates type stripping
29+ if (result.error || result.status === 9) {
30+ console.error(
31+ `bench: Node ${process.versions.node} cannot run this project's TypeScript sources.\n` +
32+ ' Node 22.6+ can with --experimental-strip-types; 22.18, 23.6 and 24+ do it by default.',
33+ );
34+ process.exit(1);
35+ }
36+ process.exit(result.status ?? 1);
37+}
scripts/bench.tsmodified+3−2View file
@@ -5,10 +5,11 @@
55 * reference, and report ms/step. The app prints the matching command under
66 * its stats line; copy it and run it here for an apples-to-apples comparison.
77 *
8- * node scripts/bench.ts --preset schnak-spots --lmax 63 --backend webgpu \
9- * --steps 500 --seed 1 --a 0.1 --b 0.9 --D1 0.0004 --D2 0.008 --dt 0.05
8+ * node scripts/bench.mjs --preset schnak-spots --lmax 63 --backend webgpu \
9+ * --steps 2000 --seed 1 --a 0.1 --b 0.9 --D1 0.0004 --D2 0.008 --dt 0.05
1010 *
1111 * The only thing missing here is the rendering: this is the solver alone.
12+ * Entry point is scripts/bench.mjs, which copes with older Node versions.
1213 */
1314 import {
1415 GpuBackend,
src/bench/runSpec.tsmodified+5−3View file
@@ -1,7 +1,7 @@
11 /**
22 * One solver run, described in a single object shared by the browser app and
33 * the command-line benchmark. The app formats the run it is currently showing
4- * into a `node scripts/bench.ts ...` command; the benchmark parses that command
4+ * into a `node scripts/bench.mjs ...` command; the benchmark parses that command
55 * back into the same object and drives the same Simulation with it. Neither
66 * side keeps its own copy of the defaults, so the two runs cannot drift apart.
77 */
@@ -33,8 +33,10 @@ export interface RunSpec {
3333 params: Params;
3434 }
3535
36-/** The command the app displays and the benchmark answers to. */
37-export const BENCH_COMMAND = 'node scripts/bench.ts';
36+/** The command the app displays and the benchmark answers to. It names the
37+ * .mjs wrapper rather than bench.ts, so that it also runs on a Node that
38+ * needs to be told to strip types (see scripts/bench.mjs). */
39+export const BENCH_COMMAND = 'node scripts/bench.mjs';
3840 export const DEFAULT_LMAX = 63;
3941 export const DEFAULT_SEED = 1;
4042 /** Long enough that clock ramp-up and the occasional scheduling hiccup wash