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 9db48c52fa3e55af5c28fcbd942363d6b8ee2ec4 421 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.remoteFile( 281 "architecture.txt", 282 import.meta.resolve("./docs/ARCHITECTURE.md"), 283); 284 285site.add("architecture.txt"); 286 287site.use(brotli()); 288site.use(sourceMaps()); 289 290site.script("copy-type-defs", () => { 291 for ( 292 const f of walkSync( 293 "./src/", 294 { includeDirs: false, exts: [".d.ts"] }, 295 ) 296 ) { 297 const dest = "dist/" + f.path.replace(/^src\//, ""); 298 const dir = path.dirname(dest); 299 ensureDirSync(dir); 300 Deno.copyFileSync(f.path, dest); 301 } 302}); 303 304site.addEventListener("afterBuild", () => { 305 // site.run("copy-type-defs"); 306}); 307 308//////////////////////////////////////////// 309// MIDDLEWARE 310//////////////////////////////////////////// 311 312// Facet HTML files are HTML fragments fetched via JS, not full pages. 313// Serving them as text/plain prevents Lume's dev server from injecting 314// its live-reload <script> tag into the fetched content. 315// 316// Also inlines any <script type="module" src="./foo.inline.js"> references so 317// that forked facets contain readable JS rather than an external file reference. 318async function facetHtmlMiddleware( 319 request: Request, 320 next: RequestHandler, 321): Promise<Response> { 322 const { pathname } = new URL(request.url); 323 const isFacetHtml = pathname.endsWith(".html") && 324 !pathname.startsWith("/testing/"); 325 const response = await next(request); 326 327 if (!isFacetHtml || !response.headers.get("content-type")?.includes("html")) { 328 return response; 329 } 330 331 let content = await response.text(); 332 content = await inlineScriptSrc(content); 333 334 const headers = new Headers(response.headers); 335 headers.set("content-type", "text/plain; charset=utf-8"); 336 return new Response(content, { 337 status: response.status, 338 statusText: response.statusText, 339 headers, 340 }); 341} 342 343const SCRIPT_SRC_RE = 344 /<script type="module" src="([^"]+\.inline\.js)"><\/script>/; 345 346async function inlineScriptSrc(content: string): Promise<string> { 347 const match = SCRIPT_SRC_RE.exec(content); 348 if (!match) return content; 349 350 const jsPath = path.join("src", match[1]); 351 try { 352 return htmlWithInlineJs({ content, jsPath, match: match[0] }); 353 } catch { 354 return content; 355 } 356} 357 358site.addEventListener("afterBuild", async () => { 359 for ( 360 const f of walkSync("./dist/", { includeDirs: false, exts: [".html"] }) 361 ) { 362 const content = Deno.readTextFileSync(f.path); 363 const match = SCRIPT_SRC_RE.exec(content); 364 if (!match) continue; 365 366 const jsPath = path.join("src", match[1]); 367 368 try { 369 const newContent = htmlWithInlineJs({ content, jsPath, match: match[0] }); 370 Deno.writeTextFileSync(f.path, newContent); 371 } catch { 372 // leave as-is if the source file can't be read 373 } 374 } 375}); 376 377site.addEventListener("afterBuild", async () => { 378 const RAW = 0x55; 379 380 async function buildFileTree( 381 dir: string, 382 prefix = "", 383 ): Promise<Record<string, string>> { 384 const tree: Record<string, string> = {}; 385 386 for (const entry of Deno.readDirSync(dir)) { 387 const entryPath = path.join(dir, entry.name); 388 const entryKey = prefix ? `${prefix}/${entry.name}` : entry.name; 389 if (entry.isDirectory) { 390 Object.assign(tree, await buildFileTree(entryPath, entryKey)); 391 } else { 392 const data = Deno.readFileSync(entryPath); 393 tree[entryKey] = await createCID(RAW, data); 394 } 395 } 396 397 return tree; 398 } 399 400 const tree = await buildFileTree("dist/"); 401 const sorted = Object.fromEntries( 402 Object.keys(tree).sort().map((k) => [k, tree[k]]), 403 ); 404 405 Deno.writeTextFileSync( 406 "./dist/file-tree.json", 407 JSON.stringify(sorted, null, 2), 408 ); 409}); 410 411function htmlWithInlineJs({ content, match, jsPath }: { 412 content: string; 413 match: string; 414 jsPath: string; 415}): string { 416 const js = 417 Deno.readTextFileSync(jsPath).split("\n").map((line) => ` ${line}`).join( 418 "\n", 419 ).trimEnd() + "\n"; 420 return content.replace(match, `<script type="module">\n${js}</script>`); 421}