33const fg = require ( 'fast-glob' ) ;
44const fs = require ( 'fs' ) ;
55const postcss = require ( 'postcss' ) ;
6- const removeDuplicatesFromArray = require ( './removeDuplicatesFromArray' ) ;
76
8- let previousGlobsResults = [ ] ;
7+ const classRegexp = / \. ( [ ^ \. \, \s \n \: \( \) \[ \] \' ~ \+ \> \* \\ ] * ) / gim;
8+
99let lastUpdate = null ;
1010let classnamesFromFiles = [ ] ;
1111
12+ /**
13+ * @type {Map<string, number }
14+ */
15+ const prevEditedTimestamp = new Map ( )
16+
1217/**
1318 * Read CSS files and extract classnames
1419 * @param {Array } patterns Glob patterns to locate files
@@ -17,27 +22,56 @@ let classnamesFromFiles = [];
1722 */
1823const generateClassnamesListSync = ( patterns , refreshRate = 5_000 ) => {
1924 const now = new Date ( ) . getTime ( ) ;
20- const files = fg . sync ( patterns , { suppressErrors : true } ) ;
21- const newGlobs = previousGlobsResults . flat ( ) . join ( ',' ) != files . flat ( ) . join ( ',' ) ;
2225 const expired = lastUpdate === null || now - lastUpdate > refreshRate ;
23- if ( newGlobs || expired ) {
24- previousGlobsResults = files ;
25- lastUpdate = now ;
26- let detectedClassnames = [ ] ;
27- for ( const file of files ) {
28- const data = fs . readFileSync ( file , 'utf-8' ) ;
29- const root = postcss . parse ( data ) ;
30- root . walkRules ( ( rule ) => {
31- const regexp = / \. ( [ ^ \. \, \s \n \: \( \) \[ \] \' ~ \+ \> \* \\ ] * ) / gim;
32- const matches = [ ...rule . selector . matchAll ( regexp ) ] ;
33- const classnames = matches . map ( ( arr ) => arr [ 1 ] ) ;
34- detectedClassnames . push ( ...classnames ) ;
35- } ) ;
36- detectedClassnames = removeDuplicatesFromArray ( detectedClassnames ) ;
26+
27+ if ( ! expired ) {
28+ return classnamesFromFiles ;
29+ }
30+ const files = fg . sync ( patterns , { suppressErrors : true , stats : true } ) ;
31+ lastUpdate = now ;
32+
33+ /**
34+ * @type {Set<string }
35+ */
36+ const detectedClassnames = new Set ( ) ;
37+ /**
38+ * @type {Set<string> }
39+ */
40+ const filesSet = new Set ( ) ;
41+ for ( const { path : file , stats } of files ) {
42+ filesSet . add ( file ) ;
43+ if ( ! stats ) { }
44+ // file is not changed -> we do need to do extra work
45+ else if ( prevEditedTimestamp . get ( file ) === stats . mtimeMs ) {
46+ continue ;
47+ } else {
48+ prevEditedTimestamp . set ( file , stats . mtimeMs ) ;
3749 }
38- classnamesFromFiles = detectedClassnames ;
50+ const data = fs . readFileSync ( file , 'utf-8' ) ;
51+ const root = postcss . parse ( data ) ;
52+ root . walkRules ( ( rule ) => {
53+ for ( const match of rule . selector . matchAll ( classRegexp ) ) {
54+ detectedClassnames . add ( match [ 1 ] ) ;
55+ }
56+ } ) ;
3957 }
40- return classnamesFromFiles ;
58+ // avoiding memory leak
59+ {
60+ /**
61+ * @type {string[] }
62+ */
63+ const keysToDelete = [ ]
64+ for ( const cachedFilePath of prevEditedTimestamp . keys ( ) ) {
65+ if ( ! filesSet . has ( cachedFilePath ) ) {
66+ keysToDelete . push ( cachedFilePath ) ;
67+ }
68+ }
69+ for ( const key of keysToDelete ) {
70+ prevEditedTimestamp . delete ( key ) ;
71+ }
72+ }
73+ classnamesFromFiles = [ ...detectedClassnames ] ;
74+ return classnamesFromFiles
4175} ;
4276
4377module . exports = generateClassnamesListSync ;
0 commit comments