├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── legacy.js ├── package-lock.json ├── package.json └── test ├── expected-sass ├── empty.css ├── indent.css ├── inheritance.css ├── mixins.css └── variables.css ├── expected ├── empty.css ├── indent.css ├── inheritance.css ├── mixins.css └── variables.css ├── main.js └── scss ├── _partial.scss ├── empty.scss ├── error.scss ├── globbed ├── app.scss └── foo │ └── bar.scss ├── includes ├── _cats.scss └── _dogs.sass ├── indent.sass ├── inheritance.scss ├── mixins.scss └── variables.scss /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | env: 4 | mocha: true 5 | node: true 6 | 7 | extends: "airbnb-base" 8 | 9 | parserOptions: 10 | sourceType: "script" 11 | 12 | rules: 13 | max-len: 14 | - 2 15 | - 120 16 | no-multi-assign: "off" 17 | no-param-reassign: "off" 18 | strict: 19 | - "error" 20 | - "safe" 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | pull_request: 8 | 9 | env: 10 | FORCE_COLOR: 2 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: 16 25 | cache: npm 26 | 27 | - name: Install npm dependencies 28 | run: npm ci 29 | 30 | - name: Lint 31 | run: npm run lint 32 | 33 | test: 34 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 35 | runs-on: ${{ matrix.os }} 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | os: 41 | - ubuntu-latest 42 | - windows-latest 43 | node: 44 | - 14 45 | - 16 46 | 47 | steps: 48 | - name: Clone repository 49 | uses: actions/checkout@v2 50 | 51 | - name: Set up Node.js 52 | uses: actions/setup-node@v2 53 | with: 54 | node-version: ${{ matrix.node }} 55 | cache: npm 56 | 57 | - name: Install npm dependencies 58 | run: npm ci 59 | 60 | - name: Test 61 | run: npm test 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # gulp-sass Changelog 2 | 3 | ## v6.0.1 4 | 5 | **March 5, 2025** 6 | 7 | 8 | 9 | ## v6.0.0 10 | 11 | **November 27, 2024** 12 | 13 | 14 | 15 | ## v5.0.0 16 | 17 | **June 25, 2021** 18 | 19 | 20 | 21 | ## v4.1.1 22 | 23 | **June 24, 2021** 24 | 25 | 26 | 27 | ## v4.1.0 28 | 29 | **April 23, 2020** 30 | 31 | 32 | 33 | ## v4.0.2 34 | 35 | **October 16, 2018** 36 | 37 | 38 | 39 | ## v4.0.1 40 | 41 | **Apr 8, 2018** 42 | 43 | 44 | 45 | ## v4.0.0 46 | 47 | **April 5, 2018** 48 | 49 | 50 | 51 | ## v3.2.1 52 | 53 | **March 24, 2018** 54 | 55 | 56 | 57 | ## v3.2.0 58 | 59 | **March 12, 2018** 60 | 61 | 62 | 63 | ## v3.1.0 64 | 65 | **January 9, 2017** 66 | 67 | 68 | 69 | ## v3.0.0 70 | 71 | **January 9, 2017** 72 | 73 | 74 | 75 | ## v2.3.2 76 | 77 | **June 15, 2016** 78 | 79 | 80 | 81 | ## v2.3.1 82 | 83 | **April 22, 2016** 84 | 85 | 86 | 87 | ## v2.3.0 88 | 89 | **April 21, 2016** 90 | 91 | 92 | 93 | ## v2.3.0-beta.1 94 | 95 | **February 4, 2016** 96 | 97 | 98 | 99 | ## v2.2.0 100 | 101 | **February 4, 2016** 102 | 103 | 104 | 105 | ## v2.1.0 106 | 107 | **November 2, 2015** 108 | 109 | 110 | 111 | ## v2.1.0-beta 112 | 113 | **September 21, 2015** 114 | 115 | * **Change** Updated to `node-sass` 3.4.0-beta1 116 | 117 | ## v2.0.4 118 | 119 | **July 15, 2015** 120 | 121 | * **Fix** Relative file path now uses `file.relative` instead of arcane `split('/').pop` magic. Resolves lots of issues with source map paths. 122 | * **Fix** Empty partials no longer copied to CSS folder 123 | 124 | ## v2.0.3 125 | 126 | **June 27, 2015** 127 | 128 | * **Fix** Empty partials are no longer copied to CSS folder 129 | 130 | ## v2.0.2 131 | 132 | **June 25, 2015** 133 | 134 | * **Fix** Error in watch stream preventing watch from continuing 135 | 136 | ## v2.0.1 137 | 138 | **May 13, 2015** 139 | 140 | * **Fix** Source maps now work as expected with Autoprefixer 141 | * **Fix** Current file directory `unshift` onto includePaths stack so it's checked first 142 | * **Fix** Error message returned is unformatted so as to not break other error handling (*i.e.* `gulp-notify`) 143 | 144 | ## v2.0.0 145 | 146 | **May 6, 2015** 147 | 148 | * **Change** Updated to `node-sass` 3.0.0 149 | 150 | ## v2.0.0-alpha.1 151 | 152 | **March 26, 2015** 153 | 154 | * **New** Added `renderSync` option that can be used through `sass.sync()` 155 | 156 | ### March 24, 2015 157 | 158 | * **Change** Updated to `node-sass` 3.0.0-alpha.1 159 | * **New** Added support for `gulp-sourcemaps` including tests 160 | * **New** Added `.editorconfig` for development consistency 161 | * **New** Added linting and test for said linting 162 | * **Change** Updated the README 163 | * **New** `logError` function to make streaming errors possible instead of breaking the stream 164 | 165 | ### 1.3.3 166 | 167 | * updated to `node-sass` 2.0 (final) 168 | * should now work with Node.js 0.12 and io.js 169 | 170 | ### 1.3.2 171 | 172 | * fixed `errLogToConsole` 173 | 174 | ### 1.3.1 175 | 176 | * bug fix 177 | 178 | ## Version 1.3.0 179 | 180 | * Supports `node-sass` 2.0 (thanks laurelnaiad!) 181 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gulp Sass 2 | 3 | `gulp-sass` is a very light-weight wrapper around either [Dart Sass][] or [Node Sass][] (which in turn is a Node binding for [LibSass][]. All of these are implementations of the [Sass][] language. 4 | 5 | [Dart Sass]: http://sass-lang.com/dart-sass 6 | [Node Sass]: https://github.com/sass/node-sass 7 | [LibSass]: https://sass-lang.com/libsass 8 | [Sass]: https://sass-lang.com 9 | 10 | ## Submitting Issues 11 | 12 | * Before creating a new issue, perform a [cursory search](https://github.com/issues?utf8=%E2%9C%93&q=repo%3Adlmanning%2Fgulp-sass+repo%3Asass%2Fdart-sass+repo%3Asass%2Fnode-sass+repo%3Asass%2Flibsass+repo%3Asass%2Fsass+repo%3Asass-eyeglass%2Feyeglass) in the Gulp Sass, Dart Sass, Node Sass, Libsass, and main Sass repos to see if a similar issue has already been submitted. Please also refer to our [Common Issues and Their Fixes](https://github.com/dlmanning/gulp-sass/wiki/Common-Issues-and-Their-Fixes) page for some basic troubleshooting. 13 | * You can create an issue [here](https://github.com/dlmanning/gulp-sass/issues). Please include as many details as possible in your report. 14 | * Issue titles should be descriptive, explaining at the high level what it is about. 15 | * Please include the version of `gulp-sass`, Node, and NPM you are using, as well as what operating system you are having a problem on. 16 | * _Do not open a [pull request](#pull-requests) to resolve an issue without first receiving feedback from a `collaborator` or `owner` and having them agree on a solution forward_. 17 | * Include screenshots and animated GIFs whenever possible; they are immensely helpful. 18 | * Issues that have a number of sub-items that need to be complete should use [task lists](https://github.com/blog/1375%0A-task-lists-in-gfm-issues-pulls-comments) to track the sub-items in the main issue comment. 19 | 20 | 21 | ## Pull Requests 22 | 23 | * **DO NOT ISSUE A PULL REQUEST WITHOUT FIRST [SUBMITTING AN ISSUE](#submitting-issues)** 24 | * Pull requests should reference their related issues. If the pull request closes an issue, [please reference its closing in your commit messages](https://help.github.com/articles/closing-issues-via-commit-messages/). Pull requests not referencing any issues will be closed. 25 | * Pull request titles should be descriptive, explaining at the high level what it is doing, and should be written in the same style as [Git commit messages](#git-commit-messages). 26 | * Update the `CHANGELOG` with the changes made by your pull request, making sure to use the proper [Emoji](#emoji-cheatsheet). 27 | * Follow our JavaScript styleguides. Tests will fail if you do not. 28 | * Ensure that you have [EditorConfig](http://editorconfig.org/) installed in your editor of choice and that it is functioning properly. 29 | * Do not squash or rebase your commits when submitting a Pull Request. It makes it much harder to follow your work and make incremental changes. 30 | * Update the [CHANGELOG](#maintaining-thechangelog) with your changes. 31 | * Branches should be made off of the most current `master` branch from `git@github.com:dlmanning/gulp-sass.git` 32 | * Pull requests should be made into our [master](https://github.com/dlmanning/gulp-sass/tree/master) branch. 33 | 34 | ### Git Commit Messages 35 | 36 | * Use the present tense (`"Add feature"` not `"Added Feature"`) 37 | * Use the imperative mood (`"Move cursor to…"` not `"Moves cursor to…"`) 38 | * Limit the first line to 72 characters or less 39 | * Consider including relevant Emoji from our [Emoji cheatsheet](#emoji-cheatsheet) 40 | 41 | ## Creating a New Version 42 | 43 | Versioning is done through [SEMVER](http://semver.org/). When creating a new version, create new release branch off of `master` with the version's name, and create a new tag with `v` prefixed with the version's name from that branch. 44 | 45 | For instance, if you are creating version `1.1.0`, you would create a branch `release/1.1.0` from `master` and create a tag `v1.1.0` from branch `release/1.1.0`. 46 | 47 | ### Maintaining the Changelog 48 | 49 | The Changelog should have a list of changes made for each version. They should be organized so additions come first, changes come second, and deletions come third. Version numbers should be 2nd level headers with the `v` in front (like a tag) and the date of the version's most recent update should be underneath in italics. 50 | 51 | Changelog messages do not need to cover each individual commit made, but rather should have individual summaries of the changes made. Changelog messages should be written in the same style as [Git commit messages](#git-commit-messages). 52 | 53 | ## Emoji Cheatsheet 54 | 55 | When creating creating commits or updating the CHANGELOG, please **start** the commit message or update with one of the following applicable Emoji. Emoji should not be used at the start of issue or pull request titles. 56 | 57 | * :art: `:art:` when improving the format/structure of the code 58 | * :racehorse: `:racehorse:` when improving performance 59 | * :memo: `:memo:` when writing long-form text (documentation, guidelines, principles, etc…) 60 | * :bug: `:bug:` when fixing a bug 61 | * :fire: `:fire:` when removing code or files 62 | * :green_heart: `:green_heart:` when fixing the CI build 63 | * :white_check_mark: `:white_check_mark:` when adding tests 64 | * :lock: `:lock:` when dealing with security 65 | * :arrow_up: `:arrow_up:` when upgrading dependencies 66 | * :arrow_down: `:arrow_down:` when downgrading dependencies 67 | * :shirt: `:shirt:` when removing linter warnings 68 | * :shipit: `:shipit:` when creating a new release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 David Manning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-sass ![npm package version](https://img.shields.io/npm/v/gulp-sass?label=npm%20version) [![Build Status](https://img.shields.io/github/workflow/status/dlmanning/gulp-sass/CI/master)](https://github.com/dlmanning/gulp-sass/actions?query=workflow%3ACI+branch%3Amaster) [![Join the chat at https://gitter.im/dlmanning/gulp-sass](https://img.shields.io/gitter/room/dlmanning/gulp-sass?color=%2346b091&label=chat&logo=gitter)](https://gitter.im/dlmanning/gulp-sass) ![Node.js support](https://img.shields.io/node/v/gulp-sass) 2 | 3 | Sass plugin for [Gulp](https://github.com/gulpjs/gulp). 4 | 5 | **_Before filing an issue, please make sure you have [updated to the latest version of `gulp-sass`](https://github.com/dlmanning/gulp-sass/wiki/Update-to-the-latest-Gulp-Sass) and have gone through our [Common Issues and Their Fixes](https://github.com/dlmanning/gulp-sass/wiki/Common-Issues-and-Their-Fixes) section._** 6 | 7 | **Migrating your existing project to version 5 or 6? Please read our (short!) [migration guides](#migrating-to-version-6).** 8 | 9 | ## Support 10 | 11 | Only [Active LTS and Current releases](https://github.com/nodejs/Release#release-schedule) are supported. 12 | 13 | ## Installation 14 | 15 | To use `gulp-sass`, you must install both `gulp-sass` itself *and* a Sass compiler. `gulp-sass` supports both [Embedded Sass][], [Dart Sass][] and [Node Sass][], although Node Sass is [deprecated](https://sass-lang.com/blog/libsass-is-deprecated). We recommend that you use Dart Sass for new projects, and migrate Node Sass projects to Dart Sass or Embedded Sass when possible. 16 | 17 | Whichever compiler you choose, it's best to install these as dev dependencies: 18 | 19 | ```sh 20 | npm install sass gulp-sass --save-dev 21 | ``` 22 | 23 | ### Importing it into your project 24 | 25 | `gulp-sass` must be imported into your gulpfile, where you provide it the compiler of your choice. To use `gulp-sass` in a CommonJS module (which is most Node.js environments), do something like this: 26 | 27 | ```js 28 | const sass = require('gulp-sass')(require('sass')); 29 | ``` 30 | 31 | To use `gulp-sass` in an ECMAScript module (which is supported in newer Node.js 14 and later), do something like this: 32 | 33 | ```js 34 | import dartSass from 'sass'; 35 | import gulpSass from 'gulp-sass'; 36 | const sass = gulpSass(dartSass); 37 | ``` 38 | 39 | ## Usage 40 | 41 | **Note:** These examples are written for CommonJS modules and assume you're using Gulp 4. For examples that work with Gulp 3, [check the docs for an earlier version of `gulp-sass`](https://github.com/dlmanning/gulp-sass/tree/v4.1.1). 42 | 43 | `gulp-sass` must be used in a Gulp task. Your task can call `sass()` (to asynchronously render your CSS), or `sass.sync()` (to synchronously render your CSS). Then, export your task with the `export` keyword. We'll show some examples of how to do that. 44 | 45 | **⚠️ Note:** When using Dart Sass, **synchronous rendering is twice as fast as asynchronous rendering**. The Sass team is exploring ways to improve asynchronous rendering with Dart Sass, but for now, you will get the best performance from `sass.sync()`. If performance is critical, you can use `sass-embedded` instead. 46 | 47 | ### Render your CSS 48 | 49 | To render your CSS with a build task, then watch your files for changes, you might write something like this: 50 | 51 | ```js 52 | 'use strict'; 53 | 54 | const gulp = require('gulp'); 55 | const sass = require('gulp-sass')(require('sass')); 56 | 57 | function buildStyles() { 58 | return gulp.src('./sass/**/*.scss') 59 | .pipe(sass().on('error', sass.logError)) 60 | .pipe(gulp.dest('./css')); 61 | }; 62 | 63 | exports.buildStyles = buildStyles; 64 | exports.watch = function () { 65 | gulp.watch('./sass/**/*.scss', buildStyles); 66 | }; 67 | ``` 68 | 69 | With synchronous rendering, that Gulp task looks like this: 70 | 71 | ```js 72 | function buildStyles() { 73 | return gulp.src('./sass/**/*.scss') 74 | .pipe(sass.sync().on('error', sass.logError)) 75 | .pipe(gulp.dest('./css')); 76 | }; 77 | ``` 78 | 79 | ### Render with options 80 | 81 | To change the final output of your CSS, you can pass an options object to your renderer. `gulp-sass` supports [Sass's JS API compile options](https://sass-lang.com/documentation/js-api/modules#compileString), with a few usage notes: 82 | 83 | - The `syntax` option is set to `indented` automatically for files with the `.sass` extension 84 | - The `sourceMap` and `sourceMapIncludeSources` options are set for you when using `gulp-sourcemaps` 85 | 86 | For example, to compress your CSS, you can call `sass({style: 'compressed'}`. In the context of a Gulp task, that looks like this: 87 | 88 | ```js 89 | function buildStyles() { 90 | return gulp.src('./sass/**/*.scss') 91 | .pipe(sass({style: 'compressed'}).on('error', sass.logError)) 92 | .pipe(gulp.dest('./css')); 93 | }; 94 | 95 | exports.buildStyles = buildStyles; 96 | ``` 97 | 98 | Or this for synchronous rendering: 99 | 100 | ```js 101 | function buildStyles() { 102 | return gulp.src('./sass/**/*.scss') 103 | .pipe(sass.sync({style: 'compressed'}).on('error', sass.logError)) 104 | .pipe(gulp.dest('./css')); 105 | }; 106 | 107 | exports.buildStyles = buildStyles; 108 | ``` 109 | 110 | ### Include a source map 111 | 112 | `gulp-sass` can be used in tandem with [`gulp-sourcemaps`](https://github.com/gulp-sourcemaps/gulp-sourcemaps) to generate source maps for the Sass-to-CSS compilation. You will need to initialize `gulp-sourcemaps` _before_ running `gulp-sass`, and write the source maps after. 113 | 114 | ```js 115 | const sourcemaps = require('gulp-sourcemaps'); 116 | 117 | function buildStyles() { 118 | return gulp.src('./sass/**/*.scss') 119 | .pipe(sourcemaps.init()) 120 | .pipe(sass().on('error', sass.logError)) 121 | .pipe(sourcemaps.write()) 122 | .pipe(gulp.dest('./css')); 123 | } 124 | 125 | exports.buildStyles = buildStyles; 126 | ``` 127 | 128 | By default, `gulp-sourcemaps` writes the source maps inline, in the compiled CSS files. To write them to a separate file, specify a path relative to the `gulp.dest()` destination in the `sourcemaps.write()` function. 129 | 130 | ```js 131 | const sourcemaps = require('gulp-sourcemaps'); 132 | 133 | function buildStyles() { 134 | return gulp.src('./sass/**/*.scss') 135 | .pipe(sourcemaps.init()) 136 | .pipe(sass().on('error', sass.logError)) 137 | .pipe(sourcemaps.write('./maps')) 138 | .pipe(gulp.dest('./css')); 139 | }; 140 | 141 | exports.buildStyles = buildStyles; 142 | ``` 143 | 144 |

