import * as m from 'monaco-editor'
-import { compile } from '@vue/compiler-dom'
+import { compile, CompilerError } from '@vue/compiler-dom'
+import { compilerOptions, initOptions } from './options'
+import { watch } from '@vue/runtime-dom'
+import { SourceMapConsumer } from 'source-map'
const self = window as any
decodeURIComponent(window.location.hash.slice(1)) ||
`<div>{{ foo + bar }}</div>`
- self.compilerOptions = {
- mode: 'module',
- prefixIdentifiers: true,
- hoistStatic: true
- }
-
+ let lastSuccessfulCode: string = `/* See console for error */`
+ let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
function compileCode(source: string): string {
console.clear()
try {
- const { code, ast } = compile(source, self.compilerOptions)
-
- console.log(ast)
- return code
+ const { code, ast, map } = compile(source, {
+ filename: 'template.vue',
+ ...compilerOptions,
+ sourceMap: true,
+ onError: displayError
+ })
+ monaco.editor.setModelMarkers(editor.getModel()!, `@vue/compiler-dom`, [])
+ console.log(`AST: `, ast)
+ lastSuccessfulCode = code + `\n\n// Check the console for the AST`
+ lastSuccessfulMap = new self._deps['source-map'].SourceMapConsumer(
+ map
+ ) as SourceMapConsumer
+ lastSuccessfulMap.computeColumnSpans()
} catch (e) {
console.error(e)
- return `/* See console for error */`
}
+ return lastSuccessfulCode
+ }
+
+ function displayError(err: CompilerError) {
+ const loc = err.loc
+ if (loc) {
+ monaco.editor.setModelMarkers(editor.getModel()!, `@vue/compiler-dom`, [
+ {
+ severity: monaco.MarkerSeverity.Error,
+ startLineNumber: loc.start.line,
+ startColumn: loc.start.column,
+ endLineNumber: loc.end.line,
+ endColumn: loc.end.column,
+ message: `Vue template compilation error: ${err.message}`,
+ code: String(err.code)
+ }
+ ])
+ }
+ throw err
}
- const sharedOptions = {
+ function reCompile() {
+ const src = editor.getValue()
+ window.location.hash = encodeURIComponent(src)
+ const res = compileCode(src)
+ if (res) {
+ output.setValue(res)
+ }
+ }
+
+ const sharedEditorOptions = {
theme: 'vs-dark',
fontSize: 14,
wordWrap: 'on',
scrollBeyondLastLine: false,
+ renderWhitespace: 'selection',
+ contextmenu: false,
minimap: {
enabled: false
}
{
value: persistedContent,
language: 'html',
- ...sharedOptions
+ ...sharedEditorOptions
}
)
- const model = editor.getModel()!
-
- model.updateOptions({
+ editor.getModel()!.updateOptions({
tabSize: 2
})
- model.onDidChangeContent(() => {
- const src = editor.getValue()
- window.location.hash = encodeURIComponent(src)
- const res = compileCode(src)
- if (res) {
- output.setValue(res)
- }
- })
-
const output = monaco.editor.create(
document.getElementById('output') as HTMLElement,
{
- value: compileCode(persistedContent),
+ value: '',
language: 'javascript',
readOnly: true,
- ...sharedOptions
+ ...sharedEditorOptions
}
)
-
output.getModel()!.updateOptions({
tabSize: 2
})
+ // handle resize
window.addEventListener('resize', () => {
editor.layout()
output.layout()
})
+
+ // update compile output when input changes
+ editor.onDidChangeModelContent(debounce(reCompile))
+
+ // highlight output code
+ let prevOutputDecos: string[] = []
+ function clearOutputDecos() {
+ prevOutputDecos = output.deltaDecorations(prevOutputDecos, [])
+ }
+
+ editor.onDidChangeCursorPosition(
+ debounce(e => {
+ clearEditorDecos()
+ if (lastSuccessfulMap) {
+ const pos = lastSuccessfulMap.generatedPositionFor({
+ source: 'template.vue',
+ line: e.position.lineNumber,
+ column: e.position.column - 1
+ })
+ if (pos.line != null && pos.column != null) {
+ prevOutputDecos = output.deltaDecorations(prevOutputDecos, [
+ {
+ range: new monaco.Range(
+ pos.line,
+ pos.column + 1,
+ pos.line,
+ pos.lastColumn ? pos.lastColumn + 2 : pos.column + 2
+ ),
+ options: {
+ inlineClassName: `highlight`
+ }
+ }
+ ])
+ output.revealPositionInCenter({
+ lineNumber: pos.line,
+ column: pos.column + 1
+ })
+ } else {
+ clearOutputDecos()
+ }
+ }
+ }, 100)
+ )
+
+ let previousEditorDecos: string[] = []
+ function clearEditorDecos() {
+ previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [])
+ }
+
+ output.onDidChangeCursorPosition(
+ debounce(e => {
+ clearOutputDecos()
+ if (lastSuccessfulMap) {
+ const pos = lastSuccessfulMap.originalPositionFor({
+ line: e.position.lineNumber,
+ column: e.position.column - 1
+ })
+ if (
+ pos.line != null &&
+ pos.column != null &&
+ !// ignore mock location
+ (pos.line === 1 && pos.column === 0)
+ ) {
+ const translatedPos = {
+ column: pos.column + 1,
+ lineNumber: pos.line
+ }
+ previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [
+ {
+ range: new monaco.Range(
+ pos.line,
+ pos.column + 1,
+ pos.line,
+ pos.column + 1
+ ),
+ options: {
+ isWholeLine: true,
+ className: `highlight`
+ }
+ }
+ ])
+ editor.revealPositionInCenter(translatedPos)
+ } else {
+ clearEditorDecos()
+ }
+ }
+ }, 100)
+ )
+
+ initOptions()
+ watch(reCompile)
+}
+
+function debounce<T extends Function>(fn: T, delay: number = 300): T {
+ let prevTimer: NodeJS.Timeout | null = null
+ return ((...args: any[]) => {
+ if (prevTimer) {
+ clearTimeout(prevTimer)
+ }
+ prevTimer = setTimeout(() => {
+ fn(...args)
+ prevTimer = null
+ }, delay)
+ }) as any
}
--- /dev/null
+import { h, reactive, createApp } from '@vue/runtime-dom'
+import { CompilerOptions } from '@vue/compiler-dom'
+
+export const compilerOptions: CompilerOptions = reactive({
+ mode: 'module',
+ prefixIdentifiers: false,
+ hoistStatic: false
+})
+
+const App = {
+ setup() {
+ return () => [
+ h('h1', `Vue 3 Template Explorer`),
+ h('div', { id: 'options' }, [
+ // mode selection
+ h('span', { class: 'options-group' }, [
+ h('span', { class: 'label' }, 'Mode:'),
+ h('input', {
+ type: 'radio',
+ id: 'mode-module',
+ name: 'mode',
+ checked: compilerOptions.mode === 'module',
+ onChange() {
+ compilerOptions.mode = 'module'
+ }
+ }),
+ h('label', { for: 'mode-module' }, 'module'),
+ h('input', {
+ type: 'radio',
+ id: 'mode-function',
+ name: 'mode',
+ checked: compilerOptions.mode === 'function',
+ onChange() {
+ compilerOptions.mode = 'function'
+ }
+ }),
+ h('label', { for: 'mode-function' }, 'function')
+ ]),
+
+ // toggle prefixIdentifiers
+ h('input', {
+ type: 'checkbox',
+ id: 'prefix',
+ disabled: compilerOptions.mode === 'module',
+ checked:
+ compilerOptions.prefixIdentifiers ||
+ compilerOptions.mode === 'module',
+ onChange(e: any) {
+ compilerOptions.prefixIdentifiers =
+ e.target.checked || compilerOptions.mode === 'module'
+ }
+ }),
+ h('label', { for: 'prefix' }, 'prefixIdentifiers'),
+
+ // toggle hoistStatic
+ h('input', {
+ type: 'checkbox',
+ id: 'hoist',
+ checked: compilerOptions.hoistStatic,
+ onChange(e: any) {
+ compilerOptions.hoistStatic = e.target.checked
+ }
+ }),
+ h('label', { for: 'hoist' }, 'hoistStatic')
+ ])
+ ]
+ }
+}
+
+export function initOptions() {
+ createApp().mount(App, document.getElementById('header') as HTMLElement)
+}