+
{{ text }}
@@ -17,19 +14,12 @@ const visible = defineModel
()
diff --git a/src/output/Preview.vue b/src/output/Preview.vue
index 19b0e93c..20c6f37d 100644
--- a/src/output/Preview.vue
+++ b/src/output/Preview.vue
@@ -1,312 +1,34 @@
-
-
-
-
-
diff --git a/src/output/Sandbox.vue b/src/output/Sandbox.vue
new file mode 100644
index 00000000..c62f480c
--- /dev/null
+++ b/src/output/Sandbox.vue
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/output/SsrOutput.vue b/src/output/SsrOutput.vue
new file mode 100644
index 00000000..744f9015
--- /dev/null
+++ b/src/output/SsrOutput.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
HTML
+
{{ html }}
+
Context
+
{{ context }}
+
+
+
+
diff --git a/src/output/moduleCompiler.ts b/src/output/moduleCompiler.ts
index b7f3266d..55af3630 100644
--- a/src/output/moduleCompiler.ts
+++ b/src/output/moduleCompiler.ts
@@ -201,7 +201,7 @@ function processModule(store: Store, src: string, filename: string) {
}
}
s.remove(node.start!, node.declaration.start!)
- } else if (node.source) {
+ } else if (node.source && node.source.value.startsWith('./')) {
// export { foo, bar } from './foo'
const importId = defineImport(node, node.source.value)
for (const spec of node.specifiers) {
diff --git a/src/output/srcdoc.html b/src/output/srcdoc.html
index 189c19ca..75fa51b3 100644
--- a/src/output/srcdoc.html
+++ b/src/output/srcdoc.html
@@ -35,7 +35,8 @@
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_ok = (response) =>
+ send_reply({ action: 'cmd_ok', args: response })
const send_error = (message, stack) =>
send_reply({ action: 'cmd_error', message, stack })
@@ -65,7 +66,11 @@
scriptEls.push(scriptEl)
await done
}
- send_ok()
+ if (window.__ssr_promise__) {
+ send_ok(await window.__ssr_promise__)
+ } else {
+ send_ok()
+ }
} catch (e) {
send_error(e.message, e.stack)
}
diff --git a/src/sourcemap.ts b/src/sourcemap.ts
new file mode 100644
index 00000000..3ecdf477
--- /dev/null
+++ b/src/sourcemap.ts
@@ -0,0 +1,119 @@
+import type { RawSourceMap } from 'source-map-js'
+import type { EncodedSourceMap as TraceEncodedSourceMap } from '@jridgewell/trace-mapping'
+import { TraceMap, eachMapping } from '@jridgewell/trace-mapping'
+import type { EncodedSourceMap as GenEncodedSourceMap } from '@jridgewell/gen-mapping'
+import { addMapping, fromMap, toEncodedMap } from '@jridgewell/gen-mapping'
+
+// trim analyzed bindings comment
+export function trimAnalyzedBindings(scriptCode: string) {
+ return scriptCode.replace(/\/\*[\s\S]*?\*\/\n/, '').trim()
+}
+/**
+ * The merge logic of sourcemap is consistent with the logic in vite-plugin-vue
+ */
+export function getSourceMap(
+ filename: string,
+ scriptCode: string,
+ scriptMap: any,
+ templateMap: any,
+): RawSourceMap {
+ let resolvedMap: RawSourceMap | undefined = undefined
+ if (templateMap) {
+ // if the template is inlined into the main module (indicated by the presence
+ // of templateMap), we need to concatenate the two source maps.
+ const from = scriptMap ?? {
+ file: filename,
+ sourceRoot: '',
+ version: 3,
+ sources: [],
+ sourcesContent: [],
+ names: [],
+ mappings: '',
+ }
+ const gen = fromMap(
+ // version property of result.map is declared as string
+ // but actually it is `3`
+ from as Omit as TraceEncodedSourceMap,
+ )
+ const tracer = new TraceMap(
+ // same above
+ templateMap as Omit as TraceEncodedSourceMap,
+ )
+ const offset =
+ (trimAnalyzedBindings(scriptCode).match(/\r?\n/g)?.length ?? 0)
+ eachMapping(tracer, (m) => {
+ if (m.source == null) return
+ addMapping(gen, {
+ source: m.source,
+ original: { line: m.originalLine, column: m.originalColumn },
+ generated: {
+ line: m.generatedLine + offset,
+ column: m.generatedColumn,
+ },
+ })
+ })
+
+ // same above
+ resolvedMap = toEncodedMap(gen) as Omit<
+ GenEncodedSourceMap,
+ 'version'
+ > as RawSourceMap
+ // if this is a template only update, we will be reusing a cached version
+ // of the main module compile result, which has outdated sourcesContent.
+ resolvedMap.sourcesContent = templateMap.sourcesContent
+ } else {
+ resolvedMap = scriptMap
+ }
+
+ return resolvedMap!
+}
+
+/*
+ * Slightly modified version of https://github.com/AriPerkkio/vite-plugin-source-map-visualizer/blob/main/src/generate-link.ts
+ */
+export function toVisualizer(code: string, sourceMap: RawSourceMap) {
+ const map = JSON.stringify(sourceMap)
+ const encoder = new TextEncoder()
+
+ // Convert the strings to Uint8Array
+ const codeArray = encoder.encode(code)
+ const mapArray = encoder.encode(map)
+
+ // Create Uint8Array for the lengths
+ const codeLengthArray = encoder.encode(codeArray.length.toString())
+ const mapLengthArray = encoder.encode(mapArray.length.toString())
+
+ // Combine the lengths and the data
+ const combinedArray = new Uint8Array(
+ codeLengthArray.length +
+ 1 +
+ codeArray.length +
+ mapLengthArray.length +
+ 1 +
+ mapArray.length,
+ )
+
+ combinedArray.set(codeLengthArray)
+ combinedArray.set([0], codeLengthArray.length)
+ combinedArray.set(codeArray, codeLengthArray.length + 1)
+ combinedArray.set(
+ mapLengthArray,
+ codeLengthArray.length + 1 + codeArray.length,
+ )
+ combinedArray.set(
+ [0],
+ codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length,
+ )
+ combinedArray.set(
+ mapArray,
+ codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length + 1,
+ )
+
+ // Convert the Uint8Array to a binary string
+ let binary = ''
+ const len = combinedArray.byteLength
+ for (let i = 0; i < len; i++) binary += String.fromCharCode(combinedArray[i])
+
+ // Convert the binary string to a base64 string and return it
+ return `https://evanw.github.io/source-map-visualization#${btoa(binary)}`
+}
diff --git a/src/store.ts b/src/store.ts
index 5ccf761c..3af17d4e 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -142,7 +142,11 @@ export function useStore(
}
}
- function setImportMap(map: ImportMap) {
+ function setImportMap(map: ImportMap, merge = false) {
+ if (merge) {
+ map = mergeImportMap(getImportMap(), map)
+ }
+
if (map.imports)
for (const [key, value] of Object.entries(map.imports)) {
if (value) {
@@ -266,9 +270,15 @@ export function useStore(
}
}
if (vueVersion.value) files._version = vueVersion.value
+ if (typescriptVersion.value !== 'latest' || files._tsVersion) {
+ files._tsVersion = typescriptVersion.value
+ }
return '#' + utoa(JSON.stringify(files))
}
- const deserialize: ReplStore['deserialize'] = (serializedState: string) => {
+ const deserialize: ReplStore['deserialize'] = (
+ serializedState: string,
+ checkBuiltinImportMap = true,
+ ) => {
if (serializedState.startsWith('#'))
serializedState = serializedState.slice(1)
let saved: any
@@ -282,10 +292,15 @@ export function useStore(
for (const filename in saved) {
if (filename === '_version') {
vueVersion.value = saved[filename]
+ } else if (filename === '_tsVersion') {
+ typescriptVersion.value = saved[filename]
} else {
setFile(files.value, filename, saved[filename])
}
}
+ if (checkBuiltinImportMap) {
+ applyBuiltinImportMap()
+ }
}
const getFiles: ReplStore['getFiles'] = () => {
const exported: Record = {}
@@ -329,7 +344,7 @@ export function useStore(
}
if (serializedState) {
- deserialize(serializedState)
+ deserialize(serializedState, false)
} else {
setDefaultFile()
}
@@ -353,6 +368,7 @@ export function useStore(
showOutput,
outputMode,
sfcOptions,
+ ssrOutput: { html: '', context: '' },
compiler,
loading,
vueVersion,
@@ -368,6 +384,7 @@ export function useStore(
deleteFile,
renameFile,
getImportMap,
+ setImportMap,
getTsConfig,
serialize,
deserialize,
@@ -413,6 +430,10 @@ export type StoreState = ToRefs<{
showOutput: boolean
outputMode: OutputModes
sfcOptions: SFCOptions
+ ssrOutput: {
+ html: string
+ context: unknown
+ }
/** `@vue/compiler-sfc` */
compiler: typeof defaultCompiler
/* only apply for compiler-sfc */
@@ -436,9 +457,15 @@ export interface ReplStore extends UnwrapRef {
deleteFile(filename: string): void
renameFile(oldFilename: string, newFilename: string): void
getImportMap(): ImportMap
+ setImportMap(map: ImportMap, merge?: boolean): void
getTsConfig(): Record
serialize(): string
- deserialize(serializedState: string): void
+ /**
+ * Deserializes the given string to restore the REPL store state.
+ * @param serializedState - The serialized state string.
+ * @param checkBuiltinImportMap - Whether to check the built-in import map. Default to true
+ */
+ deserialize(serializedState: string, checkBuiltinImportMap?: boolean): void
getFiles(): Record
setFiles(newFiles: Record, mainFile?: string): Promise
}
@@ -452,6 +479,7 @@ export type Store = Pick<
| 'showOutput'
| 'outputMode'
| 'sfcOptions'
+ | 'ssrOutput'
| 'compiler'
| 'vueVersion'
| 'locale'
@@ -472,6 +500,8 @@ export class File {
js: '',
css: '',
ssr: '',
+ clientMap: '',
+ ssrMap: '',
}
editorViewState: editor.ICodeEditorViewState | null = null
diff --git a/src/transform.ts b/src/transform.ts
index 68b5b6e4..d1ec2b19 100644
--- a/src/transform.ts
+++ b/src/transform.ts
@@ -6,6 +6,7 @@ import type {
} from 'vue/compiler-sfc'
import { type Transform, transform } from 'sucrase'
import hashId from 'hash-sum'
+import { getSourceMap, toVisualizer, trimAnalyzedBindings } from './sourcemap'
export const COMP_IDENTIFIER = `__sfc__`
@@ -17,7 +18,7 @@ function testJsx(filename: string | undefined | null) {
return !!(filename && /(\.|\b)[jt]sx$/.test(filename))
}
-async function transformTS(src: string, isJSX?: boolean) {
+function transformTS(src: string, isJSX?: boolean) {
return transform(src, {
transforms: ['typescript', ...(isJSX ? (['jsx'] as Transform[]) : [])],
jsxRuntime: 'preserve',
@@ -40,10 +41,12 @@ export async function compileFile(
if (REGEX_JS.test(filename)) {
const isJSX = testJsx(filename)
if (testTs(filename)) {
- code = await transformTS(code, isJSX)
+ code = transformTS(code, isJSX)
}
if (isJSX) {
- code = await import('./jsx').then((m) => m.transformJSX(code))
+ code = await import('./jsx').then(({ transformJSX }) =>
+ transformJSX(code),
+ )
}
compiled.js = compiled.ssr = code
return []
@@ -108,23 +111,36 @@ export async function compileFile(
const hasScoped = descriptor.styles.some((s) => s.scoped)
let clientCode = ''
let ssrCode = ''
+ let ssrScript = ''
+ let clientScriptMap: any
+ let clientTemplateMap: any
+ let ssrScriptMap: any
+ let ssrTemplateMap: any
const appendSharedCode = (code: string) => {
clientCode += code
ssrCode += code
}
+ const ceFilter = store.sfcOptions.script?.customElement || /\.ce\.vue$/
+ function isCustomElement(filters: typeof ceFilter): boolean {
+ if (typeof filters === 'boolean') {
+ return filters
+ }
+ if (typeof filters === 'function') {
+ return filters(filename)
+ }
+ return filters.test(filename)
+ }
+ let isCE = isCustomElement(ceFilter)
+
let clientScript: string
let bindings: BindingMetadata | undefined
try {
- ;[clientScript, bindings] = await doCompileScript(
- store,
- descriptor,
- id,
- false,
- isTS,
- isJSX,
- )
+ const res = await doCompileScript(store, descriptor, id, false, isTS, isJSX, isCE)
+ clientScript = res.code
+ bindings = res.bindings
+ clientScriptMap = res.map
} catch (e: any) {
return [e.stack.split('\n').slice(0, 12).join('\n')]
}
@@ -143,8 +159,11 @@ export async function compileFile(
true,
isTS,
isJSX,
+ isCE
)
- ssrCode += ssrScriptResult[0]
+ ssrScript = ssrScriptResult.code
+ ssrCode += ssrScript
+ ssrScriptMap = ssrScriptResult.map
} catch (e) {
ssrCode = `/* SSR compile error: ${e} */`
}
@@ -169,10 +188,11 @@ export async function compileFile(
isTS,
isJSX,
)
- if (Array.isArray(clientTemplateResult)) {
- return clientTemplateResult
+ if (clientTemplateResult.errors.length) {
+ return clientTemplateResult.errors
}
- clientCode += `;${clientTemplateResult}`
+ clientCode += `;${clientTemplateResult.code}`
+ clientTemplateMap = clientTemplateResult.map
const ssrTemplateResult = await doCompileTemplate(
store,
@@ -183,14 +203,21 @@ export async function compileFile(
isTS,
isJSX,
)
- if (typeof ssrTemplateResult === 'string') {
+ if (ssrTemplateResult.code) {
// ssr compile failure is fine
- ssrCode += `;${ssrTemplateResult}`
+ ssrCode += `;${ssrTemplateResult.code}`
+ ssrTemplateMap = ssrTemplateResult.map
} else {
- ssrCode = `/* SSR compile error: ${ssrTemplateResult[0]} */`
+ ssrCode = `/* SSR compile error: ${ssrTemplateResult.errors[0]} */`
}
}
+ if (isJSX) {
+ const { transformJSX } = await import('./jsx')
+ clientCode &&= transformJSX(clientCode)
+ ssrCode &&= transformJSX(ssrCode)
+ }
+
if (hasScoped) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`,
@@ -198,18 +225,6 @@ export async function compileFile(
}
// styles
- const ceFilter = store.sfcOptions.script?.customElement || /\.ce\.vue$/
- function isCustomElement(filters: typeof ceFilter): boolean {
- if (typeof filters === 'boolean') {
- return filters
- }
- if (typeof filters === 'function') {
- return filters(filename)
- }
- return filters.test(filename)
- }
- let isCE = isCustomElement(ceFilter)
-
let css = ''
let styles: string[] = []
for (const style of descriptor.styles) {
@@ -256,6 +271,19 @@ export async function compileFile(
)
compiled.js = clientCode.trimStart()
compiled.ssr = ssrCode.trimStart()
+ compiled.clientMap = toVisualizer(
+ trimAnalyzedBindings(compiled.js),
+ getSourceMap(filename, clientScript, clientScriptMap, clientTemplateMap),
+ )
+ compiled.ssrMap = toVisualizer(
+ trimAnalyzedBindings(compiled.ssr),
+ getSourceMap(
+ filename,
+ ssrScript || clientScript,
+ ssrScriptMap,
+ ssrTemplateMap,
+ ),
+ )
}
return []
@@ -268,7 +296,8 @@ async function doCompileScript(
ssr: boolean,
isTS: boolean,
isJSX: boolean,
-): Promise<[code: string, bindings: BindingMetadata | undefined]> {
+ isCustomElement: boolean,
+): Promise<{ code: string; bindings: BindingMetadata | undefined; map?: any }> {
if (descriptor.script || descriptor.scriptSetup) {
const expressionPlugins: CompilerOptions['expressionPlugins'] = []
if (isTS) {
@@ -277,7 +306,6 @@ async function doCompileScript(
if (isJSX) {
expressionPlugins.push('jsx')
}
-
const compiledScript = store.compiler.compileScript(descriptor, {
inlineTemplate: true,
...store.sfcOptions?.script,
@@ -292,14 +320,12 @@ async function doCompileScript(
expressionPlugins,
},
},
+ customElement: isCustomElement,
})
let code = compiledScript.content
if (isTS) {
code = await transformTS(code, isJSX)
}
- if (isJSX) {
- code = await import('./jsx').then((m) => m.transformJSX(code))
- }
if (compiledScript.bindings) {
code =
`/* Analyzed bindings: ${JSON.stringify(
@@ -309,9 +335,15 @@ async function doCompileScript(
)} */\n` + code
}
- return [code, compiledScript.bindings]
+ return { code, bindings: compiledScript.bindings, map: compiledScript.map }
} else {
- return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
+ // @ts-expect-error TODO remove when 3.6 is out
+ const vaporFlag = descriptor.vapor ? '__vapor: true' : ''
+ return {
+ code: `\nconst ${COMP_IDENTIFIER} = { ${vaporFlag} }`,
+ bindings: {},
+ map: undefined,
+ }
}
}
@@ -332,9 +364,11 @@ async function doCompileTemplate(
expressionPlugins.push('jsx')
}
- let { code, errors } = store.compiler.compileTemplate({
+ const res = store.compiler.compileTemplate({
isProd: false,
...store.sfcOptions?.template,
+ // @ts-expect-error TODO remove expect-error after 3.6
+ vapor: descriptor.vapor,
ast: descriptor.template!.ast,
source: descriptor.template!.content,
filename: descriptor.filename,
@@ -349,8 +383,9 @@ async function doCompileTemplate(
expressionPlugins,
},
})
+ let { code, errors, map } = res
if (errors.length) {
- return errors
+ return { code, map, errors }
}
const fnName = ssr ? `ssrRender` : `render`
@@ -364,9 +399,5 @@ async function doCompileTemplate(
if (isTS) {
code = await transformTS(code, isJSX)
}
- if (isJSX) {
- code = await import('./jsx').then((m) => m.transformJSX(code))
- }
-
- return code
+ return { code, map, errors: [] }
}
diff --git a/src/types.ts b/src/types.ts
index af62caaa..7018ca63 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,10 +13,11 @@ export interface EditorEmits {
}
export type EditorComponentType = Component
-export type OutputModes = 'preview' | EditorMode
+export type OutputModes = 'preview' | 'ssr output' | EditorMode
-export const injectKeyProps: InjectionKey>> =
- Symbol('props')
+export const injectKeyProps: InjectionKey<
+ ToRefs>
+> = Symbol('props')
export const injectKeyPreviewRef: InjectionKey<
- ComputedRef
+ ComputedRef
> = Symbol('preview-ref')
diff --git a/test/main.ts b/test/main.ts
index 52885620..43a674c7 100644
--- a/test/main.ts
+++ b/test/main.ts
@@ -58,8 +58,10 @@ const App = {
theme: theme.value,
previewTheme: previewTheme.value,
editor: MonacoEditor,
+ showOpenSourceMap: true,
// layout: 'vertical',
ssr: true,
+ showSsrOutput: true,
sfcOptions: {
script: {
// inlineTemplate: false
@@ -68,6 +70,7 @@ const App = {
// showCompileOutput: false,
// showImportMap: false
editorOptions: {
+ autoSaveText: '💾',
monacoOptions: {
// wordWrap: 'on',
},
diff --git a/tsconfig.json b/tsconfig.json
index 65d82bda..785afc59 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
"target": "esnext",
"useDefineForClassFields": false,
"module": "esnext",
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"allowJs": false,
"strict": true,
"noUnusedLocals": true,
diff --git a/vite.config.ts b/vite.config.ts
index 6ccda777..1b007b87 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -24,14 +24,8 @@ const patchCssFiles: Plugin = {
name: 'patch-css',
apply: 'build',
writeBundle() {
- // 1. MonacoEditor.css -> monaco-editor.css
+ // inject css imports to the files
const outDir = path.resolve('dist')
- fs.renameSync(
- path.resolve(outDir, 'MonacoEditor.css'),
- path.resolve(outDir, 'monaco-editor.css'),
- )
-
- // 2. inject css imports to the files
;['vue-repl', 'monaco-editor', 'codemirror-editor'].forEach((file) => {
const filePath = path.resolve(outDir, file + '.js')
const content = fs.readFileSync(filePath, 'utf-8')
@@ -63,6 +57,7 @@ export default mergeConfig(base, {
lib: {
entry: {
'vue-repl': './src/index.ts',
+ core: './src/core.ts',
'monaco-editor': './src/editor/MonacoEditor.vue',
'codemirror-editor': './src/editor/CodeMirrorEditor.vue',
},