voltra 2.0.0 → 2.1.1
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/build/cjs/config/normalize.js +93 -0
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/index.js +5 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/apply.js +11 -2
- package/build/cjs/platforms/android/apply.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +152 -130
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/gradle.js +120 -0
- package/build/cjs/platforms/android/gradle.js.map +1 -0
- package/build/cjs/platforms/ios/generated.js +246 -142
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +151 -0
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
- package/build/esm/config/normalize.js +93 -0
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/apply.js +11 -2
- package/build/esm/platforms/android/apply.js.map +1 -1
- package/build/esm/platforms/android/generated.js +152 -97
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/gradle.js +112 -0
- package/build/esm/platforms/android/gradle.js.map +1 -0
- package/build/esm/platforms/ios/generated.js +246 -109
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +69 -1
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +111 -0
- package/build/esm/platforms/shared/widgetModule.js.map +1 -0
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/types.d.ts +32 -0
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/platforms/android/apply.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/android/gradle.d.ts +18 -0
- package/build/types/platforms/android/gradle.d.ts.map +1 -0
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +9 -0
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/apply/index.ts +309 -0
- package/src/apply/preflight.ts +172 -0
- package/src/bin.ts +14 -0
- package/src/commands/apply.ts +114 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/load.ts +100 -0
- package/src/config/normalize.ts +562 -0
- package/src/config/types.ts +317 -0
- package/src/cosmiconfig.d.ts +21 -0
- package/src/dependencies/platformPackages.ts +75 -0
- package/src/discovery/android.ts +233 -0
- package/src/discovery/ios.ts +579 -0
- package/src/fs/path.ts +25 -0
- package/src/fs/readWrite.ts +102 -0
- package/src/git/status.ts +205 -0
- package/src/index.ts +204 -0
- package/src/platforms/android/apply.ts +106 -0
- package/src/platforms/android/generated.ts +1284 -0
- package/src/platforms/android/gradle.ts +155 -0
- package/src/platforms/android/manifest.ts +379 -0
- package/src/platforms/ios/apply.ts +139 -0
- package/src/platforms/ios/deploymentTarget.ts +47 -0
- package/src/platforms/ios/entitlements.ts +146 -0
- package/src/platforms/ios/generated.ts +1233 -0
- package/src/platforms/ios/mainAppEntitlements.ts +11 -0
- package/src/platforms/ios/plist.ts +321 -0
- package/src/platforms/ios/podfile.ts +255 -0
- package/src/platforms/ios/targetName.ts +11 -0
- package/src/platforms/ios/xcode.ts +181 -0
- package/src/platforms/ios/xcodeTarget.ts +899 -0
- package/src/platforms/shared/widgetModule.ts +153 -0
- package/src/reporting/clack.ts +67 -0
- package/src/reporting/summary.ts +116 -0
- package/src/state/diff.ts +21 -0
- package/src/state/files.ts +52 -0
- package/src/state/load.ts +66 -0
- package/src/state/save.ts +31 -0
- package/src/xml2js.d.ts +23 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Stats } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
7
|
+
|
|
8
|
+
import type { NormalizedIOSProjectConfig } from '../config/types'
|
|
9
|
+
|
|
10
|
+
const PBX_NATIVE_TARGET_SECTION = 'PBXNativeTarget'
|
|
11
|
+
const XC_BUILD_CONFIGURATION_SECTION = 'XCBuildConfiguration'
|
|
12
|
+
const XC_CONFIGURATION_LIST_SECTION = 'XCConfigurationList'
|
|
13
|
+
const IOS_APPLICATION_PRODUCT_TYPE = 'com.apple.product-type.application'
|
|
14
|
+
|
|
15
|
+
export interface IOSProjectDiscovery {
|
|
16
|
+
iosRoot: string
|
|
17
|
+
xcodeprojPath: string
|
|
18
|
+
pbxprojPath: string
|
|
19
|
+
podfilePath: string
|
|
20
|
+
mainTargetName: string
|
|
21
|
+
mainTargetCandidates: string[]
|
|
22
|
+
infoPlistPath: string
|
|
23
|
+
entitlementsPath?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ParsedPbxNativeTarget {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
productType: string
|
|
30
|
+
buildConfigurationListId: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ParsedXCConfigurationList {
|
|
34
|
+
id: string
|
|
35
|
+
buildConfigurationIds: string[]
|
|
36
|
+
defaultConfigurationName?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ParsedXCBuildConfiguration {
|
|
40
|
+
id: string
|
|
41
|
+
name: string
|
|
42
|
+
buildSettings: {
|
|
43
|
+
codeSignEntitlements?: string
|
|
44
|
+
infoPlistFile?: string
|
|
45
|
+
productName?: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ParsedIOSProject {
|
|
50
|
+
targets: ParsedPbxNativeTarget[]
|
|
51
|
+
configurationLists: Map<string, ParsedXCConfigurationList>
|
|
52
|
+
buildConfigurations: Map<string, ParsedXCBuildConfiguration>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class IOSProjectDiscoveryError extends VoltraCliError {
|
|
56
|
+
constructor(message: string) {
|
|
57
|
+
super(message, 'VOLTRA_IOS_DISCOVERY_FAILED')
|
|
58
|
+
this.name = 'IOSProjectDiscoveryError'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function discoverIOSProject(
|
|
63
|
+
projectRoot: string,
|
|
64
|
+
config: NormalizedIOSProjectConfig
|
|
65
|
+
): Promise<IOSProjectDiscovery> {
|
|
66
|
+
const iosRoot = await resolveIOSRoot(projectRoot, config)
|
|
67
|
+
const xcodeprojPath = await resolveXcodeprojPath(iosRoot, config)
|
|
68
|
+
const pbxprojPath = await resolvePbxprojPath(xcodeprojPath)
|
|
69
|
+
const podfilePath = await resolvePodfilePath(iosRoot, config)
|
|
70
|
+
const pbxprojContent = await fs.readFile(pbxprojPath, 'utf8')
|
|
71
|
+
const parsedProject = parseIOSProject(pbxprojContent, pbxprojPath)
|
|
72
|
+
const mainTargetCandidates = getMainTargetCandidates(parsedProject.targets)
|
|
73
|
+
const mainTarget = resolveMainTarget(mainTargetCandidates, config.mainTargetName, pbxprojPath)
|
|
74
|
+
const mainTargetBuildConfigurations = getTargetBuildConfigurations(parsedProject, mainTarget, pbxprojPath)
|
|
75
|
+
const infoPlistPath = await resolveInfoPlistPath(iosRoot, config, mainTarget, mainTargetBuildConfigurations)
|
|
76
|
+
const entitlementsPath = await resolveEntitlementsPath(iosRoot, config, mainTarget, mainTargetBuildConfigurations)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
iosRoot,
|
|
80
|
+
xcodeprojPath,
|
|
81
|
+
pbxprojPath,
|
|
82
|
+
podfilePath,
|
|
83
|
+
mainTargetName: mainTarget.name,
|
|
84
|
+
mainTargetCandidates: mainTargetCandidates.map((target) => target.name).sort(),
|
|
85
|
+
infoPlistPath,
|
|
86
|
+
entitlementsPath,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resolveIOSRoot(projectRoot: string, config: NormalizedIOSProjectConfig): Promise<string> {
|
|
91
|
+
const iosRoot = config.rootDir ?? path.join(projectRoot, 'ios')
|
|
92
|
+
|
|
93
|
+
await ensureDirectory(
|
|
94
|
+
iosRoot,
|
|
95
|
+
config.rootDir
|
|
96
|
+
? `Configured iOS root directory does not exist: ${iosRoot}`
|
|
97
|
+
: `iOS root directory does not exist at ${iosRoot}. Set ios.project.rootDir to override the default ios/ layout.`
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return iosRoot
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function resolveXcodeprojPath(iosRoot: string, config: NormalizedIOSProjectConfig): Promise<string> {
|
|
104
|
+
if (config.xcodeprojPath) {
|
|
105
|
+
await ensureDirectory(config.xcodeprojPath, `Configured Xcode project does not exist: ${config.xcodeprojPath}`)
|
|
106
|
+
|
|
107
|
+
if (!config.xcodeprojPath.endsWith('.xcodeproj')) {
|
|
108
|
+
throw new IOSProjectDiscoveryError(
|
|
109
|
+
`Configured Xcode project must point to a .xcodeproj directory: ${config.xcodeprojPath}`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return config.xcodeprojPath
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const entries = await fs.readdir(iosRoot, { withFileTypes: true })
|
|
117
|
+
const xcodeprojCandidates = entries
|
|
118
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
119
|
+
.map((entry) => path.join(iosRoot, entry.name))
|
|
120
|
+
.sort()
|
|
121
|
+
|
|
122
|
+
if (xcodeprojCandidates.length === 1) {
|
|
123
|
+
return xcodeprojCandidates[0]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (xcodeprojCandidates.length === 0) {
|
|
127
|
+
throw new IOSProjectDiscoveryError(
|
|
128
|
+
`No .xcodeproj was found in ${iosRoot}. Set ios.project.xcodeprojPath to override discovery.`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new IOSProjectDiscoveryError(
|
|
133
|
+
`Multiple .xcodeproj directories were found in ${iosRoot}: ${xcodeprojCandidates.join(
|
|
134
|
+
', '
|
|
135
|
+
)}. Set ios.project.xcodeprojPath explicitly.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function resolvePbxprojPath(xcodeprojPath: string): Promise<string> {
|
|
140
|
+
const pbxprojPath = path.join(xcodeprojPath, 'project.pbxproj')
|
|
141
|
+
await ensureFile(pbxprojPath, `Xcode project file is missing at ${pbxprojPath}`)
|
|
142
|
+
return pbxprojPath
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function resolvePodfilePath(iosRoot: string, config: NormalizedIOSProjectConfig): Promise<string> {
|
|
146
|
+
const podfilePath = config.podfilePath ?? path.join(iosRoot, 'Podfile')
|
|
147
|
+
|
|
148
|
+
await ensureFile(
|
|
149
|
+
podfilePath,
|
|
150
|
+
config.podfilePath
|
|
151
|
+
? `Configured Podfile does not exist: ${podfilePath}`
|
|
152
|
+
: `Podfile does not exist at ${podfilePath}. Set ios.project.podfilePath to override the default ios/Podfile path.`
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return podfilePath
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseIOSProject(content: string, pbxprojPath: string): ParsedIOSProject {
|
|
159
|
+
const nativeTargetSection = getPbxprojSection(content, PBX_NATIVE_TARGET_SECTION, pbxprojPath)
|
|
160
|
+
const configurationListSection = getPbxprojSection(content, XC_CONFIGURATION_LIST_SECTION, pbxprojPath)
|
|
161
|
+
const buildConfigurationSection = getPbxprojSection(content, XC_BUILD_CONFIGURATION_SECTION, pbxprojPath)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
targets: parseNativeTargets(nativeTargetSection),
|
|
165
|
+
configurationLists: parseConfigurationLists(configurationListSection),
|
|
166
|
+
buildConfigurations: parseBuildConfigurations(buildConfigurationSection),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getPbxprojSection(content: string, sectionName: string, pbxprojPath: string): string {
|
|
171
|
+
const pattern = new RegExp(`/[*] Begin ${sectionName} section [*]/([\\s\\S]*?)/[*] End ${sectionName} section [*]/`)
|
|
172
|
+
const match = content.match(pattern)
|
|
173
|
+
|
|
174
|
+
if (!match) {
|
|
175
|
+
throw new IOSProjectDiscoveryError(`Could not parse ${sectionName} section from ${pbxprojPath}`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return match[1]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseNativeTargets(section: string): ParsedPbxNativeTarget[] {
|
|
182
|
+
const targets: ParsedPbxNativeTarget[] = []
|
|
183
|
+
const entryPattern = /([A-Za-z0-9]+) \/\*[^*]+\*\/ = \{([\s\S]*?)\n\t\t\};/g
|
|
184
|
+
|
|
185
|
+
for (const match of section.matchAll(entryPattern)) {
|
|
186
|
+
const [, id, body] = match
|
|
187
|
+
const name = matchPbxprojAssignment(body, 'name')
|
|
188
|
+
const productType = matchPbxprojAssignment(body, 'productType')
|
|
189
|
+
const buildConfigurationListId = matchPbxprojReference(body, 'buildConfigurationList')
|
|
190
|
+
|
|
191
|
+
if (!name || !productType || !buildConfigurationListId) {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
targets.push({
|
|
196
|
+
id,
|
|
197
|
+
name: stripPbxprojValue(name),
|
|
198
|
+
productType: stripPbxprojValue(productType),
|
|
199
|
+
buildConfigurationListId,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return targets
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseConfigurationLists(section: string): Map<string, ParsedXCConfigurationList> {
|
|
207
|
+
const configurationLists = new Map<string, ParsedXCConfigurationList>()
|
|
208
|
+
const entryPattern = /([A-Za-z0-9]+) \/\*[^*]+\*\/ = \{([\s\S]*?)\n\t\t\};/g
|
|
209
|
+
|
|
210
|
+
for (const match of section.matchAll(entryPattern)) {
|
|
211
|
+
const [, id, body] = match
|
|
212
|
+
const buildConfigurationIds = matchBuildConfigurationIds(body)
|
|
213
|
+
|
|
214
|
+
if (buildConfigurationIds.length === 0) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
configurationLists.set(id, {
|
|
219
|
+
id,
|
|
220
|
+
buildConfigurationIds,
|
|
221
|
+
defaultConfigurationName:
|
|
222
|
+
stripPbxprojValue(matchPbxprojAssignment(body, 'defaultConfigurationName') ?? '') || undefined,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return configurationLists
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseBuildConfigurations(section: string): Map<string, ParsedXCBuildConfiguration> {
|
|
230
|
+
const buildConfigurations = new Map<string, ParsedXCBuildConfiguration>()
|
|
231
|
+
const entryPattern = /([A-Za-z0-9]+) \/\*[^*]+\*\/ = \{([\s\S]*?)\n\t\t\};/g
|
|
232
|
+
|
|
233
|
+
for (const match of section.matchAll(entryPattern)) {
|
|
234
|
+
const [, id, body] = match
|
|
235
|
+
const name = matchPbxprojAssignment(body, 'name')
|
|
236
|
+
const buildSettingsBlock = matchBuildSettingsBlock(body)
|
|
237
|
+
|
|
238
|
+
if (!name || !buildSettingsBlock) {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
buildConfigurations.set(id, {
|
|
243
|
+
id,
|
|
244
|
+
name: stripPbxprojValue(name),
|
|
245
|
+
buildSettings: {
|
|
246
|
+
codeSignEntitlements: matchBuildSetting(buildSettingsBlock, 'CODE_SIGN_ENTITLEMENTS'),
|
|
247
|
+
infoPlistFile: matchBuildSetting(buildSettingsBlock, 'INFOPLIST_FILE'),
|
|
248
|
+
productName: matchBuildSetting(buildSettingsBlock, 'PRODUCT_NAME'),
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return buildConfigurations
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getMainTargetCandidates(targets: ParsedPbxNativeTarget[]): ParsedPbxNativeTarget[] {
|
|
257
|
+
return targets.filter((target) => target.productType === IOS_APPLICATION_PRODUCT_TYPE)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resolveMainTarget(
|
|
261
|
+
targets: ParsedPbxNativeTarget[],
|
|
262
|
+
configuredMainTargetName: string | undefined,
|
|
263
|
+
pbxprojPath: string
|
|
264
|
+
): ParsedPbxNativeTarget {
|
|
265
|
+
if (targets.length === 0) {
|
|
266
|
+
throw new IOSProjectDiscoveryError(`No iOS application targets were found in ${pbxprojPath}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (configuredMainTargetName) {
|
|
270
|
+
const configuredTarget = targets.find((target) => target.name === configuredMainTargetName)
|
|
271
|
+
|
|
272
|
+
if (configuredTarget) {
|
|
273
|
+
return configuredTarget
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new IOSProjectDiscoveryError(
|
|
277
|
+
`Configured iOS main target '${configuredMainTargetName}' was not found. Available application targets: ${targets
|
|
278
|
+
.map((target) => target.name)
|
|
279
|
+
.sort()
|
|
280
|
+
.join(', ')}`
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (targets.length === 1) {
|
|
285
|
+
return targets[0]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new IOSProjectDiscoveryError(
|
|
289
|
+
`Multiple iOS application targets were found: ${targets
|
|
290
|
+
.map((target) => target.name)
|
|
291
|
+
.sort()
|
|
292
|
+
.join(', ')}. Set ios.project.mainTargetName explicitly.`
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getTargetBuildConfigurations(
|
|
297
|
+
project: ParsedIOSProject,
|
|
298
|
+
target: ParsedPbxNativeTarget,
|
|
299
|
+
pbxprojPath: string
|
|
300
|
+
): ParsedXCBuildConfiguration[] {
|
|
301
|
+
const configurationList = project.configurationLists.get(target.buildConfigurationListId)
|
|
302
|
+
|
|
303
|
+
if (!configurationList) {
|
|
304
|
+
throw new IOSProjectDiscoveryError(
|
|
305
|
+
`Build configuration list ${target.buildConfigurationListId} for target '${target.name}' was not found in ${pbxprojPath}`
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const missingConfigurationIds = configurationList.buildConfigurationIds.filter(
|
|
310
|
+
(configurationId) => !project.buildConfigurations.has(configurationId)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if (missingConfigurationIds.length > 0) {
|
|
314
|
+
throw new IOSProjectDiscoveryError(
|
|
315
|
+
`Build configurations ${missingConfigurationIds.join(', ')} for target '${
|
|
316
|
+
target.name
|
|
317
|
+
}' were not found in ${pbxprojPath}`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const buildConfigurations = configurationList.buildConfigurationIds
|
|
322
|
+
.map((configurationId) => project.buildConfigurations.get(configurationId))
|
|
323
|
+
.filter((configuration): configuration is ParsedXCBuildConfiguration => configuration !== undefined)
|
|
324
|
+
|
|
325
|
+
if (buildConfigurations.length === 0) {
|
|
326
|
+
throw new IOSProjectDiscoveryError(
|
|
327
|
+
`No build configurations were found for target '${target.name}' in ${pbxprojPath}`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const defaultConfigurationName = configurationList.defaultConfigurationName
|
|
332
|
+
|
|
333
|
+
if (!defaultConfigurationName) {
|
|
334
|
+
return buildConfigurations
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const defaultConfiguration = buildConfigurations.find(
|
|
338
|
+
(configuration) => configuration.name === defaultConfigurationName
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return defaultConfiguration
|
|
342
|
+
? [defaultConfiguration, ...buildConfigurations.filter((configuration) => configuration !== defaultConfiguration)]
|
|
343
|
+
: buildConfigurations
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveInfoPlistPath(
|
|
347
|
+
iosRoot: string,
|
|
348
|
+
config: NormalizedIOSProjectConfig,
|
|
349
|
+
target: ParsedPbxNativeTarget,
|
|
350
|
+
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
351
|
+
): Promise<string> {
|
|
352
|
+
const infoPlistPath =
|
|
353
|
+
config.infoPlistPath ??
|
|
354
|
+
resolveConsistentBuildSettingPath(
|
|
355
|
+
iosRoot,
|
|
356
|
+
target,
|
|
357
|
+
buildConfigurations,
|
|
358
|
+
'INFOPLIST_FILE',
|
|
359
|
+
(configuration) => configuration.buildSettings.infoPlistFile
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if (!infoPlistPath) {
|
|
363
|
+
throw new IOSProjectDiscoveryError(
|
|
364
|
+
`Could not determine Info.plist for target '${target.name}'. Set ios.project.infoPlistPath explicitly.`
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await ensureFile(
|
|
369
|
+
infoPlistPath,
|
|
370
|
+
config.infoPlistPath
|
|
371
|
+
? `Configured Info.plist does not exist: ${infoPlistPath}`
|
|
372
|
+
: `Discovered Info.plist does not exist: ${infoPlistPath}`
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return infoPlistPath
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function resolveEntitlementsPath(
|
|
379
|
+
iosRoot: string,
|
|
380
|
+
config: NormalizedIOSProjectConfig,
|
|
381
|
+
target: ParsedPbxNativeTarget,
|
|
382
|
+
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
383
|
+
): Promise<string | undefined> {
|
|
384
|
+
const entitlementsPath =
|
|
385
|
+
config.entitlementsPath ??
|
|
386
|
+
resolveConsistentBuildSettingPath(
|
|
387
|
+
iosRoot,
|
|
388
|
+
target,
|
|
389
|
+
buildConfigurations,
|
|
390
|
+
'CODE_SIGN_ENTITLEMENTS',
|
|
391
|
+
(configuration) => configuration.buildSettings.codeSignEntitlements
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if (!entitlementsPath) {
|
|
395
|
+
return undefined
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await ensureFile(
|
|
399
|
+
entitlementsPath,
|
|
400
|
+
config.entitlementsPath
|
|
401
|
+
? `Configured entitlements file does not exist: ${entitlementsPath}`
|
|
402
|
+
: `Discovered entitlements file does not exist: ${entitlementsPath}`
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return entitlementsPath
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function resolveConsistentBuildSettingPath(
|
|
409
|
+
iosRoot: string,
|
|
410
|
+
target: ParsedPbxNativeTarget,
|
|
411
|
+
buildConfigurations: ParsedXCBuildConfiguration[],
|
|
412
|
+
settingName: string,
|
|
413
|
+
getSettingValue: (configuration: ParsedXCBuildConfiguration) => string | undefined
|
|
414
|
+
): string | undefined {
|
|
415
|
+
const resolvedPaths = new Map<string, string[]>()
|
|
416
|
+
|
|
417
|
+
for (const configuration of buildConfigurations) {
|
|
418
|
+
const settingValue = getSettingValue(configuration)
|
|
419
|
+
|
|
420
|
+
if (!settingValue) {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const resolvedPath = resolveXcodePathValue(settingValue, iosRoot, target, configuration)
|
|
425
|
+
const configurationNames = resolvedPaths.get(resolvedPath) ?? []
|
|
426
|
+
configurationNames.push(configuration.name)
|
|
427
|
+
resolvedPaths.set(resolvedPath, configurationNames)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (resolvedPaths.size === 0) {
|
|
431
|
+
return undefined
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (resolvedPaths.size > 1) {
|
|
435
|
+
throw new IOSProjectDiscoveryError(
|
|
436
|
+
`Target '${target.name}' resolves ${settingName} to multiple paths: ${[...resolvedPaths.entries()]
|
|
437
|
+
.map(([resolvedPath, configurationNames]) => `${resolvedPath} (${configurationNames.join(', ')})`)
|
|
438
|
+
.join('; ')}. Set ios.project.${
|
|
439
|
+
settingName === 'INFOPLIST_FILE' ? 'infoPlistPath' : 'entitlementsPath'
|
|
440
|
+
} explicitly.`
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return [...resolvedPaths.keys()][0]
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveXcodePathValue(
|
|
448
|
+
value: string,
|
|
449
|
+
iosRoot: string,
|
|
450
|
+
target: ParsedPbxNativeTarget,
|
|
451
|
+
configuration: ParsedXCBuildConfiguration
|
|
452
|
+
): string {
|
|
453
|
+
const substitutions = new Map<string, string>([
|
|
454
|
+
['PROJECT_DIR', iosRoot],
|
|
455
|
+
['SRCROOT', iosRoot],
|
|
456
|
+
['TARGET_NAME', target.name],
|
|
457
|
+
])
|
|
458
|
+
|
|
459
|
+
const productName = configuration.buildSettings.productName
|
|
460
|
+
|
|
461
|
+
if (productName) {
|
|
462
|
+
substitutions.set('PRODUCT_NAME', substituteXcodeVariables(productName, substitutions))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const substitutedValue = substituteXcodeVariables(value, substitutions)
|
|
466
|
+
|
|
467
|
+
if (/\$\(|\$\{/.test(substitutedValue)) {
|
|
468
|
+
throw new IOSProjectDiscoveryError(
|
|
469
|
+
`Could not resolve Xcode build setting path '${value}' for target '${target.name}'. Set an explicit ios.project override.`
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return path.normalize(path.isAbsolute(substitutedValue) ? substitutedValue : path.join(iosRoot, substitutedValue))
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function substituteXcodeVariables(value: string, substitutions: Map<string, string>): string {
|
|
477
|
+
let nextValue = stripPbxprojValue(value)
|
|
478
|
+
|
|
479
|
+
for (let iteration = 0; iteration < 5; iteration += 1) {
|
|
480
|
+
let didReplace = false
|
|
481
|
+
|
|
482
|
+
nextValue = nextValue.replace(/\$\(([^)]+)\)|\$\{([^}]+)\}/g, (match, groupedName, bracedName) => {
|
|
483
|
+
const variableName = groupedName ?? bracedName
|
|
484
|
+
const substitution = substitutions.get(variableName)
|
|
485
|
+
|
|
486
|
+
if (substitution === undefined) {
|
|
487
|
+
return match
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
didReplace = true
|
|
491
|
+
return substitution
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
if (!didReplace) {
|
|
495
|
+
break
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return stripPbxprojValue(nextValue)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function matchBuildConfigurationIds(body: string): string[] {
|
|
503
|
+
const match = body.match(/buildConfigurations = \(([\s\S]*?)\n\t\t\t\);/)
|
|
504
|
+
|
|
505
|
+
if (!match) {
|
|
506
|
+
return []
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return [...match[1].matchAll(/([A-Za-z0-9]+) \/\*[^*]+\*\//g)].map((configurationMatch) => configurationMatch[1])
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function matchBuildSettingsBlock(body: string): string | undefined {
|
|
513
|
+
return body.match(/buildSettings = \{([\s\S]*?)\n\t\t\t\};/)?.[1]
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function matchBuildSetting(buildSettingsBlock: string, settingName: string): string | undefined {
|
|
517
|
+
return (
|
|
518
|
+
stripPbxprojValue(buildSettingsBlock.match(new RegExp(`\\b${settingName}\\s*=\\s*([^;]+);`))?.[1] ?? '') ||
|
|
519
|
+
undefined
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function matchPbxprojReference(body: string, fieldName: string): string | undefined {
|
|
524
|
+
return body.match(new RegExp(`\\b${fieldName}\\s*=\\s*([A-Za-z0-9]+)`))?.[1]
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function matchPbxprojAssignment(body: string, fieldName: string): string | undefined {
|
|
528
|
+
return body.match(new RegExp(`\\b${fieldName}\\s*=\\s*([^;]+);`))?.[1]
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function stripPbxprojValue(value: string): string {
|
|
532
|
+
const trimmedValue = value.trim()
|
|
533
|
+
|
|
534
|
+
if (trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) {
|
|
535
|
+
return trimmedValue.slice(1, -1)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return trimmedValue
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function ensureDirectory(dirPath: string, message: string): Promise<void> {
|
|
542
|
+
const stat = await readPathStat(dirPath)
|
|
543
|
+
|
|
544
|
+
if (!stat) {
|
|
545
|
+
throw new IOSProjectDiscoveryError(message)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!stat.isDirectory()) {
|
|
549
|
+
throw new IOSProjectDiscoveryError(`Expected a directory but found a file: ${dirPath}`)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function ensureFile(filePath: string, message: string): Promise<void> {
|
|
554
|
+
const stat = await readPathStat(filePath)
|
|
555
|
+
|
|
556
|
+
if (!stat) {
|
|
557
|
+
throw new IOSProjectDiscoveryError(message)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!stat.isFile()) {
|
|
561
|
+
throw new IOSProjectDiscoveryError(`Expected a file but found a directory: ${filePath}`)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function readPathStat(targetPath: string): Promise<Stats | undefined> {
|
|
566
|
+
try {
|
|
567
|
+
return await fs.stat(targetPath)
|
|
568
|
+
} catch (error: unknown) {
|
|
569
|
+
if (isNotFoundError(error)) {
|
|
570
|
+
return undefined
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
throw error
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isNotFoundError(error: unknown): error is NodeJS.ErrnoException {
|
|
578
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
579
|
+
}
|
package/src/fs/path.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
export function normalizeRelativePath(filePath: string): string {
|
|
4
|
+
return filePath.replace(/\\/g, '/')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveFromRoot(rootDir: string, filePath: string): string {
|
|
8
|
+
return path.resolve(rootDir, filePath)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveOptionalFromRoot(rootDir: string, filePath: string | undefined): string | undefined {
|
|
12
|
+
if (!filePath) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return resolveFromRoot(rootDir, filePath)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toRelativePath(rootDir: string, targetPath: string): string {
|
|
20
|
+
return normalizeRelativePath(path.relative(rootDir, targetPath))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function dirname(filePath: string): string {
|
|
24
|
+
return path.dirname(filePath)
|
|
25
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
const UTF8 = 'utf8'
|
|
6
|
+
|
|
7
|
+
export async function pathExists(filePath: string): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(filePath)
|
|
10
|
+
return true
|
|
11
|
+
} catch (error: unknown) {
|
|
12
|
+
if (isNotFoundError(error)) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function ensureDirectory(dirPath: string): Promise<void> {
|
|
21
|
+
await fs.mkdir(dirPath, { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readTextFile(filePath: string): Promise<string> {
|
|
25
|
+
return fs.readFile(filePath, UTF8)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readJsonFile<T>(filePath: string): Promise<T> {
|
|
29
|
+
const content = await readTextFile(filePath)
|
|
30
|
+
return JSON.parse(content) as T
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function writeTextFile(filePath: string, content: string): Promise<void> {
|
|
34
|
+
await ensureDirectory(path.dirname(filePath))
|
|
35
|
+
await writeTextFileAtomic(filePath, content)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
39
|
+
await writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function removeFileIfExists(filePath: string): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(filePath)
|
|
45
|
+
return true
|
|
46
|
+
} catch (error: unknown) {
|
|
47
|
+
if (isNotFoundError(error)) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function removeDirectoryIfExists(dirPath: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
await fs.rm(dirPath, { recursive: true, force: false })
|
|
58
|
+
return true
|
|
59
|
+
} catch (error: unknown) {
|
|
60
|
+
if (isNotFoundError(error)) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function removePathIfExists(targetPath: string): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
const stat = await fs.lstat(targetPath)
|
|
71
|
+
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
await fs.rm(targetPath, { recursive: true, force: false })
|
|
74
|
+
} else {
|
|
75
|
+
await fs.unlink(targetPath)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
if (isNotFoundError(error)) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function writeTextFileAtomic(filePath: string, content: string): Promise<void> {
|
|
89
|
+
const tempPath = `${filePath}.${crypto.randomUUID()}.tmp`
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await fs.writeFile(tempPath, content, UTF8)
|
|
93
|
+
await fs.rename(tempPath, filePath)
|
|
94
|
+
} catch (error: unknown) {
|
|
95
|
+
await removeFileIfExists(tempPath).catch(() => undefined)
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isNotFoundError(error: unknown): error is NodeJS.ErrnoException {
|
|
101
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
102
|
+
}
|