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,1233 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import fsPromises from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
|
|
6
|
+
import { requirePlatformPackage } from '../../dependencies/platformPackages'
|
|
7
|
+
import { ensureDirectory, pathExists, readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
8
|
+
import { normalizeRelativePath, toRelativePath } from '../../fs/path'
|
|
9
|
+
import { VoltraCliError } from '../../reporting/summary'
|
|
10
|
+
import { DYNAMIC_WIDGET_BUILD_INFO, evaluateWidgetModuleExports } from '../shared/widgetModule'
|
|
11
|
+
import { buildPlistXml, parsePlistFile } from './plist'
|
|
12
|
+
import { resolveIOSWidgetTargetName } from './targetName'
|
|
13
|
+
|
|
14
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
15
|
+
import type {
|
|
16
|
+
IOSWidgetAppIntentParameter,
|
|
17
|
+
IOSWidgetFamily,
|
|
18
|
+
NormalizedIOSWidgetConfig,
|
|
19
|
+
NormalizedVoltraIOSConfig,
|
|
20
|
+
WidgetLabel,
|
|
21
|
+
} from '../../config/types'
|
|
22
|
+
import type { ReportedChange } from '../../reporting/summary'
|
|
23
|
+
|
|
24
|
+
const VALID_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg'])
|
|
25
|
+
const FONT_EXTENSIONS = new Set(['.ttf', '.otf', '.woff', '.woff2'])
|
|
26
|
+
const MAX_IMAGE_SIZE_BYTES = 4096
|
|
27
|
+
const DEFAULT_INITIAL_STATE_LOCALE = '__default'
|
|
28
|
+
const VOLTRA_WIDGET_STRINGS_BASENAME = 'VoltraWidgets.strings'
|
|
29
|
+
const IOS_DYNAMIC_WIDGET_MANIFEST_PATH = path.join('.voltra', 'manifest.ios.json')
|
|
30
|
+
|
|
31
|
+
const IOS_WIDGET_FAMILY_MAP: Record<IOSWidgetFamily, string> = {
|
|
32
|
+
systemSmall: '.systemSmall',
|
|
33
|
+
systemMedium: '.systemMedium',
|
|
34
|
+
systemLarge: '.systemLarge',
|
|
35
|
+
systemExtraLarge: '.systemExtraLarge',
|
|
36
|
+
accessoryCircular: '.accessoryCircular',
|
|
37
|
+
accessoryRectangular: '.accessoryRectangular',
|
|
38
|
+
accessoryInline: '.accessoryInline',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const GENERATED_INITIAL_STATE_LOCALE_HELPER = `private enum VoltraGeneratedInitialStateLocale {
|
|
42
|
+
static func pickJson(from perLocale: [String: String], preferredLanguages: [String]) -> String? {
|
|
43
|
+
let entries = perLocale.filter { !$0.value.isEmpty }
|
|
44
|
+
if entries.isEmpty {
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func normalize(_ tag: String) -> String {
|
|
49
|
+
tag.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
50
|
+
.lowercased()
|
|
51
|
+
.replacingOccurrences(of: "_", with: "-")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var byNorm: [String: String] = [:]
|
|
55
|
+
for (k, v) in entries {
|
|
56
|
+
byNorm[normalize(k)] = v
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for pref in preferredLanguages {
|
|
60
|
+
let n = normalize(pref)
|
|
61
|
+
if let direct = byNorm[n] {
|
|
62
|
+
return direct
|
|
63
|
+
}
|
|
64
|
+
let lang = n.split(separator: "-").first.map(String.init) ?? n
|
|
65
|
+
for (key, val) in entries {
|
|
66
|
+
let kn = normalize(key)
|
|
67
|
+
let keyLang = kn.split(separator: "-").first.map(String.init) ?? kn
|
|
68
|
+
if keyLang == lang {
|
|
69
|
+
return val
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if let en = byNorm["en"] {
|
|
75
|
+
return en
|
|
76
|
+
}
|
|
77
|
+
if let englishFamily = entries.keys.sorted().first(where: {
|
|
78
|
+
let normalized = normalize($0)
|
|
79
|
+
return normalized == "en" || normalized.hasPrefix("en-")
|
|
80
|
+
}) {
|
|
81
|
+
return entries[englishFamily]
|
|
82
|
+
}
|
|
83
|
+
if let def = byNorm["__default"] {
|
|
84
|
+
return def
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sorted = entries.keys.sorted()
|
|
88
|
+
guard let firstKey = sorted.first else {
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
return entries[firstKey]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static func preferredLanguageTags() -> [String] {
|
|
95
|
+
Locale.preferredLanguages
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
export interface GenerateIOSFilesOptions {
|
|
101
|
+
projectRoot: string
|
|
102
|
+
ios: NormalizedVoltraIOSConfig
|
|
103
|
+
discovery: IOSProjectDiscovery
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GenerateIOSFilesResult {
|
|
107
|
+
changes: ReportedChange[]
|
|
108
|
+
files: string[]
|
|
109
|
+
warnings: string[]
|
|
110
|
+
targetName: string
|
|
111
|
+
targetPath: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface GeneratedFileResult {
|
|
115
|
+
change?: ReportedChange
|
|
116
|
+
relativePath: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface MainAppMetadata {
|
|
120
|
+
shortVersionString: string
|
|
121
|
+
buildNumber: string
|
|
122
|
+
urlTypes?: Array<{ CFBundleURLSchemes: string[] }>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type WidgetVariants = Record<string, unknown>
|
|
126
|
+
type IOSWidgetRenderer = (variants: WidgetVariants) => string
|
|
127
|
+
type IOSDynamicWidgetRenderer = (element: unknown) => unknown
|
|
128
|
+
interface IOSPlatformPackage {
|
|
129
|
+
renderWidgetToString?: unknown
|
|
130
|
+
}
|
|
131
|
+
type PrerenderedWidgetStates = Map<string, Map<string, string>>
|
|
132
|
+
type DynamicWidgetManifest = {
|
|
133
|
+
version: 1
|
|
134
|
+
platform: 'ios'
|
|
135
|
+
widgets: Array<{ id: string; entry: string }>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type DetectedIOSWidget =
|
|
139
|
+
| (NormalizedIOSWidgetConfig & { clientRendered: false })
|
|
140
|
+
| (NormalizedIOSWidgetConfig & { entry: string; clientRendered: true; clientSourcePath: string })
|
|
141
|
+
|
|
142
|
+
export class IOSGeneratedFilesError extends VoltraCliError {
|
|
143
|
+
constructor(message: string) {
|
|
144
|
+
super(message, 'VOLTRA_IOS_GENERATED_FILES_FAILED')
|
|
145
|
+
this.name = 'IOSGeneratedFilesError'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createGeneratedFilesError(message: string): IOSGeneratedFilesError {
|
|
150
|
+
return new IOSGeneratedFilesError(message)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function generateIOSFiles(options: GenerateIOSFilesOptions): Promise<GenerateIOSFilesResult> {
|
|
154
|
+
const { projectRoot, ios, discovery } = options
|
|
155
|
+
const targetName = resolveIOSWidgetTargetName(ios, discovery)
|
|
156
|
+
const targetPath = path.join(discovery.iosRoot, targetName)
|
|
157
|
+
const detectedWidgets = detectClientRenderedWidgets(projectRoot, ios.widgets)
|
|
158
|
+
const mainAppMetadata = await readMainAppMetadata(discovery.infoPlistPath)
|
|
159
|
+
const changes: ReportedChange[] = []
|
|
160
|
+
const warnings: string[] = []
|
|
161
|
+
const generatedFiles = new Set<string>()
|
|
162
|
+
|
|
163
|
+
mergeSingleResult(
|
|
164
|
+
await writeGeneratedTextFile(
|
|
165
|
+
projectRoot,
|
|
166
|
+
path.join(projectRoot, IOS_DYNAMIC_WIDGET_MANIFEST_PATH),
|
|
167
|
+
`${JSON.stringify(createDynamicWidgetsManifest(detectedWidgets), null, 2)}\n`
|
|
168
|
+
),
|
|
169
|
+
changes,
|
|
170
|
+
generatedFiles
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const infoPlistResult = await generateInfoPlistFile(
|
|
174
|
+
projectRoot,
|
|
175
|
+
targetPath,
|
|
176
|
+
targetName,
|
|
177
|
+
ios,
|
|
178
|
+
mainAppMetadata,
|
|
179
|
+
detectedWidgets
|
|
180
|
+
)
|
|
181
|
+
mergeSingleResult(infoPlistResult, changes, generatedFiles)
|
|
182
|
+
|
|
183
|
+
const entitlementsResult = await generateEntitlementsFile(projectRoot, targetPath, targetName, ios)
|
|
184
|
+
mergeSingleResult(entitlementsResult, changes, generatedFiles)
|
|
185
|
+
|
|
186
|
+
const assetResult = await generateAssetsCatalog(projectRoot, targetPath, ios.userImagesPath)
|
|
187
|
+
mergeResult(assetResult, changes, warnings, generatedFiles)
|
|
188
|
+
|
|
189
|
+
const fontsResult = await copyIOSFonts(projectRoot, targetPath, ios.fonts)
|
|
190
|
+
mergeResult(fontsResult, changes, warnings, generatedFiles)
|
|
191
|
+
|
|
192
|
+
const initialStatesResult = await generateInitialStatesSwift(projectRoot, detectedWidgets)
|
|
193
|
+
mergeSingleResult(
|
|
194
|
+
await writeGeneratedTextFile(
|
|
195
|
+
projectRoot,
|
|
196
|
+
path.join(targetPath, 'VoltraWidgetInitialStates.swift'),
|
|
197
|
+
initialStatesResult
|
|
198
|
+
),
|
|
199
|
+
changes,
|
|
200
|
+
generatedFiles
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const widgetBundleResult = await writeGeneratedTextFile(
|
|
204
|
+
projectRoot,
|
|
205
|
+
path.join(targetPath, 'VoltraWidgetBundle.swift'),
|
|
206
|
+
generateWidgetBundleSwift(detectedWidgets)
|
|
207
|
+
)
|
|
208
|
+
mergeSingleResult(widgetBundleResult, changes, generatedFiles)
|
|
209
|
+
|
|
210
|
+
const localizedStringResults = await generateLocalizedWidgetStrings(projectRoot, targetPath, detectedWidgets)
|
|
211
|
+
mergeResult(localizedStringResults, changes, warnings, generatedFiles)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
changes,
|
|
215
|
+
files: [...generatedFiles].sort(),
|
|
216
|
+
warnings,
|
|
217
|
+
targetName,
|
|
218
|
+
targetPath,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function generateInfoPlistFile(
|
|
223
|
+
projectRoot: string,
|
|
224
|
+
targetPath: string,
|
|
225
|
+
targetName: string,
|
|
226
|
+
ios: NormalizedVoltraIOSConfig,
|
|
227
|
+
mainAppMetadata: MainAppMetadata,
|
|
228
|
+
widgets: DetectedIOSWidget[]
|
|
229
|
+
): Promise<GeneratedFileResult> {
|
|
230
|
+
const plistPath = path.join(targetPath, 'Info.plist')
|
|
231
|
+
const fontNames = ios.fonts.map((fontPath) => path.basename(fontPath)).sort()
|
|
232
|
+
const serverWidgets = widgets.filter((widget) => widget.serverUpdate)
|
|
233
|
+
const hasClientRenderedWidget = widgets.some((widget) => widget.clientRendered)
|
|
234
|
+
const serverUrls = Object.fromEntries(serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.url]))
|
|
235
|
+
const serverIntervals = Object.fromEntries(
|
|
236
|
+
serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.intervalMinutes])
|
|
237
|
+
)
|
|
238
|
+
const serverRefresh = Object.fromEntries(
|
|
239
|
+
serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.refresh ?? false])
|
|
240
|
+
)
|
|
241
|
+
const infoPlist = buildPlistXml(
|
|
242
|
+
{
|
|
243
|
+
CFBundleDevelopmentRegion: '$(DEVELOPMENT_LANGUAGE)',
|
|
244
|
+
CFBundleDisplayName: targetName,
|
|
245
|
+
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
246
|
+
CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
247
|
+
CFBundleInfoDictionaryVersion: '6.0',
|
|
248
|
+
CFBundleName: '$(PRODUCT_NAME)',
|
|
249
|
+
CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)',
|
|
250
|
+
CFBundleShortVersionString: mainAppMetadata.shortVersionString,
|
|
251
|
+
CFBundleVersion: mainAppMetadata.buildNumber,
|
|
252
|
+
NSExtension: {
|
|
253
|
+
NSExtensionPointIdentifier: 'com.apple.widgetkit-extension',
|
|
254
|
+
},
|
|
255
|
+
RCTNewArchEnabled: true,
|
|
256
|
+
CFBundleURLTypes: mainAppMetadata.urlTypes,
|
|
257
|
+
UIAppFonts: fontNames.length > 0 ? fontNames : undefined,
|
|
258
|
+
Voltra_AppGroupIdentifier: ios.groupIdentifier,
|
|
259
|
+
Voltra_KeychainGroup: ios.keychainGroup,
|
|
260
|
+
Voltra_WidgetServerIntervals: Object.keys(serverIntervals).length > 0 ? serverIntervals : undefined,
|
|
261
|
+
Voltra_WidgetServerRefresh: Object.keys(serverRefresh).length > 0 ? serverRefresh : undefined,
|
|
262
|
+
NSAppTransportSecurity: createWidgetAppTransportSecurity(hasClientRenderedWidget, serverWidgets.length > 0),
|
|
263
|
+
Voltra_WidgetServerUrls: Object.keys(serverUrls).length > 0 ? serverUrls : undefined,
|
|
264
|
+
},
|
|
265
|
+
createGeneratedFilesError
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return writeGeneratedTextFile(projectRoot, plistPath, infoPlist)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createWidgetAppTransportSecurity(
|
|
272
|
+
hasClientRenderedWidget: boolean,
|
|
273
|
+
hasServerWidgets: boolean
|
|
274
|
+
): Record<string, unknown> | undefined {
|
|
275
|
+
if (!hasClientRenderedWidget && !hasServerWidgets) {
|
|
276
|
+
return undefined
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
NSAllowsLocalNetworking: true,
|
|
281
|
+
...(hasClientRenderedWidget
|
|
282
|
+
? {
|
|
283
|
+
NSExceptionDomains: {
|
|
284
|
+
localhost: {
|
|
285
|
+
NSExceptionAllowsInsecureHTTPLoads: true,
|
|
286
|
+
NSIncludesSubdomains: true,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
...(hasServerWidgets ? { NSAllowsArbitraryLoads: false } : {}),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function generateEntitlementsFile(
|
|
296
|
+
projectRoot: string,
|
|
297
|
+
targetPath: string,
|
|
298
|
+
targetName: string,
|
|
299
|
+
ios: NormalizedVoltraIOSConfig
|
|
300
|
+
): Promise<GeneratedFileResult> {
|
|
301
|
+
const entitlementsPath = path.join(targetPath, `${targetName}.entitlements`)
|
|
302
|
+
const entitlements = buildPlistXml(
|
|
303
|
+
{
|
|
304
|
+
'com.apple.security.application-groups': ios.groupIdentifier ? [ios.groupIdentifier] : undefined,
|
|
305
|
+
'keychain-access-groups': ios.keychainGroup ? [ios.keychainGroup] : undefined,
|
|
306
|
+
},
|
|
307
|
+
createGeneratedFilesError
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return writeGeneratedTextFile(projectRoot, entitlementsPath, entitlements)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function generateAssetsCatalog(
|
|
314
|
+
projectRoot: string,
|
|
315
|
+
targetPath: string,
|
|
316
|
+
userImagesPath: string
|
|
317
|
+
): Promise<GenerateIOSFilesResult> {
|
|
318
|
+
const changes: ReportedChange[] = []
|
|
319
|
+
const warnings: string[] = []
|
|
320
|
+
const generatedFiles = new Set<string>()
|
|
321
|
+
const assetsCatalogPath = path.join(targetPath, 'Assets.xcassets')
|
|
322
|
+
const rootContentsResult = await writeGeneratedTextFile(
|
|
323
|
+
projectRoot,
|
|
324
|
+
path.join(assetsCatalogPath, 'Contents.json'),
|
|
325
|
+
`${JSON.stringify({ info: { author: 'xcode', version: 1 } }, null, 2)}\n`
|
|
326
|
+
)
|
|
327
|
+
mergeSingleResult(rootContentsResult, changes, generatedFiles)
|
|
328
|
+
|
|
329
|
+
const userImages = await collectUserImages(userImagesPath)
|
|
330
|
+
const seenAssetNames = new Map<string, string>()
|
|
331
|
+
|
|
332
|
+
for (const imagePath of userImages) {
|
|
333
|
+
const extension = path.extname(imagePath).toLowerCase()
|
|
334
|
+
|
|
335
|
+
if (!VALID_IMAGE_EXTENSIONS.has(extension)) {
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const relativeAssetPath = path.relative(userImagesPath, imagePath).slice(0, -extension.length)
|
|
340
|
+
const imageName = getAssetName(relativeAssetPath, seenAssetNames)
|
|
341
|
+
const imagesetPath = path.join(assetsCatalogPath, `${imageName}.imageset`)
|
|
342
|
+
const imageFileName = `${imageName}${extension}`
|
|
343
|
+
const imageResult = await copyGeneratedFile(projectRoot, imagePath, path.join(imagesetPath, imageFileName))
|
|
344
|
+
mergeSingleResult(imageResult, changes, generatedFiles)
|
|
345
|
+
|
|
346
|
+
const contentsResult = await writeGeneratedTextFile(
|
|
347
|
+
projectRoot,
|
|
348
|
+
path.join(imagesetPath, 'Contents.json'),
|
|
349
|
+
`${JSON.stringify(
|
|
350
|
+
{
|
|
351
|
+
images: [{ filename: imageFileName, idiom: 'universal' }],
|
|
352
|
+
info: { author: 'xcode', version: 1 },
|
|
353
|
+
},
|
|
354
|
+
null,
|
|
355
|
+
2
|
|
356
|
+
)}\n`
|
|
357
|
+
)
|
|
358
|
+
mergeSingleResult(contentsResult, changes, generatedFiles)
|
|
359
|
+
|
|
360
|
+
const imageWarning = await getLargeImageWarning(imagePath, imageFileName)
|
|
361
|
+
if (imageWarning) {
|
|
362
|
+
warnings.push(imageWarning)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
changes,
|
|
368
|
+
files: [...generatedFiles],
|
|
369
|
+
warnings,
|
|
370
|
+
targetName: '',
|
|
371
|
+
targetPath,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function copyIOSFonts(projectRoot: string, targetPath: string, fonts: string[]): Promise<GenerateIOSFilesResult> {
|
|
376
|
+
const changes: ReportedChange[] = []
|
|
377
|
+
const generatedFiles = new Set<string>()
|
|
378
|
+
const warnings: string[] = []
|
|
379
|
+
const fontPaths = await resolveFontPaths(projectRoot, fonts)
|
|
380
|
+
|
|
381
|
+
for (const fontPath of fontPaths) {
|
|
382
|
+
const result = await copyGeneratedFile(projectRoot, fontPath, path.join(targetPath, path.basename(fontPath)))
|
|
383
|
+
mergeSingleResult(result, changes, generatedFiles)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
changes,
|
|
388
|
+
files: [...generatedFiles],
|
|
389
|
+
warnings,
|
|
390
|
+
targetName: '',
|
|
391
|
+
targetPath,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function generateLocalizedWidgetStrings(
|
|
396
|
+
projectRoot: string,
|
|
397
|
+
targetPath: string,
|
|
398
|
+
widgets: NormalizedIOSWidgetConfig[]
|
|
399
|
+
): Promise<GenerateIOSFilesResult> {
|
|
400
|
+
const changes: ReportedChange[] = []
|
|
401
|
+
const generatedFiles = new Set<string>()
|
|
402
|
+
const byLocale = collectGalleryStringsByLocale(widgets)
|
|
403
|
+
|
|
404
|
+
for (const [locale, entries] of byLocale.entries()) {
|
|
405
|
+
const lprojPath = path.join(targetPath, `${locale}.lproj`)
|
|
406
|
+
const stringsPath = path.join(lprojPath, VOLTRA_WIDGET_STRINGS_BASENAME)
|
|
407
|
+
const stringsContent = formatStringsFile(entries)
|
|
408
|
+
const result = await writeGeneratedTextFile(projectRoot, stringsPath, stringsContent)
|
|
409
|
+
mergeSingleResult(result, changes, generatedFiles)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
changes,
|
|
414
|
+
files: [...generatedFiles],
|
|
415
|
+
warnings: [],
|
|
416
|
+
targetName: '',
|
|
417
|
+
targetPath,
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function readMainAppMetadata(infoPlistPath: string): Promise<MainAppMetadata> {
|
|
422
|
+
const dict = await parsePlistFile(
|
|
423
|
+
infoPlistPath,
|
|
424
|
+
'main app Info.plist',
|
|
425
|
+
(message: string) => new IOSGeneratedFilesError(message)
|
|
426
|
+
)
|
|
427
|
+
const shortVersionString = readPlistString(dict, 'CFBundleShortVersionString') ?? '1.0.0'
|
|
428
|
+
const buildNumber = readPlistString(dict, 'CFBundleVersion') ?? '1'
|
|
429
|
+
const urlTypes = readUrlTypes(dict)
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
shortVersionString,
|
|
433
|
+
buildNumber,
|
|
434
|
+
urlTypes: urlTypes.length > 0 ? urlTypes : undefined,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function generateInitialStatesSwift(projectRoot: string, widgets: DetectedIOSWidget[]): Promise<string> {
|
|
439
|
+
const serverWidgets = widgets.filter(
|
|
440
|
+
(widget): widget is Extract<DetectedIOSWidget, { clientRendered: false }> => !widget.clientRendered
|
|
441
|
+
)
|
|
442
|
+
const prerenderableServerWidgets = serverWidgets.filter((widget) => widget.initialStatePath)
|
|
443
|
+
const serverStates = await prerenderWidgetStates(
|
|
444
|
+
projectRoot,
|
|
445
|
+
prerenderableServerWidgets,
|
|
446
|
+
loadIOSWidgetRenderer(projectRoot)
|
|
447
|
+
)
|
|
448
|
+
const clientStates = await prerenderClientRenderedWidgets(projectRoot, widgets)
|
|
449
|
+
const prerenderedStates = new Map([...serverStates, ...clientStates])
|
|
450
|
+
|
|
451
|
+
if (prerenderedStates.size === 0) {
|
|
452
|
+
return [
|
|
453
|
+
'//',
|
|
454
|
+
'// VoltraWidgetInitialStates.swift',
|
|
455
|
+
'//',
|
|
456
|
+
'// Auto-generated by Voltra.',
|
|
457
|
+
'// No widget initial states configured.',
|
|
458
|
+
'//',
|
|
459
|
+
'',
|
|
460
|
+
'import Foundation',
|
|
461
|
+
'',
|
|
462
|
+
'public enum VoltraWidgetInitialStates {',
|
|
463
|
+
' public static func getInitialState(for widgetId: String) -> Data? {',
|
|
464
|
+
' nil',
|
|
465
|
+
' }',
|
|
466
|
+
'}',
|
|
467
|
+
'',
|
|
468
|
+
].join('\n')
|
|
469
|
+
}
|
|
470
|
+
const widgetEntries = [...prerenderedStates.entries()]
|
|
471
|
+
.map(([widgetId, localeMap]) => {
|
|
472
|
+
const localeEntries = [...localeMap.entries()]
|
|
473
|
+
.map(([localeKey, json]) => {
|
|
474
|
+
const delimiter = getSwiftRawStringDelimiter(json)
|
|
475
|
+
return ` ${JSON.stringify(localeKey)}: ${delimiter}"${json}"${delimiter}`
|
|
476
|
+
})
|
|
477
|
+
.join(',\n')
|
|
478
|
+
return ` ${JSON.stringify(widgetId)}: [\n${localeEntries}\n ]`
|
|
479
|
+
})
|
|
480
|
+
.join(',\n')
|
|
481
|
+
|
|
482
|
+
return [
|
|
483
|
+
'//',
|
|
484
|
+
'// VoltraWidgetInitialStates.swift',
|
|
485
|
+
'//',
|
|
486
|
+
'// Auto-generated by Voltra.',
|
|
487
|
+
'// Contains pre-rendered initial states for home screen widgets.',
|
|
488
|
+
'//',
|
|
489
|
+
'',
|
|
490
|
+
'import Foundation',
|
|
491
|
+
'',
|
|
492
|
+
GENERATED_INITIAL_STATE_LOCALE_HELPER,
|
|
493
|
+
'',
|
|
494
|
+
'public enum VoltraWidgetInitialStates {',
|
|
495
|
+
' private static let bundledLocalizedStates: [String: [String: String]] = [',
|
|
496
|
+
widgetEntries,
|
|
497
|
+
' ]',
|
|
498
|
+
'',
|
|
499
|
+
' public static func getInitialState(for widgetId: String) -> Data? {',
|
|
500
|
+
' guard let perLocale = bundledLocalizedStates[widgetId] else { return nil }',
|
|
501
|
+
' let tags = VoltraGeneratedInitialStateLocale.preferredLanguageTags()',
|
|
502
|
+
' guard let jsonString = VoltraGeneratedInitialStateLocale.pickJson(from: perLocale, preferredLanguages: tags) else {',
|
|
503
|
+
' return nil',
|
|
504
|
+
' }',
|
|
505
|
+
' return jsonString.data(using: .utf8)',
|
|
506
|
+
' }',
|
|
507
|
+
'}',
|
|
508
|
+
'',
|
|
509
|
+
].join('\n')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function generateWidgetBundleSwift(widgets: DetectedIOSWidget[]): string {
|
|
513
|
+
const needsFoundation = widgets.some(
|
|
514
|
+
(widget) => isWidgetLocalizedMap(widget.displayName) || isWidgetLocalizedMap(widget.description)
|
|
515
|
+
)
|
|
516
|
+
const appIntentWidgets = widgets.filter(widgetUsesAppIntent)
|
|
517
|
+
const imports = [
|
|
518
|
+
needsFoundation ? 'import Foundation' : undefined,
|
|
519
|
+
appIntentWidgets.length > 0 ? 'import AppIntents' : undefined,
|
|
520
|
+
'import SwiftUI',
|
|
521
|
+
'import WidgetKit',
|
|
522
|
+
'import VoltraWidget',
|
|
523
|
+
]
|
|
524
|
+
.filter((value): value is string => value !== undefined)
|
|
525
|
+
.join('\n')
|
|
526
|
+
|
|
527
|
+
if (widgets.length === 0) {
|
|
528
|
+
return [
|
|
529
|
+
'//',
|
|
530
|
+
'// VoltraWidgetBundle.swift',
|
|
531
|
+
'//',
|
|
532
|
+
'// Auto-generated by Voltra.',
|
|
533
|
+
'//',
|
|
534
|
+
'',
|
|
535
|
+
imports,
|
|
536
|
+
'',
|
|
537
|
+
'@main',
|
|
538
|
+
'struct VoltraWidgetBundle: WidgetBundle {',
|
|
539
|
+
' var body: some Widget {',
|
|
540
|
+
' VoltraWidget()',
|
|
541
|
+
' }',
|
|
542
|
+
'}',
|
|
543
|
+
'',
|
|
544
|
+
].join('\n')
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const widgetStructs = widgets.map(generateWidgetStruct).join('\n\n')
|
|
548
|
+
const plainWidgets = widgets.filter((widget) => !widgetUsesAppIntent(widget))
|
|
549
|
+
const plainInstances = plainWidgets.map((widget) => ` VoltraWidget_${widget.id}()`).join('\n')
|
|
550
|
+
const appIntentInstances =
|
|
551
|
+
appIntentWidgets.length > 0
|
|
552
|
+
? ` if #available(iOS 17.0, *) {\n${appIntentWidgets
|
|
553
|
+
.map((widget) => ` VoltraWidget_${widget.id}()`)
|
|
554
|
+
.join('\n')}\n }`
|
|
555
|
+
: ''
|
|
556
|
+
const widgetInstances = [plainInstances, appIntentInstances].filter(Boolean).join('\n')
|
|
557
|
+
|
|
558
|
+
return [
|
|
559
|
+
'//',
|
|
560
|
+
'// VoltraWidgetBundle.swift',
|
|
561
|
+
'//',
|
|
562
|
+
'// Auto-generated by Voltra.',
|
|
563
|
+
'//',
|
|
564
|
+
'',
|
|
565
|
+
imports,
|
|
566
|
+
'',
|
|
567
|
+
'@main',
|
|
568
|
+
'struct VoltraWidgetBundle: WidgetBundle {',
|
|
569
|
+
' var body: some Widget {',
|
|
570
|
+
' VoltraWidget()',
|
|
571
|
+
widgetInstances,
|
|
572
|
+
' }',
|
|
573
|
+
'}',
|
|
574
|
+
'',
|
|
575
|
+
widgetStructs,
|
|
576
|
+
'',
|
|
577
|
+
].join('\n')
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function generateWidgetStruct(widget: DetectedIOSWidget): string {
|
|
581
|
+
if (widgetUsesAppIntent(widget)) {
|
|
582
|
+
return generateClientAppIntentWidgetCode(widget)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const familiesSwift = widget.supportedFamilies.map((family) => IOS_WIDGET_FAMILY_MAP[family]).join(', ')
|
|
586
|
+
const displayNameExpr = createSwiftLabelExpression(widget.id, 'displayName', widget.displayName)
|
|
587
|
+
const descriptionExpr = createSwiftLabelExpression(widget.id, 'description', widget.description)
|
|
588
|
+
const providerAndContent = widget.clientRendered
|
|
589
|
+
? [
|
|
590
|
+
' provider: VoltraClientWidgetProvider(',
|
|
591
|
+
' widgetId: widgetId,',
|
|
592
|
+
' initialState: VoltraWidgetInitialStates.getInitialState(for: widgetId)',
|
|
593
|
+
' )',
|
|
594
|
+
' ) { entry in',
|
|
595
|
+
' VoltraClientWidgetContentView(',
|
|
596
|
+
' entry: entry,',
|
|
597
|
+
' initialState: VoltraWidgetInitialStates.getInitialState(for: widgetId)',
|
|
598
|
+
' )',
|
|
599
|
+
].join('\n')
|
|
600
|
+
: [
|
|
601
|
+
' provider: VoltraHomeWidgetProvider(',
|
|
602
|
+
' widgetId: widgetId,',
|
|
603
|
+
' initialState: VoltraWidgetInitialStates.getInitialState(for: widgetId)',
|
|
604
|
+
' )',
|
|
605
|
+
' ) { entry in',
|
|
606
|
+
' VoltraHomeWidgetView(entry: entry)',
|
|
607
|
+
].join('\n')
|
|
608
|
+
|
|
609
|
+
return [
|
|
610
|
+
`public struct VoltraWidget_${widget.id}: Widget {`,
|
|
611
|
+
` private let widgetId = ${JSON.stringify(widget.id)}`,
|
|
612
|
+
'',
|
|
613
|
+
' public init() {}',
|
|
614
|
+
'',
|
|
615
|
+
' public var body: some WidgetConfiguration {',
|
|
616
|
+
' StaticConfiguration(',
|
|
617
|
+
` kind: ${JSON.stringify(`Voltra_Widget_${widget.id}`)},`,
|
|
618
|
+
providerAndContent,
|
|
619
|
+
' }',
|
|
620
|
+
` .configurationDisplayName(${displayNameExpr})`,
|
|
621
|
+
` .description(${descriptionExpr})`,
|
|
622
|
+
` .supportedFamilies([${familiesSwift}])`,
|
|
623
|
+
' .contentMarginsDisabled()',
|
|
624
|
+
' }',
|
|
625
|
+
'}',
|
|
626
|
+
].join('\n')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function widgetUsesAppIntent(widget: DetectedIOSWidget): widget is Extract<
|
|
630
|
+
DetectedIOSWidget,
|
|
631
|
+
{ clientRendered: true }
|
|
632
|
+
> & {
|
|
633
|
+
appIntent: { parameters: IOSWidgetAppIntentParameter[] }
|
|
634
|
+
} {
|
|
635
|
+
return widget.clientRendered && !!widget.appIntent && widget.appIntent.parameters.length > 0
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function generateClientAppIntentWidgetCode(
|
|
639
|
+
widget: Extract<DetectedIOSWidget, { clientRendered: true }> & {
|
|
640
|
+
appIntent: { parameters: IOSWidgetAppIntentParameter[] }
|
|
641
|
+
}
|
|
642
|
+
): string {
|
|
643
|
+
const familiesSwift = widget.supportedFamilies.map((family) => IOS_WIDGET_FAMILY_MAP[family]).join(', ')
|
|
644
|
+
const intentName = `VoltraWidget_${widget.id}_Intent`
|
|
645
|
+
const providerName = `VoltraWidget_${widget.id}_ClientProvider`
|
|
646
|
+
const intentTitle = `Configure ${widgetLabelEnglish(widget.displayName)}`
|
|
647
|
+
const displayNameExpr = createSwiftLabelExpression(widget.id, 'displayName', widget.displayName)
|
|
648
|
+
const descriptionExpr = createSwiftLabelExpression(widget.id, 'description', widget.description)
|
|
649
|
+
const parameterDeclarations = widget.appIntent.parameters
|
|
650
|
+
.map(
|
|
651
|
+
(parameter) =>
|
|
652
|
+
` @Parameter(title: ${JSON.stringify(parameter.title)}, default: ${swiftDefaultValue(parameter)})\n var ${
|
|
653
|
+
parameter.name
|
|
654
|
+
}: String`
|
|
655
|
+
)
|
|
656
|
+
.join('\n\n')
|
|
657
|
+
const initializerParameters = widget.appIntent.parameters.map((parameter) => `${parameter.name}: String`).join(', ')
|
|
658
|
+
const initializerAssignments = widget.appIntent.parameters
|
|
659
|
+
.map((parameter) => ` self.${parameter.name} = ${parameter.name}`)
|
|
660
|
+
.join('\n')
|
|
661
|
+
const configuredDictionary = createSwiftDictionaryLiteral(
|
|
662
|
+
widget.appIntent.parameters.map((parameter) => `${JSON.stringify(parameter.name)}: configuration.${parameter.name}`)
|
|
663
|
+
)
|
|
664
|
+
const defaultDictionary = createSwiftDictionaryLiteral(
|
|
665
|
+
widget.appIntent.parameters.map((parameter) => `${JSON.stringify(parameter.name)}: ${swiftDefaultValue(parameter)}`)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
return [
|
|
669
|
+
`@available(iOS 17.0, *)`,
|
|
670
|
+
`struct ${intentName}: WidgetConfigurationIntent {`,
|
|
671
|
+
` static var title: LocalizedStringResource = ${JSON.stringify(intentTitle)}`,
|
|
672
|
+
'',
|
|
673
|
+
parameterDeclarations,
|
|
674
|
+
'',
|
|
675
|
+
' init() {}',
|
|
676
|
+
` init(${initializerParameters}) {`,
|
|
677
|
+
initializerAssignments,
|
|
678
|
+
' }',
|
|
679
|
+
'}',
|
|
680
|
+
'',
|
|
681
|
+
`@available(iOS 17.0, *)`,
|
|
682
|
+
`private struct ${providerName}: AppIntentTimelineProvider {`,
|
|
683
|
+
` typealias Intent = ${intentName}`,
|
|
684
|
+
' typealias Entry = VoltraClientWidgetEntry',
|
|
685
|
+
'',
|
|
686
|
+
` private let widgetId = ${JSON.stringify(widget.id)}`,
|
|
687
|
+
'',
|
|
688
|
+
' func placeholder(in _: Context) -> VoltraClientWidgetEntry {',
|
|
689
|
+
` VoltraClientWidgetEntry(date: Date(), widgetId: widgetId, bundleReady: false, configuration: ${defaultDictionary})`,
|
|
690
|
+
' }',
|
|
691
|
+
'',
|
|
692
|
+
` func snapshot(for configuration: ${intentName}, in _: Context) async -> VoltraClientWidgetEntry {`,
|
|
693
|
+
` await VoltraClientWidgetProvider.loadEntry(widgetId: widgetId, configuration: ${configuredDictionary})`,
|
|
694
|
+
' }',
|
|
695
|
+
'',
|
|
696
|
+
` func timeline(for configuration: ${intentName}, in _: Context) async -> Timeline<VoltraClientWidgetEntry> {`,
|
|
697
|
+
` let entry = await VoltraClientWidgetProvider.loadEntry(widgetId: widgetId, configuration: ${configuredDictionary})`,
|
|
698
|
+
' return Timeline(entries: [entry], policy: .never)',
|
|
699
|
+
' }',
|
|
700
|
+
'}',
|
|
701
|
+
'',
|
|
702
|
+
`@available(iOS 17.0, *)`,
|
|
703
|
+
`public struct VoltraWidget_${widget.id}: Widget {`,
|
|
704
|
+
` private let widgetId = ${JSON.stringify(widget.id)}`,
|
|
705
|
+
'',
|
|
706
|
+
' public init() {}',
|
|
707
|
+
'',
|
|
708
|
+
' public var body: some WidgetConfiguration {',
|
|
709
|
+
' AppIntentConfiguration(',
|
|
710
|
+
` kind: ${JSON.stringify(`Voltra_Widget_${widget.id}`)},`,
|
|
711
|
+
` intent: ${intentName}.self,`,
|
|
712
|
+
` provider: ${providerName}()`,
|
|
713
|
+
' ) { entry in',
|
|
714
|
+
' VoltraClientWidgetContentView(',
|
|
715
|
+
' entry: entry,',
|
|
716
|
+
' initialState: VoltraWidgetInitialStates.getInitialState(for: widgetId)',
|
|
717
|
+
' )',
|
|
718
|
+
' }',
|
|
719
|
+
` .configurationDisplayName(${displayNameExpr})`,
|
|
720
|
+
` .description(${descriptionExpr})`,
|
|
721
|
+
` .supportedFamilies([${familiesSwift}])`,
|
|
722
|
+
' .contentMarginsDisabled()',
|
|
723
|
+
' }',
|
|
724
|
+
'}',
|
|
725
|
+
].join('\n')
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function createSwiftLabelExpression(
|
|
729
|
+
widgetId: string,
|
|
730
|
+
field: 'displayName' | 'description',
|
|
731
|
+
label: WidgetLabel
|
|
732
|
+
): string {
|
|
733
|
+
if (!isWidgetLocalizedMap(label)) {
|
|
734
|
+
return `Text(${JSON.stringify(label)})`
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const key = `voltra_widget_${widgetId}_${field}`
|
|
738
|
+
const defaultEnglish = widgetLabelEnglish(label)
|
|
739
|
+
|
|
740
|
+
return `Text(LocalizedStringResource(${JSON.stringify(key)}, defaultValue: String.LocalizationValue(${JSON.stringify(
|
|
741
|
+
defaultEnglish
|
|
742
|
+
)}), table: ${JSON.stringify('VoltraWidgets')}))`
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function swiftDefaultValue(parameter: IOSWidgetAppIntentParameter): string {
|
|
746
|
+
return JSON.stringify(parameter.default ?? '')
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function createSwiftDictionaryLiteral(entries: string[]): string {
|
|
750
|
+
return entries.length > 0 ? `[${entries.join(', ')}]` : '[:]'
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function prerenderWidgetStates(
|
|
754
|
+
projectRoot: string,
|
|
755
|
+
widgets: NormalizedIOSWidgetConfig[],
|
|
756
|
+
renderer: IOSWidgetRenderer
|
|
757
|
+
): Promise<PrerenderedWidgetStates> {
|
|
758
|
+
const prerenderedStates: PrerenderedWidgetStates = new Map()
|
|
759
|
+
|
|
760
|
+
for (const widget of widgets) {
|
|
761
|
+
const initialStatePath = widget.initialStatePath
|
|
762
|
+
|
|
763
|
+
if (!initialStatePath) {
|
|
764
|
+
continue
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const perLocalePaths =
|
|
768
|
+
typeof initialStatePath === 'string' ? { [DEFAULT_INITIAL_STATE_LOCALE]: initialStatePath } : initialStatePath
|
|
769
|
+
const localeStates = new Map<string, string>()
|
|
770
|
+
|
|
771
|
+
for (const [localeKey, modulePath] of Object.entries(perLocalePaths)) {
|
|
772
|
+
if (!(await pathExists(modulePath))) {
|
|
773
|
+
throw new IOSGeneratedFilesError(`Initial state file not found for widget '${widget.id}' at ${modulePath}`)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const widgetVariants = evaluateLegacyWidgetModule(projectRoot, modulePath)
|
|
777
|
+
localeStates.set(localeKey, renderer(widgetVariants))
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
prerenderedStates.set(widget.id, localeStates)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return prerenderedStates
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function evaluateLegacyWidgetModule(projectRoot: string, filePath: string): WidgetVariants {
|
|
787
|
+
const exports = evaluateWidgetModuleExports(projectRoot, filePath, createGeneratedFilesError) as { default?: unknown }
|
|
788
|
+
const widgetVariants = exports.default ?? exports
|
|
789
|
+
|
|
790
|
+
if (!widgetVariants || typeof widgetVariants !== 'object') {
|
|
791
|
+
throw new IOSGeneratedFilesError(`Widget file must export widget variants: ${filePath}`)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return widgetVariants as WidgetVariants
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function loadIOSWidgetRenderer(projectRoot: string): IOSWidgetRenderer {
|
|
798
|
+
const iosPackage = requirePlatformPackage<IOSPlatformPackage>(projectRoot, 'ios')
|
|
799
|
+
|
|
800
|
+
if (typeof iosPackage.renderWidgetToString !== 'function') {
|
|
801
|
+
throw new IOSGeneratedFilesError('Installed @use-voltra/ios package does not export renderWidgetToString.')
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return iosPackage.renderWidgetToString as IOSWidgetRenderer
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function loadIOSDynamicWidgetRenderer(projectRoot: string): IOSDynamicWidgetRenderer {
|
|
808
|
+
const iosPackage = requirePlatformPackage<{ renderVoltraVariantToJson?: unknown }>(projectRoot, 'ios')
|
|
809
|
+
|
|
810
|
+
if (typeof iosPackage.renderVoltraVariantToJson !== 'function') {
|
|
811
|
+
throw new IOSGeneratedFilesError('Installed @use-voltra/ios package does not export renderVoltraVariantToJson.')
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return iosPackage.renderVoltraVariantToJson as IOSDynamicWidgetRenderer
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function createDynamicWidgetsManifest(widgets: DetectedIOSWidget[]): DynamicWidgetManifest {
|
|
818
|
+
return {
|
|
819
|
+
version: 1,
|
|
820
|
+
platform: 'ios',
|
|
821
|
+
widgets: widgets
|
|
822
|
+
.filter((widget): widget is Extract<DetectedIOSWidget, { clientRendered: true }> => widget.clientRendered)
|
|
823
|
+
.map((widget) => ({
|
|
824
|
+
id: widget.id,
|
|
825
|
+
entry: widget.entry,
|
|
826
|
+
})),
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function detectClientRenderedWidgets(projectRoot: string, widgets: NormalizedIOSWidgetConfig[]): DetectedIOSWidget[] {
|
|
831
|
+
return widgets.map((widget) => detectSingleWidget(projectRoot, widget))
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function detectSingleWidget(projectRoot: string, widget: NormalizedIOSWidgetConfig): DetectedIOSWidget {
|
|
835
|
+
if (!widget.entry) {
|
|
836
|
+
return {
|
|
837
|
+
...widget,
|
|
838
|
+
clientRendered: false,
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const clientSourcePath = path.join(projectRoot, widget.entry)
|
|
843
|
+
|
|
844
|
+
if (!fs.existsSync(clientSourcePath) || !fs.statSync(clientSourcePath).isFile()) {
|
|
845
|
+
throw createGeneratedFilesError(`[voltra] Dynamic Widget "${widget.id}" entry not found at ${widget.entry}`)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const widgetModule = safelyEvaluateDynamicWidget(projectRoot, widget.id, widget.entry, clientSourcePath)
|
|
849
|
+
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
850
|
+
|
|
851
|
+
if (typeof widgetFn !== 'function') {
|
|
852
|
+
throw createGeneratedFilesError(
|
|
853
|
+
`[voltra] Dynamic Widget "${widget.id}" at ${widget.entry} must default-export a function or component.`
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
...widget,
|
|
859
|
+
entry: widget.entry,
|
|
860
|
+
clientRendered: true,
|
|
861
|
+
clientSourcePath,
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function prerenderClientRenderedWidgets(
|
|
866
|
+
projectRoot: string,
|
|
867
|
+
widgets: DetectedIOSWidget[]
|
|
868
|
+
): Promise<PrerenderedWidgetStates> {
|
|
869
|
+
const clientWidgets = widgets.filter(
|
|
870
|
+
(widget): widget is Extract<DetectedIOSWidget, { clientRendered: true }> => widget.clientRendered
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if (clientWidgets.length === 0) {
|
|
874
|
+
return new Map()
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const renderer = loadIOSDynamicWidgetRenderer(projectRoot)
|
|
878
|
+
const placeholderEnv = {
|
|
879
|
+
date: Date.now(),
|
|
880
|
+
widgetFamily: 'systemMedium',
|
|
881
|
+
colorScheme: 'light',
|
|
882
|
+
locale: 'en-US',
|
|
883
|
+
widgetRenderingMode: 'fullColor',
|
|
884
|
+
showsWidgetContainerBackground: true,
|
|
885
|
+
configuration: undefined,
|
|
886
|
+
build: DYNAMIC_WIDGET_BUILD_INFO,
|
|
887
|
+
}
|
|
888
|
+
const prerenderedStates: PrerenderedWidgetStates = new Map()
|
|
889
|
+
|
|
890
|
+
for (const widget of clientWidgets) {
|
|
891
|
+
try {
|
|
892
|
+
const widgetModule = safelyEvaluateDynamicWidget(projectRoot, widget.id, widget.entry, widget.clientSourcePath)
|
|
893
|
+
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
894
|
+
const element = widgetFn({}, placeholderEnv)
|
|
895
|
+
prerenderedStates.set(widget.id, new Map([[DEFAULT_INITIAL_STATE_LOCALE, JSON.stringify(renderer(element))]]))
|
|
896
|
+
} catch (error) {
|
|
897
|
+
if (error instanceof IOSGeneratedFilesError) {
|
|
898
|
+
throw error
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
throw createGeneratedFilesError(
|
|
902
|
+
`[voltra] Dynamic Widget "${widget.id}" failed placeholder prerender at ${widget.entry}: ${
|
|
903
|
+
error instanceof Error ? error.message : String(error)
|
|
904
|
+
}`
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return prerenderedStates
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function safelyEvaluateDynamicWidget(
|
|
913
|
+
projectRoot: string,
|
|
914
|
+
widgetId: string,
|
|
915
|
+
widgetEntry: string,
|
|
916
|
+
clientSourcePath: string
|
|
917
|
+
): unknown {
|
|
918
|
+
try {
|
|
919
|
+
return evaluateWidgetModuleExports(projectRoot, clientSourcePath, createGeneratedFilesError)
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error instanceof IOSGeneratedFilesError) {
|
|
922
|
+
throw error
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
throw createGeneratedFilesError(
|
|
926
|
+
`[voltra] Dynamic Widget "${widgetId}" failed to evaluate entry at ${widgetEntry}: ${
|
|
927
|
+
error instanceof Error ? error.message : String(error)
|
|
928
|
+
}`
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function readDynamicWidgetExport(
|
|
934
|
+
widgetId: string,
|
|
935
|
+
widgetEntry: string,
|
|
936
|
+
widgetModule: unknown
|
|
937
|
+
): (props: unknown, env: unknown) => unknown {
|
|
938
|
+
const exports =
|
|
939
|
+
widgetModule && typeof widgetModule === 'object'
|
|
940
|
+
? (widgetModule as { default?: unknown })
|
|
941
|
+
: { default: widgetModule }
|
|
942
|
+
const widgetFn = exports.default ?? widgetModule
|
|
943
|
+
|
|
944
|
+
if (typeof widgetFn !== 'function') {
|
|
945
|
+
throw createGeneratedFilesError(
|
|
946
|
+
`[voltra] Dynamic Widget "${widgetId}" at ${widgetEntry} must default-export a function or component.`
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return widgetFn as (props: unknown, env: unknown) => unknown
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function createProjectRequire(projectRoot: string): NodeRequire {
|
|
954
|
+
return createRequire(path.join(projectRoot, 'package.json'))
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function resolveFontPaths(projectRoot: string, fonts: string[]): Promise<string[]> {
|
|
958
|
+
const projectRequire = createProjectRequire(projectRoot)
|
|
959
|
+
const resolvedFontPaths = new Set<string>()
|
|
960
|
+
|
|
961
|
+
for (const font of fonts) {
|
|
962
|
+
const resolvedFontInput = await resolveFontInput(projectRoot, font, projectRequire)
|
|
963
|
+
|
|
964
|
+
if (!resolvedFontInput) {
|
|
965
|
+
throw new IOSGeneratedFilesError(`Could not resolve iOS font path: ${font}`)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const stat = await fsPromises.stat(resolvedFontInput)
|
|
969
|
+
if (!stat.isDirectory()) {
|
|
970
|
+
if (FONT_EXTENSIONS.has(path.extname(resolvedFontInput).toLowerCase())) {
|
|
971
|
+
resolvedFontPaths.add(resolvedFontInput)
|
|
972
|
+
}
|
|
973
|
+
continue
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
for (const entry of await fsPromises.readdir(resolvedFontInput)) {
|
|
977
|
+
const candidatePath = path.join(resolvedFontInput, entry)
|
|
978
|
+
if (FONT_EXTENSIONS.has(path.extname(candidatePath).toLowerCase())) {
|
|
979
|
+
resolvedFontPaths.add(candidatePath)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return [...resolvedFontPaths].sort()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function resolveFontInput(
|
|
988
|
+
projectRoot: string,
|
|
989
|
+
input: string,
|
|
990
|
+
projectRequire: NodeRequire
|
|
991
|
+
): Promise<string | null> {
|
|
992
|
+
const resolvedPath = path.isAbsolute(input) ? input : path.resolve(projectRoot, input)
|
|
993
|
+
if (await pathExists(resolvedPath)) {
|
|
994
|
+
return resolvedPath
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
return projectRequire.resolve(input)
|
|
999
|
+
} catch {
|
|
1000
|
+
return null
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async function collectUserImages(userImagesPath: string): Promise<string[]> {
|
|
1005
|
+
if (!(await pathExists(userImagesPath))) {
|
|
1006
|
+
return []
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const collectedPaths: string[] = []
|
|
1010
|
+
await collectPathsRecursively(userImagesPath, collectedPaths)
|
|
1011
|
+
return collectedPaths.sort()
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function collectPathsRecursively(currentPath: string, collectedPaths: string[]): Promise<void> {
|
|
1015
|
+
const stat = await fsPromises.lstat(currentPath)
|
|
1016
|
+
|
|
1017
|
+
if (!stat.isDirectory()) {
|
|
1018
|
+
if (!path.basename(currentPath).startsWith('.')) {
|
|
1019
|
+
collectedPaths.push(currentPath)
|
|
1020
|
+
}
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for (const entry of await fsPromises.readdir(currentPath)) {
|
|
1025
|
+
await collectPathsRecursively(path.join(currentPath, entry), collectedPaths)
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function copyGeneratedFile(
|
|
1030
|
+
projectRoot: string,
|
|
1031
|
+
sourcePath: string,
|
|
1032
|
+
destinationPath: string
|
|
1033
|
+
): Promise<GeneratedFileResult> {
|
|
1034
|
+
await ensureDirectory(path.dirname(destinationPath))
|
|
1035
|
+
const sourceContent = await fsPromises.readFile(sourcePath)
|
|
1036
|
+
const existingContent = (await pathExists(destinationPath)) ? await fsPromises.readFile(destinationPath) : undefined
|
|
1037
|
+
const relativePath = toRelativePath(projectRoot, destinationPath)
|
|
1038
|
+
|
|
1039
|
+
if (existingContent && Buffer.compare(existingContent, sourceContent) === 0) {
|
|
1040
|
+
return { relativePath }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
await fsPromises.writeFile(destinationPath, sourceContent)
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
change: {
|
|
1047
|
+
kind: existingContent ? 'updated' : 'created',
|
|
1048
|
+
path: relativePath,
|
|
1049
|
+
},
|
|
1050
|
+
relativePath,
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function writeGeneratedTextFile(
|
|
1055
|
+
projectRoot: string,
|
|
1056
|
+
destinationPath: string,
|
|
1057
|
+
content: string
|
|
1058
|
+
): Promise<GeneratedFileResult> {
|
|
1059
|
+
const existingContent = (await pathExists(destinationPath)) ? await readTextFile(destinationPath) : undefined
|
|
1060
|
+
const relativePath = toRelativePath(projectRoot, destinationPath)
|
|
1061
|
+
|
|
1062
|
+
if (existingContent === content) {
|
|
1063
|
+
return { relativePath }
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
await writeTextFile(destinationPath, content)
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
change: {
|
|
1070
|
+
kind: existingContent === undefined ? 'created' : 'updated',
|
|
1071
|
+
path: relativePath,
|
|
1072
|
+
},
|
|
1073
|
+
relativePath,
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function getLargeImageWarning(imagePath: string, fileName: string): Promise<string | undefined> {
|
|
1078
|
+
const stat = await fsPromises.stat(imagePath)
|
|
1079
|
+
|
|
1080
|
+
if (stat.size < MAX_IMAGE_SIZE_BYTES) {
|
|
1081
|
+
return undefined
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return `Image '${fileName}' is ${stat.size} bytes. Large iOS widget images may not display correctly.`
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function collectGalleryStringsByLocale(widgets: NormalizedIOSWidgetConfig[]): Map<string, Record<string, string>> {
|
|
1088
|
+
const byLocale = new Map<string, Record<string, string>>()
|
|
1089
|
+
|
|
1090
|
+
const add = (locale: string, key: string, value: string): void => {
|
|
1091
|
+
const bucket = byLocale.get(locale) ?? {}
|
|
1092
|
+
bucket[key] = value
|
|
1093
|
+
byLocale.set(locale, bucket)
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
for (const widget of widgets) {
|
|
1097
|
+
if (isWidgetLocalizedMap(widget.displayName)) {
|
|
1098
|
+
for (const [locale, value] of Object.entries(widget.displayName)) {
|
|
1099
|
+
if (value.trim()) {
|
|
1100
|
+
add(locale, `voltra_widget_${widget.id}_displayName`, value)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (isWidgetLocalizedMap(widget.description)) {
|
|
1106
|
+
for (const [locale, value] of Object.entries(widget.description)) {
|
|
1107
|
+
if (value.trim()) {
|
|
1108
|
+
add(locale, `voltra_widget_${widget.id}_description`, value)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return byLocale
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function formatStringsFile(entries: Record<string, string>): string {
|
|
1118
|
+
const sortedKeys = Object.keys(entries).sort()
|
|
1119
|
+
const lines = sortedKeys.map((key) => `${JSON.stringify(key)} = ${JSON.stringify(entries[key])};`)
|
|
1120
|
+
return `/* Voltra widget gallery strings (auto-generated) */\n${lines.join('\n')}\n`
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function widgetLabelEnglish(label: WidgetLabel): string {
|
|
1124
|
+
if (!isWidgetLocalizedMap(label)) {
|
|
1125
|
+
return label
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const englishLabel = label.en
|
|
1129
|
+
if (typeof englishLabel === 'string' && englishLabel.trim()) {
|
|
1130
|
+
return englishLabel
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
for (const [localeKey, value] of Object.entries(label)) {
|
|
1134
|
+
if ((localeKey.startsWith('en-') || localeKey.startsWith('en_')) && value.trim()) {
|
|
1135
|
+
return value
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return Object.values(label).find((value) => value.trim()) ?? ''
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function isWidgetLocalizedMap(label: WidgetLabel): label is Record<string, string> {
|
|
1143
|
+
return typeof label === 'object' && label !== null && !Array.isArray(label)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function sanitizeAssetName(value: string): string {
|
|
1147
|
+
let sanitized = value.replace(/[^A-Za-z0-9_-]/g, '-')
|
|
1148
|
+
sanitized = sanitized.replace(/-+/g, '-')
|
|
1149
|
+
sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, '')
|
|
1150
|
+
if (!/^[A-Za-z]/.test(sanitized)) {
|
|
1151
|
+
sanitized = `asset-${sanitized}`
|
|
1152
|
+
}
|
|
1153
|
+
return sanitized
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function getAssetName(relativeAssetPath: string, seenAssetNames: Map<string, string>): string {
|
|
1157
|
+
const assetName = sanitizeAssetName(path.basename(relativeAssetPath))
|
|
1158
|
+
const existingPath = seenAssetNames.get(assetName)
|
|
1159
|
+
|
|
1160
|
+
if (existingPath && existingPath !== relativeAssetPath) {
|
|
1161
|
+
throw new IOSGeneratedFilesError(
|
|
1162
|
+
`iOS widget assets must have unique basenames. Found both '${existingPath}' and '${relativeAssetPath}' resolving to asset '${assetName}'.`
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
seenAssetNames.set(assetName, relativeAssetPath)
|
|
1167
|
+
return assetName
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function readPlistString(dict: Record<string, unknown>, key: string): string | undefined {
|
|
1171
|
+
const value = dict[key]
|
|
1172
|
+
return typeof value === 'string' && value.trim() ? value : undefined
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function readUrlTypes(dict: Record<string, unknown>): Array<{ CFBundleURLSchemes: string[] }> {
|
|
1176
|
+
const urlTypesValue = dict.CFBundleURLTypes
|
|
1177
|
+
|
|
1178
|
+
if (!Array.isArray(urlTypesValue)) {
|
|
1179
|
+
return []
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return urlTypesValue
|
|
1183
|
+
.map((entry): { CFBundleURLSchemes: string[] } | undefined => {
|
|
1184
|
+
if (!entry || typeof entry !== 'object') {
|
|
1185
|
+
return undefined
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const schemesValue = (entry as { CFBundleURLSchemes?: unknown }).CFBundleURLSchemes
|
|
1189
|
+
|
|
1190
|
+
if (!Array.isArray(schemesValue)) {
|
|
1191
|
+
return undefined
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const schemes = schemesValue.filter(
|
|
1195
|
+
(scheme): scheme is string => typeof scheme === 'string' && scheme.trim().length > 0
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
return schemes.length > 0 ? { CFBundleURLSchemes: schemes } : undefined
|
|
1199
|
+
})
|
|
1200
|
+
.filter((entry): entry is { CFBundleURLSchemes: string[] } => entry !== undefined)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function getSwiftRawStringDelimiter(value: string): string {
|
|
1204
|
+
const matches = value.match(/"#+/g)
|
|
1205
|
+
if (!matches) {
|
|
1206
|
+
return '#'
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const maxHashes = Math.max(...matches.map((match) => match.length - 1))
|
|
1210
|
+
return '#'.repeat(maxHashes + 1)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function mergeResult(
|
|
1214
|
+
result: Pick<GenerateIOSFilesResult, 'changes' | 'files' | 'warnings'>,
|
|
1215
|
+
changes: ReportedChange[],
|
|
1216
|
+
warnings: string[],
|
|
1217
|
+
generatedFiles: Set<string>
|
|
1218
|
+
): void {
|
|
1219
|
+
changes.push(...result.changes)
|
|
1220
|
+
warnings.push(...result.warnings)
|
|
1221
|
+
|
|
1222
|
+
for (const filePath of result.files) {
|
|
1223
|
+
generatedFiles.add(normalizeRelativePath(filePath))
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function mergeSingleResult(result: GeneratedFileResult, changes: ReportedChange[], generatedFiles: Set<string>): void {
|
|
1228
|
+
if (result.change) {
|
|
1229
|
+
changes.push(result.change)
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
generatedFiles.add(normalizeRelativePath(result.relativePath))
|
|
1233
|
+
}
|