A Heft plugin that compiles SCSS/Sass files during the build phase. It uses sass-embedded under the hood and produces:
- TypeScript type definitions (
.d.ts) for CSS modules, giving you typed access to class names and:exportvalues - Compiled CSS files (optional) in one or more output folders
- JavaScript shims (optional) that re-export the CSS for consumption in CommonJS or ESM environments
If
sass-embeddedis not supported on your platform, you can substitute it with thesasspackage using an npm alias.
- CHANGELOG.md - Find out what's new in the latest version
Heft is part of the Rush Stack family of projects.
In your project's package.json:
{
"devDependencies": {
"@rushstack/heft": "...",
"@rushstack/heft-sass-plugin": "..."
}
}The sass task must run before typescript so that the generated .d.ts files are available when TypeScript compiles your project.
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
"phasesByName": {
"build": {
"tasksByName": {
"sass": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft-sass-plugin"
}
},
"typescript": {
"taskDependencies": ["sass"],
"taskPlugin": {
"pluginPackage": "@rushstack/heft-typescript-plugin"
}
}
}
}
}
}A minimal config uses all defaults:
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json"
}A more complete setup that emits CSS and shims for both ESM and CommonJS:
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json",
"cssOutputFolders": [
{ "folder": "lib-esm", "shimModuleFormat": "esnext" },
{ "folder": "lib-commonjs", "shimModuleFormat": "commonjs" }
],
"fileExtensions": [".module.scss", ".module.sass"],
"nonModuleFileExtensions": [".global.scss", ".global.sass"],
"silenceDeprecations": ["mixed-decls", "import", "global-builtin", "color-functions"]
}Point TypeScript at the generated type definitions by including the generatedTsFolder in your tsconfig.json:
{
"compilerOptions": {
"paths": {}
},
"include": ["src", "temp/sass-ts"]
}The plugin distinguishes between two kinds of files based on their extension:
CSS modules (extensions listed in fileExtensions, default: .sass, .scss, .css):
- Processed with
postcss-modules - Class names and
:exportvalues become properties in a generated TypeScript interface - The generated
.d.tsexports a typedstylesobject as its default export
Global stylesheets (extensions listed in nonModuleFileExtensions, default: .global.sass, .global.scss, .global.css):
- Compiled to plain CSS with no module scoping
- The generated
.d.tsis a side-effect-only module (export {}) - Useful for resets, themes, and base styles
Partials (filenames starting with _):
- Never compiled to output files; they are only meant to be
@used or@forwarded by other files
// src/Button.module.scss
.root {
background: blue;
}
.label {
font-size: 14px;
}
:export {
brandColor: #0078d4;
}Generated temp/sass-ts/Button.module.scss.d.ts:
interface IStyles {
root: string;
label: string;
brandColor: string;
}
declare const styles: IStyles;
export default styles;In your TypeScript source:
import styles from './Button.module.scss';
// styles.root, styles.label, styles.brandColor are all typed stringsAll options are set in config/sass.json. Every option is optional.
| Option | Default | Description |
|---|---|---|
srcFolder |
"src/" |
Root directory that is scanned for SCSS files |
generatedTsFolder |
"temp/sass-ts/" |
Output directory for generated .d.ts files |
secondaryGeneratedTsFolders |
[] |
Additional directories to also write .d.ts files to (e.g. "lib-esm" when publishing typings alongside compiled output) |
exportAsDefault |
true |
When true, wraps exports in a typed default interface. When false, generates individual named exports (export const className: string). Note: false is incompatible with cssOutputFolders. |
cssOutputFolders |
(none) | Folders where compiled .css files are written. Each entry is either a plain folder path string, or an object with folder and optional shimModuleFormat (see below). |
fileExtensions |
[".sass", ".scss", ".css"] |
File extensions to treat as CSS modules |
nonModuleFileExtensions |
[".global.sass", ".global.scss", ".global.css"] |
File extensions to treat as global (non-module) stylesheets |
excludeFiles |
[] |
Paths relative to srcFolder to skip entirely |
doNotTrimOriginalFileExtension |
false |
When true, preserves the original extension in the CSS output filename. E.g. styles.scss → styles.scss.css instead of styles.css. Useful when downstream tooling needs to distinguish the source format. |
preserveIcssExports |
false |
When true, keeps the :export { } block in the emitted CSS. This is needed when a webpack loader (e.g. css-loader's icssParser) must extract :export values at bundle time. Has no effect on the generated .d.ts. |
silenceDeprecations |
[] |
List of Sass deprecation codes to suppress (e.g. "mixed-decls", "import", "global-builtin", "color-functions") |
ignoreDeprecationsInDependencies |
false |
Suppresses deprecation warnings that originate from node_modules dependencies |
extends |
(none) | Path to another sass.json config file to inherit settings from |
Each entry in cssOutputFolders can be a plain string (folder path only) or an object:
{
"folder": "lib-esm",
"shimModuleFormat": "esnext"
}When shimModuleFormat is set, the plugin writes a .js shim alongside each .css file. For a CSS module, the shim re-exports the CSS:
// ESM shim (shimModuleFormat: "esnext")
export { default } from "./Button.module.css";
// CommonJS shim (shimModuleFormat: "commonjs")
module.exports = require("./Button.module.css");
module.exports.default = module.exports;For a global stylesheet, the shim is a side-effect-only import:
// ESM shim
import "./global.global.css";
export {};
// CommonJS shim
require("./global.global.css");The plugin supports the modern pkg: protocol for importing from npm packages:
@use "pkg:@fluentui/react/dist/sass/variables";The legacy ~ prefix is automatically converted to pkg: for compatibility with older stylesheets:
// These are equivalent:
@use "~@fluentui/react/dist/sass/variables";
@use "pkg:@fluentui/react/dist/sass/variables";The plugin tracks inter-file dependencies (via @use, @forward, and @import) and only recompiles files that changed or whose dependencies changed. This makes heft build --watch fast even in large projects.
Other Heft plugins can hook into the Sass compilation pipeline via the ISassPluginAccessor interface:
import { ISassPluginAccessor } from '@rushstack/heft-sass-plugin';
// In your plugin's apply() method:
const sassAccessor = session.requestAccessToPlugin<ISassPluginAccessor>(
'@rushstack/heft-sass-plugin',
'sass-plugin',
'@rushstack/heft-sass-plugin'
);
sassAccessor.hooks.postProcessCss.tapPromise('my-plugin', async (css, filePath) => {
// Transform CSS after Sass compilation but before it is written to cssOutputFolders
return transformedCss;
});The postProcessCss hook is an AsyncSeriesWaterfallHook that passes the compiled CSS string and source file path through each tap in sequence.