1082815dcSEvan Baconimport assert from 'assert';
2082815dcSEvan Baconimport findUp from 'find-up';
3082815dcSEvan Baconimport * as path from 'path';
4082815dcSEvan Baconimport resolveFrom from 'resolve-from';
5082815dcSEvan Bacon
6082815dcSEvan Baconimport { PluginError } from './errors';
7082815dcSEvan Baconimport { fileExists } from './modules';
8*8a424bebSJames Ideimport { ConfigPlugin, StaticPlugin } from '../Plugin.types';
9082815dcSEvan Bacon
10082815dcSEvan Bacon// Default plugin entry file name.
11082815dcSEvan Baconexport const pluginFileName = 'app.plugin.js';
12082815dcSEvan Bacon
13082815dcSEvan Baconfunction findUpPackageJson(root: string): string {
14082815dcSEvan Bacon  const packageJson = findUp.sync('package.json', { cwd: root });
15082815dcSEvan Bacon  assert(packageJson, `No package.json found for module "${root}"`);
16082815dcSEvan Bacon  return packageJson;
17082815dcSEvan Bacon}
18082815dcSEvan Bacon
19082815dcSEvan Baconexport function resolvePluginForModule(projectRoot: string, modulePath: string) {
20082815dcSEvan Bacon  const resolved = resolveFrom.silent(projectRoot, modulePath);
21082815dcSEvan Bacon  if (!resolved) {
22082815dcSEvan Bacon    throw new PluginError(
23082815dcSEvan Bacon      `Failed to resolve plugin for module "${modulePath}" relative to "${projectRoot}"`,
24082815dcSEvan Bacon      'PLUGIN_NOT_FOUND'
25082815dcSEvan Bacon    );
26082815dcSEvan Bacon  }
27082815dcSEvan Bacon  // If the modulePath is something like `@bacon/package/index.js` or `expo-foo/build/app`
28082815dcSEvan Bacon  // then skip resolving the module `app.plugin.js`
29082815dcSEvan Bacon  if (moduleNameIsDirectFileReference(modulePath)) {
30082815dcSEvan Bacon    return { isPluginFile: false, filePath: resolved };
31082815dcSEvan Bacon  }
32082815dcSEvan Bacon  return findUpPlugin(resolved);
33082815dcSEvan Bacon}
34082815dcSEvan Bacon
35082815dcSEvan Bacon// TODO: Test windows
36082815dcSEvan Baconfunction pathIsFilePath(name: string): boolean {
37082815dcSEvan Bacon  // Matches lines starting with: . / ~/
38082815dcSEvan Bacon  return !!name.match(/^(\.|~\/|\/)/g);
39082815dcSEvan Bacon}
40082815dcSEvan Bacon
41082815dcSEvan Baconexport function moduleNameIsDirectFileReference(name: string): boolean {
42082815dcSEvan Bacon  if (pathIsFilePath(name)) {
43082815dcSEvan Bacon    return true;
44082815dcSEvan Bacon  }
45082815dcSEvan Bacon
46082815dcSEvan Bacon  const slashCount = name.split(path.sep)?.length;
47082815dcSEvan Bacon  // Orgs (like @expo/config ) should have more than one slash to be a direct file.
48082815dcSEvan Bacon  if (name.startsWith('@')) {
49082815dcSEvan Bacon    return slashCount > 2;
50082815dcSEvan Bacon  }
51082815dcSEvan Bacon
52082815dcSEvan Bacon  // Regular packages should be considered direct reference if they have more than one slash.
53082815dcSEvan Bacon  return slashCount > 1;
54082815dcSEvan Bacon}
55082815dcSEvan Bacon
56082815dcSEvan Baconfunction resolveExpoPluginFile(root: string): string | null {
57082815dcSEvan Bacon  // Find the expo plugin root file
58082815dcSEvan Bacon  const pluginModuleFile = resolveFrom.silent(
59082815dcSEvan Bacon    root,
60082815dcSEvan Bacon    // use ./ so it isn't resolved as a node module
61082815dcSEvan Bacon    `./${pluginFileName}`
62082815dcSEvan Bacon  );
63082815dcSEvan Bacon
64082815dcSEvan Bacon  // If the default expo plugin file exists use it.
65082815dcSEvan Bacon  if (pluginModuleFile && fileExists(pluginModuleFile)) {
66082815dcSEvan Bacon    return pluginModuleFile;
67082815dcSEvan Bacon  }
68082815dcSEvan Bacon  return null;
69082815dcSEvan Bacon}
70082815dcSEvan Bacon
71082815dcSEvan Baconfunction findUpPlugin(root: string): { filePath: string; isPluginFile: boolean } {
72082815dcSEvan Bacon  // Get the closest package.json to the node module
73082815dcSEvan Bacon  const packageJson = findUpPackageJson(root);
74082815dcSEvan Bacon  // resolve the root folder for the node module
75082815dcSEvan Bacon  const moduleRoot = path.dirname(packageJson);
76082815dcSEvan Bacon  // use whatever the initial resolved file was ex: `node_modules/my-package/index.js` or `./something.js`
77082815dcSEvan Bacon  const pluginFile = resolveExpoPluginFile(moduleRoot);
78082815dcSEvan Bacon  return { filePath: pluginFile ?? root, isPluginFile: !!pluginFile };
79082815dcSEvan Bacon}
80082815dcSEvan Bacon
81082815dcSEvan Baconexport function normalizeStaticPlugin(plugin: StaticPlugin | ConfigPlugin | string): StaticPlugin {
82082815dcSEvan Bacon  if (Array.isArray(plugin)) {
83082815dcSEvan Bacon    assert(
84082815dcSEvan Bacon      plugin.length > 0 && plugin.length < 3,
85082815dcSEvan Bacon      `Wrong number of arguments provided for static config plugin, expected either 1 or 2, got ${plugin.length}`
86082815dcSEvan Bacon    );
87082815dcSEvan Bacon    return plugin;
88082815dcSEvan Bacon  }
89082815dcSEvan Bacon  return [plugin, undefined];
90082815dcSEvan Bacon}
91082815dcSEvan Bacon
92082815dcSEvan Baconexport function assertInternalProjectRoot(projectRoot?: string): asserts projectRoot {
93082815dcSEvan Bacon  assert(
94082815dcSEvan Bacon    projectRoot,
95082815dcSEvan Bacon    `Unexpected: Config \`_internal.projectRoot\` isn't defined by expo-cli, this is a bug.`
96082815dcSEvan Bacon  );
97082815dcSEvan Bacon}
98082815dcSEvan Bacon
99082815dcSEvan Bacon// Resolve the module function and assert type
100082815dcSEvan Baconexport function resolveConfigPluginFunction(projectRoot: string, pluginReference: string) {
101082815dcSEvan Bacon  const { plugin } = resolveConfigPluginFunctionWithInfo(projectRoot, pluginReference);
102082815dcSEvan Bacon  return plugin;
103082815dcSEvan Bacon}
104082815dcSEvan Bacon
105082815dcSEvan Bacon// Resolve the module function and assert type
106082815dcSEvan Baconexport function resolveConfigPluginFunctionWithInfo(projectRoot: string, pluginReference: string) {
107082815dcSEvan Bacon  const { filePath: pluginFile, isPluginFile } = resolvePluginForModule(
108082815dcSEvan Bacon    projectRoot,
109082815dcSEvan Bacon    pluginReference
110082815dcSEvan Bacon  );
111082815dcSEvan Bacon  let result: any;
112082815dcSEvan Bacon  try {
113082815dcSEvan Bacon    result = requirePluginFile(pluginFile);
114082815dcSEvan Bacon  } catch (error) {
115082815dcSEvan Bacon    if (error instanceof SyntaxError) {
116082815dcSEvan Bacon      const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`;
117082815dcSEvan Bacon      // If the plugin reference is a node module, and that node module has a syntax error, then it probably doesn't have an official config plugin.
118082815dcSEvan Bacon      if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) {
119082815dcSEvan Bacon        const pluginError = new PluginError(
120082815dcSEvan Bacon          `Package "${pluginReference}" does not contain a valid config plugin.\n${learnMoreLink}\n\n${error.message}`,
121082815dcSEvan Bacon          'INVALID_PLUGIN_IMPORT'
122082815dcSEvan Bacon        );
123082815dcSEvan Bacon        pluginError.stack = error.stack;
124082815dcSEvan Bacon        throw pluginError;
125082815dcSEvan Bacon      }
126082815dcSEvan Bacon    }
127082815dcSEvan Bacon    throw error;
128082815dcSEvan Bacon  }
129082815dcSEvan Bacon
130082815dcSEvan Bacon  const plugin = resolveConfigPluginExport({
131082815dcSEvan Bacon    plugin: result,
132082815dcSEvan Bacon    pluginFile,
133082815dcSEvan Bacon    pluginReference,
134082815dcSEvan Bacon    isPluginFile,
135082815dcSEvan Bacon  });
136082815dcSEvan Bacon  return { plugin, pluginFile, pluginReference, isPluginFile };
137082815dcSEvan Bacon}
138082815dcSEvan Bacon
139082815dcSEvan Bacon/**
140082815dcSEvan Bacon * - Resolve the exported contents of an Expo config (be it default or module.exports)
141082815dcSEvan Bacon * - Assert no promise exports
142082815dcSEvan Bacon * - Return config type
143082815dcSEvan Bacon * - Serialize config
144082815dcSEvan Bacon *
145082815dcSEvan Bacon * @param props.plugin plugin results
146082815dcSEvan Bacon * @param props.pluginFile plugin file path
147082815dcSEvan Bacon * @param props.pluginReference the string used to reference the plugin
148082815dcSEvan Bacon * @param props.isPluginFile is file path from the app.plugin.js module root
149082815dcSEvan Bacon */
150082815dcSEvan Baconexport function resolveConfigPluginExport({
151082815dcSEvan Bacon  plugin,
152082815dcSEvan Bacon  pluginFile,
153082815dcSEvan Bacon  pluginReference,
154082815dcSEvan Bacon  isPluginFile,
155082815dcSEvan Bacon}: {
156082815dcSEvan Bacon  plugin: any;
157082815dcSEvan Bacon  pluginFile: string;
158082815dcSEvan Bacon  pluginReference: string;
159082815dcSEvan Bacon  isPluginFile: boolean;
160082815dcSEvan Bacon}): ConfigPlugin<unknown> {
161082815dcSEvan Bacon  if (plugin.default != null) {
162082815dcSEvan Bacon    plugin = plugin.default;
163082815dcSEvan Bacon  }
164082815dcSEvan Bacon  if (typeof plugin !== 'function') {
165082815dcSEvan Bacon    const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`;
166082815dcSEvan Bacon    // If the plugin reference is a node module, and that node module does not export a function then it probably doesn't have a config plugin.
167082815dcSEvan Bacon    if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) {
168082815dcSEvan Bacon      throw new PluginError(
169082815dcSEvan Bacon        `Package "${pluginReference}" does not contain a valid config plugin. Module must export a function from file: ${pluginFile}\n${learnMoreLink}`,
170082815dcSEvan Bacon        'INVALID_PLUGIN_TYPE'
171082815dcSEvan Bacon      );
172082815dcSEvan Bacon    }
173082815dcSEvan Bacon    throw new PluginError(
174082815dcSEvan Bacon      `Plugin "${pluginReference}" must export a function from file: ${pluginFile}. ${learnMoreLink}`,
175082815dcSEvan Bacon      'INVALID_PLUGIN_TYPE'
176082815dcSEvan Bacon    );
177082815dcSEvan Bacon  }
178082815dcSEvan Bacon
179082815dcSEvan Bacon  return plugin;
180082815dcSEvan Bacon}
181082815dcSEvan Bacon
182082815dcSEvan Baconfunction requirePluginFile(filePath: string): any {
183082815dcSEvan Bacon  try {
184082815dcSEvan Bacon    return require(filePath);
185082815dcSEvan Bacon  } catch (error) {
186082815dcSEvan Bacon    // TODO: Improve error messages
187082815dcSEvan Bacon    throw error;
188082815dcSEvan Bacon  }
189082815dcSEvan Bacon}
190