Migrating to version 6

145 | 146 | `gulp-sass` version 6 uses the new [compile](https://sass-lang.com/documentation/js-api/modules#compileString) function internally by default. If you use any options, for instance custom importers, please compare the [new options](https://sass-lang.com/documentation/js-api/modules#compileString) with the [legacy options](https://sass-lang.com/documentation/js-api/modules#render) in order to migrate. For instance, the `outputStyle` option is now called `style`. 147 | 148 | ```diff 149 | function buildStyles() { 150 | return gulp.src('./sass/**/*.scss') 151 | - .pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError)) 152 | + .pipe(sass({style: 'compressed'}).on('error', sass.logError)) 153 | .pipe(gulp.dest('./css')); 154 | }; 155 | ``` 156 | 157 | If you want to keep using the legacy API while it's available, you can. 158 | 159 | ```js 160 | const sass = require('gulp-sass/legacy')(require('sass')); 161 | ``` 162 | 163 | If you use source maps, you may see the result change somewhat. The result will typically be absolute `file:` URLs, rather than relative ones. The result may also be the source itself, URL encoded. You can [optionally add custom importers](https://sass-lang.com/documentation/js-api/interfaces/CompileResult#sourceMap) to adjust the source maps according to your own needs. 164 | 165 |

Migrating to version 5

166 | 167 | `gulp-sass` version 5 requires Node.js 12 or later, and introduces some breaking changes. Additionally, changes in Node.js itself mean that Node fibers can no longer be used to speed up Dart Sass in Node.js 16. 168 | 169 | ### Setting a Sass compiler 170 | 171 | As of version 5, `gulp-sass` _does not include a default Sass compiler_, so you must install one (either `sass`, `sass-embedded`, or `node-sass`) along with `gulp-sass`. 172 | 173 | ```sh 174 | npm install sass gulp-sass --save-dev 175 | ``` 176 | 177 | Then, you must explicitly set that compiler in your gulpfille. Instead of setting a `compiler` prop on the `gulp-sass` instance, you pass the compiler into a function call when instantiating `gulp-sass`. 178 | 179 | These changes look something like this: 180 | 181 | ```diff 182 | - const sass = require('gulp-sass')); 183 | - const compiler = require('sass'); 184 | - sass.compiler = compiler; 185 | + const sass = require('gulp-sass')(require('sass')); 186 | ``` 187 | 188 | If you're migrating an ECMAScript module, that'll look something like this: 189 | 190 | ```diff 191 | import dartSass from 'sass'; 192 | - import sass from 'gulp-sass'; 193 | - sass.compiler = dartSass; 194 | 195 | import dartSass from 'sass'; 196 | + import gulpSass from 'gulp-sass'; 197 | + const sass = gulpSass(dartSass); 198 | ``` 199 | 200 | ### Using the legacy Sass API 201 | 202 | If you need to use the deprecated `render` Sass API, `gulp-sass` still includes legacy support. 203 | 204 | ```js 205 | 'use strict'; 206 | 207 | const gulp = require('gulp'); 208 | const sass = require('gulp-sass/legacy')(require('sass')); 209 | 210 | function buildStyles() { 211 | return gulp.src('./sass/**/*.scss') 212 | .pipe(sass().on('error', sass.logError)) 213 | .pipe(gulp.dest('./css')); 214 | }; 215 | 216 | exports.buildStyles = buildStyles; 217 | exports.watch = function () { 218 | gulp.watch('./sass/**/*.scss', buildStyles); 219 | }; 220 | ```` 221 | 222 | ### What about fibers? 223 | 224 | We used to recommend Node fibers as a way to speed up asynchronous rendering with Dart Sass. Unfortunately, [Node fibers are discontinued](https://sass-lang.com/blog/node-fibers-discontinued) and will not work in Node.js 16. The Sass team is exploring its options for future performance improvements, but for now, you will get the best performance from `sass.sync()`. 225 | 226 | ## Issues 227 | 228 | `gulp-sass` is a light-weight wrapper around either [Dart Sass][] or [Node Sass][] (which in turn is a Node.js binding for [LibSass][]. Because of this, the issue you're having likely isn't a `gulp-sass` issue, but an issue with one those projects or with [Sass][] as a whole. 229 | 230 | If you have a feature request/question about how Sass works/concerns on how your Sass gets compiled/errors in your compiling, it's likely a Dart Sass or LibSass issue and you should file your issue with one of those projects. 231 | 232 | If you're having problems with the options you're passing in, it's likely a Dart Sass or Node Sass issue and you should file your issue with one of those projects. 233 | 234 | We may, in the course of resolving issues, direct you to one of these other projects. If we do so, please follow up by searching that project's issue queue (both open and closed) for your problem and, if it doesn't exist, filing an issue with them. 235 | 236 | [Embedded Sass]: https://github.com/sass/embedded-host-node 237 | [Dart Sass]: https://sass-lang.com/dart-sass 238 | [LibSass]: https://sass-lang.com/libsass 239 | [Node Sass]: https://github.com/sass/node-sass 240 | [Sass]: https://sass-lang.com 241 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const { Transform } = require('stream'); 5 | const picocolors = require('picocolors'); 6 | const PluginError = require('plugin-error'); 7 | const replaceExtension = require('replace-ext'); 8 | const stripAnsi = require('strip-ansi'); 9 | const clonedeep = require('lodash.clonedeep'); 10 | const applySourceMap = require('vinyl-sourcemaps-apply'); 11 | 12 | const PLUGIN_NAME = 'gulp-sass'; 13 | 14 | const MISSING_COMPILER_MESSAGE = ` 15 | gulp-sass no longer has a default Sass compiler; please set one yourself. 16 | Both the "sass" and "node-sass" packages are permitted. 17 | For example, in your gulpfile: 18 | 19 | const sass = require('gulp-sass')(require('sass')); 20 | `; 21 | 22 | const transfob = (transform) => new Transform({ transform, objectMode: true }); 23 | 24 | /** 25 | * Handles returning the file to the stream 26 | */ 27 | const filePush = (file, compileResult, callback) => { 28 | file.contents = Buffer.from(compileResult.css); 29 | file.path = replaceExtension(file.path, '.css'); 30 | 31 | // Build Source Maps! 32 | if (compileResult.sourceMap) { 33 | const proto = /^file:\/\/?/; 34 | const leadingSlash = /^\//; 35 | const sassMap = compileResult.sourceMap; 36 | const base = path.resolve(file.cwd, file.base); 37 | 38 | if (!sassMap.file) { 39 | // Convert from absolute path to relative as in gulp-sass 5.0.0 40 | sassMap.file = file.history[0] 41 | .replace(base + path.sep, '') 42 | .replace(proto, ''); 43 | } 44 | 45 | // Transform to relative file paths as in gulp-sass 5.0.0 46 | sassMap.sources = sassMap.sources.map((src) => { 47 | // file uses Windows-style path separators, source is a URL. 48 | const baseUri = base.replace(/\\/g, '/'); 49 | // The current file and its content is included 50 | // as data: in the new Sass JS API. 51 | // Map it to the original file name (first history entry). 52 | if (src.startsWith('data:')) { 53 | return file.history[0] 54 | .replace(/\\/g, '/') 55 | .replace(`${baseUri}/`, '') 56 | .replace(proto, '') 57 | .replace(leadingSlash, ''); 58 | } 59 | return src 60 | .replace(proto, '') 61 | .replace(`${baseUri}/`, '') 62 | .replace(leadingSlash, ''); 63 | }); 64 | 65 | // Grab the base filename that's being worked on 66 | const sassFileSrc = file.relative; 67 | // Replace the map file with the original filename (but new extension) 68 | sassMap.file = replaceExtension(sassFileSrc, '.css'); 69 | 70 | if (file.sourceMap.sourcesContent && !sassMap.sourcesContent) { 71 | sassMap.sourcesContent = file.sourceMap.sourcesContent; 72 | } 73 | 74 | // Apply the map 75 | applySourceMap(file, sassMap); 76 | } 77 | 78 | if (file.stat) { 79 | file.stat.atime = file.stat.mtime = file.stat.ctime = new Date(); 80 | } 81 | 82 | callback(null, file); 83 | }; 84 | 85 | /** 86 | * Handles error message 87 | */ 88 | const handleError = (error, file, callback) => { 89 | const filePath = (error.file === 'stdin' ? file.path : error.file) || file.path; 90 | const relativePath = path.relative(process.cwd(), filePath); 91 | const message = `${picocolors.underline(relativePath)}\n${error.message}`; 92 | 93 | error.messageFormatted = message; 94 | error.messageOriginal = error.message; 95 | error.message = stripAnsi(message); 96 | error.relativePath = relativePath; 97 | 98 | return callback(new PluginError(PLUGIN_NAME, error)); 99 | }; 100 | 101 | /** 102 | * Main Gulp Sass function 103 | */ 104 | 105 | // eslint-disable-next-line arrow-body-style 106 | const gulpSass = (options, sync) => { 107 | return transfob((file, encoding, callback) => { 108 | if (file.isNull()) { 109 | callback(null, file); 110 | return; 111 | } 112 | 113 | if (file.isStream()) { 114 | callback(new PluginError(PLUGIN_NAME, 'Streaming not supported')); 115 | return; 116 | } 117 | 118 | if (path.basename(file.path).startsWith('_')) { 119 | callback(); 120 | return; 121 | } 122 | 123 | if (!file.contents.length) { 124 | file.path = replaceExtension(file.path, '.css'); 125 | callback(null, file); 126 | return; 127 | } 128 | 129 | const opts = clonedeep(options || {}); 130 | 131 | // Ensure `indented` if a `.sass` file 132 | if (path.extname(file.path) === '.sass') { 133 | opts.syntax = 'indented'; 134 | } 135 | 136 | // Ensure file's parent directory in the include path 137 | if (opts.loadPaths) { 138 | if (typeof opts.loadPaths === 'string') { 139 | opts.loadPaths = [opts.loadPaths]; 140 | } 141 | } else { 142 | opts.loadPaths = []; 143 | } 144 | 145 | opts.loadPaths.unshift(path.dirname(file.path)); 146 | 147 | // Generate Source Maps if the source-map plugin is present 148 | if (file.sourceMap) { 149 | opts.sourceMap = true; 150 | opts.sourceMapIncludeSources = true; 151 | } 152 | 153 | const fileContents = file.contents.toString(); 154 | if (sync !== true) { 155 | /** 156 | * Async Sass compile 157 | */ 158 | gulpSass.compiler 159 | .compileStringAsync(fileContents, opts) 160 | .then((compileResult) => { 161 | filePush(file, compileResult, callback); 162 | }) 163 | .catch((error) => { 164 | handleError(error, file, callback); 165 | }); 166 | } else { 167 | /** 168 | * Sync Sass compile 169 | */ 170 | try { 171 | filePush(file, gulpSass.compiler.compileString(fileContents, opts), callback); 172 | } catch (error) { 173 | handleError(error, file, callback); 174 | } 175 | } 176 | }); 177 | }; 178 | 179 | /** 180 | * Sync Sass compile 181 | */ 182 | gulpSass.sync = (options) => gulpSass(options, true); 183 | 184 | /** 185 | * Log errors nicely 186 | */ 187 | gulpSass.logError = function logError(error) { 188 | const message = new PluginError('sass', error).toString(); 189 | process.stderr.write(`${message}\n`); 190 | this.emit('end'); 191 | }; 192 | 193 | module.exports = (compiler) => { 194 | if (!compiler || !compiler.compile) { 195 | const message = new PluginError( 196 | PLUGIN_NAME, 197 | MISSING_COMPILER_MESSAGE, 198 | { showProperties: false }, 199 | ).toString(); 200 | process.stderr.write(`${message}\n`); 201 | process.exit(1); 202 | } 203 | 204 | gulpSass.compiler = compiler; 205 | return gulpSass; 206 | }; 207 | -------------------------------------------------------------------------------- /legacy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const { Transform } = require('stream'); 5 | const picocolors = require('picocolors'); 6 | const PluginError = require('plugin-error'); 7 | const replaceExtension = require('replace-ext'); 8 | const stripAnsi = require('strip-ansi'); 9 | const clonedeep = require('lodash.clonedeep'); 10 | const applySourceMap = require('vinyl-sourcemaps-apply'); 11 | 12 | const PLUGIN_NAME = 'gulp-sass'; 13 | 14 | const MISSING_COMPILER_MESSAGE = ` 15 | gulp-sass no longer has a default Sass compiler; please set one yourself. 16 | Both the "sass" and "node-sass" packages are permitted. 17 | For example, in your gulpfile: 18 | 19 | const sass = require('gulp-sass')(require('sass')); 20 | `; 21 | 22 | const transfob = (transform) => new Transform({ transform, objectMode: true }); 23 | 24 | /** 25 | * Handles returning the file to the stream 26 | */ 27 | const filePush = (file, sassObject, callback) => { 28 | // Build Source Maps! 29 | if (sassObject.map) { 30 | // Transform map into JSON 31 | const sassMap = JSON.parse(sassObject.map.toString()); 32 | // Grab the stdout and transform it into stdin 33 | const sassMapFile = sassMap.file.replace(/^stdout$/, 'stdin'); 34 | // Grab the base filename that's being worked on 35 | const sassFileSrc = file.relative; 36 | // Grab the path portion of the file that's being worked on 37 | const sassFileSrcPath = path.dirname(sassFileSrc); 38 | 39 | if (sassFileSrcPath) { 40 | const sourceFileIndex = sassMap.sources.indexOf(sassMapFile); 41 | // Prepend the path to all files in the sources array except the file that's being worked on 42 | sassMap.sources = sassMap.sources.map((source, index) => ( 43 | index === sourceFileIndex 44 | ? source 45 | : path.join(sassFileSrcPath, source) 46 | )); 47 | } 48 | 49 | // Remove 'stdin' from souces and replace with filenames! 50 | sassMap.sources = sassMap.sources.filter((src) => src !== 'stdin' && src); 51 | 52 | // Replace the map file with the original filename (but new extension) 53 | sassMap.file = replaceExtension(sassFileSrc, '.css'); 54 | // Apply the map 55 | applySourceMap(file, sassMap); 56 | } 57 | 58 | file.contents = sassObject.css; 59 | file.path = replaceExtension(file.path, '.css'); 60 | 61 | if (file.stat) { 62 | file.stat.atime = file.stat.mtime = file.stat.ctime = new Date(); 63 | } 64 | 65 | callback(null, file); 66 | }; 67 | 68 | /** 69 | * Handles error message 70 | */ 71 | const handleError = (error, file, callback) => { 72 | const filePath = (error.file === 'stdin' ? file.path : error.file) || file.path; 73 | const relativePath = path.relative(process.cwd(), filePath); 74 | const message = `${picocolors.underline(relativePath)}\n${error.formatted}`; 75 | 76 | error.messageFormatted = message; 77 | error.messageOriginal = error.message; 78 | error.message = stripAnsi(message); 79 | error.relativePath = relativePath; 80 | 81 | return callback(new PluginError(PLUGIN_NAME, error)); 82 | }; 83 | 84 | /** 85 | * Main Gulp Sass function 86 | */ 87 | 88 | // eslint-disable-next-line arrow-body-style 89 | const gulpSass = (options, sync) => { 90 | return transfob((file, encoding, callback) => { 91 | if (file.isNull()) { 92 | callback(null, file); 93 | return; 94 | } 95 | 96 | if (file.isStream()) { 97 | callback(new PluginError(PLUGIN_NAME, 'Streaming not supported')); 98 | return; 99 | } 100 | 101 | if (path.basename(file.path).startsWith('_')) { 102 | callback(); 103 | return; 104 | } 105 | 106 | if (!file.contents.length) { 107 | file.path = replaceExtension(file.path, '.css'); 108 | callback(null, file); 109 | return; 110 | } 111 | 112 | const opts = clonedeep(options || {}); 113 | opts.data = file.contents.toString(); 114 | 115 | // We set the file path here so that libsass can correctly resolve import paths 116 | opts.file = file.path; 117 | 118 | // Ensure `indentedSyntax` is true if a `.sass` file 119 | if (path.extname(file.path) === '.sass') { 120 | opts.indentedSyntax = true; 121 | } 122 | 123 | // Ensure file's parent directory in the include path 124 | if (opts.includePaths) { 125 | if (typeof opts.includePaths === 'string') { 126 | opts.includePaths = [opts.includePaths]; 127 | } 128 | } else { 129 | opts.includePaths = []; 130 | } 131 | 132 | opts.includePaths.unshift(path.dirname(file.path)); 133 | 134 | // Generate Source Maps if the source-map plugin is present 135 | if (file.sourceMap) { 136 | opts.sourceMap = file.path; 137 | opts.omitSourceMapUrl = true; 138 | opts.sourceMapContents = true; 139 | } 140 | 141 | if (sync !== true) { 142 | /** 143 | * Async Sass render 144 | */ 145 | gulpSass.compiler.render(opts, (error, obj) => { 146 | if (error) { 147 | handleError(error, file, callback); 148 | return; 149 | } 150 | 151 | filePush(file, obj, callback); 152 | }); 153 | } else { 154 | /** 155 | * Sync Sass render 156 | */ 157 | try { 158 | filePush(file, gulpSass.compiler.renderSync(opts), callback); 159 | } catch (error) { 160 | handleError(error, file, callback); 161 | } 162 | } 163 | }); 164 | }; 165 | 166 | /** 167 | * Sync Sass render 168 | */ 169 | gulpSass.sync = (options) => gulpSass(options, true); 170 | 171 | /** 172 | * Log errors nicely 173 | */ 174 | gulpSass.logError = function logError(error) { 175 | const message = new PluginError('sass', error.messageFormatted).toString(); 176 | process.stderr.write(`${message}\n`); 177 | this.emit('end'); 178 | }; 179 | 180 | module.exports = (compiler) => { 181 | if (!compiler || !compiler.render) { 182 | const message = new PluginError( 183 | PLUGIN_NAME, 184 | MISSING_COMPILER_MESSAGE, 185 | { showProperties: false }, 186 | ).toString(); 187 | process.stderr.write(`${message}\n`); 188 | process.exit(1); 189 | } 190 | 191 | gulpSass.compiler = compiler; 192 | return gulpSass; 193 | }; 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-sass", 3 | "version": "6.0.1", 4 | "description": "Gulp plugin for sass", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=12" 8 | }, 9 | "scripts": { 10 | "lint": "eslint --report-unused-disable-directives --ignore-path .gitignore .", 11 | "fix": "npm run lint -- --fix", 12 | "mocha": "mocha", 13 | "test": "npm run test:node-sass && npm run test:dart-sass && npm run test:legacy-dart-sass && npm run test:sass-embedded", 14 | "test:node-sass": "mocha", 15 | "test:dart-sass": "mocha -- --sass", 16 | "test:legacy-dart-sass": "mocha -- --sass --legacy", 17 | "test:sass-embedded": "mocha -- --embedded" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dlmanning/gulp-sass.git" 22 | }, 23 | "keywords": [ 24 | "gulpplugin", 25 | "sass", 26 | "gulp" 27 | ], 28 | "author": "David Manning", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/dlmanning/gulp-sass/issues" 32 | }, 33 | "homepage": "https://github.com/dlmanning/gulp-sass#readme", 34 | "files": [ 35 | "index.js", 36 | "legacy.js" 37 | ], 38 | "dependencies": { 39 | "lodash.clonedeep": "^4.5.0", 40 | "picocolors": "^1.0.0", 41 | "plugin-error": "^1.0.1", 42 | "replace-ext": "^2.0.0", 43 | "strip-ansi": "^6.0.1", 44 | "vinyl-sourcemaps-apply": "^0.2.1" 45 | }, 46 | "devDependencies": { 47 | "autoprefixer": "^10.4.0", 48 | "eslint": "^8.5.0", 49 | "eslint-config-airbnb-base": "^15.0.0", 50 | "eslint-plugin-import": "^2.25.3", 51 | "globule": "^1.3.3", 52 | "gulp": "^4.0.2", 53 | "gulp-postcss": "^9.0.1", 54 | "gulp-sourcemaps": "^3.0.0", 55 | "gulp-tap": "^2.0.0", 56 | "mocha": "^9.1.3", 57 | "node-sass": "^7.0.1", 58 | "postcss": "^8.4.5", 59 | "rimraf": "^5.0.10", 60 | "sass": "^1.45.1", 61 | "sass-embedded": "^1.49.9", 62 | "vinyl": "^2.2.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/expected-sass/empty.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlmanning/gulp-sass/bc91629adcb4735bdfa050a53841c7dc16949159/test/expected-sass/empty.css -------------------------------------------------------------------------------- /test/expected-sass/indent.css: -------------------------------------------------------------------------------- 1 | body .div { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /test/expected-sass/inheritance.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: pink; 3 | } 4 | 5 | footer { 6 | background: red; 7 | } 8 | 9 | .error, .badError { 10 | border: #f00; 11 | background: #fdd; 12 | } 13 | 14 | .error.intrusion, .intrusion.badError { 15 | font-size: 1.3em; 16 | font-weight: bold; 17 | } 18 | 19 | .badError { 20 | border-width: 3px; 21 | } -------------------------------------------------------------------------------- /test/expected-sass/mixins.css: -------------------------------------------------------------------------------- 1 | #data { 2 | float: left; 3 | margin-left: 10px; 4 | } 5 | #data th { 6 | text-align: center; 7 | font-weight: bold; 8 | } 9 | #data td, #data th { 10 | padding: 2px; 11 | } -------------------------------------------------------------------------------- /test/expected-sass/variables.css: -------------------------------------------------------------------------------- 1 | .content-navigation { 2 | border-color: #3bbfce; 3 | color: #2ca2af; 4 | } 5 | 6 | .border { 7 | padding: 8px; 8 | margin: 8px; 9 | border-color: #3bbfce; 10 | } -------------------------------------------------------------------------------- /test/expected/empty.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlmanning/gulp-sass/bc91629adcb4735bdfa050a53841c7dc16949159/test/expected/empty.css -------------------------------------------------------------------------------- /test/expected/indent.css: -------------------------------------------------------------------------------- 1 | body .div { 2 | color: blue; } 3 | -------------------------------------------------------------------------------- /test/expected/inheritance.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: pink; } 3 | 4 | footer { 5 | background: red; } 6 | 7 | .error, .badError { 8 | border: #f00; 9 | background: #fdd; } 10 | 11 | .error.intrusion, .intrusion.badError { 12 | font-size: 1.3em; 13 | font-weight: bold; } 14 | 15 | .badError { 16 | border-width: 3px; } 17 | -------------------------------------------------------------------------------- /test/expected/mixins.css: -------------------------------------------------------------------------------- 1 | #data { 2 | float: left; 3 | margin-left: 10px; } 4 | #data th { 5 | text-align: center; 6 | font-weight: bold; } 7 | #data td, #data th { 8 | padding: 2px; } 9 | -------------------------------------------------------------------------------- /test/expected/variables.css: -------------------------------------------------------------------------------- 1 | .content-navigation { 2 | border-color: #3bbfce; 3 | color: #2ca2af; } 4 | 5 | .border { 6 | padding: 8px; 7 | margin: 8px; 8 | border-color: #3bbfce; } 9 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert').strict; 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const Vinyl = require('vinyl'); 7 | const { rimraf } = require('rimraf'); 8 | const gulp = require('gulp'); 9 | const sourcemaps = require('gulp-sourcemaps'); 10 | const postcss = require('gulp-postcss'); 11 | const autoprefixer = require('autoprefixer'); 12 | const tap = require('gulp-tap'); 13 | const globule = require('globule'); 14 | 15 | function getCompiler() { 16 | if (process.argv.includes('--sass')) return 'sass'; 17 | if (process.argv.includes('--embedded')) return 'sass-embedded'; 18 | return 'node-sass'; 19 | } 20 | 21 | const COMPILER = getCompiler(); 22 | const LEGACY_API = COMPILER === 'node-sass' || process.argv.includes('--legacy'); 23 | const MODERN_COMPILER = COMPILER === 'sass' || COMPILER === 'sass-embedded'; 24 | 25 | /* eslint-disable import/no-dynamic-require */ 26 | const sass = LEGACY_API 27 | ? require('../legacy')(require(COMPILER)) 28 | : require('../index')(require(COMPILER)); 29 | /* eslint-enable import/no-dynamic-require */ 30 | 31 | const expectedTestsPath = MODERN_COMPILER ? 'expected-sass' : 'expected'; 32 | 33 | const createVinyl = (filename, contents) => { 34 | const base = path.join(__dirname, 'scss'); 35 | const filePath = path.join(base, filename); 36 | 37 | return new Vinyl({ 38 | cwd: __dirname, 39 | base, 40 | path: filePath, 41 | contents: contents || fs.readFileSync(filePath), 42 | }); 43 | }; 44 | 45 | const normaliseEOL = (str) => str.toString('utf8').replace(/\r\n/g, '\n'); 46 | 47 | describe('test helpers', () => { 48 | it('should normalise EOL', (done) => { 49 | assert.equal(normaliseEOL('foo\r\nbar'), 'foo\nbar'); 50 | assert.equal(normaliseEOL('foo\nbar'), 'foo\nbar'); 51 | done(); 52 | }); 53 | }); 54 | 55 | describe('gulp-sass -- async compile', () => { 56 | it('should pass file when it isNull()', (done) => { 57 | const stream = sass({ silenceDeprecations: ['import'] }); 58 | const emptyFile = { 59 | isNull: () => true, 60 | }; 61 | stream.on('data', (data) => { 62 | assert.deepEqual(data, emptyFile); 63 | done(); 64 | }); 65 | stream.write(emptyFile); 66 | }); 67 | 68 | it('should emit error when file isStream()', (done) => { 69 | const stream = sass({ silenceDeprecations: ['import'] }); 70 | const streamFile = { 71 | isNull: () => false, 72 | isStream: () => true, 73 | }; 74 | stream.on('error', (err) => { 75 | assert.equal(err.message, 'Streaming not supported'); 76 | done(); 77 | }); 78 | stream.write(streamFile); 79 | }); 80 | 81 | it('should compile an empty sass file', (done) => { 82 | const sassFile = createVinyl('empty.scss'); 83 | const stream = sass({ silenceDeprecations: ['import'] }); 84 | stream.on('data', (cssFile) => { 85 | assert.ok(cssFile); 86 | assert.ok(cssFile.path); 87 | assert.ok(cssFile.relative); 88 | assert.ok(cssFile.contents); 89 | assert.equal(path.basename(cssFile.path), 'empty.css'); 90 | 91 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'empty.css'), 'utf8'); 92 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 93 | done(); 94 | }); 95 | stream.write(sassFile); 96 | }); 97 | 98 | it('should compile a single sass file', (done) => { 99 | const sassFile = createVinyl('mixins.scss'); 100 | const stream = sass({ silenceDeprecations: ['import'] }); 101 | stream.on('data', (cssFile) => { 102 | assert.ok(cssFile); 103 | assert.ok(cssFile.path); 104 | assert.ok(cssFile.relative); 105 | assert.ok(cssFile.contents); 106 | 107 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'mixins.css'), 'utf8'); 108 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 109 | done(); 110 | }); 111 | stream.write(sassFile); 112 | }); 113 | 114 | it('should compile multiple sass files', (done) => { 115 | const files = [ 116 | createVinyl('mixins.scss'), 117 | createVinyl('variables.scss'), 118 | ]; 119 | const stream = sass({ silenceDeprecations: ['import'] }); 120 | let mustSee = files.length; 121 | let expectedPath = path.join(expectedTestsPath, 'mixins.css'); 122 | 123 | stream.on('data', (cssFile) => { 124 | assert.ok(cssFile); 125 | assert.ok(cssFile.path); 126 | assert.ok(cssFile.relative); 127 | assert.ok(cssFile.contents); 128 | if (cssFile.path.includes('variables')) { 129 | expectedPath = path.join(expectedTestsPath, 'variables.css'); 130 | } 131 | 132 | const actual = fs.readFileSync(path.join(__dirname, expectedPath), 'utf8'); 133 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 134 | 135 | mustSee -= 1; 136 | if (mustSee <= 0) { 137 | done(); 138 | } 139 | }); 140 | 141 | files.forEach((file) => { 142 | stream.write(file); 143 | }); 144 | }); 145 | 146 | it('should compile files with partials in another folder', (done) => { 147 | const sassFile = createVinyl('inheritance.scss'); 148 | const stream = sass({ silenceDeprecations: ['import'] }); 149 | stream.on('data', (cssFile) => { 150 | assert.ok(cssFile); 151 | assert.ok(cssFile.path); 152 | assert.ok(cssFile.relative); 153 | assert.ok(cssFile.contents); 154 | 155 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'inheritance.css'), 'utf8'); 156 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 157 | done(); 158 | }); 159 | stream.write(sassFile); 160 | }); 161 | 162 | it('should emit logError on sass error', (done) => { 163 | const errorFile = createVinyl('error.scss'); 164 | const stream = sass({ silenceDeprecations: ['import'] }); 165 | 166 | stream.on('error', sass.logError); 167 | stream.on('end', done); 168 | stream.write(errorFile); 169 | }); 170 | 171 | it('should handle sass errors', (done) => { 172 | const errorFile = createVinyl('error.scss'); 173 | const stream = sass({ silenceDeprecations: ['import'] }); 174 | 175 | stream.on('error', (err) => { 176 | // Error must include message body 177 | const messageBody = MODERN_COMPILER 178 | ? 'expected "{"' 179 | : 'property "font" must be followed by a \':\''; 180 | assert.equal(err.message.includes(messageBody), true); 181 | // Error must include file error occurs in 182 | assert.equal(err.message.includes(path.normalize('test/scss/error.scss')), true); 183 | // Error must include relativePath property 184 | assert.equal(err.relativePath, path.join('test', 'scss', 'error.scss')); 185 | done(); 186 | }); 187 | stream.write(errorFile); 188 | }); 189 | 190 | it('should preserve the original sass error message', (done) => { 191 | const errorFile = createVinyl('error.scss'); 192 | const stream = sass({ silenceDeprecations: ['import'] }); 193 | 194 | stream.on('error', (err) => { 195 | // Error must include original error message 196 | const message = MODERN_COMPILER 197 | ? 'expected "{"' 198 | : 'property "font" must be followed by a \':\''; 199 | assert.equal(err.messageOriginal.includes(message), true); 200 | // Error must not format or change the original error message 201 | assert.equal(err.messageOriginal.includes('on line 2'), false); 202 | done(); 203 | }); 204 | stream.write(errorFile); 205 | }); 206 | 207 | it('should compile a single sass file if the file name has been changed in the stream', (done) => { 208 | const sassFile = createVinyl('mixins.scss'); 209 | // Transform file name 210 | sassFile.path = path.join(path.join(__dirname, 'scss'), 'mixin--changed.scss'); 211 | 212 | const stream = sass({ silenceDeprecations: ['import'] }); 213 | stream.on('data', (cssFile) => { 214 | assert.ok(cssFile); 215 | assert.ok(cssFile.path); 216 | assert.equal(cssFile.path.split(path.sep).pop(), 'mixin--changed.css'); 217 | assert.ok(cssFile.relative); 218 | assert.ok(cssFile.contents); 219 | 220 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'mixins.css'), 'utf8'); 221 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 222 | done(); 223 | }); 224 | stream.write(sassFile); 225 | }); 226 | 227 | it('should preserve changes made in-stream to a Sass file', (done) => { 228 | const sassFile = createVinyl('mixins.scss'); 229 | // Transform file name 230 | sassFile.contents = Buffer.from(`/* Added Dynamically */${sassFile.contents.toString()}`); 231 | 232 | const stream = sass({ silenceDeprecations: ['import'] }); 233 | stream.on('data', (cssFile) => { 234 | assert.ok(cssFile); 235 | assert.ok(cssFile.path); 236 | assert.ok(cssFile.relative); 237 | assert.ok(cssFile.contents); 238 | 239 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'mixins.css'), 'utf8'); 240 | assert.equal(String(normaliseEOL(cssFile.contents)), `/* Added Dynamically */\n${normaliseEOL(actual)}`); 241 | done(); 242 | }); 243 | stream.write(sassFile); 244 | }); 245 | 246 | it('should work with gulp-sourcemaps', (done) => { 247 | const sassFile = createVinyl('inheritance.scss'); 248 | 249 | sassFile.sourceMap = '{' 250 | + '"version": 3,' 251 | + '"file": "scss/subdir/multilevelimport.scss",' 252 | + '"names": [],' 253 | + '"mappings": "",' 254 | + '"sources": [ "scss/subdir/multilevelimport.scss" ],' 255 | + '"sourcesContent": [ "@import ../inheritance;" ]' 256 | + '}'; 257 | 258 | // Expected sources are relative to file.base 259 | const expectedSources = [ 260 | 'inheritance.scss', 261 | 'includes/_cats.scss', 262 | 'includes/_dogs.sass', 263 | ]; 264 | 265 | const stream = sass({ silenceDeprecations: ['import'] }); 266 | stream.on('data', (cssFile) => { 267 | assert.ok(cssFile.sourceMap); 268 | assert.deepEqual(cssFile.sourceMap.sources.sort(), expectedSources.sort()); 269 | done(); 270 | }); 271 | stream.write(sassFile); 272 | }); 273 | 274 | it('should compile a single indented sass file', (done) => { 275 | const sassFile = createVinyl('indent.sass'); 276 | const stream = sass({ silenceDeprecations: ['import'] }); 277 | stream.on('data', (cssFile) => { 278 | assert.ok(cssFile); 279 | assert.ok(cssFile.path); 280 | assert.ok(cssFile.relative); 281 | assert.ok(cssFile.contents); 282 | 283 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'indent.css'), 'utf8'); 284 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 285 | done(); 286 | }); 287 | stream.write(sassFile); 288 | }); 289 | 290 | it('should parse files in sass and scss', (done) => { 291 | const files = [ 292 | createVinyl('mixins.scss'), 293 | createVinyl('indent.sass'), 294 | ]; 295 | const stream = sass({ silenceDeprecations: ['import'] }); 296 | let mustSee = files.length; 297 | let expectedPath = path.join(expectedTestsPath, 'mixins.css'); 298 | 299 | stream.on('data', (cssFile) => { 300 | assert.ok(cssFile); 301 | assert.ok(cssFile.path); 302 | assert.ok(cssFile.relative); 303 | assert.ok(cssFile.contents); 304 | if (cssFile.path.includes('indent')) { 305 | expectedPath = path.join(expectedTestsPath, 'indent.css'); 306 | } 307 | 308 | const actual = fs.readFileSync(path.join(__dirname, expectedPath), 'utf8'); 309 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 310 | 311 | mustSee -= 1; 312 | if (mustSee <= 0) { 313 | done(); 314 | } 315 | }); 316 | 317 | files.forEach((file) => { 318 | stream.write(file); 319 | }); 320 | }); 321 | }); 322 | 323 | describe('gulp-sass -- sync compile', () => { 324 | before((done) => { 325 | rimraf(path.join(__dirname, 'results')).finally(done); 326 | }); 327 | 328 | it('should pass file when it isNull()', (done) => { 329 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 330 | const emptyFile = { 331 | isNull: () => true, 332 | }; 333 | stream.on('data', (data) => { 334 | assert.deepEqual(data, emptyFile); 335 | done(); 336 | }); 337 | stream.write(emptyFile); 338 | }); 339 | 340 | it('should emit error when file isStream()', (done) => { 341 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 342 | const streamFile = { 343 | isNull: () => false, 344 | isStream: () => true, 345 | }; 346 | stream.on('error', (err) => { 347 | assert.equal(err.message, 'Streaming not supported'); 348 | done(); 349 | }); 350 | stream.write(streamFile); 351 | }); 352 | 353 | it('should compile a single sass file', (done) => { 354 | const sassFile = createVinyl('mixins.scss'); 355 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 356 | stream.on('data', (cssFile) => { 357 | assert.ok(cssFile); 358 | assert.ok(cssFile.path); 359 | assert.ok(cssFile.relative); 360 | assert.ok(cssFile.contents); 361 | 362 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'mixins.css'), 'utf8'); 363 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 364 | done(); 365 | }); 366 | stream.write(sassFile); 367 | }); 368 | 369 | it('should compile multiple sass files', (done) => { 370 | const files = [ 371 | createVinyl('mixins.scss'), 372 | createVinyl('variables.scss'), 373 | ]; 374 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 375 | let mustSee = files.length; 376 | let expectedPath = path.join(expectedTestsPath, 'mixins.css'); 377 | 378 | stream.on('data', (cssFile) => { 379 | assert.ok(cssFile); 380 | assert.ok(cssFile.path); 381 | assert.ok(cssFile.relative); 382 | assert.ok(cssFile.contents); 383 | 384 | if (cssFile.path.includes('variables')) { 385 | expectedPath = path.join(expectedTestsPath, 'variables.css'); 386 | } 387 | 388 | const actual = normaliseEOL(fs.readFileSync(path.join(__dirname, expectedPath), 'utf8')); 389 | assert.equal(String(normaliseEOL(cssFile.contents)), actual); 390 | 391 | mustSee -= 1; 392 | if (mustSee <= 0) { 393 | done(); 394 | } 395 | }); 396 | 397 | files.forEach((file) => { 398 | stream.write(file); 399 | }); 400 | }); 401 | 402 | it('should compile files with partials in another folder', (done) => { 403 | const sassFile = createVinyl('inheritance.scss'); 404 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 405 | 406 | stream.on('data', (cssFile) => { 407 | assert.ok(cssFile); 408 | assert.ok(cssFile.path); 409 | assert.ok(cssFile.relative); 410 | assert.ok(cssFile.contents); 411 | 412 | const actual = fs.readFileSync(path.join(__dirname, expectedTestsPath, 'inheritance.css'), 'utf8'); 413 | assert.equal(String(normaliseEOL(cssFile.contents)), normaliseEOL(actual)); 414 | done(); 415 | }); 416 | stream.write(sassFile); 417 | }); 418 | 419 | it('should handle sass errors', (done) => { 420 | const errorFile = createVinyl('error.scss'); 421 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 422 | 423 | stream.on('error', (err) => { 424 | // Error must include message body 425 | const messageBody = MODERN_COMPILER 426 | ? 'expected "{"' 427 | : 'property "font" must be followed by a \':\''; 428 | assert.equal(err.message.includes(messageBody), true); 429 | assert.equal(err.relativePath, path.join('test', 'scss', 'error.scss')); 430 | done(); 431 | }); 432 | stream.write(errorFile); 433 | }); 434 | 435 | it('should emit logError on sass error', (done) => { 436 | const errorFile = createVinyl('error.scss'); 437 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 438 | 439 | stream.on('error', sass.logError); 440 | stream.on('end', done); 441 | stream.write(errorFile); 442 | }); 443 | 444 | it('should work with gulp-sourcemaps', (done) => { 445 | const sassFile = createVinyl('inheritance.scss'); 446 | 447 | // Expected sources are relative to file.base 448 | const expectedSources = [ 449 | 'inheritance.scss', 450 | 'includes/_cats.scss', 451 | 'includes/_dogs.sass', 452 | ]; 453 | 454 | sassFile.sourceMap = '{' 455 | + '"version": 3,' 456 | + '"file": "scss/subdir/multilevelimport.scss",' 457 | + '"names": [],' 458 | + '"mappings": "",' 459 | + '"sources": [ "scss/subdir/multilevelimport.scss" ],' 460 | + '"sourcesContent": [ "@import ../inheritance;" ]' 461 | + '}'; 462 | 463 | const stream = sass.sync({ silenceDeprecations: ['import'] }); 464 | stream.on('data', (cssFile) => { 465 | assert.ok(cssFile.sourceMap); 466 | assert.deepEqual(cssFile.sourceMap.sources.sort(), expectedSources.sort()); 467 | done(); 468 | }); 469 | stream.write(sassFile); 470 | }); 471 | 472 | it('should work with gulp-sourcemaps and autoprefixer', (done) => { 473 | const expectedSourcesBefore = [ 474 | 'inheritance.scss', 475 | 'includes/_cats.scss', 476 | 'includes/_dogs.sass', 477 | ]; 478 | 479 | const expectedSourcesAfter = [ 480 | 'includes/_cats.scss', 481 | 'includes/_dogs.sass', 482 | 'inheritance.scss', 483 | ]; 484 | 485 | if (MODERN_COMPILER) { 486 | expectedSourcesAfter.push( 487 | 'inheritance.css', // added by postcss 488 | ); 489 | } 490 | 491 | gulp.src(path.join(__dirname, 'scss', 'inheritance.scss')) 492 | .pipe(sourcemaps.init()) 493 | .pipe(sass.sync({ silenceDeprecations: ['import'] })) 494 | .pipe(tap((file) => { 495 | assert.ok(file.sourceMap); 496 | assert.equal(file.sourceMap.file, 'inheritance.css'); 497 | assert.deepEqual(file.sourceMap.sources.sort(), expectedSourcesBefore.sort()); 498 | })) 499 | .pipe(postcss([autoprefixer()])) 500 | .pipe(sourcemaps.write()) 501 | .pipe(gulp.dest(path.join(__dirname, 'results'))) 502 | .pipe(tap((file) => { 503 | assert.ok(file.sourceMap); 504 | assert.equal(file.sourceMap.file, 'inheritance.css'); 505 | assert.deepEqual(file.sourceMap.sources.sort(), expectedSourcesAfter.sort()); 506 | })); 507 | done(); 508 | }); 509 | 510 | it('should work with gulp-sourcemaps and a globbed source', (done) => { 511 | const globPath = path.join(__dirname, 'scss', 'globbed'); 512 | const files = globule.find(path.join(__dirname, 'scss', 'globbed', '**', '*.scss')); 513 | const filesContent = {}; 514 | 515 | files.forEach((file) => { 516 | const source = path.normalize(path.relative(globPath, file)); 517 | filesContent[source] = fs.readFileSync(file, 'utf8'); 518 | }); 519 | 520 | gulp.src(path.join(__dirname, 'scss', 'globbed', '**', '*.scss')) 521 | .pipe(sourcemaps.init()) 522 | .pipe(sass.sync({ silenceDeprecations: ['import'] })) 523 | .pipe(tap((file) => { 524 | assert.ok(file.sourceMap); 525 | assert.ok(file.sourceMap.sourcesContent); 526 | const actual = normaliseEOL(file.sourceMap.sourcesContent[0]); 527 | const expected = normaliseEOL(filesContent[path.normalize(file.sourceMap.sources[0])]); 528 | assert.deepEqual(actual, expected); 529 | })); 530 | done(); 531 | }); 532 | 533 | it('should work with gulp-sourcemaps and autoprefixer with different file.base', (done) => { 534 | const expectedSourcesBefore = [ 535 | 'scss/inheritance.scss', 536 | 'scss/includes/_cats.scss', 537 | 'scss/includes/_dogs.sass', 538 | ]; 539 | 540 | const expectedSourcesAfter = [ 541 | 'scss/includes/_cats.scss', 542 | 'scss/includes/_dogs.sass', 543 | 'scss/inheritance.scss', 544 | ]; 545 | 546 | if (MODERN_COMPILER) { 547 | expectedSourcesAfter.push( 548 | 'scss/inheritance.css', // added by postcss 549 | ); 550 | } 551 | 552 | gulp.src(path.join(__dirname, 'scss', 'inheritance.scss'), { base: 'test' }) 553 | .pipe(sourcemaps.init()) 554 | .pipe(sass.sync({ silenceDeprecations: ['import'] })) 555 | .pipe(tap((file) => { 556 | assert.ok(file.sourceMap); 557 | assert.equal(file.sourceMap.file, 'scss/inheritance.css'); 558 | assert.deepEqual(file.sourceMap.sources.sort(), expectedSourcesBefore.sort()); 559 | })) 560 | .pipe(postcss([autoprefixer()])) 561 | .pipe(tap((file) => { 562 | assert.ok(file.sourceMap); 563 | assert.equal(file.sourceMap.file, 'scss/inheritance.css'); 564 | assert.deepEqual(file.sourceMap.sources.sort(), expectedSourcesAfter.sort()); 565 | })); 566 | done(); 567 | }); 568 | 569 | it('should work with empty files', (done) => { 570 | gulp.src(path.join(__dirname, 'scss', 'empty.scss')) 571 | .pipe(sass.sync({ silenceDeprecations: ['import'] })) 572 | .pipe(gulp.dest(path.join(__dirname, 'results'))) 573 | .pipe(tap(() => { 574 | try { 575 | fs.statSync(path.join(__dirname, 'results', 'empty.css')); 576 | } catch (error) { 577 | assert.fail('Empty file was not produced!'); 578 | } 579 | })); 580 | done(); 581 | }); 582 | }); 583 | -------------------------------------------------------------------------------- /test/scss/_partial.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/scss/empty.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlmanning/gulp-sass/bc91629adcb4735bdfa050a53841c7dc16949159/test/scss/empty.scss -------------------------------------------------------------------------------- /test/scss/error.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font 'Comic Sans'; 3 | } 4 | -------------------------------------------------------------------------------- /test/scss/globbed/app.scss: -------------------------------------------------------------------------------- 1 | p { 2 | border: 1px solid red; 3 | } 4 | 5 | small { 6 | font: { 7 | size: 72px; 8 | } 9 | } 10 | 11 | h1 { 12 | font: { 13 | size: 8px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/scss/globbed/foo/bar.scss: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5 { 2 | color: green; 3 | font-weight: 800; 4 | } 5 | -------------------------------------------------------------------------------- /test/scss/includes/_cats.scss: -------------------------------------------------------------------------------- 1 | $blue: #3bbfce; 2 | $margin: 16px; 3 | 4 | body { 5 | background: pink; 6 | } 7 | -------------------------------------------------------------------------------- /test/scss/includes/_dogs.sass: -------------------------------------------------------------------------------- 1 | $blue: #3bbfce 2 | $margin: 16px 3 | 4 | footer 5 | background: red 6 | -------------------------------------------------------------------------------- /test/scss/indent.sass: -------------------------------------------------------------------------------- 1 | $color: blue 2 | 3 | body .div 4 | color: $color 5 | -------------------------------------------------------------------------------- /test/scss/inheritance.scss: -------------------------------------------------------------------------------- 1 | @import "includes/cats"; 2 | @import "includes/dogs"; 3 | 4 | .error { 5 | border: #f00; 6 | background: #fdd; 7 | } 8 | 9 | .error.intrusion { 10 | font-size: 1.3em; 11 | font-weight: bold; 12 | } 13 | 14 | .badError { 15 | @extend .error; 16 | border-width: 3px; 17 | } 18 | -------------------------------------------------------------------------------- /test/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin table-base { 2 | th { 3 | text-align: center; 4 | font-weight: bold; 5 | } 6 | td, th {padding: 2px} 7 | } 8 | 9 | @mixin left($dist) { 10 | float: left; 11 | margin-left: $dist; 12 | } 13 | 14 | #data { 15 | @include left(10px); 16 | @include table-base; 17 | } -------------------------------------------------------------------------------- /test/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $blue: #3bbfce; 2 | $margin: 16px; 3 | 4 | .content-navigation { 5 | border-color: $blue; 6 | color: 7 | darken($blue, 9%); 8 | } 9 | 10 | .border { 11 | padding: $margin / 2; 12 | margin: $margin / 2; 13 | border-color: $blue; 14 | } 15 | --------------------------------------------------------------------------------