372 lines
14 KiB
Markdown
372 lines
14 KiB
Markdown
![cooltext394785080075403](https://user-images.githubusercontent.com/160981/136289874-26ce7269-ea08-47dd-be31-9bf0ef7a0b8d.png)
|
||
![image](https://user-images.githubusercontent.com/160981/150880682-4915c4dd-726b-4fea-8f3b-597d191f05bc.png)
|
||
|
||
[![Build Status][travis-image]][travis-url]
|
||
|
||
A plugin for [esbuild](https://esbuild.github.io/) to handle Sass & SCSS files.
|
||
|
||
### Features
|
||
* **PostCSS** & **CSS modules**
|
||
* support for **constructable stylesheet** to be used in custom elements or `dynamic style` to be added to the html page
|
||
* uses the **new [Dart Sass](https://www.npmjs.com/package/sass) Js API**.
|
||
* caching
|
||
* **url rewriting**
|
||
* pre-compiling (to add **global resources** to the sass files)
|
||
|
||
### Breaking Changes
|
||
* `type` has been simplified and now accepts only a string. If you need different types in a project [you can use more
|
||
than one instance](https://github.com/glromeo/esbuild-sass-plugin/issues/60) of the plugin.
|
||
You can have a look at the [**multiple** fixture](https://github.com/glromeo/esbuild-sass-plugin/blob/main/test/fixture/multiple)
|
||
for an example where **lit CSS** and **CSS modules** are both used in the same app
|
||
* The support for [node-sass](https://github.com/sass/node-sass) has been removed and for good.
|
||
Sadly, node-sass is at a dead end and so it's 1.x.
|
||
* `transform` now is expected to send back the CSS text in contents and anything that has to be default exported in `pluginData`.
|
||
|
||
### Install
|
||
|
||
```console
|
||
$ npm i esbuild-sass-plugin
|
||
```
|
||
|
||
### Usage
|
||
|
||
Just add it to your esbuild plugins:
|
||
|
||
```javascript
|
||
import {sassPlugin} from 'esbuild-sass-plugin'
|
||
|
||
await esbuild.build({
|
||
...
|
||
plugins: [sassPlugin()]
|
||
})
|
||
```
|
||
|
||
### Options
|
||
|
||
You can pass a series of **options** to the plugin that are a superset of Sass
|
||
[compile string options](https://sass-lang.com/documentation/js-api/interfaces/StringOptionsWithImporter). \
|
||
The following are the options specific to the plugin with their defaults whether provided:
|
||
|
||
| Option | Type | Default |
|
||
|---------------|---------------------------------------------------------|-----------------------------------------|
|
||
| filter | regular expression (in Go syntax) | <code>/\.(s[ac]ss|css)$/</code> |
|
||
| type | `"css"`<br/>`"style"`<br/>`"lit-css"`<br/>`"css-text"` | `"css"` |
|
||
| cache | boolean or Map | `true` (there is one Map per namespace) |
|
||
| transform | function | |
|
||
| [loadPaths](https://sass-lang.com/documentation/js-api/interfaces/Options#loadPaths) | string[] | [] |
|
||
| precompile | function | |
|
||
| importMapper | function | |
|
||
| cssImports | boolean | false |
|
||
| nonce | string | |
|
||
| prefer | string | preferred package.json field |
|
||
| quietDeps | boolean | false |
|
||
|
||
Two main options control the plugin: `filter` which has the same meaning of filter in [esbuild](https://esbuild.github.io/plugins/#on-load)
|
||
allowing to select the URLs handled by a plugin instance and then `type` that's what specifies how the css should be rendered and imported.
|
||
|
||
### `filter`
|
||
The default filter is quite simple but also quite permissive. When specifying a custom regex bear in mind that this
|
||
is in [Go syntax](https://pkg.go.dev/regexp/syntax)
|
||
|
||
> If you have URLs in your imports and you want the plugin to ignore them you can't just a filter expression like:
|
||
`/^(?!https?:).*\.(s[ac]ss|css)$/` because *Go regex engine doesn't support lookarounds* but you can use
|
||
> **esbuild**'s `external` option to ignore these imports or try a [solution like this one](https://esbuild.github.io/plugins/#on-resolve).
|
||
|
||
You can try to list multiple plugin instances in order so that the most specific RegEx come first:
|
||
```javascript
|
||
await esbuild.build({
|
||
...
|
||
plugins: [
|
||
sassPlugin({
|
||
filter: /\.module\.scss$/,
|
||
transform: postcssModules()
|
||
}),
|
||
sassPlugin({
|
||
filter: /\.scss$/
|
||
}),
|
||
],
|
||
...
|
||
})
|
||
```
|
||
|
||
### `type`
|
||
|
||
The example in [Usage](#usage) uses the default type `css` and will use esbuild 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 which instead will be handled by the plugin.
|
||
> if you want `url()` resolution or other processing you have to use `postcss` like in [this example](https://github.com/glromeo/esbuild-sass-plugin/issues/92#issuecomment-1219209442)
|
||
|
||
**NOTE:** Since version `2.7.0` the `css` type works also with postcss, CSS modules and more in general
|
||
with any transformation function by keeping an internal cache of CSS chunks (virtual CSS files)
|
||
importing them in the module wrapping the contents
|
||
|
||
#### `type: "style"`
|
||
In this mode the stylesheet will be in the javascript bundle
|
||
and will be dynamically added to the page when the bundle is loaded.
|
||
|
||
#### `type: "css-text"`
|
||
You can use this mode if you want to use the resulting css text as a string import
|
||
|
||
```javascript
|
||
await esbuild.build({
|
||
...
|
||
plugins: [sassPlugin({
|
||
type: "css-text",
|
||
... // for the options availanle look at 'SassPluginOptions' in index.ts
|
||
})]
|
||
})
|
||
```
|
||
|
||
...and in your module do something like
|
||
|
||
```javascript
|
||
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];
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `type: "lit-css"`
|
||
Or you can import a **lit-element** css result using `type: "lit-css"`
|
||
|
||
```javascript
|
||
import styles from './styles.scss'
|
||
|
||
@customElement("hello-world")
|
||
export default class HelloWorld extends LitElement {
|
||
|
||
static styles = styles
|
||
|
||
render() {
|
||
...
|
||
}
|
||
}
|
||
```
|
||
|
||
Look in `test/fixtures` folder for more usage examples.
|
||
|
||
### `cache`
|
||
The cache is enabled by default and can be turned off with `cache: false`.
|
||
Each plugin instance creates and maintain its own cache (as a Map) and this cache lives for the duration of the build.
|
||
If you want to pass a Map to preserve the cache amongst subsequent builds bear in mind that sharing the very same cache
|
||
between different instances might work just fine or it might lead to issues if the contents are incompatible.
|
||
> If you are not sure of what to do just keep a separate Map for each plugin instance.
|
||
|
||
### `cssImports`
|
||
when this is set to `true` the plugin rewrites the 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](https://webpack.js.org/loaders/sass-loader/#resolving-import-at-rules)
|
||
> some packages out there still use this notation (e.g. `formio`)
|
||
> \
|
||
> so I added this feature to help in cases [like this one](https://github.com/glromeo/esbuild-sass-plugin/issues/74).
|
||
|
||
### `nonce`
|
||
in presence of Content-Security-Policy
|
||
[(CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src)
|
||
the `nonce` option allows to specify the nonce attribute for the dynamically generated `<style>`
|
||
|
||
If the `nonce` string is a field access starting with `window`, `process` or `globalThis` it is left in the code without quotes.
|
||
```javascript
|
||
sassPlugin({
|
||
type: 'style',
|
||
nonce: 'window.__esbuild_nonce__'
|
||
})
|
||
```
|
||
This allows to define it globally or to leave it for a subsequent build to resolve it using [esbuild define](https://esbuild.github.io/api/#define).
|
||
```javascript
|
||
define: {'window.__esbuild_nonce__': '"12345"'}
|
||
```
|
||
|
||
### `prefer`
|
||
when this option is specified it allows to import npm packages which have `sass` or `style` fields preferring those to `main`.
|
||
|
||
> **NOTE**: This is an experimental feature
|
||
> * it replaces the internal use of `require.resolve` with browserify `resolve.sync`
|
||
> * it only applies to import prefixed by `~`
|
||
|
||
### `importMapper`
|
||
|
||
A function to customize/re-map the import path, both `import` statements in JavaScript/TypeScript code and `@import`
|
||
in Sass/SCSS are covered.
|
||
You can use this option to re-map import paths like tsconfig's `paths` option.
|
||
|
||
e.g. given this `tsconfig.json` which maps image files paths
|
||
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"baseUrl": ".",
|
||
"paths": {
|
||
"@img/*": [
|
||
"./assets/images/*"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
now you can resolve these paths with `importMapper`
|
||
|
||
```javascript
|
||
await esbuild.build({
|
||
...,
|
||
plugins: [sassPlugin({
|
||
importMapper: (path) => path.replace(/^@img\//, './assets/images/')
|
||
})]
|
||
})
|
||
```
|
||
|
||
### `precompile`
|
||
|
||
#### - Rewriting relative `url(...)`s
|
||
If your sass reference resources with relative urls (see [#48](https://github.com/glromeo/esbuild-sass-plugin/issues/48))
|
||
esbuild will struggle to rewrite those urls because it doesn't have idea of the imports that the Sass compiler
|
||
has gone through. Fortunately the new importer API allows to rewrite those relative URLs in absolute ones which
|
||
then esbuild will be able to handle.
|
||
|
||
Here is an example of how to do the `url(...)` rewrite ([make sure to handle `\` in *Windows*](https://github.com/glromeo/esbuild-sass-plugin/issues/58))
|
||
```javascript
|
||
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`)
|
||
}
|
||
})]
|
||
})
|
||
```
|
||
|
||
#### - Globals and other Shims (like sass-loader's additionalData)
|
||
Look for a complete example in the [precompile](https://github.com/glromeo/esbuild-sass-plugin/tree/main/test/fixture/precompile) fixture.
|
||
|
||
Prepending a variable for a specific `pathname`:
|
||
```javascript
|
||
const context = { color: "blue" }
|
||
|
||
await esbuild.build({
|
||
...,
|
||
plugins: [sassPlugin({
|
||
precompile(source, pathname) {
|
||
const prefix = /\/included\.scss$/.test(pathname) ? `
|
||
$color: ${context.color};
|
||
` : env
|
||
return prefix + source
|
||
}
|
||
})]
|
||
})
|
||
```
|
||
|
||
Prepending an `@import` of globals file only for the root file that triggered the compilation (to avoid nested files from importing it again):
|
||
```javascript
|
||
const context = { color: "blue" }
|
||
|
||
await esbuild.build({
|
||
...,
|
||
plugins: [sassPlugin({
|
||
precompile(source, pathname, isRoot) {
|
||
return isRoot ? `@import '/path/to/globals.scss';\n${source}` : source
|
||
}
|
||
})]
|
||
})
|
||
```
|
||
|
||
### `transform`
|
||
|
||
```typescript
|
||
async (this: SassPluginOptions, css: string, resolveDir?: string) => Promise<string>
|
||
```
|
||
|
||
It's a function which will be 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 **modules** like in the following examples.
|
||
|
||
> **NOTE:** Since `v1.5.0` transform can return either a string or an esbuild `LoadResult` object. \
|
||
> This is what `postcssModules` uses to pass Javascript modules to esbuild bypassing the plugin output altogether.
|
||
|
||
#### - PostCSS
|
||
|
||
The simplest use case is to invoke PostCSS like this:
|
||
|
||
```javascript
|
||
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
|
||
}
|
||
})]
|
||
})
|
||
|
||
```
|
||
|
||
#### - CSS Modules
|
||
|
||
A helper function is available to do all the work of calling PostCSS to create a CSS module. The usage is something
|
||
like:
|
||
|
||
```javascript
|
||
const {sassPlugin, postcssModules} = require('esbuild-sass-plugin')
|
||
|
||
esbuild.build({
|
||
...,
|
||
plugins: [sassPlugin({
|
||
transform: postcssModules({
|
||
// ...put here the options for postcss-modules: https://github.com/madyankin/postcss-modules
|
||
})
|
||
})]
|
||
})
|
||
|
||
```
|
||
`postcssModules` produces Javascript modules which are handled by esbuild's `js` loader
|
||
|
||
`postcssModules` also accepts an optional array of plugins for PostCSS as second parameter.
|
||
|
||
Look into [fixture/css-modules](https://github.com/glromeo/esbuild-sass-plugin/tree/main/test/fixture/css-modules) for
|
||
the complete example.
|
||
|
||
> **NOTE:** `postcss` and `postcss-modules` have to be added to your `package.json`.
|
||
|
||
### quietDeps
|
||
|
||
In order for `quietDeps` to correctly identify external dependencies the `url` option is defaulted to the importing file path URL.
|
||
|
||
> The `url` option creates problems when importing source SASS files from 3rd party modules in which case the best workaround is to avoid `quietDeps` and [mute the logger](https://sass-lang.com/documentation/js-api/interfaces/StringOptionsWithImporter#logger) if that's a big issue.
|
||
|
||
### pnpm
|
||
|
||
There's a working example of using `pnpm` with `@material` design
|
||
in [issue/38](https://github.com/glromeo/esbuild-sass-plugin/tree/main/test/issues/38)
|
||
|
||
### Benchmarks
|
||
**Windows 10** Pro - **i7-4770K** CPU @ **3.50**GHz - RAM **24**GB - SSD **500**GB
|
||
|
||
Given 24 × 24 = 576 lit-element files & 576 imported CSS styles plus the import of the full bootstrap 5.1
|
||
|
||
| | dart sass | dart sass (no cache) | node-sass* | node-sass (no cache) |
|
||
|------------------------|-----------|-----------------------|-------------|----------------------|
|
||
| **initial build** | 2.750s | 2.750s | 1.903s | 1.858s |
|
||
| rebuild (.ts change) | 285.959ms | 1.950s | 797.098ms | 1.689s |
|
||
| rebuild (.ts change) | 260.791ms | 1.799s | 768.213ms | 1.790s |
|
||
| rebuild (.scss change) | 234.152ms | 1.801s | 770.619ms | 1.652s |
|
||
| rebuild (.scss change) | 267.857ms | 1.738s | 750.743ms | 1.682s |
|
||
|
||
(*) node-sass is here just to give a term of comparison ...those samples were taken from 1.8.x
|
||
|
||
[travis-url]: https://app.travis-ci.com/glromeo/esbuild-sass-plugin
|
||
[travis-image]: https://app.travis-ci.com/glromeo/esbuild-sass-plugin.svg?branch=main
|