|
@@ -0,0 +1,261 @@
|
|
|
+import * as path from 'path';
|
|
|
+import * as webpack from 'webpack';
|
|
|
+import * as loaderUtils from 'loader-utils';
|
|
|
+import * as fs from 'fs';
|
|
|
+import { AddWorkerEntryPointPlugin } from './plugins/AddWorkerEntryPointPlugin';
|
|
|
+import { languagesById } from './languages';
|
|
|
+import { featuresById } from './features';
|
|
|
+import { IFeatureDefinition } from './types';
|
|
|
+
|
|
|
+const INCLUDE_LOADER_PATH = require.resolve('./loaders/include');
|
|
|
+
|
|
|
+const EDITOR_MODULE: IFeatureDefinition = {
|
|
|
+ label: 'editorWorkerService',
|
|
|
+ entry: undefined,
|
|
|
+ worker: {
|
|
|
+ id: 'vs/editor/editor',
|
|
|
+ entry: 'vs/editor/editor.worker',
|
|
|
+ fallback: undefined
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * Return a resolved path for a given Monaco file.
|
|
|
+ */
|
|
|
+function resolveMonacoPath(filePath: string): string {
|
|
|
+ return require.resolve(path.join('monaco-editor/esm', filePath));
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Return the interpolated final filename for a worker, respecting the file name template.
|
|
|
+ */
|
|
|
+function getWorkerFilename(filename: string, entry: string): string {
|
|
|
+ return loaderUtils.interpolateName(<any>{ resourcePath: entry }, filename, {
|
|
|
+ content: fs.readFileSync(resolveMonacoPath(entry))
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function getFeaturesIds(userFeatures: string[]): string[] {
|
|
|
+ function notContainedIn(arr: string[]) {
|
|
|
+ return (element: string) => arr.indexOf(element) === -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ let featuresIds: string[];
|
|
|
+
|
|
|
+ if (userFeatures.length) {
|
|
|
+ const excludedFeatures = userFeatures.filter(f => f[0] === '!').map(f => f.slice(1));
|
|
|
+ if (excludedFeatures.length) {
|
|
|
+ featuresIds = Object.keys(featuresById).filter(notContainedIn(excludedFeatures))
|
|
|
+ } else {
|
|
|
+ featuresIds = userFeatures;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ featuresIds = Object.keys(featuresById);
|
|
|
+ }
|
|
|
+
|
|
|
+ return featuresIds;
|
|
|
+}
|
|
|
+
|
|
|
+interface IMonacoEditorWebpackPluginOpts {
|
|
|
+ /**
|
|
|
+ * Include only a subset of the languages supported.
|
|
|
+ */
|
|
|
+ languages?: string[];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Include only a subset of the editor features.
|
|
|
+ * Use e.g. '!contextmenu' to exclude a certain feature.
|
|
|
+ */
|
|
|
+ features?: string[];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Specify a filename template to use for generated files.
|
|
|
+ * Use e.g. '[name].worker.[contenthash].js' to include content-based hashes.
|
|
|
+ */
|
|
|
+ filename?: string;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Override the public path from which files generated by this plugin will be served.
|
|
|
+ * This wins out over Webpack's dynamic runtime path and can be useful to avoid attempting to load workers cross-
|
|
|
+ * origin when using a CDN for other static resources.
|
|
|
+ * Use e.g. '/' if you want to load your resources from the current origin.
|
|
|
+ */
|
|
|
+ publicPath?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface IInternalMonacoEditorWebpackPluginOpts {
|
|
|
+ languages: IFeatureDefinition[];
|
|
|
+ features: IFeatureDefinition[];
|
|
|
+ filename: string;
|
|
|
+ publicPath: string;
|
|
|
+}
|
|
|
+
|
|
|
+class MonacoEditorWebpackPlugin implements webpack.Plugin {
|
|
|
+
|
|
|
+ private readonly options: IInternalMonacoEditorWebpackPluginOpts;
|
|
|
+
|
|
|
+ constructor(options: IMonacoEditorWebpackPluginOpts = {}) {
|
|
|
+ const languages = options.languages || Object.keys(languagesById);
|
|
|
+ const features = getFeaturesIds(options.features || []);
|
|
|
+ this.options = {
|
|
|
+ languages: coalesce(languages.map(id => languagesById[id])),
|
|
|
+ features: coalesce(features.map(id => featuresById[id])),
|
|
|
+ filename: options.filename || "[name].worker.js",
|
|
|
+ publicPath: options.publicPath || '',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ apply(compiler: webpack.Compiler): void {
|
|
|
+ const { languages, features, filename, publicPath } = this.options;
|
|
|
+ const compilationPublicPath = getCompilationPublicPath(compiler);
|
|
|
+ const modules = [EDITOR_MODULE].concat(languages).concat(features);
|
|
|
+ const workers: ILabeledWorkerDefinition[] = coalesce(modules.map(
|
|
|
+ ({ label, worker }) => worker && (mixin({ label }, worker))
|
|
|
+ ));
|
|
|
+ const rules = createLoaderRules(languages, features, workers, filename, publicPath, compilationPublicPath);
|
|
|
+ const plugins = createPlugins(workers, filename);
|
|
|
+ addCompilerRules(compiler, rules);
|
|
|
+ addCompilerPlugins(compiler, plugins);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface ILabeledWorkerDefinition {
|
|
|
+ label: string;
|
|
|
+ id: string;
|
|
|
+ entry: string;
|
|
|
+ fallback: string | undefined;
|
|
|
+}
|
|
|
+
|
|
|
+function addCompilerRules(compiler: webpack.Compiler, rules: webpack.RuleSetRule[]): void {
|
|
|
+ const compilerOptions = compiler.options;
|
|
|
+ if (!compilerOptions.module) {
|
|
|
+ compilerOptions.module = { rules: rules };
|
|
|
+ } else {
|
|
|
+ const moduleOptions = compilerOptions.module;
|
|
|
+ moduleOptions.rules = (moduleOptions.rules || []).concat(rules);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function addCompilerPlugins(compiler: webpack.Compiler, plugins: webpack.Plugin[]) {
|
|
|
+ plugins.forEach((plugin) => plugin.apply(compiler));
|
|
|
+}
|
|
|
+
|
|
|
+function getCompilationPublicPath(compiler: webpack.Compiler): string {
|
|
|
+ return compiler.options.output && compiler.options.output.publicPath || '';
|
|
|
+}
|
|
|
+
|
|
|
+function createLoaderRules(languages: IFeatureDefinition[], features: IFeatureDefinition[], workers: ILabeledWorkerDefinition[], filename: string, pluginPublicPath: string, compilationPublicPath: string): webpack.RuleSetRule[] {
|
|
|
+ if (!languages.length && !features.length) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ const languagePaths = flatArr(coalesce(languages.map(({ entry }) => entry)));
|
|
|
+ const featurePaths = flatArr(coalesce(features.map(({ entry }) => entry)));
|
|
|
+ const workerPaths = fromPairs(workers.map(({ label, entry }) => [label, getWorkerFilename(filename, entry)]));
|
|
|
+ if (workerPaths['typescript']) {
|
|
|
+ // javascript shares the same worker
|
|
|
+ workerPaths['javascript'] = workerPaths['typescript'];
|
|
|
+ }
|
|
|
+ if (workerPaths['css']) {
|
|
|
+ // scss and less share the same worker
|
|
|
+ workerPaths['less'] = workerPaths['css'];
|
|
|
+ workerPaths['scss'] = workerPaths['css'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (workerPaths['html']) {
|
|
|
+ // handlebars, razor and html share the same worker
|
|
|
+ workerPaths['handlebars'] = workerPaths['html'];
|
|
|
+ workerPaths['razor'] = workerPaths['html'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Determine the public path from which to load worker JS files. In order of precedence:
|
|
|
+ // 1. Plugin-specific public path.
|
|
|
+ // 2. Dynamic runtime public path.
|
|
|
+ // 3. Compilation public path.
|
|
|
+ const pathPrefix = Boolean(pluginPublicPath)
|
|
|
+ ? JSON.stringify(pluginPublicPath)
|
|
|
+ : `typeof __webpack_public_path__ === 'string' ` +
|
|
|
+ `? __webpack_public_path__ ` +
|
|
|
+ `: ${JSON.stringify(compilationPublicPath)}`
|
|
|
+
|
|
|
+ const globals = {
|
|
|
+ 'MonacoEnvironment': `(function (paths) {
|
|
|
+ function stripTrailingSlash(str) {
|
|
|
+ return str.replace(/\\/$/, '');
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ getWorkerUrl: function (moduleId, label) {
|
|
|
+ var pathPrefix = ${pathPrefix};
|
|
|
+ return (pathPrefix ? stripTrailingSlash(pathPrefix) + '/' : '') + paths[label];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ })(${JSON.stringify(workerPaths, null, 2)})`,
|
|
|
+ };
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ test: /monaco-editor[/\\]esm[/\\]vs[/\\]editor[/\\]editor.(api|main).js/,
|
|
|
+ use: [{
|
|
|
+ loader: INCLUDE_LOADER_PATH,
|
|
|
+ options: {
|
|
|
+ globals,
|
|
|
+ pre: featurePaths.map((importPath) => resolveMonacoPath(importPath)),
|
|
|
+ post: languagePaths.map((importPath) => resolveMonacoPath(importPath)),
|
|
|
+ },
|
|
|
+ }],
|
|
|
+ },
|
|
|
+ ];
|
|
|
+}
|
|
|
+
|
|
|
+function createPlugins(workers: ILabeledWorkerDefinition[], filename: string): AddWorkerEntryPointPlugin[] {
|
|
|
+ return (
|
|
|
+ (<AddWorkerEntryPointPlugin[]>[])
|
|
|
+ .concat(uniqBy(workers, ({ id }) => id).map(({ id, entry }) =>
|
|
|
+ new AddWorkerEntryPointPlugin({
|
|
|
+ id,
|
|
|
+ entry: resolveMonacoPath(entry),
|
|
|
+ filename: getWorkerFilename(filename, entry),
|
|
|
+ plugins: [
|
|
|
+ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ ))
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function flatArr<T>(items: (T | T[])[]): T[] {
|
|
|
+ return items.reduce((acc: T[], item: T | T[]) => {
|
|
|
+ if (Array.isArray(item)) {
|
|
|
+ return (<T[]>[]).concat(acc).concat(item);
|
|
|
+ }
|
|
|
+ return (<T[]>[]).concat(acc).concat([item]);
|
|
|
+ }, <T[]>[]);
|
|
|
+}
|
|
|
+
|
|
|
+function fromPairs<T>(values: [string, T][]): { [key: string]: T; } {
|
|
|
+ return values.reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), <{ [key: string]: T; }>{});
|
|
|
+}
|
|
|
+
|
|
|
+function uniqBy<T>(items: T[], iteratee: (item: T) => string): T[] {
|
|
|
+ const keys: { [key: string]: boolean; } = {};
|
|
|
+ return items.reduce((acc, item) => {
|
|
|
+ const key = iteratee(item);
|
|
|
+ if (key in keys) { return acc; }
|
|
|
+ keys[key] = true;
|
|
|
+ acc.push(item);
|
|
|
+ return acc;
|
|
|
+ }, <T[]>[]);
|
|
|
+}
|
|
|
+
|
|
|
+function mixin<DEST, SRC>(dest: DEST, src: SRC): DEST & SRC {
|
|
|
+ for (let prop in src) {
|
|
|
+ if (Object.hasOwnProperty.call(src, prop)) {
|
|
|
+ (<any>dest)[prop] = src[prop];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return <any>dest;
|
|
|
+}
|
|
|
+
|
|
|
+function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
|
|
|
+ return <T[]>array.filter(Boolean);
|
|
|
+}
|
|
|
+
|
|
|
+export = MonacoEditorWebpackPlugin;
|