monacoEditor.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /**
  2. * Copyright (c) 2013-Now https://jeesite.com All rights reserved.
  3. * No deletion without permission, or be held responsible to law.
  4. * @author ThinkGem
  5. */
  6. import { HtmlTagDescriptor, Connect, ResolvedConfig, PluginOption } from 'vite';
  7. import fs from 'fs';
  8. import path from 'path';
  9. import esbuild from 'esbuild';
  10. export function configMonacoEditorPlugin(): PluginOption {
  11. return monacoEditorPluginJeeSite({
  12. languageWorkers: ['editorWorkerService', 'json', 'html'],
  13. customDistPath: (root, buildOutDir) => `${buildOutDir}/monaco`,
  14. publicPath: 'monaco',
  15. });
  16. }
  17. // Copyright (c) 2021 vdesjs, thinkgem
  18. //
  19. // This file contains code derived from an MIT-licensed project.
  20. // The MIT License applies **only to the content of this file** and does not
  21. // extend to the rest of the project.
  22. //
  23. // Permission is hereby granted, free of charge, to any person obtaining a copy
  24. // of this software and associated documentation files (the "Software"), to deal
  25. // in the Software without restriction, including without limitation the rights
  26. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  27. // copies of the Software, and to permit persons to whom the Software is
  28. // furnished to do so, subject to the following conditions:
  29. //
  30. // The above copyright notice and this permission notice shall be included in all
  31. // copies or substantial portions of the Software.
  32. //
  33. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  34. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  35. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  36. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  37. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  38. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  39. // SOFTWARE.
  40. // src/languageWork.ts
  41. interface IWorkerDefinition {
  42. label: string;
  43. entry: string;
  44. }
  45. const languageWorkAttr: IWorkerDefinition[] = [
  46. {
  47. label: 'editorWorkerService',
  48. entry: 'monaco-editor/esm/vs/editor/editor.worker.js',
  49. },
  50. {
  51. label: 'css',
  52. entry: 'monaco-editor/esm/vs/language/css/css.worker.js',
  53. },
  54. {
  55. label: 'html',
  56. entry: 'monaco-editor/esm/vs/language/html/html.worker.js',
  57. },
  58. {
  59. label: 'json',
  60. entry: 'monaco-editor/esm/vs/language/json/json.worker.js',
  61. },
  62. {
  63. label: 'typescript',
  64. entry: 'monaco-editor/esm/vs/language/typescript/ts.worker.js',
  65. },
  66. ];
  67. const languageWorksByLabel: { [language: string]: IWorkerDefinition } = {};
  68. languageWorkAttr.forEach((languageWork) => (languageWorksByLabel[languageWork.label] = languageWork));
  69. type EditorLanguageWorks = 'css' | 'html' | 'json' | 'typescript' | 'editorWorkerService';
  70. // src/workerMiddleware.ts
  71. function getFilenameByEntry(entry: string) {
  72. entry = path.basename(entry, '.js');
  73. return entry + '.bundle.js';
  74. }
  75. const cacheDir = 'node_modules/.monaco/';
  76. function getWorkPath(works: IWorkerDefinition[], options: IMonacoEditorOpts, config: ResolvedConfig) {
  77. const workerPaths: Record<string, any> = {};
  78. for (const work of works) {
  79. if (isCDN(options.publicPath ?? '')) {
  80. workerPaths[work.label] = options.publicPath + '/' + getFilenameByEntry(work.entry);
  81. } else {
  82. workerPaths[work.label] = config.base + options.publicPath + '/' + getFilenameByEntry(work.entry);
  83. }
  84. }
  85. if (workerPaths['typescript']) {
  86. // javascript shares the same worker
  87. workerPaths['javascript'] = workerPaths['typescript'];
  88. }
  89. if (workerPaths['css']) {
  90. // scss and less share the same worker
  91. workerPaths['less'] = workerPaths['css'];
  92. workerPaths['scss'] = workerPaths['css'];
  93. }
  94. if (workerPaths['html']) {
  95. // handlebars, razor and html share the same worker
  96. workerPaths['handlebars'] = workerPaths['html'];
  97. workerPaths['razor'] = workerPaths['html'];
  98. }
  99. return workerPaths;
  100. }
  101. function workerMiddleware(middlewares: Connect.Server, config: ResolvedConfig, options: IMonacoEditorOpts): void {
  102. const works = getWorks(options);
  103. if (!works) {
  104. throw new Error('No work definition found.');
  105. }
  106. // clear cacheDir
  107. if (fs.existsSync(cacheDir)) {
  108. fs.rmSync(cacheDir, { recursive: true, force: true } as fs.RmDirOptions);
  109. }
  110. for (const work of works) {
  111. middlewares.use(config.base + options.publicPath + '/' + getFilenameByEntry(work.entry), function (req, res, next) {
  112. if (!fs.existsSync(cacheDir + getFilenameByEntry(work.entry))) {
  113. esbuild.buildSync({
  114. entryPoints: [resolveMonacoPath(work.entry)],
  115. bundle: true,
  116. outfile: cacheDir + getFilenameByEntry(work.entry),
  117. });
  118. }
  119. const contentBuffer = fs.readFileSync(cacheDir + getFilenameByEntry(work.entry));
  120. res.setHeader('Content-Type', 'text/javascript');
  121. res.end(contentBuffer);
  122. });
  123. }
  124. }
  125. // src/index.ts
  126. /**
  127. * Return a resolved path for a given Monaco file.
  128. */
  129. function resolveMonacoPath(filePath: string): string {
  130. try {
  131. const fullFilePath = path.resolve(path.join(process.cwd(), 'node_modules', filePath));
  132. if (fs.existsSync(fullFilePath)) {
  133. return fullFilePath;
  134. }
  135. return filePath;
  136. } catch (err) {
  137. throw err;
  138. }
  139. }
  140. function getWorks(options: IMonacoEditorOpts) {
  141. let works: IWorkerDefinition[] | undefined = options.languageWorkers?.map((work) => languageWorksByLabel[work]);
  142. works && options.customWorkers && works.push(...options.customWorkers);
  143. return works;
  144. }
  145. interface IMonacoEditorOpts {
  146. /**
  147. * include only a subset of the languageWorkers supported.
  148. */
  149. languageWorkers?: EditorLanguageWorks[];
  150. customWorkers?: IWorkerDefinition[];
  151. /**
  152. * Override the public path from which files generated by this plugin will be served.
  153. * This wins out over Webpack's dynamic runtime path and can be useful to avoid attempting to load workers cross-
  154. * origin when using a CDN for other static resources.
  155. * Use e.g. '/' if you want to load your resources from the current origin.
  156. */
  157. publicPath?: string;
  158. customDistPath?: (root: string, buildOutDir: string, base: string) => string;
  159. forceBuildCDN?: boolean;
  160. /**
  161. * Specify whether the editor API should be exposed through a global `monaco` object or not. This
  162. * option is applicable to `0.22.0` and newer version of `monaco-editor`. Since `0.22.0`, the ESM
  163. * version of the monaco editor does no longer define a global `monaco` object unless
  164. * `global.MonacoEnvironment = { globalAPI: true }` is set ([change
  165. * log](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md#0220-29012021)).
  166. */
  167. globalAPI?: boolean;
  168. }
  169. function monacoEditorPluginJeeSite(options?: IMonacoEditorOpts): PluginOption {
  170. const languageWorkers = options?.languageWorkers ?? (Object.keys(languageWorksByLabel) as EditorLanguageWorks[]);
  171. const publicPath = options?.publicPath ?? 'monacoeditorwork';
  172. const globalAPI = options?.globalAPI ?? false;
  173. const customWorkers = options?.customWorkers ?? [];
  174. const forceBuildCDN = options?.forceBuildCDN ?? false;
  175. options = {
  176. ...options,
  177. languageWorkers,
  178. publicPath,
  179. globalAPI,
  180. customWorkers,
  181. forceBuildCDN,
  182. };
  183. let resolvedConfig: ResolvedConfig;
  184. return {
  185. name: 'vite-plugin-moncao-editor-jeesite',
  186. configResolved(getResolvedConfig) {
  187. resolvedConfig = getResolvedConfig;
  188. },
  189. configureServer(server) {
  190. if (isCDN(publicPath)) {
  191. return;
  192. }
  193. workerMiddleware(server.middlewares, resolvedConfig, options);
  194. },
  195. transformIndexHtml(html) {
  196. const works = getWorks(options);
  197. if (!works) {
  198. throw new Error('No work definition found.');
  199. }
  200. const workerPaths = getWorkPath(works, options, resolvedConfig);
  201. const globals = {
  202. MonacoEnvironment: `(function (paths) {
  203. return {
  204. globalAPI: ${globalAPI},
  205. getWorkerUrl : function (moduleId, label) {
  206. var result = paths[label];
  207. if (/^((http:)|(https:)|(file:)|(\\/\\/))/.test(result)) {
  208. var currentUrl = String(window.location);
  209. var currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
  210. if (result.substring(0, currentOrigin.length) !== currentOrigin) {
  211. var js = '/*' + label + '*/importScripts("' + result + '");';
  212. var blob = new Blob([js], { type: 'application/javascript' });
  213. return URL.createObjectURL(blob);
  214. }
  215. }
  216. return result;
  217. }
  218. };
  219. })(${JSON.stringify(workerPaths, null, 2)})`,
  220. } as Record<string, any>;
  221. const descriptor: HtmlTagDescriptor[] = [
  222. {
  223. tag: 'script',
  224. children: Object.keys(globals)
  225. .map((key) => `self[${JSON.stringify(key)}] = ${globals[key]};`)
  226. .join('\n'),
  227. injectTo: 'head-prepend',
  228. },
  229. ];
  230. return descriptor;
  231. },
  232. writeBundle() {
  233. // 是cdn地址并且没有强制构建worker cdn则返回
  234. if (isCDN(publicPath) && !forceBuildCDN) {
  235. return;
  236. }
  237. const works = getWorks(options);
  238. const distPath = options.customDistPath
  239. ? options.customDistPath(resolvedConfig.root, resolvedConfig.build.outDir, resolvedConfig.base)
  240. : path.join(resolvedConfig.root, resolvedConfig.build.outDir, resolvedConfig.base, options.publicPath ?? '');
  241. // console.log("distPath", distPath)
  242. // write publicPath
  243. if (!fs.existsSync(distPath)) {
  244. fs.mkdirSync(distPath, {
  245. recursive: true,
  246. });
  247. }
  248. if (!works) {
  249. throw new Error('No work definition found.');
  250. }
  251. for (const work of works) {
  252. if (!fs.existsSync(cacheDir + getFilenameByEntry(work.entry))) {
  253. const entryPath = resolveMonacoPath(work.entry);
  254. esbuild.buildSync({
  255. entryPoints: [entryPath],
  256. bundle: true,
  257. outfile: cacheDir + getFilenameByEntry(work.entry),
  258. });
  259. }
  260. const contentBuffer = fs.readFileSync(cacheDir + getFilenameByEntry(work.entry));
  261. const workDistPath = path.resolve(distPath, getFilenameByEntry(work.entry));
  262. fs.writeFileSync(workDistPath, contentBuffer);
  263. }
  264. },
  265. };
  266. }
  267. function isCDN(publicPath: string) {
  268. if (/^((http:)|(https:)|(file:)|(\/\/))/.test(publicPath)) {
  269. return true;
  270. }
  271. return false;
  272. }