Skip to content

Commit 81c7245

Browse files
committed
Add dependencies and xcode helpers
1 parent f7ef72e commit 81c7245

File tree

5 files changed

+240
-1
lines changed

5 files changed

+240
-1
lines changed

package-lock.json

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/host/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@
6767
],
6868
"license": "MIT",
6969
"dependencies": {
70+
"@bacons/xcode": "^1.0.0-alpha.29",
7071
"@expo/plist": "^0.4.7",
72+
"@xmldom/xmldom": "^0.8.11",
7173
"@react-native-node-api/cli-utils": "0.1.4",
7274
"pkg-dir": "^8.0.0",
7375
"read-pkg": "^9.0.1",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import assert from "node:assert";
2+
import path from "node:path";
3+
import fs from "node:fs";
4+
import cp from "node:child_process";
5+
6+
// Using xmldom here because this is what @expo/plist uses internally and we might as well re-use it here.
7+
// Types come from packages/host/types/xmldom.d.ts (path mapping in tsconfig.node.json) to avoid pulling in lib "dom".
8+
import { DOMParser } from "@xmldom/xmldom";
9+
import xcode from "@bacons/xcode";
10+
import * as zod from "zod";
11+
12+
export type XcodeWorkspace = {
13+
version: string;
14+
fileRefs: {
15+
location: string;
16+
}[];
17+
};
18+
19+
export async function readXcodeWorkspace(workspacePath: string) {
20+
const dataFilePath = path.join(workspacePath, "contents.xcworkspacedata");
21+
assert(
22+
fs.existsSync(dataFilePath),
23+
`Expected a contents.xcworkspacedata file at '${dataFilePath}'`,
24+
);
25+
const xml = await fs.promises.readFile(dataFilePath, "utf-8");
26+
const dom = new DOMParser().parseFromString(xml, "application/xml");
27+
const version = dom.documentElement.getAttribute("version") ?? "1.0";
28+
assert.equal(version, "1.0", "Unexpected workspace version");
29+
30+
const result: XcodeWorkspace = {
31+
version,
32+
fileRefs: [],
33+
};
34+
const fileRefs = dom.documentElement.getElementsByTagName("FileRef");
35+
for (let i = 0; i < fileRefs.length; i++) {
36+
const fileRef = fileRefs.item(i);
37+
if (fileRef) {
38+
const location = fileRef.getAttribute("location");
39+
if (location) {
40+
result.fileRefs.push({
41+
location,
42+
});
43+
}
44+
}
45+
}
46+
return result;
47+
}
48+
49+
export async function findXcodeWorkspace(fromPath: string) {
50+
// Check if the directory contains a Xcode workspace
51+
const xcodeWorkspace = await fs.promises.glob(path.join("*.xcworkspace"), {
52+
cwd: fromPath,
53+
});
54+
55+
for await (const workspace of xcodeWorkspace) {
56+
return path.join(fromPath, workspace);
57+
}
58+
59+
// Check if the directory contain an ios directory and call recursively from that
60+
const iosDirectory = path.join(fromPath, "ios");
61+
if (fs.existsSync(iosDirectory)) {
62+
return findXcodeWorkspace(iosDirectory);
63+
}
64+
65+
// TODO: Consider continuing searching in parent directories
66+
throw new Error(`No Xcode workspace found in '${fromPath}'`);
67+
}
68+
69+
export async function findXcodeProject(fromPath: string) {
70+
// Read the workspace contents to find the first project
71+
const workspacePath = await findXcodeWorkspace(fromPath);
72+
const workspace = await readXcodeWorkspace(workspacePath);
73+
// Resolve the first project location to an absolute path
74+
assert(
75+
workspace.fileRefs.length > 0,
76+
"Expected at least one project in the workspace",
77+
);
78+
const [firstProject] = workspace.fileRefs;
79+
// Extract the path from the scheme (using a regex)
80+
const match = firstProject.location.match(/^([^:]*):(.*)$/);
81+
assert(match, "Expected a project path in the workspace");
82+
const [, scheme, projectPath] = match;
83+
assert(scheme, "Expected a scheme in the fileRef location");
84+
assert(projectPath, "Expected a path in the fileRef location");
85+
if (scheme === "absolute") {
86+
return projectPath;
87+
} else if (scheme === "group") {
88+
return path.resolve(path.dirname(workspacePath), projectPath);
89+
} else {
90+
throw new Error(`Unexpected scheme: ${scheme}`);
91+
}
92+
}
93+
94+
const BuildSettingsSchema = zod.array(
95+
zod.object({
96+
target: zod.string(),
97+
buildSettings: zod.partialRecord(zod.string(), zod.string()),
98+
}),
99+
);
100+
101+
export function getBuildSettings(
102+
xcodeProjectPath: string,
103+
mainTarget: xcode.PBXNativeTarget,
104+
) {
105+
const result = cp.spawnSync(
106+
"xcodebuild",
107+
[
108+
"-showBuildSettings",
109+
"-project",
110+
xcodeProjectPath,
111+
"-target",
112+
mainTarget.getDisplayName(),
113+
"-json",
114+
],
115+
{
116+
cwd: xcodeProjectPath,
117+
encoding: "utf-8",
118+
},
119+
);
120+
assert.equal(
121+
result.status,
122+
0,
123+
`Failed to run xcodebuild -showBuildSettings: ${result.stderr}`,
124+
);
125+
return BuildSettingsSchema.parse(JSON.parse(result.stdout));
126+
}
127+
128+
export function getBuildDirPath(
129+
xcodeProjectPath: string,
130+
mainTarget: xcode.PBXNativeTarget,
131+
) {
132+
const buildSettings = getBuildSettings(xcodeProjectPath, mainTarget);
133+
assert(buildSettings.length === 1, "Expected exactly one build setting");
134+
const [targetBuildSettings] = buildSettings;
135+
const { BUILD_DIR: buildDirPath } = targetBuildSettings.buildSettings;
136+
assert(buildDirPath, "Expected a build directory");
137+
return buildDirPath;
138+
}

packages/host/tsconfig.node.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
"declarationMap": true,
66
"outDir": "dist",
77
"rootDir": "src",
8-
"types": ["node"]
8+
"types": ["node"],
9+
"baseUrl": ".",
10+
"paths": {
11+
"@xmldom/xmldom": ["./types/xmldom.d.ts"]
12+
}
913
},
1014
"include": ["src/node/**/*.ts", "types/**/*.d.ts"],
1115
"exclude": ["**/*.test.ts"],

