From d1bf35c8b8127b2bb2f23ae4bb6772fd5f4c5f58 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 28 Mar 2021 18:41:33 -0400 Subject: [PATCH] workflow(sfc-playground): support multiple files --- packages/compiler-sfc/src/index.ts | 1 + packages/sfc-playground/src/App.vue | 2 + packages/sfc-playground/src/Message.vue | 2 + packages/sfc-playground/src/editor/Editor.vue | 31 ++- .../src/editor/FileSelector.vue | 118 ++++++++++ packages/sfc-playground/src/output/Output.vue | 17 +- .../sfc-playground/src/output/Preview.vue | 74 ++++--- .../src/output/moduleCompiler.ts | 207 ++++++++++++++++++ .../sfc-playground/src/output/srcdoc.html | 107 +++++---- packages/sfc-playground/src/store.ts | 154 ++++++++----- 10 files changed, 555 insertions(+), 158 deletions(-) create mode 100644 packages/sfc-playground/src/editor/FileSelector.vue create mode 100644 packages/sfc-playground/src/output/moduleCompiler.ts diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index a38d968a3..9acc94466 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -11,6 +11,7 @@ export { parse as babelParse } from '@babel/parser' export { walkIdentifiers } from './compileScript' import MagicString from 'magic-string' export { MagicString } +export { walk } from 'estree-walker' // Types export { diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue index e032f703a..f39ec3ab1 100644 --- a/packages/sfc-playground/src/App.vue +++ b/packages/sfc-playground/src/App.vue @@ -29,6 +29,8 @@ body { background-color: #f8f8f8; --nav-height: 50px; --font-code: 'Source Code Pro', monospace; + --color-branding: #3ca877; + --color-branding-dark: #416f9c; } .wrapper { diff --git a/packages/sfc-playground/src/Message.vue b/packages/sfc-playground/src/Message.vue index e2512b181..7cc239955 100644 --- a/packages/sfc-playground/src/Message.vue +++ b/packages/sfc-playground/src/Message.vue @@ -38,6 +38,8 @@ function formatMessage(err: string | Error): string { border-radius: 6px; font-family: var(--font-code); white-space: pre-wrap; + max-height: calc(100% - 50px); + overflow-y: scroll; } .msg.err { diff --git a/packages/sfc-playground/src/editor/Editor.vue b/packages/sfc-playground/src/editor/Editor.vue index 8be5d0fc5..86f4f6734 100644 --- a/packages/sfc-playground/src/editor/Editor.vue +++ b/packages/sfc-playground/src/editor/Editor.vue @@ -1,17 +1,40 @@ \ 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 + } +) + + + diff --git a/packages/sfc-playground/src/editor/FileSelector.vue b/packages/sfc-playground/src/editor/FileSelector.vue new file mode 100644 index 000000000..23c6d4f36 --- /dev/null +++ b/packages/sfc-playground/src/editor/FileSelector.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/sfc-playground/src/output/Output.vue b/packages/sfc-playground/src/output/Output.vue index 5116ea2f3..0027cdaed 100644 --- a/packages/sfc-playground/src/output/Output.vue +++ b/packages/sfc-playground/src/output/Output.vue @@ -4,12 +4,12 @@
- +
@@ -20,9 +20,9 @@ import CodeMirror from '../codemirror/CodeMirror.vue' 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('preview') @@ -35,14 +35,15 @@ const mode = ref('preview') .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; @@ -51,7 +52,7 @@ const mode = ref('preview') } button.active { - color: #42b983; - border-bottom: 3px solid #42b983; + color: var(--color-branding-dark); + border-bottom: 3px solid var(--color-branding-dark); } diff --git a/packages/sfc-playground/src/output/Preview.vue b/packages/sfc-playground/src/output/Preview.vue index 8ef3b1014..ef76c23f3 100644 --- a/packages/sfc-playground/src/output/Preview.vue +++ b/packages/sfc-playground/src/output/Preview.vue @@ -11,12 +11,11 @@ - - `.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 + 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: [] }) -const filename = 'Playground.vue' -const id = 'scope-id' -const compIdentifier = `__comp` +for (const file in store.files) { + if (file !== MAIN_FILE) { + compileFile(store.files[file]) + } +} -watchEffect(async () => { - const { code, compiled } = store +watchEffect(() => compileFile(store.activeFile)) +watchEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(store.files)) +}) + +export function setActive(filename: string) { + store.activeFilename = filename +} + +export function addFile(filename: string) { + store.files[filename] = new File(filename) + setActive(filename) +} + +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 @@ -84,20 +133,14 @@ watchEffect(async () => { 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 @@ -115,25 +158,25 @@ watchEffect(async () => { 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) { @@ -162,25 +205,18 @@ watchEffect(async () => { } if (css) { compiled.css = css.trim() - finalCode += `\ndocument.getElementById('__sfc-styles').innerHTML = ${JSON.stringify( - css - )}` } else { - compiled.css = '' + compiled.css = '/* No