]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
workflow: sfc playground
authorEvan You <yyx990803@gmail.com>
Sun, 28 Mar 2021 05:35:45 +0000 (01:35 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 28 Mar 2021 05:35:45 +0000 (01:35 -0400)
28 files changed:
.eslintrc.js
package.json
packages/compiler-sfc/package.json
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/compiler-sfc/src/warn.ts
packages/global.d.ts
packages/sfc-playground/index.html [new file with mode: 0644]
packages/sfc-playground/package.json [new file with mode: 0644]
packages/sfc-playground/src/App.vue [new file with mode: 0644]
packages/sfc-playground/src/Header.vue [new file with mode: 0644]
packages/sfc-playground/src/Message.vue [new file with mode: 0644]
packages/sfc-playground/src/SplitPane.vue [new file with mode: 0644]
packages/sfc-playground/src/codemirror/CodeMirror.vue [new file with mode: 0644]
packages/sfc-playground/src/codemirror/codemirror.css [new file with mode: 0644]
packages/sfc-playground/src/codemirror/codemirror.ts [new file with mode: 0644]
packages/sfc-playground/src/editor/Editor.vue [new file with mode: 0644]
packages/sfc-playground/src/main.ts [new file with mode: 0644]
packages/sfc-playground/src/output/Output.vue [new file with mode: 0644]
packages/sfc-playground/src/output/Preview.vue [new file with mode: 0644]
packages/sfc-playground/src/output/PreviewProxy.ts [new file with mode: 0644]
packages/sfc-playground/src/output/srcdoc.html [new file with mode: 0644]
packages/sfc-playground/src/store.ts [new file with mode: 0644]
packages/sfc-playground/src/utils.ts [new file with mode: 0644]
packages/sfc-playground/src/vue-dev-proxy.ts [new file with mode: 0644]
packages/sfc-playground/vite.config.ts [new file with mode: 0644]
rollup.config.js
yarn.lock

index 98f42a74b9a168d05d4ec44445fd9d05a19534e5..0732923e84baad4b0ea6f97707e824de7083a856 100644 (file)
@@ -56,7 +56,7 @@ module.exports = {
     },
     // Private package, browser only + no syntax restrictions
     {
-      files: ['packages/template-explorer/**'],
+      files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
       rules: {
         'no-restricted-globals': ['error', ...NodeGlobals],
         'no-restricted-syntax': 'off'
index 1e94678bb72f11ff98673aba9ec37ffa079713af..6483321009ef5b117cf64bc86537123cbabf4e25 100644 (file)
@@ -69,7 +69,7 @@
     "rollup": "~2.38.5",
     "rollup-plugin-node-builtins": "^2.1.2",
     "rollup-plugin-node-globals": "^1.4.0",
-    "rollup-plugin-node-polyfills": "^0.2.1",
+    "rollup-plugin-polyfill-node": "^0.6.2",
     "rollup-plugin-terser": "^7.0.2",
     "rollup-plugin-typescript2": "^0.27.2",
     "semver": "^7.3.2",
index ad30f3742005d19a9e7955e0ff554cea1b194fc5..229c2bea7574687212e2ad6635de8f0624c27da4 100644 (file)
@@ -3,6 +3,7 @@
   "version": "3.0.9",
   "description": "@vue/compiler-sfc",
   "main": "dist/compiler-sfc.cjs.js",
+  "module": "dist/compiler-sfc.esm-browser.js",
   "types": "dist/compiler-sfc.d.ts",
   "files": [
     "dist"
@@ -11,7 +12,7 @@
     "name": "VueCompilerSFC",
     "formats": [
       "cjs",
-      "global"
+      "esm-browser"
     ],
     "prod": false,
     "enableNonBrowserBranches": true
index 864fcbbbf7c68b87aa589cc3521c06e6f671f0e7..0cfe248958669b5d261a466e429a0392dac94835 100644 (file)
@@ -1367,7 +1367,7 @@ function markScopeIdentifier(
  * but with some subtle differences as this needs to handle a wider range of
  * possible syntax.
  */
-function walkIdentifiers(
+export function walkIdentifiers(
   root: Node,
   onIdentifier: (node: Identifier, parent: Node, parentStack: Node[]) => void
 ) {
index 2015dcb6d9c21a2ff287de52d9c56121e0669048..a38d968a39dc1fb9e1e3a3c66107ea0beb692e02 100644 (file)
@@ -6,6 +6,12 @@ export { compileScript } from './compileScript'
 export { rewriteDefault } from './rewriteDefault'
 export { generateCodeFrame } from '@vue/compiler-core'
 
+// Utilities
+export { parse as babelParse } from '@babel/parser'
+export { walkIdentifiers } from './compileScript'
+import MagicString from 'magic-string'
+export { MagicString }
+
 // Types
 export {
   SFCParseOptions,
index d53f376c847e83b6cbe52d25f8e733a38db6d8c2..dd9d3b5aa2e93e35a2dc75ad6656943116f360db 100644 (file)
@@ -16,6 +16,10 @@ export function warn(msg: string) {
 }
 
 export function warnExperimental(feature: string, rfcId: number) {
+  // eslint-disable-next-line
+  if (typeof window !== 'undefined') {
+    return
+  }
   warnOnce(
     `${feature} is still an experimental proposal.\n` +
       `Follow its status at https://github.com/vuejs/rfcs/pull/${rfcId}.`
index 2957d35acc338dcb658680511baa833588a61bb7..e6239ffd05fa2f549c2b7043ddff72d1c139573e 100644 (file)
@@ -22,3 +22,10 @@ declare namespace jest {
     toHaveBeenWarnedTimes(n: number): R
   }
 }
+
+declare module '*.vue' {
+
+}
+declare module '*?raw' {
+
+}
diff --git a/packages/sfc-playground/index.html b/packages/sfc-playground/index.html
new file mode 100644 (file)
index 0000000..15915f3
--- /dev/null
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Vue SFC Playground</title>
+
+  <link rel="preconnect" href="https://fonts.gstatic.com">
+  <link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
+
+  <!-- process shim for @vue/compiler-sfc dependency -->
+  <script>window.process = { env: {} }</script>
+  <script type="module" src="/src/main.ts"></script>
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>
\ No newline at end of file
diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json
new file mode 100644 (file)
index 0000000..5d3fed8
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "name": "@vue/sfc-playground",
+  "version": "3.0.9",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "serve": "vite preview"
+  },
+  "buildOptions": {
+    "formats": [
+      "global"
+    ],
+    "env": "development",
+    "enableNonBrowserBranches": true
+  },
+  "devDependencies": {
+    "@types/codemirror": "^0.0.108",
+    "@vitejs/plugin-vue": "^1.2.0",
+    "codemirror": "^5.60.0",
+    "vite": "^2.1.3"
+  }
+}
diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue
new file mode 100644 (file)
index 0000000..e032f70
--- /dev/null
@@ -0,0 +1,37 @@
+<template>
+  <Header />
+  <div class="wrapper">
+    <SplitPane>
+      <template #left>
+        <Editor />
+      </template>
+      <template #right>
+        <Output />
+      </template>
+    </SplitPane>
+  </div>
+</template>
+
+<script setup lang="ts">
+import Header from './Header.vue'
+import SplitPane from './SplitPane.vue'
+import Editor from './editor/Editor.vue'
+import Output from './output/Output.vue'
+</script>
+
+<style>
+body {
+  font-size: 13px;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+  color: #444;
+  margin: 0;
+  background-color: #f8f8f8;
+  --nav-height: 50px;
+  --font-code: 'Source Code Pro', monospace;
+}
+
+.wrapper {
+  height: calc(100vh - var(--nav-height));
+}
+</style>
\ No newline at end of file
diff --git a/packages/sfc-playground/src/Header.vue b/packages/sfc-playground/src/Header.vue
new file mode 100644 (file)
index 0000000..1a1ecc8
--- /dev/null
@@ -0,0 +1,23 @@
+<template>
+  <nav>
+    <h1>Vue SFC Playground</h1>
+  </nav>
+</template>
+
+<style>
+nav {
+  height: var(--nav-height);
+  box-sizing: border-box;
+  padding: 0 1em;
+  background-color: #fff;
+  box-shadow: 0 0 4px rgba(0, 0, 0, 0.33);
+  position: relative;
+  z-index: 999;
+}
+
+h1 {
+  margin: 0;
+  line-height: var(--nav-height);
+  font-weight: 500;
+}
+</style>
diff --git a/packages/sfc-playground/src/Message.vue b/packages/sfc-playground/src/Message.vue
new file mode 100644 (file)
index 0000000..e2512b1
--- /dev/null
@@ -0,0 +1,66 @@
+<template>
+  <Transition name="fade">
+    <pre v-if="err || warn"
+      class="msg"
+      :class="err ? 'err' : 'warn'">{{ formatMessage(err || warn) }}</pre>
+  </Transition>
+</template>
+
+<script setup lang="ts">
+import { defineProps } from 'vue'
+import type { CompilerError } from '@vue/compiler-sfc'
+
+defineProps(['err', 'warn'])
+
+function formatMessage(err: string | Error): string {
+  if (typeof err === 'string') {
+    return err
+  } else {
+    let msg = err.message
+    const loc = (err as CompilerError).loc
+    if (loc && loc.start) {
+      msg = `(${loc.start.line}:${loc.start.column}) ` + msg
+    }
+    return msg
+  }
+}
+</script>
+
+<style scoped>
+.msg {
+  position: absolute;
+  bottom: 0;
+  left: 8px;
+  right: 8px;
+  z-index: 10;
+  padding: 14px 20px;
+  border: 2px solid transparent;
+  border-radius: 6px;
+  font-family: var(--font-code);
+  white-space: pre-wrap;
+}
+
+.msg.err {
+  color: red;
+  border-color: red;
+  background-color: #ffd7d7;
+}
+
+.msg.warn {
+  --color: rgb(105, 95, 27);
+  color: var(--color);
+  border-color: var(--color);
+  background-color: rgb(247, 240, 205);
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: all 0.15s ease-out;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+  transform: translate(0, 10px);
+}
+</style>
diff --git a/packages/sfc-playground/src/SplitPane.vue b/packages/sfc-playground/src/SplitPane.vue
new file mode 100644 (file)
index 0000000..78a98db
--- /dev/null
@@ -0,0 +1,91 @@
+<template>
+  <div
+    ref="container"
+    class="split-pane"
+    :class="{ dragging: state.dragging }"
+    @mousemove="dragMove"
+    @mouseup="dragEnd"
+    @mouseleave="dragEnd"
+  >
+    <div class="left" :style="{ width: boundSplit() + '%' }">
+      <slot name="left" />
+      <div class="dragger" @mousedown.prevent="dragStart" />
+    </div>
+    <div class="right" :style="{ width: (100 - boundSplit()) + '%' }">
+      <slot name="right" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+
+const container = ref()
+
+const state = reactive({
+  dragging: false,
+  split: 50
+})
+
+function boundSplit() {
+  const { split } = state
+  return split < 20
+    ? 20
+    : split > 80
+      ? 80
+      : split
+}
+
+let startPosition = 0
+let startSplit = 0
+
+function dragStart(e: MouseEvent) {
+  state.dragging = true
+  startPosition = e.pageX
+  startSplit = boundSplit()
+}
+
+function dragMove(e: MouseEvent) {
+  if (state.dragging) {
+    const position = e.pageX
+    const totalSize = container.value.offsetWidth
+    const dp = position - startPosition
+    state.split = startSplit + ~~(dp / totalSize * 100)
+  }
+}
+
+function dragEnd() {
+  state.dragging = false
+}
+</script>
+
+<style scoped>
+.split-pane {
+  display: flex;
+  height: 100%;
+}
+.split-pane.dragging {
+  cursor: ew-resize;
+}
+.dragging .left,
+.dragging .right {
+  pointer-events: none;
+}
+.left,
+.right {
+  position: relative;
+  height: 100%;
+}
+.left {
+  border-right: 1px solid #ccc;
+}
+.dragger {
+  position: absolute;
+  z-index: 99;
+  top: 0;
+  bottom: 0;
+  right: -5px;
+  width: 10px;
+  cursor: ew-resize;
+}
+</style>
\ No newline at end of file
diff --git a/packages/sfc-playground/src/codemirror/CodeMirror.vue b/packages/sfc-playground/src/codemirror/CodeMirror.vue
new file mode 100644 (file)
index 0000000..18e3b3d
--- /dev/null
@@ -0,0 +1,76 @@
+<template>
+  <div class="editor" ref="el"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, defineProps, defineEmit, watchEffect } from 'vue'
+import { debounce } from '../utils'
+import CodeMirror from './codemirror'
+
+const el = ref()
+
+const props = defineProps({
+  mode: {
+    type: String,
+    default: 'htmlmixed'
+  },
+  value: {
+    type: String,
+    default: ''
+  },
+  readonly: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmit(['change'])
+
+onMounted(() => {
+  const addonOptions = {
+    autoCloseBrackets: true,
+    autoCloseTags: true,
+    foldGutter: true,
+    gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
+  }
+
+  const editor = CodeMirror(el.value!, {
+    value: '',
+    mode: props.mode,
+    readOnly: props.readonly,
+    tabSize: 2,
+    lineWrapping: true,
+    lineNumbers: true,
+    ...addonOptions
+  })
+
+  editor.on('change', () => {
+    emit('change', editor.getValue())
+  })
+
+  watchEffect(() => {
+    editor.setValue(props.value)
+  })
+  watchEffect(() => {
+    editor.setOption('mode', props.mode)
+  })
+
+  window.addEventListener('resize', debounce(() => {
+    editor.refresh()
+  }))
+})
+</script>
+
+<style>
+.editor {
+  position: relative;
+  height: 100%;
+  width: 100%;
+  overflow: hidden;
+}
+
+.CodeMirror {
+  font-family: "Source Code Pro", monospace;
+  height: 100%;
+}
+</style>
diff --git a/packages/sfc-playground/src/codemirror/codemirror.css b/packages/sfc-playground/src/codemirror/codemirror.css
new file mode 100644 (file)
index 0000000..01fa762
--- /dev/null
@@ -0,0 +1,506 @@
+/* BASICS */
+
+.CodeMirror {
+  --base: #545281;
+  --comment: hsl(210, 25%, 60%);
+  --keyword: #af4ab1;
+  --variable: #0055d1;
+  --function: #c25205;
+  --string: #2ba46d;
+  --number: #c25205;
+  --tags: #dd0000;
+  --qualifier: #ff6032;
+  --important: var(--string);
+
+  direction: ltr;
+  font-family: var(--font-code);
+  height: auto;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+  padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+  padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler,
+.CodeMirror-gutter-filler {
+  background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+  border-right: 1px solid #ddd;
+  background-color: transparent;
+  white-space: nowrap;
+}
+.CodeMirror-linenumber {
+  padding: 0 3px 0 5px;
+  min-width: 20px;
+  text-align: right;
+  color: var(--comment);
+  white-space: nowrap;
+  opacity: 0.6;
+}
+
+.CodeMirror-guttermarker {
+  color: black;
+}
+.CodeMirror-guttermarker-subtle {
+  color: #999;
+}
+
+/* FOLD GUTTER */
+
+.CodeMirror-foldmarker {
+  color: #414141;
+  text-shadow: #ff9966 1px 1px 2px, #ff9966 -1px -1px 2px, #ff9966 1px -1px 2px,
+    #ff9966 -1px 1px 2px;
+  font-family: arial;
+  line-height: 0.3;
+  cursor: pointer;
+}
+.CodeMirror-foldgutter {
+  width: 0.7em;
+}
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+  cursor: pointer;
+}
+.CodeMirror-foldgutter-open:after,
+.CodeMirror-foldgutter-folded:after {
+  content: '>';
+  font-size: 0.8em;
+  opacity: 0.8;
+  transition: transform 0.2s;
+  display: inline-block;
+  top: -0.1em;
+  position: relative;
+  transform: rotate(90deg);
+}
+.CodeMirror-foldgutter-folded:after {
+  transform: none;
+}
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+  border-left: 1px solid black;
+  border-right: none;
+  width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+  border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+  width: auto;
+  border: 0 !important;
+  background: #7e7;
+}
+.cm-fat-cursor div.CodeMirror-cursors {
+  z-index: 1;
+}
+.cm-fat-cursor-mark {
+  background-color: rgba(20, 255, 20, 0.5);
+  -webkit-animation: blink 1.06s steps(1) infinite;
+  -moz-animation: blink 1.06s steps(1) infinite;
+  animation: blink 1.06s steps(1) infinite;
+}
+.cm-animate-fat-cursor {
+  width: auto;
+  border: 0;
+  -webkit-animation: blink 1.06s steps(1) infinite;
+  -moz-animation: blink 1.06s steps(1) infinite;
+  animation: blink 1.06s steps(1) infinite;
+  background-color: #7e7;
+}
+@-moz-keyframes blink {
+  0% {
+  }
+  50% {
+    background-color: transparent;
+  }
+  100% {
+  }
+}
+@-webkit-keyframes blink {
+  0% {
+  }
+  50% {
+    background-color: transparent;
+  }
+  100% {
+  }
+}
+@keyframes blink {
+  0% {
+  }
+  50% {
+    background-color: transparent;
+  }
+  100% {
+  }
+}
+
+.cm-tab {
+  display: inline-block;
+  text-decoration: inherit;
+}
+
+.CodeMirror-rulers {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: -50px;
+  bottom: -20px;
+  overflow: hidden;
+}
+.CodeMirror-ruler {
+  border-left: 1px solid #ccc;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+}
+
+/* DEFAULT THEME */
+.cm-s-default.CodeMirror {
+  background-color: transparent;
+}
+.cm-s-default .cm-header {
+  color: blue;
+}
+.cm-s-default .cm-quote {
+  color: #090;
+}
+.cm-negative {
+  color: #d44;
+}
+.cm-positive {
+  color: #292;
+}
+.cm-header,
+.cm-strong {
+  font-weight: bold;
+}
+.cm-em {
+  font-style: italic;
+}
+.cm-link {
+  text-decoration: underline;
+}
+.cm-strikethrough {
+  text-decoration: line-through;
+}
+
+.cm-s-default .cm-atom,
+.cm-s-default .cm-def,
+.cm-s-default .cm-property,
+.cm-s-default .cm-variable-2,
+.cm-s-default .cm-variable-3,
+.cm-s-default .cm-punctuation {
+  color: var(--base);
+}
+.cm-s-default .cm-hr,
+.cm-s-default .cm-comment {
+  color: var(--comment);
+}
+.cm-s-default .cm-attribute,
+.cm-s-default .cm-keyword {
+  color: var(--keyword);
+}
+.cm-s-default .cm-variable {
+  color: var(--variable);
+}
+.cm-s-default .cm-bracket,
+.cm-s-default .cm-tag {
+  color: var(--tags);
+}
+.cm-s-default .cm-number {
+  color: var(--number);
+}
+.cm-s-default .cm-string,
+.cm-s-default .cm-string-2 {
+  color: var(--string);
+}
+.cm-s-default .cm-type {
+  color: #085;
+}
+.cm-s-default .cm-meta {
+  color: #555;
+}
+.cm-s-default .cm-qualifier {
+  color: var(--qualifier);
+}
+.cm-s-default .cm-builtin {
+  color: #7539ff;
+}
+.cm-s-default .cm-link {
+  color: var(--flash);
+}
+.cm-s-default .cm-error {
+  color: #ff008c;
+}
+.cm-invalidchar {
+  color: #ff008c;
+}
+
+.CodeMirror-composing {
+  border-bottom: 2px solid;
+}
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {
+  color: #0b0;
+}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {
+  color: #a22;
+}
+.CodeMirror-matchingtag {
+  background: rgba(255, 150, 0, 0.3);
+}
+.CodeMirror-activeline-background {
+  background: #e8f2ff;
+}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+   the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+  position: relative;
+  overflow: hidden;
+  background: white;
+}
+
+.CodeMirror-scroll {
+  overflow: scroll !important; /* Things will break if this is overridden */
+  /* 30px is the magic margin used to hide the element's real scrollbars */
+  /* See overflow: hidden in .CodeMirror */
+  margin-bottom: -30px;
+  margin-right: -30px;
+  padding-bottom: 30px;
+  height: 100%;
+  outline: none; /* Prevent dragging from highlighting the element */
+  position: relative;
+}
+.CodeMirror-sizer {
+  position: relative;
+  border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+   before actual scrolling happens, thus preventing shaking and
+   flickering artifacts. */
+.CodeMirror-vscrollbar,
+.CodeMirror-hscrollbar,
+.CodeMirror-scrollbar-filler,
+.CodeMirror-gutter-filler {
+  position: absolute;
+  z-index: 6;
+  display: none;
+}
+.CodeMirror-vscrollbar {
+  right: 0;
+  top: 0;
+  overflow-x: hidden;
+  overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+  bottom: 0;
+  left: 0;
+  overflow-y: hidden;
+  overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+  right: 0;
+  bottom: 0;
+}
+.CodeMirror-gutter-filler {
+  left: 0;
+  bottom: 0;
+}
+
+.CodeMirror-gutters {
+  position: absolute;
+  left: 0;
+  top: 0;
+  min-height: 100%;
+  z-index: 3;
+}
+.CodeMirror-gutter {
+  white-space: normal;
+  height: 100%;
+  display: inline-block;
+  vertical-align: top;
+  margin-bottom: -30px;
+}
+.CodeMirror-gutter-wrapper {
+  position: absolute;
+  z-index: 4;
+  background: none !important;
+  border: none !important;
+}
+.CodeMirror-gutter-background {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  z-index: 4;
+}
+.CodeMirror-gutter-elt {
+  position: absolute;
+  cursor: default;
+  z-index: 4;
+}
+.CodeMirror-gutter-wrapper ::selection {
+  background-color: transparent;
+}
+.CodeMirror-gutter-wrapper ::-moz-selection {
+  background-color: transparent;
+}
+
+.CodeMirror-lines {
+  cursor: text;
+  min-height: 1px; /* prevents collapsing before first draw */
+}
+.CodeMirror pre {
+  /* Reset some styles that the rest of the page might have set */
+  -moz-border-radius: 0;
+  -webkit-border-radius: 0;
+  border-radius: 0;
+  border-width: 0;
+  background: transparent;
+  font-family: inherit;
+  font-size: inherit;
+  margin: 0;
+  white-space: pre;
+  word-wrap: normal;
+  line-height: inherit;
+  color: inherit;
+  z-index: 2;
+  position: relative;
+  overflow: visible;
+  -webkit-tap-highlight-color: transparent;
+  -webkit-font-variant-ligatures: contextual;
+  font-variant-ligatures: contextual;
+}
+.CodeMirror-wrap pre {
+  word-wrap: break-word;
+  white-space: pre-wrap;
+  word-break: normal;
+}
+
+.CodeMirror-linebackground {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 0;
+}
+
+.CodeMirror-linewidget {
+  position: relative;
+  z-index: 2;
+  padding: 0.1px; /* Force widget margins to stay inside of the container */
+}
+
+.CodeMirror-rtl pre {
+  direction: rtl;
+}
+
+.CodeMirror-code {
+  outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+  position: absolute;
+  width: 100%;
+  height: 0;
+  overflow: hidden;
+  visibility: hidden;
+}
+
+.CodeMirror-cursor {
+  position: absolute;
+  pointer-events: none;
+}
+.CodeMirror-measure pre {
+  position: static;
+}
+
+div.CodeMirror-cursors {
+  visibility: hidden;
+  position: relative;
+  z-index: 3;
+}
+div.CodeMirror-dragcursors {
+  visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+  visibility: visible;
+}
+
+.CodeMirror-selected {
+  background: #d9d9d9;
+}
+.CodeMirror-focused .CodeMirror-selected {
+  background: #d7d4f0;
+}
+.CodeMirror-crosshair {
+  cursor: crosshair;
+}
+.CodeMirror-line::selection,
+.CodeMirror-line > span::selection,
+.CodeMirror-line > span > span::selection {
+  background: #d7d4f0;
+}
+.CodeMirror-line::-moz-selection,
+.CodeMirror-line > span::-moz-selection,
+.CodeMirror-line > span > span::-moz-selection {
+  background: #d7d4f0;
+}
+
+.cm-searching {
+  background-color: #ffa;
+  background-color: rgba(255, 255, 0, 0.4);
+}
+
+/* Used to force a border model for a node */
+.cm-force-border {
+  padding-right: 0.1px;
+}
+
+@media print {
+  /* Hide the cursor when printing */
+  .CodeMirror div.CodeMirror-cursors {
+    visibility: hidden;
+  }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack:after {
+  content: '';
+}
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext {
+  background: none;
+}
diff --git a/packages/sfc-playground/src/codemirror/codemirror.ts b/packages/sfc-playground/src/codemirror/codemirror.ts
new file mode 100644 (file)
index 0000000..0ae44dd
--- /dev/null
@@ -0,0 +1,19 @@
+import CodeMirror from 'codemirror'
+import './codemirror.css'
+
+// modes
+import 'codemirror/mode/javascript/javascript.js'
+import 'codemirror/mode/css/css.js'
+import 'codemirror/mode/htmlmixed/htmlmixed.js'
+
+// addons
+import 'codemirror/addon/edit/closebrackets.js'
+import 'codemirror/addon/edit/closetag.js'
+import 'codemirror/addon/comment/comment.js'
+import 'codemirror/addon/fold/foldcode.js'
+import 'codemirror/addon/fold/foldgutter.js'
+import 'codemirror/addon/fold/brace-fold.js'
+import 'codemirror/addon/fold/indent-fold.js'
+import 'codemirror/addon/fold/comment-fold.js'
+
+export default CodeMirror
diff --git a/packages/sfc-playground/src/editor/Editor.vue b/packages/sfc-playground/src/editor/Editor.vue
new file mode 100644 (file)
index 0000000..8be5d0f
--- /dev/null
@@ -0,0 +1,17 @@
+<template>
+    <CodeMirror @change="onChange" :value="initialCode" />
+    <Message :err="store.errors[0]" />
+</template>
+
+<script setup lang="ts">
+import CodeMirror from '../codemirror/CodeMirror.vue'
+import Message from '../Message.vue'
+import { store } from '../store'
+import { debounce } from '../utils'
+
+const onChange = debounce((code: string) => {
+  store.code = code
+}, 250)
+
+const initialCode = store.code
+</script>
\ No newline at end of file
diff --git a/packages/sfc-playground/src/main.ts b/packages/sfc-playground/src/main.ts
new file mode 100644 (file)
index 0000000..01433bc
--- /dev/null
@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/packages/sfc-playground/src/output/Output.vue b/packages/sfc-playground/src/output/Output.vue
new file mode 100644 (file)
index 0000000..5116ea2
--- /dev/null
@@ -0,0 +1,57 @@
+<template>
+  <div class="tab-buttons">
+    <button v-for="m of modes" :class="{ active: mode === m }" @click="mode = m">{{ m }}</button>
+  </div>
+
+  <div class="output-container">
+    <Preview v-if="mode === 'preview'" :code="store.compiled.executed" />
+    <CodeMirror
+      v-else
+      readonly
+      :mode="mode === 'css' ? 'css' : 'javascript'"
+      :value="store.compiled[mode]"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import Preview from './Preview.vue'
+import CodeMirror from '../codemirror/CodeMirror.vue'
+import { store } from '../store'
+import { ref } from 'vue'
+
+type Modes = 'preview' | 'executed' | 'js' | 'css' | 'template'
+
+const modes: Modes[] = ['preview', 'js', 'css', 'template', 'executed']
+const mode = ref<Modes>('preview')
+</script>
+
+<style scoped>
+.output-container {
+  height: calc(100% - 35px);
+  overflow: hidden;
+  position: relative;
+}
+.tab-buttons {
+  box-sizing: border-box;
+  border-bottom: 1px solid #ddd;
+}
+.tab-buttons button {
+  margin: 0;
+  font-size: 13px;
+  font-family: 'Source Code Pro', monospace;
+  border: none;
+  outline: none;
+  background-color: #f8f8f8;
+  padding: 8px 16px 6px;
+  text-transform: uppercase;
+  cursor: pointer;
+  color: #999;
+  box-sizing: border-box;
+}
+
+button.active {
+  color: #42b983;
+  border-bottom: 3px solid #42b983;
+}
+</style>
diff --git a/packages/sfc-playground/src/output/Preview.vue b/packages/sfc-playground/src/output/Preview.vue
new file mode 100644 (file)
index 0000000..8ef3b10
--- /dev/null
@@ -0,0 +1,108 @@
+<template>
+  <iframe
+    id="preview"
+    ref="iframe"
+    sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
+    :srcdoc="srcdoc"
+  ></iframe>
+  <Message :err="runtimeError" />
+  <Message v-if="!runtimeError" :warn="runtimeWarning" />
+</template>
+
+<script setup lang="ts">
+import Message from '../Message.vue'
+import { ref, onMounted, onUnmounted, watchEffect, defineProps } from 'vue'
+import srcdoc from './srcdoc.html?raw'
+import { PreviewProxy } from './PreviewProxy'
+import { sandboxVueURL } from '../store'
+
+const props = defineProps<{ code: string }>()
+
+const iframe = ref()
+const runtimeError = ref()
+const runtimeWarning = ref()
+
+let proxy: PreviewProxy
+
+async function updatePreview() {
+  if (!props.code?.trim()) {
+    return
+  }
+  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)
+  `)
+  } catch (e) {
+    runtimeError.value = e.message
+    return
+  }
+  runtimeError.value = null
+  runtimeWarning.value = null
+}
+
+onMounted(() => {
+  proxy = new PreviewProxy(iframe.value, {
+    on_fetch_progress: (progress: any) => {
+      // pending_imports = progress;
+    },
+    on_error: (event: any) => {
+      // push_logs({ level: 'error', args: [event.value] });
+      runtimeError.value = event.value
+    },
+    on_unhandled_rejection: (event: any) => {
+      let error = event.value
+      if (typeof error === 'string') error = { message: error }
+      runtimeError.value = 'Uncaught (in promise): ' + error.message
+    },
+    on_console: (log: any) => {
+      if (log.level === 'error') {
+        runtimeError.value = log.args.join('')
+      } else if (log.level === 'warn') {
+        if (log.args[0].toString().includes('[Vue warn]')) {
+          runtimeWarning.value = log.args.join('').replace(/\[Vue warn\]:/, '').trim()
+        }
+      }
+    },
+    on_console_group: (action: any) => {
+      // group_logs(action.label, false);
+    },
+    on_console_group_end: () => {
+      // ungroup_logs();
+    },
+    on_console_group_collapsed: (action: any) => {
+      // group_logs(action.label, true);
+    }
+  })
+
+  iframe.value.addEventListener('load', () => {
+    proxy.handle_links();
+    watchEffect(updatePreview)
+  });
+})
+
+onUnmounted(() => {
+  proxy.destroy()
+})
+</script>
+
+<style>
+iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+  background-color: #fff;
+}
+</style>
diff --git a/packages/sfc-playground/src/output/PreviewProxy.ts b/packages/sfc-playground/src/output/PreviewProxy.ts
new file mode 100644 (file)
index 0000000..338da5a
--- /dev/null
@@ -0,0 +1,96 @@
+// ReplProxy and srcdoc implementation from Svelte REPL
+// MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE
+
+let uid = 1
+
+export class PreviewProxy {
+  iframe: HTMLIFrameElement
+  handlers: Record<string, Function>
+  pending_cmds: Map<
+    number,
+    { resolve: (value: unknown) => void; reject: (reason?: any) => void }
+  >
+  handle_event: (e: any) => void
+
+  constructor(iframe: HTMLIFrameElement, handlers: Record<string, Function>) {
+    this.iframe = iframe
+    this.handlers = handlers
+
+    this.pending_cmds = new Map()
+
+    this.handle_event = e => this.handle_repl_message(e)
+    window.addEventListener('message', this.handle_event, false)
+  }
+
+  destroy() {
+    window.removeEventListener('message', this.handle_event)
+  }
+
+  iframe_command(action: string, args: any) {
+    return new Promise((resolve, reject) => {
+      const cmd_id = uid++
+
+      this.pending_cmds.set(cmd_id, { resolve, reject })
+
+      this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*')
+    })
+  }
+
+  handle_command_message(cmd_data: any) {
+    let action = cmd_data.action
+    let id = cmd_data.cmd_id
+    let handler = this.pending_cmds.get(id)
+
+    if (handler) {
+      this.pending_cmds.delete(id)
+      if (action === 'cmd_error') {
+        let { message, stack } = cmd_data
+        let e = new Error(message)
+        e.stack = stack
+        handler.reject(e)
+      }
+
+      if (action === 'cmd_ok') {
+        handler.resolve(cmd_data.args)
+      }
+    } else {
+      console.error('command not found', id, cmd_data, [
+        ...this.pending_cmds.keys()
+      ])
+    }
+  }
+
+  handle_repl_message(event: any) {
+    if (event.source !== this.iframe.contentWindow) return
+
+    const { action, args } = event.data
+
+    switch (action) {
+      case 'cmd_error':
+      case 'cmd_ok':
+        return this.handle_command_message(event.data)
+      case 'fetch_progress':
+        return this.handlers.on_fetch_progress(args.remaining)
+      case 'error':
+        return this.handlers.on_error(event.data)
+      case 'unhandledrejection':
+        return this.handlers.on_unhandled_rejection(event.data)
+      case 'console':
+        return this.handlers.on_console(event.data)
+      case 'console_group':
+        return this.handlers.on_console_group(event.data)
+      case 'console_group_collapsed':
+        return this.handlers.on_console_group_collapsed(event.data)
+      case 'console_group_end':
+        return this.handlers.on_console_group_end(event.data)
+    }
+  }
+
+  eval(script: string) {
+    return this.iframe_command('eval', { script })
+  }
+
+  handle_links() {
+    return this.iframe_command('catch_clicks', {})
+  }
+}
diff --git a/packages/sfc-playground/src/output/srcdoc.html b/packages/sfc-playground/src/output/srcdoc.html
new file mode 100644 (file)
index 0000000..7ef13dc
--- /dev/null
@@ -0,0 +1,201 @@
+<!doctype html>
+<html>
+       <head>
+               <style>
+                       body {
+                               font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+                               Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+                       }
+               </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);
+                                               }
+                                       }
+
+                                       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.addEventListener('message', handle_message, false);
+
+                               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);
+
+                       let previous = { level: null, args: null };
+
+                       ['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
+                               const original = console[level];
+                               console[level] = (...args) => {
+                                       if (String(args[0]).includes('You are running a development build of Vue')) {
+                                               return
+                                       }
+                                       const stringifiedArgs = stringify(args);
+                                       if (
+                                               previous.level === level &&
+                                               previous.args &&
+                                               previous.args === stringifiedArgs
+                                       ) {
+                                               parent.postMessage({ action: 'console', level, duplicate: true }, '*');
+                                       } else {
+                                               previous = { level, args: stringifiedArgs };
+
+                                               try {
+                                                       parent.postMessage({ action: 'console', level, args }, '*');
+                                               } catch (err) {
+                                                       parent.postMessage({ action: 'console', level: 'unclonable' }, '*');
+                                               }
+                                       }
+
+                                       original(...args);
+                               }
+                       });
+
+                       [
+                               { method: 'group', action: 'console_group' },
+                               { method: 'groupEnd', action: 'console_group_end' },
+                               { method: 'groupCollapsed', action: 'console_group_collapsed' },
+                       ].forEach((group_action) => {
+                               const original = console[group_action.method];
+                               console[group_action.method] = (label) => {
+                                       parent.postMessage({ action: group_action.action, label }, '*');
+
+                                       original(label);
+                               };
+                       });
+
+                       const timers = new Map();
+                       const original_time = console.time;
+                       const original_timelog = console.timeLog;
+                       const original_timeend = console.timeEnd;
+
+                       console.time = (label = 'default') => {
+                               original_time(label);
+                               timers.set(label, performance.now());
+                       }
+                       console.timeLog = (label = 'default') => {
+                               original_timelog(label);
+                               const now = performance.now();
+                               if (timers.has(label)) {
+                                       parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*');
+                               } else {
+                                       parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*');
+                               }
+                       }
+                       console.timeEnd = (label = 'default') => {
+                               original_timeend(label);
+                               const now = performance.now();
+                               if (timers.has(label)) {
+                                       parent.postMessage({ action: 'console', level: 'system-log', args: [`${label}: ${now - timers.get(label)}ms`] }, '*');
+                               } else {
+                                       parent.postMessage({ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] }, '*');
+                               }
+                               timers.delete(label);
+                       };
+
+                       const original_assert = console.assert;
+                       console.assert = (condition, ...args) => {
+                               if (condition) {
+                                       const stack = new Error().stack;
+                                       parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*');
+                               }
+                               original_assert(condition, ...args);
+                       };
+
+                       const counter = new Map();
+                       const original_count = console.count;
+                       const original_countreset = console.countReset;
+
+                       console.count = (label = 'default') => {
+                               counter.set(label, (counter.get(label) || 0) + 1);
+                               parent.postMessage({ action: 'console', level: 'system-log', args: `${label}: ${counter.get(label)}` }, '*');
+                               original_count(label);
+                       };
+
+                       console.countReset = (label = 'default') => {
+                               if (counter.has(label)) {
+                                       counter.set(label, 0);
+                               } else {
+                                       parent.postMessage({ action: 'console', level: 'system-warn', args: `Count for '${label}' does not exist` }, '*');
+                               }
+                               original_countreset(label);
+                       };
+
+                       const original_trace = console.trace;
+
+                       console.trace = (...args) => {
+                               const stack = new Error().stack;
+                               parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*');
+                               original_trace(...args);
+                       };
+
+                       function stringify(args) {
+                               try {
+                                       return JSON.stringify(args);
+                               } catch (error) {
+                                       return null;
+                               }
+                       }
+               </script>
+       </head>
+       <body>
+    <div id="app"></div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/packages/sfc-playground/src/store.ts b/packages/sfc-playground/src/store.ts
new file mode 100644 (file)
index 0000000..4774149
--- /dev/null
@@ -0,0 +1,168 @@
+import { reactive, watchEffect } from 'vue'
+import {
+  parse,
+  compileTemplate,
+  compileStyleAsync,
+  compileScript,
+  rewriteDefault,
+  CompilerError
+} from '@vue/compiler-sfc'
+
+const storeKey = 'sfc-code'
+const saved = localStorage.getItem(storeKey) || ''
+
+// @ts-ignore
+export const sandboxVueURL = 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: '',
+    js: '',
+    css: '',
+    template: ''
+  },
+  errors: [] as (string | CompilerError | SyntaxError)[]
+})
+
+const filename = 'Playground.vue'
+const id = 'scope-id'
+const compIdentifier = `__comp`
+
+watchEffect(async () => {
+  const { code, compiled } = store
+  if (!code.trim()) {
+    return
+  }
+
+  localStorage.setItem(storeKey, code)
+
+  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
+  if (descriptor.script || descriptor.scriptSetup) {
+    try {
+      const compiledScript = compileScript(descriptor, {
+        id,
+        refSugar: true,
+        inlineTemplate: true
+      })
+      compiled.js = compiledScript.content.trim()
+      finalCode +=
+        `\n` +
+        rewriteDefault(
+          rewriteVueImports(compiledScript.content),
+          compIdentifier
+        )
+    } catch (e) {
+      store.errors = [e]
+      return
+    }
+  } else {
+    compiled.js = ''
+    finalCode += `\nconst ${compIdentifier} = {}`
+  }
+
+  // template
+  if (descriptor.template && !descriptor.scriptSetup) {
+    const templateResult = compileTemplate({
+      source: descriptor.template.content,
+      filename,
+      id,
+      scoped: hasScoped,
+      slotted: descriptor.slotted,
+      isProd: false
+    })
+    if (templateResult.errors.length) {
+      store.errors = templateResult.errors
+      return
+    }
+
+    compiled.template = templateResult.code.trim()
+    finalCode += rewriteVueImports(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 */'
+  }
+  if (hasScoped) {
+    finalCode += `\n${compIdentifier}.__scopeId = ${JSON.stringify(
+      `data-v-${id}`
+    )}`
+  }
+
+  // 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()
+    finalCode += `\ndocument.getElementById('__sfc-styles').innerHTML = ${JSON.stringify(
+      css
+    )}`
+  } else {
+    compiled.css = ''
+  }
+
+  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}"`
+  )
+}
diff --git a/packages/sfc-playground/src/utils.ts b/packages/sfc-playground/src/utils.ts
new file mode 100644 (file)
index 0000000..c5f1de1
--- /dev/null
@@ -0,0 +1,9 @@
+export function debounce(fn: Function, n = 100) {
+  let handle: any
+  return (...args: any[]) => {
+    if (handle) clearTimeout(handle)
+    handle = setTimeout(() => {
+      fn(...args)
+    }, n)
+  }
+}
diff --git a/packages/sfc-playground/src/vue-dev-proxy.ts b/packages/sfc-playground/src/vue-dev-proxy.ts
new file mode 100644 (file)
index 0000000..f254416
--- /dev/null
@@ -0,0 +1,2 @@
+// serve vue to the iframe sandbox during dev.
+export * from 'vue'
diff --git a/packages/sfc-playground/vite.config.ts b/packages/sfc-playground/vite.config.ts
new file mode 100644 (file)
index 0000000..1e4c047
--- /dev/null
@@ -0,0 +1,34 @@
+import fs from 'fs'
+import path from 'path'
+import { defineConfig, Plugin } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue(), copyVuePlugin()],
+  optimizeDeps: {
+    exclude: ['consolidate']
+  }
+})
+
+function copyVuePlugin(): Plugin {
+  return {
+    name: 'copy-vue',
+    generateBundle(_opts, bundle) {
+      const filePath = path.resolve(
+        __dirname,
+        '../vue/dist/vue.runtime.esm-browser.js'
+      )
+      if (!fs.existsSync(filePath)) {
+        throw new Error(
+          `vue.runtime.esm-browser.js not built. ` +
+            `Run "yarn build vue -f esm-browser" first.`
+        )
+      }
+      this.emitFile({
+        type: 'asset',
+        fileName: 'vue.runtime.esm-browser.js',
+        source: fs.readFileSync(filePath, 'utf-8')
+      })
+    }
+  }
+}
index dead7117be841d2465c9c489d8f14c3a8b4cddb8..e8fe8d8860176fbe01c75b8dfafc342c32e6941c 100644 (file)
@@ -142,7 +142,7 @@ function createConfig(format, output, plugins = []) {
           require('@rollup/plugin-commonjs')({
             sourceMap: false
           }),
-          require('rollup-plugin-node-polyfills')(),
+          require('rollup-plugin-polyfill-node')(),
           require('@rollup/plugin-node-resolve').nodeResolve()
         ]
       : []
index 202d012453d2344ab64206383cbce13cdb41218a..98966e2ae4a282694b833480f213bf2405708ec1 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     magic-string "^0.25.7"
     resolve "^1.17.0"
 
+"@rollup/plugin-inject@^4.0.0":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-4.0.2.tgz#55b21bb244a07675f7fdde577db929c82fc17395"
+  integrity sha512-TSLMA8waJ7Dmgmoc8JfPnwUwVZgLjjIAM6MqeIFqPO2ODK36JqE0Cf2F54UTgCUuW8da93Mvoj75a6KAVWgylw==
+  dependencies:
+    "@rollup/pluginutils" "^3.0.4"
+    estree-walker "^1.0.1"
+    magic-string "^0.25.5"
+
 "@rollup/plugin-json@^4.0.0":
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
     "@rollup/pluginutils" "^3.1.0"
     magic-string "^0.25.7"
 
-"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
+"@rollup/pluginutils@^3.0.4", "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
   integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
   resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.33.tgz#d79c020f283bd50bd76101d7d300313c107325fc"
   integrity sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ==
 
+"@types/codemirror@^0.0.108":
+  version "0.0.108"
+  resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.108.tgz#e640422b666bf49251b384c390cdeb2362585bde"
+  integrity sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==
+  dependencies:
+    "@types/tern" "*"
+
 "@types/consolidate@^0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@types/consolidate/-/consolidate-0.14.0.tgz#856735b3a1421513bd12b9bdd588923bec773bff"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
   integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
 
+"@types/tern@*":
+  version "0.23.3"
+  resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
+  integrity sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==
+  dependencies:
+    "@types/estree" "*"
+
 "@types/yargs-parser@*":
   version "20.2.0"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
     "@typescript-eslint/types" "4.19.0"
     eslint-visitor-keys "^2.0.0"
 
+"@vitejs/plugin-vue@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-1.2.0.tgz#f0a92470b74761f90afc8cda204fa3bec9df09f4"
+  integrity sha512-IhSJfJH6IDNEAnhr91+2vhLLe/1SqkA/2BP19jwtn54DGI+cNbZIxiPhHIdKUpdRo0QwErOh6Jy1Maxk2uVo7A==
+
 "@zeit/schemas@2.6.0":
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.6.0.tgz#004e8e553b4cd53d538bd38eac7bcbf58a867fe3"
@@ -1696,6 +1724,11 @@ co@^4.6.0:
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
   integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
 
+codemirror@^5.60.0:
+  version "5.60.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.60.0.tgz#00a8cfd287d5d8737ceb73987f04aee2fe5860da"
+  integrity sha512-AEL7LhFOlxPlCL8IdTcJDblJm8yrAGib7I+DErJPdZd4l6imx8IMgKK3RblVgBQqz3TZJR4oknQ03bz+uNjBYA==
+
 collect-v8-coverage@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@@ -1733,7 +1766,7 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-colorette@^1.2.1:
+colorette@^1.2.1, colorette@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@@ -2419,6 +2452,11 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+esbuild@^0.9.3:
+  version "0.9.7"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.9.7.tgz#ea0d639cbe4b88ec25fbed4d6ff00c8d788ef70b"
+  integrity sha512-VtUf6aQ89VTmMLKrWHYG50uByMF4JQlVysb8dmg6cOgW8JnFCipmz7p+HNBl+RR3LLCuBxFGVauAe2wfnF9bLg==
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4617,7 +4655,7 @@ magic-string@^0.22.5:
   dependencies:
     vlq "^0.2.2"
 
-magic-string@^0.25.3, magic-string@^0.25.7:
+magic-string@^0.25.5, magic-string@^0.25.7:
   version "0.25.7"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
   integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
@@ -5460,6 +5498,15 @@ postcss@^8.1.10:
     nanoid "^3.1.20"
     source-map "^0.6.1"
 
+postcss@^8.2.1:
+  version "8.2.8"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
+  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.20"
+    source-map "^0.6.1"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -6053,15 +6100,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
-rollup-plugin-inject@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4"
-  integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==
-  dependencies:
-    estree-walker "^0.6.1"
-    magic-string "^0.25.3"
-    rollup-pluginutils "^2.8.1"
-
 rollup-plugin-node-builtins@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/rollup-plugin-node-builtins/-/rollup-plugin-node-builtins-2.1.2.tgz#24a1fed4a43257b6b64371d8abc6ce1ab14597e9"
@@ -6084,12 +6122,12 @@ rollup-plugin-node-globals@^1.4.0:
     process-es6 "^0.11.6"
     rollup-pluginutils "^2.3.1"
 
-rollup-plugin-node-polyfills@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd"
-  integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==
+rollup-plugin-polyfill-node@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.6.2.tgz#dea62e00f5cc2c174e4b4654b5daab79b1a92fc3"
+  integrity sha512-gMCVuR0zsKq0jdBn8pSXN1Ejsc458k2QsFFvQdbHoM0Pot5hEnck+pBP/FDwFS6uAi77pD3rDTytsaUStsOMlA==
   dependencies:
-    rollup-plugin-inject "^3.0.0"
+    "@rollup/plugin-inject" "^4.0.0"
 
 rollup-plugin-terser@^7.0.2:
   version "7.0.2"
@@ -6112,13 +6150,20 @@ rollup-plugin-typescript2@^0.27.2:
     resolve "1.17.0"
     tslib "2.0.1"
 
-rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.8.1:
+rollup-pluginutils@^2.3.1:
   version "2.8.2"
   resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
   integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
   dependencies:
     estree-walker "^0.6.1"
 
+rollup@^2.38.5:
+  version "2.43.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.43.0.tgz#05d1ed0bbb37080a63e68c530a84d34f61ceb56a"
+  integrity sha512-FRsYGqlo1iF/w3bv319iStAK0hyhhwon35Cbo7sGUoXaOpsZFy6Lel7UoGb5bNDE4OsoWjMH94WiVvpOM26l3g==
+  optionalDependencies:
+    fsevents "~2.3.1"
+
 rollup@~2.38.5:
   version "2.38.5"
   resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.38.5.tgz#be41ad4fe0c103a8794377afceb5f22b8f603d6a"
@@ -7141,6 +7186,18 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+vite@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-2.1.3.tgz#a31a844d26d3846b5a78f06970d1ea1f8a442955"
+  integrity sha512-bUzArZIUwADVJS/3ywCr4KKFn3a7izs4M87ZDlAlY2V34E4g1kH6p3sVNAh8/IXCn/56fwgMh3rRavPUW7qEQQ==
+  dependencies:
+    esbuild "^0.9.3"
+    postcss "^8.2.1"
+    resolve "^1.19.0"
+    rollup "^2.38.5"
+  optionalDependencies:
+    fsevents "~2.3.1"
+
 vlq@^0.2.2:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"