workflow(sfc-playground): support import map
This commit is contained in:
		
							parent
							
								
									e097bd4dd5
								
							
						
					
					
						commit
						5ee7e6bc70
					
				| @ -24,7 +24,7 @@ body { | |||||||
|   font-size: 13px; |   font-size: 13px; | ||||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, |   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, | ||||||
|     Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; |     Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | ||||||
|   color:var(--base); |   color: var(--base); | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   background-color: #f8f8f8; |   background-color: #f8f8f8; | ||||||
|   --base: #444; |   --base: #444; | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
|           <li v-for="version of publishedVersions"> |           <li v-for="version of publishedVersions"> | ||||||
|             <a @click="setVueVersion(version)">v{{ version }}</a> |             <a @click="setVueVersion(version)">v{{ version }}</a> | ||||||
|           </li> |           </li> | ||||||
|           <li><a @click="resetVueVersion">This Commit ({{ commit }})</a></li> |           <li><a @click="resetVueVersion">This Commit ({{ currentCommit }})</a></li> | ||||||
|           <li> |           <li> | ||||||
|             <a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a> |             <a href="https://app.netlify.com/sites/vue-sfc-playground/deploys" target="_blank">Commits History</a> | ||||||
|           </li> |           </li> | ||||||
| @ -51,8 +51,8 @@ import { downloadProject } from './download/download' | |||||||
| import { setVersion, resetVersion } from './sfcCompiler' | import { setVersion, resetVersion } from './sfcCompiler' | ||||||
| import { ref, onMounted } from 'vue' | import { ref, onMounted } from 'vue' | ||||||
| 
 | 
 | ||||||
| const commit = __COMMIT__ | const currentCommit = __COMMIT__ | ||||||
| const activeVersion = ref(`@${commit}`) | const activeVersion = ref(`@${currentCommit}`) | ||||||
| const publishedVersions = ref<string[]>() | const publishedVersions = ref<string[]>() | ||||||
| const expanded = ref(false) | const expanded = ref(false) | ||||||
| 
 | 
 | ||||||
| @ -72,7 +72,7 @@ async function setVueVersion(v: string) { | |||||||
| 
 | 
 | ||||||
| function resetVueVersion() { | function resetVueVersion() { | ||||||
|   resetVersion() |   resetVersion() | ||||||
|   activeVersion.value = `@${commit}` |   activeVersion.value = `@${currentCommit}` | ||||||
|   expanded.value = false |   expanded.value = false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ const onChange = debounce((code: string) => { | |||||||
| 
 | 
 | ||||||
| const activeCode = ref(store.activeFile.code) | const activeCode = ref(store.activeFile.code) | ||||||
| const activeMode = computed( | const activeMode = computed( | ||||||
|   () => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed') |   () => (store.activeFilename.endsWith('.vue') ? 'htmlmixed' : 'javascript') | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| watch( | watch( | ||||||
|  | |||||||
| @ -45,8 +45,12 @@ function focus({ el }: VNode) { | |||||||
| function doneAddFile() { | function doneAddFile() { | ||||||
|   const filename = pendingFilename.value |   const filename = pendingFilename.value | ||||||
| 
 | 
 | ||||||
|   if (!filename.endsWith('.vue') && !filename.endsWith('.js')) { |   if ( | ||||||
|     store.errors = [`Playground only supports .vue or .js files.`] |     !filename.endsWith('.vue') && | ||||||
|  |     !filename.endsWith('.js') && | ||||||
|  |     filename !== 'import-map.json' | ||||||
|  |   ) { | ||||||
|  |     store.errors = [`Playground only supports *.vue, *.js files or import-map.json.`] | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,60 +1,105 @@ | |||||||
| <template> | <template> | ||||||
|   <iframe |   <div class="preview-container" ref="container"> | ||||||
|     id="preview" | </div> | ||||||
|     ref="iframe" |  | ||||||
|     sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation" |  | ||||||
|     :srcdoc="srcdoc" |  | ||||||
|   ></iframe> |  | ||||||
|   <Message :err="runtimeError" /> |   <Message :err="runtimeError" /> | ||||||
|   <Message v-if="!runtimeError" :warn="runtimeWarning" /> |   <Message v-if="!runtimeError" :warn="runtimeWarning" /> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import Message from '../Message.vue' | import Message from '../Message.vue' | ||||||
| import { ref, onMounted, onUnmounted, watchEffect } from 'vue' | import { ref, onMounted, onUnmounted, watchEffect, watch } from 'vue' | ||||||
| import type { WatchStopHandle } from 'vue' | import type { WatchStopHandle } from 'vue' | ||||||
| import srcdoc from './srcdoc.html?raw' | import srcdoc from './srcdoc.html?raw' | ||||||
| import { PreviewProxy } from './PreviewProxy' | import { PreviewProxy } from './PreviewProxy' | ||||||
| import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler' | import { MAIN_FILE, vueRuntimeUrl } from '../sfcCompiler' | ||||||
| import { compileModulesForPreview } from './moduleCompiler' | import { compileModulesForPreview } from './moduleCompiler' | ||||||
|  | import { store } from '../store' | ||||||
| 
 | 
 | ||||||
| const iframe = ref() | const container = ref() | ||||||
| const runtimeError = ref() | const runtimeError = ref() | ||||||
| const runtimeWarning = ref() | const runtimeWarning = ref() | ||||||
| 
 | 
 | ||||||
|  | let sandbox: HTMLIFrameElement | ||||||
| let proxy: PreviewProxy | let proxy: PreviewProxy | ||||||
| let updateHandle: WatchStopHandle | let stopUpdateWatcher: WatchStopHandle | ||||||
| 
 | 
 | ||||||
| async function updatePreview() { | // create sandbox on mount | ||||||
|   runtimeError.value = null | onMounted(createSandbox) | ||||||
|   runtimeWarning.value = null |  | ||||||
|   try { |  | ||||||
|     const modules = compileModulesForPreview() |  | ||||||
|     console.log(`successfully compiled ${modules.length} modules.`) |  | ||||||
|     // reset modules |  | ||||||
|     await proxy.eval([ |  | ||||||
|       `window.__modules__ = {};window.__css__ = ''`, |  | ||||||
|       ...modules, |  | ||||||
|       ` |  | ||||||
| import { createApp as _createApp } from "${SANDBOX_VUE_URL}" |  | ||||||
| 
 | 
 | ||||||
| if (window.__app__) { | // reset sandbox when import map changes | ||||||
|   window.__app__.unmount() | watch(() => store.importMap, (importMap, prev) => { | ||||||
|   document.getElementById('app').innerHTML = '' |   if (!importMap) { | ||||||
| } |     if (prev) { | ||||||
| 
 |       // import-map.json deleted | ||||||
| document.getElementById('__sfc-styles').innerHTML = window.__css__ |       createSandbox() | ||||||
| const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default) |     } | ||||||
| app.config.errorHandler = e => console.error(e) |     return | ||||||
| app.mount('#app')`.trim() |  | ||||||
|     ]) |  | ||||||
|   } catch (e) { |  | ||||||
|     runtimeError.value = e.stack |  | ||||||
|   } |   } | ||||||
| } |   try { | ||||||
|  |     const map = JSON.parse(importMap) | ||||||
|  |     if (!map.imports) { | ||||||
|  |       store.errors = [ | ||||||
|  |         `import-map.json is missing "imports" field.` | ||||||
|  |       ] | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     if (map.imports.vue) { | ||||||
|  |       store.errors = [ | ||||||
|  |         'Select Vue versions using the top-right dropdown.\n' + | ||||||
|  |         'Specifying it in the import map has no effect.' | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |     createSandbox() | ||||||
|  |   } catch (e) { | ||||||
|  |     store.errors = [e] | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | }) | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | // reset sandbox when version changes | ||||||
|   proxy = new PreviewProxy(iframe.value, { | watch(vueRuntimeUrl, createSandbox) | ||||||
|  | 
 | ||||||
|  | onUnmounted(() => { | ||||||
|  |   proxy.destroy() | ||||||
|  |   stopUpdateWatcher && stopUpdateWatcher() | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | function createSandbox() { | ||||||
|  |   if (sandbox) { | ||||||
|  |     // clear prev sandbox | ||||||
|  |     proxy.destroy() | ||||||
|  |     stopUpdateWatcher() | ||||||
|  |     container.value.removeChild(sandbox) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   sandbox = document.createElement('iframe') | ||||||
|  |   sandbox.setAttribute('sandbox', [ | ||||||
|  |     'allow-forms', | ||||||
|  |     'allow-modals', | ||||||
|  |     'allow-pointer-lock', | ||||||
|  |     'allow-popups', | ||||||
|  |     'allow-same-origin', | ||||||
|  |     'allow-scripts', | ||||||
|  |     'allow-top-navigation-by-user-activation' | ||||||
|  |   ].join(' ')) | ||||||
|  | 
 | ||||||
|  |   let importMap: Record<string, any> | ||||||
|  |   try { | ||||||
|  |     importMap = JSON.parse(store.importMap || `{}`) | ||||||
|  |   } catch (e) { | ||||||
|  |     store.errors = [`Syntax error in import-map.json: ${e.message}`] | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!importMap.imports) { | ||||||
|  |     importMap.imports = {} | ||||||
|  |   } | ||||||
|  |   importMap.imports.vue = vueRuntimeUrl.value | ||||||
|  |   const sandboxSrc = srcdoc.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap)) | ||||||
|  |   sandbox.setAttribute('srcdoc', sandboxSrc) | ||||||
|  |   container.value.appendChild(sandbox) | ||||||
|  | 
 | ||||||
|  |   proxy = new PreviewProxy(sandbox, { | ||||||
|     on_fetch_progress: (progress: any) => { |     on_fetch_progress: (progress: any) => { | ||||||
|       // pending_imports = progress; |       // pending_imports = progress; | ||||||
|     }, |     }, | ||||||
| @ -93,19 +138,43 @@ onMounted(() => { | |||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   iframe.value.addEventListener('load', () => { |   sandbox.addEventListener('load', () => { | ||||||
|     proxy.handle_links() |     proxy.handle_links() | ||||||
|     updateHandle = watchEffect(updatePreview) |     stopUpdateWatcher = watchEffect(updatePreview) | ||||||
|   }) |   }) | ||||||
| }) | } | ||||||
| 
 | 
 | ||||||
| onUnmounted(() => { | async function updatePreview() { | ||||||
|   proxy.destroy() |   runtimeError.value = null | ||||||
|   updateHandle && updateHandle() |   runtimeWarning.value = null | ||||||
| }) |   try { | ||||||
|  |     const modules = compileModulesForPreview() | ||||||
|  |     console.log(`successfully compiled ${modules.length} modules.`) | ||||||
|  |     // reset modules | ||||||
|  |     await proxy.eval([ | ||||||
|  |       `window.__modules__ = {};window.__css__ = ''`, | ||||||
|  |       ...modules, | ||||||
|  |       ` | ||||||
|  | import { createApp as _createApp } from "vue" | ||||||
|  | 
 | ||||||
|  | if (window.__app__) { | ||||||
|  |   window.__app__.unmount() | ||||||
|  |   document.getElementById('app').innerHTML = '' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | document.getElementById('__sfc-styles').innerHTML = window.__css__ | ||||||
|  | const app = window.__app__ = _createApp(__modules__["${MAIN_FILE}"].default) | ||||||
|  | app.config.errorHandler = e => console.error(e) | ||||||
|  | app.mount('#app')`.trim() | ||||||
|  |     ]) | ||||||
|  |   } catch (e) { | ||||||
|  |     runtimeError.value = e.stack | ||||||
|  |   } | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  | .preview-container, | ||||||
| iframe { | iframe { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { store, File } from '../store' | import { store, File } from '../store' | ||||||
| import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler' | import { MAIN_FILE } from '../sfcCompiler' | ||||||
| import { | import { | ||||||
|   babelParse, |   babelParse, | ||||||
|   MagicString, |   MagicString, | ||||||
| @ -92,15 +92,6 @@ function processFile(file: File, seen = new Set<File>()) { | |||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         s.remove(node.start!, node.end!) |         s.remove(node.start!, node.end!) | ||||||
|       } else { |  | ||||||
|         if (source === 'vue') { |  | ||||||
|           // rewrite Vue imports
 |  | ||||||
|           s.overwrite( |  | ||||||
|             node.source.start!, |  | ||||||
|             node.source.end!, |  | ||||||
|             `"${SANDBOX_VUE_URL}"` |  | ||||||
|           ) |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -8,222 +8,229 @@ | |||||||
| 			} | 			} | ||||||
| 		</style> | 		</style> | ||||||
| 		<style id="__sfc-styles"></style> | 		<style id="__sfc-styles"></style> | ||||||
| 		<script type="module"> |  | ||||||
| 			let scriptEls = [] |  | ||||||
| 
 | 
 | ||||||
| 			window.__modules__ = {} | 		<!-- ES Module Shims: Import maps polyfill for modules browsers without import maps support (all except Chrome 89+) --> | ||||||
|  | 		<script async src="https://ga.jspm.io/npm:es-module-shims@0.10.1/dist/es-module-shims.min.js"></script> | ||||||
|  | 		<script id="map" type="importmap"><!--IMPORT_MAP--></script> | ||||||
| 
 | 
 | ||||||
| 			window.__export__ = (mod, key, get) => { | 		<script> | ||||||
| 				Object.defineProperty(mod, key, { | 			(() => { | ||||||
| 					enumerable: true, | 				let scriptEls = [] | ||||||
| 					configurable: true, |  | ||||||
| 					get |  | ||||||
| 				}) |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			window.__dynamic_import__ = key => { | 				window.__modules__ = {} | ||||||
| 				return Promise.resolve(window.__modules__[key]) |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			async function handle_message(ev) { | 				window.__export__ = (mod, key, get) => { | ||||||
| 				let { action, cmd_id } = ev.data; | 					Object.defineProperty(mod, key, { | ||||||
| 				const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin); | 						enumerable: true, | ||||||
| 				const send_reply = (payload) => send_message({ ...payload, cmd_id }); | 						configurable: true, | ||||||
| 				const send_ok = () => send_reply({ action: 'cmd_ok' }); | 						get | ||||||
| 				const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack }); | 					}) | ||||||
| 
 |  | ||||||
| 				if (action === 'eval') { |  | ||||||
| 					try { |  | ||||||
| 						if (scriptEls.length) { |  | ||||||
| 							scriptEls.forEach(el => { |  | ||||||
| 								document.head.removeChild(el) |  | ||||||
| 							}) |  | ||||||
| 							scriptEls.length = 0 |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						let { script: scripts } = ev.data.args |  | ||||||
| 						if (typeof scripts === 'string') scripts = [scripts] |  | ||||||
| 
 |  | ||||||
| 						for (const script of scripts) { |  | ||||||
| 							const scriptEl = document.createElement('script') |  | ||||||
| 							scriptEl.setAttribute('type', 'module') |  | ||||||
| 							// send ok in the module script to ensure sequential evaluation |  | ||||||
| 							// of multiple proxy.eval() calls |  | ||||||
| 							const done = new Promise((resolve, reject) => { |  | ||||||
| 								window.__next__ = resolve |  | ||||||
| 								scriptEl.onerror = reject |  | ||||||
| 							}) |  | ||||||
| 							scriptEl.innerHTML = script + `\nwindow.__next__()` |  | ||||||
| 							document.head.appendChild(scriptEl) |  | ||||||
| 							scriptEls.push(scriptEl) |  | ||||||
| 							await done |  | ||||||
| 						} |  | ||||||
| 						window.__next__ = undefined |  | ||||||
| 						send_ok() |  | ||||||
| 					} catch (e) { |  | ||||||
| 						send_error(e.message, e.stack); |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if (action === 'catch_clicks') { | 				window.__dynamic_import__ = key => { | ||||||
| 					try { | 					return Promise.resolve(window.__modules__[key]) | ||||||
| 						const top_origin = ev.origin; | 				} | ||||||
| 						document.body.addEventListener('click', event => { |  | ||||||
| 							if (event.which !== 1) return; |  | ||||||
| 							if (event.metaKey || event.ctrlKey || event.shiftKey) return; |  | ||||||
| 							if (event.defaultPrevented) return; |  | ||||||
| 
 | 
 | ||||||
| 							// ensure target is a link | 				async function handle_message(ev) { | ||||||
| 							let el = event.target; | 					let { action, cmd_id } = ev.data; | ||||||
| 							while (el && el.nodeName !== 'A') el = el.parentNode; | 					const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin); | ||||||
| 							if (!el || el.nodeName !== 'A') return; | 					const send_reply = (payload) => send_message({ ...payload, cmd_id }); | ||||||
|  | 					const send_ok = () => send_reply({ action: 'cmd_ok' }); | ||||||
|  | 					const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack }); | ||||||
| 
 | 
 | ||||||
| 							if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return; | 					if (action === 'eval') { | ||||||
| 
 | 						try { | ||||||
| 							event.preventDefault(); | 							if (scriptEls.length) { | ||||||
| 
 | 								scriptEls.forEach(el => { | ||||||
| 							if (el.href.startsWith(top_origin)) { | 									document.head.removeChild(el) | ||||||
| 								const url = new URL(el.href); | 								}) | ||||||
| 								if (url.hash[0] === '#') { | 								scriptEls.length = 0 | ||||||
| 									window.location.hash = url.hash; |  | ||||||
| 									return; |  | ||||||
| 								} |  | ||||||
| 							} | 							} | ||||||
| 
 | 
 | ||||||
| 							window.open(el.href, '_blank'); | 							let { script: scripts } = ev.data.args | ||||||
| 						}); | 							if (typeof scripts === 'string') scripts = [scripts] | ||||||
| 						send_ok(); |  | ||||||
| 					} catch(e) { |  | ||||||
| 						send_error(e.message, e.stack); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			window.addEventListener('message', handle_message, false); | 							for (const script of scripts) { | ||||||
| 
 | 								const scriptEl = document.createElement('script') | ||||||
| 			window.onerror = function (msg, url, lineNo, columnNo, error) { | 								scriptEl.setAttribute('type', 'module') | ||||||
| 				parent.postMessage({ action: 'error', value: error }, '*'); | 								// send ok in the module script to ensure sequential evaluation | ||||||
| 			} | 								// of multiple proxy.eval() calls | ||||||
| 
 | 								const done = new Promise((resolve, reject) => { | ||||||
| 			window.addEventListener("unhandledrejection", event => { | 									window.__next__ = resolve | ||||||
| 				parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*'); | 									scriptEl.onerror = reject | ||||||
| 			}); | 								}) | ||||||
| 
 | 								scriptEl.innerHTML = script + `\nwindow.__next__()` | ||||||
| 			let previous = { level: null, args: null }; | 								document.head.appendChild(scriptEl) | ||||||
| 
 | 								scriptEls.push(scriptEl) | ||||||
| 			['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => { | 								await done | ||||||
| 				const original = console[level]; | 							} | ||||||
| 				console[level] = (...args) => { | 							window.__next__ = undefined | ||||||
| 					const msg = String(args[0]) | 							send_ok() | ||||||
| 					if (msg.includes('You are running a development build of Vue')) { | 						} catch (e) { | ||||||
| 						return | 							send_error(e.message, e.stack); | ||||||
| 					} |  | ||||||
| 					const stringifiedArgs = stringify(args); |  | ||||||
| 					if ( |  | ||||||
| 						previous.level === level && |  | ||||||
| 						previous.args && |  | ||||||
| 						previous.args === stringifiedArgs |  | ||||||
| 					) { |  | ||||||
| 						parent.postMessage({ action: 'console', level, duplicate: true }, '*'); |  | ||||||
| 					} else { |  | ||||||
| 						previous = { level, args: stringifiedArgs }; |  | ||||||
| 
 |  | ||||||
| 						try { |  | ||||||
| 							parent.postMessage({ action: 'console', level, args }, '*'); |  | ||||||
| 						} catch (err) { |  | ||||||
| 							parent.postMessage({ action: 'console', level: 'unclonable' }, '*'); |  | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					original(...args); | 					if (action === 'catch_clicks') { | ||||||
|  | 						try { | ||||||
|  | 							const top_origin = ev.origin; | ||||||
|  | 							document.body.addEventListener('click', event => { | ||||||
|  | 								if (event.which !== 1) return; | ||||||
|  | 								if (event.metaKey || event.ctrlKey || event.shiftKey) return; | ||||||
|  | 								if (event.defaultPrevented) return; | ||||||
|  | 
 | ||||||
|  | 								// ensure target is a link | ||||||
|  | 								let el = event.target; | ||||||
|  | 								while (el && el.nodeName !== 'A') el = el.parentNode; | ||||||
|  | 								if (!el || el.nodeName !== 'A') return; | ||||||
|  | 
 | ||||||
|  | 								if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return; | ||||||
|  | 
 | ||||||
|  | 								event.preventDefault(); | ||||||
|  | 
 | ||||||
|  | 								if (el.href.startsWith(top_origin)) { | ||||||
|  | 									const url = new URL(el.href); | ||||||
|  | 									if (url.hash[0] === '#') { | ||||||
|  | 										window.location.hash = url.hash; | ||||||
|  | 										return; | ||||||
|  | 									} | ||||||
|  | 								} | ||||||
|  | 
 | ||||||
|  | 								window.open(el.href, '_blank'); | ||||||
|  | 							}); | ||||||
|  | 							send_ok(); | ||||||
|  | 						} catch(e) { | ||||||
|  | 							send_error(e.message, e.stack); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			}); |  | ||||||
| 
 | 
 | ||||||
| 			[ | 				window.addEventListener('message', handle_message, false); | ||||||
| 				{ method: 'group', action: 'console_group' }, |  | ||||||
| 				{ method: 'groupEnd', action: 'console_group_end' }, |  | ||||||
| 				{ method: 'groupCollapsed', action: 'console_group_collapsed' }, |  | ||||||
| 			].forEach((group_action) => { |  | ||||||
| 				const original = console[group_action.method]; |  | ||||||
| 				console[group_action.method] = (label) => { |  | ||||||
| 					parent.postMessage({ action: group_action.action, label }, '*'); |  | ||||||
| 
 | 
 | ||||||
| 					original(label); | 				window.onerror = function (msg, url, lineNo, columnNo, error) { | ||||||
|  | 					parent.postMessage({ action: 'error', value: error }, '*'); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				window.addEventListener("unhandledrejection", event => { | ||||||
|  | 					parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*'); | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				let previous = { level: null, args: null }; | ||||||
|  | 
 | ||||||
|  | 				['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => { | ||||||
|  | 					const original = console[level]; | ||||||
|  | 					console[level] = (...args) => { | ||||||
|  | 						const msg = String(args[0]) | ||||||
|  | 						if (msg.includes('You are running a development build of Vue')) { | ||||||
|  | 							return | ||||||
|  | 						} | ||||||
|  | 						const stringifiedArgs = stringify(args); | ||||||
|  | 						if ( | ||||||
|  | 							previous.level === level && | ||||||
|  | 							previous.args && | ||||||
|  | 							previous.args === stringifiedArgs | ||||||
|  | 						) { | ||||||
|  | 							parent.postMessage({ action: 'console', level, duplicate: true }, '*'); | ||||||
|  | 						} else { | ||||||
|  | 							previous = { level, args: stringifiedArgs }; | ||||||
|  | 
 | ||||||
|  | 							try { | ||||||
|  | 								parent.postMessage({ action: 'console', level, args }, '*'); | ||||||
|  | 							} catch (err) { | ||||||
|  | 								parent.postMessage({ action: 'console', level: 'unclonable' }, '*'); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						original(...args); | ||||||
|  | 					} | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				[ | ||||||
|  | 					{ method: 'group', action: 'console_group' }, | ||||||
|  | 					{ method: 'groupEnd', action: 'console_group_end' }, | ||||||
|  | 					{ method: 'groupCollapsed', action: 'console_group_collapsed' }, | ||||||
|  | 				].forEach((group_action) => { | ||||||
|  | 					const original = console[group_action.method]; | ||||||
|  | 					console[group_action.method] = (label) => { | ||||||
|  | 						parent.postMessage({ action: group_action.action, label }, '*'); | ||||||
|  | 
 | ||||||
|  | 						original(label); | ||||||
|  | 					}; | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				const timers = new Map(); | ||||||
|  | 				const original_time = console.time; | ||||||
|  | 				const original_timelog = console.timeLog; | ||||||
|  | 				const original_timeend = console.timeEnd; | ||||||
|  | 
 | ||||||
|  | 				console.time = (label = 'default') => { | ||||||
|  | 					original_time(label); | ||||||
|  | 					timers.set(label, performance.now()); | ||||||
|  | 				} | ||||||
|  | 				console.timeLog = (label = 'default') => { | ||||||
|  | 					original_timelog(label); | ||||||
|  | 					const now = performance.now(); | ||||||
|  | 					if (timers.has(label)) { | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*'); | ||||||
|  | 					} else { | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*'); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				console.timeEnd = (label = 'default') => { | ||||||
|  | 					original_timeend(label); | ||||||
|  | 					const now = performance.now(); | ||||||
|  | 					if (timers.has(label)) { | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*'); | ||||||
|  | 					} else { | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*'); | ||||||
|  | 					} | ||||||
|  | 					timers.delete(label); | ||||||
| 				}; | 				}; | ||||||
| 			}); |  | ||||||
| 
 | 
 | ||||||
| 			const timers = new Map(); | 				const original_assert = console.assert; | ||||||
| 			const original_time = console.time; | 				console.assert = (condition, ...args) => { | ||||||
| 			const original_timelog = console.timeLog; | 					if (condition) { | ||||||
| 			const original_timeend = console.timeEnd; | 						const stack = new Error().stack; | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*'); | ||||||
|  | 					} | ||||||
|  | 					original_assert(condition, ...args); | ||||||
|  | 				}; | ||||||
| 
 | 
 | ||||||
| 			console.time = (label = 'default') => { | 				const counter = new Map(); | ||||||
| 				original_time(label); | 				const original_count = console.count; | ||||||
| 				timers.set(label, performance.now()); | 				const original_countreset = console.countReset; | ||||||
| 			} |  | ||||||
| 			console.timeLog = (label = 'default') => { |  | ||||||
| 				original_timelog(label); |  | ||||||
| 				const now = performance.now(); |  | ||||||
| 				if (timers.has(label)) { |  | ||||||
| 					parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*'); |  | ||||||
| 				} else { |  | ||||||
| 					parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*'); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			console.timeEnd = (label = 'default') => { |  | ||||||
| 				original_timeend(label); |  | ||||||
| 				const now = performance.now(); |  | ||||||
| 				if (timers.has(label)) { |  | ||||||
| 					parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*'); |  | ||||||
| 				} else { |  | ||||||
| 					parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*'); |  | ||||||
| 				} |  | ||||||
| 				timers.delete(label); |  | ||||||
| 			}; |  | ||||||
| 
 | 
 | ||||||
| 			const original_assert = console.assert; | 				console.count = (label = 'default') => { | ||||||
| 			console.assert = (condition, ...args) => { | 					counter.set(label, (counter.get(label) || 0) + 1); | ||||||
| 				if (condition) { | 					parent.postMessage({ action: 'console', level: 'system-log', args: `${label}: ${counter.get(label)}` }, '*'); | ||||||
|  | 					original_count(label); | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				console.countReset = (label = 'default') => { | ||||||
|  | 					if (counter.has(label)) { | ||||||
|  | 						counter.set(label, 0); | ||||||
|  | 					} else { | ||||||
|  | 						parent.postMessage({ action: 'console', level: 'system-warn', args: `Count for '${label}' does not exist` }, '*'); | ||||||
|  | 					} | ||||||
|  | 					original_countreset(label); | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				const original_trace = console.trace; | ||||||
|  | 
 | ||||||
|  | 				console.trace = (...args) => { | ||||||
| 					const stack = new Error().stack; | 					const stack = new Error().stack; | ||||||
| 					parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*'); | 					parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*'); | ||||||
|  | 					original_trace(...args); | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				function stringify(args) { | ||||||
|  | 					try { | ||||||
|  | 						return JSON.stringify(args); | ||||||
|  | 					} catch (error) { | ||||||
|  | 						return null; | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 				original_assert(condition, ...args); | 			})() | ||||||
| 			}; |  | ||||||
| 
 |  | ||||||
| 			const counter = new Map(); |  | ||||||
| 			const original_count = console.count; |  | ||||||
| 			const original_countreset = console.countReset; |  | ||||||
| 
 |  | ||||||
| 			console.count = (label = 'default') => { |  | ||||||
| 				counter.set(label, (counter.get(label) || 0) + 1); |  | ||||||
| 				parent.postMessage({ action: 'console', level: 'system-log', args: `${label}: ${counter.get(label)}` }, '*'); |  | ||||||
| 				original_count(label); |  | ||||||
| 			}; |  | ||||||
| 
 |  | ||||||
| 			console.countReset = (label = 'default') => { |  | ||||||
| 				if (counter.has(label)) { |  | ||||||
| 					counter.set(label, 0); |  | ||||||
| 				} else { |  | ||||||
| 					parent.postMessage({ action: 'console', level: 'system-warn', args: `Count for '${label}' does not exist` }, '*'); |  | ||||||
| 				} |  | ||||||
| 				original_countreset(label); |  | ||||||
| 			}; |  | ||||||
| 
 |  | ||||||
| 			const original_trace = console.trace; |  | ||||||
| 
 |  | ||||||
| 			console.trace = (...args) => { |  | ||||||
| 				const stack = new Error().stack; |  | ||||||
| 				parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*'); |  | ||||||
| 				original_trace(...args); |  | ||||||
| 			}; |  | ||||||
| 
 |  | ||||||
| 			function stringify(args) { |  | ||||||
| 				try { |  | ||||||
| 					return JSON.stringify(args); |  | ||||||
| 				} catch (error) { |  | ||||||
| 					return null; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		</script> | 		</script> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { store, File } from './store' | import { store, File } from './store' | ||||||
| import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc' | import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc' | ||||||
| import * as defaultCompiler from '@vue/compiler-sfc' | import * as defaultCompiler from '@vue/compiler-sfc' | ||||||
|  | import { ref } from 'vue' | ||||||
| 
 | 
 | ||||||
| export const MAIN_FILE = 'App.vue' | export const MAIN_FILE = 'App.vue' | ||||||
| export const COMP_IDENTIFIER = `__sfc__` | export const COMP_IDENTIFIER = `__sfc__` | ||||||
| @ -16,23 +17,23 @@ const defaultVueUrl = import.meta.env.PROD | |||||||
|   ? '/vue.runtime.esm-browser.js' // to be copied on build
 |   ? '/vue.runtime.esm-browser.js' // to be copied on build
 | ||||||
|   : '/src/vue-dev-proxy' |   : '/src/vue-dev-proxy' | ||||||
| 
 | 
 | ||||||
| export let SANDBOX_VUE_URL = defaultVueUrl | export const vueRuntimeUrl = ref(defaultVueUrl) | ||||||
| 
 | 
 | ||||||
| export async function setVersion(version: string) { | export async function setVersion(version: string) { | ||||||
|   const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` |   const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` | ||||||
|   const runtimeUrl = `https://cdn.skypack.dev/@vue/runtime-dom@${version}` |   const runtimeUrl = `https://unpkg.com/@vue/runtime-dom@${version}/dist/runtime-dom.esm-browser.js` | ||||||
|   const [compiler] = await Promise.all([ |   const [compiler] = await Promise.all([ | ||||||
|     import(/* @vite-ignore */ compilerUrl), |     import(/* @vite-ignore */ compilerUrl), | ||||||
|     import(/* @vite-ignore */ runtimeUrl) |     import(/* @vite-ignore */ runtimeUrl) | ||||||
|   ]) |   ]) | ||||||
|   SFCCompiler = compiler |   SFCCompiler = compiler | ||||||
|   SANDBOX_VUE_URL = runtimeUrl |   vueRuntimeUrl.value = runtimeUrl | ||||||
|   console.info(`Now using Vue version: ${version}`) |   console.info(`Now using Vue version: ${version}`) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function resetVersion() { | export function resetVersion() { | ||||||
|   SFCCompiler = defaultCompiler |   SFCCompiler = defaultCompiler | ||||||
|   SANDBOX_VUE_URL = defaultVueUrl |   vueRuntimeUrl.value = defaultVueUrl | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function compileFile({ filename, code, compiled }: File) { | export async function compileFile({ filename, code, compiled }: File) { | ||||||
| @ -41,7 +42,7 @@ export async function compileFile({ filename, code, compiled }: File) { | |||||||
|     return |     return | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (filename.endsWith('.js')) { |   if (!filename.endsWith('.vue')) { | ||||||
|     compiled.js = compiled.ssr = code |     compiled.js = compiled.ssr = code | ||||||
|     store.errors = [] |     store.errors = [] | ||||||
|     return |     return | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ interface Store { | |||||||
|   files: Record<string, File> |   files: Record<string, File> | ||||||
|   activeFilename: string |   activeFilename: string | ||||||
|   readonly activeFile: File |   readonly activeFile: File | ||||||
|  |   readonly importMap: string | undefined | ||||||
|   errors: (string | Error)[] |   errors: (string | Error)[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -53,6 +54,10 @@ export const store: Store = reactive({ | |||||||
|   get activeFile() { |   get activeFile() { | ||||||
|     return store.files[store.activeFilename] |     return store.files[store.activeFilename] | ||||||
|   }, |   }, | ||||||
|  |   get importMap() { | ||||||
|  |     const file = store.files['import-map.json'] | ||||||
|  |     return file && file.code | ||||||
|  |   }, | ||||||
|   errors: [] |   errors: [] | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| @ -81,7 +86,17 @@ export function setActive(filename: string) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function addFile(filename: string) { | export function addFile(filename: string) { | ||||||
|   store.files[filename] = new File(filename) |   const file = (store.files[filename] = new File(filename)) | ||||||
|  | 
 | ||||||
|  |   if (filename === 'import-map.json') { | ||||||
|  |     file.code = ` | ||||||
|  | { | ||||||
|  |   "imports": { | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | }`.trim()
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   setActive(filename) |   setActive(filename) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user