export { walkIdentifiers } from './compileScript'
import MagicString from 'magic-string'
export { MagicString }
+export { walk } from 'estree-walker'
// Types
export {
background-color: #f8f8f8;
--nav-height: 50px;
--font-code: 'Source Code Pro', monospace;
+ --color-branding: #3ca877;
+ --color-branding-dark: #416f9c;
}
.wrapper {
border-radius: 6px;
font-family: var(--font-code);
white-space: pre-wrap;
+ max-height: calc(100% - 50px);
+ overflow-y: scroll;
}
.msg.err {
<template>
- <CodeMirror @change="onChange" :value="initialCode" />
+ <FileSelector/>
+ <div class="editor-container">
+ <CodeMirror @change="onChange" :value="activeCode" :mode="activeMode" />
<Message :err="store.errors[0]" />
+ </div>
</template>
<script setup lang="ts">
+import FileSelector from './FileSelector.vue'
import CodeMirror from '../codemirror/CodeMirror.vue'
import Message from '../Message.vue'
import { store } from '../store'
import { debounce } from '../utils'
+import { ref, watch, computed } from 'vue'
const onChange = debounce((code: string) => {
- store.code = code
+ store.activeFile.code = code
}, 250)
-const initialCode = store.code
-</script>
\ No newline at end of file
+const activeCode = ref(store.activeFile.code)
+const activeMode = computed(
+ () => (store.activeFilename.endsWith('.js') ? 'javascript' : 'htmlmixed')
+)
+
+watch(
+ () => store.activeFilename,
+ () => {
+ activeCode.value = store.activeFile.code
+ }
+)
+</script>
+
+<style scoped>
+.editor-container {
+ height: calc(100% - 35px);
+ overflow: hidden;
+ position: relative;
+}
+</style>
--- /dev/null
+<template>
+ <div class="file-selector">
+ <div
+ v-for="(file, i) in Object.keys(store.files)"
+ class="file"
+ :class="{ active: store.activeFilename === file }"
+ @click="setActive(file)">
+ <span class="label">{{ file }}</span>
+ <span v-if="i > 0" class="remove" @click.stop="deleteFile(file)">
+ <svg width="12" height="12" viewBox="0 0 24 24" class="svelte-cghqrp"><line stroke="#999" x1="18" y1="6" x2="6" y2="18"></line><line stroke="#999" x1="6" y1="6" x2="18" y2="18"></line></svg>
+ </span>
+ </div>
+ <div v-if="pending" class="file" >
+ <input
+ v-model="pendingFilename"
+ spellcheck="false"
+ @keyup.enter="doneAddFile"
+ @keyup.esc="cancelAddFile"
+ @vnodeMounted="focus">
+ </div>
+ <button class="add" @click="startAddFile">+</button>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { store, addFile, deleteFile, setActive } from '../store'
+import { ref } from 'vue'
+import type { VNode } from 'vue'
+
+const pending = ref(false)
+const pendingFilename = ref('Comp.vue')
+
+function startAddFile() {
+ pending.value = true
+}
+
+function cancelAddFile() {
+ pending.value = false
+}
+
+function focus({ el }: VNode) {
+ (el as HTMLInputElement).focus()
+}
+
+function doneAddFile() {
+ const filename = pendingFilename.value
+
+ if (!filename.endsWith('.vue') && !filename.endsWith('.js')) {
+ store.errors = [`Playground only supports .vue or .js files.`]
+ return
+ }
+
+ if (filename in store.files) {
+ store.errors = [`File "${filename}" already exists.`]
+ return
+ }
+
+ store.errors = []
+ pending.value = false
+ addFile(filename)
+ pendingFilename.value = 'Comp.vue'
+}
+</script>
+
+<style scoped>
+.file-selector {
+ box-sizing: border-box;
+ border-bottom: 1px solid #ddd;
+ background-color: white;
+}
+.file {
+ display: inline-block;
+ font-size: 13px;
+ font-family: var(--font-code);
+ cursor: pointer;
+ color: #999;
+ box-sizing: border-box;
+}
+.file.active {
+ color: var(--color-branding);
+ border-bottom: 3px solid var(--color-branding);
+ cursor: text;
+}
+.file span {
+ display: inline-block;
+ padding: 8px 10px 6px;
+}
+.file input {
+ width: 80px;
+ outline: none;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ padding: 4px 6px;
+ margin-left: 6px;
+}
+.file .remove {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 12px;
+ cursor: pointer;
+ padding-left: 0;
+}
+.add {
+ margin: 0;
+ font-size: 20px;
+ font-family: var(--font-code);
+ color: #999;
+ border: none;
+ outline: none;
+ background-color: transparent;
+ cursor: pointer;
+ vertical-align: middle;
+ margin-left: 6px;
+}
+.add:hover {
+ color: var(--color-branding);
+}
+</style>
</div>
<div class="output-container">
- <Preview v-if="mode === 'preview'" :code="store.compiled.executed" />
+ <Preview v-if="mode === 'preview'" />
<CodeMirror
v-else
readonly
:mode="mode === 'css' ? 'css' : 'javascript'"
- :value="store.compiled[mode]"
+ :value="store.activeFile.compiled[mode]"
/>
</div>
</template>
import { store } from '../store'
import { ref } from 'vue'
-type Modes = 'preview' | 'executed' | 'js' | 'css' | 'template'
+type Modes = 'preview' | 'js' | 'css'
-const modes: Modes[] = ['preview', 'js', 'css', 'template', 'executed']
+const modes: Modes[] = ['preview', 'js', 'css']
const mode = ref<Modes>('preview')
</script>
.tab-buttons {
box-sizing: border-box;
border-bottom: 1px solid #ddd;
+ background-color: white;
}
.tab-buttons button {
margin: 0;
font-size: 13px;
- font-family: 'Source Code Pro', monospace;
+ font-family: var(--font-code);
border: none;
outline: none;
- background-color: #f8f8f8;
+ background-color: transparent;
padding: 8px 16px 6px;
text-transform: uppercase;
cursor: pointer;
}
button.active {
- color: #42b983;
- border-bottom: 3px solid #42b983;
+ color: var(--color-branding-dark);
+ border-bottom: 3px solid var(--color-branding-dark);
}
</style>
<script setup lang="ts">
import Message from '../Message.vue'
-import { ref, onMounted, onUnmounted, watchEffect, defineProps } from 'vue'
+import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
import srcdoc from './srcdoc.html?raw'
import { PreviewProxy } from './PreviewProxy'
-import { sandboxVueURL } from '../store'
-
-const props = defineProps<{ code: string }>()
+import { MAIN_FILE, SANDBOX_VUE_URL } from '../store'
+import { compileModulesForPreview } from './moduleCompiler'
const iframe = ref()
const runtimeError = ref()
let proxy: PreviewProxy
async function updatePreview() {
- if (!props.code?.trim()) {
- return
- }
+ runtimeError.value = null
+ runtimeWarning.value = null
try {
- proxy.eval(`
- ${props.code}
-
- if (window.vueApp) {
- window.vueApp.unmount()
- }
- const container = document.getElementById('app')
- container.innerHTML = ''
-
- import { createApp as _createApp } from "${sandboxVueURL}"
- const app = window.vueApp = _createApp(__comp)
-
- app.config.errorHandler = e => console.error(e)
-
- app.mount(container)
- `)
+ const modules = compileModulesForPreview()
+ console.log(`successfully compiled ${modules.length} modules.`)
+ // reset modules
+ await proxy.eval(`
+ window.__modules__ = {}
+ window.__css__ = ''
+ `)
+ // evaluate modules
+ for (const mod of modules) {
+ await proxy.eval(mod)
+ }
+ // reboot
+ await proxy.eval(`
+ import { createApp as _createApp } from "${SANDBOX_VUE_URL}"
+ 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')
+ `)
} catch (e) {
- runtimeError.value = e.message
- return
+ runtimeError.value = e.stack
}
- runtimeError.value = null
- runtimeWarning.value = null
}
onMounted(() => {
// pending_imports = progress;
},
on_error: (event: any) => {
- // push_logs({ level: 'error', args: [event.value] });
runtimeError.value = event.value
},
on_unhandled_rejection: (event: any) => {
},
on_console: (log: any) => {
if (log.level === 'error') {
- runtimeError.value = log.args.join('')
+ if (log.args[0] instanceof Error) {
+ runtimeError.value = log.args[0].stack
+ } else {
+ runtimeError.value = log.args
+ }
} else if (log.level === 'warn') {
if (log.args[0].toString().includes('[Vue warn]')) {
- runtimeWarning.value = log.args.join('').replace(/\[Vue warn\]:/, '').trim()
+ runtimeWarning.value = log.args
+ .join('')
+ .replace(/\[Vue warn\]:/, '')
+ .trim()
}
}
},
})
iframe.value.addEventListener('load', () => {
- proxy.handle_links();
+ proxy.handle_links()
watchEffect(updatePreview)
- });
+ })
})
onUnmounted(() => {
--- /dev/null
+import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store'
+import { babelParse, MagicString, walk } from '@vue/compiler-sfc'
+import { babelParserDefaultPlugins } from '@vue/shared'
+import { Identifier, Node } from '@babel/types'
+
+export function compileModulesForPreview() {
+ return processFile(store.files[MAIN_FILE]).reverse()
+}
+
+function processFile(file: File, seen = new Set<File>()) {
+ if (seen.has(file)) {
+ return []
+ }
+ seen.add(file)
+
+ const { js, css } = file.compiled
+ const ast = babelParse(js, {
+ sourceFilename: file.filename,
+ sourceType: 'module',
+ plugins: [...babelParserDefaultPlugins]
+ }).program.body
+
+ const importedFiles = new Set<string>()
+ const importToIdMap = new Map<string, string>()
+
+ const s = new MagicString(js)
+
+ function registerImport(source: string) {
+ const filename = source.replace(/^\.\/+/, '')
+ if (!(filename in store.files)) {
+ throw new Error(`File "${filename}" does not exist.`)
+ }
+ if (importedFiles.has(filename)) {
+ return importToIdMap.get(filename)
+ }
+ importedFiles.add(filename)
+ const id = `__import_${importedFiles.size}__`
+ importToIdMap.set(filename, id)
+ s.prepend(`const ${id} = __modules__[${JSON.stringify(filename)}]\n`)
+ return id
+ }
+
+ s.prepend(
+ `const mod = __modules__[${JSON.stringify(
+ file.filename
+ )}] = Object.create(null)\n\n`
+ )
+
+ for (const node of ast) {
+ if (node.type === 'ImportDeclaration') {
+ const source = node.source.value
+ if (source === 'vue') {
+ // rewrite Vue imports
+ s.overwrite(
+ node.source.start!,
+ node.source.end!,
+ `"${SANDBOX_VUE_URL}"`
+ )
+ } else if (source.startsWith('./')) {
+ // rewrite the import to retrieve the import from global registry
+ s.remove(node.start!, node.end!)
+
+ const id = registerImport(source)
+
+ for (const spec of node.specifiers) {
+ if (spec.type === 'ImportDefaultSpecifier') {
+ s.prependRight(
+ node.start!,
+ `const ${spec.local.name} = ${id}.default\n`
+ )
+ } else if (spec.type === 'ImportSpecifier') {
+ s.prependRight(
+ node.start!,
+ `const ${spec.local.name} = ${id}.${
+ (spec.imported as Identifier).name
+ }\n`
+ )
+ } else {
+ // namespace import
+ s.prependRight(node.start!, `const ${spec.local.name} = ${id}`)
+ }
+ }
+ }
+ }
+
+ if (node.type === 'ExportDefaultDeclaration') {
+ // export default -> mod.default = ...
+ s.overwrite(node.start!, node.declaration.start!, 'mod.default = ')
+ }
+
+ if (node.type === 'ExportNamedDeclaration') {
+ if (node.source) {
+ // export { foo } from '...' -> mode.foo = __import_x__.foo
+ const id = registerImport(node.source.value)
+ let code = ``
+ for (const spec of node.specifiers) {
+ if (spec.type === 'ExportSpecifier') {
+ code += `mod.${(spec.exported as Identifier).name} = ${id}.${
+ spec.local.name
+ }\n`
+ }
+ }
+ s.overwrite(node.start!, node.end!, code)
+ } else if (node.declaration) {
+ if (
+ node.declaration.type === 'FunctionDeclaration' ||
+ node.declaration.type === 'ClassDeclaration'
+ ) {
+ // export function foo() {}
+ const name = node.declaration.id!.name
+ s.appendLeft(node.end!, `\nmod.${name} = ${name}\n`)
+ } else if (node.declaration.type === 'VariableDeclaration') {
+ // export const foo = 1, bar = 2
+ for (const decl of node.declaration.declarations) {
+ for (const { name } of extractIdentifiers(decl.id)) {
+ s.appendLeft(node.end!, `\nmod.${name} = ${name}`)
+ }
+ }
+ }
+ s.remove(node.start!, node.declaration.start!)
+ } else {
+ let code = ``
+ for (const spec of node.specifiers) {
+ if (spec.type === 'ExportSpecifier') {
+ code += `mod.${(spec.exported as Identifier).name} = ${
+ spec.local.name
+ }\n`
+ }
+ }
+ s.overwrite(node.start!, node.end!, code)
+ }
+ }
+
+ if (node.type === 'ExportAllDeclaration') {
+ const id = registerImport(node.source.value)
+ s.overwrite(node.start!, node.end!, `Object.assign(mod, ${id})`)
+ }
+ }
+
+ // dynamic import
+ walk(ast as any, {
+ enter(node) {
+ if (node.type === 'ImportExpression') {
+ }
+ }
+ })
+
+ // append CSS injection code
+ if (css) {
+ s.append(`\nwindow.__css__ += ${JSON.stringify(css)}`)
+ }
+
+ const processed = [s.toString()]
+ if (importedFiles.size) {
+ for (const imported of importedFiles) {
+ processed.push(...processFile(store.files[imported], seen))
+ }
+ }
+
+ // return a list of files to further process
+ return processed
+}
+
+function extractIdentifiers(
+ param: Node,
+ nodes: Identifier[] = []
+): Identifier[] {
+ switch (param.type) {
+ case 'Identifier':
+ nodes.push(param)
+ break
+
+ case 'MemberExpression':
+ let object: any = param
+ while (object.type === 'MemberExpression') {
+ object = object.object
+ }
+ nodes.push(object)
+ break
+
+ case 'ObjectPattern':
+ param.properties.forEach(prop => {
+ if (prop.type === 'RestElement') {
+ extractIdentifiers(prop.argument, nodes)
+ } else {
+ extractIdentifiers(prop.value, nodes)
+ }
+ })
+ break
+
+ case 'ArrayPattern':
+ param.elements.forEach(element => {
+ if (element) extractIdentifiers(element, nodes)
+ })
+ break
+
+ case 'RestElement':
+ extractIdentifiers(param.argument, nodes)
+ break
+
+ case 'AssignmentPattern':
+ extractIdentifiers(param.left, nodes)
+ break
+ }
+
+ return nodes
+}
}
</style>
<style id="__sfc-styles"></style>
- <script>
- (function(){
- let scriptEl
-
- function handle_message(ev) {
- let { action, cmd_id } = ev.data;
- const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
- 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 (action === 'eval') {
- try {
- if (scriptEl) {
- document.head.removeChild(scriptEl)
- }
- scriptEl = document.createElement('script')
- scriptEl.setAttribute('type', 'module')
- scriptEl.innerHTML = ev.data.args.script
- document.head.appendChild(scriptEl)
- send_ok();
- } catch (e) {
- send_error(e.message, e.stack);
+ <script type="module">
+ let scriptEl
+
+ function handle_message(ev) {
+ let { action, cmd_id } = ev.data;
+ const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
+ const send_reply = (payload) => send_message({ ...payload, cmd_id });
+ const send_ok = window.send_ok = () => send_reply({ action: 'cmd_ok' });
+ const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack });
+
+ if (action === 'eval') {
+ try {
+ if (scriptEl) {
+ document.head.removeChild(scriptEl)
}
+ scriptEl = document.createElement('script')
+ scriptEl.setAttribute('type', 'module')
+ // send ok in the module script to ensure sequential evaluation
+ // of multiple proxy.eval() calls
+ scriptEl.innerHTML = ev.data.args.script + `\nwindow.send_ok()`
+ document.head.appendChild(scriptEl)
+ } catch (e) {
+ send_error(e.message, e.stack);
}
+ }
- 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;
- }
+ 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.open(el.href, '_blank');
+ });
+ send_ok();
+ } catch(e) {
+ send_error(e.message, e.stack);
}
}
+ }
- window.addEventListener('message', handle_message, false);
+ window.addEventListener('message', handle_message, false);
- window.onerror = function (msg, url, lineNo, columnNo, error) {
- parent.postMessage({ action: 'error', value: error }, '*');
- }
+ 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 }, '*');
- });
- }).call(this);
+ window.addEventListener("unhandledrejection", event => {
+ parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
+ });
let previous = { level: null, args: null };
compileTemplate,
compileStyleAsync,
compileScript,
- rewriteDefault,
- CompilerError
+ rewriteDefault
} from '@vue/compiler-sfc'
-const storeKey = 'sfc-code'
-const saved =
- localStorage.getItem(storeKey) ||
- `
+const STORAGE_KEY = 'vue-sfc-playground'
+
+const welcomeCode = `
<template>
<h1>{{ msg }}</h1>
</template>
<script setup>
const msg = 'Hello World!'
</script>
-
-<style scoped>
-h1 {
- color: #42b983;
-}
-</style>
`.trim()
+export const MAIN_FILE = 'App.vue'
+export const COMP_IDENTIFIER = `__sfc__`
+
// @ts-ignore
-export const sandboxVueURL = import.meta.env.PROD
+export const SANDBOX_VUE_URL = import.meta.env.PROD
? '/vue.runtime.esm-browser.js' // to be copied on build
: '/src/vue-dev-proxy'
-export const store = reactive({
- code: saved,
- compiled: {
- executed: '',
+export class File {
+ filename: string
+ code: string
+ compiled = {
js: '',
- css: '',
- template: ''
+ css: ''
+ }
+
+ constructor(filename: string, code = '') {
+ this.filename = filename
+ this.code = code
+ }
+}
+
+interface Store {
+ files: Record<string, File>
+ activeFilename: string
+ readonly activeFile: File
+ errors: (string | Error)[]
+}
+
+const savedFiles = localStorage.getItem(STORAGE_KEY)
+const files = savedFiles
+ ? JSON.parse(savedFiles)
+ : {
+ 'App.vue': new File(MAIN_FILE, welcomeCode)
+ }
+
+export const store: Store = reactive({
+ files,
+ activeFilename: MAIN_FILE,
+ get activeFile() {
+ return store.files[store.activeFilename]
},
- errors: [] as (string | CompilerError | SyntaxError)[]
+ errors: []
+})
+
+for (const file in store.files) {
+ if (file !== MAIN_FILE) {
+ compileFile(store.files[file])
+ }
+}
+
+watchEffect(() => compileFile(store.activeFile))
+watchEffect(() => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(store.files))
})
-const filename = 'Playground.vue'
-const id = 'scope-id'
-const compIdentifier = `__comp`
+export function setActive(filename: string) {
+ store.activeFilename = filename
+}
+
+export function addFile(filename: string) {
+ store.files[filename] = new File(filename)
+ setActive(filename)
+}
-watchEffect(async () => {
- const { code, compiled } = store
+export function deleteFile(filename: string) {
+ if (confirm(`Are you sure you want to delete ${filename}?`)) {
+ if (store.activeFilename === filename) {
+ store.activeFilename = MAIN_FILE
+ }
+ delete store.files[filename]
+ }
+}
+
+async function compileFile({ filename, code, compiled }: File) {
if (!code.trim()) {
return
}
- localStorage.setItem(storeKey, code)
+ if (filename.endsWith('.js')) {
+ compiled.js = code
+ return
+ }
+ const id = await hashId(filename)
const { errors, descriptor } = parse(code, { filename, sourceMap: true })
if (errors.length) {
store.errors = errors
refSugar: true,
inlineTemplate: true
})
- compiled.js = compiledScript.content.trim()
finalCode +=
- `\n` +
- rewriteDefault(
- rewriteVueImports(compiledScript.content),
- compIdentifier
- )
+ `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
} catch (e) {
store.errors = [e]
return
}
} else {
- compiled.js = ''
- finalCode += `\nconst ${compIdentifier} = {}`
+ finalCode += `\nconst ${COMP_IDENTIFIER} = {}`
}
// template
return
}
- compiled.template = templateResult.code.trim()
finalCode +=
`\n` +
- rewriteVueImports(templateResult.code).replace(
+ templateResult.code.replace(
/\nexport (function|const) render/,
'$1 render'
)
- finalCode += `\n${compIdentifier}.render = render`
- } else {
- compiled.template = descriptor.scriptSetup
- ? '/* inlined in JS (script setup) */'
- : '/* no template present */'
+ finalCode += `\n${COMP_IDENTIFIER}.render = render`
}
if (hasScoped) {
- finalCode += `\n${compIdentifier}.__scopeId = ${JSON.stringify(
+ finalCode += `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(
`data-v-${id}`
)}`
}
+ if (finalCode) {
+ finalCode += `\nexport default ${COMP_IDENTIFIER}`
+ compiled.js = finalCode.trimStart()
+ }
+
// styles
let css = ''
for (const style of descriptor.styles) {
}
if (css) {
compiled.css = css.trim()
- finalCode += `\ndocument.getElementById('__sfc-styles').innerHTML = ${JSON.stringify(
- css
- )}`
} else {
- compiled.css = ''
+ compiled.css = '/* No <style> tags present */'
}
+ // clear errors
store.errors = []
- if (finalCode) {
- compiled.executed =
- `/* Exact code being executed in the preview iframe (different from production bundler output) */\n` +
- finalCode
- }
-})
+}
-// TODO use proper parser
-function rewriteVueImports(code: string): string {
- return code.replace(
- /\b(import \{.*?\}\s+from\s+)(?:"vue"|'vue')/g,
- `$1"${sandboxVueURL}"`
- )
+async function hashId(filename: string) {
+ const msgUint8 = new TextEncoder().encode(filename) // encode as (utf-8) Uint8Array
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
+ const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
+ return hashHex.slice(0, 8)
}