Repository navigation
Expand file tree
/
Copy pathmoduleMapper.ts
More file actions
151 lines (140 loc) · 6.41 KB
/
Copy pathmoduleMapper.ts
File metadata and controls
151 lines (140 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import BuiltinModule from 'module';
import path from 'path';
declare module 'module' {
namespace Module {
// Internal Function of Node.js:
// This is called when resolving a module,
// e.g. `require('./my-module')` -> `_resolveFilename('./my-module', ...)` is called
export function _resolveFilename(
request: string,
parent: { id: string; filename: string; paths: string[] } | string | null,
isMain?: boolean,
options?: { paths?: string[] }
): string;
}
}
const debug = require('debug')(
'expo:metro-config:supervising-transform-worker:module-mapper'
) as typeof console.log;
const requireResolveBasepath = (request: string, params?: { paths?: string[] }) =>
path.dirname(require.resolve(`${request}/package.json`, params));
const expoMetroBasepath = requireResolveBasepath('@expo/metro');
/** Modules that should be mapped to a different resolution path.
* @remarks
* This defines a list of packages we want to remap and their destinations.
* For each entry, the key is the module resolution to redirect, and the
* value is the path to resolve from.
* `createModuleMapper()` resolves these modules as if we were requiring
* them from the paths below.
*
* For example, for `expoMetroBasepath`, we're
* requiring this module as if we were inside `@expo/metro`.
*
* This means we'll always get that path's dependency.
*/
const MODULE_RESOLUTIONS: Record<string, string> = {
metro: expoMetroBasepath,
'metro-babel-transformer': expoMetroBasepath,
'metro-cache': expoMetroBasepath,
'metro-cache-key': expoMetroBasepath,
'metro-config': expoMetroBasepath,
'metro-core': expoMetroBasepath,
'metro-file-map': expoMetroBasepath,
'metro-resolver': expoMetroBasepath,
'metro-runtime': expoMetroBasepath,
'metro-source-map': expoMetroBasepath,
'metro-transform-plugins': expoMetroBasepath,
'metro-transform-worker': expoMetroBasepath,
'@expo/metro-config': requireResolveBasepath('expo'),
};
const escapeDependencyName = (dependency: string) =>
dependency.replace(/[*.?()[\]]/g, (x) => `\\${x}`);
const dependenciesToRegex = (dependencies: string[]) =>
new RegExp(`^(${dependencies.map(escapeDependencyName).join('|')})($|/.*)`);
/** Returns a resolver function that given a request to a module returns that module's remapped path. */
export const createModuleMapper = () => {
// Matches only module names, inside `MODULE_RESOLUTIONS`
const moduleTestRe = dependenciesToRegex(Object.keys(MODULE_RESOLUTIONS));
return (request: string): string | null => {
const moduleMatch = moduleTestRe.exec(request);
if (moduleMatch) {
// If the request is for a package in `MODULE_RESOLUTIONS`, we use
// the value in `MODULE_RESOLUTIONS` as a require path
const moduleSearchPath = MODULE_RESOLUTIONS[moduleMatch[1] as string];
if (moduleSearchPath) {
// Resolve the dependency request from `moduleSearchPath` instead of
// the transformer's own path
return require.resolve(request, { paths: [moduleSearchPath] });
}
}
return null;
};
};
/** Checks if we're either in a worker thread or a child process */
const isInForkedProcess = () =>
!require('worker_threads').isMainThread || typeof process.send === 'function';
let hasPatchedNodeModuleResolver = false;
/** Patches `Module._resolveFilename` (usually just does Node resolution) to override some requires and imports
* @remarks
* The user's transform worker (or their babel transformer, which is called inside the transform-worker) can
* import/require any version of metro, metro-*, or @expo/metro-config in theory. But Expo CLI uses a specific
* version of Metro.
* It's unsupported to use one version of Metro in Expo CLI but another in the transform worker or babel transformer,
* and while this *can work* sometimes, it's never correct.
*
* When called, this function modifies this Node.js thread's module resolution to redirect all imports for Metro
* packages or @expo/metro-config to the version that we know is correct.
*
* We know the versions we have are correct since we're inside @expo/metro-config in this file.
*
* NOTE: Bun also supports overriding `Module._resolveFilename`
*/
export const patchNodeModuleResolver = () => {
if (hasPatchedNodeModuleResolver) {
return;
} else if (!isInForkedProcess()) {
// If max-workers=0 is set for Metro, we will be transforming in the
// main thread (same thread as @expo/cli and Metro).
// We should not patch Module._resolveFilename if we're not in a
// separate Node.js thread to prevent `@expo/cli`'s imports from
// being manipulated. This is dangerous and it'd get hard to
// predict what would happen
debug('Module interception disabled: Not in a child process!');
}
hasPatchedNodeModuleResolver = true;
const moduleMapper = createModuleMapper();
// NOTE: Guard against mocks, see: https://github.com/danez/pirates/blob/5a81f70/lib/index.js#L8-L10
const Module: typeof BuiltinModule =
module.constructor.length > 1 ? (module.constructor as any) : BuiltinModule;
const originalResolveFilename = Module._resolveFilename;
let isInCustomResolver = false;
Module._resolveFilename = function (request, parent, isMain, options) {
if (!isInCustomResolver) {
try {
isInCustomResolver = true;
const parentId = typeof parent === 'string' ? parent : parent?.id;
if (parentId) {
// If the `transform-worker` requests a module in `MODULE_RESOLUTIONS`,
// we redirect the request to a different resolution path
// This path is based on requiring as if we're in a different module
// For example,
// 1. the user's transform-worker imports `metro-transform-worker`
// 2. this matches in `moduleMapper` and we get a replacement path
// 3. we return this redirect path here
// 4. the user's transform-worker now imports `metro-transform-worker` from `@expo/metro`'s dependencies instead
const redirectedRequest = moduleMapper(request);
if (redirectedRequest) {
debug(`Redirected request "${request}" -> "${redirectedRequest}"`);
return redirectedRequest;
}
}
} catch (error) {
debug(`Could not redirect request "${request}": ${error}`);
} finally {
// This guards against infinite recursion
isInCustomResolver = false;
}
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
};