88 */
99
1010using System ;
11- using System . Diagnostics ;
1211using System . IO ;
12+ using System . Security . Cryptography ;
13+ using System . Text ;
1314using Newtonsoft . Json ;
1415using React . Exceptions ;
1516
@@ -28,6 +29,10 @@ public class JsxTransformer : IJsxTransformer
2829 /// Suffix to append to compiled files
2930 /// </summary>
3031 private const string COMPILED_FILE_SUFFIX = ".generated.js" ;
32+ /// <summary>
33+ /// Prefix used for hash line in transformed file. Used for caching.
34+ /// </summary>
35+ private const string HASH_PREFIX = "// @hash " ;
3136
3237 /// <summary>
3338 /// Environment this JSX Transformer has been created in
@@ -41,6 +46,10 @@ public class JsxTransformer : IJsxTransformer
4146 /// File system wrapper
4247 /// </summary>
4348 private readonly IFileSystem _fileSystem ;
49+ /// <summary>
50+ /// Althorithm for calculating file hashes
51+ /// </summary>
52+ private readonly HashAlgorithm _hash = MD5 . Create ( ) ;
4453
4554 /// <summary>
4655 /// Initializes a new instance of the <see cref="JsxTransformer"/> class.
@@ -72,29 +81,40 @@ public string TransformJsxFile(string filename)
7281 getData : ( ) =>
7382 {
7483 // 2. Check on-disk cache
75- var cachePath = GetJsxOutputPath ( filename ) ;
76- if ( _fileSystem . FileExists ( cachePath ) )
84+ var contents = _fileSystem . ReadAsString ( filename ) ;
85+ var hash = CalculateHash ( contents ) ;
86+
87+ var cacheFilename = GetJsxOutputPath ( filename ) ;
88+ if ( _fileSystem . FileExists ( cacheFilename ) )
7789 {
78- // TODO: Checksum to ensure file hasn't changed
79- return _fileSystem . ReadAsString ( cachePath ) ;
90+ var cacheContents = _fileSystem . ReadAsString ( cacheFilename ) ;
91+ if ( ValidateHash ( cacheContents , hash ) )
92+ {
93+ // Cache is valid :D
94+ return cacheContents ;
95+ }
8096 }
8197
8298 // 3. Not cached, perform the transformation
83- return TransformJsxFileWithoutCache ( filename ) ;
99+ return TransformJsxWithHeader ( contents , hash ) ;
84100 }
85101 ) ;
86102 }
87103
88104 /// <summary>
89- /// Transforms a JSX file without checking if a cached version exists. For most purposes,
90- /// you'll be better off using <see cref="TransformJsxFile" /> .
105+ /// Transforms JSX into regular JavaScript, and prepends a header used for caching
106+ /// purposes .
91107 /// </summary>
92- /// <param name="filename">Name of the file to transform</param>
108+ /// <param name="contents">Contents of the input file</param>
109+ /// <param name="hash">Hash of the input. If null, it will be calculated</param>
93110 /// <returns>JavaScript</returns>
94- public string TransformJsxFileWithoutCache ( string filename )
111+ private string TransformJsxWithHeader ( string contents , string hash = null )
95112 {
96- var contents = _fileSystem . ReadAsString ( filename ) ;
97- return TransformJsx ( contents ) ;
113+ if ( string . IsNullOrEmpty ( hash ) )
114+ {
115+ hash = CalculateHash ( contents ) ;
116+ }
117+ return GetFileHeader ( hash ) + TransformJsx ( contents ) ;
98118 }
99119
100120 /// <summary>
@@ -127,6 +147,55 @@ public string TransformJsx(string input)
127147 }
128148 }
129149
150+ /// <summary>
151+ /// Calculates a hash for the specified input
152+ /// </summary>
153+ /// <param name="input">Input string</param>
154+ /// <returns>Hash of the input</returns>
155+ private string CalculateHash ( string input )
156+ {
157+ var inputBytes = Encoding . UTF8 . GetBytes ( input ) ;
158+ var hash = _hash . ComputeHash ( inputBytes ) ;
159+ return BitConverter . ToString ( hash ) . Replace ( "-" , string . Empty ) ;
160+ }
161+
162+ /// <summary>
163+ /// Validates that the cache's hash is valid. This is used to ensure the input has not
164+ /// changed, and to invalidate the cache if so.
165+ /// </summary>
166+ /// <param name="cacheContents">Contents retrieved from cache</param>
167+ /// <param name="hash">Hash of the input</param>
168+ /// <returns><c>true</c> if the cache is still valid</returns>
169+ private bool ValidateHash ( string cacheContents , string hash )
170+ {
171+ // Check if first line is hash
172+ var firstLineBreak = cacheContents . IndexOfAny ( new [ ] { '\r ' , '\n ' } ) ;
173+ var firstLine = cacheContents . Substring ( 0 , firstLineBreak ) ;
174+ if ( ! firstLine . StartsWith ( HASH_PREFIX ) )
175+ {
176+ // Cache doesn't have hash - Err on the side of caution and invalidate it.
177+ return false ;
178+ }
179+ var cacheHash = firstLine . Replace ( HASH_PREFIX , string . Empty ) ;
180+ return cacheHash == hash ;
181+ }
182+
183+ /// <summary>
184+ /// Gets the header prepended to JSX transformed files. Contains a hash that is used to
185+ /// validate the cache.
186+ /// </summary>
187+ /// <param name="hash">Hash of the input</param>
188+ /// <returns>Header for the cache</returns>
189+ private string GetFileHeader ( string hash )
190+ {
191+ return string . Format (
192+ @"{0}{1}
193+ // Automatically generated by ReactJS.NET. Do not edit, your changes will be overridden.
194+ // Version: {2}
195+ ///////////////////////////////////////////////////////////////////////////////
196+ " , HASH_PREFIX , hash , _environment . Version ) ;
197+ }
198+
130199 /// <summary>
131200 /// Returns the path the specified JSX file's compilation will be cached to if
132201 /// <see cref="TransformAndSaveJsxFile" /> is called.
@@ -150,7 +219,8 @@ public string GetJsxOutputPath(string path)
150219 public string TransformAndSaveJsxFile ( string filename )
151220 {
152221 var outputPath = GetJsxOutputPath ( filename ) ;
153- var result = TransformJsxFileWithoutCache ( filename ) ;
222+ var contents = _fileSystem . ReadAsString ( filename ) ;
223+ var result = TransformJsxWithHeader ( contents ) ;
154224 _fileSystem . WriteAsString ( outputPath , result ) ;
155225 return outputPath ;
156226 }
0 commit comments