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

ESM and CSS loading

Open oldrich-s opened this issue 7 years ago • 48 comments
trafficstars

In the ESM (EcmaScript Modules) version, css files are being imported like:

import './actionbar.css';

I have looked into the ECMA-262, TC39 proposals etc. but nowhere I could find that importing CSS files or other assets is allowed this way. Do any browsers support importing CSS files that way? Do I miss anything obvious? To me the ESM version is not ESM ;).

oldrich-s avatar May 18 '18 05:05 oldrich-s

CSS files are handled by webpack, the typical bundler when choosing ESM. This has nothing to do neither with ECMA nor with the browsers. For more information about webpack and loading CSS: https://webpack.js.org/guides/asset-management/#loading-css

flash-me avatar May 22 '18 00:05 flash-me

To me it is a mistake to introduce non-standard features to esm modules. How about if I want to use the unbundled version directly in the browser together with http2? As it is now, it is impossible without preprocessing.

oldrich-s avatar May 22 '18 05:05 oldrich-s

Sorry for the disappointment for not fitting in your expectation. I think the ESM Version is thought to be bundled with webpack in this project, I'm quite sure that bundled/optimizied/tree-shaked code is still faster than HTTP2 with multiplexing etc... But hey, there is still an AMD Version that you can use.

flash-me avatar May 22 '18 06:05 flash-me

@czb I agree.

I explicitly tried to avoid any webpack assumptions in the esm distribution, that's why the esm distribution can be loaded by other loaders.

But I could not figure out what to do with the CSS imports. Our CSS is grouped neatly with the corresponding feature. i.e. we don't have a large CSS file which is a "take it all or leave it all", but rather we have written the CSS next to the JS, so each JS file can indicate that it wants certain CSS.

You are correct, the source code cannot be loaded in its current shape via a <script type="module> tag without a minimal preprocessing to strip the CSS imports. But, I wonder, what is the alternative? What would you recommend? My plan is to wait for the committee to figure out what they're gonna do about CSS loading and adopt that once that is finalised.

alexdima avatar May 28 '18 10:05 alexdima

Personally, I load css dynamically:

export function injectFile(href: string) {
    return new Promise<void>((resolve, reject) => {
        const link = document.createElement('link')
        link.setAttribute("rel", "stylesheet")
        link.setAttribute("type", "text/css")
        link.onload = () => resolve()
        link.onerror = () => reject()
        link.setAttribute("href", href)
        document.head.appendChild(link)
    })
}

export function injectText(content: string) {
    const css = document.createElement("style")
    css.type = "text/css"
    css.innerHTML = content
    document.head.appendChild(css)
}

I guess it is equivalent to dynamic import.

oldrich-s avatar May 28 '18 12:05 oldrich-s

@czb Yes, that is what I also do in the AMD loader I wrote 5 years ago :). But I don't want to do that in each and every JS file where I use some CSS. I also want the ability to be able to collect all used CSS into a single style-sheet at packaging time, so that's why I left .css imports in JS files.

alexdima avatar May 28 '18 12:05 alexdima

I could not find any signs of css imports discussions which probably means they will not arrive in a near future.

To me, any solution is better than extending javascript by non standard features that are not even at stage 1.

oldrich-s avatar May 28 '18 14:05 oldrich-s

@czb I am not familiar with the maze of github repositories used by whatwg or w3c nor their politics.

I don't expect that ECMA-262 (which standardises ECMAScript) would concern itself with importing CSS in a web browser.

Do you happen to know where we can ask someone about the state of affairs for importing CSS ?

alexdima avatar May 28 '18 15:05 alexdima

I happen to be part of the group proposing HTML Modules - the ability to import HTML files into JavaScript modules or via <script type=module. CSS Modules are on our radar, but not proposed yet.

I can tell you that the semantics would likely be oriented around a CSS module exporting a Constructible Stylesheet: https://wicg.github.io/construct-stylesheets/index.html

ie:

import stylesheet from './styles.css';

stylesheet instanceof CSSStyleSheet; // true

// attach to global
document.moreStyleSheets.push(stylesheet);

