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
19function resolveFromProject(projectRoot: string, moduleId: string) {
20  const resolvedPath = resolveFrom.silent(projectRoot, moduleId);
21  if (!resolvedPath) {
22    throw new MetroImportError(projectRoot, moduleId);
23  }
24  return resolvedPath;
25}
26
27function importFromProject(projectRoot: string, moduleId: string) {
28  return require(resolveFromProject(projectRoot, moduleId));
29}
30
31/** Import `metro` from the project. */
32export function importMetroFromProject(projectRoot: string): typeof import('metro') {
33  return importFromProject(projectRoot, 'metro');
34}
35export function importMetroCreateWebsocketServerFromProject(
36  projectRoot: string
37): typeof import('metro/src/lib/createWebsocketServer').createWebsocketServer {
38  return importFromProject(projectRoot, 'metro/src/lib/createWebsocketServer');
39}
40export function importMetroHmrServerFromProject(
41  projectRoot: string
42): typeof import('metro/src/HmrServer').MetroHmrServer {
43  return importFromProject(projectRoot, 'metro/src/HmrServer');
44}
45
46export function importExpoMetroConfig(projectRoot: string) {
47  return importFromProjectOrFallback<typeof import('@expo/metro-config')>(
48    projectRoot,
49    '@expo/metro-config'
50  );
51}
52
53/**
54 * Attempt to use the local version of a module or fallback on the CLI version.
55 * This should only ever happen when testing Expo CLI in development.
56 */
57export function importFromProjectOrFallback<TModule>(
58  projectRoot: string,
59  moduleId: string
60): TModule {
61  const resolvedPath = resolveFrom.silent(projectRoot, moduleId);
62  if (!resolvedPath) {
63    debug(`requiring "${moduleId}" relative to the CLI`);
64    return require(require.resolve(moduleId));
65  }
66  debug(`requiring "${moduleId}" from the project:`, resolvedPath);
67  return require(resolvedPath);
68}
69
70/** Import `metro-resolver` from the project. */
71export function importMetroResolverFromProject(
72  projectRoot: string
73): typeof import('metro-resolver') {
74  return importFromProject(projectRoot, 'metro-resolver');
75}
76
77/** Import `metro-inspector-proxy` from the project. */
78export function importMetroInspectorProxyFromProject(
79  projectRoot: string
80): typeof import('metro-inspector-proxy') {
81  return importFromProject(projectRoot, 'metro-inspector-proxy');
82}
83
84/** Import `metro-inspector-proxy/src/Device` from the project. */
85export function importMetroInspectorDeviceFromProject(
86  projectRoot: string
87): typeof import('metro-inspector-proxy/src/Device') {
88  return importFromProject(projectRoot, 'metro-inspector-proxy/src/Device');
89}
90
91/**
92 * Import the internal `saveAssets()` function from `react-native` for the purpose
93 * of saving production assets as-is instead of converting them to a hash.
94 */
95export function importCliSaveAssetsFromProject(
96  projectRoot: string
97): typeof import('@react-native-community/cli-plugin-metro/build/commands/bundle/saveAssets').default {
98  return importFromProject(
99    projectRoot,
100    '@react-native-community/cli-plugin-metro/build/commands/bundle/saveAssets'
101  ).default;
102}
103
104export function importCliBuildBundleWithConfigFromProject(
105  projectRoot: string
106): typeof import('@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle').buildBundleWithConfig {
107  return importFromProject(
108    projectRoot,
109    '@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle'
110  ).buildBundleWithConfig;
111}
112
113/** Resolve the installed Metro version from project */
114export function resolveMetroVersionFromProject(projectRoot: string): string {
115  return importFromProject(projectRoot, 'metro/package.json').version;
116}
117