A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e68f727ec6d8bde0a9a06f73af2ebae7f10ffc5f 416 lines 11 kB view raw
1import type { RequestHandler } from "lume/core/server.ts"; 2 3import { dotenvRun } from "@dotenv-run/esbuild"; 4import lume from "lume/mod.ts"; 5 6import brotli from "lume/plugins/brotli.ts"; 7import esbuild from "lume/plugins/esbuild.ts"; 8import postcss from "lume/plugins/postcss.ts"; 9import sourceMaps from "lume/plugins/source_maps.ts"; 10 11import * as path from "@std/path"; 12import { ensureDirSync } from "@std/fs/ensure-dir"; 13import { walkSync } from "@std/fs/walk"; 14import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; 15import { wasmLoader } from "esbuild-plugin-wasm"; 16import autoprefixer from "autoprefixer"; 17import cssnano from "cssnano"; 18 19import { create as createCID } from "~/common/cid.js"; 20 21const site = lume({ 22 dest: "./dist", 23 src: "./src", 24 server: { 25 debugBar: false, 26 middlewares: [facetHtmlMiddleware], 27 }, 28}); 29 30export default site; 31 32//////////////////////////////////////////// 33// JS 34//////////////////////////////////////////// 35 36site.use(esbuild({ 37 extensions: [".js"], 38 options: { 39 alias: { 40 "@automerge/automerge": "https://esm.sh/@automerge/automerge@^3.2.3", 41 }, 42 bundle: true, 43 format: "esm", 44 minify: true, 45 external: ["./file-tree.json", "@awesome.me/webawesome/*"], 46 platform: "browser", 47 plugins: [ 48 // @ts-ignore 49 dotenvRun({ 50 files: [".env"], 51 }), 52 // Force @atcute/uint8array to use the browser entry (dist/index.js) 53 // instead of the Node entry (dist/index.node.js) which imports from 54 // node:crypto. The @deno/loader Workspace defaults to platform "node", 55 // causing the "node" export condition to match before "default". 56 { 57 name: "atcute-uint8array-browser", 58 setup(build) { 59 build.onLoad( 60 { filter: /@atcute\+uint8array.*index\.node\.js$/ }, 61 async (args) => { 62 const browserPath = args.path.replace( 63 "index.node.js", 64 "index.js", 65 ); 66 const contents = await Deno.readTextFile(browserPath); 67 return { contents, loader: "js" }; 68 }, 69 ); 70 }, 71 }, 72 { 73 name: "atcute-tid-browser", 74 setup(build) { 75 build.onLoad( 76 { filter: /@atcute\+tid.*random-node\.js$/ }, 77 async (args) => { 78 const browserPath = args.path.replace( 79 "random-node.js", 80 "random-web.js", 81 ); 82 const contents = await Deno.readTextFile(browserPath); 83 return { contents, loader: "js" }; 84 }, 85 ); 86 }, 87 }, 88 { 89 name: "atcute-multibase-browser", 90 setup(build) { 91 build.onLoad( 92 { filter: /@atcute[+/]multibase.*-node\.js$/ }, 93 async (args) => { 94 const browserPath = args.path.replace( 95 "-node.js", 96 "-web.js", 97 ); 98 const contents = await Deno.readTextFile(browserPath); 99 return { contents, loader: "js" }; 100 }, 101 ); 102 }, 103 }, 104 // nanoid ships a browser entry (index.browser.js) but esbuild resolves 105 // the default condition (index.js) which uses Buffer.allocUnsafe. 106 { 107 name: "nanoid-browser", 108 setup(build) { 109 build.onLoad( 110 { filter: /nanoid\/index\.js$/ }, 111 async (args) => { 112 const browserPath = args.path.replace( 113 "index.js", 114 "index.browser.js", 115 ); 116 const contents = await Deno.readTextFile(browserPath); 117 return { contents, loader: "js" }; 118 }, 119 ); 120 }, 121 }, 122 nodeModulesPolyfillPlugin({ 123 fallback: "empty", 124 modules: [], 125 }), 126 wasmLoader(), 127 ], 128 splitting: true, 129 target: "esnext", 130 }, 131})); 132 133site.add([".js"]); 134 135// *.inline.js files are inlined into their companion HTML at build/serve time. 136// Exclude them from the regular build so esbuild doesn't try to bundle them. 137site.ignore((p) => p.endsWith(".inline.js")); 138 139//////////////////////////////////////////// 140// CSS 141//////////////////////////////////////////// 142 143site.use(postcss({ 144 plugins: [ 145 autoprefixer(), 146 cssnano({ 147 preset: "default", 148 }), 149 ], 150})); 151 152site.add([".css"]); 153 154site.remoteFile( 155 "vendor/98.css", 156 import.meta.resolve("./node_modules/98.css/dist/98.css"), 157); 158 159//////////////////////////////////////////// 160// BINARY ASSETS 161//////////////////////////////////////////// 162 163site.add("/favicons", "/"); 164site.add("/fonts"); 165site.add("/images"); 166site.add("/testing"); 167site.add([".woff2"]); 168 169site.remoteFile( 170 "vendor/ms_sans_serif.woff2", 171 import.meta.resolve( 172 "./node_modules/98.css/fonts/converted/ms_sans_serif.woff2", 173 ), 174); 175 176site.remoteFile( 177 "vendor/ms_sans_serif_bold.woff2", 178 import.meta.resolve( 179 "./node_modules/98.css/fonts/converted/ms_sans_serif_bold.woff2", 180 ), 181); 182 183site.remoteFile( 184 "fonts/98.css/ms_sans_serif.woff2", 185 import.meta.resolve( 186 "./node_modules/98.css/fonts/converted/ms_sans_serif.woff2", 187 ), 188); 189 190site.remoteFile( 191 "fonts/98.css/ms_sans_serif_bold.woff2", 192 import.meta.resolve( 193 "./node_modules/98.css/fonts/converted/ms_sans_serif_bold.woff2", 194 ), 195); 196 197//////////////////////////////////////////// 198// DEFINITIONS 199//////////////////////////////////////////// 200 201site.add("/definitions"); 202 203// HELPERS 204 205site.filter("facetURI", (text) => { 206 if (text.includes("://")) { 207 return text; 208 } else { 209 return `diffuse://${text}`; 210 } 211}); 212 213site.filter("facetLoaderURL", (text) => { 214 let key = "path"; 215 216 if (text.includes("://")) { 217 key = "uri"; 218 } 219 220 return `l/?${key}=${encodeURIComponent(text)}`; 221}); 222 223//////////////////////////////////////////// 224// PHOSPHOR ICONS 225//////////////////////////////////////////// 226 227function phosphor(path: string) { 228 site.remoteFile( 229 `vendor/@phosphor-icons/web/${path}`, 230 import.meta.resolve(`./node_modules/@phosphor-icons/web/src/${path}`), 231 ); 232 233 site.add(`vendor/@phosphor-icons/web/${path}`); 234} 235 236["bold", "duotone", "fill", "light", "regular", "light"].forEach((v) => { 237 const f = v === "regular" ? "" : `-${v[0].toUpperCase()}${v.slice(1)}`; 238 phosphor(`${v}/selection.json`); 239 phosphor(`${v}/style.css`); 240 phosphor(`${v}/Phosphor${f}.svg`); 241 phosphor(`${v}/Phosphor${f}.ttf`); 242 phosphor(`${v}/Phosphor${f}.woff`); 243 phosphor(`${v}/Phosphor${f}.woff2`); 244}); 245 246//////////////////////////////////////////// 247// WEB AWESOME 248//////////////////////////////////////////// 249 250for ( 251 const f of walkSync("./node_modules/@awesome.me/webawesome/dist-cdn/", { 252 includeDirs: false, 253 }) 254) { 255 const relativePath = f.path.replace( 256 /^node_modules\/@awesome\.me\/webawesome\/dist-cdn\//, 257 "", 258 ); 259 260 const destPath = `vendor/@awesome.me/webawesome/${relativePath}`; 261 262 site.remoteFile( 263 destPath, 264 import.meta.resolve( 265 `./node_modules/@awesome.me/webawesome/dist-cdn/${relativePath}`, 266 ), 267 ); 268 269 site.copy(destPath); 270} 271 272//////////////////////////////////////////// 273// MISC 274//////////////////////////////////////////// 275 276site.add([".html"]); 277site.add([".json"]); 278site.add([".webmanifest"]); 279 280site.add("llms.txt"); 281 282site.use(brotli()); 283site.use(sourceMaps()); 284 285site.script("copy-type-defs", () => { 286 for ( 287 const f of walkSync( 288 "./src/", 289 { includeDirs: false, exts: [".d.ts"] }, 290 ) 291 ) { 292 const dest = "dist/" + f.path.replace(/^src\//, ""); 293 const dir = path.dirname(dest); 294 ensureDirSync(dir); 295 Deno.copyFileSync(f.path, dest); 296 } 297}); 298 299site.addEventListener("afterBuild", () => { 300 // site.run("copy-type-defs"); 301}); 302 303//////////////////////////////////////////// 304// MIDDLEWARE 305//////////////////////////////////////////// 306 307// Facet HTML files are HTML fragments fetched via JS, not full pages. 308// Serving them as text/plain prevents Lume's dev server from injecting 309// its live-reload <script> tag into the fetched content. 310// 311// Also inlines any <script type="module" src="./foo.inline.js"> references so 312// that forked facets contain readable JS rather than an external file reference. 313async function facetHtmlMiddleware( 314 request: Request, 315 next: RequestHandler, 316): Promise<Response> { 317 const { pathname } = new URL(request.url); 318 const isFacetHtml = pathname.endsWith(".html") && 319 !pathname.startsWith("/testing/"); 320 const response = await next(request); 321 322 if (!isFacetHtml || !response.headers.get("content-type")?.includes("html")) { 323 return response; 324 } 325 326 let content = await response.text(); 327 content = await inlineScriptSrc(content); 328 329 const headers = new Headers(response.headers); 330 headers.set("content-type", "text/plain; charset=utf-8"); 331 return new Response(content, { 332 status: response.status, 333 statusText: response.statusText, 334 headers, 335 }); 336} 337 338const SCRIPT_SRC_RE = 339 /<script type="module" src="([^"]+\.inline\.js)"><\/script>/; 340 341async function inlineScriptSrc(content: string): Promise<string> { 342 const match = SCRIPT_SRC_RE.exec(content); 343 if (!match) return content; 344 345 const jsPath = path.join("src", match[1]); 346 try { 347 return htmlWithInlineJs({ content, jsPath, match: match[0] }); 348 } catch { 349 return content; 350 } 351} 352 353site.addEventListener("afterBuild", async () => { 354 for ( 355 const f of walkSync("./dist/", { includeDirs: false, exts: [".html"] }) 356 ) { 357 const content = Deno.readTextFileSync(f.path); 358 const match = SCRIPT_SRC_RE.exec(content); 359 if (!match) continue; 360 361 const jsPath = path.join("src", match[1]); 362 363 try { 364 const newContent = htmlWithInlineJs({ content, jsPath, match: match[0] }); 365 Deno.writeTextFileSync(f.path, newContent); 366 } catch { 367 // leave as-is if the source file can't be read 368 } 369 } 370}); 371 372site.addEventListener("afterBuild", async () => { 373 const RAW = 0x55; 374 375 async function buildFileTree( 376 dir: string, 377 prefix = "", 378 ): Promise<Record<string, string>> { 379 const tree: Record<string, string> = {}; 380 381 for (const entry of Deno.readDirSync(dir)) { 382 const entryPath = path.join(dir, entry.name); 383 const entryKey = prefix ? `${prefix}/${entry.name}` : entry.name; 384 if (entry.isDirectory) { 385 Object.assign(tree, await buildFileTree(entryPath, entryKey)); 386 } else { 387 const data = Deno.readFileSync(entryPath); 388 tree[entryKey] = await createCID(RAW, data); 389 } 390 } 391 392 return tree; 393 } 394 395 const tree = await buildFileTree("dist/"); 396 const sorted = Object.fromEntries( 397 Object.keys(tree).sort().map((k) => [k, tree[k]]), 398 ); 399 400 Deno.writeTextFileSync( 401 "./dist/file-tree.json", 402 JSON.stringify(sorted, null, 2), 403 ); 404}); 405 406function htmlWithInlineJs({ content, match, jsPath }: { 407 content: string; 408 match: string; 409 jsPath: string; 410}): string { 411 const js = 412 Deno.readTextFileSync(jsPath).split("\n").map((line) => ` ${line}`).join( 413 "\n", 414 ).trimEnd() + "\n"; 415 return content.replace(match, `<script type="module">\n${js}</script>`); 416}