Skip to content

Commit 1319189

Browse files
authored
refactor(apple): port make_project! to JS (#2422)
1 parent 1f2901b commit 1319189

29 files changed

+1112
-90
lines changed

.github/prettierrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"plugins": ["prettier-plugin-organize-imports"],
23
"trailingComma": "es5",
34
"endOfLine": "auto",
45
"overrides": [

android/autolink.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
readTextFile,
1010
writeTextFile,
1111
} from "../scripts/helpers.js";
12+
import { mkdir_p } from "../scripts/utils/filesystem.mjs";
1213

1314
/**
1415
* @typedef {import("@react-native-community/cli-types").Config} Config
@@ -29,7 +30,7 @@ export function cleanDependencyName(name) {
2930
* @param {string} p
3031
*/
3132
function ensureDirForFile(p) {
32-
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o755 });
33+
mkdir_p(path.dirname(p));
3334
}
3435

3536
/**

ios/app.mjs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// @ts-check
2+
import * as nodefs from "node:fs";
3+
import * as path from "node:path";
4+
import { URL, fileURLToPath } from "node:url";
5+
import { loadAppConfig } from "../scripts/appConfig.mjs";
6+
import { findFile, readTextFile, toVersionNumber } from "../scripts/helpers.js";
7+
import { cp_r, mkdir_p } from "../scripts/utils/filesystem.mjs";
8+
import { generateAssetsCatalogs } from "./assetsCatalog.mjs";
9+
import { generateEntitlements } from "./entitlements.mjs";
10+
import { generateInfoPlist } from "./infoPlist.mjs";
11+
import { generateLocalizations, getProductName } from "./localizations.mjs";
12+
import { isBridgelessEnabled, isNewArchEnabled } from "./newArch.mjs";
13+
import { generatePrivacyManifest } from "./privacyManifest.mjs";
14+
import { isObject, isString, projectPath } from "./utils.mjs";
15+
import {
16+
PRODUCT_DISPLAY_NAME,
17+
PRODUCT_VERSION,
18+
applyBuildSettings,
19+
applyPreprocessorDefinitions,
20+
applySwiftFlags,
21+
applyUserHeaderSearchPaths,
22+
configureBuildSchemes,
23+
overrideBuildSettings,
24+
} from "./xcode.mjs";
25+
26+
/**
27+
* @import {
28+
* ApplePlatform,
29+
* JSONObject,
30+
* JSONValue,
31+
* ProjectConfiguration,
32+
* } from "../scripts/types.ts";
33+
*/
34+
35+
const SUPPORTED_PLATFORMS = ["ios", "macos", "visionos"];
36+
37+
/**
38+
* @param {string} projectRoot
39+
* @param {string} destination
40+
* @returns {void}
41+
*/
42+
function exportNodeBinaryPath(projectRoot, destination, fs = nodefs) {
43+
const node = process.argv0;
44+
fs.writeFileSync(
45+
path.join(projectRoot, ".xcode.env"),
46+
`export NODE_BINARY='${node}'\n`
47+
);
48+
fs.writeFileSync(
49+
path.join(destination, ".env"),
50+
`export PATH='${path.dirname(node)}':$PATH\n`
51+
);
52+
}
53+
54+
/**
55+
* @param {JSONValue} platformConfig
56+
* @param {string} projectRoot
57+
* @param {ApplePlatform} targetPlatform
58+
* @returns {string}
59+
*/
60+
function findReactNativePath(
61+
platformConfig,
62+
projectRoot,
63+
targetPlatform,
64+
fs = nodefs
65+
) {
66+
if (isObject(platformConfig)) {
67+
const userPath = platformConfig["reactNativePath"];
68+
if (isString(userPath)) {
69+
const p = findFile(userPath, projectRoot, fs);
70+
if (p) {
71+
return p;
72+
}
73+
}
74+
}
75+
76+
const manifestURL = new URL("../package.json", import.meta.url);
77+
const manifest = JSON.parse(readTextFile(fileURLToPath(manifestURL), fs));
78+
const npmPackageName = manifest.defaultPlatformPackages[targetPlatform];
79+
if (!npmPackageName) {
80+
throw new Error(`Unsupported target platform: ${targetPlatform}`);
81+
}
82+
83+
const pkg = findFile(`node_modules/${npmPackageName}`, projectRoot, fs);
84+
if (!pkg) {
85+
throw new Error(`Cannot find module '${npmPackageName}'`);
86+
}
87+
88+
return pkg;
89+
}
90+
91+
/**
92+
* @param {string} p
93+
* @returns {number}
94+
*/
95+
function readPackageVersion(p, fs = nodefs) {
96+
const manifest = JSON.parse(readTextFile(path.join(p, "package.json"), fs));
97+
return toVersionNumber(manifest["version"]);
98+
}
99+
100+
/**
101+
* @param {string} projectRoot
102+
* @param {ApplePlatform} targetPlatform
103+
* @param {JSONObject} options
104+
* @returns {ProjectConfiguration}
105+
*/
106+
export function generateProject(
107+
projectRoot,
108+
targetPlatform,
109+
options,
110+
fs = nodefs
111+
) {
112+
if (!SUPPORTED_PLATFORMS.includes(targetPlatform)) {
113+
throw new Error(`Unsupported platform: ${targetPlatform}`);
114+
}
115+
116+
const appConfig = loadAppConfig(projectRoot, fs);
117+
118+
const xcodeproj = "ReactTestApp.xcodeproj";
119+
120+
const xcodeprojSrc = projectPath(xcodeproj, targetPlatform);
121+
const nodeModulesDir = findFile("node_modules", projectRoot, fs);
122+
if (!nodeModulesDir) {
123+
throw new Error("Cannot not find 'node_modules' folder");
124+
}
125+
126+
const destination = path.join(nodeModulesDir, ".generated", targetPlatform);
127+
const xcodeprojDst = path.join(destination, xcodeproj);
128+
129+
// Copy Xcode project files
130+
mkdir_p(destination, fs);
131+
cp_r(xcodeprojSrc, destination, fs);
132+
configureBuildSchemes(appConfig, targetPlatform, xcodeprojDst, fs);
133+
134+
// Link source files
135+
const srcDirs = ["ReactTestApp", "ReactTestAppTests", "ReactTestAppUITests"];
136+
for (const file of srcDirs) {
137+
fs.linkSync(projectPath(file, targetPlatform), destination);
138+
}
139+
140+
// Shared code lives in `ios/ReactTestApp/`
141+
if (targetPlatform !== "ios") {
142+
const shared = path.join(destination, "Shared");
143+
if (!fs.existsSync(shared)) {
144+
const source = new URL("ReactTestApp", import.meta.url);
145+
fs.linkSync(fileURLToPath(source), shared);
146+
}
147+
}
148+
149+
generateAssetsCatalogs(appConfig, targetPlatform, destination, undefined, fs);
150+
generateEntitlements(appConfig, targetPlatform, destination, fs);
151+
generateInfoPlist(appConfig, targetPlatform, destination, fs);
152+
generatePrivacyManifest(appConfig, targetPlatform, destination, fs);
153+
generateLocalizations(appConfig, targetPlatform, destination, fs);
154+
155+
// Note the location of Node so we can use it later in script phases
156+
exportNodeBinaryPath(projectRoot, destination, fs);
157+
158+
const platformConfig = appConfig[targetPlatform];
159+
const reactNativePath = findReactNativePath(
160+
platformConfig,
161+
projectRoot,
162+
targetPlatform,
163+
fs
164+
);
165+
const reactNativeVersion = readPackageVersion(reactNativePath, fs);
166+
const useNewArch = isNewArchEnabled(options, reactNativeVersion);
167+
const useBridgeless = isBridgelessEnabled(options, reactNativeVersion);
168+
169+
/** @type {ProjectConfiguration} */
170+
const project = {
171+
xcodeprojPath: xcodeprojDst,
172+
reactNativePath,
173+
reactNativeVersion,
174+
useNewArch,
175+
useBridgeless,
176+
buildSettings: {},
177+
testsBuildSettings: {},
178+
uitestsBuildSettings: {},
179+
};
180+
181+
if (isObject(platformConfig)) {
182+
applyBuildSettings(platformConfig, project, projectRoot, destination, fs);
183+
}
184+
185+
const overrides = options["buildSettingOverrides"];
186+
if (isObject(overrides)) {
187+
overrideBuildSettings(project.buildSettings, overrides);
188+
}
189+
190+
project.buildSettings[PRODUCT_DISPLAY_NAME] = getProductName(appConfig);
191+
192+
const productVersion = appConfig["version"];
193+
project.buildSettings[PRODUCT_VERSION] =
194+
productVersion && isString(productVersion) ? productVersion : "1.0";
195+
196+
const singleApp = appConfig["singleApp"];
197+
if (isString(singleApp)) {
198+
project.singleApp = singleApp;
199+
}
200+
201+
applyPreprocessorDefinitions(project);
202+
applySwiftFlags(project);
203+
applyUserHeaderSearchPaths(project, destination);
204+
205+
return project;
206+
}

