Skip to content

Instantly share code, notes, and snippets.

@MrFoxPro
Last active July 11, 2023 16:32
Show Gist options
  • Select an option

  • Save MrFoxPro/50ef956ca8a95e55deff6b2fd0e1f459 to your computer and use it in GitHub Desktop.

Select an option

Save MrFoxPro/50ef956ca8a95e55deff6b2fd0e1f459 to your computer and use it in GitHub Desktop.
Prerendering
import { renderToStringAsync } from 'solid-js/web'
export function IndexPage() {
return <div>Here will be a landing. But now loading App.</div>
}
export async function render() {
const result = await renderToStringAsync(IndexPage)
return {
url: '/',
template: './index.html',
head: [],
body: [result],
include: [() => import('./app-loader')],
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, viewport-fit=cover" />
<!--head-->
</head>
<body class="@dark:(bg-#17181A color-white)">
<script type="module" src="./app.tsx"></script>
<!--body-->
</body>
</html>
import { resolve } from 'std/path/mod.ts'
import { walkSync } from 'std/fs/mod.ts'
import { build, createServer, type Rollup } from 'npm:vite'
import express from 'https://esm.sh/[email protected]'
export interface IRenderOutput {
url: string
template: string
head: string[]
body: string[]
include: (() => Promise<unknown>)[]
}
const command = Deno.args[0]
if (command && !['build', 'dev', ''].includes(command)) {
console.error('Wrong command')
Deno.exit(1)
}
const root = new URL('.', import.meta.url).pathname
const walkResult = walkSync(root, {
match: [/.+entry\.(tsx?|mts)/],
skip: [/node_modules/, /\.git/, /dist/],
})
const entryFiles = Array.from(walkResult)
if (!command || command == 'dev') {
const app = express()
const vite = await createServer({
root,
appType: 'custom',
logLevel: 'info',
server: {
middlewareMode: true,
watch: {
usePolling: true,
interval: 100,
},
},
})
const tasks = entryFiles.map(e => vite.ssrLoadModule(e.path).then(mod => mod.render() as IRenderOutput))
const modules = await Promise.all(tasks)
// @ts-ignore
app.use(vite.middlewares)
app.use('*', async (req, res, next) => {
const url = req.path
const out = modules.find(x => x.url == url)
if (!out) return next()
try {
const template = await renderTemplate(out)
const html = await vite.transformIndexHtml(url, template)
res.status(200).end(html)
} catch (e) {
vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
app.listen(3000, () => {
console.log(`Dev server is running on https://localhost:3000`)
})
} else if (command == 'build') {
const clientOut = resolve(root, 'dist')
const ssrOut = resolve(root, 'dist/_ssr')
const entries = entryFiles.map(e => resolve(root, e.path))
await build({
root,
build: {
rollupOptions: {
input: ['index.html'].concat(entries),
},
outDir: clientOut,
},
})
const ssr = (await build({
build: {
ssr: true,
outDir: ssrOut,
rollupOptions: { input: entries },
},
})) as Rollup.RollupOutput
const builtEntries = ssr.output.filter(chunk =>
// @ts-ignore
entryFiles.some(file => file.path == (chunk.facadeModuleId as string))
)
for (const entry of builtEntries) {
const mod = await import(resolve(ssrOut, entry.fileName))
const out = (await mod.render()) as IRenderOutput
const html = await renderTemplate(out, resolve(clientOut, out.template))
const entryHTMLPathname = out.url == '/' ? 'index.html' : out.url + '.html'
await Deno.writeTextFile(resolve(clientOut, entryHTMLPathname), html)
console.log(`🎉 Page ${entryHTMLPathname} generated!`)
}
await Deno.remove(ssrOut, { recursive: true })
}
function getUrlFromImport<T>(modFunc: () => Promise<T>) {
const regex = /import(__)?\(['|"](?<path>.+)['|"]\)/
const result = modFunc.toString().match(regex).groups.path
if (!result) throw new Error(`Can\'t parse path from ${modFunc.toString()}`)
return result
}
/*
We can't drop reading HTML file, because Vite can handle injection of CSS and other critical things only by providing file
Do it otherwise, will be too complicated
*/
export async function renderTemplate(
{ template, body, head, include }: IRenderOutput,
overridePath?: string
) {
const html = await Deno.readFile(overridePath ?? template).then(r => new TextDecoder('utf-8').decode(r))
const headString = head.join('\n')
const bodyString =
body.join('\n') +
include
.map(i => getUrlFromImport(i))
.map(e => `<script type="module" src="${e}"></script>`)
.join('\n')
return html.replace('<!--head-->', headString).replace('<!--body-->', bodyString)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment