Mounting Remote Snapshots
While inlining the contents of the files you want to mount in a WebContainer is simple enough for learning and experimentation, you will quickly run into a situation where you need more flexibility.
Besides, in a real project scenario it always makes sense to separate your data (the files to mount in this case) from the logic (the Vue component containing the Karagäz Sandbox).
That is when working with remote snapshots comes in handy.
Let's explore 2 situations. Both of which involve creating API endpoints in a Nuxt site (similar to this documentation ^^). So we would have a file structure as follows.
site-root
|_ server
| |_ api
| |_ snapshot
| |_ [snapshot].get.ts <- Server endpoint
|_ assets
|_ snapshots
|_ express <- Snapshot "express"
| |_ public
| | |_ index.html
| | |_ script.js
| | |_ style.css
| |_ package.json
| |_ server.js
|_ express-solve <- Snapshot "express-solve"
|_ public
|_ about.html
|_ index.html
So, when we hit the server endpoint at GET /api/snapshot/express
we should get the contents of the the directory server/assets/snapshots/express
ready to be mounted to the WebContainer.
Similarly, hitting GET /api/snapshot/express-solve
would return the contents of server/assets/snapshots/express-solve
.
1. Access to the File System is Possible
If you're running your site on a Node Server where you can upload files and always reach them, then you're in luck. There is a convenience utility –released by Stackblitz– exactly for that.
You just need to install
@webcontainer/snapshot as a
dependency then adjust [snapshot].get.ts
as follows.
import { snapshot } from '@webcontainer/snapshot'
import { defineEventHandler } from 'h3'
// Restrict snapshots to a fixed list as a simple security measure.
const snapshots = ['express', 'express-solve']
export default defineEventHandler(async (event) => {
// The snaphost name passed in the [snapshot] parameter of the endpoint path.
const snapshotName = getRouterParam(event, 'snapshot')
// 404 - Someone is trying to access a snapshot that is not in the list.
if (snapshotName && !snapshots.includes(snapshotName)) {
throw createError({
statusCode: 404,
statusMessage: 'Snapshot not found',
})
}
try {
// Where the magic happens. Simply pass the directory path to the snapshot() function.
const sourceSnapshot = await snapshot(`./server/assets/snapshots/${snapshotName}`)
// Set the correct content-type.
event.headers.set('content-type', 'application/octet-stream')
// Return the resulting snaphost.
return sourceSnapshot
} catch (error) {
// Something went wrong.
throw createError({
statusCode: 500,
statusMessage: 'Unable to get snapshot',
})
}
})
Then, in your sandbox component, you can retrieve the snapshots using useFetch()
as demonstrated below.
<script setup lang="ts">
import {
KrgzSandbox,
provideWebContainer,
useSandbox,
useSandboxBoot,
} from '@karagoz/sandbox'
import { onBeforeUnmount, onMounted } from 'vue'
const { boot, isBooting } = useSandboxBoot()
provideWebContainer(boot)
const sandbox = useSandbox()
const { data: initialSnapshot } = await useFetch<Response>(
'/api/snapshot/express',
{
// These options are important when working with snapshots.
headers: { Accept: 'application/octet-stream' },
responseType: 'blob',
},
)
const { data: solveSnapshot, execute: fetchSolveSnapshot } =
await useFetch<Response>('/api/snapshot/express-solve', {
immediate: false,
// These options are important when working with snapshots.
headers: { Accept: 'application/octet-stream' },
responseType: 'blob',
})
onMounted(async () => {
if (!initialSnapshot.value) return
// Ensure injected promise has been resolved
const container = await boot
// Continue initialisation
await container.mount(await initialSnapshot.value.arrayBuffer()) // Remember to call arrayBuffer()
await sandbox.bootstrap()
sandbox.editorTabs.open('./public/index.html')
})
onBeforeUnmount(() => sandbox.container.value?.teardown())
const onSolveClick = async () => {
await fetchSolveSnapshot()
if (!solveSnapshot.value) return
sandbox.container.value?.mount(await solveSnapshot.value.arrayBuffer()) // Remember to call arrayBuffer()
}
</script>
<template>
<KrgzSandbox
:booting="isBooting"
:shown-panels="['code', 'processes', 'result', 'terminal']"
@solve="onSolveClick()"
></KrgzSandbox>
</template>
2. Vercel (No File System Access)
When you deploy a server rendered Nuxt site to a provider like Vercel, then you are in a server function context and won't constant access to the file system. This means that the solution mentioned earlier won't work for you.
But have no fear, there is a solution, it just required a bit more code.
Your server/assets
directory will be picked up by Nuxt's storage layer and can be accessed through useStorage('assets:server')
. Using that, we just need to traverse the relevant storage keys and create a FileSystemTree
object and return it as JSON (not an ArrayBuffer in this case).
This is the way it is actually done on this site, so you can check the code directly on GitHub: