@bithero/monaco-editor-vite-plugin
Version:
Vite plugin to include & bundle monaco-editor
243 lines (242 loc) • 9.29 kB
JavaScript
// @bithero/monaco-editor-vite-plugin - Vite plugin to include & bundle monaco-editor
// Copyright (C) 2025-present Mai-Lapyst
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import metadata from 'monaco-editor/esm/metadata.js';
import path from 'node:path';
import fs from 'node:fs';
/**
* Little helper to ensure that a list of elements only contains truthy values
*
* @param list the list to filter
* @returns the filtered list
*/
function filterNull(list) {
return list.filter(Boolean);
}
/**
* Resolves all languages
*
* @param languages the languages configuration
* @returns the resolved feature definitions
*/
function resolveLanguages(languages, customLanguages) {
if (languages === '*' || languages === 'all') {
return filterNull(metadata.languages.concat(customLanguages));
}
if (languages.length <= 0) {
return filterNull(customLanguages);
}
const langById = {};
metadata.languages.forEach((l) => langById[l.label] = l);
function resolveLanguage(name) {
const lang = langById[name];
if (!lang) {
console.error("[bithero-monaco] unknown language:", name);
return null;
}
return lang;
}
return filterNull(languages.map(resolveLanguage).concat(customLanguages));
}
/**
* Resolves all features
*
* @param features the features configuration
* @returns the resolved feature definitions
*/
function resolveFeatures(features) {
if (!features) {
return metadata.features;
}
if (features === '*' || features === 'all') {
return metadata.features;
}
const featureById = {};
metadata.features.forEach((f) => {
if (featureById[f.label]) {
const def = featureById[f.label];
if (typeof def.entry === 'string') {
def.entry = [def.entry];
}
def.entry.push(...f.entry);
}
else {
featureById[f.label] = f;
}
});
// Monaco versions before 55.0 did store codicons differently...
const codicons_path = resolveMonacoPath('vs/base/browser/ui/codicons/codiconStyles.js');
if (fs.existsSync(codicons_path)) {
featureById['codicons'] = {
label: 'codicons',
entry: 'vs/base/browser/ui/codicons/codiconStyles.js',
};
}
function resolveFeature(name) {
const feature = featureById[name];
if (!feature) {
if (name == 'codicons' && featureById['codicon'])
return featureById['codicon'];
if (name == 'codicon' && featureById['codicons'])
return featureById['codicons'];
console.error("[bithero-monaco] unknown feature:", name);
return null;
}
return feature;
}
const excluded = features.filter((f) => f[0] === '!').map((f) => f.slice(1));
if (excluded.length > 0) {
return filterNull(Object.keys(featureById)
.filter((f) => !excluded.includes(f))
.map(resolveFeature));
}
return filterNull(features.map(resolveFeature));
}
/**
* Static entry for the editorWorkerService that always needs to be present.
*/
const editor_module = {
label: 'editorWorkerService',
entry: undefined,
worker: {
id: 'vs/editor/editor',
entry: 'vs/editor/editor.worker',
},
};
/**
* Resolves all workers, and also makes sure we're having the editorWorkerService.
*
* @param languages the resolved languages
* @param features the resolved features
* @returns list of all workers
*/
function resolveWorkers(languages, features) {
const modules = [editor_module].concat(languages).concat(features);
const workers = [];
modules.forEach((mod) => {
if (mod.worker) {
workers.push({
label: mod.label,
id: mod.worker.id,
entry: mod.worker.entry,
});
}
});
return workers;
}
/**
* Resolves an module path by utilising `import.meta.resolve`, but making sure we're returning
* the string path instead of an URL'ish thing.
*
* @param file the filepath to resolve
* @return the resolved path
*/
function resolveModule(file) {
const url = import.meta.resolve(file).toString();
return url.replace(/^file:\/\//, '');
}
/**
* Resolves an file path either against the monaco-editor esm package, or by itself.
*
* @param file the filepath to resolve
* @returns the resolved path
*/
function resolveMonacoPath(file) {
try {
return resolveModule(path.join('monaco-editor/esm', file));
}
catch (e) { }
try {
return resolveModule(path.join(process.cwd(), 'node_modules/monaco-editor/esm', file));
}
catch (e) { }
return resolveModule(file);
}
/**
* Plugin to control monaco-editor bundeling
*
* @param options options for the plugin
* @returns a new plugin instance
*/
export function monaco(options) {
const languages = resolveLanguages(options?.languages || [], options?.customLanguages || []);
const features = resolveFeatures(options?.features);
const workers = resolveWorkers(languages, features);
return {
name: 'bithero-monaco',
enforce: 'pre',
config(config) {
if (!config.optimizeDeps) {
config.optimizeDeps = {};
}
const optimizeDeps = config.optimizeDeps;
if (!optimizeDeps.exclude) {
optimizeDeps.exclude = [];
}
optimizeDeps.exclude.push('monaco-editor');
if (optimizeDeps.include) {
console.log("[bithero-monaco] removed 'monaco-editor' from the optimizeDeps.include setting.");
optimizeDeps.include = optimizeDeps.include.filter((i) => i === 'monaco-editor');
}
},
load(id) {
if (id.match(/esm[/\\]vs[/\\]editor[/\\]editor.main.js/)) {
const workerPaths = (workers.map((worker) => {
return `"${worker.label}": () => new ${worker.label}()`;
}));
const workerPathsJson = '{' + workerPaths.join(',') + '}';
const result = [
`// Generated by @bithero/monaco-editor-vite-plugin`,
`// SPDX-License-Identifier: AGPL-3.0-or-later`,
`// `,
`// Copyright (C) 2025-present Mai-Lapyst`,
`// `,
`// This program is free software: you can redistribute it and/or modify`,
`// it under the terms of the GNU Affero General Public License as published by`,
`// the Free Software Foundation, version 3.`,
`// `,
`// This program is distributed in the hope that it will be useful,`,
`// but WITHOUT ANY WARRANTY; without even the implied warranty of`,
`// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the`,
`// GNU Affero General Public License for more details.`,
`// `,
`// You should have received a copy of the GNU Affero General Public License`,
`// along with this program. If not, see <http://www.gnu.org/licenses/>.`,
...workers.map((worker) => {
return `import ${worker.label} from '${resolveMonacoPath(worker.entry)}?worker'`;
}),
`self['MonacoEnvironment'] = (function (paths) {
return {
globalAPI: ${options?.globalAPI || false},
getWorker: function (moduleId, label) {
var result = paths[label];
return result();
},
};
})(${workerPathsJson});`,
...features.flatMap((feature) => feature.entry).map((entry) => `import "${resolveMonacoPath(entry)}";`),
...languages.flatMap((lang) => lang.entry).map((entry) => `import "${resolveMonacoPath(entry)}";`),
"export * from './editor.api.js';"
].join('\n');
return result;
}
else if (id.match(/esm[/\\]vs[/\\]editor[/\\]editor.all.js/)) {
return 'throw "Please use esm/vs/editor.main.js or monaco-editor directly instead!"';
}
else if (id.match(/esm[/\\]vs[/\\]editor[/\\]edcore.main.js/)) {
return 'throw "Please use esm/vs/editor.main.js or monaco-editor directly instead!"';
}
},
};
}