|
@@ -0,0 +1,225 @@
|
|
|
|
|
+import { store, File } from './store'
|
|
|
|
|
+import {
|
|
|
|
|
+ parse,
|
|
|
|
|
+ compileTemplate,
|
|
|
|
|
+ compileStyleAsync,
|
|
|
|
|
+ compileScript,
|
|
|
|
|
+ rewriteDefault,
|
|
|
|
|
+ SFCDescriptor,
|
|
|
|
|
+ BindingMetadata
|
|
|
|
|
+} from '@vue/compiler-sfc'
|
|
|
|
|
+
|
|
|
|
|
+export const MAIN_FILE = 'App.vue'
|
|
|
|
|
+export const COMP_IDENTIFIER = `__sfc__`
|
|
|
|
|
+
|
|
|
|
|
+// @ts-ignore
|
|
|
|
|
+export const SANDBOX_VUE_URL = import.meta.env.PROD
|
|
|
|
|
+ ? '/vue.runtime.esm-browser.js' // to be copied on build
|
|
|
|
|
+ : '/src/vue-dev-proxy'
|
|
|
|
|
+
|
|
|
|
|
+export async function compileFile({ filename, code, compiled }: File) {
|
|
|
|
|
+ if (!code.trim()) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (filename.endsWith('.js')) {
|
|
|
|
|
+ compiled.js = compiled.ssr = code
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const id = await hashId(filename)
|
|
|
|
|
+ const { errors, descriptor } = parse(code, { filename, sourceMap: true })
|
|
|
|
|
+ if (errors.length) {
|
|
|
|
|
+ store.errors = errors
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ (descriptor.script && descriptor.script.lang) ||
|
|
|
|
|
+ (descriptor.scriptSetup && descriptor.scriptSetup.lang) ||
|
|
|
|
|
+ descriptor.styles.some(s => s.lang) ||
|
|
|
|
|
+ (descriptor.template && descriptor.template.lang)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ store.errors = [
|
|
|
|
|
+ 'lang="x" pre-processors are not supported in the in-browser playground.'
|
|
|
|
|
+ ]
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const hasScoped = descriptor.styles.some(s => s.scoped)
|
|
|
|
|
+ let clientCode = ''
|
|
|
|
|
+ let ssrCode = ''
|
|
|
|
|
+
|
|
|
|
|
+ const appendSharedCode = (code: string) => {
|
|
|
|
|
+ clientCode += code
|
|
|
|
|
+ ssrCode += code
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const clientScriptResult = doCompileScript(descriptor, id, false)
|
|
|
|
|
+ if (!clientScriptResult) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const [clientScript, bindings] = clientScriptResult
|
|
|
|
|
+ clientCode += clientScript
|
|
|
|
|
+
|
|
|
|
|
+ // script ssr only needs to be performed if using <script setup> where
|
|
|
|
|
+ // the render fn is inlined.
|
|
|
|
|
+ if (descriptor.scriptSetup) {
|
|
|
|
|
+ const ssrScriptResult = doCompileScript(descriptor, id, true)
|
|
|
|
|
+ if (!ssrScriptResult) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ ssrCode += ssrScriptResult[0]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // when no <script setup> is used, the script result will be identical.
|
|
|
|
|
+ ssrCode += clientScript
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // template
|
|
|
|
|
+ // only need dedicated compilation if not using <script setup>
|
|
|
|
|
+ if (descriptor.template && !descriptor.scriptSetup) {
|
|
|
|
|
+ const clientTemplateResult = doCompileTemplate(
|
|
|
|
|
+ descriptor,
|
|
|
|
|
+ id,
|
|
|
|
|
+ bindings,
|
|
|
|
|
+ false
|
|
|
|
|
+ )
|
|
|
|
|
+ if (!clientTemplateResult) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ clientCode += clientTemplateResult
|
|
|
|
|
+
|
|
|
|
|
+ const ssrTemplateResult = doCompileTemplate(descriptor, id, bindings, true)
|
|
|
|
|
+ if (!ssrTemplateResult) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ ssrCode += ssrTemplateResult
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (hasScoped) {
|
|
|
|
|
+ appendSharedCode(
|
|
|
|
|
+ `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (clientCode || ssrCode) {
|
|
|
|
|
+ appendSharedCode(
|
|
|
|
|
+ `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
|
|
|
|
|
+ `\nexport default ${COMP_IDENTIFIER}`
|
|
|
|
|
+ )
|
|
|
|
|
+ compiled.js = clientCode.trimStart()
|
|
|
|
|
+ compiled.ssr = ssrCode.trimStart()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // styles
|
|
|
|
|
+ let css = ''
|
|
|
|
|
+ for (const style of descriptor.styles) {
|
|
|
|
|
+ if (style.module) {
|
|
|
|
|
+ // TODO error
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const styleResult = await compileStyleAsync({
|
|
|
|
|
+ source: style.content,
|
|
|
|
|
+ filename,
|
|
|
|
|
+ id,
|
|
|
|
|
+ scoped: style.scoped,
|
|
|
|
|
+ modules: !!style.module
|
|
|
|
|
+ })
|
|
|
|
|
+ if (styleResult.errors.length) {
|
|
|
|
|
+ // postcss uses pathToFileURL which isn't polyfilled in the browser
|
|
|
|
|
+ // ignore these errors for now
|
|
|
|
|
+ if (!styleResult.errors[0].message.includes('pathToFileURL')) {
|
|
|
|
|
+ store.errors = styleResult.errors
|
|
|
|
|
+ }
|
|
|
|
|
+ // proceed even if css compile errors
|
|
|
|
|
+ } else {
|
|
|
|
|
+ css += styleResult.code + '\n'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (css) {
|
|
|
|
|
+ compiled.css = css.trim()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ compiled.css = '/* No <style> tags present */'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // clear errors
|
|
|
|
|
+ store.errors = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function doCompileScript(
|
|
|
|
|
+ descriptor: SFCDescriptor,
|
|
|
|
|
+ id: string,
|
|
|
|
|
+ ssr: boolean
|
|
|
|
|
+): [string, BindingMetadata | undefined] | undefined {
|
|
|
|
|
+ if (descriptor.script || descriptor.scriptSetup) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const compiledScript = compileScript(descriptor, {
|
|
|
|
|
+ id,
|
|
|
|
|
+ refSugar: true,
|
|
|
|
|
+ inlineTemplate: true,
|
|
|
|
|
+ templateOptions: {
|
|
|
|
|
+ ssr,
|
|
|
|
|
+ ssrCssVars: descriptor.cssVars
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ let code = ''
|
|
|
|
|
+ if (compiledScript.bindings) {
|
|
|
|
|
+ code += `\n/* Analyzed bindings: ${JSON.stringify(
|
|
|
|
|
+ compiledScript.bindings,
|
|
|
|
|
+ null,
|
|
|
|
|
+ 2
|
|
|
|
|
+ )} */`
|
|
|
|
|
+ }
|
|
|
|
|
+ code += `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
|
|
|
|
|
+ return [code, compiledScript.bindings]
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ store.errors = [e]
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function doCompileTemplate(
|
|
|
|
|
+ descriptor: SFCDescriptor,
|
|
|
|
|
+ id: string,
|
|
|
|
|
+ bindingMetadata: BindingMetadata | undefined,
|
|
|
|
|
+ ssr: boolean
|
|
|
|
|
+) {
|
|
|
|
|
+ const templateResult = compileTemplate({
|
|
|
|
|
+ source: descriptor.template!.content,
|
|
|
|
|
+ filename: descriptor.filename,
|
|
|
|
|
+ id,
|
|
|
|
|
+ scoped: descriptor.styles.some(s => s.scoped),
|
|
|
|
|
+ slotted: descriptor.slotted,
|
|
|
|
|
+ ssr,
|
|
|
|
|
+ ssrCssVars: descriptor.cssVars,
|
|
|
|
|
+ isProd: false,
|
|
|
|
|
+ compilerOptions: {
|
|
|
|
|
+ bindingMetadata
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ if (templateResult.errors.length) {
|
|
|
|
|
+ store.errors = templateResult.errors
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const fnName = ssr ? `ssrRender` : `render`
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ `\n${templateResult.code.replace(
|
|
|
|
|
+ /\nexport (function|const) (render|ssrRender)/,
|
|
|
|
|
+ `$1 ${fnName}`
|
|
|
|
|
+ )}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+}
|