1
1
import { createPlugin , Plugin , utils } from 'stylelint' ;
2
+ import { basename } from 'path' ;
2
3
import {
3
4
AtRule ,
4
5
atRule ,
@@ -13,8 +14,7 @@ import {
13
14
const ruleName = 'material/theme-mixin-api' ;
14
15
15
16
/** Regular expression that matches all theme mixins. */
16
- const themeMixinRegex =
17
- / ^ (?: ( m a t - .+ ) - ( d e n s i t y ) | ( m a t - .+ ) - ( d e n s i t y | c o l o r | t y p o g r a p h y | t h e m e ) ) \( ( .* ) \) $ / ;
17
+ const themeMixinRegex = / ^ ( d e n s i t y | c o l o r | t y p o g r a p h y | t h e m e ) \( ( .* ) \) $ / ;
18
18
19
19
/**
20
20
* Stylelint plugin which ensures that theme mixins have a consistent API. Besides
@@ -31,13 +31,14 @@ const themeMixinRegex =
31
31
*/
32
32
const plugin = ( isEnabled : boolean , _options : never , context : { fix : boolean } ) => {
33
33
return ( root : Root , result : Result ) => {
34
- if ( ! isEnabled ) {
34
+ const componentName = getComponentNameFromPath ( root . source ! . input . file ! ) ;
35
+
36
+ if ( ! componentName || ! isEnabled ) {
35
37
return ;
36
38
}
37
39
38
40
root . walkAtRules ( 'mixin' , node => {
39
- if ( node . params . startsWith ( '_' ) || node . params . startsWith ( 'mat-private-' ) ||
40
- node . params . startsWith ( 'mat-mdc-private-' ) ) {
41
+ if ( node . params . startsWith ( '_' ) || node . params . startsWith ( 'private-' ) ) {
41
42
// This is a private mixins that isn't intended to be consumed outside of our own code.
42
43
return ;
43
44
}
@@ -47,24 +48,22 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
47
48
return ;
48
49
}
49
50
50
- // Name of the component with prefix. e.g. `mat-mdc-button` or `mat-slide-toggle`.
51
- const componentName = matches [ 1 ] || matches [ 3 ] ;
52
51
// Type of the theme mixin. e.g. `density`, `color`, `theme`.
53
- const type = matches [ 2 ] || matches [ 4 ] ;
52
+ const type = matches [ 1 ] ;
54
53
// Naively assumes that mixin arguments can be easily retrieved by splitting based on
55
54
// a comma. This is not always correct because Sass maps can be constructed in parameters.
56
55
// These would contain commas that throw of the argument retrieval. It's acceptable that
57
56
// this rule will fail in such edge-cases. There is no AST for `postcss.AtRule` params.
58
- const args = matches [ 5 ] . split ( ',' ) ;
57
+ const args = matches [ 2 ] . split ( ',' ) . map ( arg => arg . trim ( ) ) ;
59
58
60
59
if ( type === 'theme' ) {
61
- validateThemeMixin ( node , componentName , args ) ;
60
+ validateThemeMixin ( node , args ) ;
62
61
} else {
63
62
validateIndividualSystemMixins ( node , type , args ) ;
64
63
}
65
64
} ) ;
66
65
67
- function validateThemeMixin ( node : AtRule , componentName : string , args : string [ ] ) {
66
+ function validateThemeMixin ( node : AtRule , args : string [ ] ) {
68
67
if ( args . length !== 1 ) {
69
68
reportError ( node , 'Expected theme mixin to only declare a single argument.' ) ;
70
69
} else if ( args [ 0 ] !== '$theme-or-color-config' ) {
@@ -76,9 +75,9 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
76
75
}
77
76
78
77
const themePropName = `$theme` ;
79
- const legacyColorExtractExpr = `mat- private-legacy-get-theme($theme-or-color-config)` ;
78
+ const legacyColorExtractExpr = `theming. private-legacy-get-theme($theme-or-color-config)` ;
80
79
const duplicateStylesCheckExpr =
81
- `mat- private-check-duplicate-theme-styles(${ themePropName } , '${ componentName } ')` ;
80
+ `theming. private-check-duplicate-theme-styles(${ themePropName } , '${ componentName } ')` ;
82
81
83
82
let legacyConfigDecl : Declaration | null = null ;
84
83
let duplicateStylesCheck : AtRule | null = null ;
@@ -155,14 +154,13 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
155
154
}
156
155
157
156
const expectedProperty = type === 'density' ? '$density-scale' : '$config' ;
158
- const expectedValues = [ `mat-get-${ type } -config($config-or-theme)` ] ;
159
- if ( type === 'typography' ) {
160
- expectedValues . shift ( ) ;
161
- expectedValues . unshift (
162
- 'mat-private-typography-to-2014-config(mat-get-typography-config($config-or-theme))' ,
163
- 'mat-get-typography-config($config-or-theme)' ,
164
- ) ;
165
- }
157
+ const expectedValues = type === 'typography' ?
158
+ [
159
+ 'typography.private-typography-to-2014-config(' +
160
+ 'theming.get-typography-config($config-or-theme))' ,
161
+ 'theming.get-typography-config($config-or-theme)'
162
+ ] :
163
+ [ `theming.get-${ type } -config($config-or-theme)` ] ;
166
164
let configExtractionNode : Declaration | null = null ;
167
165
let nonCommentNodeCount = 0 ;
168
166
@@ -172,7 +170,8 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
172
170
nonCommentNodeCount ++ ;
173
171
}
174
172
175
- if ( currentNode . type === 'decl' && expectedValues . includes ( currentNode . value ) ) {
173
+ if ( currentNode . type === 'decl' &&
174
+ expectedValues . includes ( stripNewlinesAndIndentation ( currentNode . value ) ) ) {
176
175
configExtractionNode = currentNode ;
177
176
break ;
178
177
}
@@ -204,6 +203,30 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
204
203
} ;
205
204
} ;
206
205
206
+ /** Figures out the name of the component from a file path. */
207
+ function getComponentNameFromPath ( filePath : string ) : string | null {
208
+ const match = basename ( filePath ) . match ( / _ ? ( .* ) - t h e m e \. s c s s $ / ) ;
209
+
210
+ if ( ! match ) {
211
+ return null ;
212
+ }
213
+
214
+ let prefix = '' ;
215
+
216
+ if ( filePath . includes ( 'material-experimental' ) ) {
217
+ prefix = 'mat-mdc-' ;
218
+ } else if ( filePath . includes ( 'material' ) ) {
219
+ prefix = 'mat-' ;
220
+ }
221
+
222
+ return prefix + match [ 1 ] ;
223
+ }
224
+
225
+ /** Strips newlines from a string and any whitespace immediately after it. */
226
+ function stripNewlinesAndIndentation ( value : string ) : string {
227
+ return value . replace ( / ( \r | \n ) \s + / g, '' ) ;
228
+ }
229
+
207
230
// Note: We need to cast the value explicitly to `Plugin` because the stylelint types
208
231
// do not type the context parameter. https://stylelint.io/developer-guide/rules#add-autofix
209
232
module . exports = createPlugin ( ruleName , plugin as Plugin ) ;
0 commit comments