@@ -5,7 +5,7 @@ import * as path from 'path';
5
5
import { render , Result , SassError } from 'node-sass' ;
6
6
import * as postcss from 'postcss' ;
7
7
import cssModules from 'postcss-modules' ;
8
- import { FileSystem , LegacyAdapters } from '@rushstack/node-core-library' ;
8
+ import { FileSystem , LegacyAdapters , Sort } from '@rushstack/node-core-library' ;
9
9
import { IStringValueTypings , StringValuesTypingsGenerator } from '@rushstack/typings-generator' ;
10
10
11
11
/**
@@ -42,10 +42,18 @@ export interface ISassConfiguration {
42
42
43
43
/**
44
44
* Files with these extensions will pass through the Sass transpiler for typings generation.
45
+ * They will be treated as SCSS modules.
45
46
* Defaults to [".sass", ".scss", ".css"]
46
47
*/
47
48
fileExtensions ?: string [ ] ;
48
49
50
+ /**
51
+ * Files with these extensions will pass through the Sass transpiler for typings generation.
52
+ * They will be treated as non-module SCSS.
53
+ * Defaults to [".global.sass", ".global.scss", ".global.css"]
54
+ */
55
+ nonModuleFileExtensions ?: string [ ] ;
56
+
49
57
/**
50
58
* A list of paths used when resolving Sass imports.
51
59
* The paths should be relative to the project root.
@@ -88,7 +96,9 @@ export class SassProcessor extends StringValuesTypingsGenerator {
88
96
const exportAsDefault : boolean =
89
97
sassConfiguration . exportAsDefault === undefined ? true : sassConfiguration . exportAsDefault ;
90
98
const exportAsDefaultInterfaceName : string = 'IExportStyles' ;
91
- const fileExtensions : string [ ] = sassConfiguration . fileExtensions || [ '.sass' , '.scss' , '.css' ] ;
99
+
100
+ const { allFileExtensions, isFileModule } = buildExtensionClassifier ( sassConfiguration ) ;
101
+
92
102
const { cssOutputFolders } = sassConfiguration ;
93
103
94
104
const getCssPaths : ( ( relativePath : string ) => string [ ] ) | undefined = cssOutputFolders
@@ -105,7 +115,7 @@ export class SassProcessor extends StringValuesTypingsGenerator {
105
115
generatedTsFolder,
106
116
exportAsDefault,
107
117
exportAsDefaultInterfaceName,
108
- fileExtensions,
118
+ fileExtensions : allFileExtensions ,
109
119
filesToIgnore : sassConfiguration . excludeFiles ,
110
120
secondaryGeneratedTsFolders : sassConfiguration . secondaryGeneratedTsFolders ,
111
121
@@ -118,6 +128,8 @@ export class SassProcessor extends StringValuesTypingsGenerator {
118
128
return ;
119
129
}
120
130
131
+ const isModule : boolean = isFileModule ( relativePath ) ;
132
+
121
133
const css : string = await this . _transpileSassAsync (
122
134
fileContents ,
123
135
filePath ,
@@ -126,16 +138,20 @@ export class SassProcessor extends StringValuesTypingsGenerator {
126
138
) ;
127
139
128
140
let classMap : IClassMap = { } ;
129
- const cssModulesClassMapPlugin : postcss . Plugin = cssModules ( {
130
- getJSON : ( cssFileName : string , json : IClassMap ) => {
131
- // This callback will be invoked during the promise evaluation of the postcss process() function.
132
- classMap = json ;
133
- } ,
134
- // Avoid unnecessary name hashing.
135
- generateScopedName : ( name : string ) => name
136
- } ) ;
137
141
138
- await postcss . default ( [ cssModulesClassMapPlugin ] ) . process ( css , { from : filePath } ) ;
142
+ if ( isModule ) {
143
+ // Not all input files are SCSS modules
144
+ const cssModulesClassMapPlugin : postcss . Plugin = cssModules ( {
145
+ getJSON : ( cssFileName : string , json : IClassMap ) => {
146
+ // This callback will be invoked during the promise evaluation of the postcss process() function.
147
+ classMap = json ;
148
+ } ,
149
+ // Avoid unnecessary name hashing.
150
+ generateScopedName : ( name : string ) => name
151
+ } ) ;
152
+
153
+ await postcss . default ( [ cssModulesClassMapPlugin ] ) . process ( css , { from : filePath } ) ;
154
+ }
139
155
140
156
if ( getCssPaths ) {
141
157
await Promise . all (
@@ -213,3 +229,69 @@ export class SassProcessor extends StringValuesTypingsGenerator {
213
229
return url ;
214
230
}
215
231
}
232
+
233
+ interface IExtensionClassifierResult {
234
+ allFileExtensions : string [ ] ;
235
+ isFileModule : ( relativePath : string ) => boolean ;
236
+ }
237
+
238
+ function buildExtensionClassifier ( sassConfiguration : ISassConfiguration ) : IExtensionClassifierResult {
239
+ const {
240
+ fileExtensions : moduleFileExtensions = [ '.sass' , '.scss' , '.css' ] ,
241
+ nonModuleFileExtensions = [ '.global.sass' , '.global.scss' , '.global.css' ]
242
+ } = sassConfiguration ;
243
+
244
+ const hasModules : boolean = moduleFileExtensions . length > 0 ;
245
+ const hasNonModules : boolean = nonModuleFileExtensions . length > 0 ;
246
+
247
+ if ( ! hasModules ) {
248
+ return {
249
+ allFileExtensions : nonModuleFileExtensions ,
250
+ isFileModule : ( relativePath : string ) => false
251
+ } ;
252
+ }
253
+ if ( ! hasNonModules ) {
254
+ return {
255
+ allFileExtensions : moduleFileExtensions ,
256
+ isFileModule : ( relativePath : string ) => true
257
+ } ;
258
+ }
259
+
260
+ const extensionClassifier : Map < string , boolean > = new Map ( ) ;
261
+ for ( const extension of moduleFileExtensions ) {
262
+ const normalizedExtension : string = extension . startsWith ( '.' ) ? extension : `.${ extension } ` ;
263
+ extensionClassifier . set ( normalizedExtension , true ) ;
264
+ }
265
+
266
+ for ( const extension of nonModuleFileExtensions ) {
267
+ const normalizedExtension : string = extension . startsWith ( '.' ) ? extension : `.${ extension } ` ;
268
+ const existingClassification : boolean | undefined = extensionClassifier . get ( normalizedExtension ) ;
269
+ if ( existingClassification === true ) {
270
+ throw new Error (
271
+ `File extension "${ normalizedExtension } " is declared as both a SCSS module and not an SCSS module.`
272
+ ) ;
273
+ }
274
+ extensionClassifier . set ( normalizedExtension , false ) ;
275
+ }
276
+
277
+ Sort . sortMapKeys ( extensionClassifier , ( key1 , key2 ) => {
278
+ // Order by length, descending, so the longest gets tested first.
279
+ return key2 . length - key1 . length ;
280
+ } ) ;
281
+
282
+ const isFileModule : ( relativePath : string ) => boolean = ( relativePath : string ) => {
283
+ // Naive comparison algorithm. O(E), where E is the number of extensions
284
+ // If performance becomes an issue, switch to using LookupByPath with a reverse iteration order using `.` as the delimiter
285
+ for ( const [ extension , isExtensionModule ] of extensionClassifier ) {
286
+ if ( relativePath . endsWith ( extension ) ) {
287
+ return isExtensionModule ;
288
+ }
289
+ }
290
+ throw new Error ( `Could not classify ${ relativePath } as a SCSS module / not an SCSS module` ) ;
291
+ } ;
292
+
293
+ return {
294
+ allFileExtensions : [ ...extensionClassifier . keys ( ) ] ,
295
+ isFileModule
296
+ } ;
297
+ }
0 commit comments