browser-protocol.js
1 import path from 'node:path' 2 import { Readable } from 'node:stream' 3 import { fileURLToPath } from 'node:url' 4 import mime from 'mime' 5 import ScopedFS from 'scoped-fs' 6 7 import { version, dependencies as packageDependencies } from '../version.js' 8 import Config from '../config.js' 9 const { theme } = Config 10 11 const CHECK_PATHS = [ 12 (path) => path, 13 (path) => path + 'index.html', 14 (path) => path + 'index.md', 15 (path) => path + '/index.html', 16 (path) => path + '/index.md', 17 (path) => path + '.html', 18 (path) => path + '.md' 19 ] 20 21 const pagesURL = new URL('../pages', import.meta.url) 22 const pagesPath = fileURLToPath(pagesURL) 23 24 const fs = new ScopedFS(pagesPath) 25 26 export default async function createHandler () { 27 return { handler: protocolHandler, close } 28 29 function close () {} 30 31 async function protocolHandler (req, sendResponse) { 32 const { url } = req 33 34 const parsed = new URL(url) 35 const { pathname, hostname } = parsed 36 const toResolve = path.join(hostname, pathname) 37 38 if (hostname === 'about') { 39 const statusCode = 200 40 41 const packagesToRender = [ 42 'hypercore-fetch', 43 'hyper-sdk', 44 'js-ipfs-fetch', 45 'ipfs-core', 46 'bt-fetch', 47 'gun-fetch', 48 'gemini-fetch' 49 ] 50 51 const dependencies = {} 52 for (const name of packagesToRender) { 53 dependencies[name] = packageDependencies[name] 54 } 55 56 const aboutInfo = { 57 version, 58 dependencies 59 } 60 61 const data = intoStream(JSON.stringify(aboutInfo, null, '\t')) 62 63 const headers = { 64 'Access-Control-Allow-Origin': '*', 65 'Allow-CSP-From': '*', 66 'Content-Type': 'application/json' 67 } 68 69 sendResponse({ 70 statusCode, 71 headers, 72 data 73 }) 74 75 return 76 } else if ((hostname === 'theme') && (pathname === '/vars.css')) { 77 const statusCode = 200 78 79 const themes = Object 80 .keys(theme) 81 .map((name) => ` --ag-theme-${name}: ${theme[name]};`) 82 .join('\n') 83 84 const data = intoStream(` 85 :root { 86 --ag-color-purple: #6e2de5; 87 --ag-color-black: #111; 88 --ag-color-white: #F2F2F2; 89 --ag-color-green: #2de56e; 90 } 91 92 :root { 93 ${themes} 94 } 95 `) 96 97 const headers = { 98 'Access-Control-Allow-Origin': '*', 99 'Allow-CSP-From': '*', 100 'Cache-Control': 'no-cache', 101 'Content-Type': 'text/css' 102 } 103 104 sendResponse({ 105 statusCode, 106 headers, 107 data 108 }) 109 110 return 111 } 112 113 try { 114 const resolvedPath = await resolveFile(toResolve) 115 const statusCode = 200 116 117 const contentType = mime.getType(resolvedPath) || 'text/plain' 118 119 const data = fs.createReadStream(resolvedPath) 120 121 const headers = { 122 'Access-Control-Allow-Origin': '*', 123 'Allow-CSP-From': 'agregore://welcome', 124 'Cache-Control': 'no-cache', 125 'Content-Type': contentType 126 } 127 128 sendResponse({ 129 statusCode, 130 headers, 131 data 132 }) 133 } catch (e) { 134 const statusCode = 404 135 136 const data = fs.createReadStream('404.html') 137 138 const headers = { 139 'Access-Control-Allow-Origin': '*', 140 'Allow-CSP-From': '*', 141 'Cache-Control': 'no-cache', 142 'Content-Type': 'text/html' 143 } 144 145 sendResponse({ 146 statusCode, 147 headers, 148 data 149 }) 150 } 151 } 152 } 153 154 async function resolveFile (path) { 155 for (const toTry of CHECK_PATHS) { 156 const tryPath = toTry(path) 157 if (await exists(tryPath)) return tryPath 158 } 159 throw new Error('Not Found') 160 } 161 162 function exists (path) { 163 return new Promise((resolve, reject) => { 164 fs.stat(path, (err, stat) => { 165 if (err) { 166 if (err.code === 'ENOENT') resolve(false) 167 else reject(err) 168 } else resolve(stat.isFile()) 169 }) 170 }) 171 } 172 173 function intoStream (data) { 174 return new Readable({ 175 read () { 176 this.push(data) 177 this.push(null) 178 } 179 }) 180 }