forked from reactjs/React.NET
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathBabel.cs
384 lines (357 loc) · 12.7 KB
/
Babel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Diagnostics;
using System.IO;
using React.Exceptions;
namespace React
{
/// <summary>
/// Handles compiling JavaScript files via Babel (http://babeljs.io/).
/// </summary>
public class Babel : IBabel
{
/// <summary>
/// Cache key for JavaScript compilation
/// </summary>
protected const string JSX_CACHE_KEY = "JSX_v3_{0}";
/// <summary>
/// Suffix to append to compiled files
/// </summary>
protected const string COMPILED_FILE_SUFFIX = ".generated.js";
/// <summary>
/// Suffix to append to source map files
/// </summary>
protected const string SOURE_MAP_FILE_SUFFIX = ".map";
/// <summary>
/// Number of lines in the header prepended to compiled files.
/// </summary>
protected const int LINES_IN_HEADER = 5;
/// <summary>
/// Environment this transformer has been created in
/// </summary>
protected readonly IReactEnvironment _environment;
/// <summary>
/// Cache used for storing compiled JavaScript
/// </summary>
protected readonly ICache _cache;
/// <summary>
/// File system wrapper
/// </summary>
protected readonly IFileSystem _fileSystem;
/// <summary>
/// Hash algorithm for file-based cache
/// </summary>
protected readonly IFileCacheHash _fileCacheHash;
/// <summary>
/// Site-wide configuration
/// </summary>
protected readonly IReactSiteConfiguration _config;
/// <summary>
/// The serialized Babel configuration
/// </summary>
protected readonly string _babelConfig;
/// <summary>
/// Initializes a new instance of the <see cref="Babel"/> class.
/// </summary>
/// <param name="environment">The ReactJS.NET environment</param>
/// <param name="cache">The cache to use for compilation</param>
/// <param name="fileSystem">File system wrapper</param>
/// <param name="fileCacheHash">Hash algorithm for file-based cache</param>
/// <param name="siteConfig">Site-wide configuration</param>
public Babel(IReactEnvironment environment, ICache cache, IFileSystem fileSystem, IFileCacheHash fileCacheHash, IReactSiteConfiguration siteConfig)
{
_environment = environment;
_cache = cache;
_fileSystem = fileSystem;
_fileCacheHash = fileCacheHash;
_config = siteConfig;
_babelConfig = siteConfig.BabelConfig.Serialize(_config.BabelVersion);
}
/// <summary>
/// Transforms a JavaScript file. Results of the transformation are cached.
/// </summary>
/// <param name="filename">Name of the file to load</param>
/// <returns>JavaScript</returns>
public virtual string TransformFile(string filename)
{
return TransformFileWithSourceMap(filename, false).Code;
}
/// <summary>
/// Transforms a JavaScript file via Babel and also returns a source map to map the
/// compiled source to the original version. Results of the transformation are cached.
/// </summary>
/// <param name="filename">Name of the file to load</param>
/// <param name="forceGenerateSourceMap">
/// <c>true</c> to re-transform the file if a cached version with no source map is available
/// </param>
/// <returns>JavaScript and source map</returns>
public virtual JavaScriptWithSourceMap TransformFileWithSourceMap(
string filename,
bool forceGenerateSourceMap = false
)
{
var cacheKey = string.Format(JSX_CACHE_KEY, filename);
// 1. Check in-memory cache. We need to invalidate any in-memory cache if there's no
// source map cached and forceGenerateSourceMap is true.
var cached = _cache.Get<JavaScriptWithSourceMap>(cacheKey);
var cacheIsValid = cached != null && (!forceGenerateSourceMap || cached.SourceMap != null);
if (cacheIsValid)
{
return cached;
}
// 2. Check on-disk cache
var contents = _fileSystem.ReadAsString(filename);
var hash = _fileCacheHash.CalculateHash(contents);
var output = LoadFromFileCache(filename, hash, forceGenerateSourceMap);
if (output == null)
{
// 3. Not cached, perform the transformation
try
{
output = TransformWithHeader(filename, contents, hash);
}
catch (BabelException ex)
{
// Add the filename to the error message
throw new BabelException(string.Format(
"In file \"{0}\": {1}",
filename,
ex.Message
), ex);
}
}
// Cache the result from above (either disk cache or live transformation) to memory
var fullPath = _fileSystem.MapPath(filename);
_cache.Set(
cacheKey,
output,
slidingExpiration: TimeSpan.FromMinutes(30),
cacheDependencyFiles: new[] { fullPath }
);
return output;
}
/// <summary>
/// Loads a transformed JavaScript file from the disk cache. If the cache is invalid or there is
/// no cached version, returns <c>null</c>.
/// </summary>
/// <param name="filename">Name of the file to load</param>
/// /// <param name="hash">Hash of the input file, to validate the cache</param>
/// <param name="forceGenerateSourceMap">
/// <c>true</c> to re-transform the file if a cached version with no source map is available
/// </param>
/// <returns></returns>
protected virtual JavaScriptWithSourceMap LoadFromFileCache(string filename, string hash, bool forceGenerateSourceMap)
{
var cacheFilename = GetOutputPath(filename);
if (!_fileSystem.FileExists(cacheFilename))
{
// Cache file doesn't exist on disk
return null;
}
var cacheContents = _fileSystem.ReadAsString(cacheFilename);
if (!_fileCacheHash.ValidateHash(cacheContents, hash))
{
// Hash of the cache is invalid (file changed since the time the cache was written).
return null;
}
// Cache is valid :D
// See if we have a source map cached alongside the file
SourceMap sourceMap = null;
var sourceMapFilename = GetSourceMapOutputPath(filename);
if (_fileSystem.FileExists(sourceMapFilename))
{
try
{
var sourceMapString = _fileSystem.ReadAsString(sourceMapFilename);
if (!string.IsNullOrEmpty(sourceMapString))
{
sourceMap = SourceMap.FromJson(sourceMapString);
}
}
catch (Exception e)
{
// Just ignore it
Trace.WriteLine("Error reading source map file: " + e.Message);
}
}
// If forceGenerateSourceMap is true, we need to explicitly ignore this cached version
// if there's no source map
if (forceGenerateSourceMap && sourceMap == null)
{
return null;
}
return new JavaScriptWithSourceMap
{
Code = cacheContents,
SourceMap = sourceMap,
Hash = hash,
};
}
/// <summary>
/// Transforms JavaScript via Babel, and prepends a header used for caching
/// purposes.
/// </summary>
/// <param name="filename">Name of the file being transformed</param>
/// <param name="contents">Contents of the input file</param>
/// <param name="hash">Hash of the input. If null, it will be calculated</param>
/// <returns>JavaScript</returns>
protected virtual JavaScriptWithSourceMap TransformWithHeader(
string filename,
string contents,
string hash = null
)
{
var result = TransformWithSourceMap(contents, filename);
if (string.IsNullOrEmpty(hash))
{
hash = _fileCacheHash.CalculateHash(contents);
}
// Prepend header to generated code
var header = GetFileHeader(hash, result.BabelVersion);
result.Code = header + result.Code;
result.Hash = hash;
if (result.SourceMap != null && result.SourceMap.Mappings != null)
{
// Since we prepend a header to the code, the source map no longer matches up exactly
// (it's off by the number of lines in the header). Fix this issue by adding five
// blank lines to the source map. This is kinda hacky but saves us having to load a
// proper source map library. If this ever breaks, I'll replace it with actual proper
// source map modification code (https://gist.github.com/Daniel15/4bdb15836bfd960c2956).
result.SourceMap.Mappings = ";;;;;" + result.SourceMap.Mappings;
}
return result;
}
/// <summary>
/// Transforms JavaScript via Babel. The result is not cached. Use
/// <see cref="TransformFile"/> if loading from a file since this will cache the result.
/// </summary>
/// <param name="input">JavaScript</param>
/// <param name="filename">Name of the file being transformed</param>
/// <returns>JavaScript</returns>
public virtual string Transform(string input, string filename = "unknown")
{
try
{
var output = _environment.ExecuteWithBabel<string>(
"ReactNET_transform",
input,
_babelConfig,
filename
);
return output;
}
catch (Exception ex)
{
throw new BabelException(ex.Message, ex);
}
}
/// <summary>
/// Transforms JavaScript via Babel and also returns a source map to map the compiled
/// source to the original version. The result is not cached.
/// </summary>
/// <param name="input">JavaScript</param>
/// <param name="filename">Name of the file being transformed</param>
/// <returns>JavaScript and source map</returns>
public virtual JavaScriptWithSourceMap TransformWithSourceMap(
string input,
string filename = "unknown"
)
{
try
{
return _environment.ExecuteWithBabel<JavaScriptWithSourceMap>(
"ReactNET_transform_sourcemap",
input,
_babelConfig,
filename
);
}
catch (Exception ex)
{
throw new BabelException(ex.Message, ex);
}
}
/// <summary>
/// Gets the header prepended to transformed files. Contains a hash that is used to
/// validate the cache.
/// </summary>
/// <param name="hash">Hash of the input</param>
/// <param name="babelVersion">Version of Babel used to perform this transformation</param>
/// <returns>Header for the cache</returns>
protected virtual string GetFileHeader(string hash, string babelVersion)
{
return string.Format(
@"{0}
// Automatically generated by ReactJS.NET. Do not edit, your changes will be overridden.
// Version: {1} with Babel {3}
// Generated at: {2}
///////////////////////////////////////////////////////////////////////////////
", _fileCacheHash.AddPrefix(hash), _environment.Version, DateTime.Now, babelVersion);
}
/// <summary>
/// Returns the path the specified file's compilation will be cached to
/// </summary>
/// <param name="path">Path of the input file</param>
/// <returns>Output path of the compiled file</returns>
public virtual string GetOutputPath(string path)
{
return Path.Combine(
Path.GetDirectoryName(path),
Path.GetFileNameWithoutExtension(path) + COMPILED_FILE_SUFFIX
);
}
/// <summary>
/// Returns the path the specified file's source map will be cached to if
/// <see cref="TransformAndSaveFile"/> is called.
/// </summary>
/// <param name="path">Path of the input file</param>
/// <returns>Output path of the source map</returns>
public virtual string GetSourceMapOutputPath(string path)
{
return GetOutputPath(path) + SOURE_MAP_FILE_SUFFIX;
}
/// <summary>
/// Transforms JavaScript via Babel and saves the result into a ".generated.js" file
/// alongside the original file.
/// </summary>
/// <param name="filename">Name of the file to load</param>
/// <returns>File contents</returns>
public virtual string TransformAndSaveFile(
string filename
)
{
var outputPath = GetOutputPath(filename);
var contents = _fileSystem.ReadAsString(filename);
if (CacheIsValid(contents, outputPath))
return outputPath;
var result = TransformWithHeader(filename, contents, null);
var sourceMapPath = GetSourceMapOutputPath(filename);
_fileSystem.WriteAsString(outputPath, result.Code);
_fileSystem.WriteAsString(sourceMapPath, result.SourceMap == null ? string.Empty : result.SourceMap.ToJson());
return outputPath;
}
/// <summary>
/// Checks whether an input file (given as inputFileContents) should be transpiled
/// by calculating the hash and comparing it to the hash value stored
/// in the file given by outputPath. If the outputPath file does not
/// exist the input file should always be transpiled.
/// </summary>
/// <param name="inputFileContents">The contents of the input file.</param>
/// <param name="outputPath">The output path of the (possibly previously) generated file.</param>
/// <returns>Returns false if the file should be transpiled, true otherwise.</returns>
public virtual bool CacheIsValid(string inputFileContents, string outputPath)
{
if (!_fileSystem.FileExists(outputPath))
return false;
var hashForInputFile = _fileCacheHash.CalculateHash(inputFileContents);
var existingOutputContents = _fileSystem.ReadAsString(outputPath);
var fileHasNotChanged = _fileCacheHash.ValidateHash(existingOutputContents, hashForInputFile);
return fileHasNotChanged;
}
}
}