index.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import * as m from 'monaco-editor'
  2. import { compile, CompilerError, CompilerOptions } from '@vue/compiler-dom'
  3. import { compile as ssrCompile } from '@vue/compiler-ssr'
  4. import { compilerOptions, initOptions, ssrMode } from './options'
  5. import { watch } from '@vue/runtime-dom'
  6. import { SourceMapConsumer } from 'source-map'
  7. declare global {
  8. interface Window {
  9. monaco: typeof m
  10. _deps: any
  11. init: () => void
  12. }
  13. }
  14. interface PersistedState {
  15. src: string
  16. ssr: boolean
  17. options: CompilerOptions
  18. }
  19. window.init = () => {
  20. const monaco = window.monaco
  21. const persistedState: PersistedState = JSON.parse(
  22. decodeURIComponent(window.location.hash.slice(1)) ||
  23. localStorage.getItem('state') ||
  24. `{}`
  25. )
  26. ssrMode.value = persistedState.ssr
  27. Object.assign(compilerOptions, persistedState.options)
  28. let lastSuccessfulCode: string = `/* See console for error */`
  29. let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
  30. function compileCode(source: string): string {
  31. console.clear()
  32. try {
  33. const errors: CompilerError[] = []
  34. const compileFn = ssrMode.value ? ssrCompile : compile
  35. const start = performance.now()
  36. const { code, ast, map } = compileFn(source, {
  37. filename: 'template.vue',
  38. ...compilerOptions,
  39. sourceMap: true,
  40. onError: err => {
  41. errors.push(err)
  42. }
  43. })
  44. console.log(`Compiled in ${(performance.now() - start).toFixed(2)}ms.`)
  45. monaco.editor.setModelMarkers(
  46. editor.getModel()!,
  47. `@vue/compiler-dom`,
  48. errors.filter(e => e.loc).map(formatError)
  49. )
  50. console.log(`AST: `, ast)
  51. lastSuccessfulCode = code + `\n\n// Check the console for the AST`
  52. lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map)
  53. lastSuccessfulMap!.computeColumnSpans()
  54. } catch (e) {
  55. console.error(e)
  56. }
  57. return lastSuccessfulCode
  58. }
  59. function formatError(err: CompilerError) {
  60. const loc = err.loc!
  61. return {
  62. severity: monaco.MarkerSeverity.Error,
  63. startLineNumber: loc.start.line,
  64. startColumn: loc.start.column,
  65. endLineNumber: loc.end.line,
  66. endColumn: loc.end.column,
  67. message: `Vue template compilation error: ${err.message}`,
  68. code: String(err.code)
  69. }
  70. }
  71. function reCompile() {
  72. const src = editor.getValue()
  73. // every time we re-compile, persist current state
  74. const state = JSON.stringify({
  75. src,
  76. ssr: ssrMode.value,
  77. options: compilerOptions
  78. } as PersistedState)
  79. localStorage.setItem('state', state)
  80. window.location.hash = encodeURIComponent(state)
  81. const res = compileCode(src)
  82. if (res) {
  83. output.setValue(res)
  84. }
  85. }
  86. const sharedEditorOptions: m.editor.IEditorConstructionOptions = {
  87. theme: 'vs-dark',
  88. fontSize: 14,
  89. wordWrap: 'on',
  90. scrollBeyondLastLine: false,
  91. renderWhitespace: 'selection',
  92. contextmenu: false,
  93. minimap: {
  94. enabled: false
  95. }
  96. }
  97. const editor = monaco.editor.create(document.getElementById('source')!, {
  98. value: persistedState.src || `<div>Hello World!</div>`,
  99. language: 'html',
  100. ...sharedEditorOptions
  101. })
  102. editor.getModel()!.updateOptions({
  103. tabSize: 2
  104. })
  105. const output = monaco.editor.create(document.getElementById('output')!, {
  106. value: '',
  107. language: 'javascript',
  108. readOnly: true,
  109. ...sharedEditorOptions
  110. })
  111. output.getModel()!.updateOptions({
  112. tabSize: 2
  113. })
  114. // handle resize
  115. window.addEventListener('resize', () => {
  116. editor.layout()
  117. output.layout()
  118. })
  119. // update compile output when input changes
  120. editor.onDidChangeModelContent(debounce(reCompile))
  121. // highlight output code
  122. let prevOutputDecos: string[] = []
  123. function clearOutputDecos() {
  124. prevOutputDecos = output.deltaDecorations(prevOutputDecos, [])
  125. }
  126. editor.onDidChangeCursorPosition(
  127. debounce(e => {
  128. clearEditorDecos()
  129. if (lastSuccessfulMap) {
  130. const pos = lastSuccessfulMap.generatedPositionFor({
  131. source: 'template.vue',
  132. line: e.position.lineNumber,
  133. column: e.position.column - 1
  134. })
  135. if (pos.line != null && pos.column != null) {
  136. prevOutputDecos = output.deltaDecorations(prevOutputDecos, [
  137. {
  138. range: new monaco.Range(
  139. pos.line,
  140. pos.column + 1,
  141. pos.line,
  142. pos.lastColumn ? pos.lastColumn + 2 : pos.column + 2
  143. ),
  144. options: {
  145. inlineClassName: `highlight`
  146. }
  147. }
  148. ])
  149. output.revealPositionInCenter({
  150. lineNumber: pos.line,
  151. column: pos.column + 1
  152. })
  153. } else {
  154. clearOutputDecos()
  155. }
  156. }
  157. }, 100)
  158. )
  159. let previousEditorDecos: string[] = []
  160. function clearEditorDecos() {
  161. previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [])
  162. }
  163. output.onDidChangeCursorPosition(
  164. debounce(e => {
  165. clearOutputDecos()
  166. if (lastSuccessfulMap) {
  167. const pos = lastSuccessfulMap.originalPositionFor({
  168. line: e.position.lineNumber,
  169. column: e.position.column - 1
  170. })
  171. if (
  172. pos.line != null &&
  173. pos.column != null &&
  174. !// ignore mock location
  175. (pos.line === 1 && pos.column === 0)
  176. ) {
  177. const translatedPos = {
  178. column: pos.column + 1,
  179. lineNumber: pos.line
  180. }
  181. previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [
  182. {
  183. range: new monaco.Range(
  184. pos.line,
  185. pos.column + 1,
  186. pos.line,
  187. pos.column + 1
  188. ),
  189. options: {
  190. isWholeLine: true,
  191. className: `highlight`
  192. }
  193. }
  194. ])
  195. editor.revealPositionInCenter(translatedPos)
  196. } else {
  197. clearEditorDecos()
  198. }
  199. }
  200. }, 100)
  201. )
  202. initOptions()
  203. watch(reCompile)
  204. }
  205. function debounce<T extends (...args: any[]) => any>(
  206. fn: T,
  207. delay: number = 300
  208. ): T {
  209. let prevTimer: number | null = null
  210. return ((...args: any[]) => {
  211. if (prevTimer) {
  212. clearTimeout(prevTimer)
  213. }
  214. prevTimer = window.setTimeout(() => {
  215. fn(...args)
  216. prevTimer = null
  217. }, delay)
  218. }) as any
  219. }