]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
workflow(sfc-playground): add ssr compile output
authorEvan You <yyx990803@gmail.com>
Tue, 30 Mar 2021 16:36:59 +0000 (12:36 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 30 Mar 2021 16:36:59 +0000 (12:36 -0400)
packages/sfc-playground/src/output/Output.vue
packages/sfc-playground/src/output/Preview.vue
packages/sfc-playground/src/output/moduleCompiler.ts
packages/sfc-playground/src/sfcCompiler.ts [new file with mode: 0644]
packages/sfc-playground/src/store.ts

index 87b189094f2b62e35fc1210335e642ec25bc7abc..66ee51bb0069c48963913d364a9beb0c64204cfa 100644 (file)
@@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue'
 import { store } from '../store'
 import { ref } from 'vue'
 
-type Modes = 'preview' | 'js' | 'css'
+type Modes = 'preview' | 'js' | 'css' | 'ssr'
 
-const modes: Modes[] = ['preview', 'js', 'css']
+const modes: Modes[] = ['preview', 'js', 'css', 'ssr']
 const mode = ref<Modes>('preview')
 </script>
 
index 0c3feac2fdc8b0f9e2003f027587ee1380537618..230025a5f4d0064e66a198d919439b115c28894e 100644 (file)
@@ -14,7 +14,7 @@ import Message from '../Message.vue'
 import { ref, onMounted, onUnmounted, watchEffect } from 'vue'
 import srcdoc from './srcdoc.html?raw'
 import { PreviewProxy } from './PreviewProxy'
-import { MAIN_FILE, SANDBOX_VUE_URL } from '../store'
+import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
 import { compileModulesForPreview } from './moduleCompiler'
 
 const iframe = ref()
index 31f6bc067997f360a243e9fe9ef1065db9cc242b..ea22d6b462ba22bc98c7082a41431cf2daf3a9e2 100644 (file)
@@ -1,4 +1,5 @@
-import { store, MAIN_FILE, SANDBOX_VUE_URL, File } from '../store'
+import { store, File } from '../store'
+import { MAIN_FILE, SANDBOX_VUE_URL } from '../sfcCompiler'
 import {
   babelParse,
   MagicString,
diff --git a/packages/sfc-playground/src/sfcCompiler.ts b/packages/sfc-playground/src/sfcCompiler.ts
new file mode 100644 (file)
index 0000000..e0ab8d2
--- /dev/null
@@ -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)
+}
index 2f38287c5ed542627d93a34d2450b5456975673f..320f6cd304aa0f85e9092826d8d0472f8f4f0041 100644 (file)
@@ -1,11 +1,5 @@
 import { reactive, watchEffect } from 'vue'
-import {
-  parse,
-  compileTemplate,
-  compileStyleAsync,
-  compileScript,
-  rewriteDefault
-} from '@vue/compiler-sfc'
+import { compileFile, MAIN_FILE } from './sfcCompiler'
 
 const welcomeCode = `
 <template>
@@ -17,20 +11,13 @@ const msg = 'Hello World!'
 </script>
 `.trim()
 
-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 class File {
   filename: string
   code: string
   compiled = {
     js: '',
-    css: ''
+    css: '',
+    ssr: ''
   }
 
   constructor(filename: string, code = '') {
@@ -106,143 +93,3 @@ export function deleteFile(filename: string) {
     delete store.files[filename]
   }
 }
-
-async function compileFile({ filename, code, compiled }: File) {
-  if (!code.trim()) {
-    return
-  }
-
-  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
-    return
-  }
-
-  const hasScoped = descriptor.styles.some(s => s.scoped)
-  let finalCode = ''
-
-  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
-  }
-
-  // script
-  let compiledScript
-  if (descriptor.script || descriptor.scriptSetup) {
-    try {
-      compiledScript = compileScript(descriptor, {
-        id,
-        refSugar: true,
-        inlineTemplate: true
-      })
-      if (compiledScript.bindings) {
-        finalCode += `\n/* Analyzed bindings: ${JSON.stringify(
-          compiledScript.bindings,
-          null,
-          2
-        )} */`
-      }
-      finalCode +=
-        `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
-    } catch (e) {
-      store.errors = [e]
-      return
-    }
-  } else {
-    finalCode += `\nconst ${COMP_IDENTIFIER} = {}`
-  }
-
-  // template
-  if (descriptor.template && !descriptor.scriptSetup) {
-    const templateResult = compileTemplate({
-      source: descriptor.template.content,
-      filename,
-      id,
-      scoped: hasScoped,
-      slotted: descriptor.slotted,
-      isProd: false,
-      compilerOptions: {
-        bindingMetadata: compiledScript && compiledScript.bindings
-      }
-    })
-    if (templateResult.errors.length) {
-      store.errors = templateResult.errors
-      return
-    }
-
-    finalCode +=
-      `\n` +
-      templateResult.code.replace(
-        /\nexport (function|const) render/,
-        '$1 render'
-      )
-    finalCode += `\n${COMP_IDENTIFIER}.render = render`
-  }
-  if (hasScoped) {
-    finalCode += `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(
-      `data-v-${id}`
-    )}`
-  }
-
-  if (finalCode) {
-    finalCode += `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}`
-    finalCode += `\nexport default ${COMP_IDENTIFIER}`
-    compiled.js = finalCode.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 = []
-}
-
-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)
-}