diff --git a/packages/metro-cache/src/Cache.js b/packages/metro-cache/src/Cache.js index a8732b3f..261ca336 100644 --- a/packages/metro-cache/src/Cache.js +++ b/packages/metro-cache/src/Cache.js @@ -12,14 +12,14 @@ import type {CacheStore} from 'metro-cache'; -class Cache { - _stores: $ReadOnlyArray; +class Cache { + _stores: $ReadOnlyArray>; - constructor(stores: $ReadOnlyArray) { + constructor(stores: $ReadOnlyArray>) { this._stores = stores; } - async get(key: Buffer): Promise { + async get(key: Buffer): Promise { const stores = this._stores; const length = stores.length; @@ -38,7 +38,7 @@ class Cache { return null; } - set(key: Buffer, value: mixed): void { + set(key: Buffer, value: T): void { Promise.all(this._stores.map(store => store.set(key, value))).catch(err => { process.nextTick(() => { throw err; diff --git a/packages/metro-cache/src/__tests__/Cache.test.js b/packages/metro-cache/src/__tests__/Cache.test.js index c74e7b66..024d4ebf 100644 --- a/packages/metro-cache/src/__tests__/Cache.test.js +++ b/packages/metro-cache/src/__tests__/Cache.test.js @@ -52,6 +52,8 @@ describe('Cache', () => { }); it('awaits for promises on stores, even if they return undefined', async () => { + jest.useFakeTimers(); + let resolve; const store1 = createStore(); @@ -67,7 +69,9 @@ describe('Cache', () => { expect(store2.get).not.toHaveBeenCalled(); resolve(undefined); + await promise; + jest.runAllTimers(); expect(store1.get).toHaveBeenCalledTimes(1); expect(store2.get).toHaveBeenCalledTimes(1); diff --git a/packages/metro-cache/src/index.js b/packages/metro-cache/src/index.js index 06e14755..c7869ba0 100644 --- a/packages/metro-cache/src/index.js +++ b/packages/metro-cache/src/index.js @@ -12,6 +12,9 @@ const Cache = require('./Cache'); +const stableHash = require('./stableHash'); + export type {CacheStore} from './types.flow'; module.exports.Cache = Cache; +module.exports.stableHash = stableHash; diff --git a/packages/metro-cache/src/types.flow.js b/packages/metro-cache/src/types.flow.js index ca6c34a6..e2905299 100644 --- a/packages/metro-cache/src/types.flow.js +++ b/packages/metro-cache/src/types.flow.js @@ -10,7 +10,7 @@ 'use strict'; -export type CacheStore = { - get(key: Buffer): ?mixed | Promise, - set(key: Buffer, value: mixed): void | Promise, +export type CacheStore = { + get(key: Buffer): ?T | Promise, + set(key: Buffer, value: T): void | Promise, }; diff --git a/packages/metro/src/Bundler/__tests__/Bundler-test.js b/packages/metro/src/Bundler/__tests__/Bundler-test.js index 3f0756c4..9fb89293 100644 --- a/packages/metro/src/Bundler/__tests__/Bundler-test.js +++ b/packages/metro/src/Bundler/__tests__/Bundler-test.js @@ -34,6 +34,7 @@ var commonOptions = { allowBundleUpdates: false, assetExts: defaults.assetExts, assetRegistryPath: '/AssetRegistry.js', + cacheStores: [], cacheVersion: 'smth', enableBabelRCLookup: true, extraNodeModules: {}, diff --git a/packages/metro/src/Bundler/index.js b/packages/metro/src/Bundler/index.js index 0959d54e..06eb14da 100644 --- a/packages/metro/src/Bundler/index.js +++ b/packages/metro/src/Bundler/index.js @@ -18,6 +18,7 @@ const defaults = require('../defaults'); const fs = require('fs'); const getTransformCacheKeyFn = require('../lib/getTransformCacheKeyFn'); +const {Cache, stableHash} = require('metro-cache'); const { toSegmentTuple, fromRawMappings, @@ -25,12 +26,16 @@ const { } = require('metro-source-map'); import type {PostProcessModules} from '../DeltaBundler'; -import type {Options as JSTransformerOptions} from '../JSTransformer/worker'; +import type { + Options as JSTransformerOptions, + TransformedCode, +} from '../JSTransformer/worker'; import type {DynamicRequiresBehavior} from '../ModuleGraph/worker/collectDependencies'; import type {GlobalTransformCache} from '../lib/GlobalTransformCache'; import type {TransformCache} from '../lib/TransformCaching'; import type {Reporter} from '../lib/reporting'; import type {BabelSourceMap} from '@babel/core'; +import type {CacheStore} from 'metro-cache'; import type { MetroSourceMapSegmentTuple, MetroSourceMap, @@ -80,6 +85,7 @@ export type Options = {| +assetRegistryPath: string, +asyncRequireModulePath: string, +blacklistRE?: RegExp, + +cacheStores: $ReadOnlyArray>, +cacheVersion: string, +dynamicDepsInPackages: DynamicRequiresBehavior, +enableBabelRCLookup: boolean, @@ -108,16 +114,18 @@ export type Options = {| class Bundler { _opts: Options; + _cache: ?Cache; _transformer: Transformer; _depGraphPromise: Promise; _projectRoots: $ReadOnlyArray; _getTransformOptions: void | GetTransformOptions; constructor(opts: Options) { - this._opts = opts; - opts.projectRoots.forEach(verifyRootExists); + this._opts = opts; + this._cache = opts.cacheStores.length ? new Cache(opts.cacheStores) : null; + this._transformer = new Transformer({ asyncRequireModulePath: opts.asyncRequireModulePath, maxWorkers: opts.maxWorkers, @@ -157,16 +165,7 @@ class Bundler { reporter: opts.reporter, resetCache: opts.resetCache, sourceExts: opts.sourceExts, - transformCode: (module, code, transformCodeOptions) => - this._transformer.transform( - module.path, - module.localPath, - code, - module.isPolyfill(), - transformCodeOptions, - this._opts.assetExts, - this._opts.assetRegistryPath, - ), + transformCode: this._cachedTransformCode.bind(this), transformCache: opts.transformCache, watch: opts.watch, }); @@ -276,6 +275,50 @@ class Bundler { map: result.map ? toBabelSegments(result.map).map(toSegmentTuple) : [], }; } + + async _cachedTransformCode( + module, + code, + transformCodeOptions, + ): Promise { + const cache = this._cache; + let result; + let key; + + // First, try getting the result from the cache if enabled. + if (cache) { + key = stableHash([ + module.localPath, + code, + transformCodeOptions, + this._opts.assetExts, + this._opts.assetRegistryPath, + this._opts.cacheVersion, + ]); + + result = await cache.get(key); + } + + // Second, if there was no result, compute it ourselves. + if (!result) { + result = await this._transformer.transform( + module.path, + module.localPath, + code, + module.isPolyfill(), + transformCodeOptions, + this._opts.assetExts, + this._opts.assetRegistryPath, + ); + } + + // Third, propagate the result to all cache layers. + if (key && cache) { + cache.set(key, result); + } + + return result; + } } function verifyRootExists(root) { diff --git a/packages/metro/src/Config.js b/packages/metro/src/Config.js index adaf14ad..572c767e 100644 --- a/packages/metro/src/Config.js +++ b/packages/metro/src/Config.js @@ -20,6 +20,7 @@ import type { PostProcessBundleSourcemap, } from './Bundler'; import type {PostProcessModules} from './DeltaBundler'; +import type {TransformedCode} from './JSTransformer/worker'; import type {DynamicRequiresBehavior} from './ModuleGraph/worker/collectDependencies'; import type {IncomingMessage, ServerResponse} from 'http'; import type {CacheStore} from 'metro-cache'; @@ -35,7 +36,7 @@ export type ConfigT = { /** * List of all store caches. */ - cacheStores: Array, + cacheStores: Array>, /** * Can be used to generate a key that will invalidate the whole metro cache diff --git a/packages/metro/src/DeltaBundler/__tests__/DeltaTransformer-test.js b/packages/metro/src/DeltaBundler/__tests__/DeltaTransformer-test.js index f6a1c807..2ea0cdd1 100644 --- a/packages/metro/src/DeltaBundler/__tests__/DeltaTransformer-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/DeltaTransformer-test.js @@ -30,6 +30,7 @@ const defaults = require('../../defaults'); const bundlerOptions = { allowBundleUpdates: false, assetExts: defaults.assetExts, + cacheStores: [], cacheVersion: 'smth', enableBabelRCLookup: true, extraNodeModules: {}, diff --git a/packages/metro/src/Server/index.js b/packages/metro/src/Server/index.js index da660d88..31ced04e 100644 --- a/packages/metro/src/Server/index.js +++ b/packages/metro/src/Server/index.js @@ -40,12 +40,14 @@ import type { PostMinifyProcess, PostProcessBundleSourcemap, } from '../Bundler'; +import type {CacheStore} from 'metro-cache'; import type {MetroSourceMap} from 'metro-source-map'; import type {TransformCache} from '../lib/TransformCaching'; import type {Symbolicate} from './symbolicate'; import type {AssetData} from '../Assets'; import type {RamBundleInfo} from '../DeltaBundler/Serializers'; import type {PostProcessModules} from '../DeltaBundler'; +import type {TransformedCode} from '../JSTransformer/worker'; const { Logger: {createActionStartEntry, createActionEndEntry, log}, } = require('metro-core'); @@ -66,6 +68,7 @@ class Server { _opts: { assetExts: Array, blacklistRE: void | RegExp, + cacheStores: $ReadOnlyArray>, cacheVersion: string, createModuleIdFactory?: () => (path: string) => number, enableBabelRCLookup: boolean, @@ -117,6 +120,7 @@ class Server { assetExts: options.assetTransforms ? [] : assetExts, assetRegistryPath: options.assetRegistryPath, blacklistRE: options.blacklistRE, + cacheStores: options.cacheStores || [], cacheVersion: options.cacheVersion, dynamicDepsInPackages: options.dynamicDepsInPackages || 'throwAtRuntime', createModuleIdFactory: options.createModuleIdFactory,