packages/host/types/xmldom.d.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Local type declaration for @xmldom/xmldom that does not pull in lib "dom".
3+
* Used via path mapping in tsconfig.node.json so we get correct xmldom types
4+
* without bleeding global DOM types (Document, Node, Element, etc.) into the project.
5+
*/
6+
declare module "@xmldom/xmldom" {
7+
interface XmldomDocument {
8+
readonly documentElement: XmldomElement;
9+
}
10+
11+
interface XmldomElement {
12+
getAttribute(name: string): string | null;
13+
getElementsByTagName(name: string): XmldomNodeList<XmldomElement>;
14+
}
15+
16+
interface XmldomNodeList<T = XmldomElement> {
17+
readonly length: number;
18+
item(index: number): T | null;
19+
}
20+
21+
interface XmldomDOMParserOptions {
22+
locator?: unknown;
23+
errorHandler?:
24+
| ((level: string, msg: unknown) => unknown)
25+
| {
26+
warning?: (msg: unknown) => unknown;
27+
error?: (msg: unknown) => unknown;
28+
fatalError?: (msg: unknown) => unknown;
29+
};
30+
}
31+
32+
interface XmldomDOMParserInstance {
33+
parseFromString(xmlsource: string, mimeType?: string): XmldomDocument;
34+
}
35+
36+
interface XmldomDOMParserStatic {
37+
new (options?: XmldomDOMParserOptions): XmldomDOMParserInstance;
38+
}
39+
40+
export const DOMParser: XmldomDOMParserStatic;
41+
export const XMLSerializer: unknown;
42+
export const DOMImplementation: unknown;
43+
}

0 commit comments

Comments
 (0)