1import resolveFrom from 'resolve-from';
2
3const debug = require('debug')('expo:metro:import');
4
5// These resolvers enable us to test the CLI in older projects.
6// We may be able to get rid of this in the future.
7// TODO: Maybe combine with AsyncResolver?
8class MetroImportError extends Error {
9  constructor(projectRoot: string, moduleId: string) {
10    super(
11      `Missing package "${moduleId}" in the project at: ${projectRoot}\n` +
12        'This usually means "react-native" is not installed. ' +
13        'Please verify that dependencies in package.json include "react-native" ' +
14        'and run `yarn` or `npm install`.'
15    );
16  }
17}
18
19export function importCliServerApiFromProject(
20  projectRoot: string
21): typeof import('@react-native-community/cli-server-api') {
22  return importFromProject(projectRoot, '@react-native-community/cli-server-api');
23}
24
25export function importMetroSourceMapComposeSourceMapsFromProject(
26  projectRoot: string
27): typeof import('metro-source-map').composeSourceMaps {
28  return importFromProject(projectRoot, 'metro-source-map/src/composeSourceMaps');
29}
30
31export function resolveFromProject(projectRoot: string, moduleId: string) {
32  const resolvedPath = resolveFrom.silent(projectRoot, moduleId);
33  if (!resolvedPath) {
34    throw new MetroImportError(projectRoot, moduleId);
35  }
36  return resolvedPath;
37}
38
39function importFromProject(projectRoot: string, moduleId: string) {
40  return require(resolveFromProject(projectRoot, moduleId));
41}
42
43/** Import `metro` from the project. */
44export function importMetroFromProject(projectRoot: string): typeof import('metro') {
45  return importFromProject(projectRoot, 'metro');
46}
47export function importMetroServerFromProject(projectRoot: string): typeof import('metro').Server {
48  return importFromProject(projectRoot, 'metro/src/Server');
49}
50export function importMetroCreateWebsocketServerFromProject(
51  projectRoot: string
52): typeof import('metro/src/lib/createWebsocketServer').createWebsocketServer {
53  return importFromProject(projectRoot, 'metro/src/lib/createWebsocketServer');
54}
55export function importMetroHmrServerFromProject(
56  projectRoot: string
57): typeof import('metro/src/HmrServer').MetroHmrServer {
58  return importFromProject(projectRoot, 'metro/src/HmrServer');
59}
60
61export function importExpoMetroConfig(projectRoot: string) {
62  return importFromProjectOrFallback<typeof import('@expo/metro-config')>(
63    projectRoot,
64    '@expo/metro-config'
65  );
66}
67
68/**
69 * Attempt to use the local version of a module or fallback on the CLI version.
70 * This should only ever happen when testing Expo CLI in development.
71 */
72export function importFromProjectOrFallback<TModule>(
73  projectRoot: string,
74  moduleId: string
75): TModule {
76  const resolvedPath = resolveFrom.silent(projectRoot, moduleId);
77  if (!resolvedPath) {
78    debug(`requiring "${moduleId}" relative to the CLI`);
79    return require(require.resolve(moduleId));
80  }
81  debug(`requiring "${moduleId}" from the project:`, resolvedPath);
82  return require(resolvedPath);
83}
84
85/** Import `metro-resolver` from the project. */
86export function importMetroResolverFromProject(
87  projectRoot: string
88): typeof import('metro-resolver') {
89  return importFromProject(projectRoot, 'metro-resolver');
90}
91
92/** Import `metro-inspector-proxy` from the project. */
93export function importMetroInspectorProxyFromProject(
94  projectRoot: string
95): typeof import('metro-inspector-proxy') {
96  return importFromProject(projectRoot, 'metro-inspector-proxy');
97}
98
99/** Import `metro-inspector-proxy/src/Device` from the project. */
100export function importMetroInspectorDeviceFromProject(
101  projectRoot: string
102): typeof import('metro-inspector-proxy/src/Device') {
103  return importFromProject(projectRoot, 'metro-inspector-proxy/src/Device');
104}
105
106/**
107 * Import the internal `saveAssets()` function from `react-native` for the purpose
108 * of saving production assets as-is instead of converting them to a hash.
109 */
110export function importCliSaveAssetsFromProject(
111  projectRoot: string
112): typeof import('@react-native-community/cli-plugin-metro/build/commands/bundle/saveAssets').default {
113  return importFromProject(
114    projectRoot,
115    '@react-native-community/cli-plugin-metro/build/commands/bundle/saveAssets'
116  ).default;
117}
118
119/** Resolve the installed Metro version from project */
120export function resolveMetroVersionFromProject(projectRoot: string): string {
121  return importFromProject(projectRoot, 'metro/package.json').version;
122}
123