|
1 | 1 | # @rushstack/heft-sass-plugin |
2 | 2 |
|
3 | | -This is a Heft plugin for using sass-embedded during the "build" stage. |
4 | | -If `sass-embedded` is not supported on your platform, you can override the dependency via npm alias to use the `sass` package instead. |
| 3 | +A [Heft](https://heft.rushstack.io/) plugin that compiles SCSS/Sass files during the build phase. It uses [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) under the hood and produces: |
| 4 | + |
| 5 | +- **TypeScript type definitions** (`.d.ts`) for CSS modules, giving you typed access to class names and `:export` values |
| 6 | +- **Compiled CSS files** (optional) in one or more output folders |
| 7 | +- **JavaScript shims** (optional) that re-export the CSS for consumption in CommonJS or ESM environments |
| 8 | + |
| 9 | +> If `sass-embedded` is not supported on your platform, you can substitute it with the [`sass`](https://www.npmjs.com/package/sass) package using an npm alias. |
5 | 10 |
|
6 | 11 | ## Links |
7 | 12 |
|
8 | | -- [CHANGELOG.md]( |
9 | | - https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-sass-plugin/CHANGELOG.md) - Find |
10 | | - out what's new in the latest version |
| 13 | +- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-sass-plugin/CHANGELOG.md) - Find out what's new in the latest version |
11 | 14 |
|
12 | 15 | Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. |
| 16 | + |
| 17 | +## Setup |
| 18 | + |
| 19 | +### 1. Add the plugin to your project |
| 20 | + |
| 21 | +In your project's `package.json`: |
| 22 | + |
| 23 | +```json |
| 24 | +{ |
| 25 | + "devDependencies": { |
| 26 | + "@rushstack/heft": "...", |
| 27 | + "@rushstack/heft-sass-plugin": "..." |
| 28 | + } |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +### 2. Register the plugin in `config/heft.json` |
| 33 | + |
| 34 | +The `sass` task must run before `typescript` so that the generated `.d.ts` files are available when TypeScript compiles your project. |
| 35 | + |
| 36 | +```json |
| 37 | +{ |
| 38 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", |
| 39 | + "phasesByName": { |
| 40 | + "build": { |
| 41 | + "tasksByName": { |
| 42 | + "sass": { |
| 43 | + "taskPlugin": { |
| 44 | + "pluginPackage": "@rushstack/heft-sass-plugin" |
| 45 | + } |
| 46 | + }, |
| 47 | + |
| 48 | + "typescript": { |
| 49 | + "taskDependencies": ["sass"], |
| 50 | + "taskPlugin": { |
| 51 | + "pluginPackage": "@rushstack/heft-typescript-plugin" |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +### 3. Create `config/sass.json` |
| 61 | + |
| 62 | +A minimal config uses all defaults: |
| 63 | + |
| 64 | +```json |
| 65 | +{ |
| 66 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json" |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +A more complete setup that emits CSS and shims for both ESM and CommonJS: |
| 71 | + |
| 72 | +```json |
| 73 | +{ |
| 74 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json", |
| 75 | + "cssOutputFolders": [ |
| 76 | + { "folder": "lib-esm", "shimModuleFormat": "esnext" }, |
| 77 | + { "folder": "lib-commonjs", "shimModuleFormat": "commonjs" } |
| 78 | + ], |
| 79 | + "fileExtensions": [".module.scss", ".module.sass"], |
| 80 | + "nonModuleFileExtensions": [".global.scss", ".global.sass"], |
| 81 | + "silenceDeprecations": ["mixed-decls", "import", "global-builtin", "color-functions"] |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### 4. Add generated files to `tsconfig.json` |
| 86 | + |
| 87 | +Point TypeScript at the generated type definitions by including the `generatedTsFolder` in your `tsconfig.json`: |
| 88 | + |
| 89 | +```json |
| 90 | +{ |
| 91 | + "compilerOptions": { |
| 92 | + "paths": {} |
| 93 | + }, |
| 94 | + "include": ["src", "temp/sass-ts"] |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +## CSS Modules vs. global stylesheets |
| 99 | + |
| 100 | +The plugin distinguishes between two kinds of files based on their extension: |
| 101 | + |
| 102 | +**CSS modules** (extensions listed in `fileExtensions`, default: `.sass`, `.scss`, `.css`): |
| 103 | +- Processed with [`postcss-modules`](https://www.npmjs.com/package/postcss-modules) |
| 104 | +- Class names and `:export` values become properties in a generated TypeScript interface |
| 105 | +- The generated `.d.ts` exports a typed `styles` object as its default export |
| 106 | + |
| 107 | +**Global stylesheets** (extensions listed in `nonModuleFileExtensions`, default: `.global.sass`, `.global.scss`, `.global.css`): |
| 108 | +- Compiled to plain CSS with no module scoping |
| 109 | +- The generated `.d.ts` is a side-effect-only module (`export {}`) |
| 110 | +- Useful for resets, themes, and base styles |
| 111 | + |
| 112 | +**Partials** (filenames starting with `_`): |
| 113 | +- Never compiled to output files; they are only meant to be `@use`d or `@forward`ed by other files |
| 114 | + |
| 115 | +### Example: CSS module |
| 116 | + |
| 117 | +```scss |
| 118 | +// src/Button.module.scss |
| 119 | +.root { |
| 120 | + background: blue; |
| 121 | +} |
| 122 | +.label { |
| 123 | + font-size: 14px; |
| 124 | +} |
| 125 | +:export { |
| 126 | + brandColor: #0078d4; |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +Generated `temp/sass-ts/Button.module.scss.d.ts`: |
| 131 | + |
| 132 | +```typescript |
| 133 | +interface IStyles { |
| 134 | + root: string; |
| 135 | + label: string; |
| 136 | + brandColor: string; |
| 137 | +} |
| 138 | +declare const styles: IStyles; |
| 139 | +export default styles; |
| 140 | +``` |
| 141 | + |
| 142 | +In your TypeScript source: |
| 143 | + |
| 144 | +```typescript |
| 145 | +import styles from './Button.module.scss'; |
| 146 | +// styles.root, styles.label, styles.brandColor are all typed strings |
| 147 | +``` |
| 148 | + |
| 149 | +## Configuration reference |
| 150 | + |
| 151 | +All options are set in `config/sass.json`. Every option is optional. |
| 152 | + |
| 153 | +| Option | Default | Description | |
| 154 | +|---|---|---| |
| 155 | +| `srcFolder` | `"src/"` | Root directory that is scanned for SCSS files | |
| 156 | +| `generatedTsFolder` | `"temp/sass-ts/"` | Output directory for generated `.d.ts` files | |
| 157 | +| `secondaryGeneratedTsFolders` | `[]` | Additional directories to also write `.d.ts` files to (e.g. `"lib-esm"` when publishing typings alongside compiled output) | |
| 158 | +| `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`. | |
| 159 | +| `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). | |
| 160 | +| `fileExtensions` | `[".sass", ".scss", ".css"]` | File extensions to treat as CSS modules | |
| 161 | +| `nonModuleFileExtensions` | `[".global.sass", ".global.scss", ".global.css"]` | File extensions to treat as global (non-module) stylesheets | |
| 162 | +| `excludeFiles` | `[]` | Paths relative to `srcFolder` to skip entirely | |
| 163 | +| `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. | |
| 164 | +| `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`. | |
| 165 | +| `silenceDeprecations` | `[]` | List of Sass deprecation codes to suppress (e.g. `"mixed-decls"`, `"import"`, `"global-builtin"`, `"color-functions"`) | |
| 166 | +| `ignoreDeprecationsInDependencies` | `false` | Suppresses deprecation warnings that originate from `node_modules` dependencies | |
| 167 | +| `extends` | _(none)_ | Path to another `sass.json` config file to inherit settings from | |
| 168 | + |
| 169 | +### CSS output folders and JS shims |
| 170 | + |
| 171 | +Each entry in `cssOutputFolders` can be a plain string (folder path only) or an object: |
| 172 | + |
| 173 | +```json |
| 174 | +{ |
| 175 | + "folder": "lib-esm", |
| 176 | + "shimModuleFormat": "esnext" |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +When `shimModuleFormat` is set, the plugin writes a `.js` shim alongside each `.css` file. For a CSS module, the shim re-exports the CSS: |
| 181 | + |
| 182 | +```js |
| 183 | +// ESM shim (shimModuleFormat: "esnext") |
| 184 | +export { default } from "./Button.module.css"; |
| 185 | + |
| 186 | +// CommonJS shim (shimModuleFormat: "commonjs") |
| 187 | +module.exports = require("./Button.module.css"); |
| 188 | +module.exports.default = module.exports; |
| 189 | +``` |
| 190 | + |
| 191 | +For a global stylesheet, the shim is a side-effect-only import: |
| 192 | + |
| 193 | +```js |
| 194 | +// ESM shim |
| 195 | +import "./global.global.css"; |
| 196 | +export {}; |
| 197 | + |
| 198 | +// CommonJS shim |
| 199 | +require("./global.global.css"); |
| 200 | +``` |
| 201 | + |
| 202 | +## Sass import resolution |
| 203 | + |
| 204 | +The plugin supports the modern `pkg:` protocol for importing from npm packages: |
| 205 | + |
| 206 | +```scss |
| 207 | +@use "pkg:@fluentui/react/dist/sass/variables"; |
| 208 | +``` |
| 209 | + |
| 210 | +The legacy `~` prefix is automatically converted to `pkg:` for compatibility with older stylesheets: |
| 211 | + |
| 212 | +```scss |
| 213 | +// These are equivalent: |
| 214 | +@use "~@fluentui/react/dist/sass/variables"; |
| 215 | +@use "pkg:@fluentui/react/dist/sass/variables"; |
| 216 | +``` |
| 217 | + |
| 218 | +## Incremental builds |
| 219 | + |
| 220 | +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. |
| 221 | + |
| 222 | +## Plugin accessor API |
| 223 | + |
| 224 | +Other Heft plugins can hook into the Sass compilation pipeline via the `ISassPluginAccessor` interface: |
| 225 | + |
| 226 | +```typescript |
| 227 | +import { ISassPluginAccessor } from '@rushstack/heft-sass-plugin'; |
| 228 | + |
| 229 | +// In your plugin's apply() method: |
| 230 | +const sassAccessor = session.requestAccessToPlugin<ISassPluginAccessor>( |
| 231 | + '@rushstack/heft-sass-plugin', |
| 232 | + 'sass-plugin', |
| 233 | + '@rushstack/heft-sass-plugin' |
| 234 | +); |
| 235 | + |
| 236 | +sassAccessor.hooks.postProcessCss.tapPromise('my-plugin', async (css, filePath) => { |
| 237 | + // Transform CSS after Sass compilation but before it is written to cssOutputFolders |
| 238 | + return transformedCss; |
| 239 | +}); |
| 240 | +``` |
| 241 | + |
| 242 | +The `postProcessCss` hook is an `AsyncSeriesWaterfallHook` that passes the compiled CSS string and source file path through each tap in sequence. |
0 commit comments