Skip to content

Commit

Permalink
Move resolveRequest to the front of the resolution algorithm
Browse files Browse the repository at this point in the history
Summary:
Changelog:

* **[Breaking]** Changes to the [custom resolver](https://facebook.github.io/metro/docs/configuration/#resolverequest) API.
  * Custom resolvers are called before `package.json`-based redirections and no longer receive a separate `realModuleName` argument.
  * Throwing an error in a custom resolver terminates the resolution algorithm.
  * Custom resolvers must always either return a (non-null) resolution, or throw.
  * Custom resolvers receive a modified, read-only `context` argument, containing the standard resolver as `context.resolveRequest` for easy chaining.

Reviewed By: javache

Differential Revision: D33842765

fbshipit-source-id: 81a6c052f238c4738bd17a0ee8e5483a456c71cb
  • Loading branch information
motiz88 authored and facebook-github-bot committed Jan 31, 2022
1 parent ccfec1e commit d81d887
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 110 deletions.
20 changes: 12 additions & 8 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,20 @@ This option works similarly to how [$NODE_PATH](https://nodejs.org/api/modules.h

Type: `?CustomResolver`

An optional function used to resolve requests. Particularly useful for cases where aliases are used. For example:
An optional function used to resolve requests. Particularly useful for cases where aliases or custom protocols are used. For example:

```javascript
resolveRequest: (context, realModuleName, platform, moduleName) => {
// Resolve file path logic...

return {
filePath: "path/to/file",
type: 'sourceFile',
};
resolveRequest: (context, moduleName, platform) => {
if (moduleName.startsWith('my-custom-resolver:')) {
// Resolve file path logic...
// NOTE: Throw an error if there is no resolution.
return {
filePath: "path/to/file",
type: 'sourceFile',
};
}
// Optionally, chain to the standard Metro resolver.
return context.resolveRequest(context, moduleName, platform);
}
```

Expand Down
133 changes: 96 additions & 37 deletions packages/metro-resolver/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,9 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
context,
{...context, resolveRequest: Resolver.resolve},
'does-not-exist',
null,
'does-not-exist',
);
});

Expand All @@ -532,10 +531,9 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
context,
{...context, resolveRequest: Resolver.resolve},
'./does-not-exist',
null,
'./does-not-exist',
);
});

Expand All @@ -548,10 +546,9 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
context,
{...context, resolveRequest: Resolver.resolve},
'/does-not-exist',
null,
'/does-not-exist',
);
});

Expand All @@ -564,10 +561,9 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
context,
{...context, resolveRequest: Resolver.resolve},
'some-package',
null,
'some-package',
);
});

Expand All @@ -578,10 +574,14 @@ describe('resolveRequest', () => {
}
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(context, 'Foo', null, 'Foo');
expect(resolveRequest).toBeCalledWith(
{...context, resolveRequest: Resolver.resolve},
'Foo',
null,
);
});

