|
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 | + ref="sandbox" |
| 27 | + :show="props.show" |
| 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