feat: repl (#94)

This commit is contained in:
三咲智子 Kevin Deng 2024-01-21 16:51:28 +08:00 committed by GitHub
parent 32604cf91c
commit 51098cff94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 156 additions and 56 deletions

View File

@ -38,7 +38,7 @@
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs", "build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
"build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime", "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
"build-browser-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler && node scripts/build.js vue -f esm-browser", "build-browser-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler && node scripts/build.js vue -f esm-browser",
"build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser", "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer vue-vapor -f esm-browser",
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build", "build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"postinstall": "simple-git-hooks" "postinstall": "simple-git-hooks"

View File

@ -46,6 +46,7 @@
"@vue/compiler-core": "workspace:*", "@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*", "@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*", "@vue/compiler-ssr": "workspace:*",
"@vue/compiler-vapor": "workspace:*",
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",

View File

@ -27,6 +27,7 @@ import {
} from './template/transformSrcset' } from './template/transformSrcset'
import { generateCodeFrame, isObject } from '@vue/shared' import { generateCodeFrame, isObject } from '@vue/shared'
import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerDOM from '@vue/compiler-dom'
import * as CompilerVapor from '@vue/compiler-vapor'
import * as CompilerSSR from '@vue/compiler-ssr' import * as CompilerSSR from '@vue/compiler-ssr'
import consolidate from '@vue/consolidate' import consolidate from '@vue/consolidate'
import { warnOnce } from './warn' import { warnOnce } from './warn'
@ -55,6 +56,7 @@ export interface SFCTemplateCompileOptions {
scoped?: boolean scoped?: boolean
slotted?: boolean slotted?: boolean
isProd?: boolean isProd?: boolean
vapor?: boolean
ssr?: boolean ssr?: boolean
ssrCssVars?: string[] ssrCssVars?: string[]
inMap?: RawSourceMap inMap?: RawSourceMap
@ -171,6 +173,7 @@ function doCompileTemplate({
source, source,
ast: inAST, ast: inAST,
ssr = false, ssr = false,
vapor = false,
ssrCssVars, ssrCssVars,
isProd = false, isProd = false,
compiler, compiler,
@ -205,7 +208,12 @@ function doCompileTemplate({
const shortId = id.replace(/^data-v-/, '') const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}` const longId = `data-v-${shortId}`
const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM const defaultCompiler = vapor
? // TODO ssr
(CompilerVapor as TemplateCompiler)
: ssr
? (CompilerSSR as TemplateCompiler)
: CompilerDOM
compiler = compiler || defaultCompiler compiler = compiler || defaultCompiler
if (compiler !== defaultCompiler) { if (compiler !== defaultCompiler) {

View File

@ -13,7 +13,7 @@
"vite": "^5.0.5" "vite": "^5.0.5"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^3.1.1", "@vue/repl": "4.0.0-alpha.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -1,10 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import Header from './Header.vue' import Header from './Header.vue'
import { Repl, ReplStore, SFCOptions } from '@vue/repl' import {
Repl,
type SFCOptions,
useStore,
useVueImportMap,
mergeImportMap,
File,
StoreState,
} from '@vue/repl'
import type Monaco from '@vue/repl/monaco-editor' import type Monaco from '@vue/repl/monaco-editor'
import type CodeMirror from '@vue/repl/codemirror-editor' import type CodeMirror from '@vue/repl/codemirror-editor'
import { ref, watchEffect, onMounted } from 'vue' import { ref, watchEffect, onMounted, computed, shallowRef, watch } from 'vue'
import { shallowRef } from 'vue'
const EditorComponent = shallowRef<typeof Monaco | typeof CodeMirror>() const EditorComponent = shallowRef<typeof Monaco | typeof CodeMirror>()
@ -26,78 +33,135 @@ const setVH = () => {
window.addEventListener('resize', setVH) window.addEventListener('resize', setVH)
setVH() setVH()
const useProdMode = ref(false)
const useSSRMode = ref(false) const useSSRMode = ref(false)
const useVaporMode = ref(false)
const {
vueVersion,
productionMode,
importMap: vueImportMap,
} = useVueImportMap({
runtimeDev: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js`
: `${location.origin}/src/vue-dev-proxy`,
runtimeProd: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.prod.js`
: `${location.origin}/src/vue-dev-proxy-prod`,
serverRenderer: import.meta.env.PROD
? `${location.origin}/server-renderer.esm-browser.js`
: `${location.origin}/src/vue-server-renderer-dev-proxy`,
})
const importMap = computed(() =>
mergeImportMap(vueImportMap.value, {
imports: {
'vue/vapor': import.meta.env.PROD
? `${location.origin}/vue-vapor.esm-browser.js`
: `${location.origin}/src/vue-vapor-dev-proxy`,
},
}),
)
let hash = location.hash.slice(1) let hash = location.hash.slice(1)
if (hash.startsWith('__DEV__')) { if (hash.startsWith('__DEV__')) {
hash = hash.slice(7) hash = hash.slice(7)
useProdMode.value = false productionMode.value = false
} }
if (hash.startsWith('__PROD__')) { if (hash.startsWith('__PROD__')) {
hash = hash.slice(8) hash = hash.slice(8)
useProdMode.value = true productionMode.value = true
} }
if (hash.startsWith('__SSR__')) { if (hash.startsWith('__SSR__')) {
hash = hash.slice(7) hash = hash.slice(7)
useSSRMode.value = true useSSRMode.value = true
} }
if (hash.startsWith('__VAPOR__')) {
hash = hash.slice(9)
useVaporMode.value = true
}
const store = new ReplStore({ const files: StoreState['files'] = ref(Object.create(null))
serializedState: hash, const mainFile = ref('src/App.vue')
productionMode: useProdMode.value,
defaultVueRuntimeURL: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js`
: `${location.origin}/src/vue-dev-proxy`,
defaultVueRuntimeProdURL: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.prod.js`
: `${location.origin}/src/vue-dev-proxy-prod`,
defaultVueServerRendererURL: import.meta.env.PROD
? `${location.origin}/server-renderer.esm-browser.js`
: `${location.origin}/src/vue-server-renderer-dev-proxy`,
})
// enable experimental features // enable experimental features
const sfcOptions: SFCOptions = { const sfcOptions = computed(
(): SFCOptions => ({
script: { script: {
inlineTemplate: useProdMode.value, inlineTemplate: productionMode.value,
isProd: useProdMode.value, isProd: productionMode.value,
propsDestructure: true, propsDestructure: true,
}, },
style: { style: {
isProd: useProdMode.value, isProd: productionMode.value,
}, },
template: { template: {
isProd: useProdMode.value, vapor: useVaporMode.value,
isProd: productionMode.value,
compilerOptions: { compilerOptions: {
isCustomElement: (tag: string) => tag === 'mjx-container', isCustomElement: (tag: string) => tag === 'mjx-container',
}, },
}, },
} }),
)
const store = useStore(
{
files,
vueVersion,
builtinImportMap: importMap,
sfcOptions,
mainFile,
},
hash,
)
// @ts-expect-error
globalThis.store = store
watch(
useVaporMode,
() => {
if (useVaporMode.value) {
files.value['src/index.html'] = new File(
'src/index.html',
`<script type="module">
import { render } from 'vue/vapor'
import App from './App.vue'
render(App, {}, '#app')` +
'<' +
'/script>' +
`<div id="app"></div>`,
true,
)
mainFile.value = 'src/index.html'
store.activeFile = files.value['src/App.vue']
} else if (files.value['src/index.html']?.hidden) {
delete files.value['src/index.html']
mainFile.value = 'src/App.vue'
}
},
{ immediate: true },
)
// persist state // persist state
watchEffect(() => { watchEffect(() => {
const newHash = store const newHash = store
.serialize() .serialize()
.replace(/^#/, useVaporMode.value ? `#__VAPOR__` : `#`)
.replace(/^#/, useSSRMode.value ? `#__SSR__` : `#`) .replace(/^#/, useSSRMode.value ? `#__SSR__` : `#`)
.replace(/^#/, useProdMode.value ? `#__PROD__` : `#`) .replace(/^#/, productionMode.value ? `#__PROD__` : `#`)
history.replaceState({}, '', newHash) history.replaceState({}, '', newHash)
}) })
function toggleProdMode() { function toggleProdMode() {
const isProd = (useProdMode.value = !useProdMode.value) productionMode.value = !productionMode.value
sfcOptions.script!.inlineTemplate =
sfcOptions.script!.isProd =
sfcOptions.template!.isProd =
sfcOptions.style!.isProd =
isProd
store.toggleProduction()
store.setFiles(store.getFiles())
} }
function toggleSSR() { function toggleSSR() {
useSSRMode.value = !useSSRMode.value useSSRMode.value = !useSSRMode.value
store.setFiles(store.getFiles()) }
function toggleVapor() {
useVaporMode.value = !useVaporMode.value
} }
function reloadPage() { function reloadPage() {
@ -117,11 +181,13 @@ onMounted(() => {
<template> <template>
<Header <Header
:store="store" :store="store"
:prod="useProdMode" :prod="productionMode"
:ssr="useSSRMode" :ssr="useSSRMode"
:vapor="useVaporMode"
@toggle-theme="toggleTheme" @toggle-theme="toggleTheme"
@toggle-prod="toggleProdMode" @toggle-prod="toggleProdMode"
@toggle-ssr="toggleSSR" @toggle-ssr="toggleSSR"
@toggle-vapor="toggleVapor"
@reload-page="reloadPage" @reload-page="reloadPage"
/> />
<Repl <Repl
@ -135,7 +201,6 @@ onMounted(() => {
:store="store" :store="store"
:showCompileOutput="true" :showCompileOutput="true"
:autoResize="true" :autoResize="true"
:sfcOptions="sfcOptions"
:clearConsole="false" :clearConsole="false"
:preview-options="{ :preview-options="{
customCode: { customCode: {

View File

@ -14,11 +14,13 @@ const props = defineProps<{
store: ReplStore store: ReplStore
prod: boolean prod: boolean
ssr: boolean ssr: boolean
vapor: boolean
}>() }>()
const emit = defineEmits([ const emit = defineEmits([
'toggle-theme', 'toggle-theme',
'toggle-ssr', 'toggle-ssr',
'toggle-prod', 'toggle-prod',
'toggle-vapor',
'reload-page', 'reload-page',
]) ])
@ -27,7 +29,7 @@ const { store } = props
const currentCommit = __COMMIT__ const currentCommit = __COMMIT__
const vueVersion = ref(`@${currentCommit}`) const vueVersion = ref(`@${currentCommit}`)
const vueURL = store.getImportMap().imports.vue const vueURL = store.getImportMap().imports?.vue
if (vueURL && !vueURL.startsWith(location.origin)) { if (vueURL && !vueURL.startsWith(location.origin)) {
const versionMatch = vueURL.match(/runtime-dom@([^/]+)/) const versionMatch = vueURL.match(/runtime-dom@([^/]+)/)
if (versionMatch) vueVersion.value = versionMatch[1] if (versionMatch) vueVersion.value = versionMatch[1]
@ -35,12 +37,12 @@ if (vueURL && !vueURL.startsWith(location.origin)) {
async function setVueVersion(v: string) { async function setVueVersion(v: string) {
vueVersion.value = `loading...` vueVersion.value = `loading...`
await store.setVueVersion(v) store.vueVersion = v
vueVersion.value = v vueVersion.value = v
} }
function resetVueVersion() { function resetVueVersion() {
store.resetVueVersion() store.vueVersion = undefined
vueVersion.value = `@${currentCommit}` vueVersion.value = `@${currentCommit}`
} }
@ -73,7 +75,7 @@ function toggleDark() {
</h1> </h1>
<div class="links"> <div class="links">
<VersionSelect <VersionSelect
v-model="store.state.typescriptVersion" v-model="store.typescriptVersion"
pkg="typescript" pkg="typescript"
label="TypeScript Version" label="TypeScript Version"
/> />
@ -102,6 +104,14 @@ function toggleDark() {
> >
<span>{{ prod ? 'PROD' : 'DEV' }}</span> <span>{{ prod ? 'PROD' : 'DEV' }}</span>
</button> </button>
<button
title="Toggle vapor mode"
class="toggle-vapor"
:class="{ enabled: vapor }"
@click="$emit('toggle-vapor')"
>
<span>{{ vapor ? 'VAPOR ON' : 'VAPOR OFF' }}</span>
</button>
<button <button
title="Toggle server rendering mode" title="Toggle server rendering mode"
class="toggle-ssr" class="toggle-ssr"
@ -202,6 +212,7 @@ h1 img {
} }
.toggle-prod span, .toggle-prod span,
.toggle-vapor span,
.toggle-ssr span { .toggle-ssr span {
font-size: 12px; font-size: 12px;
border-radius: 4px; border-radius: 4px;
@ -226,6 +237,15 @@ h1 img {
background-color: var(--green); background-color: var(--green);
} }
.toggle-vapor span {
background-color: var(--btn-bg);
}
.toggle-vapor.enabled span {
color: #fff;
background-color: var(--green);
}
.toggle-dark svg { .toggle-dark svg {
width: 18px; width: 18px;
height: 18px; height: 18px;

View File

@ -0,0 +1,2 @@
// serve vue/vapor to the iframe sandbox during dev.
export * from 'vue/vapor'

View File

@ -52,6 +52,7 @@ function copyVuePlugin(): Plugin {
copyFile(`../vue/dist/vue.runtime.esm-browser.js`) copyFile(`../vue/dist/vue.runtime.esm-browser.js`)
copyFile(`../vue/dist/vue.runtime.esm-browser.prod.js`) copyFile(`../vue/dist/vue.runtime.esm-browser.prod.js`)
copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`) copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`)
copyFile(`../vue-vapor/dist/vue-vapor.esm-browser.js`)
}, },
} }
} }

View File

@ -219,6 +219,9 @@ importers:
'@vue/compiler-ssr': '@vue/compiler-ssr':
specifier: workspace:* specifier: workspace:*
version: link:../compiler-ssr version: link:../compiler-ssr
'@vue/compiler-vapor':
specifier: workspace:*
version: link:../compiler-vapor
'@vue/shared': '@vue/shared':
specifier: workspace:* specifier: workspace:*
version: link:../shared version: link:../shared
@ -368,8 +371,8 @@ importers:
packages/sfc-playground: packages/sfc-playground:
dependencies: dependencies:
'@vue/repl': '@vue/repl':
specifier: ^3.1.1 specifier: 4.0.0-alpha.0
version: 3.3.0 version: 4.0.0-alpha.0
file-saver: file-saver:
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.5 version: 2.0.5
@ -1634,8 +1637,8 @@ packages:
engines: {node: '>= 0.12.0'} engines: {node: '>= 0.12.0'}
dev: true dev: true
/@vue/repl@3.3.0: /@vue/repl@4.0.0-alpha.0:
resolution: {integrity: sha512-A9tdO7obt/kpFUHdgGoRnan6bZjfz/WAJ5+DpPkvgNEc960W+bJraURv8MUVtH2Id/byWotKbUve2jTakiccSw==} resolution: {integrity: sha512-kGgnon2yV1y0eKeWatys4by32XXCDSdq31Rwx0cd8xXAIK0GIL0AeSMCvVUrNE2ke8rFVYe6xMmpOd1iCcM0Zg==}
dev: false dev: false
/@zeit/schemas@2.29.0: /@zeit/schemas@2.29.0: