|
1 | 1 | <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' |
16 | 3 | import { injectKeyProps } from '../../src/types'
|
| 4 | +import Sandbox from './Sandbox.vue' |
17 | 5 |
|
18 | 6 | const props = defineProps<{ show: boolean; ssr: boolean }>()
|
19 | 7 |
|
20 | 8 | const { store, clearConsole, theme, previewTheme, previewOptions } =
|
21 | 9 | inject(injectKeyProps)!
|
22 | 10 |
|
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, |
45 | 13 | )
|
46 | 14 |
|
47 |
| -function switchPreviewTheme() { |
48 |
| - if (!previewTheme.value) return |
| 15 | +const sandboxRef = useTemplateRef('sandbox') |
| 16 | +const container = computed(() => sandboxRef.value?.container) |
49 | 17 |
|
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, |
65 | 21 | })
|
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 }) |
285 | 22 | </script>
|
286 | 23 |
|
287 | 24 | <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" |
298 | 33 | />
|
299 | 34 | </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