// or to a ShadowRoot
class MyElement extends HTMLElement {
  constructor() {
    this.attachShadow({mode: 'open'}).moreStyleSheets.push(stylesheet);
  }
}

If you wanted to be future-facing to such a spec, the most proper thing to do would be to create a JS modules that has no side-effects and returns a style sheet object from a JS module, ie:

styles.css.js:

// create a container and scope to hold a style sheet:
const container = document.createElement('div');
const shadowRoot = container.attachShadow({mode: 'open'});

// create a <style> element to add css text to
let styleElement = document.createElement('style');

// add the styles
styleElement.textContent = `
  .monaco-editor .accessibilityHelpWidget {
	padding: 10px;
	vertical-align: middle;
	overflow: scroll;
  }
`;
// add the <style> to the document so it creates a style sheet
shadowRoot.appendChild(styleElement);

export default let stylesheet = styleElement.sheet;

However, because this has no side-effects, this would require rewriting the importing code. Which is kind of ugly without the moreStyleSheets property of the Constructable StyleSheets proposal.

So for now, because I don't think you're using Shadow DOM yet, I think the best option is to build .css files into JS modules, that attach the CSS to the main document:

export default styleElement = document.createElement('style');
styleElement.textContent = `
.monaco-editor .accessibilityHelpWidget {
	padding: 10px;
	vertical-align: middle;
	overflow: scroll;
}
document.head.appendChild(styleElement);
`;

Then your import sites can remain the same as the are now.

justinfagnani avatar May 28 '18 15:05 justinfagnani

Thank you for the explanation @justinfagnani

I'm going to think about this for a while, I was hoping to end up in a situation with the /esm/ folder in our distribution where its contents are both future-proof and consumable today via loaders such as webpack or rollup.

I don't believe webpack nor rollup would understand the creation of <style> elements code pattern and be able to extract the CSS to a separate CSS file.

I also don't think browsers will really like this creation of hundreds of <style> elements (speed-wise). Even if the pattern would change to create a single <style> element and keep appending to it, things might not be as good as extracting the CSS to a single file... I think browsers prioritise downloading CSS over other resources like images...

alexdima avatar May 28 '18 16:05 alexdima

Hundreds of stylesheets will be fine. We have lots of experience with styles directly embedded into HTML imports and JS modules on the Polymer team. The future is styles and templates packaged with components, and browsers are well positioned for that. Because modules are statically imported, they can be prefetched as discovered and with http/2 you can get lots of loading in parallel. The tools can analyze the dependency graph and add <link rel=modulepreload> tags to head which causees fetches before the browser even discovering the whole graph.

If the distribution stays dependent on WebPack, can you at least rename the folder and documentation from "esm" to WebPack? It's very misleading that you can't actually load the modules in the browser, IMO.

justinfagnani avatar May 28 '18 16:05 justinfagnani

So I modified the gulpfile to build .css into .css.js and this gets things closer to working, but there was an exception about require not being defined:

languageFeatures.js:192 ReferenceError: require is not defined
    at editorSimpleWorker.js:522
    at new Promise_ctor (winjs.base.js:1822)
    at EditorSimpleWorkerImpl.BaseEditorSimpleWorker.loadForeignModule (editorSimpleWorker.js:521)
    at webWorker.js:57
    at ShallowCancelThenPromise.CompletePromise_then [as then] (winjs.base.js:1743)
    at MonacoWebWorkerImpl._getForeignProxy (webWorker.js:56)
    at MonacoWebWorkerImpl.getProxy (webWorker.js:87)
    at WorkerManager._getClient (workerManager.js:76)
    at WorkerManager.getLanguageServiceWorker (workerManager.js:105)
    at DiagnostcsAdapter.worker [as _worker] (tsMode.js:47)

justinfagnani avatar May 29 '18 00:05 justinfagnani

Any updates on this? Is it just that we're waiting on ECMA for new CSS loading spec? If so, it would be great to have a ESM spec compliant version using one of the solutions @justinfagnani put forward for the time being!

zevisert avatar Jul 21 '18 00:07 zevisert

