Skip to content

Commit 6f9ca7c

Browse files
committed
fix(init): verify bundled iOS plist parsing
1 parent 3641a0e commit 6f9ca7c

4 files changed

Lines changed: 152 additions & 2 deletions

File tree

bunfig.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[install]
22
# Only install package versions published at least 2 days ago
33
minimumReleaseAge = 172800
4+
5+
[test]
6+
preload = ["./packages/cli-core/src/test/version-preload.ts"]

packages/cli-core/src/commands/init/ios/apple-entitlement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { lstat, readFile } from "node:fs/promises";
22
import { dirname, isAbsolute, resolve } from "node:path";
3-
import plist from "@expo/plist";
43
import {
54
planIOSAssociatedDomain,
65
type IOSAssociatedDomainBlockerCode,
@@ -19,6 +18,7 @@ import {
1918
type IOSMissingEntitlementsSettingsPlan,
2019
} from "./entitlements-settings.ts";
2120
import { isRecord } from "./pbx.ts";
21+
import { parseIOSPlist } from "./plist.ts";
2222

2323
const APPLE_SIGN_IN_KEY = "com.apple.developer.applesignin";
2424
const APPLE_SIGN_IN_VALUE = "Default";
@@ -229,7 +229,7 @@ function inspectEntitlementsBytes(
229229
}
230230
const bom = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
231231
const source = new TextDecoder("utf-8", { fatal: true }).decode(bom ? bytes.slice(3) : bytes);
232-
const parsed: unknown = plist.parse(source);
232+
const parsed = parseIOSPlist(source);
233233
if (!isRecord(parsed)) throw new Error("plist root is not a dictionary");
234234
const rawValue = parsed[APPLE_SIGN_IN_KEY];
235235
const structure = appleKeyStructure(source);
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { expect, setDefaultTimeout, test } from "bun:test";
2+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join, resolve } from "node:path";
5+
import { createIOSFixture } from "./test-helpers.ts";
6+
7+
setDefaultTimeout(30_000);
8+
9+
const cliEntry = resolve(import.meta.dir, "../../../cli.ts");
10+
const repositoryRoot = resolve(import.meta.dir, "../../../../../..");
11+
12+
function isolatedCLIEnvironment(configDir: string): Record<string, string | undefined> {
13+
const env: Record<string, string | undefined> = { ...Bun.env };
14+
for (const key of Object.keys(env)) {
15+
if (key.includes("CLERK")) delete env[key];
16+
}
17+
delete env.CI;
18+
delete env.DO_NOT_TRACK;
19+
delete env.NO_UPDATE_NOTIFIER;
20+
return {
21+
...env,
22+
NO_COLOR: "1",
23+
CLERK_CONFIG_DIR: configDir,
24+
CLERK_TELEMETRY_DISABLED: "1",
25+
};
26+
}
27+
28+
async function run(
29+
command: string[],
30+
options: { cwd: string; env?: Record<string, string | undefined> },
31+
) {
32+
const child = Bun.spawn(command, {
33+
...options,
34+
stdout: "pipe",
35+
stderr: "pipe",
36+
});
37+
const [stdout, stderr, exitCode] = await Promise.all([
38+
new Response(child.stdout).text(),
39+
new Response(child.stderr).text(),
40+
child.exited,
41+
]);
42+
return { stdout, stderr, exitCode };
43+
}
44+
45+
test("the compiled CLI semantically parses iOS XML plists", async () => {
46+
const temporaryRoot = await mkdtemp(join(tmpdir(), "clerk-ios-compiled-cli-"));
47+
try {
48+
const binary = join(temporaryRoot, "clerk");
49+
const fixtureRoot = join(temporaryRoot, "fixture");
50+
const configDir = join(temporaryRoot, "config");
51+
await mkdir(configDir);
52+
await createIOSFixture(fixtureRoot, {
53+
complete: true,
54+
includeKey: false,
55+
localSecrets: true,
56+
});
57+
await Bun.write(
58+
join(fixtureRoot, "MyApp", "LocalSecrets.plist"),
59+
'<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>CLERK_PUBLISHABLE_KEY</key><string>replace-me</string></dict></plist>',
60+
);
61+
await Bun.write(
62+
join(configDir, "config.json"),
63+
`${JSON.stringify({
64+
profiles: {},
65+
telemetryNoticeShown: true,
66+
machineUuid: "00000000-0000-4000-8000-000000000000",
67+
})}\n`,
68+
);
69+
70+
const compiled = await run(
71+
[
72+
process.execPath,
73+
"build",
74+
"--compile",
75+
"--minify",
76+
"--no-compile-autoload-dotenv",
77+
"--no-compile-autoload-bunfig",
78+
cliEntry,
79+
"--outfile",
80+
binary,
81+
],
82+
{ cwd: repositoryRoot },
83+
);
84+
expect(compiled.exitCode, `${compiled.stdout}\n${compiled.stderr}`).toBe(0);
85+
86+
const result = await run(
87+
[binary, "--mode", "human", "init", "--dry-run", "--json", "--sign-in-with-apple"],
88+
{ cwd: fixtureRoot, env: isolatedCLIEnvironment(configDir) },
89+
);
90+
expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0);
91+
expect(result.stderr).toBe("");
92+
93+
const output = JSON.parse(result.stdout) as {
94+
inspection: {
95+
appTargets: Array<{
96+
configurations: Array<{
97+
entitlements?: {
98+
associatedDomains: string[];
99+
literalAppIdentifierPrefix?: string;
100+
};
101+
}>;
102+
}>;
103+
diagnostics: Array<{ code: string }>;
104+
};
105+
plan: {
106+
steps: Array<{ id: string; status: string; automatable: boolean }>;
107+
};
108+
};
109+
const configurations = output.inspection.appTargets[0]?.configurations ?? [];
110+
expect(configurations).toHaveLength(2);
111+
expect(configurations.map((configuration) => configuration.entitlements)).toEqual([
112+
expect.objectContaining({
113+
associatedDomains: ["webcredentials:clerk.example.test"],
114+
literalAppIdentifierPrefix: "LEGACY1234",
115+
}),
116+
expect.objectContaining({
117+
associatedDomains: ["webcredentials:clerk.example.test"],
118+
literalAppIdentifierPrefix: "LEGACY1234",
119+
}),
120+
]);
121+
expect(output.inspection.diagnostics.map((diagnostic) => diagnostic.code)).not.toContain(
122+
"xcode.unreadable-entitlements",
123+
);
124+
expect(output.plan.steps).toContainEqual(
125+
expect.objectContaining({
126+
id: "configure-publishable-key",
127+
status: "required",
128+
automatable: true,
129+
}),
130+
);
131+
expect(output.plan.steps).toContainEqual(
132+
expect.objectContaining({
133+
id: "enable-native-apple",
134+
status: "required",
135+
automatable: true,
136+
}),
137+
);
138+
expect(result.stdout).not.toContain("unreadable-entitlements");
139+
expect(result.stdout).not.toContain("unreadable-local-secrets");
140+
} finally {
141+
await rm(temporaryRoot, { recursive: true, force: true });
142+
}
143+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Bun macro imports can be order-dependent when a test first reaches the CLI
2+
// through a deeply mocked command graph. Load the real version module before
3+
// test files so every isolated worker resolves its macro deterministically.
4+
import "../lib/version.ts";

0 commit comments

Comments
 (0)