it('is called with the platform and redirected module path', () => {
it('is called with the platform and non-redirected module path', () => {
const contextWithRedirect = Object.assign({}, context, {
redirectModulePath: filePath => filePath + '.redirected',
});
Expand All @@ -593,37 +593,44 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
contextWithRedirect,
'does-not-exist.redirected',
'android',
{...contextWithRedirect, resolveRequest: Resolver.resolve},
'does-not-exist',
'android',
);
});

it('is not called if redirectModulePath returns false', () => {
it('is called if redirectModulePath returns false', () => {
resolveRequest.mockImplementation(() => ({
type: 'sourceFile',
filePath: '/some/fake/path',
}));
const contextWithRedirect = Object.assign({}, context, {
redirectModulePath: filePath => false,
});
expect(Resolver.resolve(contextWithRedirect, 'does-not-exist', 'android'))
.toMatchInlineSnapshot(`
Object {
"type": "empty",
"filePath": "/some/fake/path",
"type": "sourceFile",
}
`);
expect(resolveRequest).not.toBeCalled();
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
{...contextWithRedirect, resolveRequest: Resolver.resolve},
'does-not-exist',
'android',
);
});

it('can forward requests to the standard resolver', () => {
// This test shows a common pattern for wrapping the standard resolver.
resolveRequest.mockImplementation(
(ctx, realModuleName, platform, moduleName) => {
return Resolver.resolve(
Object.assign({}, ctx, {resolveRequest: null}),
moduleName,
platform,
);
},
);
resolveRequest.mockImplementation((ctx, moduleName, platform) => {
return Resolver.resolve(
Object.assign({}, ctx, {resolveRequest: null}),
moduleName,
platform,
);
});
expect(() => {
Resolver.resolve(context, 'does-not-exist', 'android');
}).toThrowErrorMatchingInlineSnapshot(`
Expand All @@ -635,30 +642,82 @@ describe('resolveRequest', () => {
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
context,
{...context, resolveRequest: Resolver.resolve},
'does-not-exist',
'android',
'does-not-exist',
);
});

it('can forward Haste requests to the standard resolver', () => {
resolveRequest.mockImplementation(
(ctx, realModuleName, platform, moduleName) => {
return Resolver.resolve(
{...ctx, resolveRequest: null},
moduleName,
platform,
);
},
);
resolveRequest.mockImplementation((ctx, moduleName, platform) => {
return Resolver.resolve(
{...ctx, resolveRequest: null},
moduleName,
platform,
);
});
expect(Resolver.resolve(context, 'Foo', null)).toMatchInlineSnapshot(`
Object {
"filePath": "/haste/Foo.js",
"type": "sourceFile",
}
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(context, 'Foo', null, 'Foo');
expect(resolveRequest).toBeCalledWith(
{...context, resolveRequest: Resolver.resolve},
'Foo',
null,
);
});

it('can forward requests to the standard resolver via resolveRequest', () => {
resolveRequest.mockImplementation((ctx, moduleName, platform) => {
return ctx.resolveRequest(ctx, moduleName, platform);
});
expect(() => {
Resolver.resolve(context, 'does-not-exist', 'android');
}).toThrowErrorMatchingInlineSnapshot(`
"Module does not exist in the Haste module map or in these directories:
/root/project/node_modules
/root/node_modules
/node_modules
"
`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
{...context, resolveRequest: Resolver.resolve},
'does-not-exist',
'android',
);
});

it('throwing an error stops the standard resolution', () => {
resolveRequest.mockImplementation((ctx, moduleName, platform) => {
throw new Error('Custom resolver hit an error');
});
const {resolveRequest: _, ...contextWithoutCustomResolver} = context;
// Ensure that the request has a standard resolution.
expect(
Resolver.resolve(
contextWithoutCustomResolver,
'/root/project/foo.js',
'android',
),
).toMatchInlineSnapshot(`
Object {
"filePath": "/root/project/foo.js",
"type": "sourceFile",
}
`);
// Ensure that we don't get this standard resolution if we throw.
expect(() => {
Resolver.resolve(context, '/root/project/foo.js', 'android');
}).toThrowErrorMatchingInlineSnapshot(`"Custom resolver hit an error"`);
expect(resolveRequest).toBeCalledTimes(1);
expect(resolveRequest).toBeCalledWith(
{...context, resolveRequest: Resolver.resolve},
'/root/project/foo.js',
'android',
);
});
});
8 changes: 5 additions & 3 deletions packages/metro-resolver/src/formatFileCandidates.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ function formatFileCandidates(candidates: FileCandidates): string {
if (candidates.type === 'asset') {
return candidates.name;
}
return `${candidates.filePathPrefix}(${candidates.candidateExts
.filter(Boolean)
.join('|')})`;
let formatted = candidates.filePathPrefix;
if (candidates.candidateExts.length) {
formatted += '(' + candidates.candidateExts.filter(Boolean).join('|') + ')';
}
return formatted;
}

module.exports = formatFileCandidates;
33 changes: 14 additions & 19 deletions packages/metro-resolver/src/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,18 @@ function resolve(
): Resolution {
const resolveRequest = context.resolveRequest;
if (
!resolveRequest &&
(isRelativeImport(moduleName) || isAbsolutePath(moduleName))
resolveRequest &&
// Prevent infinite recursion in the trivial case
resolveRequest !== resolve
) {
return resolveRequest(
Object.freeze({...context, resolveRequest: resolve}),
moduleName,
platform,
);
}

if (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) {
return resolveModulePath(context, moduleName, platform);
}

Expand All @@ -55,8 +64,7 @@ function resolve(
const isDirectImport =
isRelativeImport(realModuleName) || isAbsolutePath(realModuleName);

// We disable the direct file loading to let the custom resolvers deal with it
if (!resolveRequest && isDirectImport) {
if (isDirectImport) {
// derive absolute path /.../node_modules/originModuleDir/realModuleName
const fromModuleParentIdx =
originModulePath.lastIndexOf('node_modules' + path.sep) + 13;
Expand All @@ -68,31 +76,18 @@ function resolve(
return resolveModulePath(context, absPath, platform);
}

if (!resolveRequest && context.allowHaste && !isDirectImport) {
if (context.allowHaste && !isDirectImport) {
const normalizedName = normalizePath(realModuleName);
const result = resolveHasteName(context, normalizedName, platform);
if (result.type === 'resolved') {
return result.resolution;
}
}

if (resolveRequest) {
try {
const resolution = resolveRequest(
context,
realModuleName,
platform,
moduleName,
);
if (resolution) {
return resolution;
}
} catch (error) {}
}
const {disableHierarchicalLookup} = context;

const nodeModulesPaths = [];
let next = path.dirname(originModulePath);
const {disableHierarchicalLookup} = context;

if (!disableHierarchicalLookup) {
let candidate;
Expand Down

0 comments on commit d81d887

Please sign in to comment.