Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions packages/metro-file-map/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type CacheManagerWriteOptions = Readonly<{
export type ChangeEvent = Readonly<{
logger?: null | undefined | RootPerfLogger;
changes: ReadonlyFileSystemChanges<Readonly<ChangedFileMetadata>>;
pluginChanges: ReadonlyMap<string, unknown>;
rootDir: string;
}>;

Expand Down Expand Up @@ -202,6 +203,25 @@ interface FileSystem_2 {
}
export {FileSystem_2 as FileSystem};

export function getPluginChanges<ChangeSummary>(
event: ChangeEvent,
plugin: FileMapPlugin<
/**
* > | plugin: FileMapPlugin<empty, empty, ChangeSummary>,
* | ^^^^^ Unsupported feature: Translating "empty type" is currently not supported.
**/
any,
/**
* > | plugin: FileMapPlugin<empty, empty, ChangeSummary>,
* | ^^^^^ Unsupported feature: Translating "empty type" is currently not supported.
**/
any,
ChangeSummary
>,
): ChangeSummary | void;

export type HasteChanges = Readonly<{changedNames: ReadonlySet<string>}>;

export class HasteConflictsError extends Error {
constructor(conflicts: ReadonlyArray<HasteConflict>);
getDetailedMessage(pathsRelativeToRoot: null | undefined | string): string;
Expand All @@ -220,7 +240,7 @@ export type HasteMapItem = {
[platform: string]: HasteMapItemMetadata;
};

export class HastePlugin implements HasteMap, FileMapPlugin<null, string | null> {
export class HastePlugin implements HasteMap, FileMapPlugin<null, string | null, HasteChanges | void> {
constructor(options: HasteMapOptions);
assertValid(): void;
computeConflicts(): Array<HasteConflict>;
Expand All @@ -232,7 +252,7 @@ export class HastePlugin implements HasteMap, FileMapPlugin<null, string | null>
getWorker(): FileMapPluginWorker;
initialize(opts: FileMapPluginInitOptions<null, string | null>): Promise<void>;
readonly name: 'haste';
onChanged(delta: ReadonlyFileSystemChanges<null | undefined | string>): void;
onChanged(delta: ReadonlyFileSystemChanges<null | undefined | string>): HasteChanges | void;
setModule(id: string, module: HasteMapItemMetadata): void;
}

Expand All @@ -258,15 +278,16 @@ export type HealthCheckResult =

export type InputFileMapPlugin = FileMapPlugin<
/**
* > | export type InputFileMapPlugin = FileMapPlugin<empty, empty>;
* > | export type InputFileMapPlugin = FileMapPlugin<empty, empty, unknown>;
* | ^^^^^ Unsupported feature: Translating "empty type" is currently not supported.
**/
any,
/**
* > | export type InputFileMapPlugin = FileMapPlugin<empty, empty>;
* > | export type InputFileMapPlugin = FileMapPlugin<empty, empty, unknown>;
* | ^^^^^ Unsupported feature: Translating "empty type" is currently not supported.
**/
any
any,
unknown
>;

export type InputOptions = Readonly<{
Expand Down
56 changes: 51 additions & 5 deletions packages/metro-file-map/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
} from '../flow-types';
import type {DependencyPlugin} from '../index';
import type {default as FileMapT} from '../index';
import type {HasteMapOptions} from '../plugins/HastePlugin';
import type HastePluginT, {HasteMapOptions} from '../plugins/HastePlugin';
import type {MockMapOptions} from '../plugins/MockPlugin';
import type {WatcherOptions} from '../watchers/common';
import typeof WorkerModule from '../worker';
Expand Down Expand Up @@ -300,7 +300,7 @@ let buildNewFileMap: (
) => Promise<{
...BuildResult,
fileMap: FileMapT,
hasteMap: HasteMap,
hasteMap: HastePluginT,
mockMap: ?MockMap,
dependencyPlugin: ?DependencyPlugin,
}>;
Expand Down Expand Up @@ -502,6 +502,24 @@ describe('FileMap', () => {
).toMatchObject({exists: true, type: 'f', data: null});
});

test('throws on plugins that share a name, with or without a worker', () => {
const MockPlugin = require('../plugins/MockPlugin').default;
const mockConfig = {
console: globalThis.console,
rootDir: defaultConfig.rootDir,
mocksPattern: /__mocks__/,
throwOnModuleCollision: false,
};
expect(
() =>
new FileMap({
...defaultConfig,
// MockPlugin has no worker, so holds no plugin data slot
plugins: [new MockPlugin(mockConfig), new MockPlugin(mockConfig)],
}),
).toThrow('metro-file-map: Duplicate plugin name: mocks');
});

test('ignores files given a pattern', async () => {
mockFs[p('/project/fruits/Kiwi.js')] = `
// Kiwi!
Expand Down Expand Up @@ -1741,6 +1759,28 @@ describe('FileMap', () => {
}
});

fm_it(
'publishes what each plugin reports about a batch on the change event',
async ({fileMap, hasteMap}) => {
const getPluginChanges = require('../lib/getPluginChanges').default;
const fruitsRoot = path.join('/', 'project', 'fruits');
mockFs[path.join(fruitsRoot, 'Tomato.js')] = '// Tomato!';
mockEmitters[fruitsRoot].emitFileEvent({
event: 'touch',
relativePath: 'Tomato.js',
metadata: MOCK_CHANGE_FILE,
});
mockDeleteFile(fruitsRoot, 'Banana.js');
const changeEvent = await waitForItToChange(fileMap);

expect(getPluginChanges(changeEvent, hasteMap)).toEqual({
changedNames: new Set(['Tomato', 'Banana']),
});
// Only plugins with something to say are present
expect([...changeEvent.pluginChanges.keys()]).toEqual([hasteMap.name]);
},
);

function mockDeleteFile(root: string, relativePath: string) {
const e = mockEmitters[root];
e.emitFileEvent({event: 'delete', relativePath});
Expand All @@ -1755,7 +1795,7 @@ describe('FileMap', () => {

function fm_it(
title: string,
fn: (fm: Readonly<{fileMap: FileMap, hasteMap: HasteMap}>) => unknown,
fn: (fm: Readonly<{fileMap: FileMap, hasteMap: HastePluginT}>) => unknown,
options?: FileMapTestOptions = {},
): void {
options = options || {};
Expand All @@ -1778,7 +1818,7 @@ describe('FileMap', () => {

fm_it.only = (
title: string,
fn: (fm: Readonly<{fileMap: FileMap, hasteMap: HasteMap}>) => unknown,
fn: (fm: Readonly<{fileMap: FileMap, hasteMap: HastePluginT}>) => unknown,
options?: FileMapTestOptions,
): void => fm_it(title, fn, {...options, only: true});

Expand Down Expand Up @@ -2618,7 +2658,13 @@ describe('FileMap', () => {
relativePath: 'tropical',
});

await waitForItToChange(fileMap);
const changeEvent = await waitForItToChange(fileMap);

// Plugins are updated inside the recrawl, and what they report
// still reaches the event
expect(
require('../lib/getPluginChanges').default(changeEvent, hasteMap),
).toEqual({changedNames: new Set(['Mango', 'Papaya'])});

// Verify crawl was called with the correct directory
expect(mockNodeCrawler).toHaveBeenNthCalledWith(
Expand Down
20 changes: 18 additions & 2 deletions packages/metro-file-map/src/flow-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export type ChangedFileMetadata = Readonly<{
export type ChangeEvent = Readonly<{
logger?: ?RootPerfLogger,
changes: ReadonlyFileSystemChanges<Readonly<ChangedFileMetadata>>,
/**
* What each plugin reported about this batch of changes, by plugin name. A
* plugin whose `onChanged` returned nothing is absent. Read an entry with
* `getPluginChanges`, which knows its type.
*/
pluginChanges: ReadonlyMap<string, unknown>,
rootDir: string,
}>;

Expand Down Expand Up @@ -300,19 +306,29 @@ export type V8Serializable = V8SerializablePrimitive | V8SerializableCollection;
export interface FileMapPlugin<
in SerializableState extends void | V8Serializable = void | V8Serializable,
in PerFileData extends void | V8Serializable = void | V8Serializable,
out ChangeSummary = void,
> {
// Must be unique among the plugins of a file map.
readonly name: string;
initialize(
initOptions: FileMapPluginInitOptions<SerializableState, PerFileData>,
): Promise<void>;
assertValid(): void;
onChanged(changes: ReadonlyFileSystemChanges<?PerFileData>): void;
/**
* Update the plugin's state for a batch of changes. It is called
* synchronously, immediately before the batch is emitted as a `ChangeEvent`.
*
* A plugin may return a summary of what changed in terms only it knows,
* such as the Haste names that were rebound, for the file map to publish as
* `ChangeEvent.pluginChanges`. Return nothing if there is nothing to say.
*/
onChanged(changes: ReadonlyFileSystemChanges<?PerFileData>): ChangeSummary;
getSerializableSnapshot(): void | V8Serializable;
getCacheKey(): string;
getWorker(): ?FileMapPluginWorker;
}

export type InputFileMapPlugin = FileMapPlugin<empty, empty>;
export type InputFileMapPlugin = FileMapPlugin<empty, empty, unknown>;

export interface MetadataWorker {
processFile(
Expand Down
Loading
Loading