index.ts 6.5 KB

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