App.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <script setup lang="ts">
  2. import Header from './Header.vue'
  3. import {
  4. Repl,
  5. type SFCOptions,
  6. useStore,
  7. useVueImportMap,
  8. mergeImportMap,
  9. File,
  10. StoreState,
  11. ImportMap,
  12. } from '@vue/repl'
  13. import Monaco from '@vue/repl/monaco-editor'
  14. import { ref, watchEffect, onMounted, computed, watch } from 'vue'
  15. import welcomeSFC from './welcome.vue?raw'
  16. const replRef = ref<InstanceType<typeof Repl>>()
  17. const setVH = () => {
  18. document.documentElement.style.setProperty('--vh', window.innerHeight + `px`)
  19. }
  20. window.addEventListener('resize', setVH)
  21. setVH()
  22. const useSSRMode = ref(false)
  23. const useVaporMode = ref(true)
  24. const AUTO_SAVE_STORAGE_KEY = 'vue-sfc-playground-auto-save'
  25. const initAutoSave: boolean = JSON.parse(
  26. localStorage.getItem(AUTO_SAVE_STORAGE_KEY) ?? 'true',
  27. )
  28. const autoSave = ref(initAutoSave)
  29. const {
  30. vueVersion,
  31. productionMode,
  32. importMap: vueImportMap,
  33. } = useVueImportMap({
  34. runtimeDev: import.meta.env.PROD
  35. ? `${location.origin}/vue.runtime.esm-browser.js`
  36. : `${location.origin}/src/vue-dev-proxy`,
  37. runtimeProd: import.meta.env.PROD
  38. ? `${location.origin}/vue.runtime.esm-browser.prod.js`
  39. : `${location.origin}/src/vue-dev-proxy-prod`,
  40. serverRenderer: import.meta.env.PROD
  41. ? `${location.origin}/server-renderer.esm-browser.js`
  42. : `${location.origin}/src/vue-server-renderer-dev-proxy`,
  43. })
  44. const importMap = computed(() => {
  45. const vapor = import.meta.env.PROD
  46. ? `${location.origin}/vue-vapor.esm-browser.js`
  47. : `${location.origin}/src/vue-vapor-dev-proxy`
  48. const vaporImportMap: ImportMap = {
  49. imports: {
  50. 'vue/vapor': vapor,
  51. },
  52. }
  53. if (useVaporMode.value) vaporImportMap.imports!.vue = vapor
  54. return mergeImportMap(vueImportMap.value, vaporImportMap)
  55. })
  56. let hash = location.hash.slice(1)
  57. if (hash.startsWith('__DEV__')) {
  58. hash = hash.slice(7)
  59. productionMode.value = false
  60. }
  61. if (hash.startsWith('__PROD__')) {
  62. hash = hash.slice(8)
  63. productionMode.value = true
  64. }
  65. if (hash.startsWith('__SSR__')) {
  66. hash = hash.slice(7)
  67. useSSRMode.value = true
  68. }
  69. if (hash.startsWith('__VAPOR__')) {
  70. hash = hash.slice(9)
  71. useVaporMode.value = true
  72. }
  73. const files: StoreState['files'] = ref(Object.create(null))
  74. // enable experimental features
  75. const sfcOptions = computed(
  76. (): SFCOptions => ({
  77. script: {
  78. inlineTemplate: productionMode.value,
  79. isProd: productionMode.value,
  80. propsDestructure: true,
  81. vapor: useVaporMode.value,
  82. },
  83. style: {
  84. isProd: productionMode.value,
  85. },
  86. template: {
  87. vapor: useVaporMode.value,
  88. isProd: productionMode.value,
  89. compilerOptions: {
  90. isCustomElement: (tag: string) =>
  91. tag === 'mjx-container' || tag.startsWith('custom-'),
  92. },
  93. },
  94. }),
  95. )
  96. const store = useStore(
  97. {
  98. files,
  99. vueVersion,
  100. builtinImportMap: importMap,
  101. sfcOptions,
  102. template: ref({
  103. welcomeSFC: welcomeSFC,
  104. }),
  105. },
  106. hash,
  107. )
  108. // @ts-expect-error
  109. globalThis.store = store
  110. watch(
  111. useVaporMode,
  112. () => {
  113. if (useVaporMode.value) {
  114. files.value['src/index.html'] = new File(
  115. 'src/index.html',
  116. `<script type="module">
  117. import { createVaporApp } from 'vue/vapor'
  118. import App from './App.vue'
  119. createVaporApp(App).mount('#app')` +
  120. '<' +
  121. '/script>' +
  122. `<div id="app"></div>`,
  123. true,
  124. )
  125. store.mainFile = 'src/index.html'
  126. } else if (files.value['src/index.html']?.hidden) {
  127. delete files.value['src/index.html']
  128. store.mainFile = 'src/App.vue'
  129. if (store.activeFile.filename === 'src/index.html') {
  130. store.activeFile = files.value['src/App.vue']
  131. }
  132. }
  133. },
  134. { immediate: true },
  135. )
  136. // persist state
  137. watchEffect(() => {
  138. const newHash = store
  139. .serialize()
  140. .replace(/^#/, useVaporMode.value ? `#__VAPOR__` : `#`)
  141. .replace(/^#/, useSSRMode.value ? `#__SSR__` : `#`)
  142. .replace(/^#/, productionMode.value ? `#__PROD__` : `#`)
  143. history.replaceState({}, '', newHash)
  144. })
  145. function toggleProdMode() {
  146. productionMode.value = !productionMode.value
  147. }
  148. function toggleSSR() {
  149. useSSRMode.value = !useSSRMode.value
  150. }
  151. function toggleVapor() {
  152. useVaporMode.value = !useVaporMode.value
  153. }
  154. function toggleAutoSave() {
  155. autoSave.value = !autoSave.value
  156. localStorage.setItem(AUTO_SAVE_STORAGE_KEY, String(autoSave.value))
  157. }
  158. function reloadPage() {
  159. replRef.value?.reload()
  160. }
  161. const theme = ref<'dark' | 'light'>('dark')
  162. function toggleTheme(isDark: boolean) {
  163. theme.value = isDark ? 'dark' : 'light'
  164. }
  165. onMounted(() => {
  166. const cls = document.documentElement.classList
  167. toggleTheme(cls.contains('dark'))
  168. // @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency
  169. window.process = { env: {} }
  170. })
  171. </script>
  172. <template>
  173. <Header
  174. :store="store"
  175. :prod="productionMode"
  176. :ssr="useSSRMode"
  177. :vapor="useVaporMode"
  178. :autoSave="autoSave"
  179. @toggle-theme="toggleTheme"
  180. @toggle-prod="toggleProdMode"
  181. @toggle-ssr="toggleSSR"
  182. @toggle-autosave="toggleAutoSave"
  183. @toggle-vapor="toggleVapor"
  184. @reload-page="reloadPage"
  185. />
  186. <Repl
  187. ref="replRef"
  188. :theme="theme"
  189. :editor="Monaco"
  190. @keydown.ctrl.s.prevent
  191. @keydown.meta.s.prevent
  192. :ssr="useSSRMode"
  193. :autoSave="autoSave"
  194. :store="store"
  195. :showCompileOutput="true"
  196. :autoResize="true"
  197. :clearConsole="false"
  198. :preview-options="{
  199. customCode: {
  200. importCode: `import { initCustomFormatter } from 'vue'`,
  201. useCode: `if (window.devtoolsFormatters) {
  202. const index = window.devtoolsFormatters.findIndex((v) => v.__vue_custom_formatter)
  203. window.devtoolsFormatters.splice(index, 1)
  204. initCustomFormatter()
  205. } else {
  206. initCustomFormatter()
  207. }`,
  208. },
  209. }"
  210. />
  211. </template>
  212. <style>
  213. .dark {
  214. color-scheme: dark;
  215. }
  216. body {
  217. font-size: 13px;
  218. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
  219. Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  220. margin: 0;
  221. --base: #444;
  222. --nav-height: 50px;
  223. }
  224. .vue-repl {
  225. height: calc(var(--vh) - var(--nav-height)) !important;
  226. }
  227. button {
  228. border: none;
  229. outline: none;
  230. cursor: pointer;
  231. margin: 0;
  232. background-color: transparent;
  233. }
  234. </style>