Skip to content

Commit 2d7307b

Browse files
iclantonclaude
andcommitted
[heft-sass-plugin] Improve README and sass.json template
- Rewrote README with setup instructions, configuration reference table, CSS modules vs. global stylesheets explanation, shim generation docs, import resolution notes, and plugin accessor API - Updated sass.json template: removed non-existent preserveSCSSExtension and importIncludePaths options, added missing nonModuleFileExtensions and preserveIcssExports, updated cssOutputFolders example to show shimModuleFormat usage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e540e9b commit 2d7307b

3 files changed

Lines changed: 288 additions & 34 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/heft-sass-plugin",
5+
"comment": "Improve project README.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-sass-plugin"
10+
}
Lines changed: 235 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,242 @@
11
# @rushstack/heft-sass-plugin
22

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.
510
611
## Links
712

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
1114

1215
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.

heft-plugins/heft-sass-plugin/src/templates/sass.json

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*
1111
* To delete an inherited setting, set it to `null` in this file.
1212
*/
13-
// "extends": "base-project/config/serve-command.json",
13+
// "extends": "base-project/config/sass.json",
1414

1515
/**
1616
* The root directory for project source code.
@@ -27,74 +27,88 @@
2727
// "generatedTsFolder": "temp/sass-ts/",
2828

2929
/**
30-
* Optional additional folders to which Sass typings should be output.
30+
* Optional additional folders to which Sass typings should be output. Useful when publishing typings
31+
* alongside compiled output (e.g. "lib-esm").
3132
*/
3233
// "secondaryGeneratedTsFolders": [],
3334

3435
/**
35-
* Determines whether export values are wrapped in a default property, or not.
36+
* Determines whether CSS module exports are wrapped in a typed default interface (true) or emitted as
37+
* individual named exports (false).
38+
*
39+
* Note: setting this to false is incompatible with cssOutputFolders.
3640
*
3741
* Default value: true
3842
*/
3943
// "exportAsDefault": false,
4044

4145
/**
42-
* If specified, folders where compiled CSS files will be emitted to. They will be named by appending
43-
* ".css" to the source file name for ease of reference translation, unless "preserveSCSSExtension" is set.
46+
* If specified, folders where compiled CSS files will be emitted. Each entry is either a folder path string,
47+
* or an object with a "folder" property and an optional "shimModuleFormat" property. When "shimModuleFormat"
48+
* is set to "commonjs" or "esnext", a JavaScript shim file is emitted alongside each CSS file to re-export it
49+
* in the specified module format.
4450
*
4551
* Default value: undefined
4652
*/
47-
// "cssOutputFolders": [],
48-
49-
/**
50-
* If set, when emitting compiled CSS from a file with a ".scss" extension, the emitted CSS will have
51-
* the extension ".scss" instead of ".scss.css".
52-
*
53-
* Default value: false
54-
*/
55-
// "preserveSCSSExtension": true,
53+
// "cssOutputFolders": [
54+
// { "folder": "lib-esm", "shimModuleFormat": "esnext" },
55+
// { "folder": "lib-commonjs", "shimModuleFormat": "commonjs" }
56+
// ],
5657

5758
/**
58-
* Files with these extensions will pass through the Sass transpiler for typings generation.
59+
* Files with these extensions will be treated as CSS modules and pass through the Sass transpiler for
60+
* typings generation and/or CSS emit.
5961
*
6062
* Default value: [".sass", ".scss", ".css"]
6163
*/
62-
// "fileExtensions": [".sass", ".scss"],
64+
// "fileExtensions": [".module.scss", ".module.sass"],
6365

6466
/**
65-
* A list of paths used when resolving Sass imports. The paths should be relative to the project root.
67+
* Files with these extensions will be treated as non-module (global) stylesheets and pass through the Sass
68+
* transpiler for typings generation and/or CSS emit. The generated typings are side-effect-only (export {}).
6669
*
67-
* Default value: ["node_modules", "src"]
70+
* Default value: [".global.sass", ".global.scss", ".global.css"]
6871
*/
69-
// "importIncludePaths": ["node_modules", "src"],
72+
// "nonModuleFileExtensions": [".global.scss", ".global.sass"],
7073

7174
/**
72-
* A list of file paths relative to the "src" folder that should be excluded from typings generation.
75+
* A list of file paths relative to the "src" folder that should be excluded from typings generation
76+
* and/or CSS emit.
7377
*
7478
* Default value: undefined
7579
*/
7680
// "excludeFiles": [],
7781

7882
/**
79-
* If set, deprecation warnings from dependencies will be suppressed.
83+
* If true, the original file extension will not be trimmed when generating the output CSS filename.
84+
* For example, "styles.scss" will generate "styles.scss.css" instead of "styles.css".
8085
*
8186
* Default value: false
8287
*/
83-
// "ignoreDeprecationsInDependencies": true,
88+
// "doNotTrimOriginalFileExtension": true,
8489

8590
/**
86-
* If set, the specified deprecation warnings will be suppressed.
91+
* If true, the ICSS ":export" block will be preserved in the emitted CSS output. This is necessary when
92+
* the CSS is consumed by a webpack loader (e.g. css-loader's icssParser) that extracts ":export" values
93+
* at bundle time. Has no effect on the generated ".d.ts" file.
8794
*
88-
* Default value: []
95+
* Default value: false
8996
*/
90-
// "silenceDeprecations": ["mixed-decls"],
97+
// "preserveIcssExports": true,
9198

9299
/**
93-
* If true, the original file extension will not be trimmed when generating the output CSS file.
94-
* The generated CSS file will retain its original extension. For example, "styles.scss" will generate
95-
* "styles.scss.css" instead of "styles.css".
100+
* If set, deprecation warnings that originate from dependencies will be suppressed.
96101
*
97102
* Default value: false
98103
*/
99-
// "doNotTrimOriginalFileExtension": true
104+
// "ignoreDeprecationsInDependencies": true,
105+
106+
/**
107+
* A list of Sass deprecation codes to silence. Useful for suppressing known warnings from deprecated
108+
* features that are not yet actionable. Common values: "mixed-decls", "import", "global-builtin",
109+
* "color-functions".
110+
*
111+
* Default value: []
112+
*/
113+
// "silenceDeprecations": ["mixed-decls"]
100114
}

0 commit comments

Comments
 (0)