workflow: improve template explorer

This commit is contained in:
Evan You 2019-10-04 17:43:20 -04:00
parent 8d49b97cc3
commit 0873254c6c
7 changed files with 313 additions and 53 deletions

View File

@ -22,7 +22,8 @@ import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
advancePositionWithMutation, advancePositionWithMutation,
assert, assert,
isSimpleIdentifier isSimpleIdentifier,
loadDep
} from './utils' } from './utils'
import { isString, isArray } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { TO_STRING, CREATE_VNODE, COMMENT } from './runtimeConstants' import { TO_STRING, CREATE_VNODE, COMMENT } from './runtimeConstants'
@ -97,7 +98,7 @@ function createCodegenContext(
map: map:
__BROWSER__ || !sourceMap __BROWSER__ || !sourceMap
? undefined ? undefined
: new (require('source-map')).SourceMapGenerator(), : new (loadDep('source-map')).SourceMapGenerator(),
helper(name) { helper(name) {
return prefixIdentifiers ? name : `_${name}` return prefixIdentifiers ? name : `_${name}`

View File

@ -41,7 +41,9 @@ export const transformIf = createStructuralDirectiveTransform(
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim()) (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) { ) {
const loc = dir.exp ? dir.exp.loc : node.loc 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) dir.exp = createSimpleExpression(`true`, false, loc)
} }

View File

@ -29,7 +29,7 @@ import { PropsExpression } from './transforms/transformElement'
let _parse: typeof parse let _parse: typeof parse
let _walk: typeof walk let _walk: typeof walk
function loadDep(name: string) { export function loadDep(name: string) {
if (typeof process !== 'undefined' && isFunction(require)) { if (typeof process !== 'undefined' && isFunction(require)) {
return require(name) return require(name)
} else { } else {

View 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"> <link rel="stylesheet" data-name="vs/editor/editor.main" href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css">
<style> <link rel="stylesheet" href="./style.css">
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>
<div id="header"></div>
<div id="source" class="editor"></div> <div id="source" class="editor"></div>
<div id="output" class="editor"></div> <div id="output" class="editor"></div>
<script src="../../node_modules/acorn/dist/acorn.js"></script> <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/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="../../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./dist/template-explorer.global.js"></script> <script src="./dist/template-explorer.global.js"></script>
<script> <script>
window._deps = { window._deps = {
acorn, acorn,
'estree-walker': estreeWalker 'estree-walker': estreeWalker,
'source-map': sourceMap
} }
require.config({ require.config({

View File

@ -1,5 +1,8 @@
import * as m from 'monaco-editor' 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 const self = window as any
@ -9,30 +12,64 @@ self.init = () => {
decodeURIComponent(window.location.hash.slice(1)) || decodeURIComponent(window.location.hash.slice(1)) ||
`<div>{{ foo + bar }}</div>` `<div>{{ foo + bar }}</div>`
self.compilerOptions = { let lastSuccessfulCode: string = `/* See console for error */`
mode: 'module', let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
prefixIdentifiers: true,
hoistStatic: true
}
function compileCode(source: string): string { function compileCode(source: string): string {
console.clear() console.clear()
try { try {
const { code, ast } = compile(source, self.compilerOptions) const { code, ast, map } = compile(source, {
filename: 'template.vue',
console.log(ast) ...compilerOptions,
return code 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) { } catch (e) {
console.error(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
}
function reCompile() {
const src = editor.getValue()
window.location.hash = encodeURIComponent(src)
const res = compileCode(src)
if (res) {
output.setValue(res)
} }
} }
const sharedOptions = { const sharedEditorOptions = {
theme: 'vs-dark', theme: 'vs-dark',
fontSize: 14, fontSize: 14,
wordWrap: 'on', wordWrap: 'on',
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
renderWhitespace: 'selection',
contextmenu: false,
minimap: { minimap: {
enabled: false enabled: false
} }
@ -43,41 +80,134 @@ self.init = () => {
{ {
value: persistedContent, value: persistedContent,
language: 'html', language: 'html',
...sharedOptions ...sharedEditorOptions
} }
) )
const model = editor.getModel()! editor.getModel()!.updateOptions({
model.updateOptions({
tabSize: 2 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( const output = monaco.editor.create(
document.getElementById('output') as HTMLElement, document.getElementById('output') as HTMLElement,
{ {
value: compileCode(persistedContent), value: '',
language: 'javascript', language: 'javascript',
readOnly: true, readOnly: true,
...sharedOptions ...sharedEditorOptions
} }
) )
output.getModel()!.updateOptions({ output.getModel()!.updateOptions({
tabSize: 2 tabSize: 2
}) })
// handle resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
editor.layout() editor.layout()
output.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
} }

View File

@ -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)
}

View File

@ -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);
}