diff --git a/packages/sfc-playground/src/output/Output.vue b/packages/sfc-playground/src/output/Output.vue index 87b18909..66ee51bb 100644 --- a/packages/sfc-playground/src/output/Output.vue +++ b/packages/sfc-playground/src/output/Output.vue @@ -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('preview') diff --git a/packages/sfc-playground/src/output/Preview.vue b/packages/sfc-playground/src/output/Preview.vue index 0c3feac2..230025a5 100644 --- a/packages/sfc-playground/src/output/Preview.vue +++ b/packages/sfc-playground/src/output/Preview.vue @@ -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() diff --git a/packages/sfc-playground/src/output/moduleCompiler.ts b/packages/sfc-playground/src/output/moduleCompiler.ts index 31f6bc06..ea22d6b4 100644 --- a/packages/sfc-playground/src/output/moduleCompiler.ts +++ b/packages/sfc-playground/src/output/moduleCompiler.ts @@ -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 index 00000000..e0ab8d2f --- /dev/null +++ b/packages/sfc-playground/src/sfcCompiler.ts @@ -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 `.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