]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
workflow: improve template explorer
authorEvan You <yyx990803@gmail.com>
Fri, 4 Oct 2019 21:43:20 +0000 (17:43 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 4 Oct 2019 21:43:20 +0000 (17:43 -0400)
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/utils.ts
packages/template-explorer/index.html
packages/template-explorer/src/index.ts
packages/template-explorer/src/options.ts [new file with mode: 0644]
packages/template-explorer/style.css [new file with mode: 0644]

index 9e4e866262e8fc57c40e5bb02fa7091ae682cc10..8337c3a58c8287b5313b516487924dfb0b2363a3 100644 (file)
@@ -22,7 +22,8 @@ import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import {
   advancePositionWithMutation,
   assert,
-  isSimpleIdentifier
+  isSimpleIdentifier,
+  loadDep
 } from './utils'
 import { isString, isArray } from '@vue/shared'
 import { TO_STRING, CREATE_VNODE, COMMENT } from './runtimeConstants'
@@ -97,7 +98,7 @@ function createCodegenContext(
     map:
       __BROWSER__ || !sourceMap
         ? undefined
-        : new (require('source-map')).SourceMapGenerator(),
+        : new (loadDep('source-map')).SourceMapGenerator(),
 
     helper(name) {
       return prefixIdentifiers ? name : `_${name}`
index b78d395c3988b6e2341cb0b5783aea08bfc24068..25700c34c4351a26ea97e296fcd64ba0e8ab5bf3 100644 (file)
@@ -41,7 +41,9 @@ export const transformIf = createStructuralDirectiveTransform(
       (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
     ) {
       const loc = dir.exp ? dir.exp.loc : node.loc
-      context.onError(createCompilerError(ErrorCodes.X_IF_NO_EXPRESSION, loc))
+      context.onError(
+        createCompilerError(ErrorCodes.X_IF_NO_EXPRESSION, dir.loc)
+      )
       dir.exp = createSimpleExpression(`true`, false, loc)
     }
 
index c8711d14013110b97ea36a68561310ea1b483816..e77a4856a6f65b90c3064e1a1f3cb8f4a9679fe7 100644 (file)
@@ -29,7 +29,7 @@ import { PropsExpression } from './transforms/transformElement'
 let _parse: typeof parse
 let _walk: typeof walk
 
-function loadDep(name: string) {
+export function loadDep(name: string) {
   if (typeof process !== 'undefined' && isFunction(require)) {
     return require(name)
   } else {
index 41c46c35f79dadc5b5be9ec064954bd3e614edf9..349758adc1c55e6640f498745cdba476bcabf606 100644 (file)
@@ -1,35 +1,21 @@
+<title>Vue Template Explorer</title>
 <link rel="stylesheet" data-name="vs/editor/editor.main" href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css">
-<style>
-body {
-  margin: 0;
-}
-.editor {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  box-sizing: border-box;
-}
-#source {
-  left: 0;
-  width: 40%;
-}
-#output {
-  left: 40%;
-  width: 60%;
-}
-</style>
+<link rel="stylesheet" href="./style.css">
 
+<div id="header"></div>
 <div id="source" class="editor"></div>
 <div id="output" class="editor"></div>
 
 <script src="../../node_modules/acorn/dist/acorn.js"></script>
 <script src="../../node_modules/estree-walker/dist/estree-walker.umd.js"></script>
+<script src="../../node_modules/source-map/dist/source-map.js"></script>
 <script src="../../node_modules/monaco-editor/min/vs/loader.js"></script>
 <script src="./dist/template-explorer.global.js"></script>
 <script>
 window._deps = {
   acorn,
-  'estree-walker': estreeWalker
+  'estree-walker': estreeWalker,
+  'source-map': sourceMap
 }
 
 require.config({
index ece20fb10892d17a041403130f84b7709a8d03ff..89c33f99bee0ee9c45858b55c0a349b6b0b68997 100644 (file)
@@ -1,5 +1,8 @@
 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
 
@@ -9,30 +12,64 @@ self.init = () => {
     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
     }
@@ -43,41 +80,134 @@ self.init = () => {
     {
       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
 }
diff --git a/packages/template-explorer/src/options.ts b/packages/template-explorer/src/options.ts
new file mode 100644 (file)
index 0000000..9d6e497
--- /dev/null
@@ -0,0 +1,72 @@
+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)
+}
diff --git a/packages/template-explorer/style.css b/packages/template-explorer/style.css
new file mode 100644 (file)
index 0000000..defada1
--- /dev/null
@@ -0,0 +1,69 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}
+
+#header {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 60px;
+  box-sizing: border-box;
+  background-color: #1e1e1e;
+  border-bottom: 1px solid #333;
+  padding: 0.3em 1.6em;
+  color: #fff;
+}
+
+h1 {
+  font-size: 18px;
+  display: inline-block;
+}
+
+#options {
+  float: right;
+  margin-top: 1em;
+}
+
+.options-group {
+  margin-right: 30px;
+}
+
+#header span, #header label, #header input {
+  display: inline-block;
+}
+
+#header .label {
+  font-weight: bold;
+}
+
+#header input {
+  margin-left: 12px;
+  margin-right: 6px;
+}
+
+#header label {
+  color: #999;
+}
+
+.editor {
+  position: absolute;
+  top: 60px;
+  bottom: 0;
+  box-sizing: border-box;
+}
+
+#source {
+  left: 0;
+  width: 45%;
+}
+
+#output {
+  left: 45%;
+  width: 55%;
+}
+
+.highlight {
+  background-color: rgba(46, 120, 190, 0.5);
+}