diff --git a/packages/metro-file-map/API.md b/packages/metro-file-map/API.md index b95d448ce8..193b526369 100644 --- a/packages/metro-file-map/API.md +++ b/packages/metro-file-map/API.md @@ -119,6 +119,26 @@ export class DuplicateHasteCandidatesError extends Error { export type FileData = Map; +export class FileDataPlugin implements FileMapPlugin { + constructor($$PARAM_0$$: FileDataPluginOptions); + assertValid(): void; + getCacheKey(): string; + getFileSystem(): FileMapPluginInitOptions['files']; + getSerializableSnapshot(): null; + getWorker(): FileMapPluginWorker; + initialize(initOptions: FileMapPluginInitOptions): Promise; + readonly name: string; + onChanged(_changes: ReadonlyFileSystemChanges): void; + processFile(mixedPath: string): ReturnType['processFile']>; +} + +export type FileDataPluginOptions = Readonly< + Omit & { + name: string; + cacheKey: string; + } +>; + class FileMap extends EventEmitter { constructor(options: InputOptions); build(): Promise; diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index c2c614eb8f..1658398902 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -107,7 +107,6 @@ jest.mock('../crawlers/watchman', () => ({ 0, // visited hash, typeof contentOrLink !== 'string' ? 1 : 0, - null, // Haste name ]); } } else { @@ -1707,6 +1706,64 @@ describe('FileMap', () => { ); } + test('a lazy plugin processes files on demand, and its data is reset when the file changes', async () => { + const FileDataPlugin = require('../plugins/FileDataPlugin').default; + const lazyPlugin = new FileDataPlugin({ + name: 'lazy-test-plugin', + cacheKey: 'lazy-test-plugin-1', + worker: { + modulePath: require.resolve('./lazy_plugin_worker.js'), + setupArgs: {}, + }, + // Not consulted for a lazy plugin + filter: () => true, + lazy: true, + }); + const fileMap = new FileMap({ + ...defaultConfig, + watch: true, + plugins: [lazyPlugin], + }); + await fileMap.build(); + try { + const bananaPath = path.join('/', 'project', 'fruits', 'Banana.js'); + const getPluginData = () => { + const result = lazyPlugin.getFileSystem().lookup(bananaPath); + if (!result.exists || result.type !== 'f') { + throw new Error('Expected a file'); + } + return result.pluginData; + }; + const onMetadata = jest.fn(); + fileMap.on('metadata', onMetadata); + + // The crawl does not run a lazy plugin's worker + expect(getPluginData()).toBeUndefined(); + + const expected = {length: String(mockFs[bananaPath]).length}; + expect(lazyPlugin.processFile(bananaPath)).toEqual(expected); + // The data is stored, and the cache told there is something to save + expect(getPluginData()).toEqual(expected); + expect(onMetadata).toHaveBeenCalledTimes(1); + + mockFs[bananaPath] = '// A changed banana'; + mockEmitters[path.join('/', 'project', 'fruits')].emitFileEvent({ + event: 'touch', + relativePath: 'Banana.js', + metadata: MOCK_CHANGE_FILE, + }); + await waitForItToChange(fileMap); + + // Nor does a change, which leaves the file unprocessed again + expect(getPluginData()).toBeUndefined(); + expect(lazyPlugin.processFile(bananaPath)).toEqual({ + length: '// A changed banana'.length, + }); + } finally { + await fileMap.end(); + } + }); + function mockDeleteFile(root: string, relativePath: string) { const e = mockEmitters[root]; e.emitFileEvent({event: 'delete', relativePath}); diff --git a/packages/metro-file-map/src/__tests__/lazy_plugin_worker.js b/packages/metro-file-map/src/__tests__/lazy_plugin_worker.js new file mode 100644 index 0000000000..cab56759c1 --- /dev/null +++ b/packages/metro-file-map/src/__tests__/lazy_plugin_worker.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall react_native + */ + +/* eslint-disable import/no-commonjs */ + +'use strict'; + +/*:: +import type {MetadataWorker, WorkerMessage, V8Serializable} from '../flow-types'; +*/ + +// A plugin worker for tests, whose data for a file is the length of its content. +module.exports = class LazyPluginWorker /*:: implements MetadataWorker */ { + processFile( + data /*: WorkerMessage */, + utils /*: Readonly<{getContent: () => Buffer }> */, + ) /*: V8Serializable */ { + return {length: utils.getContent().toString().length}; + } +}; diff --git a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js index e3ee5dc01f..16f8591185 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js @@ -76,29 +76,23 @@ const CASES = [ [ true, new Map([ - ['foo.js', [expect.any(Number), 245, 0, null, 0, null]], - [ - join('directory', 'bar.js'), - [expect.any(Number), 245, 0, null, 0, null], - ], + ['foo.js', [expect.any(Number), 245, 0, null, 0]], + [join('directory', 'bar.js'), [expect.any(Number), 245, 0, null, 0]], [ 'link-to-directory', - [expect.any(Number), 9, 0, null, expect.oneOf(1, 'directory'), null], + [expect.any(Number), 9, 0, null, expect.oneOf(1, 'directory')], ], [ 'link-to-foo.js', - [expect.any(Number), 6, 0, null, expect.oneOf(1, 'foo.js'), null], + [expect.any(Number), 6, 0, null, expect.oneOf(1, 'foo.js')], ], ]), ], [ false, new Map([ - [ - join('directory', 'bar.js'), - [expect.any(Number), 245, 0, null, 0, null], - ], - ['foo.js', [expect.any(Number), 245, 0, null, 0, null]], + [join('directory', 'bar.js'), [expect.any(Number), 245, 0, null, 0]], + ['foo.js', [expect.any(Number), 245, 0, null, 0]], ]), ], ]; @@ -118,7 +112,7 @@ describe.each(Object.keys(CRAWLERS))( previousState: { fileSystem: new TreeFS({ rootDir: FIXTURES_DIR, - files: new Map([['removed.js', [123, 234, 0, null, 0, null]]]), + files: new Map([['removed.js', [123, 234, 0, null, 0]]]), processFile: () => { throw new Error('Not implemented'); }, diff --git a/packages/metro-file-map/src/crawlers/__tests__/node-test.js b/packages/metro-file-map/src/crawlers/__tests__/node-test.js index 0677ad5f9a..cdfb3b92ba 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/node-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/node-test.js @@ -130,7 +130,7 @@ describe('node crawler', () => { // Tomato is not included because its mtime is unchanged expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null], + 'fruits/directory/strawberry.js': [33, 42, 0, null, 0], }), ); @@ -144,8 +144,8 @@ describe('node crawler', () => { // be found when crawling this directory. const files = createMap({ 'fruits/previouslyExisted.js': [30, 40, 1, null, 0, null], - 'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null], - 'fruits/tomato.js': [32, 42, 0, null, 0, null], + 'fruits/directory/strawberry.js': [33, 42, 0, null, 0], + 'fruits/tomato.js': [32, 42, 0, null, 0], }); const {changedFiles, removedFiles} = await nodeCrawl({ @@ -216,8 +216,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null], - 'fruits/tomato.js': [32, 42, 0, null, 0, null], + 'fruits/directory/strawberry.js': [33, 42, 0, null, 0], + 'fruits/tomato.js': [32, 42, 0, null, 0], }), ); expect(removedFiles).toEqual(new Set()); diff --git a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js index eef2f78d97..1a3e99dff5 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js @@ -118,9 +118,9 @@ describe('watchman watch', () => { }; mockFiles = createMap({ - [MELON_RELATIVE]: [33, 43, 0, null, 0, null], - [STRAWBERRY_RELATIVE]: [30, 40, 0, null, 0, null], - [TOMATO_RELATIVE]: [31, 41, 0, null, 0, null], + [MELON_RELATIVE]: [33, 43, 0, null, 0], + [STRAWBERRY_RELATIVE]: [30, 40, 0, null, 0], + [TOMATO_RELATIVE]: [31, 41, 0, null, 0], }); }); @@ -223,7 +223,7 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 40, 0, null, 0, null], + [KIWI_RELATIVE]: [42, 40, 0, null, 0], }), ); @@ -296,7 +296,7 @@ describe('watchman watch', () => { // banana is not included because it is unchanged expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 52, 0, null, 0, null], + [KIWI_RELATIVE]: [42, 52, 0, null, 0], [TOMATO_RELATIVE]: [76, 41, 1, mockTomatoSha1, 0, 'Tomato'], }), ); @@ -373,7 +373,7 @@ describe('watchman watch', () => { // Melon is not included because it is unchanged. expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 52, 0, null, 0, null], + [KIWI_RELATIVE]: [42, 52, 0, null, 0], }), ); @@ -542,7 +542,7 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 40, 0, null, 0, null], + [KIWI_RELATIVE]: [42, 40, 0, null, 0], }), ); diff --git a/packages/metro-file-map/src/crawlers/node/index.js b/packages/metro-file-map/src/crawlers/node/index.js index a7d40a50a5..10296203da 100644 --- a/packages/metro-file-map/src/crawlers/node/index.js +++ b/packages/metro-file-map/src/crawlers/node/index.js @@ -75,7 +75,6 @@ function find( 0, null, stat.isSymbolicLink() ? 1 : 0, - null, ]); } } diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 61b07d2bb3..0cbeb7d106 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -334,7 +334,6 @@ export default async function watchmanCrawl({ 0, sha1hex ?? null, symlinkInfo, - null, ]; // If watchman is fresh, the removed files map starts with all files diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 73bb8a6249..11d62eac5a 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -222,6 +222,18 @@ export type FileMapPluginInitOptions< | {exists: true, type: 'd'}, }>, pluginState: ?SerializableState, + /** + * Synchronously run this plugin's worker, in-band, on the regular file at + * `mixedPath`, store the result as its plugin data and return it. Throws if + * the path is not a regular file, or the plugin has no worker. + * + * Plugin data is `undefined` for a file until a worker has run on it, which + * is when a lazy plugin should call this, and is reset to `undefined` when + * the file changes. `null` is a result like any other, so what a worker + * returns is stored with `undefined` replaced by `null`, and a file is + * processed at most once while it is unchanged. + */ + processFile: (mixedPath: string) => PerFileData, }>; export type FileMapPluginWorker = Readonly<{ @@ -230,6 +242,13 @@ export type FileMapPluginWorker = Readonly<{ setupArgs: JsonData, }>, filter: ({normalPath: string, isNodeModules: boolean}) => boolean, + /** + * If true, the worker is not run on files as they are crawled or changed, + * and `filter` is not consulted. It runs only on the files the plugin asks + * for, through the `processFile` it is given on initialization. Use this + * where a small, unpredictable subset of matching files is ever needed. + */ + lazy?: boolean, }>; type V8SerializablePrimitive = string | number | boolean | null; @@ -293,7 +312,7 @@ export type FileMetadata = [ /* visited */ 0 | 1, /* sha1 */ ?string, /* symlink */ 0 | 1 | string, // string specifies target, if known - /* plugindata */ + /* plugindata - one slot per plugin with a worker, `undefined` until run */ ... ]; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 70101072b6..38401c7206 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -142,6 +142,8 @@ type InternalEnqueuedEvent = Readonly< export {DiskCacheManager} from './cache/DiskCacheManager'; export {NoopCacheManager} from './cache/NoopCacheManager'; export {default as DependencyPlugin} from './plugins/DependencyPlugin'; +export {default as FileDataPlugin} from './plugins/FileDataPlugin'; +export type {FileDataPluginOptions} from './plugins/FileDataPlugin'; export type {DependencyPluginOptions} from './plugins/DependencyPlugin'; export {DuplicateHasteCandidatesError} from './plugins/haste/DuplicateHasteCandidatesError'; export {HasteConflictsError} from './plugins/haste/HasteConflictsError'; @@ -472,6 +474,32 @@ export default class FileMap extends EventEmitter { ), }, pluginState: initialData?.plugins.get(plugin.name), + processFile: mixedPath => { + invariant( + dataIdx != null, + 'metro-file-map: Plugin "%s" has no worker to process files with', + plugin.name, + ); + const result = fileSystem.lookup(mixedPath); + if (!result.exists || result.type !== 'f') { + throw new Error( + `metro-file-map: Cannot process ${mixedPath}, which is not a regular file`, + ); + } + const pluginData = this.#fileProcessor.processFileForPlugin( + result.realPath, + result.metadata, + dataIdx - H.PLUGINDATA, + ); + debug( + 'Lazily processed file for %s: %s', + plugin.name, + mixedPath, + ); + // Inform caches that there is new data to save. + this.emit('metadata'); + return pluginData; + }, }), ), ), @@ -928,7 +956,6 @@ export default class FileMap extends EventEmitter { 0, null, change.metadata.type === 'l' ? 1 : 0, - null, ]; try { diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index 051c6bfa42..4033fe2177 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -13,6 +13,7 @@ import type { FileMapPluginWorker, FileMetadata, PerfLogger, + V8Serializable, WorkerMessage, WorkerMetadata, WorkerSetupArgs, @@ -153,6 +154,30 @@ export class FileProcessor { : null; } + /** + * Synchronously run one plugin's worker on a regular file and store the + * result as that plugin's data. This is how a lazy plugin's files are + * processed, so neither its filter nor the exclusion of node_modules + * applies - the plugin asked for this file. + */ + processFileForPlugin( + absolutePath: string, + fileMetadata: FileMetadata, + pluginIdx: number, + ): V8Serializable { + const reply = this.#inBandWorker.processFile({ + computeSha1: false, + filePath: absolutePath, + maybeReturnContent: false, + pluginsToRun: [pluginIdx], + }); + // `undefined` is reserved to mean that no worker has run. + const pluginData = reply.pluginData?.[0] ?? null; + // $FlowFixMe[invalid-tuple-index] + fileMetadata[H.PLUGINDATA + pluginIdx] = pluginData; + return pluginData; + } + #getWorkerInput( normalPath: string, fileMetadata: FileMetadata, @@ -175,7 +200,10 @@ export class FileProcessor { // Indices of plugins with a passing filter const pluginsToRun = this.#pluginWorkers?.reduce((prev, plugin, idx) => { - if (plugin.filter({isNodeModules, normalPath})) { + if ( + plugin.lazy !== true && + plugin.filter({isNodeModules, normalPath}) + ) { prev.push(idx); } return prev; @@ -262,8 +290,9 @@ function processWorkerReply( const pluginData = metadata.pluginData; if (pluginData) { for (const [i, pluginIdx] of pluginsRun.entries()) { + // `undefined` is reserved to mean that no worker has run. // $FlowFixMe[invalid-tuple-index] - fileMetadata[H.PLUGINDATA + pluginIdx] = pluginData[i]; + fileMetadata[H.PLUGINDATA + pluginIdx] = pluginData[i] ?? null; } } diff --git a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js index fa28b2633c..63d4289277 100644 --- a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js +++ b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js @@ -223,6 +223,115 @@ describe('processBatch', () => { ); }); + test('lazy plugins are not run on a batch, and their filter is not consulted', async () => { + const eagerFilter = jest.fn().mockReturnValue(true); + const lazyFilter = jest.fn().mockReturnValue(true); + + const processor = new FileProcessor({ + ...defaultOptions, + pluginWorkers: [ + { + worker: {modulePath: 'mock-lazy-plugin', setupArgs: {}}, + filter: lazyFilter, + lazy: true, + }, + { + worker: {modulePath: 'mock-eager-plugin', setupArgs: {}}, + filter: eagerFilter, + }, + ], + }); + + await processor.processBatch( + [[p('src/package.json'), [123, 234, 0, null, 0, null]]], + {computeSha1: false, maybeReturnContent: false}, + ); + + expect(mockWorkerFn).toHaveBeenCalledTimes(1); + expect(mockWorkerFn).toHaveBeenCalledWith( + expect.objectContaining({pluginsToRun: [1]}), + ); + expect(lazyFilter).not.toHaveBeenCalled(); + }); + + test('a file matched only by a lazy plugin is not processed', async () => { + const processor = new FileProcessor({ + ...defaultOptions, + pluginWorkers: [ + { + worker: {modulePath: 'mock-lazy-plugin', setupArgs: {}}, + filter: () => true, + lazy: true, + }, + ], + }); + + await processor.processBatch( + [[p('src/package.json'), [123, 234, 0, null, 0, null]]], + {computeSha1: false, maybeReturnContent: false}, + ); + + expect(mockWorkerFn).not.toHaveBeenCalled(); + }); + + describe('processFileForPlugin', () => { + const pluginWorkers = [ + { + worker: {modulePath: 'mock-eager-plugin', setupArgs: {}}, + filter: () => true, + }, + { + worker: {modulePath: 'mock-lazy-plugin', setupArgs: {}}, + // On-demand processing does not consult the filter + filter: () => false, + lazy: true, + }, + ]; + + test('runs only that plugin, in band, and stores its data', () => { + const processor = new FileProcessor({...defaultOptions, pluginWorkers}); + const metadata: FileMetadata = [123, 234, 0, null, 0, 'eager data']; + mockWorkerFn.mockReturnValueOnce({pluginData: [{name: 'pkg'}]}); + + // A file in node_modules, which batch processing never runs plugins on + const absolutePath = p('/root/node_modules/pkg/package.json'); + expect(processor.processFileForPlugin(absolutePath, metadata, 1)).toEqual( + {name: 'pkg'}, + ); + + expect(mockWorkerFn).toHaveBeenCalledTimes(1); + expect(mockWorkerFn).toHaveBeenCalledWith({ + computeSha1: false, + filePath: absolutePath, + maybeReturnContent: false, + pluginsToRun: [1], + }); + expect(MockJestWorker).not.toHaveBeenCalled(); + // Only this plugin's slot is written. In particular the file is not + // marked visited, since the other workers have not seen it. + expect(metadata).toEqual([ + 123, + 234, + 0, + null, + 0, + 'eager data', + {name: 'pkg'}, + ]); + }); + + test('stores null when the worker returns nothing', () => { + const processor = new FileProcessor({...defaultOptions, pluginWorkers}); + const metadata: FileMetadata = [123, 234, 0, null, 0, null]; + mockWorkerFn.mockReturnValueOnce({pluginData: [undefined]}); + + expect( + processor.processFileForPlugin(p('/root/package.json'), metadata, 1), + ).toBe(null); + expect(metadata[H.PLUGINDATA + 1]).toBe(null); + }); + }); + test('worker reply plugin data is mapped to correct fileMetadata indices', async () => { const mockFilter1 = jest.fn().mockReturnValue(true); const mockFilter2 = jest.fn().mockReturnValue(false); diff --git a/packages/metro-file-map/src/plugins/FileDataPlugin.js b/packages/metro-file-map/src/plugins/FileDataPlugin.js index 5b35163ed6..f050a5a6fe 100644 --- a/packages/metro-file-map/src/plugins/FileDataPlugin.js +++ b/packages/metro-file-map/src/plugins/FileDataPlugin.js @@ -36,10 +36,11 @@ export default class FileDataPlugin< #worker: FileMapPluginWorker; #cacheKey: string; #files: ?FileMapPluginInitOptions['files']; + #processFile: ?FileMapPluginInitOptions['processFile']; - constructor({name, worker, filter, cacheKey}: FileDataPluginOptions) { + constructor({name, worker, filter, lazy, cacheKey}: FileDataPluginOptions) { this.name = name; - this.#worker = {worker, filter}; + this.#worker = {worker, filter, lazy}; this.#cacheKey = cacheKey; } @@ -47,6 +48,22 @@ export default class FileDataPlugin< initOptions: FileMapPluginInitOptions, ): Promise { this.#files = initOptions.files; + this.#processFile = initOptions.processFile; + } + + /** + * Run this plugin's worker now on the file at `mixedPath`, store its data + * and return it. A lazy plugin calls this for a file whose data, as given by + * `getFileSystem().lookup()`, is `undefined`. + */ + processFile( + mixedPath: string, + ): ReturnType['processFile']> { + const processFile = this.#processFile; + if (processFile == null) { + throw new Error(`${this.name} plugin has not been initialized`); + } + return processFile(mixedPath); } getFileSystem(): FileMapPluginInitOptions['files'] { diff --git a/packages/metro-file-map/src/plugins/__tests__/FileDataPlugin-test.js b/packages/metro-file-map/src/plugins/__tests__/FileDataPlugin-test.js new file mode 100644 index 0000000000..e22b0c8270 --- /dev/null +++ b/packages/metro-file-map/src/plugins/__tests__/FileDataPlugin-test.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import FileDataPlugin from '../FileDataPlugin'; + +const options = { + name: 'test-plugin', + cacheKey: 'test-plugin-1', + worker: {modulePath: 'mock-worker', setupArgs: {}}, + filter: () => true, +}; + +const files = { + lookup: (): {exists: false} => ({exists: false}), + fileIterator: () => [], +}; + +describe('FileDataPlugin', () => { + test('is not lazy by default', () => { + expect(new FileDataPlugin(options).getWorker().lazy).not.toBe( + true, + ); + }); + + test('passes `lazy` to its worker description', () => { + expect( + new FileDataPlugin({...options, lazy: true}).getWorker(), + ).toEqual({worker: options.worker, filter: options.filter, lazy: true}); + }); + + test('processFile throws before the plugin is initialized', () => { + const plugin = new FileDataPlugin(options); + expect(() => plugin.processFile('/project/package.json')).toThrow( + 'test-plugin plugin has not been initialized', + ); + }); + + test('processFile asks the file map to process the file', async () => { + const plugin = new FileDataPlugin({...options, lazy: true}); + const processFile = jest.fn<[string], ?string>(() => 'file data'); + await plugin.initialize({files, pluginState: null, processFile}); + + expect(plugin.processFile('/project/package.json')).toBe('file data'); + expect(processFile).toHaveBeenCalledWith('/project/package.json'); + }); +}); diff --git a/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js b/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js index cba63d824c..59adc7c55d 100644 --- a/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js +++ b/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js @@ -82,6 +82,9 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { ]), lookup: jest.fn(), }, + processFile: () => { + throw new Error('Not implemented'); + }, pluginState: null, }; await hasteMap.initialize(initialState); @@ -107,6 +110,9 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup: jest.fn(), }, + processFile: () => { + throw new Error('Not implemented'); + }, pluginState: null, }); }); @@ -139,6 +145,9 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup: jest.fn(), }, + processFile: () => { + throw new Error('Not implemented'); + }, pluginState: null, }); }); @@ -183,6 +192,9 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup, }, + processFile: () => { + throw new Error('Not implemented'); + }, pluginState: null, }); }); diff --git a/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js b/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js index 465fb6102a..6a2fe80188 100644 --- a/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js +++ b/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js @@ -117,6 +117,9 @@ Duplicate manual mock found for \`foo\`: throw new Error('should not be used'); }, }, + processFile: () => { + throw new Error('Not implemented'); + }, pluginState: { mocks: new Map([ ['bar', 'some/__mocks__/bar.js'],