From fa043e79b4f8f3ba348039036995221bd73f937d Mon Sep 17 00:00:00 2001 From: River Tae Smith <22485304+r-tae@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:50:03 +1000 Subject: [PATCH 1/2] Add autoreloading development server --- .DS_Store | Bin 10244 -> 0 bytes .gitignore | 2 + dev.js | 129 + package.json | 3 +- simple-live-reload.js | 93 + template.html | 6 +- .../WO_SubCollections/ro-crate-preview.html | 6 +- test_data/sample/ro-crate-preview.html | 39230 +++++++++++++--- test_data/thesis/ro-crate-preview.html | 6 +- 9 files changed, 32997 insertions(+), 6478 deletions(-) delete mode 100644 .DS_Store create mode 100644 dev.js create mode 100644 simple-live-reload.js diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7e24c3b3019e8f181befd00dc3f97446bf20844b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMZ)_Ar6rZ;(bayFquKaO1P*y7fwLobRTK?$uA5x%Jxt5j!9(%V-yRx@?-0odb zLad_32x_9y4<=$X8uf!DDjH*g$`|0Ph8R&|q6TB4iS>g4HU4?CvsVtd5=@98W+&Ns zGjHC^y!p-V?cU50LZCILFCj!FgoyYsNh>ickytx>E=fY5iJ%1RPnN_}PC9dmU8o2V z2oVSo2oVSo2obn;5WsIXOX4Ih!#qSFL?A?93<36jQ1M|h7RV7U{Z|KO{t*CWIja4H zWy%UD)j-AqIl?8jU;^G1CGQHs5d(O4)T@DAERZ8y^6mtJ!v_R2LvTU?e|8*S_00*y zxD4|Ufe?X95nyNcGGY;vB(1^X^ZU%>=UdWFdwbGylBf}tGhIKlJanw3xK+PQYi6i; zR293CN@5d-=)@oilJ-kE^uQo)u?G{9a}Hh3w%Dhs$6CtaG2f?g$4)!vy}T0pV!tOY z7-fD-#A9A!o>ANkjz-7>($qP4nkS^gozauQpKc-ZSf&zIPPg)&TZAQ zTUf2fajayatR**@#&Cj8YjbQ*Gi@X5cQADWO}6%#w(bnKIyqZ+{l0PDAW5Q>pvs;- zjg5_UYQyrTy>;rI=7#z@b!l_M-o27Iy=L*sjftIB+IF5`uHy?acyW@+ygrui$5M%v z>2AtOqNl7|iRR+1g2Sihk7MQL+c-SJxdJ?21n`KGJV}oC$eR+hI*%B1XFSuf-AzW; zMVX*gE@IW0a_qaa25aw2IaV%hy9pXeX;vy{X`azyS#k3L!|nHs-5x6qxK7q<^9>V} z=3HZ=fp%uzXh&*Xzz0HhOg; zn7$R;2HYVnYjh7IDFf{q)cQMvde*5!H$A7&xDFSxnGM&X>;5w6l&Z-}w{nk2KNE2` zOgnF4AmHZmc#vvhwH$sQL9EsX+ai*gWD)-4SWC8Iz-7oja)7)@4wARX`{XD&PCg~4 z$k*f>@-6v}{6u~wXURG88wgMZ5vYc#FdOE>0;q*XXo3~65>`P6tb+~E1$RLoY=Z=B z2Mz9nEO?NEJUj@G!+tmbPr~!?8oUl~z?<+69D$GEV>kg{!dGw_&cM$w3ctYb@CW<} ze+dzxT9_rM!j0T#p%7ku!5RV~uqBp?A2%PoID}m#BV)~-a_2P>8K)|FEKHp?{RS2w z%T~t5xWvg<;9D_^ed=C7c%QodPG7{ilNd2GqqF4e3juX;p!Fi>jol(6F0J6!s(h1( zoTn3bb*?;5M7Gn3yjmNr6EVK1;8*3v(IpZ_5iR4@+URYPNP$?Ag@v#TmP0d= zlu>jItc6Zk4?WO}H0_50NP!MJU=U2Cs*7Z0R2_kbVIMpSPr=jh3_J_Z!9jQlUWQlT zRV42rcoz;|M9Fm*QWB1oreyIKwPV$}voAd2e^E=2xA$N{E1HjIZOgF- z(XT=nUFdRH3lRtr2oVSo2oVSoxLOF5@qtu!|KBnG|NmEOx^SEjfe?Xf5CJG}kGHp? zW8?k!W_Rr>KHKqOiP?>C>36}5nJ~S79gh`u9nUU>n67ZFXXRL+8{rZ)nEvxW1H$wF OmEVko=l}oZ`Trk_SLz}F diff --git a/.gitignore b/.gitignore index c44b1a67..1410632f 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,5 @@ dist .yarn/install-state.gz .pnp.* package-lock.json + +.DS_Store \ No newline at end of file diff --git a/dev.js b/dev.js new file mode 100644 index 00000000..8ffc8dce --- /dev/null +++ b/dev.js @@ -0,0 +1,129 @@ +import * as fs from "node:fs"; +import * as http from "node:http"; +import * as path from "node:path"; +import { Readable } from 'node:stream'; +import { spawn } from 'node:child_process'; + +const PORT = 8000; + +const MIME_TYPES = { + default: "application/octet-stream", + html: "text/html; charset=UTF-8", + js: "text/javascript", + css: "text/css", + png: "image/png", + jpg: "image/jpeg", + gif: "image/gif", + ico: "image/x-icon", + svg: "image/svg+xml", +}; + +const toBool = [() => true, () => false]; + +const STATIC_PATH = path.join(process.cwd(), "./test_data"); + +const PAGES = [] + +try { + const dir = await fs.opendirSync(STATIC_PATH); + for await (const dirent of dir) + if (fs.existsSync(path.join(STATIC_PATH, dirent.name, "ro-crate-preview.html"))) { + PAGES.push({ + name: dirent.name, + link: path.join(dirent.name, "ro-crate-preview.html") + }) + } +} catch (err) { + console.error(err); +} + +function indexPage() { + let contents = ` + + +

RO Crate HTML Lite development server

+
    + ` + contents += "
      " + for (let page of PAGES) { + contents += `
    • ${page.name}
    • ` + } + contents += "
    " + + return { + found: true, + ext: "html", + stream: Readable.from(contents), + } +} + +const prepareFile = async (url) => { + if (url === "/") { + return indexPage() + } + if (url.startsWith("/script")) { + filePath = path.join(process.cwd(), url.slice(7)) + const stream = fs.createReadStream(filePath); + const { mtimeMs } = fs.statSync(filePath); + return { found: true, ext: "js", mtime: mtimeMs, stream } + } + const paths = [STATIC_PATH, url]; + if (url.endsWith("/")) paths.push("../index.html"); + var filePath = path.join(...paths); + const pathTraversal = !filePath.startsWith(STATIC_PATH); + const exists = await fs.promises.access(filePath).then(...toBool); + const found = !pathTraversal && exists; + if (found) { + const ext = path.extname(filePath).substring(1).toLowerCase(); + const stream = fs.createReadStream(filePath); + const { mtimeMs } = fs.statSync(filePath); + return { found, ext, mtime: mtimeMs, stream }; + } else { + return { found: false } + } +}; + +http + .createServer(async (req, res) => { + const file = await prepareFile(req.url); + const statusCode = file.found ? 200 : 404; + const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default; + if (file.found) { + res.writeHead(statusCode, { + "Content-Type": mimeType, + "ETag": file.mtime || null, + }); + file.stream.pipe(res); + } else { + res.writeHead(statusCode, { "Content-Type": mimeType }); + Readable.from("404 not found").pipe(res); + if (req.method !== "HEAD") { + console.log(`Error: ${req.method} ${req.url} ${statusCode}`); + } + } + }) + .listen(PORT); + +console.log(`Server running at http://localhost:${PORT}/\n`); + +const reloadPreview = (fullPath, filename) => { + const proc = spawn("node", + [ + "index.js", + fullPath.slice(0, -filename.length) + ] + ) + proc.on('close', () => console.log(`recreated ${fullPath.slice(STATIC_PATH.length)}`)) +} + +fs.watch(path.join(process.cwd(), "template.html"), async (_eventType, _filename) => { + for await (const page of PAGES) { + reloadPreview(path.join(STATIC_PATH, page.name, "ro-crate-metadata.json"), "ro-crate-metadata.json") + } +}) diff --git a/package.json b/package.json index d1d24317..1ff6eee0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "main": "index.js", "scripts": { "test": "mocha test/test-bounding-box.js", - "visualize": "node test/test-bounding-box.js" + "visualize": "node test/test-bounding-box.js", + "dev": "node --no-warnings dev.js" }, "author": "Peter Sefton", "license": "GPL-3.0", diff --git a/simple-live-reload.js b/simple-live-reload.js new file mode 100644 index 00000000..8b1201ed --- /dev/null +++ b/simple-live-reload.js @@ -0,0 +1,93 @@ +/*Copyright 2025 Lean Rada. + Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the “Software”), to deal in the Software without restriction, including +without limitation therights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ +if ( + location.hostname === "localhost" || + location.hostname === "127.0.0.1" || + location.hostname === "[::1]" +) { + const interval = Number(document.currentScript?.dataset.interval || 1000); + const debug = document.currentScript?.hasAttribute("data-debug") || false; + + let watching = new Set(); + watch(location.href); + + new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + watch(entry.name); + } + }).observe({ type: "resource", buffered: true }); + + function watch(urlString) { + if (!urlString) return; + const url = new URL(urlString); + if (url.origin !== location.origin) return; + + if (watching.has(url.href)) return; + watching.add(url.href); + + if (debug) { + console.log("[simple-live-reload] watching", url.href); + } + + let focused = false; + let etag, lastModified, contentLength; + let request = { method: "head", cache: "no-store" }; + + async function check() { + try { + if (document.hidden) return; + if (focused) return; + } finally { + focused = document.hasFocus(); + } + + const res = await fetch(url, request); + if (res.status === 405 || res.status === 501) { + request.method = "get"; + request.headers = { + Range: "bytes=0-0", + }; + return check(); + } + + const newETag = res.headers.get("ETag"); + const newLastModified = res.headers.get("Last-Modified"); + const newContentLength = res.headers.get("Content-Length"); + + if ( + (etag && etag !== newETag) || + (lastModified && lastModified !== newLastModified) || + (contentLength && contentLength !== newContentLength) + ) { + if (debug) { + console.log("[simple-live-reload] change detected in", url.href); + } + try { + location.reload(); + } catch (e) { + location = location; + } + } + + etag = newETag; + lastModified = newLastModified; + contentLength = newContentLength; + } + + check(); + setInterval(check, interval); + document.addEventListener( + "visibilitychange", + () => !document.hidden && check() + ); + } +} diff --git a/template.html b/template.html index 05af78a0..43c0b6dc 100644 --- a/template.html +++ b/template.html @@ -4,6 +4,8 @@ {{ title }} + +