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,11 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
4
|
+
|
|
5
|
+
export function resolveMainAppEntitlementsPath(discovery: IOSProjectDiscovery): string {
|
|
6
|
+
if (discovery.entitlementsPath) {
|
|
7
|
+
return discovery.entitlementsPath
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return path.join(path.dirname(discovery.infoPlistPath), `${discovery.mainTargetName}.entitlements`)
|
|
11
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { parseStringPromise } from 'xml2js'
|
|
2
|
+
|
|
3
|
+
import { readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
4
|
+
import { toRelativePath } from '../../fs/path'
|
|
5
|
+
import { VoltraCliError } from '../../reporting/summary'
|
|
6
|
+
|
|
7
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
8
|
+
import type { NormalizedVoltraIOSConfig } from '../../config/types'
|
|
9
|
+
import type { ReportedChange } from '../../reporting/summary'
|
|
10
|
+
|
|
11
|
+
interface OrderedPlistNode {
|
|
12
|
+
'#name'?: string
|
|
13
|
+
_?: string
|
|
14
|
+
$$?: OrderedPlistNode[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TaggedPlistScalar {
|
|
18
|
+
__voltraPlistScalarType: 'data' | 'date'
|
|
19
|
+
value: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type PlistErrorFactory = (message: string) => Error
|
|
23
|
+
|
|
24
|
+
export interface EnsureInfoPlistOptions {
|
|
25
|
+
projectRoot: string
|
|
26
|
+
ios: NormalizedVoltraIOSConfig
|
|
27
|
+
discovery: IOSProjectDiscovery
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EnsureInfoPlistResult {
|
|
31
|
+
change?: ReportedChange
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class IOSInfoPlistMutationError extends VoltraCliError {
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(message, 'VOLTRA_IOS_INFO_PLIST_FAILED')
|
|
37
|
+
this.name = 'IOSInfoPlistMutationError'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<EnsureInfoPlistResult> {
|
|
42
|
+
const { projectRoot, ios, discovery } = options
|
|
43
|
+
const infoPlist = await parsePlistFile(discovery.infoPlistPath, 'main app Info.plist', createInfoPlistError)
|
|
44
|
+
|
|
45
|
+
infoPlist.NSSupportsLiveActivities = true
|
|
46
|
+
infoPlist.NSSupportsLiveActivitiesFrequentUpdates = false
|
|
47
|
+
|
|
48
|
+
setOrDeleteVoltraKey(infoPlist, 'Voltra_AppGroupIdentifier', ios.groupIdentifier)
|
|
49
|
+
setOrDeleteVoltraKey(infoPlist, 'Voltra_KeychainGroup', ios.keychainGroup)
|
|
50
|
+
setOrDeleteVoltraKey(infoPlist, 'Voltra_EnablePushNotifications', ios.enablePushNotifications ? true : undefined)
|
|
51
|
+
|
|
52
|
+
const widgetIds = ios.widgets.map((widget) => widget.id)
|
|
53
|
+
setOrDeleteVoltraKey(infoPlist, 'Voltra_WidgetIds', widgetIds.length > 0 ? widgetIds : undefined)
|
|
54
|
+
|
|
55
|
+
const serverWidgets = ios.widgets.filter((widget) => widget.serverUpdate)
|
|
56
|
+
const serverUrls = Object.fromEntries(serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.url]))
|
|
57
|
+
const serverIntervals = Object.fromEntries(
|
|
58
|
+
serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.intervalMinutes])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
setOrDeleteVoltraKey(
|
|
62
|
+
infoPlist,
|
|
63
|
+
'Voltra_WidgetServerUrls',
|
|
64
|
+
Object.keys(serverUrls).length > 0 ? serverUrls : undefined
|
|
65
|
+
)
|
|
66
|
+
setOrDeleteVoltraKey(
|
|
67
|
+
infoPlist,
|
|
68
|
+
'Voltra_WidgetServerIntervals',
|
|
69
|
+
Object.keys(serverIntervals).length > 0 ? serverIntervals : undefined
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const nextContent = buildPlistXml(infoPlist, createInfoPlistError)
|
|
73
|
+
const change = await writePlistIfChanged(projectRoot, discovery.infoPlistPath, nextContent)
|
|
74
|
+
|
|
75
|
+
return { change }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function parsePlistFile(
|
|
79
|
+
filePath: string,
|
|
80
|
+
errorContext: string,
|
|
81
|
+
createError: PlistErrorFactory
|
|
82
|
+
): Promise<Record<string, unknown>> {
|
|
83
|
+
let content: string
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
content = await readTextFile(filePath)
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
throw createError(`Failed to read ${errorContext} at ${filePath}: ${getErrorMessage(error)}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let parsed: unknown
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
parsed = await parseStringPromise(content, {
|
|
95
|
+
explicitArray: false,
|
|
96
|
+
explicitChildren: true,
|
|
97
|
+
preserveChildrenOrder: true,
|
|
98
|
+
})
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
throw createError(`Failed to parse ${errorContext} at ${filePath}: ${getErrorMessage(error)}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const plistRoot = getPlistRoot(parsed, errorContext, filePath, createError)
|
|
104
|
+
return parsePlistDict(plistRoot, errorContext, filePath, createError)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildPlistXml(value: unknown, createError: PlistErrorFactory): string {
|
|
108
|
+
try {
|
|
109
|
+
return [
|
|
110
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
111
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
112
|
+
'<plist version="1.0">',
|
|
113
|
+
renderPlistValue(value, 0, createError),
|
|
114
|
+
'</plist>',
|
|
115
|
+
'',
|
|
116
|
+
].join('\n')
|
|
117
|
+
} catch (error: unknown) {
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw createError(`Failed to build plist XML: ${String(error)}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getPlistRoot(
|
|
127
|
+
value: unknown,
|
|
128
|
+
errorContext: string,
|
|
129
|
+
filePath: string,
|
|
130
|
+
createError: PlistErrorFactory
|
|
131
|
+
): OrderedPlistNode {
|
|
132
|
+
if (!value || typeof value !== 'object' || !('plist' in value)) {
|
|
133
|
+
throw createError(`Parsed ${errorContext} at ${filePath} is missing the plist root.`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const plistValue = (value as { plist?: { dict?: unknown } }).plist
|
|
137
|
+
|
|
138
|
+
if (!plistValue || typeof plistValue !== 'object' || !('dict' in plistValue)) {
|
|
139
|
+
throw createError(`Parsed ${errorContext} at ${filePath} is missing the root dict.`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!plistValue.dict || typeof plistValue.dict !== 'object') {
|
|
143
|
+
throw createError(`Parsed ${errorContext} at ${filePath} contains an invalid root dict.`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return plistValue.dict as OrderedPlistNode
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parsePlistDict(
|
|
150
|
+
node: OrderedPlistNode,
|
|
151
|
+
errorContext: string,
|
|
152
|
+
filePath: string,
|
|
153
|
+
createError: PlistErrorFactory
|
|
154
|
+
): Record<string, unknown> {
|
|
155
|
+
const children = getOrderedChildren(node)
|
|
156
|
+
const result: Record<string, unknown> = {}
|
|
157
|
+
|
|
158
|
+
for (let index = 0; index < children.length; index += 2) {
|
|
159
|
+
const keyNode = children[index]
|
|
160
|
+
const valueNode = children[index + 1]
|
|
161
|
+
|
|
162
|
+
if (keyNode?.['#name'] !== 'key' || typeof keyNode._ !== 'string' || keyNode._.trim().length === 0) {
|
|
163
|
+
throw createError(`Parsed ${errorContext} at ${filePath} contains an invalid dict key.`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!valueNode) {
|
|
167
|
+
throw createError(`Parsed ${errorContext} at ${filePath} is missing a value for key '${keyNode._}'.`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
result[keyNode._] = parsePlistValue(valueNode, errorContext, filePath, createError)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parsePlistArray(
|
|
177
|
+
node: OrderedPlistNode,
|
|
178
|
+
errorContext: string,
|
|
179
|
+
filePath: string,
|
|
180
|
+
createError: PlistErrorFactory
|
|
181
|
+
): unknown[] {
|
|
182
|
+
return getOrderedChildren(node).map((child) => parsePlistValue(child, errorContext, filePath, createError))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parsePlistValue(
|
|
186
|
+
node: OrderedPlistNode,
|
|
187
|
+
errorContext: string,
|
|
188
|
+
filePath: string,
|
|
189
|
+
createError: PlistErrorFactory
|
|
190
|
+
): unknown {
|
|
191
|
+
switch (node['#name']) {
|
|
192
|
+
case 'string':
|
|
193
|
+
return node._ ?? ''
|
|
194
|
+
case 'integer':
|
|
195
|
+
case 'real': {
|
|
196
|
+
const rawValue = node._ ?? ''
|
|
197
|
+
const numericValue = Number(rawValue)
|
|
198
|
+
|
|
199
|
+
if (!Number.isFinite(numericValue)) {
|
|
200
|
+
throw createError(
|
|
201
|
+
`Parsed ${errorContext} at ${filePath} contains an invalid ${node['#name']} value '${rawValue}'.`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return numericValue
|
|
206
|
+
}
|
|
207
|
+
case 'true':
|
|
208
|
+
return true
|
|
209
|
+
case 'false':
|
|
210
|
+
return false
|
|
211
|
+
case 'dict':
|
|
212
|
+
return parsePlistDict(node, errorContext, filePath, createError)
|
|
213
|
+
case 'array':
|
|
214
|
+
return parsePlistArray(node, errorContext, filePath, createError)
|
|
215
|
+
case 'data':
|
|
216
|
+
return { __voltraPlistScalarType: 'data', value: node._ ?? '' } satisfies TaggedPlistScalar
|
|
217
|
+
case 'date':
|
|
218
|
+
return { __voltraPlistScalarType: 'date', value: node._ ?? '' } satisfies TaggedPlistScalar
|
|
219
|
+
default:
|
|
220
|
+
throw createError(
|
|
221
|
+
`Parsed ${errorContext} at ${filePath} contains unsupported plist node '${node['#name'] ?? 'unknown'}'.`
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getOrderedChildren(node: OrderedPlistNode): OrderedPlistNode[] {
|
|
227
|
+
return Array.isArray(node.$$) ? node.$$ : []
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderPlistValue(value: unknown, indentLevel: number, createError: PlistErrorFactory): string {
|
|
231
|
+
const indent = ' '.repeat(indentLevel)
|
|
232
|
+
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
const items = value.map((item) => renderPlistValue(item, indentLevel + 1, createError)).join('\n')
|
|
235
|
+
return `${indent}<array>\n${items}\n${indent}</array>`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (isTaggedPlistScalar(value)) {
|
|
239
|
+
return `${indent}<${value.__voltraPlistScalarType}>${escapePlistText(value.value)}</${
|
|
240
|
+
value.__voltraPlistScalarType
|
|
241
|
+
}>`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (value && typeof value === 'object') {
|
|
245
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined)
|
|
246
|
+
const lines = entries.flatMap(([key, entryValue]) => [
|
|
247
|
+
`${indent} <key>${escapePlistText(key)}</key>`,
|
|
248
|
+
renderPlistValue(entryValue, indentLevel + 1, createError),
|
|
249
|
+
])
|
|
250
|
+
return `${indent}<dict>\n${lines.join('\n')}\n${indent}</dict>`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof value === 'string') {
|
|
254
|
+
return `${indent}<string>${escapePlistText(value)}</string>`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeof value === 'number') {
|
|
258
|
+
return Number.isInteger(value) ? `${indent}<integer>${value}</integer>` : `${indent}<real>${value}</real>`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof value === 'boolean') {
|
|
262
|
+
return `${indent}<${value ? 'true' : 'false'}/>`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (value === null || value === undefined) {
|
|
266
|
+
throw createError('Cannot encode null or undefined in plist output.')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
throw createError(`Unsupported plist value type: ${typeof value}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isTaggedPlistScalar(value: unknown): value is TaggedPlistScalar {
|
|
273
|
+
return (
|
|
274
|
+
!!value &&
|
|
275
|
+
typeof value === 'object' &&
|
|
276
|
+
'__voltraPlistScalarType' in value &&
|
|
277
|
+
((value as TaggedPlistScalar).__voltraPlistScalarType === 'data' ||
|
|
278
|
+
(value as TaggedPlistScalar).__voltraPlistScalarType === 'date') &&
|
|
279
|
+
typeof (value as TaggedPlistScalar).value === 'string'
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function setOrDeleteVoltraKey(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
284
|
+
if (value === undefined) {
|
|
285
|
+
delete target[key]
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
target[key] = value
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function writePlistIfChanged(
|
|
293
|
+
projectRoot: string,
|
|
294
|
+
plistPath: string,
|
|
295
|
+
nextContent: string
|
|
296
|
+
): Promise<ReportedChange | undefined> {
|
|
297
|
+
const previousContent = await readTextFile(plistPath)
|
|
298
|
+
|
|
299
|
+
if (previousContent === nextContent) {
|
|
300
|
+
return undefined
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await writeTextFile(plistPath, nextContent)
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
kind: 'updated',
|
|
307
|
+
path: toRelativePath(projectRoot, plistPath),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function escapePlistText(value: string): string {
|
|
312
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getErrorMessage(error: unknown): string {
|
|
316
|
+
return error instanceof Error ? error.message : String(error)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createInfoPlistError(message: string): IOSInfoPlistMutationError {
|
|
320
|
+
return new IOSInfoPlistMutationError(message)
|
|
321
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
import { readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
5
|
+
import { toRelativePath } from '../../fs/path'
|
|
6
|
+
import { VoltraCliError } from '../../reporting/summary'
|
|
7
|
+
import { resolveIOSWidgetTargetName } from './targetName'
|
|
8
|
+
import { VOLTRA_MIN_IOS_DEPLOYMENT_TARGET, isIOSDeploymentTargetAtLeast } from './deploymentTarget'
|
|
9
|
+
|
|
10
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
11
|
+
import type { NormalizedVoltraIOSConfig } from '../../config/types'
|
|
12
|
+
import type { ReportedChange } from '../../reporting/summary'
|
|
13
|
+
|
|
14
|
+
const VOLTRA_MANAGED_BLOCK_START = '# >>> VOLTRA MANAGED BLOCK: widget extension target'
|
|
15
|
+
const VOLTRA_MANAGED_BLOCK_END = '# <<< VOLTRA MANAGED BLOCK: widget extension target'
|
|
16
|
+
const LEGACY_BLOCK_HEADER = '# Voltra Widget Extension Target'
|
|
17
|
+
const LEGACY_BLOCK_NOTICE = '# DO NOT MODIFY THIS FILE - IT IS AUTO-GENERATED BY THE VOLTRA PLUGIN'
|
|
18
|
+
|
|
19
|
+
export interface EnsurePodfileBlockOptions {
|
|
20
|
+
projectRoot: string
|
|
21
|
+
ios: NormalizedVoltraIOSConfig
|
|
22
|
+
discovery: IOSProjectDiscovery
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EnsurePodfileBlockResult {
|
|
26
|
+
change?: ReportedChange
|
|
27
|
+
targetName: string
|
|
28
|
+
warnings?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class IOSPodfileMutationError extends VoltraCliError {
|
|
32
|
+
constructor(message: string) {
|
|
33
|
+
super(message, 'VOLTRA_IOS_PODFILE_FAILED')
|
|
34
|
+
this.name = 'IOSPodfileMutationError'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function ensurePodfileBlock(options: EnsurePodfileBlockOptions): Promise<EnsurePodfileBlockResult> {
|
|
39
|
+
const { projectRoot, ios, discovery } = options
|
|
40
|
+
const targetName = resolveIOSWidgetTargetName(ios, discovery)
|
|
41
|
+
const currentContent = await readPodfile(discovery.podfilePath)
|
|
42
|
+
const nextBlock = buildManagedPodfileBlock(targetName)
|
|
43
|
+
const platformResult = await ensurePodfileDeploymentTarget(currentContent, projectRoot)
|
|
44
|
+
const nextContent = reconcilePodfile(platformResult.content, nextBlock, targetName)
|
|
45
|
+
|
|
46
|
+
if (currentContent === nextContent) {
|
|
47
|
+
return { targetName, warnings: platformResult.warnings }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await writeTextFile(discovery.podfilePath, nextContent)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
change: {
|
|
54
|
+
kind: 'updated',
|
|
55
|
+
path: toRelativePath(projectRoot, discovery.podfilePath),
|
|
56
|
+
},
|
|
57
|
+
targetName,
|
|
58
|
+
warnings: platformResult.warnings,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface EnsurePodfileDeploymentTargetResult {
|
|
63
|
+
content: string
|
|
64
|
+
warnings?: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function ensurePodfileDeploymentTarget(
|
|
68
|
+
content: string,
|
|
69
|
+
projectRoot: string
|
|
70
|
+
): Promise<EnsurePodfileDeploymentTargetResult> {
|
|
71
|
+
const lines = content.split('\n')
|
|
72
|
+
const platformLineIndex = lines.findIndex((line) => /^\s*platform\s+:ios\s*,/.test(line))
|
|
73
|
+
|
|
74
|
+
if (platformLineIndex === -1) {
|
|
75
|
+
return {
|
|
76
|
+
content,
|
|
77
|
+
warnings: [
|
|
78
|
+
`Could not find an iOS platform declaration in the Podfile. Ensure it resolves to iOS ${VOLTRA_MIN_IOS_DEPLOYMENT_TARGET} or newer.`,
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const line = lines[platformLineIndex]
|
|
84
|
+
const match = line.match(/^(\s*platform\s+:ios\s*,\s*)(.*?)(\s*(?:#.*)?)$/)
|
|
85
|
+
|
|
86
|
+
if (!match) {
|
|
87
|
+
return {
|
|
88
|
+
content,
|
|
89
|
+
warnings: [getUnknownPodfilePlatformWarning()],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const [, prefix, rawValue, suffix] = match
|
|
94
|
+
const platformValue = rawValue.trim()
|
|
95
|
+
const nextValue = await resolveNextPodfilePlatformValue(platformValue, projectRoot)
|
|
96
|
+
|
|
97
|
+
if (nextValue === undefined) {
|
|
98
|
+
return {
|
|
99
|
+
content,
|
|
100
|
+
warnings: [getUnknownPodfilePlatformWarning()],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (nextValue === platformValue) {
|
|
105
|
+
return { content }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines[platformLineIndex] = `${prefix}${nextValue}${suffix}`
|
|
109
|
+
return { content: lines.join('\n') }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function resolveNextPodfilePlatformValue(
|
|
113
|
+
platformValue: string,
|
|
114
|
+
projectRoot: string
|
|
115
|
+
): Promise<string | undefined> {
|
|
116
|
+
const literalVersion = parseRubyStringLiteral(platformValue)
|
|
117
|
+
|
|
118
|
+
if (literalVersion) {
|
|
119
|
+
return isIOSDeploymentTargetAtLeast(literalVersion, VOLTRA_MIN_IOS_DEPLOYMENT_TARGET)
|
|
120
|
+
? platformValue
|
|
121
|
+
: `'${VOLTRA_MIN_IOS_DEPLOYMENT_TARGET}'`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (platformValue === 'min_ios_version_supported') {
|
|
125
|
+
const reactNativeMinVersion = await resolveReactNativeMinIOSVersion(projectRoot)
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
reactNativeMinVersion &&
|
|
129
|
+
isIOSDeploymentTargetAtLeast(reactNativeMinVersion, VOLTRA_MIN_IOS_DEPLOYMENT_TARGET)
|
|
130
|
+
) {
|
|
131
|
+
return platformValue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `'${VOLTRA_MIN_IOS_DEPLOYMENT_TARGET}'`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseRubyStringLiteral(value: string): string | undefined {
|
|
141
|
+
const match = value.match(/^(['"])(\d+(?:\.\d+){0,2})\1$/)
|
|
142
|
+
return match?.[2]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function resolveReactNativeMinIOSVersion(projectRoot: string): Promise<string | undefined> {
|
|
146
|
+
try {
|
|
147
|
+
const requireFromProject = createRequire(path.join(projectRoot, 'package.json'))
|
|
148
|
+
const reactNativePackagePath = requireFromProject.resolve('react-native/package.json')
|
|
149
|
+
const helpersPath = path.join(path.dirname(reactNativePackagePath), 'scripts', 'cocoapods', 'helpers.rb')
|
|
150
|
+
const helpersContent = await readTextFile(helpersPath)
|
|
151
|
+
return helpersContent.match(/def self\.min_ios_version_supported[\s\S]*?return ['"]([^'"]+)['"]/)?.[1]
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getUnknownPodfilePlatformWarning(): string {
|
|
158
|
+
return `Could not verify the Podfile iOS platform declaration. Ensure it resolves to iOS ${VOLTRA_MIN_IOS_DEPLOYMENT_TARGET} or newer.`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildManagedPodfileBlock(targetName: string): string {
|
|
162
|
+
const escapedTargetName = escapeRubySingleQuotedString(targetName)
|
|
163
|
+
|
|
164
|
+
return [
|
|
165
|
+
VOLTRA_MANAGED_BLOCK_START,
|
|
166
|
+
'# Voltra widget extension target managed by voltra apply.',
|
|
167
|
+
`target '${escapedTargetName}' do`,
|
|
168
|
+
" use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if defined?(podfile_properties) && podfile_properties['ios.useFrameworks']",
|
|
169
|
+
" use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']",
|
|
170
|
+
'',
|
|
171
|
+
" require 'pathname'",
|
|
172
|
+
' project_root = "#{Pod::Config.instance.installation_root}/.."',
|
|
173
|
+
" abort('[voltra] Error: @use-voltra/ios-client must be installed in the app project before running pod install.') unless system(\"node --print \\\"require.resolve('@use-voltra/ios-client/package.json', {paths: ['#{project_root}']})\\\" >/dev/null 2>&1\")",
|
|
174
|
+
" voltra_client_root = `node --print \"require.resolve('@use-voltra/ios-client/package.json', {paths: ['#{project_root}']})\" 2>/dev/null`.strip.gsub('/package.json', '')",
|
|
175
|
+
" voltra_widget_podspec_root = File.join(voltra_client_root, 'ios')",
|
|
176
|
+
' podspec_dir_path = Pathname.new(voltra_widget_podspec_root).relative_path_from(Pathname.new(__dir__)).to_path',
|
|
177
|
+
'',
|
|
178
|
+
" pod 'VoltraWidget', :path => podspec_dir_path",
|
|
179
|
+
'end',
|
|
180
|
+
VOLTRA_MANAGED_BLOCK_END,
|
|
181
|
+
].join('\n')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function reconcilePodfile(currentContent: string, nextBlock: string, targetName: string): string {
|
|
185
|
+
const normalizedContent = stripTrailingWhitespace(currentContent)
|
|
186
|
+
const managedBlockPattern = createManagedBlockPattern()
|
|
187
|
+
const legacyBlockPattern = createLegacyBlockPattern()
|
|
188
|
+
|
|
189
|
+
if (managedBlockPattern.test(normalizedContent)) {
|
|
190
|
+
return ensureTrailingNewline(normalizedContent.replace(managedBlockPattern, nextBlock))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (legacyBlockPattern.test(normalizedContent)) {
|
|
194
|
+
return ensureTrailingNewline(normalizedContent.replace(legacyBlockPattern, nextBlock))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (hasUnmanagedTargetBlock(normalizedContent, targetName)) {
|
|
198
|
+
throw new IOSPodfileMutationError(
|
|
199
|
+
`Podfile already contains an unmanaged target '${targetName}'. Remove it or choose a different ios.targetName before running voltra apply.`
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const separator = normalizedContent.length === 0 ? '' : '\n\n'
|
|
204
|
+
return ensureTrailingNewline(`${normalizedContent}${separator}${nextBlock}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createManagedBlockPattern(): RegExp {
|
|
208
|
+
return new RegExp(
|
|
209
|
+
`${escapeRegExp(VOLTRA_MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(VOLTRA_MANAGED_BLOCK_END)}`,
|
|
210
|
+
'm'
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createLegacyBlockPattern(): RegExp {
|
|
215
|
+
return new RegExp(
|
|
216
|
+
`${escapeRegExp(LEGACY_BLOCK_HEADER)}\\n${escapeRegExp(LEGACY_BLOCK_NOTICE)}\\ntarget '[^']+' do[\\s\\S]*?\\nend`,
|
|
217
|
+
'm'
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function readPodfile(podfilePath: string): Promise<string> {
|
|
222
|
+
try {
|
|
223
|
+
return await readTextFile(podfilePath)
|
|
224
|
+
} catch (error: unknown) {
|
|
225
|
+
throw new IOSPodfileMutationError(`Failed to read Podfile at ${podfilePath}: ${getErrorMessage(error)}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stripTrailingWhitespace(value: string): string {
|
|
230
|
+
return value.replace(/\s+$/, '')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ensureTrailingNewline(value: string): string {
|
|
234
|
+
return `${value}\n`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function escapeRubySingleQuotedString(value: string): string {
|
|
238
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function escapeRegExp(value: string): string {
|
|
242
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hasUnmanagedTargetBlock(content: string, targetName: string): boolean {
|
|
246
|
+
const targetPattern = new RegExp(
|
|
247
|
+
`(^|\\n)target '${escapeRegExp(escapeRubySingleQuotedString(targetName))}' do(\\n|$)`,
|
|
248
|
+
'm'
|
|
249
|
+
)
|
|
250
|
+
return targetPattern.test(content)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getErrorMessage(error: unknown): string {
|
|
254
|
+
return error instanceof Error ? error.message : String(error)
|
|
255
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
2
|
+
import type { NormalizedVoltraIOSConfig } from '../../config/types'
|
|
3
|
+
|
|
4
|
+
export function resolveIOSWidgetTargetName(ios: NormalizedVoltraIOSConfig, discovery: IOSProjectDiscovery): string {
|
|
5
|
+
if (ios.targetName) {
|
|
6
|
+
return ios.targetName
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const sanitizedTargetName = discovery.mainTargetName.replace(/[^A-Za-z0-9_]/g, '')
|
|
10
|
+
return `${sanitizedTargetName}LiveActivity`
|
|
11
|
+
}
|