index.ts 7.3 KB

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