monaco-editor icon indicating copy to clipboard operation
monaco-editor copied to clipboard

Ship two esm variants

Open hediet opened this issue 2 months ago • 15 comments

A common feature request is to have two esm variants:

  • Variant 1: for bundlers
    • with myComponent.ts importing CSS via import "./myComponent.css";, so that the CSS of unused components can be removed automatically
    • with external npm dependencies to make updating easier
  • Variant 2: for browsers
    • should load from source
      • no CSS imports (or via assert)
      • no external npm dependencies

Currently, the ESM build contains CSS-imports, but bundles npm dependencies.

Options:

  1. Have esm (variant 1, default) and esm-bundled (variant 2, via import * as m from "monaco-editor/bundled";)
  2. Have esm (variant 2, default) and esm-unbundled (variant 1, via import * as m from "monaco-editor/unbundled";)
  3. Have different npm packages, e.g. monaco-editor-bundled and monaco-editor.
    • This would also solve the problem that npm dependencies of variant 1 can be updated (e.g. the dependency could be dompurify: ^3.2.7), but not for variant 2 (it would need to be dompurify: 3.2.7, as its sources are bundled with the monaco-editor sources). Variant 2 still needs the dompurify dependency to track CVEs. Having different packages would solve this.

Related:

  • https://github.com/rollup/rollup/discussions/6085
  • https://github.com/microsoft/monaco-editor/issues/886
  • https://github.com/microsoft/monaco-editor/issues/5057
  • https://github.com/microsoft/monaco-editor/issues/4526

hediet avatar Oct 20 '25 09:10 hediet

external npm dependecies are possible, if they are available as real ESM modules. Then you could use a importmap. I do this in my designer: https://github.com/node-projects/web-component-designer-demo/blob/179724e176d6e8b7402fd060870413f837d548aa/index.html#L21

jogibear9988 avatar Oct 20 '25 11:10 jogibear9988

Thanks for looking into that issue. How about shipping a bundler-and-browser-ready version with { type: "css" }; import attributes directly?

Most bundlers can understand it:

And browsers (namely FF and Safari) can patch it with minimal overhead using es-module-shims.

