Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/heft-parameter-plugin](./build-tests/heft-parameter-plugin/) | This project contains a Heft plugin that adds a custom parameter to built-in actions |
| [/build-tests/heft-parameter-plugin-test](./build-tests/heft-parameter-plugin-test/) | This project exercises a built-in Heft action with a custom parameter |
| [/build-tests/heft-rspack-everything-test](./build-tests/heft-rspack-everything-test/) | Building this project tests every task and config file for Heft when targeting the web browser runtime using Rspack |
| [/build-tests/heft-sass-doNotTrimOriginalFileExtension-test](./build-tests/heft-sass-doNotTrimOriginalFileExtension-test/) | Tests the doNotTrimOriginalFileExtension option for heft-sass-plugin |
| [/build-tests/heft-sass-test](./build-tests/heft-sass-test/) | This project illustrates a minimal tutorial Heft project targeting the web browser runtime |
| [/build-tests/heft-swc-test](./build-tests/heft-swc-test/) | Building this project tests building with SWC |
| [/build-tests/heft-typescript-composite-test](./build-tests/heft-typescript-composite-test/) | Building this project tests behavior of Heft when the tsconfig.json file uses project references. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib-css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",

"phasesByName": {
"build": {
"cleanFiles": [{ "includeGlobs": ["lib-commonjs", "lib-css", "temp"] }],

"tasksByName": {
"sass": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft-sass-plugin"
}
},
"typescript": {
"taskDependencies": ["sass"],
"taskPlugin": {
"pluginPackage": "@rushstack/heft-typescript-plugin"
}
}
}
},

