index.ts 7.4 KB

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