Skip to content

Commit 3b568bb

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 3b568bb

3 files changed

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