Skip to content

Commit

Permalink
3.1.0 makes sass-embedded optional
Browse files Browse the repository at this point in the history
  • Loading branch information
glromeo authored and glromeo committed Feb 22, 2024
1 parent 6af7f8f commit a161615
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 316 deletions.
46 changes: 30 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ 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
* **[Sass Embedded](https://github.com/sass/sass/issues/3296) Async API**. (thanks to @NathanBeddoeWebDev)
* Support for **[Sass Embedded](https://github.com/sass/sass/issues/3296) Async API**. (thanks to @NathanBeddoeWebDev)
* caching
* **url rewriting**
* pre-compiling (to add **global resources** to the sass files)

### Breaking Changes (...maybe)
* upgraded to esbuild 0.20
* It turned out that sass-embedded is not available on every platform (this sucks!) so, in order to improve the compatibility of the
plugin I had to make it a peer dependency. Once installed, it can be used by setting the new option `embedded` to `true`

### Install

Expand All @@ -41,20 +42,20 @@ 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&vert;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 |

| Option | Type | Default |
|--------------|--------------------------------------------------------------------------------------------------------|------------------------------------------|
| filter | regular expression (in Go syntax) | <code>/\.(s[ac]ss&vert;css)$/</code> |
| type | `"css"`<br/>`"style"`<br/>`"lit-css"`<br/>`"css-text"` <br/> `(css: string, nonce?: string) => string` | `"css"` |
| cache | boolean or Map | `true` (there is one Map per namespace) |
| transform | function | |
| loadPaths | [string[]](https://sass-lang.com/documentation/js-api/interfaces/Options#loadPaths) | [] |
| precompile | function | |
| importMapper | function | |
| cssImports | boolean | false |
| nonce | string | |
| prefer | string | preferred package.json field |
| quietDeps | boolean | false |
| embedded | 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.

Expand Down Expand Up @@ -83,6 +84,14 @@ await esbuild.build({
})
```

### `embedded`

This option enables the usage of the faster `sass-embedded` and is **false** by default just **for compatibility reason**.

> Make sure that the `sass-embedded` has been installed as a peer dependency
> or add it manually to your project if your package manager doesn't do that for you
> then set this option to `true` and enjoy the speed boost!
### `type`

The example in [Usage](#usage) uses the default type `css` and will use esbuild CSS loader so your transpiled Sass
Expand Down Expand Up @@ -145,6 +154,11 @@ export default class HelloWorld extends LitElement {
}
```
#### `type: 'function'`
You can now provide your own module factory as type. It has to be a function that receives 2 parameters
the css text and the nonce token and returns the source content to be added in place of the import.
Look in `test/fixtures` folder for more usage examples.
### `cache`
Expand Down
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "esbuild-sass-plugin",
"version": "3.0.0",
"version": "3.1.0",
"description": "esbuild plugin for sass/scss files supporting both css loader and css result import (lit-element)",
"main": "lib/index.js",
"keywords": [
Expand Down Expand Up @@ -41,22 +41,24 @@
]
},
"dependencies": {
"resolve": "^1.22.6",
"sass-embedded": "^1.70.0"
"resolve": "^1.22.8",
"sass": "^1.71.1"
},
"devDependencies": {
"@types/node": "^20.11.10",
"@types/node": "^20.11.20",
"@types/resolve": "^1.20.6",
"esbuild": "^0.20.0",
"esbuild": "^0.20.1",
"mocha-toolkit": "^1.0.7",
"postcss": "^8.4.31",
"postcss": "^8.4.35",
"postcss-modules": "^6.0.0",
"postcss-url": "^10.1.3",
"source-map": "^0.7.4",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"sass-embedded": "^1.71.1"
},
"peerDependencies": {
"esbuild": "^0.20.0"
"esbuild": "^0.20.1",
"sass-embedded": "^1.71.1"
}
}
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {OnLoadResult} from 'esbuild'
import {StringOptions} from 'sass-embedded'
import {StringOptions} from 'sass'
import {sassPlugin} from './plugin'

export type Type = 'css' | 'style' | 'css-text' | 'lit-css'
export type Type = 'css' | 'style' | 'css-text' | 'lit-css' | ((cssText: string, nonce?: string) => string)

export type SassPluginOptions = StringOptions<'async'> & {
export type SassPluginOptions = StringOptions<'sync'|'async'> & {

/**
* Careful: this RegExp has to respect Go limitations!!!
Expand Down Expand Up @@ -76,6 +76,11 @@ export type SassPluginOptions = StringOptions<'async'> & {
*
*/
prefer?: 'sass' | 'style' | 'main'

/**
* To enable the sass-embedded compiler
*/
embedded?: boolean
}

export default sassPlugin
Expand Down
11 changes: 3 additions & 8 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {SassPluginOptions} from './index'
import {getContext, makeModule, modulesPaths, parseNonce, posixRelative, DEFAULT_FILTER} from './utils'
import {useCache} from './cache'
import {createRenderer} from './render'
import {initAsyncCompiler} from 'sass-embedded'

/**
*
Expand All @@ -22,7 +21,7 @@ export function sassPlugin(options: SassPluginOptions = {}): Plugin {

const type = options.type ?? 'css'

if (options['picomatch'] || options['exclude'] || typeof type !== 'string') {
if (options['picomatch'] || options['exclude'] || typeof type !== 'string' && typeof type !== 'function') {
console.log('The type array, exclude and picomatch options are no longer supported, please refer to the README for alternatives.')
}

Expand Down Expand Up @@ -71,15 +70,11 @@ export function sassPlugin(options: SassPluginOptions = {}): Plugin {
}))
}

const compiler = await initAsyncCompiler();

onDispose(()=> compiler.dispose())

const renderAsync = createRenderer(compiler, options, options.sourceMap ?? sourcemap)
const renderSass = await createRenderer(options, options.sourceMap ?? sourcemap, onDispose)

onLoad({filter: options.filter ?? DEFAULT_FILTER}, useCache(options, fsStatCache, async path => {
try {
let {cssText, watchFiles, warnings} = await renderAsync(path)
let {cssText, watchFiles, warnings} = await renderSass(path)
if (!warnings) {
warnings = []
}
Expand Down
32 changes: 26 additions & 6 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,39 @@ import {dirname, parse, relative, resolve, sep} from 'path'
import fs, {readFileSync} from 'fs'
import {createResolver, fileSyntax, sourceMappingURL, DEFAULT_FILTER} from './utils'
import {PartialMessage} from 'esbuild'
import * as sass from 'sass-embedded'
import {ImporterResult, initAsyncCompiler} from 'sass-embedded'
import {fileURLToPath, pathToFileURL} from 'url'
import {SassPluginOptions} from './index'
import {AsyncCompiler} from 'sass-embedded/dist/types/compile'
import {ImporterResult} from 'sass'
import {StringOptions} from 'sass/types/options'
import {CompileResult} from 'sass/types/compile'

export type RenderAsync = (path: string) => Promise<RenderResult>
export type RenderSass = (path: string) => Promise<RenderResult>

export type RenderResult = {
cssText: string
watchFiles: string[]
warnings?: PartialMessage[]
}

export function createRenderer(compiler: AsyncCompiler, options: SassPluginOptions = {}, sourcemap: boolean): RenderAsync {
export type Compiler = (source: string, options?: StringOptions<any>) => CompileResult | Promise<CompileResult>

async function createCompiler(embedded:boolean|undefined, onDispose: (callback: () => void) => void): Promise<Compiler> {
if (embedded) {
const {initAsyncCompiler} = await import('sass-embedded')
const compiler = await initAsyncCompiler();
onDispose(compiler.dispose.bind(compiler))
return compiler.compileStringAsync.bind(compiler) as Compiler
} else {
const {compileString} = await import('sass')
return compileString
}
}

export async function createRenderer(
options: SassPluginOptions = {},
sourcemap: boolean,
onDispose: (callback: () => void) => void
): Promise<RenderSass> {

const loadPaths = options.loadPaths!
const resolveModule = createResolver(options, loadPaths)
Expand Down Expand Up @@ -62,6 +80,8 @@ export function createRenderer(compiler: AsyncCompiler, options: SassPluginOptio

const sepTilde = `${sep}~`

const compileString = await createCompiler(options.embedded, onDispose)

/**
* renderAsync
*/
Expand Down Expand Up @@ -113,7 +133,7 @@ export function createRenderer(compiler: AsyncCompiler, options: SassPluginOptio
css,
loadedUrls,
sourceMap
} = await compiler.compileStringAsync(source, {
} = await compileString(source, {
sourceMapIncludeSources: true,
...options,
logger,
Expand Down
8 changes: 5 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {SassPluginOptions, Type} from './index'
import {AcceptedPlugin, Postcss} from 'postcss'
import PostcssModulesPlugin from 'postcss-modules'
import {BuildOptions, OnLoadResult} from 'esbuild'
import {Syntax} from 'sass-embedded'
import {Syntax} from 'sass'
import {parse, relative, resolve} from 'path'
import {existsSync} from 'fs'
import {SyncOpts} from 'resolve'
Expand Down Expand Up @@ -116,16 +116,18 @@ document.head
export {css};
`

export function makeModule(contents: string, type: Type, nonce?: string) {
export function makeModule(contents: string, type: Type, nonce?: string):string {
switch (type) {
case 'style':
return styleModule(contents, nonce)
case 'lit-css':
return cssResultModule(contents)
case 'css-text':
return cssTextModule(contents)
default:
case 'css':
return contents
default:
return type(contents, nonce)
}
}

Expand Down
25 changes: 13 additions & 12 deletions test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {sassPlugin} from '../src'
import {readCssFile, readTextFile, useFixture, writeTextFile} from './test-toolkit'
import fs from 'fs'
import {expect} from 'chai'
import {BuildResult} from 'esbuild'

describe('unit tests', function () {

Expand Down Expand Up @@ -155,7 +156,7 @@ describe('unit tests', function () {

it('captures warnings in entrypoint', async function () {
const options = useFixture('warnings')
let warnings = []
let warnings = [] as BuildResult["warnings"]

await esbuild.build({
...options,
Expand All @@ -164,7 +165,7 @@ describe('unit tests', function () {
outdir: './out',
bundle: true,
plugins: [
sassPlugin({syntax: 'nested'}),
sassPlugin({syntax: 'indented'}),
{
name: 'capture-build-end-warnings',
setup: function (build) {
Expand All @@ -179,14 +180,14 @@ describe('unit tests', function () {
expect(warnings.length).to.equal(1)

expect(warnings[0].text).to.include('This selector doesn\'t have any properties')
expect(warnings[0].location.file).to.equal('index.sass')
expect(warnings[0].location.line).to.equal(3)
expect(warnings[0].location.lineText).to.equal('p')
expect(warnings[0].location!.file).to.equal('index.sass')
expect(warnings[0].location!.line).to.equal(3)
expect(warnings[0].location!.lineText).to.equal('p')
})

it('captures warnings in imports', async function () {
const options = useFixture('warnings')
let warnings = []
let warnings = [] as BuildResult["warnings"]

await esbuild.build({
...options,
Expand All @@ -209,14 +210,14 @@ describe('unit tests', function () {

expect(warnings.length).to.equal(2)

const indexWarning = warnings.find(w => w.location.file.endsWith('index.sass'))
const indexWarning = warnings.find(w => w.location!.file.endsWith('index.sass'))!
expect(indexWarning.text).to.include('This selector doesn\'t have any properties')
expect(indexWarning.location.line).to.equal(3)
expect(indexWarning.location.lineText).to.equal('p')
expect(indexWarning.location!.line).to.equal(3)
expect(indexWarning.location!.lineText).to.equal('p')

const partialWarning = warnings.find(w => w.location.file.endsWith('_partial.sass'))
const partialWarning = warnings.find(w => w.location!.file.endsWith('_partial.sass'))!
expect(partialWarning.text).to.include('This selector doesn\'t have any properties')
expect(partialWarning.location.line).to.equal(0)
expect(partialWarning.location.lineText).to.equal('div')
expect(partialWarning.location!.line).to.equal(0)
expect(partialWarning.location!.lineText).to.equal('div')
})
})
Loading

0 comments on commit a161615

Please sign in to comment.