Skip to content

Commit d6ebb66

Browse files
committed
feat: export Sandbox
1 parent 47da477 commit d6ebb66

File tree

3 files changed

+351
-295
lines changed

3 files changed

+351
-295
lines changed

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { default as Repl } from './Repl.vue'
22
export { default as Preview } from './output/Preview.vue'
3+
export { default as Sandbox } from './output/Sandbox.vue'
4+
export type { SandboxProps } from './output/Sandbox.vue'
35
export {
46
useStore,
57
File,

src/output/Preview.vue

+17-295
Original file line numberDiff line numberDiff line change
@@ -1,312 +1,34 @@
11
<script setup lang="ts">
2-
import Message from '../Message.vue'
3-
import {
4-
type WatchStopHandle,
5-
inject,
6-
onMounted,
7-
onUnmounted,
8-
ref,
9-
useTemplateRef,
10-
watch,
11-
watchEffect,
12-
} from 'vue'
13-
import srcdoc from './srcdoc.html?raw'
14-
import { PreviewProxy } from './PreviewProxy'
15-
import { compileModulesForPreview } from './moduleCompiler'
2+
import { computed, inject, useTemplateRef } from 'vue'
163
import { injectKeyProps } from '../../src/types'
4+
import Sandbox from './Sandbox.vue'
175
186
const props = defineProps<{ show: boolean; ssr: boolean }>()
197
208
const { store, clearConsole, theme, previewTheme, previewOptions } =
219
inject(injectKeyProps)!
2210
23-
const containerRef = useTemplateRef('container')
24-
const runtimeError = ref<string>()
25-
const runtimeWarning = ref<string>()
26-
27-
let sandbox: HTMLIFrameElement
28-
let proxy: PreviewProxy
29-
let stopUpdateWatcher: WatchStopHandle | undefined
30-
31-
// create sandbox on mount
32-
onMounted(createSandbox)
33-
34-
// reset sandbox when import map changes
35-
watch(
36-
() => store.value.getImportMap(),
37-
() => {
38-
try {
39-
createSandbox()
40-
} catch (e: any) {
41-
store.value.errors = [e as Error]
42-
return
43-
}
44-
},
11+
const sandboxTheme = computed(() =>
12+
previewTheme.value ? theme.value : undefined,
4513
)
4614
47-
function switchPreviewTheme() {
48-
if (!previewTheme.value) return
15+
const sandboxRef = useTemplateRef('sandbox')
16+
const container = computed(() => sandboxRef.value?.container)
4917
50-
const html = sandbox.contentDocument?.documentElement
51-
if (html) {
52-
html.className = theme.value
53-
} else {
54-
// re-create sandbox
55-
createSandbox()
56-
}
57-
}
58-
59-
// reset theme
60-
watch([theme, previewTheme], switchPreviewTheme)
61-
62-
onUnmounted(() => {
63-
proxy.destroy()
64-
stopUpdateWatcher && stopUpdateWatcher()
18+
defineExpose({
19+
reload: () => sandboxRef.value?.reload(),
20+
container,
6521
})
66-
67-
function createSandbox() {
68-
if (sandbox) {
69-
// clear prev sandbox
70-
proxy.destroy()
71-
stopUpdateWatcher && stopUpdateWatcher()
72-
containerRef.value?.removeChild(sandbox)
73-
}
74-
75-
sandbox = document.createElement('iframe')
76-
sandbox.setAttribute(
77-
'sandbox',
78-
[
79-
'allow-forms',
80-
'allow-modals',
81-
'allow-pointer-lock',
82-
'allow-popups',
83-
'allow-same-origin',
84-
'allow-scripts',
85-
'allow-top-navigation-by-user-activation',
86-
].join(' '),
87-
)
88-
89-
const importMap = store.value.getImportMap()
90-
const sandboxSrc = srcdoc
91-
.replace(
92-
/<html>/,
93-
`<html class="${previewTheme.value ? theme.value : ''}">`,
94-
)
95-
.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap))
96-
.replace(
97-
/<!-- PREVIEW-OPTIONS-HEAD-HTML -->/,
98-
previewOptions.value?.headHTML || '',
99-
)
100-
.replace(
101-
/<!--PREVIEW-OPTIONS-PLACEHOLDER-HTML-->/,
102-
previewOptions.value?.placeholderHTML || '',
103-
)
104-
sandbox.srcdoc = sandboxSrc
105-
containerRef.value?.appendChild(sandbox)
106-
107-
proxy = new PreviewProxy(sandbox, {
108-
on_fetch_progress: (progress: any) => {
109-
// pending_imports = progress;
110-
},
111-
on_error: (event: any) => {
112-
const msg =
113-
event.value instanceof Error ? event.value.message : event.value
114-
if (
115-
msg.includes('Failed to resolve module specifier') ||
116-
msg.includes('Error resolving module specifier')
117-
) {
118-
runtimeError.value =
119-
msg.replace(/\. Relative references must.*$/, '') +
120-
`.\nTip: edit the "Import Map" tab to specify import paths for dependencies.`
121-
} else {
122-
runtimeError.value = event.value
123-
}
124-
},
125-
on_unhandled_rejection: (event: any) => {
126-
let error = event.value
127-
if (typeof error === 'string') {
128-
error = { message: error }
129-
}
130-
runtimeError.value = 'Uncaught (in promise): ' + error.message
131-
},
132-
on_console: (log: any) => {
133-
if (log.duplicate) {
134-
return
135-
}
136-
if (log.level === 'error') {
137-
if (log.args[0] instanceof Error) {
138-
runtimeError.value = log.args[0].message
139-
} else {
140-
runtimeError.value = log.args[0]
141-
}
142-
} else if (log.level === 'warn') {
143-
if (log.args[0].toString().includes('[Vue warn]')) {
144-
runtimeWarning.value = log.args
145-
.join('')
146-
.replace(/\[Vue warn\]:/, '')
147-
.trim()
148-
}
149-
}
150-
},
151-
on_console_group: (action: any) => {
152-
// group_logs(action.label, false);
153-
},
154-
on_console_group_end: () => {
155-
// ungroup_logs();
156-
},
157-
on_console_group_collapsed: (action: any) => {
158-
// group_logs(action.label, true);
159-
},
160-
})
161-
162-
sandbox.addEventListener('load', () => {
163-
proxy.handle_links()
164-
stopUpdateWatcher = watchEffect(updatePreview)
165-
switchPreviewTheme()
166-
})
167-
}
168-
169-
async function updatePreview() {
170-
if (import.meta.env.PROD && clearConsole.value) {
171-
console.clear()
172-
}
173-
runtimeError.value = undefined
174-
runtimeWarning.value = undefined
175-
176-
let isSSR = props.ssr
177-
if (store.value.vueVersion) {
178-
const [major, minor, patch] = store.value.vueVersion
179-
.split('.')
180-
.map((v) => parseInt(v, 10))
181-
if (major === 3 && (minor < 2 || (minor === 2 && patch < 27))) {
182-
alert(
183-
`The selected version of Vue (${store.value.vueVersion}) does not support in-browser SSR.` +
184-
` Rendering in client mode instead.`,
185-
)
186-
isSSR = false
187-
}
188-
}
189-
190-
try {
191-
const { mainFile } = store.value
192-
193-
// if SSR, generate the SSR bundle and eval it to render the HTML
194-
if (isSSR && mainFile.endsWith('.vue')) {
195-
const ssrModules = compileModulesForPreview(store.value, true)
196-
console.info(
197-
`[@vue/repl] successfully compiled ${ssrModules.length} modules for SSR.`,
198-
)
199-
await proxy.eval([
200-
`const __modules__ = {};`,
201-
...ssrModules,
202-
`import { renderToString as _renderToString } from 'vue/server-renderer'
203-
import { createSSRApp as _createApp } from 'vue'
204-
const AppComponent = __modules__["${mainFile}"].default
205-
AppComponent.name = 'Repl'
206-
const app = _createApp(AppComponent)
207-
if (!app.config.hasOwnProperty('unwrapInjectedRef')) {
208-
app.config.unwrapInjectedRef = true
209-
}
210-
app.config.warnHandler = () => {}
211-
window.__ssr_promise__ = _renderToString(app).then(html => {
212-
document.body.innerHTML = '<div id="app">' + html + '</div>' + \`${
213-
previewOptions.value?.bodyHTML || ''
214-
}\`
215-
}).catch(err => {
216-
console.error("SSR Error", err)
217-
})
218-
`,
219-
])
220-
}
221-
222-
// compile code to simulated module system
223-
const modules = compileModulesForPreview(store.value)
224-
console.info(
225-
`[@vue/repl] successfully compiled ${modules.length} module${
226-
modules.length > 1 ? `s` : ``
227-
}.`,
228-
)
229-
230-
const codeToEval = [
231-
`window.__modules__ = {};window.__css__ = [];` +
232-
`if (window.__app__) window.__app__.unmount();` +
233-
(isSSR
234-
? ``
235-
: `document.body.innerHTML = '<div id="app"></div>' + \`${
236-
previewOptions.value?.bodyHTML || ''
237-
}\``),
238-
...modules,
239-
`document.querySelectorAll('style[css]').forEach(el => el.remove())
240-
document.head.insertAdjacentHTML('beforeend', window.__css__.map(s => \`<style css>\${s}</style>\`).join('\\n'))`,
241-
]
242-
243-
// if main file is a vue file, mount it.
244-
if (mainFile.endsWith('.vue')) {
245-
codeToEval.push(
246-
`import { ${
247-
isSSR ? `createSSRApp` : `createApp`
248-
} as _createApp } from "vue"
249-
${previewOptions.value?.customCode?.importCode || ''}
250-
const _mount = () => {
251-
const AppComponent = __modules__["${mainFile}"].default
252-
AppComponent.name = 'Repl'
253-
const app = window.__app__ = _createApp(AppComponent)
254-
if (!app.config.hasOwnProperty('unwrapInjectedRef')) {
255-
app.config.unwrapInjectedRef = true
256-
}
257-
app.config.errorHandler = e => console.error(e)
258-
${previewOptions.value?.customCode?.useCode || ''}
259-
app.mount('#app')
260-
}
261-
if (window.__ssr_promise__) {
262-
window.__ssr_promise__.then(_mount)
263-
} else {
264-
_mount()
265-
}`,
266-
)
267-
}
268-
269-
// eval code in sandbox
270-
await proxy.eval(codeToEval)
271-
} catch (e: any) {
272-
console.error(e)
273-
runtimeError.value = (e as Error).message
274-
}
275-
}
276-
277-
/**
278-
* Reload the preview iframe
279-
*/
280-
function reload() {
281-
sandbox.contentWindow?.location.reload()
282-
}
283-
284-
defineExpose({ reload, container: containerRef })
28522
</script>
28623

28724
<template>
288-
<div
289-
v-show="show"
290-
ref="container"
291-
class="iframe-container"
292-
:class="{ [theme]: previewTheme }"
293-
/>
294-
<Message :err="(previewOptions?.showRuntimeError ?? true) && runtimeError" />
295-
<Message
296-
v-if="!runtimeError && (previewOptions?.showRuntimeWarning ?? true)"
297-
:warn="runtimeWarning"
25+
<Sandbox
26+
v-show="props.show"
27+
ref="sandbox"
28+
:store="store"
29+
:theme="sandboxTheme"
30+
:preview-options="previewOptions"
31+
:ssr="props.ssr"
32+
:clear-console="clearConsole"
29833
/>
29934
</template>
300-
301-
<style scoped>
302-
.iframe-container,
303-
.iframe-container :deep(iframe) {
304-
width: 100%;
305-
height: 100%;
306-
border: none;
307-
background-color: #fff;
308-
}
309-
.iframe-container.dark :deep(iframe) {
310-
background-color: #1e1e1e;
311-
}
312-
</style>

0 commit comments

Comments
 (0)