Skip to content

Commit ba0f3bc

Browse files
authored
test: add tests for the JS-based Xcode project generator (#2425)
1 parent 8800a0f commit ba0f3bc

File tree

9 files changed

+772
-49
lines changed

9 files changed

+772
-49
lines changed

ios/app.mjs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as path from "node:path";
44
import { URL, fileURLToPath } from "node:url";
55
import { loadAppConfig } from "../scripts/appConfig.mjs";
66
import { findFile, readTextFile, toVersionNumber } from "../scripts/helpers.js";
7-
import { cp_r, mkdir_p } from "../scripts/utils/filesystem.mjs";
7+
import { cp_r, mkdir_p, rm_r } from "../scripts/utils/filesystem.mjs";
88
import { generateAssetsCatalogs } from "./assetsCatalog.mjs";
99
import { generateEntitlements } from "./entitlements.mjs";
1010
import { generateInfoPlist } from "./infoPlist.mjs";
@@ -34,13 +34,23 @@ import {
3434

3535
const SUPPORTED_PLATFORMS = ["ios", "macos", "visionos"];
3636

37+
/**
38+
* @param {string} platform
39+
* @returns {asserts platform is ApplePlatform}
40+
*/
41+
function assertSupportedPlatform(platform) {
42+
if (!SUPPORTED_PLATFORMS.includes(platform)) {
43+
throw new Error(`Unsupported platform: ${platform}`);
44+
}
45+
}
46+
3747
/**
3848
* @param {string} projectRoot
3949
* @param {string} destination
4050
* @returns {void}
4151
*/
4252
function exportNodeBinaryPath(projectRoot, destination, fs = nodefs) {
43-
const node = process.argv0;
53+
const node = process.argv[0];
4454
fs.writeFileSync(
4555
path.join(projectRoot, ".xcode.env"),
4656
`export NODE_BINARY='${node}'\n`
@@ -99,7 +109,7 @@ function readPackageVersion(p, fs = nodefs) {
99109

100110
/**
101111
* @param {string} projectRoot
102-
* @param {ApplePlatform} targetPlatform
112+
* @param {string} targetPlatform
103113
* @param {JSONObject} options
104114
* @returns {ProjectConfiguration}
105115
*/
@@ -109,9 +119,7 @@ export function generateProject(
109119
options,
110120
fs = nodefs
111121
) {
112-
if (!SUPPORTED_PLATFORMS.includes(targetPlatform)) {
113-
throw new Error(`Unsupported platform: ${targetPlatform}`);
114-
}
122+
assertSupportedPlatform(targetPlatform);
115123

116124
const appConfig = loadAppConfig(projectRoot, fs);
117125

@@ -134,15 +142,19 @@ export function generateProject(
134142
// Link source files
135143
const srcDirs = ["ReactTestApp", "ReactTestAppTests", "ReactTestAppUITests"];
136144
for (const file of srcDirs) {
137-
fs.linkSync(projectPath(file, targetPlatform), destination);
145+
const symlink = path.join(destination, file);
146+
if (fs.existsSync(symlink)) {
147+
rm_r(symlink, fs);
148+
}
149+
fs.symlinkSync(projectPath(file, targetPlatform), symlink);
138150
}
139151

140152
// Shared code lives in `ios/ReactTestApp/`
141153
if (targetPlatform !== "ios") {
142154
const shared = path.join(destination, "Shared");
143155
if (!fs.existsSync(shared)) {
144156
const source = new URL("ReactTestApp", import.meta.url);
145-
fs.linkSync(fileURLToPath(source), shared);
157+
fs.symlinkSync(fileURLToPath(source), shared);
146158
}
147159
}
148160

@@ -168,8 +180,8 @@ export function generateProject(
168180

169181
/** @type {ProjectConfiguration} */
170182
const project = {
171-
xcodeprojPath: xcodeprojDst,
172-
reactNativePath,
183+
xcodeprojPath: path.resolve(xcodeprojDst),
184+
reactNativePath: path.resolve(reactNativePath),
173185
reactNativeVersion,
174186
useNewArch,
175187
useBridgeless,
@@ -178,9 +190,7 @@ export function generateProject(
178190
uitestsBuildSettings: {},
179191
};
180192

181-
if (isObject(platformConfig)) {
182-
applyBuildSettings(platformConfig, project, projectRoot, destination, fs);
183-
}
193+
applyBuildSettings(platformConfig, project, projectRoot, destination, fs);
184194

185195
const overrides = options["buildSettingOverrides"];
186196
if (isObject(overrides)) {

ios/assetsCatalog.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function generateAssetsCatalogs(
9292
const xcassets_dst = path.join(destination, path.basename(xcassets_src));
9393

9494
rm_r(xcassets_dst, fs);
95-
cp_r(xcassets_src, xcassets_dst, fs);
95+
cp_r(xcassets_src, destination, fs);
9696

9797
const platformConfig = appConfig[targetPlatform];
9898
if (!isObject(platformConfig)) {

ios/utils.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function plistFromJSON(source, filename) {
6868
*/
6969
export function projectPath(p, targetPlatform) {
7070
const packageDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
71-
return path.join(packageDir, targetPlatform, p);
71+
return path.resolve(packageDir, targetPlatform, p);
7272
}
7373

7474
/**

ios/xcode.mjs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isObject, isString } from "./utils.mjs";
99
* @import {
1010
* ApplePlatform,
1111
* JSONObject,
12+
* JSONValue,
1213
* ProjectConfiguration,
1314
* XmlOptions,
1415
* } from "../scripts/types.js";
@@ -32,7 +33,7 @@ export const USER_HEADER_SEARCH_PATHS = "USER_HEADER_SEARCH_PATHS";
3233
export const WARNING_CFLAGS = "WARNING_CFLAGS";
3334

3435
/**
35-
* @param {JSONObject} platformConfig
36+
* @param {JSONValue} platformConfig
3637
* @param {ProjectConfiguration} project
3738
* @param {string} projectRoot
3839
* @param {string} destination
@@ -45,7 +46,9 @@ export function applyBuildSettings(
4546
destination,
4647
fs = nodefs
4748
) {
48-
const codeSignEntitlements = platformConfig["codeSignEntitlements"];
49+
const config = isObject(platformConfig) ? platformConfig : {};
50+
51+
const codeSignEntitlements = config["codeSignEntitlements"];
4952
if (isString(codeSignEntitlements)) {
5053
const appManifest = findFile("app.json", projectRoot, fs);
5154
if (!appManifest) {
@@ -58,19 +61,19 @@ export function applyBuildSettings(
5861
project.buildSettings[CODE_SIGN_ENTITLEMENTS] = relPath;
5962
}
6063

61-
const codeSignIdentity = platformConfig["codeSignIdentity"];
64+
const codeSignIdentity = config["codeSignIdentity"];
6265
if (isString(codeSignIdentity)) {
6366
project.buildSettings[CODE_SIGN_IDENTITY] = codeSignIdentity;
6467
}
6568

66-
const developmentTeam = platformConfig["developmentTeam"];
69+
const developmentTeam = config["developmentTeam"];
6770
if (isString(developmentTeam)) {
6871
project.buildSettings[DEVELOPMENT_TEAM] = developmentTeam;
6972
project.testsBuildSettings[DEVELOPMENT_TEAM] = developmentTeam;
7073
project.uitestsBuildSettings[DEVELOPMENT_TEAM] = developmentTeam;
7174
}
7275

73-
const bundleIdentifier = platformConfig["bundleIdentifier"];
76+
const bundleIdentifier = config["bundleIdentifier"];
7477
if (isString(bundleIdentifier)) {
7578
project.buildSettings[PRODUCT_BUNDLE_IDENTIFIER] = bundleIdentifier;
7679
project.testsBuildSettings[PRODUCT_BUNDLE_IDENTIFIER] =
@@ -79,7 +82,7 @@ export function applyBuildSettings(
7982
`${bundleIdentifier}UITests`;
8083
}
8184

82-
const buildNumber = platformConfig["buildNumber"];
85+
const buildNumber = config["buildNumber"];
8386
project.buildSettings[PRODUCT_BUILD_NUMBER] =
8487
buildNumber && isString(buildNumber) ? buildNumber : "1";
8588

@@ -161,7 +164,7 @@ export function applyUserHeaderSearchPaths({ buildSettings }, destination) {
161164
const existingPaths = buildSettings[USER_HEADER_SEARCH_PATHS];
162165
const searchPaths = Array.isArray(existingPaths) ? existingPaths : [];
163166

164-
searchPaths.push(path.dirname(destination));
167+
searchPaths.push(path.resolve(path.dirname(destination)));
165168

166169
buildSettings[USER_HEADER_SEARCH_PATHS] = searchPaths;
167170
}

scripts/appConfig.mjs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,17 @@ import { findFile, readJSONFile } from "./helpers.js";
66

77
const SOURCE_KEY = Symbol.for("source");
88

9-
/** @type {JSONObject} */
10-
let appConfig;
11-
129
/**
1310
* @param {string} projectRoot
1411
* @returns {JSONObject}
1512
*/
1613
export function loadAppConfig(projectRoot, fs = nodefs) {
17-
if (!appConfig) {
18-
const configFile = findFile("app.json", projectRoot, fs);
19-
appConfig = configFile ? readJSONFile(configFile) : {};
20-
appConfig[SOURCE_KEY] = configFile || projectRoot;
21-
}
14+
const configFile = findFile("app.json", projectRoot, fs);
15+
16+
/** @type {JSONObject} */
17+
const appConfig = configFile ? readJSONFile(configFile, fs) : {};
18+
appConfig[SOURCE_KEY] = configFile || projectRoot;
19+
2220
return appConfig;
2321
}
2422

test/fs.mock.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,47 @@
11
/* node:coverage disable */
22
import type { DirectoryJSON } from "memfs";
33
import { fs as memfs, vol } from "memfs";
4+
import * as path from "node:path";
5+
import { mkdir_p } from "../scripts/utils/filesystem.mjs";
46

57
export const fs = memfs as unknown as typeof import("node:fs");
68

7-
// Stub `cpSync` until memfs implements it
8-
fs.cpSync = fs.cpSync ?? (() => undefined);
9+
// Add simple `cpSync` implementation until memfs implements it
10+
fs.cpSync =
11+
fs.cpSync ??
12+
((src: string, dst: string, options) => {
13+
const srcStat = fs.statSync(src);
14+
const dstStat = fs.statSync(dst);
15+
if (!srcStat.isDirectory()) {
16+
const finalDst = dstStat.isDirectory()
17+
? path.join(dst, path.basename(src))
18+
: dst;
19+
return fs.copyFileSync(src, finalDst);
20+
}
21+
22+
let finalDst: string;
23+
if (!dstStat.isDirectory()) {
24+
fs.rmSync(dst);
25+
finalDst = dst;
26+
} else {
27+
finalDst = path.join(dst, path.basename(src));
28+
}
29+
30+
mkdir_p(finalDst, fs);
31+
for (const filename of fs.readdirSync(src)) {
32+
const p = path.join(src, filename);
33+
const pStat = fs.statSync(p);
34+
if (pStat.isDirectory()) {
35+
if (options?.recursive) {
36+
fs.cpSync(p, finalDst, options);
37+
} else {
38+
mkdir_p(path.join(finalDst, filename));
39+
}
40+
} else {
41+
fs.copyFileSync(p, path.join(finalDst, filename));
42+
}
43+
}
44+
});
945

1046
export function setMockFiles(files: DirectoryJSON = {}) {
1147
vol.reset();

0 commit comments

Comments
 (0)