Skip to content

Commit

Permalink
TreeFS: Symlink-enabled, prefix-tree implementation of FileSystem (#…
Browse files Browse the repository at this point in the history
…926)

Summary:
Pull Request resolved: #926

Currently, `HasteFS` is the only implementation of the `FileSystem` interface, used by Metro to check for file existence during resolution, and to retrieve metadata (file hashes, etc) during transformation. `HasteFS` is based on a simple, performant `Map` of  real paths to metadata.

This diff introduces `TreeFS`, which is based on an in-memory prefix tree representation, split on path separators, of all watched files.

By using a prefix tree, we can lookup files whose paths segments are directory symlinks or files which are themselves links to other files.

We use `TreeFS` in preference to `HasteFS` when `config.resolver.unstable_enableSymlinks` (default: `false`) is `true`.

Reviewed By: motiz88

Differential Revision: D42552999

fbshipit-source-id: 2705f34436d549831b51e4773bce39094a6ec507
  • Loading branch information
robhogan authored and facebook-github-bot committed Feb 14, 2023
1 parent a0ba994 commit bd87dbc
Show file tree
Hide file tree
Showing 6 changed files with 683 additions and 125 deletions.
136 changes: 136 additions & 0 deletions packages/metro-file-map/src/__tests__/FileSystem-test.js
@@ -0,0 +1,136 @@
/**
* 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 type {FileMetaData, Path} from '../flow-types';
let mockPathModule;
jest.mock('path', () => mockPathModule);
jest.mock('../lib/fast_path', () => mockPathModule);

describe.each([['win32'], ['posix']])(
'FileSystem implementations on %s',
platform => {
beforeAll(() => {
mockPathModule = jest.requireActual<{}>('path')[platform];
});

// Convenience function to write paths with posix separators but convert them
// to system separators
const p: string => string = filePath =>
platform === 'win32'
? filePath.replace(/\//g, '\\').replace(/^\\/, 'C:\\')
: filePath;

describe.each([['TreeFS'], ['HasteFS']])('%s', label => {
let FileSystem;
let fs;

beforeAll(() => {
jest.resetModules();
FileSystem =
label === 'HasteFS'
? require('../HasteFS').default
: require('../lib/TreeFS').default;

fs = new FileSystem({
rootDir: p('/project'),
files: new Map([
[p('foo/another.js'), ['', 0, 0, 1, '', '', 0]],
[p('../outside/external.js'), ['', 0, 0, 1, '', '', 0]],
[p('bar.js'), ['', 234, 0, 1, '', '', 0]],
]),
});
});

test('getAllFiles returns all files by absolute path', () => {
expect(fs.getAllFiles().sort()).toEqual([
p('/outside/external.js'),
p('/project/bar.js'),
p('/project/foo/another.js'),
]);
});

test.each([
p('/outside/external.js'),
p('/project/bar.js'),
p('/project/foo/another.js'),
])('existence check passes: %s', filePath => {
expect(fs.exists(filePath)).toBe(true);
});

test('existence check fails for directories', () => {
expect(fs.exists(p('/project/foo'))).toBe(false);
});

test('implements linkStats()', () => {
expect(fs.linkStats(p('./bar.js'))).toEqual({
fileType: 'f',
modifiedTime: 234,
});
});

describe('matchFiles', () => {
test('matches files against a pattern', async () => {
expect(
fs.matchFiles(
mockPathModule.sep === mockPathModule.win32.sep
? /project\\foo/
: /project\/foo/,
),
).toEqual([p('/project/foo/another.js')]);
});
});

describe('matchFilesWithContext', () => {
test('matches files against context', () => {
const fs = new FileSystem({
rootDir: p('/root'),
files: new Map<Path, FileMetaData>([
[p('foo/another.js'), ['', 0, 0, 0, '', '', 0]],
[p('bar.js'), ['', 0, 0, 0, '', '', 0]],
]),
});

expect(fs.getAllFiles()).toEqual([
p('/root/foo/another.js'),
p('/root/bar.js'),
]);

// Test non-recursive skipping deep paths
expect(
fs.matchFilesWithContext(p('/root'), {
filter: new RegExp(
// Test starting with `./` since this is mandatory for parity with Webpack.
/^\.\/.*/,
),
recursive: false,
}),
).toEqual([p('/root/bar.js')]);

// Test inner directory
expect(
fs.matchFilesWithContext(p('/root/foo'), {
filter: new RegExp(/.*/),
recursive: true,
}),
).toEqual([p('/root/foo/another.js')]);

// Test recursive
expect(
fs.matchFilesWithContext(p('/root'), {
filter: new RegExp(/.*/),
recursive: true,
}),
).toEqual([p('/root/foo/another.js'), p('/root/bar.js')]);
});
});
});
},
);
124 changes: 0 additions & 124 deletions packages/metro-file-map/src/__tests__/HasteFS-test.js

This file was deleted.

2 changes: 2 additions & 0 deletions packages/metro-file-map/src/__tests__/index-test.js
Expand Up @@ -1663,6 +1663,8 @@ describe('HasteMap', () => {
fileType: 'l',
modifiedTime: 46,
});
// getModuleName traverses the symlink, verifying the link is read.
expect(fileSystem.getModuleName(filePath)).toEqual('Strawberry');
},
{config: {enableSymlinks: true}},
);
Expand Down
6 changes: 5 additions & 1 deletion packages/metro-file-map/src/index.js
Expand Up @@ -47,6 +47,7 @@ import deepCloneRawModuleMap from './lib/deepCloneRawModuleMap';
import * as fastPath from './lib/fast_path';
import getPlatformExtension from './lib/getPlatformExtension';
import normalizePathSep from './lib/normalizePathSep';
import TreeFS from './lib/TreeFS';
import HasteModuleMap from './ModuleMap';
import {Watcher} from './Watcher';
import {worker} from './worker';
Expand Down Expand Up @@ -358,10 +359,13 @@ export default class HasteMap extends EventEmitter {

const rootDir = this._options.rootDir;
const fileData = initialData.files;
const fileSystem = new HasteFS({
this._startupPerfLogger?.point('constructFileSystem_start');
const FileSystem = this._options.enableSymlinks ? TreeFS : HasteFS;
const fileSystem = new FileSystem({
files: fileData,
rootDir,
});
this._startupPerfLogger?.point('constructFileSystem_end');
const {map, mocks, duplicates} = initialData;
const rawModuleMap: RawModuleMap = {
duplicates,
Expand Down

0 comments on commit bd87dbc

Please sign in to comment.