an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
at main 184 lines 5.0 kB view raw
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}