A music player that connects to your cloud/distributed storage.
1import { dotenvRun } from "@dotenv-run/esbuild";
2import lume from "lume/mod.ts";
3
4import brotli from "lume/plugins/brotli.ts";
5import esbuild from "lume/plugins/esbuild.ts";
6import postcss from "lume/plugins/postcss.ts";
7import sourceMaps from "lume/plugins/source_maps.ts";
8
9import * as path from "@std/path";
10import { ensureDirSync } from "@std/fs/ensure-dir";
11import { walkSync } from "@std/fs/walk";
12import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill";
13import { wasmLoader } from "esbuild-plugin-wasm";
14import autoprefixer from "autoprefixer";
15import cssnano from "cssnano";
16
17import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from "@zip-js/zip-js";
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: [],
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") || p.endsWith("SKILL.md"));
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.script("copy-type-defs", () => {
281 for (
282 const f of walkSync(
283 "./src/",
284 { includeDirs: false, exts: [".d.ts"] },
285 )
286 ) {
287 const dest = "dist/" + f.path.replace(/^src\//, "");
288 const dir = path.dirname(dest);
289 ensureDirSync(dir);
290 Deno.copyFileSync(f.path, dest);
291 }
292});
293
294// SKILLS
295
296site.remoteFile(
297 "skills/diffuse-facet/docs/architecture.txt",
298 import.meta.resolve("./docs/ARCHITECTURE.md"),
299);
300
301site.remoteFile(
302 "skills/diffuse-facet/example/index.html",
303 import.meta.resolve("./src/facets/data/sources/index.html"),
304);
305
306site.add("skills/diffuse-facet/docs/architecture.txt");
307site.add("skills/diffuse-facet/example/index.html");
308site.add("/definitions", "/skills/diffuse-facet/docs/definitions");
309site.copy("skills/diffuse-facet/SKILL.md");
310site.add("skills");
311
312site.addEventListener("afterBuild", async () => {
313 const skillsDir = "dist/skills/diffuse-facet";
314 const zipWriter = new ZipWriter(new Uint8ArrayWriter());
315
316 for (const entry of walkSync(skillsDir, { includeDirs: false })) {
317 if (entry.path.endsWith(".br")) continue;
318 await zipWriter.add(
319 "diffuse-facet/" + entry.path.slice(skillsDir.length + 1),
320 new Uint8ArrayReader(Deno.readFileSync(entry.path)),
321 );
322 }
323
324 Deno.writeFileSync("dist/skills/diffuse-facet.zip", await zipWriter.close());
325});
326
327////////////////////////////////////////////
328// FILE TREE
329////////////////////////////////////////////
330
331site.addEventListener("afterBuild", async () => {
332 const RAW = 0x55;
333
334 async function buildFileTree(
335 dir: string,
336 prefix = "",
337 ): Promise<Record<string, string>> {
338 const tree: Record<string, string> = {};
339
340 for (const entry of Deno.readDirSync(dir)) {
341 const entryPath = path.join(dir, entry.name);
342 const entryKey = prefix ? `${prefix}/${entry.name}` : entry.name;
343 if (entry.isDirectory) {
344 Object.assign(tree, await buildFileTree(entryPath, entryKey));
345 } else {
346 const data = Deno.readFileSync(entryPath);
347 tree[entryKey] = await createCID(RAW, data);
348 }
349 }
350
351 return tree;
352 }
353
354 const tree = await buildFileTree("dist/");
355 const sorted = Object.fromEntries(
356 Object.keys(tree).sort().map((k) => [k, tree[k]]),
357 );
358
359 Deno.writeTextFileSync(
360 "./dist/file-tree.json",
361 JSON.stringify(sorted, null, 2),
362 );
363});
364
365////////////////////////////////////////////
366// INLINE JS FOR FACETS
367////////////////////////////////////////////
368
369const SCRIPT_SRC_RE =
370 /<script type="module" src="([^"]+\.inline\.js)"><\/script>/;
371
372site.process([".html"], (pages) => {
373 for (const page of pages) {
374 const content = page.text;
375 if (!content) continue;
376 const match = SCRIPT_SRC_RE.exec(content);
377 if (!match) continue;
378
379 const jsPath = path.join("src", match[1]);
380 try {
381 page.text = htmlWithInlineJs({ content, jsPath, match: match[0] });
382 } catch {
383 // leave as-is if the source file can't be read
384 }
385 }
386});
387
388function htmlWithInlineJs({ content, match, jsPath }: {
389 content: string;
390 match: string;
391 jsPath: string;
392}): string {
393 const js =
394 Deno.readTextFileSync(jsPath).split("\n").map((line) => ` ${line}`).join(
395 "\n",
396 ).trimEnd() + "\n";
397 return content.replace(match, `<script type="module">\n${js}</script>`);
398}
399
400////////////////////////////////////////////
401// COMPRESSION
402////////////////////////////////////////////
403
404site.use(brotli());
405site.use(sourceMaps());