"test": {
"phaseDependencies": ["build"],
"tasksByName": {
"jest": {
"taskPlugin": {
"pluginPackage": "@rushstack/heft-jest-plugin"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "@rushstack/heft-jest-plugin/includes/jest-shared.config.json",

"roots": ["<rootDir>/lib-commonjs"],
"testMatch": ["<rootDir>/lib-commonjs/**/*.test.{cjs,js}"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json",

"operationSettings": [
{
"operationName": "_phase:build",
"outputFolderNames": ["lib-commonjs", "lib-css", "temp/sass-ts"]
},
{
"operationName": "_phase:test",
"outputFolderNames": ["coverage"]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json",

"cssOutputFolders": ["lib-css"],
"fileExtensions": [".module.scss"],
"nonModuleFileExtensions": [".global.scss"],
"doNotTrimOriginalFileExtension": true,
"silenceDeprecations": ["mixed-decls", "import", "global-builtin", "color-functions"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "heft-sass-doNotTrimOriginalFileExtension-test",
"description": "Tests the doNotTrimOriginalFileExtension option for heft-sass-plugin",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "heft build --clean",
"_phase:build": "heft run --only build -- --clean",
"_phase:test": "heft run --only test -- --clean"
},
"devDependencies": {
"@rushstack/heft": "workspace:*",
"@rushstack/heft-jest-plugin": "workspace:*",
"@rushstack/heft-sass-plugin": "workspace:*",
"@rushstack/heft-typescript-plugin": "workspace:*",
"@types/heft-jest": "1.0.1",
"@types/node": "20.17.19",
"typescript": "~5.8.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Test SCSS module for verifying doNotTrimOriginalFileExtension output.
* Expected output: styles.module.scss.css (not styles.module.css)
*/

.label {
color: royalblue;
}

.container {
padding: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Test non-module SCSS for verifying doNotTrimOriginalFileExtension output.
* Expected output: stylesGlobal.global.scss.css (not stylesGlobal.global.css)
*/

.globalWrapper {
font-size: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SASS No Shims (doNotTrimOriginalFileExtension) styles.module.scss: files 1`] = `
Array [
"styles.module.scss.css",
]
`;

exports[`SASS No Shims (doNotTrimOriginalFileExtension) styles.module.scss: styles.module.scss.css 1`] = `
"/**
* Test SCSS module for verifying doNotTrimOriginalFileExtension output.
* Expected output: styles.module.scss.css (not styles.module.css)
*/
.label {
color: royalblue;
}

.container {
padding: 16px;
}"
`;

exports[`SASS No Shims (doNotTrimOriginalFileExtension) stylesGlobal.global.scss: files 1`] = `
Array [
"stylesGlobal.global.scss.css",
]
`;

exports[`SASS No Shims (doNotTrimOriginalFileExtension) stylesGlobal.global.scss: stylesGlobal.global.scss.css 1`] = `
"/**
* Test non-module SCSS for verifying doNotTrimOriginalFileExtension output.
* Expected output: stylesGlobal.global.scss.css (not stylesGlobal.global.css)
*/
.globalWrapper {
font-size: 16px;
}"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'node:path';
import { validateSnapshots, getScssFiles } from './validateSnapshots';

describe('SASS No Shims (doNotTrimOriginalFileExtension)', () => {
const libFolder: string = path.join(__dirname, '../../lib-css');
getScssFiles().forEach((fileName: string) => {
it(fileName, () => {
validateSnapshots(libFolder, fileName);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/// <reference types="node" />
import * as fs from 'node:fs';
import * as path from 'node:path';

export function getScssFiles(): string[] {
const srcFolder: string = path.join(__dirname, '../../src');
const sourceFiles: string[] = fs
.readdirSync(srcFolder, { withFileTypes: true })
.filter((file: fs.Dirent) => {
const { name } = file;
return file.isFile() && !name.startsWith('_') && (name.endsWith('.sass') || name.endsWith('.scss'));
})
.map((dirent) => dirent.name);
return sourceFiles;
}

export function validateSnapshots(dir: string, fileName: string): void {
const originalExt: string = path.extname(fileName);
const basename: string = path.basename(fileName, originalExt) + '.';
const files: fs.Dirent[] = fs.readdirSync(dir, { withFileTypes: true });
const filteredFiles: fs.Dirent[] = files.filter((file: fs.Dirent) => {
return file.isFile() && file.name.startsWith(basename);
});
expect(filteredFiles.map((x) => x.name)).toMatchSnapshot(`files`);
filteredFiles.forEach((file: fs.Dirent) => {
if (!file.isFile() || !file.name.startsWith(basename)) {
return;
}
const filePath: string = path.join(dir, file.name);
const fileContents: string = fs.readFileSync(filePath, 'utf8');
const normalizedFileContents: string = fileContents.replace(/\r/gm, '');
expect(normalizedFileContents).toMatchSnapshot(`${file.name}`);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "http://json.schemastore.org/tsconfig",

"compilerOptions": {
"outDir": "lib-commonjs",
"rootDir": "src",
"rootDirs": ["src", "temp/sass-ts"],

"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],

"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Add a `doNotTrimOriginalFileExtension` option. When enabled, the original file extension is preserved in the CSS output filename (e.g. `styles.scss` emits `styles.scss.css` instead of `styles.css`)",
"type": "minor",
"packageName": "@rushstack/heft-sass-plugin"
}
],
"packageName": "@rushstack/heft-sass-plugin",
"email": "iclanton@users.noreply.github.com"
}
24 changes: 24 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion heft-plugins/heft-sass-plugin/src/SassPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ISassConfigurationJson {
nonModuleFileExtensions?: string[];
silenceDeprecations?: string[];
excludeFiles?: string[];
doNotTrimOriginalFileExtension?: boolean;
}

const SASS_CONFIGURATION_LOCATION: string = 'config/sass.json';
Expand Down Expand Up @@ -96,7 +97,8 @@ export default class SassPlugin implements IHeftPlugin {
fileExtensions,
nonModuleFileExtensions,
silenceDeprecations,
excludeFiles
excludeFiles,
doNotTrimOriginalFileExtension
} = sassConfigurationJson || {};

function resolveFolder(folder: string): string {
Expand All @@ -123,6 +125,7 @@ export default class SassPlugin implements IHeftPlugin {
};
}),
silenceDeprecations,
doNotTrimOriginalFileExtension,
postProcessCssAsync: hooks.postProcessCss.isUsed()
? async (cssText: string) => hooks.postProcessCss.promise(cssText)
: undefined
Expand Down
39 changes: 31 additions & 8 deletions heft-plugins/heft-sass-plugin/src/SassProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const SIMPLE_IDENTIFIER_REGEX: RegExp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
*/
export interface ICssOutputFolder {
folder: string;
shimModuleFormat: 'commonjs' | 'esnext' | undefined;
shimModuleFormat?: 'commonjs' | 'esnext';
}

/**
Expand Down Expand Up @@ -114,6 +114,13 @@ export interface ISassProcessorOptions {
*/
silenceDeprecations?: readonly string[];

/**
* If true, the original file extension will not be trimmed when generating the output CSS file. The generated CSS
* file will retain its original extension. For example, "styles.scss" will generate "styles.scss.css"
* instead of "styles.css".
*/
doNotTrimOriginalFileExtension?: boolean;

/**
* A callback to further modify the raw CSS text after it has been generated. Only relevant if emitting CSS files.
*/
Expand Down Expand Up @@ -738,8 +745,14 @@ export class SassProcessor {
}

record.cssVersion = contentHash;
const { cssOutputFolders, dtsOutputFolders, srcFolder, exportAsDefault, postProcessCssAsync } =
this._options;
const {
cssOutputFolders,
dtsOutputFolders,
srcFolder,
exportAsDefault,
doNotTrimOriginalFileExtension,
postProcessCssAsync
} = this._options;

// Handle CSS modules
let moduleMap: JsonObject | undefined;
Expand Down Expand Up @@ -779,16 +792,26 @@ export class SassProcessor {
);
}

const filename: string = path.basename(relativeFilePath);
const extensionStart: number = filename.lastIndexOf('.');
const cssPathFromJs: string = `./${filename.slice(0, extensionStart)}.css`;
const relativeCssPath: string = `${relativeFilePath.slice(0, relativeFilePath.lastIndexOf('.'))}.css`;

if (cssOutputFolders && cssOutputFolders.length > 0) {
if (!exportAsDefault) {
throw new Error(`The "cssOutputFolders" option is not supported when "exportAsDefault" is false.`);
}

const filename: string = path.basename(relativeFilePath);
let cssFilename: string;
let relativeCssPath: string;
if (doNotTrimOriginalFileExtension) {
cssFilename = `${filename}.css`;
relativeCssPath = `${relativeFilePath}.css`;
} else {
const extensionStart: number = filename.lastIndexOf('.');
cssFilename = `${filename.slice(0, extensionStart)}.css`;

const relativeFilePathStart: number = relativeFilePath.lastIndexOf('.');
relativeCssPath = `${relativeFilePath.slice(0, relativeFilePathStart)}.css`;
}

const cssPathFromJs: string = `./${cssFilename}`;
for (const cssOutputFolder of cssOutputFolders) {
const { folder, shimModuleFormat } = cssOutputFolder;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
"items": {
"type": "string"
}
},

"doNotTrimOriginalFileExtension": {
"type": "boolean",
"description": "If true, the original file extension will not be trimmed when generating the output CSS file. The generated CSS file will retain its original extension. For example, \"styles.scss\" will generate \"styles.scss.css\" instead of \"styles.css\"."
}
}
}
Loading