1// A worktree's own samples, in a page on a dev server that serves another 2// checkout: for the headless tools (tools/measure, tools/check-songs), which 3// play songs on the main dev server while a review may have re-rendered or 4// added samples in its worktree. 5// 6// The dev server's sample map (pages/jev-samples/strudel.json.js) lists each 7// file as `@fs/<its checkout>/samples/<sound>/<file>`, so the map says which 8// checkout it serves. When that is not this one, `route(page)` answers the 9// page's request for the map with one built from this checkout's samples/ 10// (sampleMap.mjs, the site's own builder), and its requests for sample files 11// under either checkout's samples/ from this checkout's files, so a sample 12// only this worktree has plays too, and one it deleted does not. 13import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs'; 14import { extname, join } from 'node:path'; 15import { SAMPLE_EXTENSIONS, sampleMap } from '../../website/src/jev/sampleMap.mjs'; 16 17const FS_ENTRY = /^@fs(\/.*?)\/samples\/[^/]+\/[^/]+$/; 18 19// The checkout whose samples/ the dev server at `url` serves. 20async function servedRoot(url) { 21 const res = await fetch(`${url}/jev-samples/strudel.json`); 22 if (!res.ok) throw new Error(`${url}/jev-samples/strudel.json answered ${res.status}`); 23 const map = await res.json(); 24 for (const [sound, files] of Object.entries(map)) { 25 if (sound === '_base') continue; 26 const root = [files].flat()[0]?.match(FS_ENTRY)?.[1]; 27 if (root) return root; 28 } 29 throw new Error(`${url}'s sample map names no /@fs/…/samples/ file, so which checkout it serves is unknown`); 30} 31 32// [`samples/<sound>/<file>`, absolute path] for every sample file in `root`. 33function samplesIn(root) { 34 const dir = join(root, 'samples'); 35 const isDir = (p) => statSync(p).isDirectory(); 36 return readdirSync(dir) 37 .filter((sound) => isDir(join(dir, sound))) 38 .flatMap((sound) => 39 readdirSync(join(dir, sound)) 40 .filter((file) => SAMPLE_EXTENSIONS.includes(extname(file).slice(1))) 41 .map((file) => [`samples/${sound}/${file}`, join(dir, sound, file)]), 42 ); 43} 44 45// null when the dev server at `url` serves `root` itself; otherwise 46// { served, root, sounds, route(page) }, `served` being the checkout the 47// server has, and `route` making a page hear `root`'s samples instead. 48export async function worktreeSamples({ url, root }) { 49 const served = realpathSync(await servedRoot(url)); 50 root = realpathSync(root); 51 if (served === root) return null; 52 const files = samplesIn(root); 53 const map = sampleMap(files.map(([rel]) => [rel, `/@fs${root}/${rel}`])); 54 const prefixes = [served, root].map((r) => `/@fs${r}/`); 55 const local = (requestUrl) => { 56 const path = decodeURIComponent(new URL(requestUrl).pathname); 57 const prefix = prefixes.find((p) => path.startsWith(`${p}samples/`)); 58 return prefix && join(root, path.slice(prefix.length)); 59 }; 60 return { 61 served, 62 root, 63 sounds: Object.keys(map).length - 1, 64 async route(page) { 65 await page.route('**/jev-samples/strudel.json', (route) => route.fulfill({ json: map })); 66 await page.route( 67 (u) => Boolean(local(u.href)), 68 (route) => { 69 const file = local(route.request().url()); 70 if (!existsSync(file)) return route.fulfill({ status: 404, body: `not in ${root}/samples` }); 71 return route.fulfill({ path: file }); 72 }, 73 ); 74 }, 75 }; 76} 77 78// The line a tool prints when it routes, so its output says whose samples 79// it heard. 80export const routingNote = (tool, w) => 81 `${tool}: this is a worktree; songs hear its samples (${w.root}/samples, ${w.sounds} sounds), ` + 82 `not the dev server's (${w.served}/samples)`;