ios/assetsCatalog.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as nodefs from "node:fs";
44
import * as path from "node:path";
55
import { sourceForAppConfig } from "../scripts/appConfig.mjs";
66
import { readJSONFile } from "../scripts/helpers.js";
7+
import { cp_r, mkdir_p, rm_r } from "../scripts/utils/filesystem.mjs";
78
import { isObject, projectPath } from "./utils.mjs";
89

910
/**
@@ -90,8 +91,8 @@ export function generateAssetsCatalogs(
9091
);
9192
const xcassets_dst = path.join(destination, path.basename(xcassets_src));
9293

93-
fs.rmSync(xcassets_dst, { force: true, maxRetries: 3, recursive: true });
94-
fs.cpSync(xcassets_src, xcassets_dst, { recursive: true });
94+
rm_r(xcassets_dst, fs);
95+
cp_r(xcassets_src, xcassets_dst, fs);
9596

9697
const platformConfig = appConfig[targetPlatform];
9798
if (!isObject(platformConfig)) {
@@ -120,11 +121,10 @@ export function generateAssetsCatalogs(
120121
}
121122

122123
const appManifestDir = sourceForAppConfig(appConfig);
123-
const mkdirOptions = { recursive: true, mode: 0o755 };
124124

125125
for (const [setName, appIcon] of appIcons) {
126126
const appIconSet = path.join(destination, `${setName}.appiconset`);
127-
fs.mkdirSync(appIconSet, mkdirOptions);
127+
mkdir_p(appIconSet, fs);
128128

129129
const icon = path.join(appManifestDir, appIcon.filename);
130130
const extname = path.extname(icon);

ios/localizations.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// @ts-check
2+
import * as nodefs from "node:fs";
3+
import * as path from "node:path";
4+
import { readTextFile } from "../scripts/helpers.js";
5+
import { mkdir_p } from "../scripts/utils/filesystem.mjs";
6+
import { isString, projectPath } from "./utils.mjs";
7+
8+
/**
9+
* @typedef {import("../scripts/types.ts").ApplePlatform} ApplePlatform;
10+
* @typedef {import("../scripts/types.ts").JSONObject} JSONObject;
11+
*/
12+
13+
const DEFAULT_APP_NAME = "ReactTestApp";
14+
15+
/**
16+
* @param {JSONObject} appConfig
17+
* @returns {string}
18+
*/
19+
export function getProductName(appConfig) {
20+
const { name, displayName } = appConfig;
21+
const productName = displayName || name;
22+
return productName && isString(productName) ? productName : DEFAULT_APP_NAME;
23+
}
24+
25+
/**
26+
* @param {JSONObject} appConfig
27+
* @param {ApplePlatform} targetPlatform
28+
* @param {string} destination
29+
* @returns {void}
30+
*/
31+
export function generateLocalizations(
32+
appConfig,
33+
targetPlatform,
34+
destination,
35+
fs = nodefs
36+
) {
37+
const localizationsDir = "Localizations";
38+
const localizations_src = projectPath(localizationsDir, targetPlatform);
39+
if (!fs.existsSync(localizations_src)) {
40+
return;
41+
}
42+
43+
const mainStrings = "Main.strings";
44+
const productName = getProductName(appConfig);
45+
const localizations_dst = path.join(destination, localizationsDir);
46+
47+
for (const entry of fs.readdirSync(localizations_src)) {
48+
if (entry.startsWith(".")) {
49+
continue;
50+
}
51+
52+
const lproj = path.join(localizations_dst, entry);
53+
mkdir_p(lproj, fs);
54+
55+
const stringsFile = path.join(localizations_src, entry, mainStrings);
56+
const strings = readTextFile(stringsFile, fs);
57+
fs.writeFileSync(
58+
path.join(lproj, mainStrings),
59+
strings.replaceAll(DEFAULT_APP_NAME, productName)
60+
);
61+
}
62+
}