I'd love to be able to run the following in the browser (just prototyping myself) - and have it load the editor:

<!doctype html>
<html>
  <meta charset="utf-8">
<body>
  <div id="container">
</body>
<script type="module">

  import monaco from '//dev.jspm.io/monaco-editor/esm/vs/editor/editor.main.js';
  console.log(monaco);

</script>
</html>

Currently it does not load and I see a large number of these errors in the console

Failed to load module script: The server responded with a non-JavaScript MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.

Here's an example plnkr I was hoping to be able to hack on using the monaco editor in the browser https://plnkr.co/edit/0fMrMAjNeH2PVGAh206m?p=preview to prototype something...

staxmanade avatar Jul 27 '18 23:07 staxmanade

@alexandrudima @kqadem Any news?

JosefJezek avatar Nov 12 '18 09:11 JosefJezek

I wanted to prototype earlier on and faced a similar issue. My temporary solution was to make my local server transform the css files into javascript that append a stylesheet in the head. Here is my server for whoever needs it:

const connect = require('connect');
const http = require('http');
const static = require('serve-static');
const transform = require('connect-static-transform');

const css = transform({
    root: __dirname,
    match: /(.+)\.css/,
    transform: (path, content, send) => {
        const c = `
        const css = document.createElement('style');
        css.type = "text/css";
        css.textContent = \`${content}\`;
        document.head.appendChild(css);
        `;
        send(c, {'Content-Type': 'application/javascript'});
    },
});

const app = connect().use(css).use(static(__dirname));

http.createServer(app).listen(8000);

paulvarache avatar Jan 07 '19 09:01 paulvarache

@JosefJezek

Didn't realize I'm still subscribed to this. Never intended to participate in such discussion. Personally I don't even know where the problem is. If you want ESM spec compliance, you can either

  • provide a single .css file containing all styles
  • provide seperate .css file for each module in the same directory to have an association between them (or any other convention, e.g. same file name pattern etc..)
  • make use of CSSOM within the JavaScript

thats it from your side as the 'providing side'. The question of how the consumer works with it in the end shouldn't be your concern as long as your stuff comply the specs (imho)

flash-me avatar Jan 19 '19 17:01 flash-me

Hello, is there any update on distributing a browser/spec-friendly ESM build?

I think the suggestions given above are all quite reasonable:

  • Distribute your css as .css.js stylesheet modules
  • Distributing a js module and a bundled stylesheet

Browser-friendly ES modules with minimal side-effects per module are an incredibly useful tool for publishing future-proof packages. As of now, I can't really ship a browser-runnable package without forcing the user into webpack or parcel.

Alternatively, Rollup support would also make this transformation much easier.

e111077 avatar May 29 '19 23:05 e111077

I can't believe we are still not on standard here. Is there any effort being made to resolve this yet? In this day and age staxmanade example is the reasonable choice.

webmetal avatar Aug 21 '19 16:08 webmetal

I forgot about Monaco not being standards compliant here after being away from two years, and hit this roadblock again when trying to load code directly into a browser.

Again, can the ESM distribution be renamed to "webpack" or something? It's just misleading at this point. If the library really is distributed as modules then typing import('https://unpkg.com/[email protected]/esm/vs/editor/editor.all.js?module'); in devtools should work without errors. Monaco currently has dozens of errors.

Then, can the CSS just be built and distributed a different way? Many libraries that use standard modules these days publish CSS inside of JS modules, sometimes via a build step. Vexflow and CodeMirror 6 are two I've seen recently. Everything made with LitElement works this way. The benefit is that the CSS is part of the library's module graph and you don't need any bundler to get the right files loaded.

justinfagnani avatar Jun 06 '20 17:06 justinfagnani

For anyone else struggling with this: yes. This library is not exported as a valid ESM module.

If you need to use it on a real-life project (specially if you rely on popular abstractions like create-react-app or next-js), it needs to be transpiled and loaded with a separate webpack rule.

