import * as path from 'path' ; import { render , Result , SassError } from 'node-sass' ; import postcss from 'postcss' ; import cssModules from 'postcss-modules' ; import { FileSystem } from '@rushstack/node-core-library' ; import { IStringValueTypings , StringValuesTypingsGenerator } from '@rushstack/typings-generator' ; public export interface ISassConfiguration { /**
     * Source code root directory.
     * Defaults to "src/".
     */ srcFolder ? : string ; /**
     * Output directory for generated Sass typings.
     * Defaults to "temp/sass-ts/".
     */ generatedTsFolder ? : string ; /**
     * Output directories for compiled CSS
     */ cssOutputFolders ? : string [ ] | undefined ; /**
     * Determines if export values are wrapped in a default property, or not.
     * Defaults to true.
     */ exportAsDefault ? : boolean ; /**
     * Files with these extensions will pass through the Sass transpiler for typings generation.
     * Defaults to [".sass", ".scss", ".css"]
     */ fileExtensions ? : string [ ] ; /**
     * A list of paths used when resolving Sass imports.
     * The paths should be relative to the project root.
     * Defaults to ["node_modules", "src"]
     */ importIncludePaths ? : string [ ] ; /**
     * A list of file paths relative to the "src" folder that should be excluded from typings generation.
     */ excludeFiles ? : string [ ] ; } public export interface ISassTypingsGeneratorOptions { buildFolder : string ; sassConfiguration : ISassConfiguration ; } interface IClassMap { [ className ] : string ; } interface ICssModulesOptions { getJSON ( cssFileName , json ) : void ; generateScopeName ( name ) : string ; } type TCssModules = ( options : ICssModulesOptions ) => TCssModules ; public export class SassTypingsGenerator extends StringValuesTypingsGenerator { param buildFolder public constructor ( options ) { const { buildFolder , sassConfiguration } = options ; const srcFolder = sassConfiguration . srcFolder || path . join ( buildFolder , 'src' ) ; const generatedTsFolder = sassConfiguration . generatedTsFolder || path . join ( buildFolder , 'temp' , 'sass-ts' ) ; const exportAsDefault = sassConfiguration . exportAsDefault === undefined ? true : sassConfiguration . exportAsDefault ; const exportAsDefaultInterfaceName = 'IExportStyles' ; const fileExtensions = sassConfiguration . fileExtensions || [ '.sass' , '.scss' , '.css' ] ; const { cssOutputFolders } = sassConfiguration ; const getCssPaths = cssOutputFolders ? ( relativePath ) : string [ ] => { return cssOutputFolders . map ( ( folder ) => `${ folder }/${ relativePath . endsWith ( '.css' ) ? relativePath : `${ relativePath }.css` }` ) ; } : undefined ; super ( { srcFolder , generatedTsFolder , exportAsDefault , exportAsDefaultInterfaceName , fileExtensions , filesToIgnore : sassConfiguration . excludeFiles , getAdditionalOutputFiles : getCssPaths , parseAndGenerateTypings : async ( fileContents , filePath , relativePath ) => { if ( this . _isSassPartial ( filePath ) ) { return ; } const css = await this . _transpileSassAsync ( fileContents , filePath , buildFolder , sassConfiguration . importIncludePaths ) ; let classMap = { } ; const cssModulesClassMapPlugin = cssModules ( { getJSON : ( cssFileName , json ) => { classMap = json ; } , generateScopedName : ( name ) => name } ) ; await postcss ( [ cssModulesClassMapPlugin ] ) . process ( css , { from : filePath } ) ; if ( getCssPaths ) { await Promise . all ( getCssPaths ( relativePath ) . map ( async ( cssFile ) => { await FileSystem . writeFileAsync ( cssFile , css , { ensureFolderExists : true } ) ; } ) ) ; } const sortedClassNames = Object . keys ( classMap ) . sort ( ) ; const sassTypings = { typings : sortedClassNames . map ( ( exportName ) => { return { exportName } ; } ) } ; return sassTypings ; } } ) ; } /**
     * Sass partial files are snippets of CSS meant to be included in other Sass files.
     * Partial filenames always begin with a leading underscore and do not produce a CSS output file.
     */ private _isSassPartial ( filePath ) { return path . basename ( filePath ) [ 0 ] === '_' ; } private async _transpileSassAsync ( fileContents , filePath , buildFolder , importIncludePaths ) { const result = await new Promise ( ( resolve , reject ) => { render ( { data : fileContents , file : filePath , importer : ( url ) => ( { file : this . _patchSassUrl ( url ) } ) , includePaths : importIncludePaths ? importIncludePaths : [ path . join ( buildFolder , 'node_modules' ) , path . join ( buildFolder , 'src' ) ] , indentedSyntax : path . extname ( filePath ) . toLowerCase ( ) === '.sass' } , ( err , result ) => { if ( err ) { return reject ( new Error ( `${ err . file }(${ err . column },${ err . line }): ${ err . message }` ) ) ; } resolve ( result ) ; } ) ; } ) ; for ( const dependency of result . stats . includedFiles ) { this . registerDependency ( filePath , dependency ) ; } return result . css . toString ( ) ; } private _patchSassUrl ( url ) { if ( url [ 0 ] === '~' ) { return 'node_modules/' + url . slice ( 1 ) ; } return url ; } }