1+ // @ts -check
12'use strict' ;
23
34const fg = require ( 'fast-glob' ) ;
45const fs = require ( 'fs' ) ;
56const postcss = require ( 'postcss' ) ;
6- const removeDuplicatesFromArray = require ( './removeDuplicatesFromArray' ) ;
77
8- let previousGlobsResults = [ ] ;
8+ const classRegexp = / \. ( [ ^ \. \, \s \n \: \( \) \[ \] \' ~ \+ \> \* \\ ] * ) / gim;
9+
910let lastUpdate = null ;
1011let classnamesFromFiles = [ ] ;
1112
13+ /**
14+ * @type {Map<string, {
15+ * timestamp: number;
16+ * classes: ReadonlySet<string>
17+ * }>}
18+ */
19+ const prevFileCache = new Map ( ) ;
20+
21+ const SetHelper = {
22+ /**
23+ *
24+ * @template T
25+ * @param {Set<T> } set
26+ * @param {Iterable<T> } items
27+ */
28+ addMany : ( set , items ) => {
29+ for ( const item of items ) {
30+ set . add ( item ) ;
31+ }
32+ } ,
33+ } ;
34+
1235/**
1336 * Read CSS files and extract classnames
1437 * @param {Array } patterns Glob patterns to locate files
@@ -17,26 +40,74 @@ let classnamesFromFiles = [];
1740 */
1841const generateClassnamesListSync = ( patterns , refreshRate = 5_000 ) => {
1942 const now = new Date ( ) . getTime ( ) ;
20- const files = fg . sync ( patterns , { suppressErrors : true } ) ;
21- const newGlobs = previousGlobsResults . flat ( ) . join ( ',' ) != files . flat ( ) . join ( ',' ) ;
2243 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 ) {
44+
45+ if ( ! expired ) {
46+ return classnamesFromFiles ;
47+ }
48+ const files = fg . sync ( patterns , { suppressErrors : true , stats : true } ) ;
49+ lastUpdate = now ;
50+
51+ /**
52+ * @type {Set<string> }
53+ */
54+ const detectedClassnames = new Set ( ) ;
55+ /**
56+ * @type {Set<string> }
57+ */
58+ const filesSet = new Set ( ) ;
59+ for ( const { path : file , stats } of files ) {
60+ const prevData = prevFileCache . get ( file ) ;
61+ const timestamp = stats ?. mtimeMs ;
62+ /**
63+ * @type {ReadonlySet<string> }
64+ */
65+ let classes ;
66+ // file is not changed -> we do need to do extra work
67+ if ( prevData && prevData ?. timestamp === timestamp ) {
68+ classes = prevData . classes ;
69+ } else {
70+ /**
71+ * @type {Set<string> }
72+ */
73+ const curClasses = new Set ( ) ;
2874 const data = fs . readFileSync ( file , 'utf-8' ) ;
2975 const root = postcss . parse ( data ) ;
3076 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 ) ;
77+ for ( const match of rule . selector . matchAll ( classRegexp ) ) {
78+ curClasses . add ( match [ 1 ] ) ;
79+ }
3580 } ) ;
36- detectedClassnames = removeDuplicatesFromArray ( detectedClassnames ) ;
81+
82+ classes = curClasses ;
83+
84+ if ( timestamp ) {
85+ prevFileCache . set ( file , {
86+ classes,
87+ timestamp,
88+ } ) ;
89+ }
90+ }
91+
92+ SetHelper . addMany ( detectedClassnames , classes ) ;
93+ filesSet . add ( file ) ;
94+ }
95+ // avoiding memory leak
96+ {
97+ /**
98+ * @type {string[] }
99+ */
100+ const keysToDelete = [ ] ;
101+ for ( const cachedFilePath of prevFileCache . keys ( ) ) {
102+ if ( ! filesSet . has ( cachedFilePath ) ) {
103+ keysToDelete . push ( cachedFilePath ) ;
104+ }
105+ }
106+ for ( const key of keysToDelete ) {
107+ prevFileCache . delete ( key ) ;
37108 }
38- classnamesFromFiles = detectedClassnames ;
39109 }
110+ classnamesFromFiles = [ ...detectedClassnames ] ;
40111 return classnamesFromFiles ;
41112} ;
42113
0 commit comments