A plugin for esbuild to handle Sass & SCSS files.
- PostCSS & CSS modules
- support for constructable stylesheet to be used in custom elements or
dynamic styleto be added to the html page - Support for Sass Embedded Async API
- caching
- url rewriting
- pre-compiling (to add global resources to the sass files)
$ npm i esbuild-sass-pluginJust add it to your esbuild plugins:
import {sassPlugin} from 'esbuild-sass-plugin'
await esbuild.build({
...
plugins: [sassPlugin()]
})You can pass a series of options to the plugin that are a superset of Sass
compile string options.
The following are the options specific to the plugin with their defaults where applicable:
| Option | Type | Default |
|---|---|---|
| filter | regular expression (in Go syntax) | /.(s[ac]ss|css)$/ |
| type | "css""local-css""style""lit-css""css-text" (css:string,nonce?:string)=>string |
"css" |
| cache | boolean or Map | true (there is one Map per namespace) |
| transform | function | |
| loadPaths | string[] | [] |
| precompile | function | |
| importMapper | function | |
| cssImports | boolean | false |
| nonce | string | |
| prefer | string | preferred package.json field |
| namedExports | boolean or function | false |
| quietDeps | boolean | false |
| silenceDeprecations | string[] | [] |
| embedded | boolean | false |
Two main options control the plugin: filter which has the same meaning of filter in esbuild
allowing to select the URLs handled by a plugin instance, and type which specifies how the CSS should be rendered and imported.
The default filter is quite simple but also quite permissive. When specifying a custom regex bear in mind that this is in Go syntax
If you have URLs in your imports and you want the plugin to ignore them you can't use a filter expression like:
/^(?!https?:).*\.(s[ac]ss|css)$/because Go regex engine doesn't support lookarounds but you can use esbuild'sexternaloption to ignore these imports or try a solution like this one.
You can list multiple plugin instances so that the most specific RegEx comes first:
await esbuild.build({
...
plugins: [
sassPlugin({
filter: /\.module\.scss$/,
transform: postcssModules()
}),
sassPlugin({
filter: /\.scss$/
}),
],
...
})This option enables the faster sass-embedded and is false by default for compatibility.
sass-embeddedis not available on every platform, so it is a peer dependency. Install it manually if your package manager doesn't do it automatically, then setembedded: trueto enjoy the speed boost!
The example in Usage uses the default type css and will use esbuild's CSS loader, so your transpiled Sass
will be in index.css alongside your bundle.
In all other cases esbuild won't process the CSS content — that is handled by the plugin instead.
If you want
url()resolution or other processing you need to usepostcsslike in this example
NOTE: Since version 2.7.0 the css type also works with PostCSS, CSS modules, and any transformation function
by keeping an internal cache of CSS chunks (virtual CSS files) and importing them in the module wrapping the contents.
This mode uses esbuild's built-in CSS modules support (i.e. the local-css loader).
Use this for lightweight Sass integration that leverages esbuild's built-in CSS processing features:
await esbuild.build({
...
plugins: [
sassPlugin({
filter: /\.module\.scss$/,
type: 'local-css'
}),
sassPlugin({
filter: /\.scss$/,
type: 'css'
}),
],
...
})In this mode the stylesheet will be in the JavaScript bundle and will be dynamically added to the page when the bundle is loaded.
Use this mode to import the resulting CSS as a string:
await esbuild.build({
...
plugins: [sassPlugin({
type: "css-text",
...
})]
})...and in your module do something like:
import cssText from './styles.scss'
customElements.define('hello-world', class HelloWorld extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.sheet = new CSSStyleSheet();
this.sheet.replaceSync(cssText);
this.shadowRoot.adoptedStyleSheets = [this.sheet];
}
}Import a lit-element css result using type: "lit-css":
import styles from './styles.scss'
@customElement("hello-world")
export default class HelloWorld extends LitElement {
static styles = styles
render() {
...
}
}You can provide your own module factory as type — a function that receives the CSS text and the nonce token and returns the source content to be used in place of the import.
Look in test/fixtures folder for more usage examples.
The cache is enabled by default and can be turned off with cache: false.
Each plugin instance creates and maintains its own cache (as a Map) that lives for the duration of the build.
You can pass a Map to preserve the cache across subsequent builds, but sharing the same cache between different
instances may cause issues if the contents are incompatible.
If you are unsure, keep a separate Map for each plugin instance.
When set to true the plugin rewrites node-modules relative URLs starting with the ~ prefix so that
esbuild can resolve them similarly to what css-loader does.
Although this practice is kind of deprecated nowadays some packages still use this notation (e.g.
formio). See issue #74 for context.
In the presence of Content-Security-Policy
(CSP)
the nonce option allows specifying a nonce attribute for dynamically generated <style> elements.
If the nonce string starts with window, process, or globalThis it is left unquoted in the code:
sassPlugin({
type: 'style',
nonce: 'window.__esbuild_nonce__'
})This allows defining it globally or leaving it for a subsequent build to resolve via esbuild define:
define: {'window.__esbuild_nonce__': '"12345"'}When specified, allows importing npm packages that have sass or style fields in package.json, preferring those over main.
NOTE: This is an experimental feature
- it replaces the internal use of
require.resolvewith browserifyresolve.sync- it only applies to imports prefixed by
~
A function to customize/re-map import paths, covering both import statements in JavaScript/TypeScript
and @import in Sass/SCSS. You can use this option to re-map import paths like tsconfig's paths option.
e.g. given this tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@img/*": ["./assets/images/*"]
}
}
}resolve these paths with importMapper:
await esbuild.build({
...,
plugins: [sassPlugin({
importMapper: (path) => path.replace(/^@img\//, './assets/images/')
})]
})If your Sass references resources with relative URLs (see #48) esbuild will struggle to rewrite them because it has no knowledge of the imports the Sass compiler has gone through. The importer API allows rewriting relative URLs as absolute ones, which esbuild can then handle.
Here is an example of how to do the url(...) rewrite (make sure to handle \ on Windows):
const path = require('path')
await esbuild.build({
...,
plugins: [sassPlugin({
precompile(source, pathname) {
const basedir = path.dirname(pathname)
return source.replace(/(url\(['"]?)(\.\.?\/)([^'")]+['"]?\))/g, `$1${basedir}/$2$3`)
}
})]
})Look for a complete example in the precompile fixture.
Prepending a variable for a specific pathname:
const context = { color: "blue" }
await esbuild.build({
...,
plugins: [sassPlugin({
precompile(source, pathname) {
const prefix = /\/included\.scss$/.test(pathname)
? `$color: ${context.color};`
: ''
return prefix + source
}
})]
})Prepending an @import of a globals file only for the root file (to avoid re-importing in nested files):
await esbuild.build({
...,
plugins: [sassPlugin({
precompile(source, pathname, isRoot) {
return isRoot ? `@import '/path/to/globals.scss';\n${source}` : source
}
})]
})async (this: SassPluginOptions, css: string, resolveDir?: string) => Promise<string>A function invoked before passing the CSS to esbuild or wrapping it in a module. It can be used to do PostCSS processing and/or to create CSS modules.
NOTE: Since
v1.5.0transform can return either a string or an esbuildLoadResultobject. This is whatpostcssModulesuses to pass JavaScript modules to esbuild, bypassing the plugin output altogether.
const postcss = require('postcss')
const autoprefixer = require('autoprefixer')
const postcssPresetEnv = require('postcss-preset-env')
esbuild.build({
...,
plugins: [
sassPlugin({
async transform(source, resolveDir) {
const {css} = await postcss([autoprefixer, postcssPresetEnv({stage: 0})]).process(source)
return css
}
})
]
})A helper function is available to do all the work of calling PostCSS to create a CSS module:
const {sassPlugin, postcssModules} = require('esbuild-sass-plugin')
esbuild.build({
...,
plugins: [sassPlugin({
transform: postcssModules({
// ...options for postcss-modules: https://github.com/madyankin/postcss-modules
})
})]
})postcssModules produces JavaScript modules handled by esbuild's js loader.
It also accepts an optional array of PostCSS plugins as a second parameter.
Look into fixture/css-modules for the complete example.
NOTE:
postcssandpostcss-modulesmust be added to yourpackage.json.
When using transform (e.g. with PostCSS), this option allows named exports alongside the default export.
When set to true, "safe-identifiers" is used to sanitize names and ensure they are valid JS identifiers.
Any altered identifier name will be logged like:
Exported 'class-name' as 'class_name' in 'test/fixtures/named-exports/style.css'
Exported 'switch' as '_switch' in 'test/fixtures/named-exports/style.css'The original name is still available on the default export:
import style, { class_name, _switch } from './style.css'
console.log(style['class-name'] === class_name) // true
console.log(style['switch'] === _switch) // trueYou can also supply a function to control how exported names are generated:
namedExports: name => /-/.test(name) && name.replace(/-/g, '_')In order for quietDeps to correctly identify external dependencies, the url option is defaulted to the importing file path URL.
The
urloption can cause problems when importing source Sass files from third-party modules. The best workaround is to avoidquietDepsand mute the logger if needed.
Accepts an array of deprecation identifiers to omit from logs during a build.
There's a working example of using pnpm with @material design
in issue/38
Windows 11 Pro - i7-490K CPU @ 4.00GHz - RAM 32GB - SSD 500GB
Given 24 × 24 = 576 lit-element files & 576 imported CSS styles plus the import of the full bootstrap 5.1
| sass-embedded | sass-embedded (no cache) | dart sass | dart sass (no cache) | |
|---|---|---|---|---|
| initial build | 731.312ms | 779.363ms | 2.450s | 2.450s |
| rebuild (.ts change) | 170.35ms | 188.861ms | 179.125ms | 1.710s |
| rebuild (.ts change) | 155.802ms | 167.413ms | 176.849ms | 1.576s |
| rebuild (.scss change) | 203.746ms | 160.601ms | 188.164ms | 1.575s |
| rebuild (.scss change) | 152.733ms | 144.754ms | 145.835ms | 1.520s |

