Skip to content

Commit 77bee75

Browse files
Jean Lauliacfacebook-github-bot
authored andcommitted
packager: GlobalTransformCache: make key globalized
Reviewed By: davidaurelio Differential Revision: D4835217 fbshipit-source-id: b43456e1e1f83c849a887b07f4f01f8ed0e9df4b
1 parent b3872e8 commit 77bee75

File tree

11 files changed

+187
-69
lines changed

11 files changed

+187
-69
lines changed

packager/react-packager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const debug = require('debug');
1717
const invariant = require('fbjs/lib/invariant');
1818

1919
import type Server from './src/Server';
20-
import type GlobalTransformCache from './src/lib/GlobalTransformCache';
20+
import type {GlobalTransformCache} from './src/lib/GlobalTransformCache';
2121
import type {Reporter} from './src/lib/reporting';
2222
import type {HasteImpl} from './src/node-haste/Module';
2323

packager/src/Bundler/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,19 @@ import type {
4646
TransformOptions,
4747
} from '../JSTransformer/worker/worker';
4848
import type {Reporter} from '../lib/reporting';
49-
import type GlobalTransformCache from '../lib/GlobalTransformCache';
49+
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
50+
51+
export type ExtraTransformOptions = {
52+
+inlineRequires?: {+blacklist: {[string]: true}} | boolean,
53+
+preloadedModules?: Array<string> | false,
54+
+ramGroups?: Array<string>,
55+
};
5056

5157
export type GetTransformOptions = (
5258
mainModuleName: string,
5359
options: {},
5460
getDependencies: string => Promise<Array<string>>,
55-
) => {} | Promise<{}>;
61+
) => ExtraTransformOptions | Promise<ExtraTransformOptions>;
5662

5763
type Asset = {
5864
__packager_asset: boolean,

packager/src/JSTransformer/worker/worker.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,19 @@ type Transformer = {
3636
};
3737

3838
export type TransformOptions = {
39+
+dev: boolean,
3940
generateSourceMaps: boolean,
41+
+hot: boolean,
42+
+inlineRequires: {+blacklist: {[string]: true}} | boolean,
4043
platform: string,
41-
preloadedModules?: Array<string>,
44+
preloadedModules?: Array<string> | false,
4245
projectRoots: Array<string>,
4346
ramGroups?: Array<string>,
4447
} & BabelTransformOptions;
4548

4649
export type Options = {
4750
+dev: boolean,
51+
+extern?: boolean,
4852
+minify: boolean,
4953
platform: string,
5054
transform: TransformOptions,

packager/src/Resolver/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {Reporter} from '../lib/reporting';
2424
import type {TransformCode} from '../node-haste/Module';
2525
import type Cache from '../node-haste/Cache';
2626
import type {GetTransformCacheKey} from '../lib/TransformCache';
27-
import type GlobalTransformCache from '../lib/GlobalTransformCache';
27+
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
2828

2929
type MinifyCode = (filePath: string, code: string, map: SourceMap) =>
3030
Promise<{code: string, map: SourceMap}>;

packager/src/Server/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import type Bundle from '../Bundler/Bundle';
3434
import type HMRBundle from '../Bundler/HMRBundle';
3535
import type {Reporter} from '../lib/reporting';
3636
import type {GetTransformOptions} from '../Bundler';
37-
import type GlobalTransformCache from '../lib/GlobalTransformCache';
37+
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
3838
import type {SourceMap, Symbolicate} from './symbolicate';
3939

4040
const {

packager/src/lib/GlobalTransformCache.js

Lines changed: 143 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,53 @@ const FetchError = require('node-fetch/lib/fetch-error');
1616

1717
const crypto = require('crypto');
1818
const fetch = require('node-fetch');
19-
const imurmurhash = require('imurmurhash');
2019
const jsonStableStringify = require('json-stable-stringify');
2120
const 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';
2426
import 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+
2657
type FetchResultURIs = (keys: Array<string>) => Promise<Map<string, string>>;
2758
type FetchResultFromURI = (uri: string) => Promise<?CachedResult>;
2859
type 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

3768
type 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-
124132
export type TransformProfile = {+dev: boolean, +minify: boolean, +platform: string};
125133

126134
function 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

Comments
 (0)