1082815dcSEvan Baconimport Debug from 'debug';
2082815dcSEvan Bacon
3*8a424bebSJames Ideimport { BaseModOptions, withBaseMod } from './withMod';
4082815dcSEvan Baconimport {
5082815dcSEvan Bacon  ConfigPlugin,
6082815dcSEvan Bacon  ExportedConfig,
7082815dcSEvan Bacon  ExportedConfigWithProps,
8082815dcSEvan Bacon  ModPlatform,
9082815dcSEvan Bacon} from '../Plugin.types';
10082815dcSEvan Bacon
11082815dcSEvan Baconconst debug = Debug('expo:config-plugins:base-mods');
12082815dcSEvan Bacon
13082815dcSEvan Baconexport type ForwardedBaseModOptions = Partial<
14082815dcSEvan Bacon  Pick<BaseModOptions, 'saveToInternal' | 'skipEmptyMod'>
15082815dcSEvan Bacon>;
16082815dcSEvan Bacon
17082815dcSEvan Baconexport type BaseModProviderMethods<
18082815dcSEvan Bacon  ModType,
19*8a424bebSJames Ide  Props extends ForwardedBaseModOptions = ForwardedBaseModOptions,
20082815dcSEvan Bacon> = {
21082815dcSEvan Bacon  getFilePath: (config: ExportedConfigWithProps<ModType>, props: Props) => Promise<string> | string;
22082815dcSEvan Bacon  read: (
23082815dcSEvan Bacon    filePath: string,
24082815dcSEvan Bacon    config: ExportedConfigWithProps<ModType>,
25082815dcSEvan Bacon    props: Props
26082815dcSEvan Bacon  ) => Promise<ModType> | ModType;
27082815dcSEvan Bacon  write: (
28082815dcSEvan Bacon    filePath: string,
29082815dcSEvan Bacon    config: ExportedConfigWithProps<ModType>,
30082815dcSEvan Bacon    props: Props
31082815dcSEvan Bacon  ) => Promise<void> | void;
32082815dcSEvan Bacon  /**
33082815dcSEvan Bacon   * If the mod supports introspection, and avoids making any filesystem modifications during compilation.
34082815dcSEvan Bacon   * By enabling, this mod, and all of its descendants will be run in introspection mode.
35082815dcSEvan Bacon   * This should only be used for static files like JSON or XML, and not for application files that require regexes,
36082815dcSEvan Bacon   * or complex static files that require other files to be generated like Xcode `.pbxproj`.
37082815dcSEvan Bacon   */
38082815dcSEvan Bacon  isIntrospective?: boolean;
39082815dcSEvan Bacon};
40082815dcSEvan Bacon
41082815dcSEvan Baconexport type CreateBaseModProps<
42082815dcSEvan Bacon  ModType,
43*8a424bebSJames Ide  Props extends ForwardedBaseModOptions = ForwardedBaseModOptions,
44082815dcSEvan Bacon> = {
45082815dcSEvan Bacon  methodName: string;
46082815dcSEvan Bacon  platform: ModPlatform;
47082815dcSEvan Bacon  modName: string;
48082815dcSEvan Bacon} & BaseModProviderMethods<ModType, Props>;
49082815dcSEvan Bacon
50082815dcSEvan Baconexport function createBaseMod<
51082815dcSEvan Bacon  ModType,
52*8a424bebSJames Ide  Props extends ForwardedBaseModOptions = ForwardedBaseModOptions,
53082815dcSEvan Bacon>({
54082815dcSEvan Bacon  methodName,
55082815dcSEvan Bacon  platform,
56082815dcSEvan Bacon  modName,
57082815dcSEvan Bacon  getFilePath,
58082815dcSEvan Bacon  read,
59082815dcSEvan Bacon  write,
60082815dcSEvan Bacon  isIntrospective,
61082815dcSEvan Bacon}: CreateBaseModProps<ModType, Props>): ConfigPlugin<Props | void> {
62082815dcSEvan Bacon  const withUnknown: ConfigPlugin<Props | void> = (config, _props) => {
63082815dcSEvan Bacon    const props = _props || ({} as Props);
64082815dcSEvan Bacon    return withBaseMod<ModType>(config, {
65082815dcSEvan Bacon      platform,
66082815dcSEvan Bacon      mod: modName,
67082815dcSEvan Bacon      skipEmptyMod: props.skipEmptyMod ?? true,
68082815dcSEvan Bacon      saveToInternal: props.saveToInternal ?? false,
69082815dcSEvan Bacon      isProvider: true,
70082815dcSEvan Bacon      isIntrospective,
71082815dcSEvan Bacon      async action({ modRequest: { nextMod, ...modRequest }, ...config }) {
72082815dcSEvan Bacon        try {
73082815dcSEvan Bacon          let results: ExportedConfigWithProps<ModType> = {
74082815dcSEvan Bacon            ...config,
75082815dcSEvan Bacon            modRequest,
76082815dcSEvan Bacon          };
77082815dcSEvan Bacon
78082815dcSEvan Bacon          const filePath = await getFilePath(results, props);
79082815dcSEvan Bacon          debug(`mods.${platform}.${modName}: file path: ${filePath || '[skipped]'}`);
80082815dcSEvan Bacon          const modResults = await read(filePath, results, props);
81082815dcSEvan Bacon
82082815dcSEvan Bacon          results = await nextMod!({
83082815dcSEvan Bacon            ...results,
84082815dcSEvan Bacon            modResults,
85082815dcSEvan Bacon            modRequest,
86082815dcSEvan Bacon          });
87082815dcSEvan Bacon
88082815dcSEvan Bacon          assertModResults(results, modRequest.platform, modRequest.modName);
89082815dcSEvan Bacon
90082815dcSEvan Bacon          await write(filePath, results, props);
91082815dcSEvan Bacon          return results;
92082815dcSEvan Bacon        } catch (error: any) {
93082815dcSEvan Bacon          error.message = `[${platform}.${modName}]: ${methodName}: ${error.message}`;
94082815dcSEvan Bacon          throw error;
95082815dcSEvan Bacon        }
96082815dcSEvan Bacon      },
97082815dcSEvan Bacon    });
98082815dcSEvan Bacon  };
99082815dcSEvan Bacon
100082815dcSEvan Bacon  if (methodName) {
101082815dcSEvan Bacon    Object.defineProperty(withUnknown, 'name', {
102082815dcSEvan Bacon      value: methodName,
103082815dcSEvan Bacon    });
104082815dcSEvan Bacon  }
105082815dcSEvan Bacon
106082815dcSEvan Bacon  return withUnknown;
107082815dcSEvan Bacon}
108082815dcSEvan Bacon
109082815dcSEvan Baconexport function assertModResults(results: any, platformName: string, modName: string) {
110082815dcSEvan Bacon  // If the results came from a mod, they'd be in the form of [config, data].
111082815dcSEvan Bacon  // Ensure the results are an array and omit the data since it should've been written by a data provider plugin.
112082815dcSEvan Bacon  const ensuredResults = results;
113082815dcSEvan Bacon
114082815dcSEvan Bacon  // Sanity check to help locate non compliant mods.
115082815dcSEvan Bacon  if (!ensuredResults || typeof ensuredResults !== 'object' || !ensuredResults?.mods) {
116082815dcSEvan Bacon    throw new Error(
117082815dcSEvan Bacon      `Mod \`mods.${platformName}.${modName}\` evaluated to an object that is not a valid project config. Instead got: ${JSON.stringify(
118082815dcSEvan Bacon        ensuredResults
119082815dcSEvan Bacon      )}`
120082815dcSEvan Bacon    );
121082815dcSEvan Bacon  }
122082815dcSEvan Bacon  return ensuredResults;
123082815dcSEvan Bacon}
124082815dcSEvan Bacon
125082815dcSEvan Baconfunction upperFirst(name: string): string {
126082815dcSEvan Bacon  return name.charAt(0).toUpperCase() + name.slice(1);
127082815dcSEvan Bacon}
128082815dcSEvan Bacon
129082815dcSEvan Baconexport function createPlatformBaseMod<
130082815dcSEvan Bacon  ModType,
131*8a424bebSJames Ide  Props extends ForwardedBaseModOptions = ForwardedBaseModOptions,
132082815dcSEvan Bacon>({ modName, ...props }: Omit<CreateBaseModProps<ModType, Props>, 'methodName'>) {
133082815dcSEvan Bacon  // Generate the function name to ensure it's uniform and also to improve stack traces.
134082815dcSEvan Bacon  const methodName = `with${upperFirst(props.platform)}${upperFirst(modName)}BaseMod`;
135082815dcSEvan Bacon  return createBaseMod<ModType, Props>({
136082815dcSEvan Bacon    methodName,
137082815dcSEvan Bacon    modName,
138082815dcSEvan Bacon    ...props,
139082815dcSEvan Bacon  });
140082815dcSEvan Bacon}
141082815dcSEvan Bacon
142082815dcSEvan Bacon/** A TS wrapper for creating provides */
143082815dcSEvan Baconexport function provider<ModType, Props extends ForwardedBaseModOptions = ForwardedBaseModOptions>(
144082815dcSEvan Bacon  props: BaseModProviderMethods<ModType, Props>
145082815dcSEvan Bacon) {
146082815dcSEvan Bacon  return props;
147082815dcSEvan Bacon}
148082815dcSEvan Bacon
149082815dcSEvan Bacon/** Plugin to create and append base mods from file providers */
150082815dcSEvan Baconexport function withGeneratedBaseMods<ModName extends string>(
151082815dcSEvan Bacon  config: ExportedConfig,
152082815dcSEvan Bacon  {
153082815dcSEvan Bacon    platform,
154082815dcSEvan Bacon    providers,
155082815dcSEvan Bacon    ...props
156082815dcSEvan Bacon  }: ForwardedBaseModOptions & {
157edc75823SEvan Bacon    /** Officially supports `'ios' | 'android'` (`ModPlatform`). Arbitrary strings are supported for adding out-of-tree platforms. */
158edc75823SEvan Bacon    platform: ModPlatform & string;
159082815dcSEvan Bacon    providers: Partial<Record<ModName, BaseModProviderMethods<any, any>>>;
160082815dcSEvan Bacon  }
161082815dcSEvan Bacon): ExportedConfig {
162082815dcSEvan Bacon  return Object.entries(providers).reduce((config, [modName, value]) => {
163082815dcSEvan Bacon    const baseMod = createPlatformBaseMod({ platform, modName, ...(value as any) });
164082815dcSEvan Bacon    return baseMod(config, props);
165082815dcSEvan Bacon  }, config);
166082815dcSEvan Bacon}
167