an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
1import * as esbuild from "npm:esbuild@0.20.2";
2import { ReactCompilerEsbuildPlugin } from "./reactcompileresbuild.ts";
3import { cache } from "npm:esbuild-plugin-cache";
4import tailwindconfig from "../tailwind.config.ts";
5import tailwindcss from "https://esm.sh/tailwindcss@3";
6import postcss from "https://esm.sh/postcss@8";
7import autoprefixer from "https://esm.sh/autoprefixer@10";
8import { renderToString } from "https://esm.sh/react-dom@19.1.1/server";
9import { createElement } from "https://esm.sh/react@19.1.1";
10import { Root } from "./browser/landing-shared.tsx";
11
12// helper build function
13async function build({
14 entry,
15 initialData,
16}: {
17 entry: "index" | "view";
18 initialData?: {
19 config: {
20 inviteOnly: boolean;
21 //port: number;
22 did: string;
23 host: string;
24 indexPriority?: string[];
25 };
26 users: {
27 did: string;
28 role: string;
29 registrationdate: string;
30 onboardingstatus: string;
31 pfp?: string;
32 displayname: string;
33 handle: string;
34 }[];
35 };
36}) {
37 const template = await Deno.readTextFile(
38 `./shared-landing/template-${entry}.html`
39 );
40 const importmap = {
41 imports: {
42 "react/jsx-runtime": "https://esm.sh/react@19.1.1/jsx-runtime",
43 "react/compiler-runtime": "https://esm.sh/react@19.1.1/compiler-runtime",
44 },
45 };
46
47 const result = await esbuild.build({
48 entryPoints: [`./shared-landing/browser/landing-${entry}.tsx`],
49 bundle: true,
50 format: "esm",
51 jsx: "transform",
52 write: false, // keep in memory
53 loader: { ".tsx": "tsx" },
54 jsxFactory: "React.createElement",
55 jsxFragment: "React.Fragment",
56 platform: "neutral", //"browser",
57 external: ["react/jsx-runtime", "react/compiler-runtime"],
58 plugins: [
59 cache({ importmap, directory: "./cache" }),
60 ReactCompilerEsbuildPlugin({
61 filter: /\.tsx?$/,
62 sourceMaps: true,
63 runtimeModulePath: "https://esm.sh/react@19.1.1/jsx-runtime",
64 }),
65 ],
66 });
67 const rawcss = await Deno.readTextFile("./shared-landing/app.css");
68
69 // @ts-ignore its fiiine
70 const cssResult = await postcss([
71 tailwindcss(tailwindconfig),
72 autoprefixer(),
73 ]).process(rawcss, {
74 from: "./shared-landing/app.css",
75 map: false,
76 });
77
78 const js = result.outputFiles[0].text;
79
80 const jshash = hashString(js);
81 const csshash = hashString(cssResult.css);
82 const html = template.replace(
83 "<!--SCRIPT-INJECT-->",
84 `<script type="module" src="/landing-${entry}.js?v=${jshash}"></script>
85 <link href="./app.css?v=${csshash}" rel="stylesheet">
86 <style>${cssResult}</style>
87 <script id="initial-data" type="application/json">
88 ${JSON.stringify(initialData)}
89 </script>`
90 );
91 // const html = template.replace(
92 // "<!--SCRIPT-INJECT-->",
93 // `<script type="module" src="/landing-${entry}.js?v=${jshash}"></script>
94 // <link href="./app.css?v=${csshash}" rel="stylesheet">`
95 // );
96 const ssr = renderToString(
97 createElement(
98 () =>
99 Root({
100 type: entry,
101 initialData
102 }),
103 null
104 )
105 );
106 const ssrhtml = html.replace("<!--APP-INJECT-->", `${ssr}`);
107
108 return { js, html: ssrhtml, css: cssResult.css };
109}
110
111// public compile function
112export async function compile({
113 target,
114 initialData
115}: {
116 target: "index" | "view";
117 initialData?: {
118 config: {
119 inviteOnly: boolean;
120 //port: number;
121 did: string;
122 host: string;
123 indexPriority?: string[];
124 };
125 users: {
126 did: string;
127 role: string;
128 registrationdate: string;
129 onboardingstatus: string;
130 pfp?: string;
131 displayname: string;
132 handle: string;
133 }[];
134 };
135}) {
136 return await build({entry: target, initialData});
137}
138
139// watch loop
140export async function devWatch({target,initialData,onBuild}:{
141 target: "index" | "view",
142 initialData?: {
143 config: {
144 inviteOnly: boolean;
145 //port: number;
146 did: string;
147 host: string;
148 indexPriority?: string[];
149 };
150 users: {
151 did: string;
152 role: string;
153 registrationdate: string;
154 onboardingstatus: string;
155 pfp?: string;
156 displayname: string;
157 handle: string;
158 }[];
159 },
160 onBuild: (data: { js: string; html: string; css: string }) => void
161 }
162) {
163 for await (const event of Deno.watchFs(".")) {
164 if (event.paths.some((p) => p.endsWith(".tsx"))) {
165 console.log("Rebuilding bundle…");
166 const data = await compile({target,initialData});
167 onBuild(data);
168 }
169 }
170}
171
172async function hashString(content: string): Promise<string> {
173 const encoder = new TextEncoder();
174 const data = encoder.encode(content);
175
176 // SHA-256 hash
177 const hashBuffer = await crypto.subtle.digest("SHA-256", data);
178
179 // Convert buffer to hex string
180 return Array.from(new Uint8Array(hashBuffer))
181 .map((b) => b.toString(16).padStart(2, "0"))
182 .join("")
183 .slice(0, 8); // optional: shorten hash for filenames
184}