1import fs from 'fs';
2import path from 'path';
3
4import { getAppDelegate, getSourceRoot } from './Paths';
5import { withBuildSourceFile } from './XcodeProjectFile';
6import { addResourceFileToGroup, getProjectName } from './utils/Xcodeproj';
7import { ConfigPlugin, XcodeProject } from '../Plugin.types';
8import { withXcodeProject } from '../plugins/ios-plugins';
9
10const templateBridgingHeader = `//
11//  Use this file to import your target's public headers that you would like to expose to Swift.
12//
13`;
14
15/**
16 * Ensure a Swift bridging header is created for the project.
17 * This helps fix problems related to using modules that are written in Swift (lottie, FBSDK).
18 *
19 * 1. Ensures the file exists given the project path.
20 * 2. Writes the file and links to Xcode as a resource file.
21 * 3. Sets the build configuration `SWIFT_OBJC_BRIDGING_HEADER = [PROJECT_NAME]-Bridging-Header.h`
22 */
23export const withSwiftBridgingHeader: ConfigPlugin = (config) => {
24  return withXcodeProject(config, (config) => {
25    config.modResults = ensureSwiftBridgingHeaderSetup({
26      project: config.modResults,
27      projectRoot: config.modRequest.projectRoot,
28    });
29    return config;
30  });
31};
32
33export function ensureSwiftBridgingHeaderSetup({
34  projectRoot,
35  project,
36}: {
37  projectRoot: string;
38  project: XcodeProject;
39}) {
40  // Only create a bridging header if using objective-c
41  if (shouldCreateSwiftBridgingHeader({ projectRoot, project })) {
42    const projectName = getProjectName(projectRoot);
43    const bridgingHeader = createBridgingHeaderFileName(projectName);
44    // Ensure a bridging header is created in the Xcode project.
45    project = createBridgingHeaderFile({
46      project,
47      projectName,
48      projectRoot,
49      bridgingHeader,
50    });
51    // Designate the newly created file as the Swift bridging header in the Xcode project.
52    project = linkBridgingHeaderFile({
53      project,
54      bridgingHeader: path.join(projectName, bridgingHeader),
55    });
56  }
57  return project;
58}
59
60function shouldCreateSwiftBridgingHeader({
61  projectRoot,
62  project,
63}: {
64  projectRoot: string;
65  project: XcodeProject;
66}): boolean {
67  // Only create a bridging header if the project is using in Objective C (AppDelegate is written in Objc).
68  const isObjc = getAppDelegate(projectRoot).language !== 'swift';
69  return isObjc && !getDesignatedSwiftBridgingHeaderFileReference({ project });
70}
71
72/**
73 * @returns String matching the default name used when Xcode automatically creates a bridging header file.
74 */
75function createBridgingHeaderFileName(projectName: string): string {
76  return `${projectName}-Bridging-Header.h`;
77}
78
79export function getDesignatedSwiftBridgingHeaderFileReference({
80  project,
81}: {
82  project: XcodeProject;
83}): string | null {
84  const configurations = project.pbxXCBuildConfigurationSection();
85  // @ts-ignore
86  for (const { buildSettings } of Object.values(configurations || {})) {
87    // Guessing that this is the best way to emulate Xcode.
88    // Using `project.addToBuildSettings` modifies too many targets.
89    if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') {
90      if (
91        typeof buildSettings.SWIFT_OBJC_BRIDGING_HEADER === 'string' &&
92        buildSettings.SWIFT_OBJC_BRIDGING_HEADER
93      ) {
94        return buildSettings.SWIFT_OBJC_BRIDGING_HEADER;
95      }
96    }
97  }
98  return null;
99}
100
101/**
102 *
103 * @param bridgingHeader The bridging header filename ex: `ExpoAPIs-Bridging-Header.h`
104 * @returns
105 */
106export function linkBridgingHeaderFile({
107  project,
108  bridgingHeader,
109}: {
110  project: XcodeProject;
111  bridgingHeader: string;
112}): XcodeProject {
113  const configurations = project.pbxXCBuildConfigurationSection();
114  // @ts-ignore
115  for (const { buildSettings } of Object.values(configurations || {})) {
116    // Guessing that this is the best way to emulate Xcode.
117    // Using `project.addToBuildSettings` modifies too many targets.
118    if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') {
119      buildSettings.SWIFT_OBJC_BRIDGING_HEADER = bridgingHeader;
120    }
121  }
122
123  return project;
124}
125
126export function createBridgingHeaderFile({
127  projectRoot,
128  projectName,
129  project,
130  bridgingHeader,
131}: {
132  project: XcodeProject;
133  projectName: string;
134  projectRoot: string;
135  bridgingHeader: string;
136}): XcodeProject {
137  const bridgingHeaderProjectPath = path.join(getSourceRoot(projectRoot), bridgingHeader);
138  if (!fs.existsSync(bridgingHeaderProjectPath)) {
139    // Create the file
140    fs.writeFileSync(bridgingHeaderProjectPath, templateBridgingHeader, 'utf8');
141  }
142
143  // This is non-standard, Xcode generates the bridging header in `/ios` which is kinda annoying.
144  // Instead, this'll generate the default header in the application code folder `/ios/myproject/`.
145  const filePath = `${projectName}/${bridgingHeader}`;
146  // Ensure the file is linked with Xcode resource files
147  if (!project.hasFile(filePath)) {
148    project = addResourceFileToGroup({
149      filepath: filePath,
150      groupName: projectName,
151      project,
152      // Not sure why, but this is how Xcode generates it.
153      isBuildFile: false,
154      verbose: false,
155    });
156  }
157  return project;
158}
159
160export const withNoopSwiftFile: ConfigPlugin = (config) => {
161  return withBuildSourceFile(config, {
162    filePath: 'noop-file.swift',
163    contents: [
164      '//',
165      '// @generated',
166      '// A blank Swift file must be created for native modules with Swift files to work correctly.',
167      '//',
168      '',
169    ].join('\n'),
170  });
171};
172