Skip to content

Commit

Permalink
refactor: move playground logic inside pinia store
Browse files Browse the repository at this point in the history
Co-authored-by: Eduardo San Martin Morote <[email protected]>
  • Loading branch information
antfu and posva committed Dec 5, 2023
1 parent 11f5b9a commit c54653d
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 167 deletions.
7 changes: 0 additions & 7 deletions components/PanelPreviewClient.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ watch(
syncColorMode,
{ flush: 'sync' },
)
onMounted(async () => {
await mountPlayground(
play,
colorMode.value,
)
})
</script>

<template>
Expand Down
2 changes: 1 addition & 1 deletion components/PanelTerminal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const play = usePlaygroundStore()
v-if="play.status !== 'init' && play.status !== 'mount'"
hover="bg-active" rounded p1
title="Restart terminal"
@click="play.actions.restartServer()"
@click="play.restartServer()"
>
<div i-ph-arrow-clockwise-duotone />
</button>
Expand Down
2 changes: 1 addition & 1 deletion components/TheNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const play = usePlaygroundStore()
rounded p2
hover="bg-active"
title="Download as ZIP"
@click="play.actions.downloadZip()"
@click="play.downloadZip()"
>
<div i-ph-download-duotone text-2xl />
</button>
Expand Down
147 changes: 0 additions & 147 deletions composables/web-container.ts

This file was deleted.

149 changes: 138 additions & 11 deletions stores/playground.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Raw } from 'vue'
import type { WebContainer } from '@webcontainer/api'
import type { WebContainer, WebContainerProcess } from '@webcontainer/api'
import type { VirtualFile } from '../structures/VirtualFile'
import { templates } from '~/templates'

export const PlaygroundStatusOrder = [
'init',
Expand All @@ -12,11 +13,6 @@ export const PlaygroundStatusOrder = [

export type PlaygroundStatus = typeof PlaygroundStatusOrder[number] | 'error'

export interface PlaygroundActions {
restartServer(): Promise<void>
downloadZip(): Promise<void>
}

export const usePlaygroundStore = defineStore('playground', () => {
const status = ref<PlaygroundStatus>('init')
const error = shallowRef<{ message: string }>()
Expand All @@ -30,10 +26,140 @@ export const usePlaygroundStore = defineStore('playground', () => {
})
const previewUrl = computed(() => previewLocation.value.origin + previewLocation.value.fullPath)

// Actions that will be replaced later on
const actions: PlaygroundActions = {
async restartServer() {},
async downloadZip() {},
const colorMode = useColorMode()

let processInstall: WebContainerProcess | undefined
let processDev: WebContainerProcess | undefined

// Mount the playground
if (import.meta.client) {
;(async () => {
const { files: _files, tree } = await templates.basic({
nuxtrc: [
// Have color mode on initial load
colorMode.value === 'dark'
? 'app.head.htmlAttrs.class=dark'
: '',
],
})

const wc = await import('@webcontainer/api')
.then(({ WebContainer }) => WebContainer.boot())

webcontainer.value = wc
files.value = _files

_files.forEach((file) => {
file.wc = wc
})

wc.on('server-ready', async (port, url) => {
// Nuxt listen to multiple ports, and 'server-ready' is emitted for each of them
// We need the main one
if (port === 3000) {
previewLocation.value = {
origin: url,
fullPath: '/',
}
}
})

wc.on('error', (err) => {
status.value = 'error'
error.value = err
})

status.value = 'mount'
await wc.mount(tree)

startServer()

// In dev, when doing HMR, we kill the previous process while reusing the same WebContainer
if (import.meta.hot) {
import.meta.hot.accept(() => {
killPreviousProcess()
})
}
})()
}

function killPreviousProcess() {
processInstall?.kill()
processDev?.kill()
}

async function startServer() {
if (!import.meta.client)
return

const wc = webcontainer.value!

killPreviousProcess()

status.value = 'install'

processInstall = await wc.spawn('pnpm', ['install'])
stream.value = processInstall.output
const installExitCode = await processInstall.exit

if (installExitCode !== 0) {
status.value = 'error'
error.value = {
message: `Unable to run npm install, exit as ${installExitCode}`,
}
throw new Error('Unable to run npm install')
}

status.value = 'start'
processDev = await wc.spawn('pnpm', ['run', 'dev', '--no-qr'])
stream.value = processDev.output
}

async function downloadZip() {
if (!import.meta.client)
return

const wc = webcontainer.value!

const { default: JSZip } = await import('jszip')
const zip = new JSZip()

type Zip = typeof zip

const crawlFiles = async (dir: string, zip: Zip) => {
const files = await wc.fs.readdir(dir, { withFileTypes: true })

await Promise.all(
files.map(async (file) => {
if (isFileIgnored(file.name))
return

if (file.isFile()) {
// TODO: If it's package.json, we modify to remove some fields
const content = await wc.fs.readFile(`${dir}/${file.name}`, 'utf8')
zip.file(file.name, content)
}
else if (file.isDirectory()) {
const folder = zip.folder(file.name)!
return crawlFiles(`${dir}/${file.name}`, folder)
}
}),
)
}

await crawlFiles('.', zip)

const blob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(blob)
const date = new Date()
const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
const link = document.createElement('a')
link.href = url
// TODO: have a better name with the current tutorial name
link.download = `nuxt-playground-${dateString}.zip`
link.click()
link.remove()
URL.revokeObjectURL(url)
}

return {
Expand All @@ -44,7 +170,8 @@ export const usePlaygroundStore = defineStore('playground', () => {
webcontainer,
previewUrl,
previewLocation,
actions,
restartServer: startServer,
downloadZip,
}
})

Expand Down

0 comments on commit c54653d

Please sign in to comment.