Skip to content

Commit

Permalink
Resolve files to real paths when unstable_enableSymlinks (#925)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #925

This makes a minimally invasive change to `metro-resolver` to run source file and asset resolutions through a new `getRealPath` method of `FileSystem`.

Custom `resolveRequest` implementations are not affected - for the time being they're expected to take responsibility for returning real paths on their own, but they may now use `content.unstable_getRealPath` to do so.

This is not intended as a final design, but the resolver changes will dovetail into planned DependencyGraph work where we'll need to track non-existent resolution candidates (by their "candidate path", but ultimately resolve to real paths).

Changelog: [Experimental] Implement `resolver.unstable_enableSymlinks`

Reviewed By: jacdebug

Differential Revision: D42847996

fbshipit-source-id: b67ce7a689afc4074080d1e1c47042057904384c
  • Loading branch information
robhogan authored and facebook-github-bot committed Feb 15, 2023
1 parent 6e6f36f commit a1e233c
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 7 deletions.
4 changes: 4 additions & 0 deletions packages/metro-file-map/src/HasteFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,8 @@ export default class HasteFS implements MutableFileSystem {
}
return this.#files.get(this._normalizePath(filePath));
}

getRealPath(filePath: Path): Path {
throw new Error('HasteFS.getRealPath() is not implemented.');
}
}
1 change: 1 addition & 0 deletions packages/metro-file-map/src/flow-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export interface FileSystem {
getAllFiles(): Array<Path>;
getDependencies(file: Path): ?Array<string>;
getModuleName(file: Path): ?string;
getRealPath(file: Path): ?string;
getSerializableSnapshot(): FileData;
getSha1(file: Path): ?string;

Expand Down
13 changes: 13 additions & 0 deletions packages/metro-file-map/src/lib/TreeFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,19 @@ export default class TreeFS implements MutableFileSystem {
return files;
}

getRealPath(filePath: Path): ?string {
const normalPath = this._normalizePath(filePath);
const metadata = this.#files.get(normalPath);
if (metadata && metadata[H.SYMLINK] === 0) {
return fastPath.resolve(this.#rootDir, normalPath);
}
const result = this._lookupByNormalPath(normalPath, {follow: true});
if (!result || result.node instanceof Map) {
return null;
}
return fastPath.resolve(this.#rootDir, result.normalPath);
}

addOrModify(filePath: Path, metadata: FileMetaData): void {
const normalPath = this._normalizePath(filePath);
this.bulkAddOrModify(new Map([[normalPath, metadata]]));
Expand Down
22 changes: 22 additions & 0 deletions packages/metro-file-map/src/lib/__tests__/TreeFS-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => {
});
});

describe('getRealPath', () => {
test.each([
[p('/project/foo/link-to-another.js'), p('/project/foo/another.js')],
[p('/project/foo/link-to-bar.js'), p('/project/bar.js')],
[p('link-to-foo/link-to-another.js'), p('/project/foo/another.js')],
[p('/project/root/outside/external.js'), p('/outside/external.js')],
[p('/outside/../project/bar.js'), p('/project/bar.js')],
])('%s -> %s', (givenPath, expectedRealPath) =>
expect(tfs.getRealPath(givenPath)).toEqual(expectedRealPath),
);

test.each([
[p('/project/foo')],
[p('/project/bar.js/bad-parent')],
[p('/project/root/outside')],
[p('/project/link-to-nowhere')],
[p('/project/not/exists')],
])('returns null for directories or broken paths: %s', givenPath =>
expect(tfs.getRealPath(givenPath)).toEqual(null),
);
});

describe('matchFilesWithContext', () => {
test('non-recursive, skipping deep paths', () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('with package exports resolution disabled', () => {
}),
originModulePath: '/root/src/main.js',
unstable_enablePackageExports: false,
unstable_getRealPath: null,
};

expect(Resolver.resolve(context, 'test-pkg', null)).toEqual({
Expand Down
48 changes: 48 additions & 0 deletions packages/metro-resolver/src/__tests__/symlinks-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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
*/

'use strict';

import type {ResolutionContext} from '../index';

const FailedToResolvePathError = require('../errors/FailedToResolvePathError');
const Resolver = require('../index');
import {createResolutionContext} from './utils';

const fileMap = {
'/root/project/foo.js': '',
'/root/project/baz/index.js': '',
'/root/project/baz.js': {realPath: null},
'/root/project/link-to-foo.js': {realPath: '/root/project/foo.js'},
};

const CONTEXT: ResolutionContext = {
...createResolutionContext(fileMap, {enableSymlinks: true}),
originModulePath: '/root/project/foo.js',
};

it('resolves to a real path when the chosen candidate is a symlink', () => {
expect(Resolver.resolve(CONTEXT, './link-to-foo', null)).toEqual({
type: 'sourceFile',
filePath: '/root/project/foo.js',
});
});

it('does not resolve to a broken symlink', () => {
// ./baz.js is a broken link, baz/index.js is real
expect(() => Resolver.resolve(CONTEXT, './baz.js', null)).toThrow(
FailedToResolvePathError,
);
expect(Resolver.resolve(CONTEXT, './baz', null)).toEqual({
type: 'sourceFile',
filePath: '/root/project/baz/index.js',
});
});
24 changes: 22 additions & 2 deletions packages/metro-resolver/src/__tests__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import path from 'path';
* Data structure approximating a file tree. Should be populated with complete
* paths mapping to file contents.
*/
type MockFileMap = {[path: string]: string};
type MockFileMap = $ReadOnly<{
[path: string]: ?(string | $ReadOnly<{realPath: ?string}>),
}>;

/**
* Create a new partial `ResolutionContext` object given a mock file structure.
Expand All @@ -27,12 +29,12 @@ type MockFileMap = {[path: string]: string};
*/
export function createResolutionContext(
fileMap: MockFileMap,
{enableSymlinks}: $ReadOnly<{enableSymlinks?: boolean}> = {},
): $Diff<ResolutionContext, {originModulePath: string}> {
return {
allowHaste: true,
customResolverOptions: {},
disableHierarchicalLookup: false,
doesFileExist: (filePath: string) => filePath in fileMap,
extraNodeModules: null,
isAssetFile: () => false,
mainFields: ['browser', 'main'],
Expand All @@ -50,6 +52,24 @@ export function createResolutionContext(
unstable_enablePackageExports: false,
unstable_logWarning: () => {},
...createPackageAccessors(fileMap),
...(enableSymlinks === true
? {
doesFileExist: (filePath: string) =>
// Should return false unless realpath(filePath) exists. We mock shallow
// dereferencing.
fileMap[filePath] != null &&
(typeof fileMap[filePath] === 'string' ||
typeof fileMap[filePath].realPath === 'string'),
unstable_getRealPath: filePath =>
typeof fileMap[filePath] === 'string'
? filePath
: fileMap[filePath]?.realPath,
}
: {
doesFileExist: (filePath: string) =>
typeof fileMap[filePath] === 'string',
unstable_getRealPath: null,
}),
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/metro-resolver/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
FileAndDirCandidates,
FileCandidates,
FileResolution,
GetRealPath,
IsAssetFile,
ResolutionContext,
Resolution,
Expand Down
7 changes: 6 additions & 1 deletion packages/metro-resolver/src/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,12 @@ function resolveSourceFileForExt(
if (redirectedPath === false) {
return {type: 'empty'};
}
if (context.doesFileExist(redirectedPath)) {
if (context.unstable_getRealPath) {
const maybeRealPath = context.unstable_getRealPath(redirectedPath);
if (maybeRealPath != null) {
return maybeRealPath;
}
} else if (context.doesFileExist(redirectedPath)) {
return redirectedPath;
}
context.candidateExts.push(extension);
Expand Down
2 changes: 2 additions & 0 deletions packages/metro-resolver/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type PackageInfo = $ReadOnly<{
* Check existence of a single file.
*/
export type DoesFileExist = (filePath: string) => boolean;
export type GetRealPath = (path: string) => ?string;
export type IsAssetFile = (fileName: string) => boolean;

/**
Expand Down Expand Up @@ -141,6 +142,7 @@ export type ResolutionContext = $ReadOnly<{
[platform: string]: $ReadOnlyArray<string>,
}>,
unstable_enablePackageExports: boolean,
unstable_getRealPath?: ?GetRealPath,
unstable_logWarning: (message: string) => void,
}>;

Expand Down
27 changes: 23 additions & 4 deletions packages/metro/src/node-haste/DependencyGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,23 @@ class DependencyGraph extends EventEmitter {
reporter: this._config.reporter,
resolveAsset: (dirPath: string, assetName: string, extension: string) => {
const basePath = dirPath + path.sep + assetName;
const assets = [
let assets = [
basePath + extension,
...this._config.resolver.assetResolutions.map(
resolution => basePath + '@' + resolution + 'x' + extension,
),
].filter(candidate => this._fileSystem.exists(candidate));
];

if (this._config.resolver.unstable_enableSymlinks) {
assets = assets
.map(candidate => this._fileSystem.getRealPath(candidate))
.filter(Boolean);
} else {
assets = assets.filter(candidate =>
this._fileSystem.exists(candidate),
);
}

return assets.length ? assets : null;
},
resolveRequest: this._config.resolver.resolveRequest,
Expand All @@ -215,6 +226,9 @@ class DependencyGraph extends EventEmitter {
this._config.resolver.unstable_conditionsByPlatform,
unstable_enablePackageExports:
this._config.resolver.unstable_enablePackageExports,
unstable_getRealPath: this._config.resolver.unstable_enableSymlinks
? path => this._fileSystem.getRealPath(path)
: null,
});
}

Expand All @@ -237,14 +251,19 @@ class DependencyGraph extends EventEmitter {
const containerName =
splitIndex !== -1 ? filename.slice(0, splitIndex + 4) : filename;

// TODO Calling realpath allows us to get a hash for a given path even when
// Prior to unstable_enableSymlinks:
// Calling realpath allows us to get a hash for a given path even when
// it's a symlink to a file, which prevents Metro from crashing in such a
// case. However, it doesn't allow Metro to track changes to the target file
// of the symlink. We should fix this by implementing a symlink map into
// Metro (or maybe by implementing those "extra transformation sources" we've
// been talking about for stuff like CSS or WASM).
//
// This is unnecessary with a symlink-aware fileSystem implementation.
const resolvedPath = this._config.resolver.unstable_enableSymlinks
? containerName
: fs.realpathSync(containerName);

const resolvedPath = fs.realpathSync(containerName);
const sha1 = this._fileSystem.getSha1(resolvedPath);

if (!sha1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
CustomResolver,
DoesFileExist,
FileCandidates,
GetRealPath,
IsAssetFile,
Resolution,
ResolveAsset,
Expand Down Expand Up @@ -76,6 +77,7 @@ type Options<TPackage> = $ReadOnly<{
[platform: string]: $ReadOnlyArray<string>,
}>,
unstable_enablePackageExports: boolean,
unstable_getRealPath: ?GetRealPath,
}>;

class ModuleResolver<TPackage: Packageish> {
Expand Down Expand Up @@ -138,6 +140,7 @@ class ModuleResolver<TPackage: Packageish> {
unstable_conditionNames,
unstable_conditionsByPlatform,
unstable_enablePackageExports,
unstable_getRealPath,
} = this._options;

try {
Expand All @@ -157,6 +160,7 @@ class ModuleResolver<TPackage: Packageish> {
unstable_conditionNames,
unstable_conditionsByPlatform,
unstable_enablePackageExports,
unstable_getRealPath,
unstable_logWarning: this._logWarning,
customResolverOptions: resolverOptions.customResolverOptions ?? {},
originModulePath: fromModule.path,
Expand Down

0 comments on commit a1e233c

Please sign in to comment.