Here's a few things that worked on my current stack, but each toolchain will have it's entry points to allow webpack tweaks (or the eject button, which leaves you with plain webpack) :)

  • webpack plugin for monaco editor to write less configs :scroll:
  • react monaco editor to skip writing the react bindings ---> but still uses monaco-editor --> so it inherits the same ESM issues :0
  • I use next-js, luckily they expose a lot of entry points to play with webpack config. Depending on your choice of styling (css/css-modules/sass/css-in-js) you'll need to set a special loader rule for the monaco styles and ttf fonts! Here's a hint on how it could look, but it depends on your css flavor
// pseudo-code from the docs here: 
// https://github.com/react-monaco-editor/react-monaco-editor#using-with-webpack
// Specify separate paths
const path = require('path');
const APP_DIR = path.resolve(__dirname, './src');
const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');

// pass webpack module.rules
{
  test: /\.css$/,
  include: APP_DIR,
  use: [{
    loader: 'style-loader', // or whatever you use
  }, {
    loader: 'css-loader', // or whatever you use
    options: {
      modules: true,
      namedExport: true,
    },
  }],
}, {
  test: /\.css$/,
  include: MONACO_DIR,
  use: ['style-loader', 'css-loader'],
},
, {
  test: /\.ttf$/,
  include: MONACO_DIR,
  use: ['file-loader'],
}
  • next-transpile-modules will transpile the nasty code!!! Could also be written on webpack or as a prebuild step I guess? But it needs to be transpiled at some point otherwise it will just not work :)
  • if using ssr we get a node-ish env on the first render... so don't forget to make it a dynamic import to avoid missing browser globals!
import dynamic from 'next/dynamic'

const MonacoEditor = dynamic(import('react-monaco-editor'), { ssr: false })

<MonacoEditor ... />
  • web workers need to be handled by nextjs as static content to be available on the final bundle :robot:

Hope this list of hints helps others trying to consume it on their projects... but also highlights how much effort/knowledge/fiddling is needed to actually use the library :)

sombreroEnPuntas avatar Jun 17 '20 07:06 sombreroEnPuntas

Reiterating what @justinfagnani has said above; calling this esm is misleading at best. At worst a casual adopter will install the library and attempt to import it natively only to find that this does not work, wasting time. If a bundler like webpack is necessary this obviates the benefits of language-level, dependency-free imports. Monaco is awesome, and I hope that this can be addressed with more urgency. Thanks!

klebba avatar Jul 14 '20 08:07 klebba

To save time for y'all:

Unfortunately, the ESM folder does NOT contain an ESM distribution

The ESM folder gives you the impression that it contains an ESM implementation of Monaco. It turns out that it tries to load CSS using import statements. Thus, modern browsers supporting ESM will not be able to import Monaco using ESM.

Starcounter-Jack avatar Sep 21 '20 12:09 Starcounter-Jack

Thanks all; I got here after trying to get some node.js-based mocha tests that tried to test monarch syntax highlighting in an ESM project. I can't see a way around this, other than webpack-ing up my tests and letting the transpiler do its magic; which is a layer of complexity I was avoiding til now (and annoyingly the mocha<->webpack stuff's all broken too).

mattgodbolt avatar Nov 23 '20 02:11 mattgodbolt

@oldrich-s You are right, moreover, that import './actionbar.css' is one of those custom hacky patches that reach consensus among devs-on-the-edge, to inject text as a stylesheet on the fly, so nothing about it anything ressembling a 'module' XD

meanwhile at ECMALabs, 2 years later, they have come up with this thing, and hit a roadblock there:

import "https://example.com/module";
import "https://example.com/module" assert { type: "css" };

source: whatpr.org ramblings

Oh well, can we blame people though for just wanting to move forward while standards are being discussed XD

If you face this issue: image

Personally I would take that import and put it where it on another css, since you are not getting any 'css scoping' benefit anyway, and this is actually compliant since ages ago:

import "./deezstyles.css";

.my-already-cascading-styleshits {
}

Or... you could also instantiate some DOM wrappers and stick them a shadowDOM so then you can assign it an instance of a CSSStytleSheet with your CSSRule, which effectively isolates your css classname and it doesnt pollute the global scope.

