diff --git a/features.js b/features.js new file mode 100644 index 00000000..927f677e --- /dev/null +++ b/features.js @@ -0,0 +1,158 @@ +module.exports = { + accessibilityHelp: { + entry: 'vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp', + worker: undefined, + }, + bracketMatching: { + entry: 'vs/editor/contrib/bracketMatching/bracketMatching', + worker: undefined, + }, + caretOperations: { + entry: 'vs/editor/contrib/caretOperations/caretOperations', + worker: undefined, + }, + clipboard: { + entry: 'vs/editor/contrib/clipboard/clipboard', + worker: undefined, + }, + codelens: { + entry: 'vs/editor/contrib/codelens/codelensController', + worker: undefined, + }, + colorDetector: { + entry: 'vs/editor/contrib/colorPicker/colorDetector', + worker: undefined, + }, + comment: { + entry: 'vs/editor/contrib/comment/comment', + worker: undefined, + }, + contextmenu: { + entry: 'vs/editor/contrib/contextmenu/contextmenu', + worker: undefined, + }, + coreCommands: { + entry: 'vs/editor/browser/controller/coreCommands', + worker: undefined, + }, + cursorUndo: { + entry: 'vs/editor/contrib/cursorUndo/cursorUndo', + worker: undefined, + }, + dnd: { + entry: 'vs/editor/contrib/dnd/dnd', + worker: undefined, + }, + find: { + entry: 'vs/editor/contrib/find/findController', + worker: undefined, + }, + folding: { + entry: 'vs/editor/contrib/folding/folding', + worker: undefined, + }, + format: { + entry: 'vs/editor/contrib/format/formatActions', + worker: undefined, + }, + gotoDeclarationCommands: { + entry: 'vs/editor/contrib/goToDeclaration/goToDeclarationCommands', + worker: undefined, + }, + gotoDeclarationMouse: { + entry: 'vs/editor/contrib/goToDeclaration/goToDeclarationMouse', + worker: undefined, + }, + gotoError: { + entry: 'vs/editor/contrib/gotoError/gotoError', + worker: undefined, + }, + gotoLine: { + entry: 'vs/editor/standalone/browser/quickOpen/gotoLine', + worker: undefined, + }, + hover: { + entry: 'vs/editor/contrib/hover/hover', + worker: undefined, + }, + inPlaceReplace: { + entry: 'vs/editor/contrib/inPlaceReplace/inPlaceReplace', + worker: undefined, + }, + inspectTokens: { + entry: 'vs/editor/standalone/browser/inspectTokens/inspectTokens', + worker: undefined, + }, + iPadShowKeyboard: { + entry: 'vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard', + worker: undefined, + }, + linesOperations: { + entry: 'vs/editor/contrib/linesOperations/linesOperations', + worker: undefined, + }, + links: { + entry: 'vs/editor/contrib/links/links', + worker: undefined, + }, + multicursor: { + entry: 'vs/editor/contrib/multicursor/multicursor', + worker: undefined, + }, + parameterHints: { + entry: 'vs/editor/contrib/parameterHints/parameterHints', + worker: undefined, + }, + quickCommand: { + entry: 'vs/editor/standalone/browser/quickOpen/quickCommand', + worker: undefined, + }, + quickFixCommands: { + entry: 'vs/editor/contrib/quickFix/quickFixCommands', + worker: undefined, + }, + quickOutline: { + entry: 'vs/editor/standalone/browser/quickOpen/quickOutline', + worker: undefined, + }, + referenceSearch: { + entry: 'vs/editor/contrib/referenceSearch/referenceSearch', + worker: undefined, + }, + rename: { + entry: 'vs/editor/contrib/rename/rename', + worker: undefined, + }, + smartSelect: { + entry: 'vs/editor/contrib/smartSelect/smartSelect', + worker: undefined, + }, + snippets: { + entry: 'vs/editor/contrib/snippet/snippetController2', + worker: undefined, + }, + suggest: { + entry: 'vs/editor/contrib/suggest/suggestController', + worker: undefined, + }, + toggleHighContrast: { + entry: 'vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast', + worker: undefined, + }, + toggleTabFocusMode: { + entry: 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode', + worker: undefined, + }, + transpose: { + entry: 'vs/editor/contrib/caretOperations/transpose', + worker: undefined, + }, + wordHighlighter: { + entry: 'vs/editor/contrib/wordHighlighter/wordHighlighter', + worker: undefined, + }, + wordOperations: { + entry: 'vs/editor/contrib/wordOperations/wordOperations', + worker: undefined, + }, +}; diff --git a/index.js b/index.js new file mode 100644 index 00000000..52bf519e --- /dev/null +++ b/index.js @@ -0,0 +1,188 @@ +const path = require('path'); +const AddWorkerEntryPointPlugin = require('./plugins/AddWorkerEntryPointPlugin'); +const INCLUDE_LOADER_PATH = require.resolve('./loaders/include'); + +const IGNORED_IMPORTS = { + [resolveMonacoPath('vs/language/typescript/lib/typescriptServices')]: [ + 'fs', + 'path', + 'os', + 'crypto', + 'source-map-support', + ], +}; +const MONACO_EDITOR_API_PATHS = [ + resolveMonacoPath('vs/editor/editor.main'), + resolveMonacoPath('vs/editor/editor.api') +]; +const WORKER_LOADER_PATH = resolveMonacoPath('vs/editor/common/services/editorSimpleWorker'); +const EDITOR_MODULE = { + label: 'editorWorkerService', + entry: undefined, + worker: { + id: 'vs/editor/editor', + entry: 'vs/editor/editor.worker', + output: 'editor.worker.js', + fallback: undefined + }, + alias: undefined, +}; +const LANGUAGES = require('./languages'); +const FEATURES = require('./features'); + +function resolveMonacoPath(filePath) { + return require.resolve(path.join('../esm', filePath)); +} + +const languagesById = fromPairs( + flatMap(toPairs(LANGUAGES), ([id, language]) => + [id, ...(language.alias || [])].map((label) => [label, { label, ...language }]) + ) +); +const featuresById = mapValues(FEATURES, (feature, key) => ({ label: key, ...feature })) + +class MonacoWebpackPlugin { + constructor(webpack, options = {}) { + const languages = options.languages || Object.keys(languagesById); + const features = options.features || Object.keys(featuresById); + const output = options.output || ''; + this.webpack = webpack; + this.options = { + languages: languages.map((id) => languagesById[id]).filter(Boolean), + features: features.map(id => featuresById[id]).filter(Boolean), + output, + }; + } + + apply(compiler) { + const webpack = this.webpack; + const { languages, features, output } = this.options; + const publicPath = getPublicPath(compiler); + const modules = [EDITOR_MODULE, ...languages, ...features]; + const workers = modules.map( + ({ label, alias, worker }) => worker && ({ label, alias, ...worker }) + ).filter(Boolean); + const rules = createLoaderRules(languages, features, workers, publicPath); + const plugins = createPlugins(webpack, workers, output); + addCompilerRules(compiler, rules); + addCompilerPlugins(compiler, plugins); + } +} + +function addCompilerRules(compiler, rules) { + const compilerOptions = compiler.options; + const moduleOptions = compilerOptions.module || (compilerOptions.module = {}); + const existingRules = moduleOptions.rules || (moduleOptions.rules = []); + existingRules.push(...rules); +} + +function addCompilerPlugins(compiler, plugins) { + plugins.forEach((plugin) => plugin.apply(compiler)); +} + +function getPublicPath(compiler) { + return compiler.options.output && compiler.options.output.publicPath || ''; +} + +function stripTrailingSlash(string) { + return string.replace(/\/$/, ''); +} + +function createLoaderRules(languages, features, workers, publicPath) { + if (!languages.length && !features.length) { return []; } + const languagePaths = languages.map(({ entry }) => entry).filter(Boolean); + const featurePaths = features.map(({ entry }) => entry).filter(Boolean); + const workerPaths = workers.reduce((acc, { label, output }) => Object.assign(acc, { + [label]: `${publicPath ? `${stripTrailingSlash(publicPath)}/` : ''}${output}`, + }), {}); + const globals = { + 'MonacoEnvironment': `((paths) => ({ getWorkerUrl: (moduleId, label) => paths[label] }))(${ + JSON.stringify(workerPaths, null, 2) + })`, + }; + return [ + { + test: MONACO_EDITOR_API_PATHS, + use: [ + { + loader: INCLUDE_LOADER_PATH, + options: { + globals, + pre: featurePaths.map((importPath) => resolveMonacoPath(importPath)), + post: languagePaths.map((importPath) => resolveMonacoPath(importPath)), + }, + }, + ], + }, + ]; +} + +function createPlugins(webpack, workers, outputPath) { + const workerFallbacks = workers.reduce((acc, { id, fallback }) => (fallback ? Object.assign(acc, { + [id]: resolveMonacoPath(fallback) + }) : acc), {}); + return [ + ...Object.keys(IGNORED_IMPORTS).map((id) => + createIgnoreImportsPlugin(webpack, id, IGNORED_IMPORTS[id]) + ), + ...uniqBy(workers, ({ id }) => id).map(({ id, entry, output }) => + new AddWorkerEntryPointPlugin(webpack, { + id, + entry: resolveMonacoPath(entry), + filename: path.join(outputPath, output), + plugins: [ + createContextPlugin(webpack, WORKER_LOADER_PATH, {}), + new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), + ], + }) + ), + ...(workerFallbacks ? [createContextPlugin(webpack, WORKER_LOADER_PATH, workerFallbacks)] : []), + ]; +} + +function createContextPlugin(webpack, filePath, contextPaths) { + return new webpack.ContextReplacementPlugin( + new RegExp(`^${path.dirname(filePath)}$`), + '', + contextPaths + ); +} + +function createIgnoreImportsPlugin(webpack, targetPath, ignoredModules) { + return new webpack.IgnorePlugin( + new RegExp(`^(${ignoredModules.map((id) => `(${id})`).join('|')})$`), + new RegExp(`^${path.dirname(targetPath)}$`) + ); +} + +function flatMap(items, iteratee) { + return items.map(iteratee).reduce((acc, item) => [...acc, ...item], []); +} + +function toPairs(object) { + return Object.keys(object).map((key) => [key, object[key]]); +} + +function fromPairs(values) { + return values.reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), {}); +} + +function mapValues(object, iteratee) { + return Object.keys(object).reduce( + (acc, key) => Object.assign(acc, { [key]: iteratee(object[key], key) }), + {} + ); +} + +function uniqBy(items, iteratee) { + const keys = {}; + return items.reduce((acc, item) => { + const key = iteratee(item); + if (key in keys) { return acc; } + keys[key] = true; + acc.push(item); + return acc; + }, []); +} + +module.exports = MonacoWebpackPlugin; diff --git a/languages.js b/languages.js new file mode 100644 index 00000000..7aa65b56 --- /dev/null +++ b/languages.js @@ -0,0 +1,222 @@ +module.exports = { + bat: { + entry: 'vs/basic-languages/bat/bat.contribution', + worker: undefined, + alias: undefined, + }, + coffee: { + entry: 'vs/basic-languages/coffee/coffee.contribution', + worker: undefined, + alias: undefined, + }, + cpp: { + entry: 'vs/basic-languages/cpp/cpp.contribution', + worker: undefined, + alias: undefined, + }, + csharp: { + entry: 'vs/basic-languages/csharp/csharp.contribution', + worker: undefined, + alias: undefined, + }, + csp: { + entry: 'vs/basic-languages/csp/csp.contribution', + worker: undefined, + alias: undefined, + }, + css: { + entry: 'vs/language/css/monaco.contribution', + worker: { + id: 'vs/language/css/cssWorker', + entry: 'vs/language/css/css.worker', + output: 'css.worker.js', + fallback: 'vs/language/css/cssWorker', + }, + alias: undefined, + }, + dockerfile: { + entry: 'vs/basic-languages/dockerfile/dockerfile.contribution', + worker: undefined, + alias: undefined, + }, + fsharp: { + entry: 'vs/basic-languages/fsharp/fsharp.contribution', + worker: undefined, + alias: undefined, + }, + go: { + entry: 'vs/basic-languages/go/go.contribution', + worker: undefined, + alias: undefined, + }, + handlebars: { + entry: 'vs/basic-languages/handlebars/handlebars.contribution', + worker: undefined, + alias: undefined, + }, + html: { + entry: 'vs/language/html/monaco.contribution', + worker: { + id: 'vs/language/html/htmlWorker', + entry: 'vs/language/html/html.worker', + output: 'html.worker.js', + fallback: 'vs/language/html/htmlWorker', + }, + alias: undefined, + }, + ini: { + entry: 'vs/basic-languages/ini/ini.contribution', + worker: undefined, + alias: undefined, + }, + java: { + entry: 'vs/basic-languages/java/java.contribution', + worker: undefined, + alias: undefined, + }, + json: { + entry: 'vs/language/json/monaco.contribution', + worker: { + id: 'vs/language/json/jsonWorker', + entry: 'vs/language/json/json.worker', + output: 'json.worker.js', + fallback: 'vs/language/json/jsonWorker', + }, + alias: undefined, + }, + less: { + entry: 'vs/basic-languages/less/less.contribution', + worker: undefined, + alias: undefined, + }, + lua: { + entry: 'vs/basic-languages/lua/lua.contribution', + worker: undefined, + alias: undefined, + }, + markdown: { + entry: 'vs/basic-languages/markdown/markdown.contribution', + worker: undefined, + alias: undefined, + }, + msdax: { + entry: 'vs/basic-languages/msdax/msdax.contribution', + worker: undefined, + alias: undefined, + }, + mysql: { + entry: 'vs/basic-languages/mysql/mysql.contribution', + worker: undefined, + alias: undefined, + }, + objective: { + entry: 'vs/basic-languages/objective-c/objective-c.contribution', + worker: undefined, + alias: undefined, + }, + pgsql: { + entry: 'vs/basic-languages/pgsql/pgsql.contribution', + worker: undefined, + alias: undefined, + }, + php: { + entry: 'vs/basic-languages/php/php.contribution', + worker: undefined, + alias: undefined, + }, + postiats: { + entry: 'vs/basic-languages/postiats/postiats.contribution', + worker: undefined, + alias: undefined, + }, + powershell: { + entry: 'vs/basic-languages/powershell/powershell.contribution', + worker: undefined, + alias: undefined, + }, + pug: { + entry: 'vs/basic-languages/pug/pug.contribution', + worker: undefined, + alias: undefined, + }, + python: { + entry: 'vs/basic-languages/python/python.contribution', + worker: undefined, + alias: undefined, + }, + r: { + entry: 'vs/basic-languages/r/r.contribution', + worker: undefined, + alias: undefined, + }, + razor: { + entry: 'vs/basic-languages/razor/razor.contribution', + worker: undefined, + alias: undefined, + }, + redis: { + entry: 'vs/basic-languages/redis/redis.contribution', + worker: undefined, + alias: undefined, + }, + redshift: { + entry: 'vs/basic-languages/redshift/redshift.contribution', + worker: undefined, + alias: undefined, + }, + ruby: { + entry: 'vs/basic-languages/ruby/ruby.contribution', + worker: undefined, + alias: undefined, + }, + sb: { + entry: 'vs/basic-languages/sb/sb.contribution', + worker: undefined, + alias: undefined, + }, + scss: { + entry: 'vs/basic-languages/scss/scss.contribution', + worker: undefined, + alias: undefined, + }, + solidity: { + entry: 'vs/basic-languages/solidity/solidity.contribution', + worker: undefined, + alias: undefined, + }, + sql: { + entry: 'vs/basic-languages/sql/sql.contribution', + worker: undefined, + alias: undefined, + }, + swift: { + entry: 'vs/basic-languages/swift/swift.contribution', + worker: undefined, + alias: undefined, + }, + typescript: { + entry: 'vs/language/typescript/monaco.contribution', + worker: { + id: 'vs/language/typescript/tsWorker', + entry: 'vs/language/typescript/ts.worker', + output: 'typescript.worker.js', + fallback: 'vs/language/typescript/tsWorker', + }, + alias: ['javascript'], + }, + vb: { + entry: 'vs/basic-languages/vb/vb.contribution', + worker: undefined, + alias: undefined, + }, + xml: { + entry: 'vs/basic-languages/xml/xml.contribution', + worker: undefined, + alias: undefined, + }, + yaml: { + entry: 'vs/basic-languages/yaml/yaml.contribution', + worker: undefined, + alias: undefined, + }, +}; diff --git a/loaders/include.js b/loaders/include.js new file mode 100644 index 00000000..a0a02b32 --- /dev/null +++ b/loaders/include.js @@ -0,0 +1,20 @@ +const loaderUtils = require('loader-utils'); + +module.exports.pitch = function pitch(remainingRequest) { + const { globals = undefined, pre = [], post = [] } = loaderUtils.getOptions(this) || {}; + + // HACK: NamedModulesPlugin overwrites existing modules when requesting the same module via + // different loaders, so we need to circumvent this by appending a suffix to make the name unique + // See https://github.com/webpack/webpack/issues/4613#issuecomment-325178346 for details + this._module.userRequest = `include-loader!${this._module.userRequest}`; + + return [ + ...(globals + ? Object.keys(globals).map((key) => `self[${JSON.stringify(key)}] = ${globals[key]};`) + : [] + ), + ...pre.map((include) => `require(${loaderUtils.stringifyRequest(this, include)});`), + `module.exports = require(${loaderUtils.stringifyRequest(this, `!!${remainingRequest}`)});`, + ...post.map((include) => `require(${loaderUtils.stringifyRequest(this, include)});`), + ].join('\n'); +}; diff --git a/plugins/AddWorkerEntryPointPlugin.js b/plugins/AddWorkerEntryPointPlugin.js new file mode 100644 index 00000000..04f2ab9d --- /dev/null +++ b/plugins/AddWorkerEntryPointPlugin.js @@ -0,0 +1,35 @@ +class AddWorkerEntryPointPlugin { + constructor(webpack, { + id, + entry, + filename, + chunkFilename = undefined, + plugins = undefined, + }) { + this.webpack = webpack; + this.options = { id, entry, filename, chunkFilename, plugins }; + } + + apply(compiler) { + const webpack = this.webpack; + const { id, entry, filename, chunkFilename, plugins } = this.options; + compiler.hooks.make.tapAsync('AddWorkerEntryPointPlugin', (compilation, callback) => { + const outputOptions = { + filename, + chunkFilename, + publicPath: compilation.outputOptions.publicPath, + // HACK: globalObject is necessary to fix https://github.com/webpack/webpack/issues/6642 + globalObject: 'this', + }; + const childCompiler = compilation.createChildCompiler(id, outputOptions, [ + new webpack.webworker.WebWorkerTemplatePlugin(), + new webpack.LoaderTargetPlugin('webworker'), + new webpack.SingleEntryPlugin(compiler.context, entry, 'main'), + ]); + plugins.forEach((plugin) => plugin.apply(childCompiler)); + childCompiler.runAsChild(callback); + }); + } +} + +module.exports = AddWorkerEntryPointPlugin;