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