Comments (85)
Yeah CSS support isn't fully baked yet. My first priority right now is to fix JavaScript code splitting since that's a more widely-used feature. After that, getting CSS to a better state will be my first priority.
I'm currently working on a rewrite of JavaScript code splitting to address a number of related features: #399, splitting for iife
and cjs
, manual chunks, top-level await, entry point hashes. The main aspect of the rewrite is changing code in shared chunks to be lazily-evaluated, which makes a lot of additional use cases possible. Getting all of those working together is pretty complicated and it's unfortunately taking longer than I'd like.
I have limited time to work on this at the moment due to the holidays, so all of this is not going to come out next week. I'm hoping for it to land in the next few months.
from esbuild.
Hey @evanw :) First off, just wanted to say great work on esbuild. We (@ryanflorence and I) have been loving it so far! We are using it to build Remix (https://remix.run).
Side note: I thought it was awesome when I saw unpkg being used in the esbuild docs about how to build a plugin. Seeing something I built being used in a quality project like esbuild is just so cool ๐ค
I just wanted to share a few thoughts here. My initial thought is that I wish loading CSS worked more like the file
loader. If you're creating an extra file in the build output, what I really need is the URL to that file. Then, I can include it with a <link>
tag.
A few reasons why I prefer using a <link>
tag (instead of loading CSS via JavaScript, or inlining it into the page):
<link>
tags avoid content layout shift (loading CSS via JS causes jank)<link>
tags support media queries for stuff like light/dark mode, responsive layouts, etc. This is great when you don't want to make the browser parse + apply CSS it doesn't need!- You can use HTTP caching with
<link>
tags and share that cache across pages - Multiple .js files can re-use the same .css file
import
- From a web perf perspective, importing CSS via a
<link>
tag can load in parallel with your JavaScript. Injecting styles via JS depends on JS loading first (so CSS is blocked by JavaScript). This is particularly problematic when server rendering because it eliminates the whole benefit because you can't render a styled page. You have to wait for JS to load!
In addition, regardless of whether you use a <link>
tag or a loadStyle()
function (as was previously discussed), both methods really just need a URL to the stylesheet.
Here's how you'd use a <link>
tag:
import typography from './typography.css';
import typographyDark from './typographyDark.css';
function MyApp() {
return (
<html>
<head>
<link rel="stylesheet" href={typography} media="(prefers-color-scheme: light)" />
<link rel="stylesheet" href={typographyDark} media="(prefers-color-scheme: dark)" />
</head>
</html>
);
}
And here's what you'd do if you prefer loadStyle()
:
import typography from './typography.css';
import typographyDark from './typographyDark.css';
if (window.matchMedia('(prefers-color-scheme: dark)')) {
loadStyle(typographyDark);
} else {
loadStyle(typography);
}
I also realize you're thinking about doing some code splitting with CSS. In that case, maybe an array of URLs could be returned? ๐คท
Anyway, as I already said we've been really happy with using esbuild to build Remix and we appreciate the amount of care you've taken to get it right. I hope our perspective here helps as you consider what to do about first-class CSS support in esbuild.
from esbuild.
The newly-released version 0.7.7 now has experimental CSS support. From the release notes:
This release introduces the new
css
loader, enabled by default for.css
files. It has the following features:
You can now use esbuild to process CSS files by passing a CSS file as an entry point. This means CSS is a new first-class file type and you can use it without involving any JavaScript code at all.
When bundling is enabled, esbuild will bundle multiple CSS files together if they are referenced using the
@import "./file.css";
syntax. CSS files can be excluded from the bundle by marking them as external similar to JavaScript files.There is basic support for pretty-printing CSS, and for whitespace removal when the
--minify
flag is present. There isn't any support for CSS syntax compression yet. Note that pretty-printing and whitespace removal both rely on the CSS syntax being recognized. Currently esbuild only recognizes certain CSS syntax and passes through unrecognized syntax unchanged.Some things to keep in mind:
CSS support is a significant undertaking and this is the very first release. There are almost certainly going to be issues. This is an experimental release to land the code and get feedback.
There is no support for CSS modules yet. Right now all class names are in the global namespace. Importing a CSS file into a JavaScript file will not result in any import names.
There is currently no support for code splitting of CSS. I haven't tested multiple entry-point scenarios yet and code splitting will require additional changes to the AST format.
There's still a long way to go but this feels like a good point to publish what's there so far. It should already be useful for a limited set of use cases, and then I will expand use cases over time.
This feature will be especially useful with the addition of the plugin API (see issue #111) because then esbuild's bundler can be a "CSS linker" that runs on the output of whatever CSS post-processor you're using. The plugin API hasn't been released yet because I wanted to get basic CSS support in first so that there are at least two different core file types for the API to abstract over.
from esbuild.
I'm starting to work on CSS support. I just landed a basic parser and AST, and will be working on integrating it into the build system next.
Note that I'm not planning to support the full CSS ecosystem with this feature. Today's CSS is a very diverse ecosystem with many non-standard language variants. This is possible in part due to the fault-tolerant nature of CSS. I'm imagining this feature as mainly a "CSS linker" that is responsible for the critical parts of joining CSS files together and removing unused CSS code. The CSS parser will be expecting real CSS for use with browsers. Non-standard extensions may still be parsed without syntax errors but may be passed through unmodified (e.g. not properly minified or renamed) since they aren't fully understood by the parser. It should be possible to use syntax extensions (e.g. SASS) with a plugin that transforms the source code before esbuild reads it.
from esbuild.
I'm considering this to be implemented now that support for composes
is in (see the latest release notes for details). Any additional improvements and/or bug fixes can be tracked using new issues.
from esbuild.
Some things to try:
- If you don't need to bundle the CSS, then you may be able to literally just use the
file
loader for.css
files. - If you do need to bundle the CSS, you could try writing a plugin that calls
esbuild.build()
to bundle the CSS and then returns the URL:
const cssFilePlugin = {
name: 'css-file',
setup(build) {
const path = require('path')
build.onResolve({ filter: /\.css$/ }, args => {
return {
path: path.join(args.resolveDir, args.path),
namespace: 'css-file',
}
})
build.onLoad({ filter: /.*/, namespace: 'css-file' }, async (args) => {
const result = await esbuild.build({
entryPoints: [args.path],
bundle: true,
write: false,
})
return {
contents: result.outputFiles[0].text,
loader: 'file'
}
})
},
}
from esbuild.
The idea for the standard CSS modules is that they will return an instance of
CSSStyleSheet
(https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet and https://developers.google.com/web/updates/2019/02/constructable-stylesheets). So a "polyfill" would instantiate it with the css text. Shadow roots have a nice API for using those stylesheets, but they're not necessarily coupled to it. Hope fully it will be adopted more broadly.
Interesting, thanks for the clarification. FWIW I think this should be trivial to do with a plugin. That will let you experiment with this proposal.
My main concern is about what's enabled by default in esbuild, so far most (all?) things require an explicit opt-in which I think is important.
My current plan:
-
By default, esbuild will use the
css
loader for.css
files which will only interpret@import
statements and use those for bundling CSS together. This should also naturally work with code splitting without any additional configuration. When a CSS file is used as an entry point, the resulting output file will also be a CSS file without any JavaScript file (i.e. CSS will have first-class support). -
When a JavaScript file does
import "./file.css"
that will cause the generation of a parallel set of CSS files alongside the output files for the JavaScript entry points. So if you run esbuild onapp.ts
and that imports a JavaScript file that imports a CSS file, the output folder will containapp.js
andapp.css
. When a JavaScript file doesimport * as styles from "./file.css"
thestyles
object will be empty (no properties). -
CSS modules are opt-in by using the
css-module
loader. I may have a default extension mapping built-in for.module.css
to override the behavior of.css
, since that's a common convention in the wild. Only then willimport * as styles from "./file.css"
have any properties. This loader will still have the same output file generation behavior as the normal CSS loader. All imported CSS files will be bundled together into a single CSS output file (or perhaps more than one if code splitting is active and there are multiple entry points).
from esbuild.
And how will we be able to deal with sass/scss?
If you are using another language that sits on top of CSS, you will likely always need to use a plugin for it with esbuild.
from esbuild.
Code splitting doesn't work well with CSS at the moment, sorry. I'm working hard to fix this but it's a big undertaking. This is why there are warning signs next to code splitting and CSS in the documentation. See also #608.
from esbuild.
An initial implementation of the local CSS name transform has now shipped in version 0.18.14. It's off by default for now and must be manually enabled with either the local-css
or global-css
loaders. Documentation for this is now available here: https://esbuild.github.io/content-types/#local-css.
In addition, esbuild's support for CSS source maps was overhauled to be closer to parity with JS. There are now per-token source mappings in esbuild's CSS output, and esbuild uses the names
source map field to provide the original name for renamed CSS names.
If anyone wants to try it out, it would be interesting to hear what you think. Please consider the implementation experimental and subject to change as I plan to improve it more in the future (which is why this issue is still open).
from esbuild.
FYI for people following this issue: #415 was recently implemented making it possible to bundle url(...)
references to images in CSS.
from esbuild.
Hey there, good work on this.
Just wanted to share my experience of trying this out. I was hoping that import styles from "./my-css-file.css"
would yield a CSSStyleSheet
that can be used in conjunction with Constructable Stylesheets, as @LarsDenBakker was also pointing out in this comment. As it stands right now, I'm actually not seeing how I might be able to retrieve the StyleSheet or a string representing it such that I can work with it (without implementing it in JS/TS).
I'm aware that there is a gazillion ways of handling styles on the web. I'm also aware that the expectations for how this might work varies greatly from web developer to web developer. I'm also aware that many tools do the module-returning-class-names thing, probably the vast majority. But I would argue strongly in favor of .css
files becoming modules with a default export pointing to a CSSStyleSheet
. For the following reasons:
-
This is in line with current standardization efforts as seen with Import Assertions which is the foundation behind the JSON Modules proposal (which will most likely also be foundation behind CSS Modules).
-
This allows for passing styles to a root (such as the document element or any Shadow Root) via
adoptedStyleSheets
. -
This simplifies the implementation. In time, developers can leverage the plugin infrastructure to "override" the default behavior for different behavior, for example old-fashioned CSS Modules that returns an object of class names, etc.
Web developers who work with Shadow DOM are relying on being able to append stylesheets to a root at any point in the DOM tree, and won't benefit from, say, the imported styles being appended to the document root.
from esbuild.
^ ooo we finally have CSS modules / local class names implemented ๐
from esbuild.
@wessberg Can you confirm if the plugin API lets you write a plugin to enable the CSSStyleSheet
use case? I'm unfamiliar with the proposal but I imagine writing a plugin for this should be pretty simple if you just need to construct an object and return it.
The form of CSS modules where class names are local to the file and are exported to JavaScript seems better for building scalable web applications to me. That enables tree shaking and code splitting for CSS, which other solutions don't support AFAIK. Tree shaking of CSS isn't really possible to build as an esbuild plugin because it requires deep integration with the JavaScript layer. So I am still planning to explore this in esbuild itself.
from esbuild.
@evanw do you have an estimate of when this will be released? Awesome work on esbuild!
from esbuild.
Hi,
Any status on this issue?
I'm using a fork of https://www.npmjs.com/package/esbuild-css-modules-plugin but would be great to have something built-in.
Thanks a lot for the hard work!
from esbuild.
Promised I would get back to you. The plugin system works great! :-) It works as expected. Would be neat if plugins could get ahold of the esbuild
options. For example, I wanted to know if esbuild
was instructed to produce source maps from inside the plugin but didn't have any way of knowing, as far as I'm aware.
from esbuild.
The missing part is to create
loadStyle
function that must be accessible in any of the js file.
Plugins can introduce new virtual modules to do that:
build.onResolve({ filter: /^loadStyle$/ }, () => {
return { path: 'loadStyle', namespace: 'loadStyleShim' }
})
build.onLoad({ filter: /^loadStyle$/, namespace: 'loadStyleShim' }, () => {
return { contents: `export function loadStyle() { ... }` }
})
You could then do this:
return {
contents: `
import {loadStyle} from 'loadStyle'
loadStyle(${JSON.stringify(args.path)})
`,
loader: 'js',
}
from esbuild.
The missing part is to create
loadStyle
function that must be accessible in any of the js file.Plugins can introduce new virtual modules to do that:
build.onResolve({ filter: /^loadStyle$/ }, () => { return { path: 'loadStyle', namespace: 'loadStyleShim' } }) build.onLoad({ filter: /^loadStyle$/, namespace: 'loadStyleShim' }, () => { return { contents: `export function loadStyle() { ... }` } })You could then do this:
return { contents: ` import {loadStyle} from 'loadStyle' loadStyle(${JSON.stringify(args.path)}) `, loader: 'js', }
Wow, thanks, this is great, now what's left is the meat - creating style loader that handles css/scss with option to include postcss so we can use tailwind in esbuild...
from esbuild.
Hello @evanw. I am writing an application for Electron. Chrome now supports import assertions, I trying to prevent transforming line import style from 'style.css assert { type: 'css' }
, but i want to use esbuild for css bundling. Is it possible to transform css by esbuild and prevent transform import line (just replace import path)?
from esbuild.
@evanw One big benefit of CSS Modules is that it that it transparently mangles selectors thus giving the guarantee that all selectors will be unique. This effectively mitigates nearly all css ordering issues.
When CSS Modules is implemented, we should also be able to code split css to the same granularity that we code split JS -- which would be awesome.
from esbuild.
Here's my naive implementation:
const fs = require('fs');
const styleLoaderPlugin = {
name: 'styleLoader',
setup: build => {
// replace CSS imports with synthetic 'loadStyle' imports
build.onLoad({ filter: /\.css$/ }, async args => {
return {
contents: `
import {loadStyle} from 'loadStyle';
loadStyle(${JSON.stringify(args.path)});
`,
loader: 'js',
};
});
// resolve 'loadStyle' imports to the virtual loadStyleShim namespace which is this plugin
build.onResolve({ filter: /^loadStyle$/ }, args => {
return { path: `loadStyle(${JSON.stringify(args.importer)})`, namespace: 'loadStyleShim' };
});
// define the loadStyle() function that injects CSS as a style tag
build.onLoad({ filter: /^loadStyle\(.*\)$/, namespace: 'loadStyleShim' }, async args => {
const match = /^loadStyle\(\"(.*)"\)$/.exec(args.path);
const cssFilePath = match[1];
const cssFileContents = String(fs.readFileSync(cssFilePath));
return {
contents: `
export function loadStyle() {
const style = document.createElement('style');
style.innerText = \`${cssFileContents}\`;
document.querySelector('head').appendChild(style);
}
`,
};
});
},
};
module.exports = {
styleLoaderPlugin,
};
This seems to work and mimics the webpack style-loader functionality of injecting a <style>
tag into the HTML document's head.
However, I also have a .less file inside one of my node_modules dependencies, and I'm a bit stuck on figuring out how exactly to deal with that. I can use the less package to compile the file to CSS, I can even include that in this plugin, but what I'd like at the end of the day is: LESS is compiled to CSS -- esbuild builds it as a CSS entry point, resolving imports and bundling it into a single file (either on the disk or just in memory so that I have access to it from JS) -- my plugin above injects that into the HTML. I'm not quite sure if it's possible to have content handled this way - @evanw could you enlighten me, please?
from esbuild.
I have splitting bundles with css-chunks, but i can't load css on page...
I have some resolver for this, but its not working with splitting: true...
let fakecsspathResolvePlugin = { name: 'fakecsspath', setup(build) { let path = require('path') build.onResolve({ filter: /\.esbuild-svelte-fake-css$/ }, ({ path }) => { return { path, namespace: 'fakecss' } }) build.onLoad({ filter: /\.esbuild-svelte-fake-css$/, namespace: 'fakecss' }, ({ path }) => { const css = cssCode.get(path); return css ? { contents: css, loader: "css" } : null; }) }, }
Fixed ๐๐ปfrom v0.12.0 @evanw Tx for great bundler๐ค
from esbuild.
I will test it out and get back to you :-)
And again, while I realize some other bundlers are doing something similar to what you're planning for with CSS support in esbuild
, I'm just seeing a pretty massive gap between what is being worked on in the standards bodies and what is being implemented or have already been implemented across some bundlers. This might lead to confusion when the native runtime does things rapidly different to bundlers.
Importantly, it leaves out everyone working with Shadow DOM where styles are applied at roots across the DOM tree rather than globally at the document root, and where styles are already local to the tree from the Shadow root they are being applied to. These users will either need to declare the styles in JS/TS and either inline them in a <style>
tag appended to the root or manually generate CSSStyleSheets from them or write a custom plugin as suggested, and add it to adoptedStyleSheets for that root.
I think that the more "standards-compliant" (I put this in quotes, because standardization efforts are still ongoing here) approach should be default, and the more application-specific behavior should come from plugins.
But I of course respect your decision. I come from Rollup, which is pretty founded in being as close to native ES module semantics as practically possible, but there are obvious advantages to esbuild
(ease of use, performance), and I want this tool to succeed for everyone, including those that use Shadow DOM :-)
from esbuild.
I needed a loader that could import
a .less
file in TypeScript as a string (compiled to CSS), and I couldn't find anything to do that, so I wrote a plugin for it: https://github.com/arnog/esbuild-plugin-less
I hope someone else find this useful, and that this use case will be supported when the full CSS support is in.
Just switched to esbuild
and I'm super impressed. Great job @evanw !
from esbuild.
It would be lovely to have support for anonymous imports:
import "./styles.css"Which would scramble the classes in the CSS file and also in the component files (React in my use case)
I think this is a bad idea. There's no way to guarantee you are in fact scrambling the correct strings in the JS files. I imagine you're thinking of Tailwind here, but remember that TW does not change your source files. It actually accepts that it may compile additional classes that aren't actually used.
Also, importing a style object is a strength. With the right TS config, you can gain type safety across this boundary, ensuring that you aren't referencing classes that don't exist. Even without this benefit, the compiler flattens these references. There's no downside.
Lastly, there's a place for css imports that don't mangle, and we already have a convention for it. The burden of proof for giving up a convention should rightly be greater than for greenfield experimentation.
from esbuild.
Binary resources that don't need to be parsed such as PNG, JPEG, and font files are all the same to a bundler. Fonts don't need to be special cased as far as I'm aware. I think the existing loaders such as dataurl
and file
should cover fonts.
from esbuild.
- That's up to you, but it would probably look like some kind of on-load plugin that returns a JavaScript stub for esbuild to process: https://esbuild.github.io/plugins/#load-callbacks. That JavaScript stub could then do whatever you need it to.
- Intercepting import paths is done with an on-resolve plugin: https://esbuild.github.io/plugins/#resolve-callbacks. Plugins run for all file types including JavaScript and CSS.
from esbuild.
Probably unrelated, but I created a plugin that does so with scss, also supporting postcss plugin chains
https://github.com/Squirrel-Network/esbuild-sass-modules-plugin
from esbuild.
@evanw that's great to hear! There are different kinds of CSS modules out there, and there is also ongoing for a web standard for CSS modules (see whatwg/html#4898 and https://github.com/tc39/proposal-import-assertions). WIll esbuild allow specifying which variant to use?
from esbuild.
I listed import assertions just to indicate that there will be a standard way to import non-JS files.
The idea for the standard CSS modules is that they will return an instance of CSSStyleSheet
(https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet and https://developers.google.com/web/updates/2019/02/constructable-stylesheets). So a "polyfill" would instantiate it with the css text. Shadow roots have a nice API for using those stylesheets, but they're not necessarily coupled to it. Hope fully it will be adopted more broadly.
My main concern is about what's enabled by default in esbuild, so far most (all?) things require an explicit opt-in which I think is important.
from esbuild.
In other news, I'm also tripping up on the tilde-prefix convention that Webpack's style-loader uses to resolve imports from node_module
(https://webpack.js.org/loaders/css-loader/#import). Specifically, these occur in @import
statements inside of CSS/LESS files. I tried to extend the esbuild plugin to handle these imports but it doesn't seem to catch them - does the plugin even run on CSS files or JS/TS only?
from esbuild.
And how will we be able to deal with sass/scss?
If you are using another language that sits on top of CSS, you will likely always need to use a plugin for it with esbuild.
Could you clarify this a bit? Is it conceivable that scss modules could be supported provided that a plug-in does the compilation of scss to css?
from esbuild.
It would be lovely to have support for anonymous imports:
import "./styles.css"
Which would scramble the classes in the CSS file and also in the component files (React in my use case)
This has never been developed for any bundler and is a common issue component library developers face when deciding how to build their libraries. I specifically came across this issue while creating a framework for component libraries https://github.com/moishinetzer/PBandJ
On the one hand, using named imports slows down development and requires styles
to be referenced all over the file. On the other hand, if you go with CSS injection, you have no guarantee that your classes won't clash with your users' projects!
For example, you (realistically) could never use the class name .button
as that will most definitely be used throughout your users' projects.
This has been tried before here:
In reality, asking users to import a bundled CSS file is unrealistic as it does not get past the first point and only passes on the responsibility to the user to decide how to deal with these issues.
To conclude, this feature could be groundbreaking as no one provides this level of support (in-JS anonymous CSS imports that are bundled and local to its component). This includes tsup, rollup, and parcel, and I genuinely couldn't find any bundler that does this. (I would love to stand corrected)
from esbuild.
The spec version of CSS Modules returns a CSSStyleSheet
which is perfect for Web Components. There's no reading of the stylesheet either. You just attach it via adoptedStyleSheets
, either in Shadow DOM or document
. No mangling, no class inspection, no re-writing.
I would imagine a modern use case of an anonymous import would rewrite this:
import globalStyles from './ButtonGlobals.css' assert { type: 'css' };
document.adoptedStyleSheets = [...document.adoptedStyleSheets, globalStyles];
as this:
import './ButtonGlobals.css' assert { type: 'css' };
If you need some sort of customization of reading and parsing the actual CSSStyleSheet
, it would make sense to use build.onLoad
on an esbuild plugin that converts it to whatever custom, (class-based in your use case) Javascript Object.
from esbuild.
For some reason the statements:
@import "tailwindcss/base.css";
@import "tailwindcss/utilities.css";
@import "tailwindcss/components.css";
Are transformed into:
/* ../node_modules/.pnpm/[email protected][email protected]/node_modules/tailwindcss/base.css */
@tailwind base;
/* ../node_modules/.pnpm/[email protected][email protected]/node_modules/tailwindcss/utilities.css */
@tailwind utilities;
/* ../node_modules/.pnpm/[email protected][email protected]/node_modules/tailwindcss/components.css */
@tailwind components;
Rather than simply inlining them as desired with:
esbuild --bundle src/index.css
These naturally are not valid CSS statements and will not display the necessary styles.
from esbuild.
Using the assert
keyword like that is problematic for a few reasons. First of all, it has been deprecated since it was introduced and will never be a part of JavaScript (the standards committee changed their minds about it). Second of all, import assertions by design canโt affect how the module is loaded (the spec says this explicitly).
Import assertions are potentially going to be replaced with something else called import attributes, which will hypothetically use the with
keyword and which may fix this limitation (so attributes will be able to affect how the module is loaded. However, this feature hasnโt been added to any real JavaScript runtime yet so it remains to be seen how it will actually work when it becomes a part of JavaScript for real.
Given that the standards committee has already changed their mind and "unshipped" this proposal after it reached Stage 3, Iโm planning to wait to add support for import attributes to esbuild until after thereโs an implementation of it shipping in a real JavaScript runtime. This also means waiting to add support for it to any built-in CSS behavior as well as to esbuildโs plugin API.
from esbuild.
@jimmywarting This issue is about implementing https://github.com/css-modules/css-modules, not about import attributes. We can discuss how to integrate import attributes into esbuild after they are shipped in a real browser. Please leave the discussion of import attributes out of this issue.
@joelmoss I saw your comment but I'm confused. By server-rendered HTML I'm imagining something like this:
import { button } from './button.css'
console.log(`<div class="${button}"></div>`)
This should already works with what was shipped, as you can see here. Can you elaborate about why that doesn't work in your case?
from esbuild.
Canโt wait for built-in implementation! The existing plugins donโt work well in some cases because they all seem to be based on lightningcss, which requires native Node modules and appears to be poorly compatible with Yarnโs zero-install mode.
from esbuild.
+1 for the provided impl in "last" release, fulfills my expectations and needs for now. Thanks a lot.
from esbuild.
@asbjorn-brevio Yes. If a plugin wants to make use of the local-css
loader, it has to set that in the onLoad() callback, or at least return {loader: 'default'} and let user set that in the build options. Here's a minimal example to write such plugin:
// build.mjs
import { build } from "esbuild";
import { compile } from "sass";
var sass_module_plugin = {
name: "local-sass",
setup({ onLoad }) {
onLoad({ filter: /\.module\.scss$/ }, (args) => {
const { css } = compile(args.path);
return { contents: css, loader: "local-css" };
// ^^^^^^^^^^^
});
},
};
await build({
entryPoints: ["index.js"],
bundle: true,
outdir: "dist",
plugins: [sass_module_plugin],
}).catch(() => process.exit(1));
// index.js
import { foo } from "./foo.module.scss";
console.log(foo);
/* foo.module.scss */
$name: "foo";
.#{$name} {
color: red;
}
from esbuild.
Would this include font files, or is that a separate task?
from esbuild.
@evanw will the CSS feature inline the minified CSS in the javascript bundle or emit it as a separate file? While inlining is a good fit for client rendered applications, it is not ideal for server or static rendered applications. Ideally, esbuild would emit a number of CSS chunks, and the metafile would detail which CSS chunks correspond to each javascript entrypoint. A server or build tool would then be able to add references to the CSS for each page using <link>
tags in the HTML it generates.
from esbuild.
Import assertions seem unrelated to CSS to me. They just look like a way to tell the runtime what the file type is. Bundlers already have a way of doing this: they just use the file extension without needing an inline annotation, so this information is not necessary for bundlers.
This seems like an API to inject normal CSS inside a shadow DOM element? It looks like it would just be normal CSS as far as the bundler is concerned.
The CSS module implementation I'm thinking of adopting is the original one: https://github.com/css-modules/css-modules. Specifically, the use of local-by-default names, the :local
and :global
selectors, the composes
declarations, and being able to import local names as strings in JavaScript.
will the CSS feature inline the minified CSS in the javascript bundle or emit it as a separate file?
It will be in a separate file. I believe this is the current expected behavior. It's how Parcel works, for example. I was not considering inlining it in the JavaScript code. I am also planning on having code splitting work the same way across JavaScript and CSS files.
from esbuild.
Thanks, I think that default makes sense ๐
from esbuild.
That behavior sounds ideal. Can't wait for it!
from esbuild.
Hey Evan!
I was just curious if you have any roadmap for CSS, by chance. I saw mentions of features that are still lacking (#468 (comment)) plus mentions that a CSS rewrite is on the to-do list (#519 (comment)), but thereโs no full picture at the moment, as far as Iโm aware.
Iโm writing this primary because we at Framer are migrating to ESBuild, and thereโre two CSS issues weโre currently facing:
So Iโm trying to figure out how much we should invest into workarounds for these on our side. (Like, what if the CSS rewrites comes out in a week and fixes both issues?)
from esbuild.
Is it possible to tell esbuild to only bundle @import statements, but not other urls like background urls or font files? Similar to postcss-import
.
I tried --external:woff2
or --external:jpg
, but that did not work.
from esbuild.
I tried
--external:woff2
or--external:jpg
, but that did not work.
Marking files as external by file extension requires a wildcard: --external:*.woff2
. Documentation for this feature is here: https://esbuild.github.io/api/#external.
from esbuild.
Marking files as external by file extension requires a wildcard
Ah yes, that partially works.
For example: I am using --external:*.jpg --external:*.gif --external:*.woff2 --bundle
which is slightly error prone due to requering every extension to be listed.
It worked for url(/static/img/steam.gif)
, but not for url(/static/img/salesbg.jpg?v=2)
or ../fonts/Inter-Regular.woff2?v=3.15
. It doesn't like the query parameter in urls.
from esbuild.
Ah, I see. The fully general form of marking a file as external is to use an on-resolve plugin: https://esbuild.github.io/plugins/#resolve-callbacks. That works for arbitrarily complex requirements.
from esbuild.
Does esbuild handle css injection to DOM ?
Current behavior of another builder is to generate js to inject imported css to DOM, so for example:
import "./index.css"
will generate javascript code to inject a <style>
to <head>
or <body>
, and then replacing import "./index.css"
with the generated code.
from esbuild.
Does esbuild handle css injection to DOM ?
Current behavior of another builder is to generate js to inject imported css to DOM, so for example:import "./index.css"
will generate javascript code to inject a
<style>
to<head>
or<body>
, and then replacingimport "./index.css"
with the generated code.
I just did this as a plugin:
export const cssLoader = () => ({
name: 'css-loader',
setup: function (build: any) {
build.onLoad({ filter: /\.s?css$/ }, async (args: any) => {
return {
contents: `loadStyle("${args.path}")`,
loader: 'js',
}
})
},
})
The missing part is to create loadStyle
function that must be accessible in any of the js file.
from esbuild.
Hey @evanw, sorry for nagging but I'm still really hoping you could point me in the right direction regarding these 2 questions:
- How should I approach less files if I'm handling the css files with a self-written style-loader plugin that injects them into a style tag in the page?
- How should I approach the tilde-prefixed imports in css/less files? Specifically, can I intercept the css import resolution from an esbuild plugin or does that only work for javascript files?
from esbuild.
I have splitting bundles with css-chunks, but i can't load css on page...
I have some resolver for this, but its not working with splitting: true...
let fakecsspathResolvePlugin = {
name: 'fakecsspath',
setup(build) {
let path = require('path')
build.onResolve({ filter: /\.esbuild-svelte-fake-css$/ }, ({ path }) => {
return { path, namespace: 'fakecss' }
})
build.onLoad({ filter: /\.esbuild-svelte-fake-css$/, namespace: 'fakecss' }, ({ path }) => {
const css = cssCode.get(path);
return css ? { contents: css, loader: "css" } : null;
})
},
}
from esbuild.
Splitting css by js-chunks ok, but havenโt any loaders logic...
from esbuild.
What's the progress here? And how will we be able to deal with sass/scss?
from esbuild.
This effectively mitigates nearly all css ordering issues.
I believe that is true only if you are using exclusively css modules. If you have a mix of global css and css modules, which is common, then it's important to maintain the import order so the cascade is correct.
from esbuild.
I wasn't able to get CSS files to be minified when using:
import styles from './MDWButton.css' assert { type: 'css' };
My JS files load fine in Chrome natively, but to make it work on FireFox, I have to convert to CSSStyleSheet
:
import { readFile } from 'node:fs/promises';
import CleanCSS from 'clean-css';
import esbuild from 'esbuild';
const cleanCss = new CleanCSS();
await esbuild.build({
entryPoints: ['index.js'],
format: 'esm',
sourcemap: true,
minify: true,
bundle: true,
outfile: 'index.min.js',
plugins: [{
name: 'css import assertions',
setup: (build) => {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const css = await readFile(args.path, 'utf8');
const { styles } = cleanCss.minify(css);
const contents = `
let contents = \`${styles.replaceAll(/`/g, '\\`')}\`;
let styles;
try {
styles = new CSSStyleSheet();
styles.replaceSync(contents);
} catch (e) {
styles = contents;
}
export default styles;`;
return { contents };
});
},
}],
});
As for Safari, which doesn't support CSSStyleSheet
, I just return the string and rebuild it as an inline style. That's all fine, but is there a way to make minify with the built-in CSS minifier? I was trying to avoid extra dependencies than just npx esbuild
.
from esbuild.
is there a way to make minify with the built-in CSS minifier?
@clshortfuse I guess you're asking how to use esbuild instead of clean-css to minify a css file. This is quite easy:
let args_path = '/path/to/a.css'
let r = await esbuild.build({
entryPoints: [args_path],
bundle: true,
minify: true,
write: false, // <- get result in memory
target: ['safari6'], // <- adjust css features
})
let minified = r[0].text.trimEnd()
from esbuild.
@hyrious Thanks!
I didn't even consider just calling esbuild within esbuild:
import esbuild from 'esbuild';
/** @type {import('esbuild').Plugin} */
export default {
name: 'css import assertions',
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const { outputFiles } = await esbuild.build({
entryPoints: [args.path],
bundle: true,
minify: build.initialOptions.minify,
minifyWhitespace: build.initialOptions.minifyWhitespace,
minifyIdentifiers: build.initialOptions.minifyIdentifiers,
minifySyntax: build.initialOptions.minifySyntax,
target: build.initialOptions.target,
write: false,
});
const [file] = outputFiles;
const { text } = file;
const jsText = text.trim()
.replaceAll(/`/g, '\\`')
.replaceAll(/\\([\da-f]+)/gi, (match, p1) => String.fromCodePoint(Number.parseInt(p1, 16)));
const contents = /* js */ `
let contents = \`${jsText}\`;
let sheet;
try {
sheet = new CSSStyleSheet();
sheet.replaceSync(contents);
} catch (e) {
const doc = document.implementation.createHTMLDocument()
const style = doc.createElement('style');
style.textContent = contents;
doc.head.append(style);
sheet = style.sheet;
// Note: Removing style from document will nullify sheet
}
export default sheet;`;
return { contents };
});
},
};
Works great! Thanks, again.
Edit (2023-01-26): Rewritten as ESM Plugin and fixed Unicode. Also, now always returns CSSStyleSheet
which makes more sense for type-checking. On browsers that don't support constructable CSSStyleSheet, you'll have to convert it in runtime to whatever works for you (HTMLStyleElement
or just raw string), like so:
const asString = [...styleSheet.cssRules].map((r) => r.cssText).join('\n');
const asElement = document.createElement('style');
el.textContent = asString;
from esbuild.
One more simple solution with configurable hash - https://github.com/Valexr/Slidy/blob/master/packages/svelte/cssmodules.js
from esbuild.
I am importing my css files with the builtin dataurl strategy, and i notice that it does not follow any @import directives (so the data url will be useless if the css is modular)
@evanw is there anyway currently to get a dataurl that fully traversed the @import tree?
from esbuild.
I think this is a bad idea. There's no way to guarantee you are in fact scrambling the correct strings in the JS files.
I don't know what you mean by there's no way to guarantee the scramble. It's exactly how regular CSS modules work rather it runs through them iteratively rather than on an import basis.
I imagine you're thinking of Tailwind here, but remember that TW does not change your source files. It actually accepts that it may compile additional classes that aren't actually used.
Funnily enough, I thought tailwind would be a problem for my suggested implementation, given you must import the tailwind derivatives, and they have to be global inherently to apply to all of the classes in every component. On your last point, it doesn't compile additional classes that aren't used, given it now solely runs on its JIT compiler.
Also, importing a style object is a strength. With the right TS config, you can gain type safety across this boundary, ensuring that you aren't referencing classes that don't exist. Even without this benefit, the compiler flattens these references. There's no downside.
I've never seen this being implemented properly (the .d.css
files never quite work properly for CSS, but it's a reasonable point. This does not, however, detract from how the implementation could still support named imports. My suggestion is specifically based on anonymous imports allowing developers to use other stylesheets that aren't modules or allowing them not to need to tag on from the styles
being imported.
Lastly, there's a place for css imports that don't mangle, and we already have a convention for it. The burden of proof for giving up a convention should rightly be greater than for greenfield experimentation.
The goal of this is, as I mentioned clearly in my original text: to simplify the developer experience by allowing anonymous imports (not everyone enjoys/knows how to use css modules), which will also allow external CSS files to be imported and release the stress of component library developers on how they bundle and release styles along with their components (It's a tough problem! Giving it a proper shot will show how hard it can be to do)
from esbuild.
I wonder what should happen if css module is imported inside JS/TS which is called inside a worker?
Right now we just import css and it is attached to the global css code but there is no css encapsulation.
Is the intention to have a single css output but with modified class names to get "modularity"?
from esbuild.
I wonder what should happen if css module is imported inside JS/TS which is called inside a worker?
CSSStyleSheet
is part of window (window.CSSStyleSheet
). It means Web Workers and Service Workers can't natively build their own CSSStyleSheet
. It means, even if they support the import, you can't polyfill it. I do believe it's supported, but haven't tried it.
Is the intention to have a single css output but with modified class names to get "modularity"?
That sounds like custom encapsulation. You don't need classes with Web Components which is the direction of spec (see adoptedStyleSheets
). There's no need to tag elements with classes to identify them. You can use #id
for identification because of Shadow Root. If you need scoping, then you need to modify the style sheet. (edit: or write the CSS already scoped)
If you do want to modify (scope) CSS, then you have to run the custom steps in compile phase before passing the CSS (preprocessor) or as a build script (postprocessor). I'm not sure esbuild
has the ability to stream the CSS tokens as it reads them, so if we're talking compiler phase, you'd have to use your own CSS processor (cssnano
).
Depending on your usage, runtime may be better, especially if you can tap into web workers, because in theory, you can run import CSSStyleSheet
objects in parallel, and pass back to the main thread with your encapsulation. Anything compiler-phase will hit the main thread and all you'd get with native CSS imports is parallel CSSStyleSheet
parsing done by the browser (hits your main thread as a ready to use CSSStyleSheet
instead of a string).
from esbuild.
Most think of css modules in term of the original css-module, aka:
import styles from './style.css'
// import { className } from './style.css'
element.innerHTML = '<div class="' + styles.className + '">'
This is very useful way of turning them into kind of vanilla javascript modules that can be treeshaked later on. And it's useful but i don't think we should be striving for this non-standard solution cuz that's not how native import without build steps works... importing a css with import stylesheet from './style.css' assert { type: 'css' }
should return a CSSStyleSheet
So that's what should be imported.
I'm uncertain if it would be good to inlined the css and then constructing a CSSStyleSheet
A css can also be imported with <link>
. and if some library imports jquery-ui via a cdn from a <link>
then import
should really be fetching the same css and use the same cache.
i also might want to put the css into CacheStorage with service workers.
So i guess it would be better if when you try to import a css like this:
import style from './style.css' assert { type: css }
then it turns into this top level await solution:
export default await fetch(import.meta.resolve('./style.css'))
.then(res => res.text())
.then(css => {
const style = new CSSStyleSheet()
style.replace(css)
return style
})
from esbuild.
@jimmywarting Hi ๐
The problem with the top level await is that it makes it asynchronous. For JS executing before first render you might want that CSSStylesheet
parsed before first paint.
My use case is Web Components, so they are registered before DOMContentLoad, constructed, and adoptedStyleSheet
added to the shadow root all before the first browser "tick". Right now it all works without bundling on Chrome, so I'd expect esbuild
to just minify/bundle without any side effects.
Async imports does a syntax, so I'd imagine the bundler can analyze for that.
import("foo.css", { with: { type: "css" } })
Spec has changed a bit. See
https://github.com/tc39/proposal-import-attributes
I have a comment a bit higher up that shows how to replace the import with anything you want. It's not perfect, but works around Safari not having constructable CSSStyleSheet
. Note that new CSSStyleSheet
is part of window
so I don't think it'll work on Service Workers which may cause an issue with the your cache strategy. (Haven't tried it personally).
I put my implementation on ice, since Safari team is waiting for spec to get finalized before implementing and it's impossible to polyfill for unbundled environments. But last I tried, it worked fine.
Considering we all have different use cases, it might be helpful for esbuild to give us devs a more streamlined approach to implementing all our different strategies.
Edit: Just in case, here is the actual code I was using.
from esbuild.
so I'd expect
esbuild
to just minify/bundle without any side effects.
That was mostly what i wanted to get out of my comment... i would not expect either to get anything else out other than a CSSStylesheet
if it now works natively in chrome. my solution is just brainstorming solutions to deliver it.
from esbuild.
Thanks โค๏ธ for the native implementation.
In case you need support for composes
and local names for keyframe animations, grid lines, etc you can use parcel's Lightning CSS via my adapter: https://github.com/mhsdesign/esbuild-plugin-lightningcss-modules
from esbuild.
Amazing! But unfortunately it's only useful for use with JS imports. And that is no good for use within server rendered HTML. Step in the right direction, but quite limited - at least for me.
from esbuild.
Amazing! But unfortunately it's only useful for use with JS imports. And that is no good for use within server rendered HTML. Step in the right direction, but quite limited - at least for me.
Wait, the renaming is happening in the build step and imports are just references to string values of css classes. Am I missing something?
from esbuild.
Wait, the renaming is happening in the build step and imports are just references to string values of css classes. Am I missing something?
Yes, but the exported class names will get renamed if and when collisions occur.
from esbuild.
๐
I think it was a good decision to rename it to something else entirely and having it off by default (for now) given now that it's possible to natively import css files without bundlers now.
I could expect that this css preprocessor tech becomes less relevant the more ppl start to use custom elements (web components)
It's unfortunately that the term "CSS Module" became overloaded.
from esbuild.
I tested my existing plugin for native CSS modules and because plugins are resolved first, it's not an issue even if default is turned on. Though right now we can't differentiate between code that loads
import { button } from './button.css';
import globalStyleSheet from './globals.css' assert { type: 'css' };
Also, files only load once meaning you can't opt into using local-css
based on one import type and opt into using the plugin for native CSS Modules.
import globalStyleSheet from './globals.css' assert { type: 'css' };
import { button } from './globals.css';
Nothing major, but just edge cases. Maybe something in OnLoadOptions
or OnLoadArgs
can be passed to differentiate the two. Nice work!
from esbuild.
How would ppl feel about this kind of hypothetical syntax?
import globalStyleSheet from './globals.css' with { type: 'css' } // native (untouched)
import { button } from './globals.css' with { type: 'x-css-local' } // esbuild preprocess solution.
import * as util from './util.ts' with { type: 'x-typescript' }
from esbuild.
As I mentioned earlier, this is only useful for use with JS imports. And that is no good for use within server rendered HTML. So is quite limited.
Do you have plans to expand this to expose the renamed class names, perhaps as part of the metafile?
from esbuild.
Can you elaborate about why that doesn't work in your case?
The problem is that you are assuming that I will only be using JS and importing the CSS, which is not always true. Sometimes I'm including CSS with <link>
tags in HTML. So this implementation will not work in that case.
To make local-css work outside of JS, esbuild will need to expose the exported CSS classes perhaps as part of the metafile, or support a way of making the exported class names deterministic, similarl to how webpack's CSS-loader does it using localIdentName
.
Right now it is not a huge issue for me, as I have already built my own naive implementation of CSS modules, which works so far. But I would much prefer an esbuild native solution.
Hope that helps explain things a little. thx
from esbuild.
I think including the CSS with a <link>
tag in the HTML should be fine if the HTML is JS-generated. Something like this. If you're talking about linking to CSS from hand-written (non JS-generated) HTML and then minifying CSS names within the HTML itself then yes, esbuild's local CSS name transform won't work for that use case. But I'd argue that in that situation you probably shouldn't be processing the linked CSS using the local CSS name transform. Maybe providing an example of what you're trying to achieve would help.
To answer your deeper question: I'm open exposing the name mapping somehow at some point, but I haven't figured out how yet. Having specific example use cases for this data to think through will be helpful when adding that feature so thanks for describing what you want to achieve.
from esbuild.
My specific use case is with Ruby on Rails, so the server generated HTML is from Ruby, not JS. But sometimes I use React, which is why I have a mix of JS and server (Ruby) generated HTML.
When the HTML is generated from the server, one or more stylesheets are also included as <link>
tags, with the src of each being generated by esbuild, and my custom CSS module plugin. If the name of the CSS files ends with .module.css
all classnames in that file are renamed to something like [className]-[filenameHash]
.
Now because these renamed class names are deterministic, and I already know the filenameHash
and className
, I can then use said class names anywhere in the server generated HTML.
I have a little helper that I can call on the server by simply passing it the name of the class and the path of the CSS file.
So as an example, I have a CSS file at /some/path/to/styles.module.css
and a class name of myclass
. I add the <link>
tag with a src
of /some/path/to/styles.module.css
into the HTML. Then I calculate the hash of that path, giving me this:
.myclass-abcd1234 {
color: red;
}
<div class="myclass-abcd1234" />
The above would actually be far better than reading any exposed name mapping, as I do not not need to know anything about the mapping. It would also be a lot less work to implement. There is also no chance of conflicts, as it is safe to assume that all classes within each stylesheet are unique.
Hope that helps explain my use case.
from esbuild.
Note that I'm not planning to support the full CSS ecosystem with this feature. Today's CSS is a very diverse ecosystem with many non-standard language variants. This is possible in part due to the fault-tolerant nature of CSS. I'm imagining this feature as mainly a "CSS linker" that is responsible for the critical parts of joining CSS files together and removing unused CSS code. The CSS parser will be expecting real CSS for use with browsers. Non-standard extensions may still be parsed without syntax errors but may be passed through unmodified (e.g. not properly minified or renamed) since they aren't fully understood by the parser. It should be possible to use syntax extensions (e.g. SASS) with a plugin that transforms the source code before esbuild reads it.
Question about the (highlighted) last sentence above:
Is this supported in the current version (0.18.17)?
If not, are there still plans for supporting it?
I tried setting local-css
for .module.scss
, .module.css
, .scss
and .css
and as far as I can tell none of the classes from the SCSS files are getting renamed (even conflicting ones). The output JS seems to have been transformed as expected though
from esbuild.
with a plugin
I believe you need a plug-in to transform SCSS to the custom CSS syntax with :global()
and :local()
support (not just regular CSS). After that the plugin calls esbuild with the local-css
or global-css
loader on the transformed CSS and returns its result.
from esbuild.
@clshortfuse Yeah, I'm using esbuild-sass-plugin
to transform the SCSS. Is the plugin the problem?
from esbuild.
@hyrious Got it, thanks!
I've postponed migrating to esbuild awaiting this feature so I haven't internalized how this all works yet. Sorry for the noise.
from esbuild.
FWIW, local-css
is working well for CSS in my simple case so far. I havenโt yet tested it very thoroughly, but in a very simple (for now) frontend migration from esbuild-css-modules-plugin
to local-css
loader was painless, it required no changes to TSX/CSS sources and let me shed a lot of dependencies right away.
from esbuild.
Related Issues (20)
- ERROR: Expected "*/" to terminate multi-line comment HOT 2
- Expected ";" but found "const" - typescript transformer HOT 2
- Option to disable injected comment of input path file HOT 2
- Additional directories to watch HOT 3
- esbuild does not fail with undefined functions HOT 5
- Segmentation fault when using with vite and sveltekit project HOT 1
- Glob imports must match exactly
- Pass argument to IIFE HOT 2
- Esbuild causing bundle breakage when applied twice to the same bundle
- Splitting results in `Dynamic require of "<filename>" is not supported` when targeting `chrome62`
- [Feature Request] Add built-in HTML loader HOT 1
- Imports from stdio entrypoint don't have a namespace set
- DCE is not removed when value is from a const in another module.
- Get metafile info from a `watch()` rebuild HOT 2
- removes the backslash "\" when minify option is false HOT 4
- [Sourcemap] Private properties don't get sourcemapped correctly HOT 3
- ENV Secrets accessible from js code in a React + Rails APP. HOT 7
- [Feature] Ability to save charts from Bundle Size Analyzer as svg files
- TypeError: Cannot set property X of #<Object> which has only a getter HOT 1
- Line limit option (lineLimit) breaks code HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. ๐๐๐
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from esbuild.