@swiftbrowser/xcodeproj 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # @swiftbrowser/xcodeproj
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reads Xcode projects without Xcode: a pure-TypeScript parser for `project.pbxproj`
4
+ (Node built-ins only) that answers the questions a CLI has about an iOS app:
5
+ which targets exist, which Swift files and asset catalogs they compile, and what
6
+ their build settings are.
7
+
8
+ ```ts
9
+ import { findXcodeProjects, readXcodeProject } from '@swiftbrowser/xcodeproj';
10
+
11
+ const [xcodeproj] = findXcodeProjects(process.cwd()); // ./*.xcodeproj, else one level down
12
+ const project = readXcodeProject(xcodeproj);
13
+ const [app] = project.appTargets(); // targets whose product is an application
14
+
15
+ app.name; // 'GuessTheFlag'
16
+ app.productName; // PRODUCT_NAME with $(TARGET_NAME) resolved
17
+ app.bundleIdentifier; // PRODUCT_BUNDLE_IDENTIFIER
18
+ app.displayName; // INFOPLIST_KEY_CFBundleDisplayName, else CFBundleDisplayName from INFOPLIST_FILE
19
+ app.appIconName; // ASSETCATALOG_COMPILER_APPICON_NAME
20
+ app.swiftVersion; // SWIFT_VERSION
21
+ app.deploymentTarget; // IPHONEOS_DEPLOYMENT_TARGET
22
+ app.sources; // absolute paths of the .swift files, in navigator order
23
+ app.models; // the Core ML models (.mlmodel, .mlpackage) it compiles
24
+ app.resources; // absolute paths of the Resources build phase entries
25
+ app.assetCatalogs; // the *.xcassets among them (and in synchronized folders)
26
+ app.dependencies; // names of target dependencies
27
+ app.packageProducts; // Swift package products the target links
28
+ app.packageDependencies; // the same, each with its package (URL and version rule, or local path)
29
+ project.packages; // the project's Swift package references
30
+ app.settings('Release'); // merged build settings for a configuration
31
+ ```
32
+
33
+ ## API
34
+
35
+ - `readXcodeProject(xcodeprojPath)` parses `<xcodeprojPath>/project.pbxproj` and
36
+ returns an `XcodeProject`: `path`, `root` (the directory containing the
37
+ bundle, what `$(SRCROOT)` means), `objectVersion`, `targets` (every
38
+ `PBXNativeTarget`), `packages` (its `XCRemoteSwiftPackageReference`s, with
39
+ the dependency rule, and `XCLocalSwiftPackageReference`s, with the absolute
40
+ path; each with SwiftPM's identity, see `packageIdentity`) and `appTargets()`.
41
+ - `parsePbxproj(text)` returns the raw object graph: `{ objectVersion,
42
+ rootObject, objects }`, where each object has an `isa` plus arbitrary fields
43
+ (strings, arrays, nested dictionaries). `parseOldStylePlist(text)` parses any
44
+ old-style property list fragment.
45
+ - `xcodeProjectFromDocument(doc, xcodeprojPath)` interprets an already parsed
46
+ document as the project at that path.
47
+ - `findXcodeProjects(dir)` lists the `*.xcodeproj` bundles directly in `dir`,
48
+ else one level down (sorted), skipping `Pods/`, `Carthage/`, `DerivedData/`,
49
+ `.build/`, `node_modules/` and dot-directories.
50
+ - `parseXmlPlist(text)` is the minimal XML plist reader used for `Info.plist`.
51
+
52
+ ## How paths are resolved
53
+
54
+ A file's absolute path is the project root plus the chain of group `path`s
55
+ leading to it, following each object's `sourceTree`: `<group>` is relative to
56
+ the parent group, `SOURCE_ROOT` to the root, `<absolute>` is absolute; others
57
+ (`BUILT_PRODUCTS_DIR`, `SDKROOT`, ...) fall back to the root. Groups without a
58
+ `path` contribute nothing. Build phase files are listed in the order of the
59
+ group tree (what the Xcode navigator shows), not the arbitrary order of the
60
+ build phase. Paths are resolved exactly as written; a project whose group `path`
61
+ differs from the folder on disk only in letter case works on macOS but not on a
62
+ case-sensitive filesystem.
63
+
64
+ Xcode 16 "synchronized folders" (`PBXFileSystemSynchronizedRootGroup`
65
+ referenced from a target's `fileSystemSynchronizedGroups`) are read from disk:
66
+ every `*.swift` under the folder (recursive, sorted) is a source of the target
67
+ and every `*.xcassets` an asset catalog, minus the paths listed in the target's
68
+ `PBXFileSystemSynchronizedBuildFileExceptionSet.membershipExceptions`. A folder
69
+ missing on disk contributes nothing.
70
+
71
+ ## Build settings
72
+
73
+ `target.settings(configuration?)` merges the project-level `buildSettings` of
74
+ the named `XCBuildConfiguration` with the target-level ones on top. The default
75
+ configuration is `Debug` when the target has one, else the configuration list's
76
+ `defaultConfigurationName`. Array-valued settings are space-joined.
77
+ `$(TARGET_NAME)`, `$(PRODUCT_NAME)`, `$(SRCROOT)` and `$(PROJECT_DIR)` (also in
78
+ `${...}` form) are expanded; every other `$(...)` reference, including
79
+ `$(inherited)` and modifiers such as `$(X:rfc1034identifier)`, is left as is.
80
+
81
+ ## Not handled
82
+
83
+ - `.xcconfig` files referenced by `baseConfigurationReference` are not read, so
84
+ settings defined only there are absent.
85
+ - Binary `Info.plist` files are skipped (only XML plists are read).
86
+ - `PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet` (Xcode 16
87
+ per-build-phase exceptions) and `PBXBuildRule`s are ignored.
88
+ - Workspaces (`.xcworkspace`), referenced projects (`PBXReferenceProxy`) and
89
+ `PBXAggregateTarget`/`PBXLegacyTarget` are not modelled; only
90
+ `PBXNativeTarget`s appear in `targets`.
package/dist/find.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Locating `.xcodeproj` bundles on disk, for a CLI run from a project folder.
3
+ */
4
+ /**
5
+ * The `.xcodeproj` bundles directly in `dir`, as absolute paths (sorted). When
6
+ * there are none, looks one level down, skipping `Pods/`, `Carthage/`,
7
+ * `DerivedData/`, `.build/`, `node_modules/` and dot-directories.
8
+ */
9
+ export declare function findXcodeProjects(dir: string): string[];
package/dist/find.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Locating `.xcodeproj` bundles on disk, for a CLI run from a project folder.
3
+ */
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ /** Directories whose `.xcodeproj`s are generated or third party, never the user's app. */
7
+ const IGNORED_DIRECTORIES = new Set(['Pods', 'Carthage', 'DerivedData', '.build', 'node_modules']);
8
+ /**
9
+ * The `.xcodeproj` bundles directly in `dir`, as absolute paths (sorted). When
10
+ * there are none, looks one level down, skipping `Pods/`, `Carthage/`,
11
+ * `DerivedData/`, `.build/`, `node_modules/` and dot-directories.
12
+ */
13
+ export function findXcodeProjects(dir) {
14
+ const absolute = path.resolve(dir);
15
+ const direct = xcodeProjectsIn(absolute);
16
+ if (direct.length > 0)
17
+ return direct;
18
+ const nested = [];
19
+ for (const entry of readDirectories(absolute)) {
20
+ if (IGNORED_DIRECTORIES.has(entry) || entry.startsWith('.'))
21
+ continue;
22
+ nested.push(...xcodeProjectsIn(path.join(absolute, entry)));
23
+ }
24
+ return nested.sort();
25
+ }
26
+ function xcodeProjectsIn(dir) {
27
+ return readDirectories(dir)
28
+ .filter((name) => name.endsWith('.xcodeproj'))
29
+ .sort()
30
+ .map((name) => path.join(dir, name));
31
+ }
32
+ /** Names of the subdirectories of `dir` (empty when it cannot be read). */
33
+ function readDirectories(dir) {
34
+ try {
35
+ return fs
36
+ .readdirSync(dir, { withFileTypes: true })
37
+ .filter((entry) => entry.isDirectory())
38
+ .map((entry) => entry.name);
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @swiftbrowser/xcodeproj: reads Xcode projects without Xcode.
3
+ *
4
+ * - `readXcodeProject(path)` interprets `<path>/project.pbxproj` as targets
5
+ * with resolved sources, asset catalogs and build settings.
6
+ * - `parsePbxproj(text)` gives the raw object graph.
7
+ * - `findXcodeProjects(dir)` locates `.xcodeproj` bundles.
8
+ */
9
+ export { parsePbxproj, parseOldStylePlist, tokenizePbxproj, PbxprojSyntaxError, type PbxValue, type PbxDict, type PbxObject, type PbxprojDocument, } from './pbxproj.js';
10
+ export { readXcodeProject, xcodeProjectFromDocument, expandVariables, packageIdentity, APPLICATION_PRODUCT_TYPE, type XcodeProject, type XcodeTarget, type SwiftPackageReference, type SwiftPackageRequirement, type SwiftPackageProductDependency, } from './project.js';
11
+ export { findXcodeProjects } from './find.js';
12
+ export { parseXmlPlist, type PlistValue, type PlistDict } from './plist.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @swiftbrowser/xcodeproj: reads Xcode projects without Xcode.
3
+ *
4
+ * - `readXcodeProject(path)` interprets `<path>/project.pbxproj` as targets
5
+ * with resolved sources, asset catalogs and build settings.
6
+ * - `parsePbxproj(text)` gives the raw object graph.
7
+ * - `findXcodeProjects(dir)` locates `.xcodeproj` bundles.
8
+ */
9
+ export { parsePbxproj, parseOldStylePlist, tokenizePbxproj, PbxprojSyntaxError, } from './pbxproj.js';
10
+ export { readXcodeProject, xcodeProjectFromDocument, expandVariables, packageIdentity, APPLICATION_PRODUCT_TYPE, } from './project.js';
11
+ export { findXcodeProjects } from './find.js';
12
+ export { parseXmlPlist } from './plist.js';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Parser for `project.pbxproj`, the file inside every `.xcodeproj` bundle.
3
+ *
4
+ * The format is the old-style ASCII (NeXTSTEP) property list:
5
+ *
6
+ * // !$*UTF8*$!
7
+ * {
8
+ * archiveVersion = 1;
9
+ * objectVersion = 56;
10
+ * objects = {
11
+ * 518D77712AD7303000E0BF20 (comment: ContentView.swift) = {isa = PBXFileReference; path = ContentView.swift; sourceTree = "<group>"; };
12
+ * ...
13
+ * };
14
+ * rootObject = 518D77662AD7303000E0BF20 (comment: Project object);
15
+ * }
16
+ *
17
+ * Dictionaries are `{ key = value; }`, arrays are `( a, b, )`, and every scalar is
18
+ * a string: either an unquoted word (letters, digits, `_ . / $ - + :` ...) or a
19
+ * double-quoted string with backslash escapes. Block comments (slash-star) and
20
+ * line comments (double slash) may appear anywhere; Xcode puts a block comment
21
+ * with the object's name after every object id, as sketched above. The parser
22
+ * is tolerant of trailing commas and of a missing `;` before `}`.
23
+ *
24
+ * This module only builds the raw object graph; `project.ts` interprets it.
25
+ */
26
+ /** A pbxproj value: a string, an array of values or a dictionary of values. */
27
+ export type PbxValue = string | PbxValue[] | PbxDict;
28
+ export interface PbxDict {
29
+ [key: string]: PbxValue;
30
+ }
31
+ /** One entry of `objects`: every object has an `isa` class name plus arbitrary fields. */
32
+ export interface PbxObject extends PbxDict {
33
+ isa: string;
34
+ }
35
+ /** The parsed file. `objects` is keyed by object id (usually 24 hex digits). */
36
+ export interface PbxprojDocument {
37
+ archiveVersion: number;
38
+ objectVersion: number;
39
+ rootObject: string;
40
+ objects: Record<string, PbxObject>;
41
+ }
42
+ /** Thrown for malformed input; `offset` is the character index where parsing failed. */
43
+ export declare class PbxprojSyntaxError extends Error {
44
+ readonly offset: number;
45
+ readonly line: number;
46
+ readonly column: number;
47
+ constructor(message: string, offset: number, line: number, column: number);
48
+ }
49
+ type TokenKind = '{' | '}' | '(' | ')' | '=' | ';' | ',' | 'string' | 'eof';
50
+ interface Token {
51
+ kind: TokenKind;
52
+ /** The string value (unescaped) for `string` tokens; the punctuation otherwise. */
53
+ value: string;
54
+ /** Character offset of the token's first character, for error messages. */
55
+ offset: number;
56
+ }
57
+ /** Breaks the text into tokens, dropping comments. */
58
+ export declare function tokenizePbxproj(text: string): Token[];
59
+ /**
60
+ * Parses any old-style property list text into its value tree. `parsePbxproj`
61
+ * is the usual entry point; this one is handy for fragments and tests.
62
+ */
63
+ export declare function parseOldStylePlist(text: string): PbxValue;
64
+ /** Parses the text of a `project.pbxproj` file into its raw object graph. */
65
+ export declare function parsePbxproj(text: string): PbxprojDocument;
66
+ export declare function isDict(value: PbxValue | undefined): value is PbxDict;
67
+ export {};
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Parser for `project.pbxproj`, the file inside every `.xcodeproj` bundle.
3
+ *
4
+ * The format is the old-style ASCII (NeXTSTEP) property list:
5
+ *
6
+ * // !$*UTF8*$!
7
+ * {
8
+ * archiveVersion = 1;
9
+ * objectVersion = 56;
10
+ * objects = {
11
+ * 518D77712AD7303000E0BF20 (comment: ContentView.swift) = {isa = PBXFileReference; path = ContentView.swift; sourceTree = "<group>"; };
12
+ * ...
13
+ * };
14
+ * rootObject = 518D77662AD7303000E0BF20 (comment: Project object);
15
+ * }
16
+ *
17
+ * Dictionaries are `{ key = value; }`, arrays are `( a, b, )`, and every scalar is
18
+ * a string: either an unquoted word (letters, digits, `_ . / $ - + :` ...) or a
19
+ * double-quoted string with backslash escapes. Block comments (slash-star) and
20
+ * line comments (double slash) may appear anywhere; Xcode puts a block comment
21
+ * with the object's name after every object id, as sketched above. The parser
22
+ * is tolerant of trailing commas and of a missing `;` before `}`.
23
+ *
24
+ * This module only builds the raw object graph; `project.ts` interprets it.
25
+ */
26
+ /** Thrown for malformed input; `offset` is the character index where parsing failed. */
27
+ export class PbxprojSyntaxError extends Error {
28
+ offset;
29
+ line;
30
+ column;
31
+ constructor(message, offset, line, column) {
32
+ super(`${message} (line ${line}, column ${column})`);
33
+ this.offset = offset;
34
+ this.line = line;
35
+ this.column = column;
36
+ this.name = 'PbxprojSyntaxError';
37
+ }
38
+ }
39
+ const PUNCTUATION = new Set(['{', '}', '(', ')', '=', ';', ',']);
40
+ /** Characters that end an unquoted word (besides whitespace and comment starts). */
41
+ const WORD_TERMINATORS = new Set([...PUNCTUATION, '"']);
42
+ function isWhitespace(ch) {
43
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v';
44
+ }
45
+ /** Escape sequences recognised inside quoted strings (besides `\\uXXXX`, octal and the quote/backslash themselves). */
46
+ const SIMPLE_ESCAPES = {
47
+ n: '\n',
48
+ t: '\t',
49
+ r: '\r',
50
+ a: '\x07',
51
+ b: '\b',
52
+ f: '\f',
53
+ v: '\v',
54
+ '"': '"',
55
+ "'": "'",
56
+ '\\': '\\',
57
+ };
58
+ /** Breaks the text into tokens, dropping comments. */
59
+ export function tokenizePbxproj(text) {
60
+ const tokens = [];
61
+ const length = text.length;
62
+ let i = 0;
63
+ // A function declaration (not an arrow) so TypeScript treats calls as assertions.
64
+ function fail(message, offset) {
65
+ throw syntaxError(text, message, offset);
66
+ }
67
+ while (i < length) {
68
+ const ch = text[i];
69
+ if (isWhitespace(ch)) {
70
+ i++;
71
+ continue;
72
+ }
73
+ // Comments: `/* ... */` and `// ...` to end of line.
74
+ if (ch === '/' && text[i + 1] === '*') {
75
+ const end = text.indexOf('*/', i + 2);
76
+ if (end < 0)
77
+ fail('Unterminated block comment', i);
78
+ i = end + 2;
79
+ continue;
80
+ }
81
+ if (ch === '/' && text[i + 1] === '/') {
82
+ const end = text.indexOf('\n', i + 2);
83
+ i = end < 0 ? length : end + 1;
84
+ continue;
85
+ }
86
+ if (PUNCTUATION.has(ch)) {
87
+ tokens.push({ kind: ch, value: ch, offset: i });
88
+ i++;
89
+ continue;
90
+ }
91
+ if (ch === '"') {
92
+ const start = i;
93
+ i++;
94
+ let value = '';
95
+ for (;;) {
96
+ if (i >= length)
97
+ fail('Unterminated string', start);
98
+ const c = text[i];
99
+ if (c === '"') {
100
+ i++;
101
+ break;
102
+ }
103
+ if (c !== '\\') {
104
+ value += c;
105
+ i++;
106
+ continue;
107
+ }
108
+ // Backslash escape.
109
+ const e = text[i + 1];
110
+ if (e === undefined)
111
+ fail('Unterminated string', start);
112
+ if (e === 'U' || e === 'u') {
113
+ // `\Uxxxx`: up to four hex digits.
114
+ const hex = /^[0-9a-fA-F]{1,4}/.exec(text.slice(i + 2, i + 6));
115
+ if (!hex)
116
+ fail('Invalid \\U escape', i);
117
+ value += String.fromCharCode(parseInt(hex[0], 16));
118
+ i += 2 + hex[0].length;
119
+ }
120
+ else if (e >= '0' && e <= '7') {
121
+ // `\ooo`: up to three octal digits.
122
+ const oct = /^[0-7]{1,3}/.exec(text.slice(i + 1, i + 4));
123
+ value += String.fromCharCode(parseInt(oct[0], 8));
124
+ i += 1 + oct[0].length;
125
+ }
126
+ else {
127
+ // Unknown escapes keep the character (`\x` -> `x`), like Foundation does.
128
+ value += SIMPLE_ESCAPES[e] ?? e;
129
+ i += 2;
130
+ }
131
+ }
132
+ tokens.push({ kind: 'string', value, offset: start });
133
+ continue;
134
+ }
135
+ // Unquoted word: runs until whitespace, punctuation, a quote or a comment start.
136
+ const start = i;
137
+ while (i < length) {
138
+ const c = text[i];
139
+ if (isWhitespace(c) || WORD_TERMINATORS.has(c))
140
+ break;
141
+ if (c === '/' && (text[i + 1] === '*' || text[i + 1] === '/'))
142
+ break;
143
+ i++;
144
+ }
145
+ if (i === start)
146
+ fail(`Unexpected character ${JSON.stringify(ch)}`, i);
147
+ tokens.push({ kind: 'string', value: text.slice(start, i), offset: start });
148
+ }
149
+ tokens.push({ kind: 'eof', value: '', offset: length });
150
+ return tokens;
151
+ }
152
+ function syntaxError(text, message, offset) {
153
+ let line = 1;
154
+ let lineStart = 0;
155
+ for (let i = 0; i < offset && i < text.length; i++) {
156
+ if (text[i] === '\n') {
157
+ line++;
158
+ lineStart = i + 1;
159
+ }
160
+ }
161
+ return new PbxprojSyntaxError(message, offset, line, offset - lineStart + 1);
162
+ }
163
+ // ----- Parser --------------------------------------------------------------
164
+ class Parser {
165
+ text;
166
+ tokens;
167
+ pos = 0;
168
+ constructor(text, tokens) {
169
+ this.text = text;
170
+ this.tokens = tokens;
171
+ }
172
+ peek() {
173
+ return this.tokens[this.pos];
174
+ }
175
+ next() {
176
+ const token = this.tokens[this.pos];
177
+ if (token.kind !== 'eof')
178
+ this.pos++;
179
+ return token;
180
+ }
181
+ fail(message, token = this.peek()) {
182
+ const found = token.kind === 'eof' ? 'end of input' : JSON.stringify(token.value);
183
+ throw syntaxError(this.text, `${message}, found ${found}`, token.offset);
184
+ }
185
+ expect(kind) {
186
+ const token = this.peek();
187
+ if (token.kind !== kind)
188
+ this.fail(`Expected ${kind}`);
189
+ return this.next();
190
+ }
191
+ /** Parses the whole document: one top-level value followed by end of input. */
192
+ parseDocument() {
193
+ const value = this.parseValue();
194
+ if (this.peek().kind !== 'eof')
195
+ this.fail('Expected end of input');
196
+ return value;
197
+ }
198
+ parseValue() {
199
+ const token = this.peek();
200
+ switch (token.kind) {
201
+ case '{':
202
+ return this.parseDict();
203
+ case '(':
204
+ return this.parseArray();
205
+ case 'string':
206
+ return this.next().value;
207
+ default:
208
+ return this.fail('Expected a value');
209
+ }
210
+ }
211
+ parseDict() {
212
+ this.expect('{');
213
+ const dict = {};
214
+ while (this.peek().kind !== '}') {
215
+ if (this.peek().kind === 'eof')
216
+ this.fail('Unterminated dictionary');
217
+ const key = this.expect('string').value;
218
+ this.expect('=');
219
+ dict[key] = this.parseValue();
220
+ // The `;` after an entry is required by Xcode but we accept a missing one before `}`.
221
+ if (this.peek().kind === ';')
222
+ this.next();
223
+ else if (this.peek().kind === 'eof')
224
+ this.fail('Unterminated dictionary');
225
+ else if (this.peek().kind !== '}')
226
+ this.fail('Expected ; after dictionary entry');
227
+ }
228
+ this.expect('}');
229
+ return dict;
230
+ }
231
+ parseArray() {
232
+ this.expect('(');
233
+ const items = [];
234
+ while (this.peek().kind !== ')') {
235
+ if (this.peek().kind === 'eof')
236
+ this.fail('Unterminated array');
237
+ items.push(this.parseValue());
238
+ // Commas separate items; a trailing comma is the norm. A missing comma is
239
+ // tolerated since values are self-delimiting.
240
+ if (this.peek().kind === ',')
241
+ this.next();
242
+ }
243
+ this.expect(')');
244
+ return items;
245
+ }
246
+ }
247
+ /**
248
+ * Parses any old-style property list text into its value tree. `parsePbxproj`
249
+ * is the usual entry point; this one is handy for fragments and tests.
250
+ */
251
+ export function parseOldStylePlist(text) {
252
+ return new Parser(text, tokenizePbxproj(text)).parseDocument();
253
+ }
254
+ /** Parses the text of a `project.pbxproj` file into its raw object graph. */
255
+ export function parsePbxproj(text) {
256
+ const value = parseOldStylePlist(text);
257
+ if (!isDict(value))
258
+ throw new Error('project.pbxproj: top-level value is not a dictionary');
259
+ const objectsValue = value['objects'];
260
+ if (!isDict(objectsValue))
261
+ throw new Error('project.pbxproj: missing `objects` dictionary');
262
+ const objects = {};
263
+ for (const [id, object] of Object.entries(objectsValue)) {
264
+ if (!isDict(object) || typeof object['isa'] !== 'string') {
265
+ throw new Error(`project.pbxproj: object ${id} has no isa`);
266
+ }
267
+ objects[id] = object;
268
+ }
269
+ const rootObject = value['rootObject'];
270
+ if (typeof rootObject !== 'string')
271
+ throw new Error('project.pbxproj: missing `rootObject`');
272
+ return {
273
+ archiveVersion: toInteger(value['archiveVersion'], 1),
274
+ objectVersion: toInteger(value['objectVersion'], 0),
275
+ rootObject,
276
+ objects,
277
+ };
278
+ }
279
+ export function isDict(value) {
280
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
281
+ }
282
+ function toInteger(value, fallback) {
283
+ if (typeof value !== 'string')
284
+ return fallback;
285
+ const n = Number.parseInt(value, 10);
286
+ return Number.isNaN(n) ? fallback : n;
287
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal reader for XML property lists (`Info.plist`).
3
+ *
4
+ * Only what the project reader needs: the top-level `<dict>` with string,
5
+ * integer, real, boolean, date, data, array and nested dict values. Binary
6
+ * plists (`bplist00`) and the old-style ASCII format are not handled here;
7
+ * `parseXmlPlist` returns `undefined` for anything it cannot read, and the
8
+ * caller treats that as "no Info.plist values available".
9
+ */
10
+ export type PlistValue = string | number | boolean | PlistValue[] | PlistDict;
11
+ export interface PlistDict {
12
+ [key: string]: PlistValue;
13
+ }
14
+ /** Parses an XML plist. Returns `undefined` when the text is not an XML plist. */
15
+ export declare function parseXmlPlist(text: string): PlistValue | undefined;