ios/newArch.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// @ts-check
2+
import { v } from "../scripts/helpers.js";
3+
4+
/**
5+
* @typedef {import("../scripts/types.ts").JSONObject} JSONObject;
6+
*/
7+
8+
/**
9+
* @param {number} reactNativeVersion
10+
*/
11+
export function supportsNewArch(reactNativeVersion) {
12+
return reactNativeVersion === 0 || reactNativeVersion >= v(0, 71, 0);
13+
}
14+
15+
/**
16+
* @param {JSONObject} options
17+
* @param {number} reactNativeVersion
18+
* @returns {boolean}
19+
*/
20+
export function isNewArchEnabled(options, reactNativeVersion) {
21+
if (!supportsNewArch(reactNativeVersion)) {
22+
return false;
23+
}
24+
25+
const envVar = process.env["RCT_NEW_ARCH_ENABLED"];
26+
if (typeof envVar === "string") {
27+
return envVar !== "0";
28+
}
29+
30+
return Boolean(options["fabricEnabled"]);
31+
}
32+
33+
/**
34+
* @param {JSONObject} options
35+
* @param {number} reactNativeVersion
36+
* @returns {boolean}
37+
*/
38+
export function isBridgelessEnabled(options, reactNativeVersion) {
39+
if (isNewArchEnabled(options, reactNativeVersion)) {
40+
if (reactNativeVersion >= v(0, 74, 0)) {
41+
return options["bridgelessEnabled"] !== false;
42+
}
43+
if (reactNativeVersion >= v(0, 73, 0)) {
44+
return Boolean(options["bridgelessEnabled"]);
45+
}
46+
}
47+
return false;
48+
}

ios/privacyManifest.mjs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ import * as path from "node:path";
44
import { isObject, plistFromJSON } from "./utils.mjs";
55

66
/**
7-
* @import { ApplePlatform, JSONObject, JSONValue } from "../scripts/types.js";
8-
*
9-
* @typedef {{
10-
* NSPrivacyTracking: boolean;
11-
* NSPrivacyTrackingDomains: JSONValue[];
12-
* NSPrivacyCollectedDataTypes: JSONValue[];
13-
* NSPrivacyAccessedAPITypes: JSONValue[];
14-
* }} PrivacyManifest;
7+
* @import {
8+
* ApplePlatform,
9+
* JSONObject,
10+
* PrivacyManifest
11+
* } from "../scripts/types.js";
1512
*/
1613

1714
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files

0 commit comments

Comments
 (0)