Because that's the spirit! 😂

weisk avatar Dec 07 '20 07:12 weisk

There should be an official solution, but I've found some luck using Skypack which bundles npm packages to ESM on-demand.

import * as monaco from 'https://cdn.skypack.dev/[email protected]';

This is also very fast to load because it bundles the entry-point and eliminates the waterfall effect.

There's a limitation with resolving assets within CSS though, and fails to load codicon.ttf.

Not the best solution but I worked around this using a service worker:

self.addEventListener('activate', event => {
	event.waitUntil(clients.claim());
});

self.addEventListener('fetch', (event) => {
	if (event.request.url.endsWith('codicon.ttf')) {
		event.respondWith(
			fetch('https://unpkg.com/[email protected]/esm/vs/base/browser/ui/codicons/codicon/codicon.ttf')
		);
	}
});

privatenumber avatar Dec 14 '20 06:12 privatenumber

For anyone else struggling with this: yes. This library is not exported as a valid ESM module.

If you need to use it on a real-life project (especially if you rely on popular abstractions like create-react-app or next-js), it needs to be transpiled and loaded with a separate webpack rule.

Here's a few things that worked on my current stack, but each toolchain will have it's entry points to allow webpack tweaks (or the eject button, which leaves you with plain webpack) :)

  • webpack plugin for monaco editor to write less configs 📜
  • react monaco editor to skip writing the react bindings ---> but still uses monaco-editor --> so it inherits the same ESM issues :0
  • I use next-js, luckily they expose a lot of entry points to play with webpack config. Depending on your choice of styling (css/css-modules/sass/css-in-js) you'll need to set a special loader rule for the monaco styles and ttf fonts! Here's a hint on how it could look, but it depends on your css flavor
// pseudo-code from the docs here: 
// https://github.com/react-monaco-editor/react-monaco-editor#using-with-webpack
// Specify separate paths
const path = require('path');
const APP_DIR = path.resolve(__dirname, './src');
const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');

// pass webpack module.rules
{
  test: /\.css$/,
  include: APP_DIR,
  use: [{
    loader: 'style-loader', // or whatever you use
  }, {
    loader: 'css-loader', // or whatever you use
    options: {
      modules: true,
      namedExport: true,
    },
  }],
}, {
  test: /\.css$/,
  include: MONACO_DIR,
  use: ['style-loader', 'css-loader'],
},
, {
  test: /\.ttf$/,
  include: MONACO_DIR,
  use: ['file-loader'],
}
  • next-transpile-modules will transpile the nasty code!!! Could also be written on webpack or as a prebuild step I guess? But it needs to be transpiled at some point otherwise it will just not work :)
  • if using ssr we get a node-ish env on the first render... so don't forget to make it a dynamic import to avoid missing browser globals!
import dynamic from 'next/dynamic'

const MonacoEditor = dynamic(import('react-monaco-editor'), { ssr: false })

<MonacoEditor ... />
  • web workers need to be handled by nextjs as static content to be available on the final bundle 🤖

Hope this list of hints helps others trying to consume it on their projects... but also highlights how much effort/knowledge/fiddling is needed to actually use the library :)

This is such a great write up but didn't work for me on Nextjs 10.

michaelwschultz avatar Jun 01 '21 20:06 michaelwschultz

https://web.dev/css-module-scripts/ indicates a possible way forward:

import sheet from './styles.css' assert { type: 'css' };
document.adoptedStyleSheets = [sheet];
shadowRoot.adoptedStyleSheets = [sheet];

or the async variant

const cssModule = await import('./style.css', {
  assert: { type: 'css' }
});
document.adoptedStyleSheets = [cssModule.default];

alexdima avatar Dec 20 '21 14:12 alexdima

@alexdima does this mean you will change the imports to standard ones? and will then the esm folder contain a version wich could directly be loaded from browser?

jogibear9988 avatar Dec 21 '21 01:12 jogibear9988

@alexdima do you work on this? is it possible to help with something?

jogibear9988 avatar Dec 25 '21 15:12 jogibear9988