1082815dcSEvan Baconimport fs from 'fs';
2082815dcSEvan Baconimport path from 'path';
3082815dcSEvan Bacon
4082815dcSEvan Baconimport { getAppDelegate, getSourceRoot } from './Paths';
5082815dcSEvan Baconimport { withBuildSourceFile } from './XcodeProjectFile';
6082815dcSEvan Baconimport { addResourceFileToGroup, getProjectName } from './utils/Xcodeproj';
7*8a424bebSJames Ideimport { ConfigPlugin, XcodeProject } from '../Plugin.types';
8*8a424bebSJames Ideimport { withXcodeProject } from '../plugins/ios-plugins';
9082815dcSEvan Bacon
10082815dcSEvan Baconconst templateBridgingHeader = `//
11082815dcSEvan Bacon//  Use this file to import your target's public headers that you would like to expose to Swift.
12082815dcSEvan Bacon//
13082815dcSEvan Bacon`;
14082815dcSEvan Bacon
15082815dcSEvan Bacon/**
16082815dcSEvan Bacon * Ensure a Swift bridging header is created for the project.
17082815dcSEvan Bacon * This helps fix problems related to using modules that are written in Swift (lottie, FBSDK).
18082815dcSEvan Bacon *
19082815dcSEvan Bacon * 1. Ensures the file exists given the project path.
20082815dcSEvan Bacon * 2. Writes the file and links to Xcode as a resource file.
21082815dcSEvan Bacon * 3. Sets the build configuration `SWIFT_OBJC_BRIDGING_HEADER = [PROJECT_NAME]-Bridging-Header.h`
22082815dcSEvan Bacon */
23082815dcSEvan Baconexport const withSwiftBridgingHeader: ConfigPlugin = (config) => {
24082815dcSEvan Bacon  return withXcodeProject(config, (config) => {
25082815dcSEvan Bacon    config.modResults = ensureSwiftBridgingHeaderSetup({
26082815dcSEvan Bacon      project: config.modResults,
27082815dcSEvan Bacon      projectRoot: config.modRequest.projectRoot,
28082815dcSEvan Bacon    });
29082815dcSEvan Bacon    return config;
30082815dcSEvan Bacon  });
31082815dcSEvan Bacon};
32082815dcSEvan Bacon
33082815dcSEvan Baconexport function ensureSwiftBridgingHeaderSetup({
34082815dcSEvan Bacon  projectRoot,
35082815dcSEvan Bacon  project,
36082815dcSEvan Bacon}: {
37082815dcSEvan Bacon  projectRoot: string;
38082815dcSEvan Bacon  project: XcodeProject;
39082815dcSEvan Bacon}) {
40082815dcSEvan Bacon  // Only create a bridging header if using objective-c
41082815dcSEvan Bacon  if (shouldCreateSwiftBridgingHeader({ projectRoot, project })) {
42082815dcSEvan Bacon    const projectName = getProjectName(projectRoot);
43082815dcSEvan Bacon    const bridgingHeader = createBridgingHeaderFileName(projectName);
44082815dcSEvan Bacon    // Ensure a bridging header is created in the Xcode project.
45082815dcSEvan Bacon    project = createBridgingHeaderFile({
46082815dcSEvan Bacon      project,
47082815dcSEvan Bacon      projectName,
48082815dcSEvan Bacon      projectRoot,
49082815dcSEvan Bacon      bridgingHeader,
50082815dcSEvan Bacon    });
51082815dcSEvan Bacon    // Designate the newly created file as the Swift bridging header in the Xcode project.
52082815dcSEvan Bacon    project = linkBridgingHeaderFile({
53082815dcSEvan Bacon      project,
54082815dcSEvan Bacon      bridgingHeader: path.join(projectName, bridgingHeader),
55082815dcSEvan Bacon    });
56082815dcSEvan Bacon  }
57082815dcSEvan Bacon  return project;
58082815dcSEvan Bacon}
59082815dcSEvan Bacon
60082815dcSEvan Baconfunction shouldCreateSwiftBridgingHeader({
61082815dcSEvan Bacon  projectRoot,
62082815dcSEvan Bacon  project,
63082815dcSEvan Bacon}: {
64082815dcSEvan Bacon  projectRoot: string;
65082815dcSEvan Bacon  project: XcodeProject;
66082815dcSEvan Bacon}): boolean {
67082815dcSEvan Bacon  // Only create a bridging header if the project is using in Objective C (AppDelegate is written in Objc).
68ed3bd27bSEvan Bacon  const isObjc = getAppDelegate(projectRoot).language !== 'swift';
69082815dcSEvan Bacon  return isObjc && !getDesignatedSwiftBridgingHeaderFileReference({ project });
70082815dcSEvan Bacon}
71082815dcSEvan Bacon
72082815dcSEvan Bacon/**
73082815dcSEvan Bacon * @returns String matching the default name used when Xcode automatically creates a bridging header file.
74082815dcSEvan Bacon */
75082815dcSEvan Baconfunction createBridgingHeaderFileName(projectName: string): string {
76082815dcSEvan Bacon  return `${projectName}-Bridging-Header.h`;
77082815dcSEvan Bacon}
78082815dcSEvan Bacon
79082815dcSEvan Baconexport function getDesignatedSwiftBridgingHeaderFileReference({
80082815dcSEvan Bacon  project,
81082815dcSEvan Bacon}: {
82082815dcSEvan Bacon  project: XcodeProject;
83082815dcSEvan Bacon}): string | null {
84082815dcSEvan Bacon  const configurations = project.pbxXCBuildConfigurationSection();
85082815dcSEvan Bacon  // @ts-ignore
86082815dcSEvan Bacon  for (const { buildSettings } of Object.values(configurations || {})) {
87082815dcSEvan Bacon    // Guessing that this is the best way to emulate Xcode.
88082815dcSEvan Bacon    // Using `project.addToBuildSettings` modifies too many targets.
89082815dcSEvan Bacon    if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') {
90082815dcSEvan Bacon      if (
91082815dcSEvan Bacon        typeof buildSettings.SWIFT_OBJC_BRIDGING_HEADER === 'string' &&
92082815dcSEvan Bacon        buildSettings.SWIFT_OBJC_BRIDGING_HEADER
93082815dcSEvan Bacon      ) {
94082815dcSEvan Bacon        return buildSettings.SWIFT_OBJC_BRIDGING_HEADER;
95082815dcSEvan Bacon      }
96082815dcSEvan Bacon    }
97082815dcSEvan Bacon  }
98082815dcSEvan Bacon  return null;
99082815dcSEvan Bacon}
100082815dcSEvan Bacon
101082815dcSEvan Bacon/**
102082815dcSEvan Bacon *
103082815dcSEvan Bacon * @param bridgingHeader The bridging header filename ex: `ExpoAPIs-Bridging-Header.h`
104082815dcSEvan Bacon * @returns
105082815dcSEvan Bacon */
106082815dcSEvan Baconexport function linkBridgingHeaderFile({
107082815dcSEvan Bacon  project,
108082815dcSEvan Bacon  bridgingHeader,
109082815dcSEvan Bacon}: {
110082815dcSEvan Bacon  project: XcodeProject;
111082815dcSEvan Bacon  bridgingHeader: string;
112082815dcSEvan Bacon}): XcodeProject {
113082815dcSEvan Bacon  const configurations = project.pbxXCBuildConfigurationSection();
114082815dcSEvan Bacon  // @ts-ignore
115082815dcSEvan Bacon  for (const { buildSettings } of Object.values(configurations || {})) {
116082815dcSEvan Bacon    // Guessing that this is the best way to emulate Xcode.
117082815dcSEvan Bacon    // Using `project.addToBuildSettings` modifies too many targets.
118082815dcSEvan Bacon    if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') {
119082815dcSEvan Bacon      buildSettings.SWIFT_OBJC_BRIDGING_HEADER = bridgingHeader;
120082815dcSEvan Bacon    }
121082815dcSEvan Bacon  }
122082815dcSEvan Bacon
123082815dcSEvan Bacon  return project;
124082815dcSEvan Bacon}
125082815dcSEvan Bacon
126082815dcSEvan Baconexport function createBridgingHeaderFile({
127082815dcSEvan Bacon  projectRoot,
128082815dcSEvan Bacon  projectName,
129082815dcSEvan Bacon  project,
130082815dcSEvan Bacon  bridgingHeader,
131082815dcSEvan Bacon}: {
132082815dcSEvan Bacon  project: XcodeProject;
133082815dcSEvan Bacon  projectName: string;
134082815dcSEvan Bacon  projectRoot: string;
135082815dcSEvan Bacon  bridgingHeader: string;
136082815dcSEvan Bacon}): XcodeProject {
137082815dcSEvan Bacon  const bridgingHeaderProjectPath = path.join(getSourceRoot(projectRoot), bridgingHeader);
138082815dcSEvan Bacon  if (!fs.existsSync(bridgingHeaderProjectPath)) {
139082815dcSEvan Bacon    // Create the file
140082815dcSEvan Bacon    fs.writeFileSync(bridgingHeaderProjectPath, templateBridgingHeader, 'utf8');
141082815dcSEvan Bacon  }
142082815dcSEvan Bacon
143082815dcSEvan Bacon  // This is non-standard, Xcode generates the bridging header in `/ios` which is kinda annoying.
144082815dcSEvan Bacon  // Instead, this'll generate the default header in the application code folder `/ios/myproject/`.
145082815dcSEvan Bacon  const filePath = `${projectName}/${bridgingHeader}`;
146082815dcSEvan Bacon  // Ensure the file is linked with Xcode resource files
147082815dcSEvan Bacon  if (!project.hasFile(filePath)) {
148082815dcSEvan Bacon    project = addResourceFileToGroup({
149082815dcSEvan Bacon      filepath: filePath,
150082815dcSEvan Bacon      groupName: projectName,
151082815dcSEvan Bacon      project,
152082815dcSEvan Bacon      // Not sure why, but this is how Xcode generates it.
153082815dcSEvan Bacon      isBuildFile: false,
154082815dcSEvan Bacon      verbose: false,
155082815dcSEvan Bacon    });
156082815dcSEvan Bacon  }
157082815dcSEvan Bacon  return project;
158082815dcSEvan Bacon}
159082815dcSEvan Bacon
160082815dcSEvan Baconexport const withNoopSwiftFile: ConfigPlugin = (config) => {
161082815dcSEvan Bacon  return withBuildSourceFile(config, {
162082815dcSEvan Bacon    filePath: 'noop-file.swift',
163082815dcSEvan Bacon    contents: [
164082815dcSEvan Bacon      '//',
165082815dcSEvan Bacon      '// @generated',
166082815dcSEvan Bacon      '// A blank Swift file must be created for native modules with Swift files to work correctly.',
167082815dcSEvan Bacon      '//',
168082815dcSEvan Bacon      '',
169082815dcSEvan Bacon    ].join('\n'),
170082815dcSEvan Bacon  });
171082815dcSEvan Bacon};
172