// import React from 'react' // import ReactDOMServer from 'react-dom/server' import { contentType } from 'media-types' import { Router } from 'oak' // import App from '~/client/App.tsx' function indexOfAll(source: string, query: string): number[] { let currentPosition = 0 const buffer: number[] = [] while (currentPosition !== -1) { const lastPosition = currentPosition currentPosition = source.indexOf(query, lastPosition + 1) if (currentPosition !== -1) buffer.push(currentPosition) } // ensure it's sorted in case while loop betrays us return buffer.sort() } interface ResolvedEnvVar { callsiteBounds: [number, number] varname: string value?: string } const ENV_SEARCH_STRING = "Deno.env.get(\"" const ENV_SEARCH_END_STRING = "\")" function resolveClientDenoEnvVars(source: string): string { const startIndices = indexOfAll(source, ENV_SEARCH_STRING) const resolved = startIndices.map((startIndex) => { const endIndex = source.indexOf(ENV_SEARCH_END_STRING, startIndex) const varname = source.slice(startIndex + ENV_SEARCH_STRING.length, endIndex) const bounds = [startIndex, endIndex + ENV_SEARCH_END_STRING.length] const value = Deno.env.get(varname) return { callsiteBounds: bounds, varname, value: `"${value}"`, } }) function replaceBounds(input: string, newText: string | undefined, start: number, end: number) { return `${input.slice(0, start)}${newText}${input.slice(end)}` } // reassemble source code by reversing the order and replacing strings const reassembledSource = resolved.reverse().reduce( (currentSource, {callsiteBounds, value, varname}) => { if (value === undefined) throw new Error(`Deno env variable ${varname} found in client code but not provided to server.`) return replaceBounds(currentSource, value, callsiteBounds[0], callsiteBounds[1]) }, source, ) return reassembledSource } function mapDict( dict: Record, fn: (entry: [string, T], index: number) => [string, U], ): Record { return Object.fromEntries( Object.entries(dict).map(fn) ) } async function createClientBundle() { const {files} = await Deno.emit('server/client.tsx', { bundle: 'module', importMapPath: 'import_map.json', compilerOptions: { lib: ["dom", "dom.iterable", "esnext"], allowJs: true, jsx: "react", strictPropertyInitialization: false, }, }) const bundle = mapDict(files, ([path, source], i) => { const resolved = resolveClientDenoEnvVars(source) if (Deno.env.get("DEBUG")) { Deno.writeTextFileSync(`debug/output${i}.js`, source) Deno.writeTextFileSync(`debug/resolved${i}.js`, resolved) } return [path, resolved] }) return bundle } const bundle = await createClientBundle() function getBundleFile(path: string) { return bundle[`deno:///${path}`] } function getAssetFile(file: string): Uint8Array | null { try { const asset = Deno.readFileSync(`assets/${file}`) return asset } catch { return null } } const html = ` Tomecraft
` const staticRouter = new Router() // handle static routes by proxying to web resources // https://deno.com/deploy/docs/serve-static-assets staticRouter .get('/', (ctx) => { ctx.response.body = html }) .get('/styles.css', (ctx) => { const styles = Deno.readTextFileSync('client/styles.css') const contentTypeValue = contentType('styles.css') const headers = new Headers(ctx.response.headers) if (contentTypeValue) { headers.set('Content-Type', contentTypeValue) } ctx.response.headers = headers ctx.response.body = styles }) .get('/assets/:pathname', async (ctx, next) => { const assetPath = ctx.params.pathname if (!assetPath) { return await next() } const file = getAssetFile(assetPath) if (!file) { return await next() } const filePathParts = assetPath.split('/') const contentTypeValue = contentType(filePathParts[filePathParts.length - 1]) const headers = new Headers(ctx.response.headers) if (contentTypeValue) { headers.set('Content-Type', contentTypeValue) } ctx.response.headers = headers ctx.response.body = file }) .get('/:pathname', async (ctx, next) => { const pathname = ctx.params.pathname if (!pathname) { return await next() } const file = getBundleFile(pathname) if (!file) { return await next() } // get just the last bit so we can determine the correct filetype // contentType from media-types@v2.10.0 checks path.includes('/') const filePathParts = pathname.split('/') const contentTypeValue = contentType(filePathParts[filePathParts.length - 1]) const headers = new Headers(ctx.response.headers) if (contentTypeValue) { headers.set('Content-Type', contentTypeValue) } ctx.response.headers = headers if (pathname.endsWith('.js')) { ctx.response.type = 'application/javascript' } ctx.response.body = file }) export default staticRouter