index.ts 7.7 KB

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