@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 +89 -2
- package/dist/find.d.ts +9 -0
- package/dist/find.js +43 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/pbxproj.d.ts +67 -0
- package/dist/pbxproj.js +287 -0
- package/dist/plist.d.ts +15 -0
- package/dist/plist.js +147 -0
- package/dist/project.d.ts +157 -0
- package/dist/project.js +578 -0
- package/package.json +31 -4
package/dist/plist.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
/** Parses an XML plist. Returns `undefined` when the text is not an XML plist. */
|
|
11
|
+
export function parseXmlPlist(text) {
|
|
12
|
+
if (!/<plist[\s>]/.test(text))
|
|
13
|
+
return undefined;
|
|
14
|
+
const reader = new XmlPlistReader(text);
|
|
15
|
+
try {
|
|
16
|
+
return reader.read();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class XmlPlistReader {
|
|
23
|
+
text;
|
|
24
|
+
pos = 0;
|
|
25
|
+
constructor(text) {
|
|
26
|
+
this.text = text;
|
|
27
|
+
}
|
|
28
|
+
read() {
|
|
29
|
+
const plist = this.nextTag();
|
|
30
|
+
if (!plist || plist.name !== 'plist')
|
|
31
|
+
throw new Error('not a plist');
|
|
32
|
+
if (plist.selfClosing)
|
|
33
|
+
return undefined;
|
|
34
|
+
const value = this.readValue();
|
|
35
|
+
this.expectClosing('plist');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/** Reads the next opening tag (skipping the prolog, DOCTYPE, comments and whitespace). */
|
|
39
|
+
nextTag() {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const lt = this.text.indexOf('<', this.pos);
|
|
42
|
+
if (lt < 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (this.text.startsWith('<?', lt)) {
|
|
45
|
+
this.pos = this.text.indexOf('?>', lt) + 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (this.text.startsWith('<!--', lt)) {
|
|
49
|
+
this.pos = this.text.indexOf('-->', lt) + 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (this.text.startsWith('<!', lt)) {
|
|
53
|
+
this.pos = this.text.indexOf('>', lt) + 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const gt = this.text.indexOf('>', lt);
|
|
57
|
+
if (gt < 0)
|
|
58
|
+
throw new Error('unterminated tag');
|
|
59
|
+
let inside = this.text.slice(lt + 1, gt).trim();
|
|
60
|
+
const selfClosing = inside.endsWith('/');
|
|
61
|
+
if (selfClosing)
|
|
62
|
+
inside = inside.slice(0, -1).trim();
|
|
63
|
+
const name = inside.split(/\s/, 1)[0] ?? '';
|
|
64
|
+
this.pos = gt + 1;
|
|
65
|
+
return { name, selfClosing, end: gt + 1 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Peeks: is the next tag the closing `</name>`? Consumes it when so. */
|
|
69
|
+
atClosing(name) {
|
|
70
|
+
const rest = this.text.slice(this.pos);
|
|
71
|
+
const match = /^\s*<\/\s*([^\s>]+)\s*>/.exec(rest);
|
|
72
|
+
if (!match || match[1] !== name)
|
|
73
|
+
return false;
|
|
74
|
+
this.pos += match[0].length;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
expectClosing(name) {
|
|
78
|
+
if (!this.atClosing(name))
|
|
79
|
+
throw new Error(`expected </${name}>`);
|
|
80
|
+
}
|
|
81
|
+
/** Reads the text content up to `</name>` (entities decoded). */
|
|
82
|
+
readText(name) {
|
|
83
|
+
const close = this.text.indexOf(`</${name}>`, this.pos);
|
|
84
|
+
if (close < 0)
|
|
85
|
+
throw new Error(`expected </${name}>`);
|
|
86
|
+
const raw = this.text.slice(this.pos, close);
|
|
87
|
+
this.pos = close + name.length + 3;
|
|
88
|
+
return decodeEntities(raw);
|
|
89
|
+
}
|
|
90
|
+
readValue() {
|
|
91
|
+
const tag = this.nextTag();
|
|
92
|
+
if (!tag)
|
|
93
|
+
throw new Error('expected a value');
|
|
94
|
+
switch (tag.name) {
|
|
95
|
+
case 'string':
|
|
96
|
+
case 'date':
|
|
97
|
+
case 'data':
|
|
98
|
+
return tag.selfClosing ? '' : this.readText(tag.name);
|
|
99
|
+
case 'integer':
|
|
100
|
+
case 'real':
|
|
101
|
+
return tag.selfClosing ? 0 : Number(this.readText(tag.name).trim());
|
|
102
|
+
case 'true':
|
|
103
|
+
return true;
|
|
104
|
+
case 'false':
|
|
105
|
+
return false;
|
|
106
|
+
case 'array': {
|
|
107
|
+
const items = [];
|
|
108
|
+
if (tag.selfClosing)
|
|
109
|
+
return items;
|
|
110
|
+
while (!this.atClosing('array'))
|
|
111
|
+
items.push(this.readValue());
|
|
112
|
+
return items;
|
|
113
|
+
}
|
|
114
|
+
case 'dict': {
|
|
115
|
+
const dict = {};
|
|
116
|
+
if (tag.selfClosing)
|
|
117
|
+
return dict;
|
|
118
|
+
while (!this.atClosing('dict')) {
|
|
119
|
+
const keyTag = this.nextTag();
|
|
120
|
+
if (!keyTag || keyTag.name !== 'key')
|
|
121
|
+
throw new Error('expected <key>');
|
|
122
|
+
const key = keyTag.selfClosing ? '' : this.readText('key');
|
|
123
|
+
dict[key] = this.readValue();
|
|
124
|
+
}
|
|
125
|
+
return dict;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`unexpected <${tag.name}>`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const NAMED_ENTITIES = {
|
|
133
|
+
amp: '&',
|
|
134
|
+
lt: '<',
|
|
135
|
+
gt: '>',
|
|
136
|
+
quot: '"',
|
|
137
|
+
apos: "'",
|
|
138
|
+
};
|
|
139
|
+
function decodeEntities(text) {
|
|
140
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (whole, body) => {
|
|
141
|
+
if (body.startsWith('#x'))
|
|
142
|
+
return String.fromCodePoint(parseInt(body.slice(2), 16));
|
|
143
|
+
if (body.startsWith('#'))
|
|
144
|
+
return String.fromCodePoint(parseInt(body.slice(1), 10));
|
|
145
|
+
return NAMED_ENTITIES[body] ?? whole;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprets a parsed `project.pbxproj` (pbxproj.ts) as targets with resolved
|
|
3
|
+
* file paths and build settings, so a CLI can find an iOS app's Swift sources,
|
|
4
|
+
* asset catalogs and settings without Xcode.
|
|
5
|
+
*
|
|
6
|
+
* Object graph in brief: the `rootObject` is a `PBXProject` with `targets`
|
|
7
|
+
* (`PBXNativeTarget`), a `mainGroup` (the tree of `PBXGroup`s and
|
|
8
|
+
* `PBXFileReference`s) and a `buildConfigurationList`. Each target has
|
|
9
|
+
* `buildPhases` (`PBXSourcesBuildPhase`, `PBXResourcesBuildPhase`, ...) whose
|
|
10
|
+
* `files` are `PBXBuildFile`s pointing at a `fileRef`, plus its own
|
|
11
|
+
* `buildConfigurationList` (`XCConfigurationList` -> `XCBuildConfiguration`
|
|
12
|
+
* with `buildSettings`). Xcode 16 projects can instead point a target at
|
|
13
|
+
* `PBXFileSystemSynchronizedRootGroup`s: folders whose contents are members of
|
|
14
|
+
* the target implicitly, minus per-target exception sets.
|
|
15
|
+
*/
|
|
16
|
+
import { type PbxprojDocument } from './pbxproj.js';
|
|
17
|
+
export declare const APPLICATION_PRODUCT_TYPE = "com.apple.product-type.application";
|
|
18
|
+
export interface XcodeProject {
|
|
19
|
+
/** Absolute path of the `.xcodeproj` bundle. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Directory containing the `.xcodeproj`: the source root, what `$(SRCROOT)` means. */
|
|
22
|
+
root: string;
|
|
23
|
+
objectVersion: number;
|
|
24
|
+
/**
|
|
25
|
+
* The Swift packages the project references (`PBXProject.packageReferences`):
|
|
26
|
+
* remote ones (`XCRemoteSwiftPackageReference`, with Xcode's dependency rule)
|
|
27
|
+
* and local ones (`XCLocalSwiftPackageReference`), in project order.
|
|
28
|
+
*/
|
|
29
|
+
packages: SwiftPackageReference[];
|
|
30
|
+
/** Every `PBXNativeTarget`, in project order. */
|
|
31
|
+
targets: XcodeTarget[];
|
|
32
|
+
/** The targets whose product is an application (`com.apple.product-type.application`). */
|
|
33
|
+
appTargets(): XcodeTarget[];
|
|
34
|
+
}
|
|
35
|
+
export interface XcodeTarget {
|
|
36
|
+
name: string;
|
|
37
|
+
/** e.g. `com.apple.product-type.application`. */
|
|
38
|
+
productType: string;
|
|
39
|
+
/** `PRODUCT_NAME` with `$(TARGET_NAME)` resolved; falls back to the target name. */
|
|
40
|
+
productName: string;
|
|
41
|
+
/** `PRODUCT_BUNDLE_IDENTIFIER`. */
|
|
42
|
+
bundleIdentifier?: string;
|
|
43
|
+
/**
|
|
44
|
+
* `INFOPLIST_KEY_CFBundleDisplayName`, else `CFBundleDisplayName` from the
|
|
45
|
+
* `INFOPLIST_FILE` when that file exists and is an XML plist.
|
|
46
|
+
*/
|
|
47
|
+
displayName?: string;
|
|
48
|
+
/** `ASSETCATALOG_COMPILER_APPICON_NAME`. */
|
|
49
|
+
appIconName?: string;
|
|
50
|
+
/** `SWIFT_VERSION`. */
|
|
51
|
+
swiftVersion?: string;
|
|
52
|
+
/** `IPHONEOS_DEPLOYMENT_TARGET`. */
|
|
53
|
+
deploymentTarget?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Absolute paths of the `.swift` files compiled by the target: the Sources
|
|
56
|
+
* build phase in project (navigator) order, then the files of synchronized
|
|
57
|
+
* folders (sorted).
|
|
58
|
+
*/
|
|
59
|
+
sources: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Absolute paths of the Core ML models the target compiles (`.mlmodel`
|
|
62
|
+
* files, `.mlpackage` bundles): the Sources build phase in project order,
|
|
63
|
+
* then those in synchronized folders. Xcode generates a Swift class for each.
|
|
64
|
+
*/
|
|
65
|
+
models: string[];
|
|
66
|
+
/** Absolute paths of the files and folders in the Resources build phase, in project (navigator) order. */
|
|
67
|
+
resources: string[];
|
|
68
|
+
/** Absolute paths of the `*.xcassets` among the resources and in synchronized folders. */
|
|
69
|
+
assetCatalogs: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Absolute paths of the target's localized string sources: string catalogs
|
|
72
|
+
* (`*.xcstrings`) and `.strings` / `.stringsdict` files (in `<language>.lproj`
|
|
73
|
+
* folders), among the resources and in synchronized folders. Xcode compiles
|
|
74
|
+
* them into the app's string tables.
|
|
75
|
+
*/
|
|
76
|
+
localizations: string[];
|
|
77
|
+
/** Names of the targets this one depends on (`PBXTargetDependency`). */
|
|
78
|
+
dependencies: string[];
|
|
79
|
+
/** Swift package products the target links (`XCSwiftPackageProductDependency.productName`). */
|
|
80
|
+
packageProducts: string[];
|
|
81
|
+
/**
|
|
82
|
+
* The same products with the package each comes from, when the project
|
|
83
|
+
* records it (`XCSwiftPackageProductDependency.package`; a local package
|
|
84
|
+
* added as a folder reference by older Xcodes has none).
|
|
85
|
+
*/
|
|
86
|
+
packageDependencies: SwiftPackageProductDependency[];
|
|
87
|
+
/**
|
|
88
|
+
* Merged build settings for a configuration: project-level settings first,
|
|
89
|
+
* target-level settings on top. `configuration` defaults to the target's
|
|
90
|
+
* default configuration: `Debug` when it has one, else the configuration
|
|
91
|
+
* list's `defaultConfigurationName`, else the first configuration.
|
|
92
|
+
* `$(TARGET_NAME)`, `$(PRODUCT_NAME)`, `$(SRCROOT)` and `$(PROJECT_DIR)` are
|
|
93
|
+
* expanded; other `$(...)` references are left as they are. An `.xcconfig`
|
|
94
|
+
* referenced by `baseConfigurationReference` is not read.
|
|
95
|
+
*/
|
|
96
|
+
settings(configuration?: string): Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The version rule of a remote package, as Xcode's "Dependency Rule" stores
|
|
100
|
+
* it in `XCRemoteSwiftPackageReference.requirement`.
|
|
101
|
+
*/
|
|
102
|
+
export type SwiftPackageRequirement = {
|
|
103
|
+
kind: 'upToNextMajorVersion';
|
|
104
|
+
minimumVersion: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'upToNextMinorVersion';
|
|
107
|
+
minimumVersion: string;
|
|
108
|
+
} | {
|
|
109
|
+
kind: 'exactVersion';
|
|
110
|
+
version: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'versionRange';
|
|
113
|
+
minimumVersion: string;
|
|
114
|
+
maximumVersion: string;
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'branch';
|
|
117
|
+
branch: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'revision';
|
|
120
|
+
revision: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* A Swift package the project references. `identity` is SwiftPM's name for
|
|
124
|
+
* it: the last path component of the URL (or folder), without `.git`,
|
|
125
|
+
* lower-cased; it is what `.product(name:package:)` names.
|
|
126
|
+
*/
|
|
127
|
+
export type SwiftPackageReference = {
|
|
128
|
+
kind: 'remote';
|
|
129
|
+
url: string;
|
|
130
|
+
identity: string;
|
|
131
|
+
/** Absent when the project's rule has a kind this reader does not know. */
|
|
132
|
+
requirement?: SwiftPackageRequirement;
|
|
133
|
+
} | {
|
|
134
|
+
kind: 'local';
|
|
135
|
+
/** Absolute path of the package folder (`relativePath` from the source root). */
|
|
136
|
+
path: string;
|
|
137
|
+
identity: string;
|
|
138
|
+
};
|
|
139
|
+
/** A package product a target links, with its package when the project says. */
|
|
140
|
+
export interface SwiftPackageProductDependency {
|
|
141
|
+
product: string;
|
|
142
|
+
package?: SwiftPackageReference;
|
|
143
|
+
}
|
|
144
|
+
/** SwiftPM's identity of a package at a URL or path: its last component, without `.git`, lower-cased. */
|
|
145
|
+
export declare function packageIdentity(location: string): string;
|
|
146
|
+
/** Reads `<xcodeprojPath>/project.pbxproj` and interprets it. */
|
|
147
|
+
export declare function readXcodeProject(xcodeprojPath: string): XcodeProject;
|
|
148
|
+
/**
|
|
149
|
+
* Interprets an already parsed document as the project at `xcodeprojPath`
|
|
150
|
+
* (which need not exist; its parent directory is the source root).
|
|
151
|
+
*/
|
|
152
|
+
export declare function xcodeProjectFromDocument(doc: PbxprojDocument, xcodeprojPath: string): XcodeProject;
|
|
153
|
+
/**
|
|
154
|
+
* Replaces `$(NAME)` and `${NAME}` for the names in `variables`; other
|
|
155
|
+
* references (`$(inherited)`, `$(SDKROOT)`, ...) are kept verbatim.
|
|
156
|
+
*/
|
|
157
|
+
export declare function expandVariables(value: string, variables: Record<string, string>): string;
|