Added code structure for posts including scripts

post/wasm-gol-2
Ashelyn Dawn 1 year ago
parent 0ba51e86b8
commit c42e58a54d

@ -0,0 +1,39 @@
'use client'
import { useEffect, useRef } from "react"
interface Module {
setup?: (wabt?: any) => Promise<undefined>
cleanup?: () => Promise<undefined>
}
export default function PageScript({ script, wasm }: { script?: string, wasm?: Uint8Array }) {
const moduleRef = useRef<Module | null>(null)
useEffect(() => {
if (!script) return undefined
import(/* webpackIgnore: true */ script)
.then(scriptModule => {
if (!wasm) return [scriptModule]
return WebAssembly.instantiate(Uint8Array.from(wasm)).then(({ instance }) => [scriptModule, instance])
})
.then(([scriptModule, wasmModule]) => {
moduleRef.current = scriptModule
if (scriptModule.setup && typeof scriptModule.setup === 'function')
scriptModule.setup(wasmModule)
})
return () => {
const mod = moduleRef.current
if (mod?.cleanup && typeof mod.cleanup === 'function') {
mod.cleanup()
}
}
}, [])
return null
}

@ -1,13 +1,14 @@
import { Metadata } from "next" import { Metadata } from "next"
import { Code } from "bright" import { Code } from "bright"
import { getPostSlugs, loadSinglePage } from "~/utils/post" import { getPostSlugs, loadPageMetada, loadSinglePage } from "~/utils/post"
import MarkdownToJSX from 'markdown-to-jsx' import MarkdownToJSX from 'markdown-to-jsx'
import styles from "~/styles/post.module.css" import styles from "~/styles/post.module.css"
import { ReactElement } from "react" import { ReactElement } from "react"
import PageScript from "./PageScript"
export async function generateMetadata({ params: { slug } }: { params: { slug: string } }): Promise<Metadata> { export async function generateMetadata({ params: { slug } }: { params: { slug: string } }): Promise<Metadata> {
const post = await loadSinglePage(slug) const post = await loadPageMetada(slug)
return { return {
title: post?.title title: post?.title
@ -33,6 +34,7 @@ export default async function Post({ params: { slug } }: { params: { slug: strin
<p className={styles.subtitle}>{post.subtitle}</p> <p className={styles.subtitle}>{post.subtitle}</p>
<Markdown>{post.body}</Markdown> <Markdown>{post.body}</Markdown>
{post.script && <PageScript script={post.script} wasm={post.wasm} />}
</article> </article>
</> </>
); );

@ -1,6 +1,6 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
import { Post, getPostSlugs, loadSinglePage } from "~/utils/post" import { PostMeta, getPostSlugs, loadPageMetada } from "~/utils/post"
import styles from "~/styles/index.module.css" import styles from "~/styles/index.module.css"
@ -10,18 +10,18 @@ export const metadata: Metadata = {
export default async function Index() { export default async function Index() {
const slugs = await getPostSlugs() const slugs = await getPostSlugs()
const posts = await Promise.all(slugs.map(loadSinglePage)) const posts = await Promise.all(slugs.map(loadPageMetada))
const sortedPosts = const sortedPosts =
posts posts
.filter((post) => post !== null) .filter((post) => post !== null)
.filter((post) => !post?.unlisted) .filter((post) => !post?.unlisted)
.sort((a: Post, b: Post) => b.date.valueOf() - a.date.valueOf()) .sort((a: PostMeta, b: PostMeta) => b.date.valueOf() - a.date.valueOf())
return ( return (
<> <>
<h1>All posts</h1> <h1>All posts</h1>
{sortedPosts.map((post: Post, i: number) => ( {sortedPosts.map((post: PostMeta, i: number) => (
<> <>
{i >= 1 && sortedPosts[i - 1]?.date.getFullYear() != post.date.getFullYear() && ( {i >= 1 && sortedPosts[i - 1]?.date.getFullYear() != post.date.getFullYear() && (
<p className={styles.yearSeparator}>{post.date.getFullYear()}</p> <p className={styles.yearSeparator}>{post.date.getFullYear()}</p>

19
package-lock.json generated

@ -14,7 +14,8 @@
"markdown-to-jsx": "^7.2.0", "markdown-to-jsx": "^7.2.0",
"next": "^13.4.1", "next": "^13.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"wabt": "1.0.19"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.1.0", "@types/node": "20.1.0",
@ -521,6 +522,22 @@
"node": ">=12.20" "node": ">=12.20"
} }
}, },
"node_modules/wabt": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.19.tgz",
"integrity": "sha512-z/7XRZB8tPRW0XdE8HYbA95w2kjus5AwOHnJ5NT9PqzaNZ7z/zHnUdpNdB78TFMgWt+D/u01lg1jG6nWiFmyEw==",
"bin": {
"wasm-decompile": "bin/wasm-decompile",
"wasm-interp": "bin/wasm-interp",
"wasm-objdump": "bin/wasm-objdump",
"wasm-opcodecnt": "bin/wasm-opcodecnt",
"wasm-strip": "bin/wasm-strip",
"wasm-validate": "bin/wasm-validate",
"wasm2c": "bin/wasm2c",
"wasm2wat": "bin/wasm2wat",
"wat2wasm": "bin/wat2wasm"
}
},
"node_modules/zod": { "node_modules/zod": {
"version": "3.21.4", "version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",

@ -14,7 +14,8 @@
"markdown-to-jsx": "^7.2.0", "markdown-to-jsx": "^7.2.0",
"next": "^13.4.1", "next": "^13.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"wabt": "1.0.19"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.1.0", "@types/node": "20.1.0",

@ -1,17 +1,26 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
import wabtModule from 'wabt'
import frontmatter from 'front-matter' import frontmatter from 'front-matter'
export interface Post { export interface PostMeta {
slug: string, slug: string,
date: Date, date: Date,
title: string, title: string,
subtitle?: string, subtitle?: string,
body: string,
unlisted: boolean, unlisted: boolean,
filePath: string,
}
export interface Post extends Omit<PostMeta, "filePath"> {
body: string,
script?: string,
wasm?: Uint8Array,
} }
type Attributes = { [key: string]: any }
const POST_FILE_PATTERN = /^(?<date>[0-9]{4}-[0-9]{2}-[0-9]{2}(-[0-9]{1,2})?)_(?<slug>[^\.]+)\.md$/ const POST_FILE_PATTERN = /^(?<date>[0-9]{4}-[0-9]{2}-[0-9]{2}(-[0-9]{1,2})?)_(?<slug>[^\.]+)\.md$/
export async function getPostSlugs(): Promise<string[]> { export async function getPostSlugs(): Promise<string[]> {
@ -26,7 +35,7 @@ export function getSlugFromFilePath(postPath: string): string | null {
return regexResult.groups.slug return regexResult.groups.slug
} }
export async function loadSinglePage(slug: string): Promise<Post | null> { export async function loadPageMetada(slug: string): Promise<PostMeta | null> {
const postsDir = path.join(process.cwd(), 'posts') const postsDir = path.join(process.cwd(), 'posts')
const postPaths = await fs.readdir(postsDir) const postPaths = await fs.readdir(postsDir)
const postMatch: RegExpExecArray | null | undefined = postPaths const postMatch: RegExpExecArray | null | undefined = postPaths
@ -37,10 +46,9 @@ export async function loadSinglePage(slug: string): Promise<Post | null> {
if (!postMatch) return null; if (!postMatch) return null;
const fileName = `${postMatch.groups?.date}_${postMatch.groups?.slug}.md` const fileName = `${postMatch.groups?.date}_${postMatch.groups?.slug}.md`
const fileContents = (await fs.readFile(path.join(process.cwd(), 'posts', fileName))).toString('utf8') const filePath = path.join(process.cwd(), 'posts', fileName)
const { attributes, body } = frontmatter(fileContents) const fileContents = (await fs.readFile(filePath)).toString('utf8')
const { attributes } = frontmatter(fileContents)
type Attributes = { [key: string]: any }
const data: Attributes = attributes as Attributes const data: Attributes = attributes as Attributes
if (!data.title) return null; if (!data.title) return null;
@ -49,11 +57,53 @@ export async function loadSinglePage(slug: string): Promise<Post | null> {
const date = new Date(Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day))) const date = new Date(Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day)))
return { return {
slug: postMatch.groups?.slug || '',
date, date,
filePath,
slug: postMatch.groups?.slug || '',
title: data.title, title: data.title,
subtitle: data.subtitle, subtitle: data.subtitle,
unlisted: typeof data.unlisted === 'boolean' ? data.unlisted : false, unlisted: typeof data.unlisted === 'boolean' ? data.unlisted : false,
}
}
export async function loadSinglePage(slug: string): Promise<Post | null> {
const postMeta = await loadPageMetada(slug)
if (!postMeta) return null
const fileContents = (await fs.readFile(postMeta.filePath)).toString('utf8')
const { attributes, body } = frontmatter(fileContents)
const data: Attributes = attributes as Attributes
if (data.script) {
const scriptPath = path.join(process.cwd(), 'public/scripts', data.script)
try {
await fs.readFile(scriptPath)
} catch {
throw new Error(`Could not find script ${scriptPath} referenced in post ${slug}`)
} finally {
data.script = `/scripts/${data.script}`
}
}
if (data.wasm) {
const wabt = await wabtModule()
const wasmPath = path.join(process.cwd(), 'public/scripts', data.wasm)
const fileName = data.wasm.replace(/^.*\//, '')
const wasmText = (await fs.readFile(wasmPath))
const wasmIntermed = wabt.parseWat(fileName, wasmText)
const { buffer: wasmBinary } = wasmIntermed.toBinary({ write_debug_names: true })
data.wasm = wasmBinary
}
const { filePath, ...otherMeta } = postMeta
return {
...otherMeta,
script: data.script,
wasm: data.wasm,
body body
} }
} }

Loading…
Cancel
Save