index.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import type * as webpack from 'webpack';
  2. import * as path from 'path';
  3. import * as loaderUtils from 'loader-utils';
  4. import * as fs from 'fs';
  5. import { AddWorkerEntryPointPlugin } from './plugins/AddWorkerEntryPointPlugin';
  6. import { IFeatureDefinition } from './types';
  7. import { ILoaderOptions } from './loaders/include';
  8. import { EditorLanguage, EditorFeature, NegatedEditorFeature } from 'monaco-editor/esm/metadata';
  9. const INCLUDE_LOADER_PATH = require.resolve('./loaders/include');
  10. const EDITOR_MODULE: IFeatureDefinition = {
  11. label: 'editorWorkerService',
  12. entry: undefined,
  13. worker: {
  14. id: 'vs/editor/editor',
  15. entry: 'vs/editor/editor.worker'
  16. }
  17. };
  18. /**
  19. * Return a resolved path for a given Monaco file.
  20. */
  21. function resolveMonacoPath(filePath: string, monacoEditorPath: string | undefined): string {
  22. if (monacoEditorPath) {
  23. return require.resolve(path.join(monacoEditorPath, 'esm', filePath));
  24. }
  25. try {
  26. return require.resolve(path.join('monaco-editor/esm', filePath));
  27. } catch (err) {}
  28. try {
  29. return require.resolve(path.join(process.cwd(), 'node_modules/monaco-editor/esm', filePath));
  30. } catch (err) {}
  31. return require.resolve(filePath);
  32. }
  33. /**
  34. * Return the interpolated final filename for a worker, respecting the file name template.
  35. */
  36. function getWorkerFilename(
  37. filename: string,
  38. entry: string,
  39. monacoEditorPath: string | undefined
  40. ): string {
  41. return loaderUtils.interpolateName(<any>{ resourcePath: entry }, filename, {
  42. content: fs.readFileSync(resolveMonacoPath(entry, monacoEditorPath))
  43. });
  44. }
  45. interface EditorMetadata {
  46. features: IFeatureDefinition[];
  47. languages: IFeatureDefinition[];
  48. }
  49. function getEditorMetadata(monacoEditorPath: string | undefined): EditorMetadata {
  50. const metadataPath = resolveMonacoPath('metadata.js', monacoEditorPath);
  51. return require(metadataPath);
  52. }
  53. function resolveDesiredFeatures(
  54. metadata: EditorMetadata,
  55. userFeatures: (EditorFeature | NegatedEditorFeature)[] | undefined
  56. ): IFeatureDefinition[] {
  57. const featuresById: { [feature: string]: IFeatureDefinition } = {};
  58. metadata.features.forEach((feature) => (featuresById[feature.label] = feature));
  59. function notContainedIn(arr: string[]) {
  60. return (element: string) => arr.indexOf(element) === -1;
  61. }
  62. let featuresIds: string[];
  63. if (userFeatures && userFeatures.length) {
  64. const excludedFeatures = userFeatures.filter((f) => f[0] === '!').map((f) => f.slice(1));
  65. if (excludedFeatures.length) {
  66. featuresIds = Object.keys(featuresById).filter(notContainedIn(excludedFeatures));
  67. } else {
  68. featuresIds = userFeatures;
  69. }
  70. } else {
  71. featuresIds = Object.keys(featuresById);
  72. }
  73. return coalesce(featuresIds.map((id) => featuresById[id]));
  74. }
  75. function resolveDesiredLanguages(
  76. metadata: EditorMetadata,
  77. userLanguages: EditorLanguage[] | undefined,
  78. userCustomLanguages: IFeatureDefinition[] | undefined
  79. ): IFeatureDefinition[] {
  80. const languagesById: { [language: string]: IFeatureDefinition } = {};
  81. metadata.languages.forEach((language) => (languagesById[language.label] = language));
  82. const languages = userLanguages || Object.keys(languagesById);
  83. return coalesce(languages.map((id) => languagesById[id])).concat(userCustomLanguages || []);
  84. }
  85. declare namespace MonacoEditorWebpackPlugin {
  86. interface IMonacoEditorWebpackPluginOpts {
  87. /**
  88. * Include only a subset of the languages supported.
  89. */
  90. languages?: EditorLanguage[];
  91. /**
  92. * Custom languages (outside of the ones shipped with the `monaco-editor`).
  93. */
  94. customLanguages?: IFeatureDefinition[];
  95. /**
  96. * Include only a subset of the editor features.
  97. * Use e.g. '!contextmenu' to exclude a certain feature.
  98. */
  99. features?: (EditorFeature | NegatedEditorFeature)[];
  100. /**
  101. * Specify a filename template to use for generated files.
  102. * Use e.g. '[name].worker.[contenthash].js' to include content-based hashes.
  103. */
  104. filename?: string;
  105. /**
  106. * The absolute file system path to the monaco-editor npm module.
  107. * Use e.g. `C:\projects\my-project\node-modules\monaco-editor`
  108. */
  109. monacoEditorPath?: string;
  110. /**
  111. * Override the public path from which files generated by this plugin will be served.
  112. * This wins out over Webpack's dynamic runtime path and can be useful to avoid attempting to load workers cross-
  113. * origin when using a CDN for other static resources.
  114. * Use e.g. '/' if you want to load your resources from the current origin.
  115. */
  116. publicPath?: string;
  117. /**
  118. * Specify whether the editor API should be exposed through a global `monaco` object or not. This
  119. * option is applicable to `0.22.0` and newer version of `monaco-editor`. Since `0.22.0`, the ESM
  120. * version of the monaco editor does no longer define a global `monaco` object unless
  121. * `global.MonacoEnvironment = { globalAPI: true }` is set ([change
  122. * log](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md#0220-29012021)).
  123. */
  124. globalAPI?: boolean;
  125. }
  126. }
  127. interface IInternalMonacoEditorWebpackPluginOpts {
  128. languages: IFeatureDefinition[];
  129. features: IFeatureDefinition[];
  130. filename: string;
  131. monacoEditorPath: string | undefined;
  132. publicPath: string;
  133. globalAPI: boolean;
  134. }
  135. class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance {
  136. private readonly options: IInternalMonacoEditorWebpackPluginOpts;
  137. constructor(options: MonacoEditorWebpackPlugin.IMonacoEditorWebpackPluginOpts = {}) {
  138. const monacoEditorPath = options.monacoEditorPath;
  139. const metadata = getEditorMetadata(monacoEditorPath);
  140. const languages = resolveDesiredLanguages(metadata, options.languages, options.customLanguages);
  141. const features = resolveDesiredFeatures(metadata, options.features);
  142. this.options = {
  143. languages,
  144. features,
  145. filename: options.filename || '[name].worker.js',
  146. monacoEditorPath,
  147. publicPath: options.publicPath || '',
  148. globalAPI: options.globalAPI || false
  149. };
  150. }
  151. apply(compiler: webpack.Compiler): void {
  152. const { languages, features, filename, monacoEditorPath, publicPath, globalAPI } = this.options;
  153. const compilationPublicPath = getCompilationPublicPath(compiler);
  154. const modules = [EDITOR_MODULE].concat(languages).concat(features);
  155. const workers: ILabeledWorkerDefinition[] = [];
  156. modules.forEach((module) => {
  157. if (module.worker) {
  158. workers.push({
  159. label: module.label,
  160. id: module.worker.id,
  161. entry: module.worker.entry
  162. });
  163. }
  164. });
  165. const rules = createLoaderRules(
  166. languages,
  167. features,
  168. workers,
  169. filename,
  170. monacoEditorPath,
  171. publicPath,
  172. compilationPublicPath,
  173. globalAPI
  174. );
  175. const plugins = createPlugins(compiler, workers, filename, monacoEditorPath);
  176. addCompilerRules(compiler, rules);
  177. addCompilerPlugins(compiler, plugins);
  178. }
  179. }
  180. interface ILabeledWorkerDefinition {
  181. label: string;
  182. id: string;
  183. entry: string;
  184. }
  185. function addCompilerRules(compiler: webpack.Compiler, rules: webpack.RuleSetRule[]): void {
  186. const compilerOptions = compiler.options;
  187. if (!compilerOptions.module) {
  188. compilerOptions.module = <any>{ rules: rules };
  189. } else {
  190. const moduleOptions = compilerOptions.module;
  191. moduleOptions.rules = (moduleOptions.rules || []).concat(rules);
  192. }
  193. }
  194. function addCompilerPlugins(compiler: webpack.Compiler, plugins: webpack.WebpackPluginInstance[]) {
  195. plugins.forEach((plugin) => plugin.apply(compiler));
  196. }
  197. function getCompilationPublicPath(compiler: webpack.Compiler): string {
  198. if (compiler.options.output && compiler.options.output.publicPath) {
  199. if (typeof compiler.options.output.publicPath === 'string') {
  200. return compiler.options.output.publicPath;
  201. } else {
  202. console.warn(`Cannot handle options.publicPath (expected a string)`);
  203. }
  204. }
  205. return '';
  206. }
  207. function createLoaderRules(
  208. languages: IFeatureDefinition[],
  209. features: IFeatureDefinition[],
  210. workers: ILabeledWorkerDefinition[],
  211. filename: string,
  212. monacoEditorPath: string | undefined,
  213. pluginPublicPath: string,
  214. compilationPublicPath: string,
  215. globalAPI: boolean
  216. ): webpack.RuleSetRule[] {
  217. if (!languages.length && !features.length) {
  218. return [];
  219. }
  220. const languagePaths = flatArr(coalesce(languages.map((language) => language.entry)));
  221. const featurePaths = flatArr(coalesce(features.map((feature) => feature.entry)));
  222. const workerPaths = fromPairs(
  223. workers.map(({ label, entry }) => [label, getWorkerFilename(filename, entry, monacoEditorPath)])
  224. );
  225. if (workerPaths['typescript']) {
  226. // javascript shares the same worker
  227. workerPaths['javascript'] = workerPaths['typescript'];
  228. }
  229. if (workerPaths['css']) {
  230. // scss and less share the same worker
  231. workerPaths['less'] = workerPaths['css'];
  232. workerPaths['scss'] = workerPaths['css'];
  233. }
  234. if (workerPaths['html']) {
  235. // handlebars, razor and html share the same worker
  236. workerPaths['handlebars'] = workerPaths['html'];
  237. workerPaths['razor'] = workerPaths['html'];
  238. }
  239. // Determine the public path from which to load worker JS files. In order of precedence:
  240. // 1. Plugin-specific public path.
  241. // 2. Dynamic runtime public path.
  242. // 3. Compilation public path.
  243. const pathPrefix = Boolean(pluginPublicPath)
  244. ? JSON.stringify(pluginPublicPath)
  245. : `typeof __webpack_public_path__ === 'string' ` +
  246. `? __webpack_public_path__ ` +
  247. `: ${JSON.stringify(compilationPublicPath)}`;
  248. const globals = {
  249. MonacoEnvironment: `(function (paths) {
  250. function stripTrailingSlash(str) {
  251. return str.replace(/\\/$/, '');
  252. }
  253. return {
  254. globalAPI: ${globalAPI},
  255. getWorkerUrl: function (moduleId, label) {
  256. var pathPrefix = ${pathPrefix};
  257. var result = (pathPrefix ? stripTrailingSlash(pathPrefix) + '/' : '') + paths[label];
  258. if (/^((http:)|(https:)|(file:)|(\\/\\/))/.test(result)) {
  259. var currentUrl = String(window.location);
  260. var currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
  261. if (result.substring(0, currentOrigin.length) !== currentOrigin) {
  262. if(/^(\\/\\/)/.test(result)) {
  263. result = window.location.protocol + result
  264. }
  265. var js = '/*' + label + '*/importScripts("' + result + '");';
  266. var blob = new Blob([js], { type: 'application/javascript' });
  267. return URL.createObjectURL(blob);
  268. }
  269. }
  270. return result;
  271. }
  272. };
  273. })(${JSON.stringify(workerPaths, null, 2)})`
  274. };
  275. const options: ILoaderOptions = {
  276. globals,
  277. pre: featurePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)),
  278. post: languagePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath))
  279. };
  280. return [
  281. {
  282. test: /esm[/\\]vs[/\\]editor[/\\]editor.(api|main).js/,
  283. use: [
  284. {
  285. loader: INCLUDE_LOADER_PATH,
  286. options
  287. }
  288. ]
  289. }
  290. ];
  291. }
  292. function createPlugins(
  293. compiler: webpack.Compiler,
  294. workers: ILabeledWorkerDefinition[],
  295. filename: string,
  296. monacoEditorPath: string | undefined
  297. ): AddWorkerEntryPointPlugin[] {
  298. const webpack = compiler.webpack ?? require('webpack');
  299. return (<AddWorkerEntryPointPlugin[]>[]).concat(
  300. workers.map(
  301. ({ id, entry }) =>
  302. new AddWorkerEntryPointPlugin({
  303. id,
  304. entry: resolveMonacoPath(entry, monacoEditorPath),
  305. filename: getWorkerFilename(filename, entry, monacoEditorPath),
  306. plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })]
  307. })
  308. )
  309. );
  310. }
  311. function flatArr<T>(items: (T | T[])[]): T[] {
  312. return items.reduce((acc: T[], item: T | T[]) => {
  313. if (Array.isArray(item)) {
  314. return (<T[]>[]).concat(acc).concat(item);
  315. }
  316. return (<T[]>[]).concat(acc).concat([item]);
  317. }, <T[]>[]);
  318. }
  319. function fromPairs<T>(values: [string, T][]): { [key: string]: T } {
  320. return values.reduce(
  321. (acc, [key, value]) => Object.assign(acc, { [key]: value }),
  322. <{ [key: string]: T }>{}
  323. );
  324. }
  325. function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
  326. return <T[]>array.filter(Boolean);
  327. }
  328. export = MonacoEditorWebpackPlugin;