@@ -16,22 +16,53 @@ const FetchError = require('node-fetch/lib/fetch-error');
1616
1717const crypto = require ( 'crypto' ) ;
1818const fetch = require ( 'node-fetch' ) ;
19- const imurmurhash = require ( 'imurmurhash' ) ;
2019const jsonStableStringify = require ( 'json-stable-stringify' ) ;
2120const path = require ( 'path' ) ;
2221
23- import type { Options as TransformOptions } from '../JSTransformer/worker/worker' ;
22+ import type {
23+ Options as TransformWorkerOptions ,
24+ TransformOptions ,
25+ } from '../JSTransformer/worker/worker' ;
2426import type { CachedResult , GetTransformCacheKey } from './TransformCache' ;
2527
28+ /**
29+ * The API that a global transform cache must comply with. To implement a
30+ * custom cache, implement this interface and pass it as argument to the
31+ * application's top-level `Server` class.
32+ */
33+ export type GlobalTransformCache = {
34+ /**
35+ * Synchronously determine if it is worth trying to fetch a result from the
36+ * cache. This can be used, for instance, to exclude sets of options we know
37+ * will never be cached.
38+ */
39+ shouldFetch ( props : FetchProps ) : boolean ,
40+
41+ /**
42+ * Try to fetch a result. It doesn't actually need to fetch from a server,
43+ * the global cache could be instantiated locally for example.
44+ */
45+ fetch ( props : FetchProps ) : Promise < ?CachedResult > ,
46+
47+ /**
48+ * Try to store a result, without waiting for the success or failure of the
49+ * operation. Consequently, the actual storage operation could be done at a
50+ * much later point if desired. It is recommended to actually have this
51+ * function be a no-op in production, and only do the storage operation from
52+ * a script running on your Continuous Integration platform.
53+ */
54+ store ( props : FetchProps , result : CachedResult ) : void ,
55+ } ;
56+
2657type FetchResultURIs = ( keys : Array < string > ) => Promise < Map < string , string >> ;
2758type FetchResultFromURI = ( uri : string ) => Promise < ?CachedResult > ;
2859type StoreResults = ( resultsByKey : Map < string , CachedResult > ) => Promise < void > ;
2960
30- type FetchProps = {
61+ export type FetchProps = {
3162 filePath : string ,
3263 sourceCode : string ,
3364 getTransformCacheKey : GetTransformCacheKey ,
34- transformOptions : TransformOptions ,
65+ transformOptions : TransformWorkerOptions ,
3566} ;
3667
3768type URI = string ;
@@ -98,29 +129,6 @@ class KeyResultStore {
98129
99130}
100131
101- /**
102- * The transform options contain absolute paths. This can contain, for example,
103- * the username if someone works their home directory (very likely). We get rid
104- * of this local data for the global cache, otherwise nobody would share the
105- * same cache keys. The project roots should not be needed as part of the cache
106- * key as they should not affect the transformation of a single particular file.
107- */
108- function globalizeTransformOptions (
109- options : TransformOptions ,
110- ) : TransformOptions {
111- const { transform} = options ;
112- if ( transform == null ) {
113- return options ;
114- }
115- return {
116- ...options ,
117- transform : {
118- ...transform ,
119- projectRoots : [ ] ,
120- } ,
121- } ;
122- }
123-
124132export type TransformProfile = { + dev : boolean , + minify : boolean , + platform : string } ;
125133
126134function profileKey ( { dev, minify, platform} : TransformProfile ) : string {
@@ -177,11 +185,12 @@ function validateCachedResult(cachedResult: mixed): ?CachedResult {
177185 return null;
178186}
179187
180- class GlobalTransformCache {
188+ class URIBasedGlobalTransformCache {
181189
182190 _fetcher : KeyURIFetcher ;
183191 _fetchResultFromURI : FetchResultFromURI ;
184192 _profileSet : TransformProfileSet ;
193+ _optionsHasher : OptionsHasher ;
185194 _store : ?KeyResultStore ;
186195
187196 static FetchFailedError ;
@@ -194,31 +203,34 @@ class GlobalTransformCache {
194203 * of returning the content directly allows for independent and parallel
195204 * fetching of each result, that may be arbitrarily large JSON blobs.
196205 */
197- constructor (
198- fetchResultURIs : FetchResultURIs ,
206+ constructor ( props : {
199207 fetchResultFromURI : FetchResultFromURI ,
200- storeResults : ? StoreResults ,
208+ fetchResultURIs : FetchResultURIs ,
201209 profiles : Iterable < TransformProfile > ,
202- ) {
203- this . _fetcher = new KeyURIFetcher ( fetchResultURIs ) ;
204- this . _profileSet = new TransformProfileSet ( profiles ) ;
205- this . _fetchResultFromURI = fetchResultFromURI ;
206- if ( storeResults != null ) {
207- this . _store = new KeyResultStore ( storeResults ) ;
210+ rootPath : string ,
211+ storeResults : StoreResults | null ,
212+ } ) {
213+ this . _fetcher = new KeyURIFetcher ( props . fetchResultURIs ) ;
214+ this . _profileSet = new TransformProfileSet ( props . profiles ) ;
215+ this . _fetchResultFromURI = props . fetchResultFromURI ;
216+ this . _optionsHasher = new OptionsHasher ( props . rootPath ) ;
217+ if ( props . storeResults != null ) {
218+ this . _store = new KeyResultStore ( props . storeResults ) ;
208219 }
209220 }
210221
211222 /**
212223 * Return a key for identifying uniquely a source file.
213224 */
214- static keyOf ( props : FetchProps ) {
215- const stableOptions = globalizeTransformOptions ( props . transformOptions ) ;
216- const digest = crypto . createHash ( 'sha1' ) . update ( [
217- jsonStableStringify ( stableOptions ) ,
218- props . getTransformCacheKey ( props . sourceCode , props . filePath , props . transformOptions ) ,
219- imurmurhash ( props . sourceCode ) . result ( ) . toString ( ) ,
220- ] . join ( '$' ) ) . digest ( 'hex' ) ;
221- return `${digest } - $ { path . basename ( props . filePath ) } `;
225+ keyOf ( props : FetchProps ) {
226+ const hash = crypto . createHash ( 'sha1' ) ;
227+ const { sourceCode, filePath, transformOptions} = props ;
228+ this . _optionsHasher . hashTransformWorkerOptions ( hash , transformOptions ) ;
229+ const cacheKey = props . getTransformCacheKey ( sourceCode , filePath , transformOptions ) ;
230+ hash . update ( JSON . stringify ( cacheKey ) ) ;
231+ hash . update ( crypto . createHash ( 'sha1' ) . update ( sourceCode ) . digest ( 'hex' ) ) ;
232+ const digest = hash . digest ( 'hex' ) ;
233+ return `${ digest } -${ path . basename ( filePath ) } ` ;
222234 }
223235
224236 /**
@@ -249,8 +261,8 @@ class GlobalTransformCache {
249261 * waiting a little time before retring if experience shows it's useful.
250262 */
251263 static fetchResultFromURI ( uri : string ) : Promise < CachedResult > {
252- return GlobalTransformCache ._fetchResultFromURI(uri).catch(error => {
253- if (!GlobalTransformCache .shouldRetryAfterThatError(error)) {
264+ return URIBasedGlobalTransformCache . _fetchResultFromURI ( uri ) . catch ( error => {
265+ if ( ! URIBasedGlobalTransformCache . shouldRetryAfterThatError ( error ) ) {
254266 throw error ;
255267 }
256268 return this . _fetchResultFromURI ( uri ) ;
@@ -284,7 +296,7 @@ class GlobalTransformCache {
284296 * key yet, or an error happened, processed separately.
285297 */
286298 async fetch ( props : FetchProps ) : Promise < ?CachedResult > {
287- const uri = await this . _fetcher . fetch ( GlobalTransformCache . keyOf ( props ) ) ;
299+ const uri = await this . _fetcher . fetch ( this . keyOf ( props ) ) ;
288300 if ( uri == null ) {
289301 return null ;
290302 }
@@ -293,12 +305,92 @@ class GlobalTransformCache {
293305
294306 store ( props : FetchProps , result : CachedResult ) {
295307 if ( this . _store != null ) {
296- this . _store . store ( GlobalTransformCache . keyOf ( props ) , result ) ;
308+ this . _store . store ( this . keyOf ( props ) , result ) ;
309+ }
310+ }
311+
312+ }
313+
314+ class OptionsHasher {
315+ _rootPath : string ;
316+
317+ constructor ( rootPath : string ) {
318+ this . _rootPath = rootPath ;
319+ }
320+
321+ /**
322+ * This function is extra-conservative with how it hashes the transform
323+ * options. In particular:
324+ *
325+ * * we need to hash paths relative to the root, not the absolute paths,
326+ * otherwise everyone would have a different cache, defeating the
327+ * purpose of global cache;
328+ * * we need to reject any additional field we do not know of, because
329+ * they could contain absolute path, and we absolutely want to process
330+ * these.
331+ *
332+ * Theorically, Flow could help us prevent any other field from being here by
333+ * using *exact* object type. In practice, the transform options are a mix of
334+ * many different fields including the optional Babel fields, and some serious
335+ * cleanup will be necessary to enable rock-solid typing.
336+ */
337+ hashTransformWorkerOptions ( hash : crypto$Hash , options : TransformWorkerOptions ) : crypto$Hash {
338+ const { dev, minify, platform, transform, extern, ...unknowns } = options ;
339+ const unknownKeys = Object . keys ( unknowns ) ;
340+ if ( unknownKeys . length > 0 ) {
341+ const message = `these worker option fields are unknown: ${ JSON . stringify ( unknownKeys ) } ` ;
342+ throw new CannotHashOptionsError ( message ) ;
343+ }
344+ // eslint-disable-next-line no-undef, no-bitwise
345+ hash . update ( new Buffer ( [ + dev | + minify << 1 | + ! ! extern << 2 ] ) ) ;
346+ hash . update ( JSON . stringify ( platform ) ) ;
347+ return this . hashTransformOptions ( hash , transform ) ;
348+ }
349+
350+ /**
351+ * The transform options contain absolute paths. This can contain, for
352+ * example, the username if someone works their home directory (very likely).
353+ * We get rid of this local data for the global cache, otherwise nobody would
354+ * share the same cache keys. The project roots should not be needed as part
355+ * of the cache key as they should not affect the transformation of a single
356+ * particular file.
357+ */
358+ hashTransformOptions ( hash : crypto$Hash , options : TransformOptions ) : crypto$Hash {
359+ const { generateSourceMaps, dev, hot, inlineRequires, platform,
360+ preloadedModules, projectRoots, ramGroups, ...unknowns } = options ;
361+ const unknownKeys = Object . keys ( unknowns ) ;
362+ if ( unknownKeys . length > 0 ) {
363+ const message = `these transform option fields are unknown: ${ JSON . stringify ( unknownKeys ) } ` ;
364+ throw new CannotHashOptionsError ( message ) ;
365+ }
366+ // eslint-disable-next-line no-undef
367+ hash . update ( new Buffer ( [
368+ // eslint-disable-next-line no-bitwise
369+ + dev | + generateSourceMaps << 1 | + hot << 2 | + ! ! inlineRequires << 3 ,
370+ ] ) ) ;
371+ hash . update ( JSON . stringify ( platform ) ) ;
372+ let relativeBlacklist = [ ] ;
373+ if ( typeof inlineRequires === 'object' ) {
374+ relativeBlacklist = this . relativizeFilePaths ( Object . keys ( inlineRequires . blacklist ) ) ;
297375 }
376+ const relativeProjectRoots = this . relativizeFilePaths ( projectRoots ) ;
377+ const optionTuple = [ relativeBlacklist , preloadedModules , relativeProjectRoots , ramGroups ] ;
378+ hash . update ( JSON . stringify ( optionTuple ) ) ;
379+ return hash ;
298380 }
299381
382+ relativizeFilePaths ( filePaths : Array < string > ) : Array < string > {
383+ return filePaths . map ( filepath => path . relative ( this . _rootPath , filepath ) ) ;
384+ }
385+ }
386+
387+ class CannotHashOptionsError extends Error {
388+ constructor ( message : string ) {
389+ super ( ) ;
390+ this . message = message ;
391+ }
300392}
301393
302- GlobalTransformCache . FetchFailedError = FetchFailedError ;
394+ URIBasedGlobalTransformCache . FetchFailedError = FetchFailedError ;
303395
304- module . exports = GlobalTransformCache ;
396+ module . exports = { URIBasedGlobalTransformCache , CannotHashOptionsError } ;
0 commit comments