That would allow optimised builds (bundlers tree-shaking or browsers loading css only when import'ed) and also future-proofing the build as import attributes have reached Stage 4 and are now part of the official spec.

I am suggesting Component.js with import styles from "./Component.css" with { type: "css" }; as TypeScript doesn't seem to know what to do with css imports yet:

  • https://github.com/microsoft/TypeScript/issues/46689
  • https://github.com/microsoft/TypeScript/issues/46135

dmnsgn avatar Oct 20 '25 12:10 dmnsgn

Thanks for looking into that issue. How about shipping a bundler-and-browser-ready version with { type: "css" }; import attributes directly?

I also thought of this, but the difference is here, the

import 'xxxxx.css'

wich are in the modules at the moment, are not used. They are only for bundlers to show that they need to include this css. And the other return a CSSStyleSheet wich then needs to be added to a "adoptedStylesheets" array.

jogibear9988 avatar Oct 20 '25 13:10 jogibear9988

assert isn't a thing. I suspect you mean import attributes (with).

I think the proper solution is to just import CSS in the standard way, which means as CSS Module Scripts with import attributes.

Bundlers should be supporting this, but if they don't natively, it's available in some as plugins, like my Rollup plugin: https://www.npmjs.com/package/rollup-plugin-css-modules

justinfagnani avatar Oct 20 '25 18:10 justinfagnani

@justinfagnani I first thought the same, but they do nothing with the css after the import, it's only for a bundler to know to include it. What should they do with the imported CSSStyleSheet object? If they assign to document.adoptedStylesheets it's also bad, cause for example, I want to use monaco inside of a webcomponent (i do so at the moment, so I know it works). It would also not help, if the use import attributes, import a lot of css files wich are then unused....

So I thought to ship to ESM variants would be the best non breaking solution at the moment.

jogibear9988 avatar Oct 20 '25 18:10 jogibear9988

look for example here:

https://github.com/microsoft/vscode/blob/ce8f9521e2d53322b13d223730468071790cd0b7/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts#L16

jogibear9988 avatar Oct 20 '25 18:10 jogibear9988

There are two things you can do with a CSSStyleSheet object: adopt it to the document, or adopt it to a ShadowRoot.

IMO, Monaco should have an API to initialize a scope (document or shadow root) and that API should then adopt the stylesheets to the scope. This would make Monaco's styling shadow DOM compatible.

Failing that, you can always implement the current behavior. Adapting that code example:

import editorStyles from './editor.css' with {type: 'css'};

if (!document.adoptedStyleSheets.has(editorStyles)) {
  document.adoptedStyleSheets.push(editorStyles);
}

justinfagnani avatar Oct 20 '25 18:10 justinfagnani

Yeah, but this would be a big change in the whole codebase. I for sure would like this, but I think not this will happen any time soon.

And so, the shipment of a version without any css imports is better then the current one. At the moment I need to load via it's own require, (https://github.com/node-projects/web-component-designer-demo/blob/25594b11328b35f6862af2841eab02f4daa22f7f/index.html#L62) and javascript librarys loaded after it then find the existing "require" on window and think they are loaded in a node environment and will fail... I hate it

jogibear9988 avatar Oct 20 '25 18:10 jogibear9988

Would css import via import attributes support this bundling scenario:

Source

index.ts:

export * from "./component1.ts";
export * from "./component2.ts";

component1.ts:

import styles1 from './component1.css' with {type: 'css'};
if (!document.adoptedStyleSheets.has(styles1)) {
  document.adoptedStyleSheets.push(styles1);
}
export class Component1 {}

component1.css:

.component1 { color: green; }

component2.ts:

import styles2 from './component2.css' with {type: 'css'};
if (!document.adoptedStyleSheets.has(styles2)) {
  document.adoptedStyleSheets.push(styles2);
}
export class Component2 {}

component2.css:

.component2 { color: red; }

Bundled

bundled.js:

import styles from './bundled.css' with {type: 'css'};
if (!document.adoptedStyleSheets.has(styles )) {
  document.adoptedStyleSheets.push(styles );
}
export class Component1 {}
export class Component2 {}

bundled.css:

.component1 { color: green; }
.component2 { color: red; }

As the modules can do anything with styles1 and styles2, I wonder how bundlers could determine that it is safe to merge the documents and that the stylesheet only needs to be attached once. If there is no bundler that can bundle the css files into one, we cannot just have the ESM version with css import attributes alone, as the monaco editor imports ~100 css files, which would cause a noticable loading delay.

hediet avatar Oct 20 '25 19:10 hediet

Don't know if the bundlers can do it, but I on my side find it not correct if you add styles automaticly to the document. As I use webcomponents I don't need the styles in body, I need them in my shadow root. (not only in one, it can be instanciated multiple times)

Not sure if the bundler could combine the css to one file (but I think there are addons or config for that), but what you can also do, include the CSS inside of javascript. But there is then a abstraction needed to add it to the document or to shadow roots.

jogibear9988 avatar Oct 20 '25 19:10 jogibear9988

If you want then the

  editor.create(this._editor, options);

should maybe check if

 handOverElement.getRootNode().adoptedStyleSheets

has the needed stylesheets, and if not, add them.

Or there should be something in the options (like add styleSheetsTo, or disable Add stylesheets, ...)

jogibear9988 avatar Oct 20 '25 20:10 jogibear9988

I've created a sample website wich includes monaco as an ESM version and also includes "dompurify" as an NPM package. There is also a javascript patcher included, wich removes all css imports and replaces the dompurify import.

see here: https://github.com/jogibear9988/test-monaco-esm

Wich this changes you could use monaco 0.54 directly as ESM in the browser.

(The dompurify change is not needed, I only wanted to show how the load of external modules via importmap works)

jogibear9988 avatar Oct 20 '25 21:10 jogibear9988

I also added a webcomponent sample, see file https://github.com/jogibear9988/test-monaco-esm/blob/main/wc-sample.html

There I also saw, monaco adds a few styles to the document directly via script:

Image

jogibear9988 avatar Oct 20 '25 21:10 jogibear9988

And I added a minified esm version, bundled with esbuild (reduced loadsize from 10 to 4 mb (brotly 820kb))

jogibear9988 avatar Oct 21 '25 08:10 jogibear9988

Not everything seems to work: With HTML content I saw now the webworker does not run:

Image

jogibear9988 avatar Oct 22 '25 21:10 jogibear9988