voltra 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/apply/index.js +4 -1
- package/build/cjs/apply/index.js.map +1 -1
- package/build/cjs/config/normalize.js +131 -29
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/config/perConfiguration.js +29 -0
- package/build/cjs/config/perConfiguration.js.map +1 -0
- package/build/cjs/config/serverUpdate.js +119 -0
- package/build/cjs/config/serverUpdate.js.map +1 -0
- package/build/cjs/config/widgetKind.js +25 -0
- package/build/cjs/config/widgetKind.js.map +1 -0
- package/build/cjs/dependencies/platformPackages.js +9 -0
- package/build/cjs/dependencies/platformPackages.js.map +1 -1
- package/build/cjs/discovery/ios.js +50 -31
- package/build/cjs/discovery/ios.js.map +1 -1
- package/build/cjs/index.js +6 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +98 -40
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/widgetSizing.js +89 -0
- package/build/cjs/platforms/android/widgetSizing.js.map +1 -0
- package/build/cjs/platforms/ios/apply.js +21 -11
- package/build/cjs/platforms/ios/apply.js.map +1 -1
- package/build/cjs/platforms/ios/buildConfigurationValues.js +73 -0
- package/build/cjs/platforms/ios/buildConfigurationValues.js.map +1 -0
- package/build/cjs/platforms/ios/entitlements.js +34 -19
- package/build/cjs/platforms/ios/entitlements.js.map +1 -1
- package/build/cjs/platforms/ios/generated.js +79 -32
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/plist.js +22 -4
- package/build/cjs/platforms/ios/plist.js.map +1 -1
- package/build/cjs/platforms/ios/targetName.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +166 -23
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +18 -145
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -1
- package/build/esm/apply/index.js +4 -1
- package/build/esm/apply/index.js.map +1 -1
- package/build/esm/config/normalize.js +131 -29
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/config/perConfiguration.js +25 -0
- package/build/esm/config/perConfiguration.js.map +1 -0
- package/build/esm/config/serverUpdate.js +112 -0
- package/build/esm/config/serverUpdate.js.map +1 -0
- package/build/esm/config/widgetKind.js +20 -0
- package/build/esm/config/widgetKind.js.map +1 -0
- package/build/esm/dependencies/platformPackages.js +8 -0
- package/build/esm/dependencies/platformPackages.js.map +1 -1
- package/build/esm/discovery/ios.js +50 -31
- package/build/esm/discovery/ios.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/generated.js +100 -42
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/widgetSizing.js +85 -0
- package/build/esm/platforms/android/widgetSizing.js.map +1 -0
- package/build/esm/platforms/ios/apply.js +21 -11
- package/build/esm/platforms/ios/apply.js.map +1 -1
- package/build/esm/platforms/ios/buildConfigurationValues.js +68 -0
- package/build/esm/platforms/ios/buildConfigurationValues.js.map +1 -0
- package/build/esm/platforms/ios/entitlements.js +34 -19
- package/build/esm/platforms/ios/entitlements.js.map +1 -1
- package/build/esm/platforms/ios/generated.js +80 -33
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/plist.js +22 -4
- package/build/esm/platforms/ios/plist.js.map +1 -1
- package/build/esm/platforms/ios/targetName.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +166 -23
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +16 -107
- package/build/esm/platforms/shared/widgetModule.js.map +1 -1
- package/build/types/apply/index.d.ts.map +1 -1
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/perConfiguration.d.ts +18 -0
- package/build/types/config/perConfiguration.d.ts.map +1 -0
- package/build/types/config/serverUpdate.d.ts +80 -0
- package/build/types/config/serverUpdate.d.ts.map +1 -0
- package/build/types/config/types.d.ts +91 -30
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/config/widgetKind.d.ts +11 -0
- package/build/types/config/widgetKind.d.ts.map +1 -0
- package/build/types/dependencies/platformPackages.d.ts +1 -0
- package/build/types/dependencies/platformPackages.d.ts.map +1 -1
- package/build/types/discovery/ios.d.ts +10 -0
- package/build/types/discovery/ios.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/generated.d.ts.map +1 -1
- package/build/types/platforms/android/widgetSizing.d.ts +50 -0
- package/build/types/platforms/android/widgetSizing.d.ts.map +1 -0
- package/build/types/platforms/ios/apply.d.ts.map +1 -1
- package/build/types/platforms/ios/buildConfigurationValues.d.ts +33 -0
- package/build/types/platforms/ios/buildConfigurationValues.d.ts.map +1 -0
- package/build/types/platforms/ios/entitlements.d.ts +4 -4
- package/build/types/platforms/ios/entitlements.d.ts.map +1 -1
- package/build/types/platforms/ios/generated.d.ts +14 -2
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/plist.d.ts +3 -3
- package/build/types/platforms/ios/plist.d.ts.map +1 -1
- package/build/types/platforms/ios/podfile.d.ts +2 -2
- package/build/types/platforms/ios/podfile.d.ts.map +1 -1
- package/build/types/platforms/ios/targetName.d.ts +2 -2
- package/build/types/platforms/ios/targetName.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts +4 -2
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +12 -3
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -1
- package/package.json +5 -17
- package/src/apply/index.ts +4 -1
- package/src/config/normalize.node.test.ts +61 -0
- package/src/config/normalize.ts +188 -44
- package/src/config/perConfiguration.ts +49 -0
- package/src/config/serverUpdate.ts +160 -0
- package/src/config/types.ts +97 -30
- package/src/config/widgetKind.node.test.ts +27 -0
- package/src/config/widgetKind.ts +27 -0
- package/src/dependencies/platformPackages.ts +11 -0
- package/src/discovery/ios.ts +104 -60
- package/src/index.ts +8 -0
- package/src/platforms/android/generated.ts +144 -47
- package/src/platforms/android/widgetSizing.ts +112 -0
- package/src/platforms/ios/apply.ts +21 -11
- package/src/platforms/ios/buildConfigurationValues.ts +105 -0
- package/src/platforms/ios/entitlements.ts +55 -31
- package/src/platforms/ios/generated.node.test.ts +61 -0
- package/src/platforms/ios/generated.ts +137 -38
- package/src/platforms/ios/plist.ts +36 -7
- package/src/platforms/ios/podfile.ts +2 -2
- package/src/platforms/ios/targetName.ts +2 -2
- package/src/platforms/ios/xcodeTarget.ts +248 -43
- package/src/platforms/shared/widgetModule.ts +22 -140
|
@@ -3,20 +3,23 @@ import fsPromises from 'node:fs/promises'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
|
|
6
|
-
import { requirePlatformPackage } from '../../dependencies/platformPackages'
|
|
6
|
+
import { getPlatformClientPackageVersion, requirePlatformPackage } from '../../dependencies/platformPackages'
|
|
7
7
|
import { ensureDirectory, pathExists, readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
8
8
|
import { normalizeRelativePath, toRelativePath } from '../../fs/path'
|
|
9
9
|
import { VoltraCliError } from '../../reporting/summary'
|
|
10
|
-
import {
|
|
10
|
+
import { createDynamicWidgetBuildInfo, createGeneratedWidgetModuleLoader } from '../shared/widgetModule'
|
|
11
|
+
import { iosWidgetKindOverrides, iosWidgetKind } from '../../config/widgetKind'
|
|
11
12
|
import { buildPlistXml, parsePlistFile } from './plist'
|
|
12
13
|
import { resolveIOSWidgetTargetName } from './targetName'
|
|
13
14
|
|
|
15
|
+
import type { WidgetModuleLoader } from '@use-voltra/compiler'
|
|
16
|
+
|
|
14
17
|
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
15
18
|
import type {
|
|
16
19
|
IOSWidgetAppIntentParameter,
|
|
17
20
|
IOSWidgetFamily,
|
|
18
21
|
NormalizedIOSWidgetConfig,
|
|
19
|
-
|
|
22
|
+
ResolvedVoltraIOSConfig,
|
|
20
23
|
WidgetLabel,
|
|
21
24
|
} from '../../config/types'
|
|
22
25
|
import type { ReportedChange } from '../../reporting/summary'
|
|
@@ -99,7 +102,7 @@ const GENERATED_INITIAL_STATE_LOCALE_HELPER = `private enum VoltraGeneratedIniti
|
|
|
99
102
|
|
|
100
103
|
export interface GenerateIOSFilesOptions {
|
|
101
104
|
projectRoot: string
|
|
102
|
-
ios:
|
|
105
|
+
ios: ResolvedVoltraIOSConfig
|
|
103
106
|
discovery: IOSProjectDiscovery
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -135,7 +138,7 @@ type DynamicWidgetManifest = {
|
|
|
135
138
|
widgets: Array<{ id: string; entry: string }>
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
type DetectedIOSWidget =
|
|
141
|
+
export type DetectedIOSWidget =
|
|
139
142
|
| (NormalizedIOSWidgetConfig & { clientRendered: false })
|
|
140
143
|
| (NormalizedIOSWidgetConfig & { entry: string; clientRendered: true; clientSourcePath: string })
|
|
141
144
|
|
|
@@ -152,13 +155,20 @@ function createGeneratedFilesError(message: string): IOSGeneratedFilesError {
|
|
|
152
155
|
|
|
153
156
|
export async function generateIOSFiles(options: GenerateIOSFilesOptions): Promise<GenerateIOSFilesResult> {
|
|
154
157
|
const { projectRoot, ios, discovery } = options
|
|
158
|
+
const voltraVersion = getPlatformClientPackageVersion(projectRoot, 'ios')
|
|
155
159
|
const targetName = resolveIOSWidgetTargetName(ios, discovery)
|
|
156
160
|
const targetPath = path.join(discovery.iosRoot, targetName)
|
|
157
|
-
const detectedWidgets = detectClientRenderedWidgets(projectRoot, ios.widgets)
|
|
158
161
|
const mainAppMetadata = await readMainAppMetadata(discovery.infoPlistPath)
|
|
159
162
|
const changes: ReportedChange[] = []
|
|
160
|
-
const warnings: string[] = []
|
|
163
|
+
const warnings: string[] = [...(await getDivergentMainAppMetadataWarnings(discovery, mainAppMetadata))]
|
|
161
164
|
const generatedFiles = new Set<string>()
|
|
165
|
+
const widgetModuleLoader = createGeneratedWidgetModuleLoader(
|
|
166
|
+
projectRoot,
|
|
167
|
+
'ios',
|
|
168
|
+
createGeneratedFilesError,
|
|
169
|
+
(message) => warnings.push(message)
|
|
170
|
+
)
|
|
171
|
+
const detectedWidgets = detectClientRenderedWidgets(projectRoot, widgetModuleLoader, ios.widgets)
|
|
162
172
|
|
|
163
173
|
mergeSingleResult(
|
|
164
174
|
await writeGeneratedTextFile(
|
|
@@ -176,7 +186,8 @@ export async function generateIOSFiles(options: GenerateIOSFilesOptions): Promis
|
|
|
176
186
|
targetName,
|
|
177
187
|
ios,
|
|
178
188
|
mainAppMetadata,
|
|
179
|
-
detectedWidgets
|
|
189
|
+
detectedWidgets,
|
|
190
|
+
voltraVersion
|
|
180
191
|
)
|
|
181
192
|
mergeSingleResult(infoPlistResult, changes, generatedFiles)
|
|
182
193
|
|
|
@@ -189,7 +200,12 @@ export async function generateIOSFiles(options: GenerateIOSFilesOptions): Promis
|
|
|
189
200
|
const fontsResult = await copyIOSFonts(projectRoot, targetPath, ios.fonts)
|
|
190
201
|
mergeResult(fontsResult, changes, warnings, generatedFiles)
|
|
191
202
|
|
|
192
|
-
const initialStatesResult = await generateInitialStatesSwift(
|
|
203
|
+
const initialStatesResult = await generateInitialStatesSwift(
|
|
204
|
+
projectRoot,
|
|
205
|
+
widgetModuleLoader,
|
|
206
|
+
detectedWidgets,
|
|
207
|
+
voltraVersion
|
|
208
|
+
)
|
|
193
209
|
mergeSingleResult(
|
|
194
210
|
await writeGeneratedTextFile(
|
|
195
211
|
projectRoot,
|
|
@@ -223,15 +239,22 @@ async function generateInfoPlistFile(
|
|
|
223
239
|
projectRoot: string,
|
|
224
240
|
targetPath: string,
|
|
225
241
|
targetName: string,
|
|
226
|
-
ios:
|
|
242
|
+
ios: ResolvedVoltraIOSConfig,
|
|
227
243
|
mainAppMetadata: MainAppMetadata,
|
|
228
|
-
widgets: DetectedIOSWidget[]
|
|
244
|
+
widgets: DetectedIOSWidget[],
|
|
245
|
+
voltraVersion: string
|
|
229
246
|
): Promise<GeneratedFileResult> {
|
|
230
247
|
const plistPath = path.join(targetPath, 'Info.plist')
|
|
231
248
|
const fontNames = ios.fonts.map((fontPath) => path.basename(fontPath)).sort()
|
|
232
249
|
const serverWidgets = widgets.filter((widget) => widget.serverUpdate)
|
|
233
250
|
const hasClientRenderedWidget = widgets.some((widget) => widget.clientRendered)
|
|
234
|
-
|
|
251
|
+
// Keys of the intervals dictionary are the set of server-driven widget ids; a URL appears only
|
|
252
|
+
// when app.json set one, because it may instead arrive at runtime via setWidgetServerUpdate.
|
|
253
|
+
const serverUrls = Object.fromEntries(
|
|
254
|
+
serverWidgets
|
|
255
|
+
.filter((widget) => widget.serverUpdate?.url !== undefined)
|
|
256
|
+
.map((widget) => [widget.id, widget.serverUpdate?.url])
|
|
257
|
+
)
|
|
235
258
|
const serverIntervals = Object.fromEntries(
|
|
236
259
|
serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.intervalMinutes])
|
|
237
260
|
)
|
|
@@ -257,6 +280,10 @@ async function generateInfoPlistFile(
|
|
|
257
280
|
UIAppFonts: fontNames.length > 0 ? fontNames : undefined,
|
|
258
281
|
Voltra_AppGroupIdentifier: ios.groupIdentifier,
|
|
259
282
|
Voltra_KeychainGroup: ios.keychainGroup,
|
|
283
|
+
Voltra_Version: voltraVersion,
|
|
284
|
+
// The extension resolves its own kinds too, and Bundle.main there is the extension bundle,
|
|
285
|
+
// so the map has to be written here as well as into the app's Info.plist.
|
|
286
|
+
Voltra_WidgetKinds: iosWidgetKindOverrides(widgets),
|
|
260
287
|
Voltra_WidgetServerIntervals: Object.keys(serverIntervals).length > 0 ? serverIntervals : undefined,
|
|
261
288
|
Voltra_WidgetServerRefresh: Object.keys(serverRefresh).length > 0 ? serverRefresh : undefined,
|
|
262
289
|
NSAppTransportSecurity: createWidgetAppTransportSecurity(hasClientRenderedWidget, serverWidgets.length > 0),
|
|
@@ -296,7 +323,7 @@ async function generateEntitlementsFile(
|
|
|
296
323
|
projectRoot: string,
|
|
297
324
|
targetPath: string,
|
|
298
325
|
targetName: string,
|
|
299
|
-
ios:
|
|
326
|
+
ios: ResolvedVoltraIOSConfig
|
|
300
327
|
): Promise<GeneratedFileResult> {
|
|
301
328
|
const entitlementsPath = path.join(targetPath, `${targetName}.entitlements`)
|
|
302
329
|
const entitlements = buildPlistXml(
|
|
@@ -418,6 +445,41 @@ async function generateLocalizedWidgetStrings(
|
|
|
418
445
|
}
|
|
419
446
|
}
|
|
420
447
|
|
|
448
|
+
/**
|
|
449
|
+
* The widget extension has a single Info.plist, so its version has to come from one of the app's.
|
|
450
|
+
* An app whose build configurations disagree on the version would ship an extension that matches
|
|
451
|
+
* only some of them, which App Store validation rejects, so say so rather than picking silently.
|
|
452
|
+
*/
|
|
453
|
+
async function getDivergentMainAppMetadataWarnings(
|
|
454
|
+
discovery: IOSProjectDiscovery,
|
|
455
|
+
mainAppMetadata: MainAppMetadata
|
|
456
|
+
): Promise<string[]> {
|
|
457
|
+
const warnings: string[] = []
|
|
458
|
+
|
|
459
|
+
for (const infoPlistPath of discovery.infoPlistPaths.slice(1)) {
|
|
460
|
+
const metadata = await readMainAppMetadata(infoPlistPath)
|
|
461
|
+
|
|
462
|
+
if (
|
|
463
|
+
metadata.shortVersionString === mainAppMetadata.shortVersionString &&
|
|
464
|
+
metadata.buildNumber === mainAppMetadata.buildNumber
|
|
465
|
+
) {
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
warnings.push(
|
|
470
|
+
`${toRelativePath(discovery.iosRoot, infoPlistPath)} sets version ${metadata.shortVersionString} (${
|
|
471
|
+
metadata.buildNumber
|
|
472
|
+
}) while ${toRelativePath(discovery.iosRoot, discovery.infoPlistPath)} sets ${
|
|
473
|
+
mainAppMetadata.shortVersionString
|
|
474
|
+
} (${
|
|
475
|
+
mainAppMetadata.buildNumber
|
|
476
|
+
}). The widget extension is generated with the version of the default build configuration. Set ios.project.infoPlistPath to choose a different one.`
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return warnings
|
|
481
|
+
}
|
|
482
|
+
|
|
421
483
|
async function readMainAppMetadata(infoPlistPath: string): Promise<MainAppMetadata> {
|
|
422
484
|
const dict = await parsePlistFile(
|
|
423
485
|
infoPlistPath,
|
|
@@ -435,17 +497,22 @@ async function readMainAppMetadata(infoPlistPath: string): Promise<MainAppMetada
|
|
|
435
497
|
}
|
|
436
498
|
}
|
|
437
499
|
|
|
438
|
-
async function generateInitialStatesSwift(
|
|
500
|
+
async function generateInitialStatesSwift(
|
|
501
|
+
projectRoot: string,
|
|
502
|
+
loader: WidgetModuleLoader,
|
|
503
|
+
widgets: DetectedIOSWidget[],
|
|
504
|
+
voltraVersion: string
|
|
505
|
+
): Promise<string> {
|
|
439
506
|
const serverWidgets = widgets.filter(
|
|
440
507
|
(widget): widget is Extract<DetectedIOSWidget, { clientRendered: false }> => !widget.clientRendered
|
|
441
508
|
)
|
|
442
509
|
const prerenderableServerWidgets = serverWidgets.filter((widget) => widget.initialStatePath)
|
|
443
510
|
const serverStates = await prerenderWidgetStates(
|
|
444
|
-
|
|
511
|
+
loader,
|
|
445
512
|
prerenderableServerWidgets,
|
|
446
513
|
loadIOSWidgetRenderer(projectRoot)
|
|
447
514
|
)
|
|
448
|
-
const clientStates = await prerenderClientRenderedWidgets(projectRoot, widgets)
|
|
515
|
+
const clientStates = await prerenderClientRenderedWidgets(projectRoot, loader, widgets, voltraVersion)
|
|
449
516
|
const prerenderedStates = new Map([...serverStates, ...clientStates])
|
|
450
517
|
|
|
451
518
|
if (prerenderedStates.size === 0) {
|
|
@@ -519,7 +586,7 @@ function generateWidgetBundleSwift(widgets: DetectedIOSWidget[]): string {
|
|
|
519
586
|
appIntentWidgets.length > 0 ? 'import AppIntents' : undefined,
|
|
520
587
|
'import SwiftUI',
|
|
521
588
|
'import WidgetKit',
|
|
522
|
-
'import
|
|
589
|
+
'import VoltraRuntime',
|
|
523
590
|
]
|
|
524
591
|
.filter((value): value is string => value !== undefined)
|
|
525
592
|
.join('\n')
|
|
@@ -585,9 +652,14 @@ function generateWidgetStruct(widget: DetectedIOSWidget): string {
|
|
|
585
652
|
const familiesSwift = widget.supportedFamilies.map((family) => IOS_WIDGET_FAMILY_MAP[family]).join(', ')
|
|
586
653
|
const displayNameExpr = createSwiftLabelExpression(widget.id, 'displayName', widget.displayName)
|
|
587
654
|
const descriptionExpr = createSwiftLabelExpression(widget.id, 'description', widget.description)
|
|
655
|
+
// `entry` picks the render engine and `serverUpdate` picks where the data comes from, so the two
|
|
656
|
+
// keys together select one of four providers. Runtime code never asks "is this server-driven?" —
|
|
657
|
+
// the answer is baked in here, once, at generate time.
|
|
588
658
|
const providerAndContent = widget.clientRendered
|
|
589
659
|
? [
|
|
590
|
-
|
|
660
|
+
` provider: ${
|
|
661
|
+
widget.serverUpdate ? 'VoltraDynamicWidgetServerUpdateProvider' : 'VoltraClientWidgetProvider'
|
|
662
|
+
}(`,
|
|
591
663
|
' widgetId: widgetId,',
|
|
592
664
|
' initialState: VoltraWidgetInitialStates.getInitialState(for: widgetId)',
|
|
593
665
|
' )',
|
|
@@ -614,7 +686,7 @@ function generateWidgetStruct(widget: DetectedIOSWidget): string {
|
|
|
614
686
|
'',
|
|
615
687
|
' public var body: some WidgetConfiguration {',
|
|
616
688
|
' StaticConfiguration(',
|
|
617
|
-
` kind: ${JSON.stringify(
|
|
689
|
+
` kind: ${JSON.stringify(iosWidgetKind(widget))},`,
|
|
618
690
|
providerAndContent,
|
|
619
691
|
' }',
|
|
620
692
|
` .configurationDisplayName(${displayNameExpr})`,
|
|
@@ -664,6 +736,22 @@ function generateClientAppIntentWidgetCode(
|
|
|
664
736
|
const defaultDictionary = createSwiftDictionaryLiteral(
|
|
665
737
|
widget.appIntent.parameters.map((parameter) => `${JSON.stringify(parameter.name)}: ${swiftDefaultValue(parameter)}`)
|
|
666
738
|
)
|
|
739
|
+
// A server-driven Dynamic Widget fetches on every timeline request and schedules the next one
|
|
740
|
+
// from its resolved interval; a plain one has nothing to ask again for, so its policy is .never.
|
|
741
|
+
const timelineBody = widget.serverUpdate
|
|
742
|
+
? [
|
|
743
|
+
' return await VoltraDynamicWidgetServerUpdateProvider.timeline(',
|
|
744
|
+
' widgetId: widgetId,',
|
|
745
|
+
' family: context.family,',
|
|
746
|
+
` configuration: ${configuredDictionary}`,
|
|
747
|
+
' )',
|
|
748
|
+
' }',
|
|
749
|
+
]
|
|
750
|
+
: [
|
|
751
|
+
` let entry = await VoltraClientWidgetProvider.loadEntry(widgetId: widgetId, configuration: ${configuredDictionary})`,
|
|
752
|
+
' return Timeline(entries: [entry], policy: .never)',
|
|
753
|
+
' }',
|
|
754
|
+
]
|
|
667
755
|
|
|
668
756
|
return [
|
|
669
757
|
`@available(iOS 17.0, *)`,
|
|
@@ -693,10 +781,8 @@ function generateClientAppIntentWidgetCode(
|
|
|
693
781
|
` await VoltraClientWidgetProvider.loadEntry(widgetId: widgetId, configuration: ${configuredDictionary})`,
|
|
694
782
|
' }',
|
|
695
783
|
'',
|
|
696
|
-
` func timeline(for configuration: ${intentName}, in
|
|
697
|
-
|
|
698
|
-
' return Timeline(entries: [entry], policy: .never)',
|
|
699
|
-
' }',
|
|
784
|
+
` func timeline(for configuration: ${intentName}, in context: Context) async -> Timeline<VoltraClientWidgetEntry> {`,
|
|
785
|
+
...timelineBody,
|
|
700
786
|
'}',
|
|
701
787
|
'',
|
|
702
788
|
`@available(iOS 17.0, *)`,
|
|
@@ -707,7 +793,7 @@ function generateClientAppIntentWidgetCode(
|
|
|
707
793
|
'',
|
|
708
794
|
' public var body: some WidgetConfiguration {',
|
|
709
795
|
' AppIntentConfiguration(',
|
|
710
|
-
` kind: ${JSON.stringify(
|
|
796
|
+
` kind: ${JSON.stringify(iosWidgetKind(widget))},`,
|
|
711
797
|
` intent: ${intentName}.self,`,
|
|
712
798
|
` provider: ${providerName}()`,
|
|
713
799
|
' ) { entry in',
|
|
@@ -751,7 +837,7 @@ function createSwiftDictionaryLiteral(entries: string[]): string {
|
|
|
751
837
|
}
|
|
752
838
|
|
|
753
839
|
async function prerenderWidgetStates(
|
|
754
|
-
|
|
840
|
+
loader: WidgetModuleLoader,
|
|
755
841
|
widgets: NormalizedIOSWidgetConfig[],
|
|
756
842
|
renderer: IOSWidgetRenderer
|
|
757
843
|
): Promise<PrerenderedWidgetStates> {
|
|
@@ -773,7 +859,7 @@ async function prerenderWidgetStates(
|
|
|
773
859
|
throw new IOSGeneratedFilesError(`Initial state file not found for widget '${widget.id}' at ${modulePath}`)
|
|
774
860
|
}
|
|
775
861
|
|
|
776
|
-
const widgetVariants = evaluateLegacyWidgetModule(
|
|
862
|
+
const widgetVariants = evaluateLegacyWidgetModule(loader, modulePath)
|
|
777
863
|
localeStates.set(localeKey, renderer(widgetVariants))
|
|
778
864
|
}
|
|
779
865
|
|
|
@@ -783,9 +869,8 @@ async function prerenderWidgetStates(
|
|
|
783
869
|
return prerenderedStates
|
|
784
870
|
}
|
|
785
871
|
|
|
786
|
-
function evaluateLegacyWidgetModule(
|
|
787
|
-
const
|
|
788
|
-
const widgetVariants = exports.default ?? exports
|
|
872
|
+
function evaluateLegacyWidgetModule(loader: WidgetModuleLoader, filePath: string): WidgetVariants {
|
|
873
|
+
const widgetVariants = loader.loadDefaultExport(filePath)
|
|
789
874
|
|
|
790
875
|
if (!widgetVariants || typeof widgetVariants !== 'object') {
|
|
791
876
|
throw new IOSGeneratedFilesError(`Widget file must export widget variants: ${filePath}`)
|
|
@@ -827,11 +912,19 @@ function createDynamicWidgetsManifest(widgets: DetectedIOSWidget[]): DynamicWidg
|
|
|
827
912
|
}
|
|
828
913
|
}
|
|
829
914
|
|
|
830
|
-
function detectClientRenderedWidgets(
|
|
831
|
-
|
|
915
|
+
function detectClientRenderedWidgets(
|
|
916
|
+
projectRoot: string,
|
|
917
|
+
loader: WidgetModuleLoader,
|
|
918
|
+
widgets: NormalizedIOSWidgetConfig[]
|
|
919
|
+
): DetectedIOSWidget[] {
|
|
920
|
+
return widgets.map((widget) => detectSingleWidget(projectRoot, loader, widget))
|
|
832
921
|
}
|
|
833
922
|
|
|
834
|
-
function detectSingleWidget(
|
|
923
|
+
function detectSingleWidget(
|
|
924
|
+
projectRoot: string,
|
|
925
|
+
loader: WidgetModuleLoader,
|
|
926
|
+
widget: NormalizedIOSWidgetConfig
|
|
927
|
+
): DetectedIOSWidget {
|
|
835
928
|
if (!widget.entry) {
|
|
836
929
|
return {
|
|
837
930
|
...widget,
|
|
@@ -845,7 +938,7 @@ function detectSingleWidget(projectRoot: string, widget: NormalizedIOSWidgetConf
|
|
|
845
938
|
throw createGeneratedFilesError(`[voltra] Dynamic Widget "${widget.id}" entry not found at ${widget.entry}`)
|
|
846
939
|
}
|
|
847
940
|
|
|
848
|
-
const widgetModule = safelyEvaluateDynamicWidget(
|
|
941
|
+
const widgetModule = safelyEvaluateDynamicWidget(loader, widget.id, widget.entry, clientSourcePath)
|
|
849
942
|
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
850
943
|
|
|
851
944
|
if (typeof widgetFn !== 'function') {
|
|
@@ -864,7 +957,9 @@ function detectSingleWidget(projectRoot: string, widget: NormalizedIOSWidgetConf
|
|
|
864
957
|
|
|
865
958
|
async function prerenderClientRenderedWidgets(
|
|
866
959
|
projectRoot: string,
|
|
867
|
-
|
|
960
|
+
loader: WidgetModuleLoader,
|
|
961
|
+
widgets: DetectedIOSWidget[],
|
|
962
|
+
voltraVersion: string
|
|
868
963
|
): Promise<PrerenderedWidgetStates> {
|
|
869
964
|
const clientWidgets = widgets.filter(
|
|
870
965
|
(widget): widget is Extract<DetectedIOSWidget, { clientRendered: true }> => widget.clientRendered
|
|
@@ -883,13 +978,13 @@ async function prerenderClientRenderedWidgets(
|
|
|
883
978
|
widgetRenderingMode: 'fullColor',
|
|
884
979
|
showsWidgetContainerBackground: true,
|
|
885
980
|
configuration: undefined,
|
|
886
|
-
build:
|
|
981
|
+
build: createDynamicWidgetBuildInfo(voltraVersion),
|
|
887
982
|
}
|
|
888
983
|
const prerenderedStates: PrerenderedWidgetStates = new Map()
|
|
889
984
|
|
|
890
985
|
for (const widget of clientWidgets) {
|
|
891
986
|
try {
|
|
892
|
-
const widgetModule = safelyEvaluateDynamicWidget(
|
|
987
|
+
const widgetModule = safelyEvaluateDynamicWidget(loader, widget.id, widget.entry, widget.clientSourcePath)
|
|
893
988
|
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
894
989
|
const element = widgetFn({}, placeholderEnv)
|
|
895
990
|
prerenderedStates.set(widget.id, new Map([[DEFAULT_INITIAL_STATE_LOCALE, JSON.stringify(renderer(element))]]))
|
|
@@ -910,13 +1005,13 @@ async function prerenderClientRenderedWidgets(
|
|
|
910
1005
|
}
|
|
911
1006
|
|
|
912
1007
|
function safelyEvaluateDynamicWidget(
|
|
913
|
-
|
|
1008
|
+
loader: WidgetModuleLoader,
|
|
914
1009
|
widgetId: string,
|
|
915
1010
|
widgetEntry: string,
|
|
916
1011
|
clientSourcePath: string
|
|
917
1012
|
): unknown {
|
|
918
1013
|
try {
|
|
919
|
-
return
|
|
1014
|
+
return loader.load(clientSourcePath)
|
|
920
1015
|
} catch (error) {
|
|
921
1016
|
if (error instanceof IOSGeneratedFilesError) {
|
|
922
1017
|
throw error
|
|
@@ -1231,3 +1326,7 @@ function mergeSingleResult(result: GeneratedFileResult, changes: ReportedChange[
|
|
|
1231
1326
|
|
|
1232
1327
|
generatedFiles.add(normalizeRelativePath(result.relativePath))
|
|
1233
1328
|
}
|
|
1329
|
+
|
|
1330
|
+
export const __test__ = {
|
|
1331
|
+
generateWidgetBundleSwift,
|
|
1332
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { parseStringPromise } from 'xml2js'
|
|
2
2
|
|
|
3
|
+
import { iosWidgetKindOverrides } from '../../config/widgetKind'
|
|
3
4
|
import { readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
4
5
|
import { toRelativePath } from '../../fs/path'
|
|
5
6
|
import { VoltraCliError } from '../../reporting/summary'
|
|
6
7
|
|
|
7
8
|
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
8
|
-
import type {
|
|
9
|
+
import type { ResolvedVoltraIOSConfig } from '../../config/types'
|
|
9
10
|
import type { ReportedChange } from '../../reporting/summary'
|
|
10
11
|
|
|
11
12
|
interface OrderedPlistNode {
|
|
@@ -23,12 +24,12 @@ type PlistErrorFactory = (message: string) => Error
|
|
|
23
24
|
|
|
24
25
|
export interface EnsureInfoPlistOptions {
|
|
25
26
|
projectRoot: string
|
|
26
|
-
ios:
|
|
27
|
+
ios: ResolvedVoltraIOSConfig
|
|
27
28
|
discovery: IOSProjectDiscovery
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface EnsureInfoPlistResult {
|
|
31
|
-
|
|
32
|
+
changes: ReportedChange[]
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export class IOSInfoPlistMutationError extends VoltraCliError {
|
|
@@ -40,7 +41,26 @@ export class IOSInfoPlistMutationError extends VoltraCliError {
|
|
|
40
41
|
|
|
41
42
|
export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<EnsureInfoPlistResult> {
|
|
42
43
|
const { projectRoot, ios, discovery } = options
|
|
43
|
-
const
|
|
44
|
+
const changes: ReportedChange[] = []
|
|
45
|
+
|
|
46
|
+
// Build configurations of a multi-environment app can each reference their own Info.plist.
|
|
47
|
+
for (const infoPlistPath of discovery.infoPlistPaths) {
|
|
48
|
+
const change = await ensureSingleInfoPlist(projectRoot, ios, infoPlistPath)
|
|
49
|
+
|
|
50
|
+
if (change) {
|
|
51
|
+
changes.push(change)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { changes }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function ensureSingleInfoPlist(
|
|
59
|
+
projectRoot: string,
|
|
60
|
+
ios: ResolvedVoltraIOSConfig,
|
|
61
|
+
infoPlistPath: string
|
|
62
|
+
): Promise<ReportedChange | undefined> {
|
|
63
|
+
const infoPlist = await parsePlistFile(infoPlistPath, 'main app Info.plist', createInfoPlistError)
|
|
44
64
|
|
|
45
65
|
infoPlist.NSSupportsLiveActivities = true
|
|
46
66
|
infoPlist.NSSupportsLiveActivitiesFrequentUpdates = false
|
|
@@ -52,8 +72,18 @@ export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<
|
|
|
52
72
|
const widgetIds = ios.widgets.map((widget) => widget.id)
|
|
53
73
|
setOrDeleteVoltraKey(infoPlist, 'Voltra_WidgetIds', widgetIds.length > 0 ? widgetIds : undefined)
|
|
54
74
|
|
|
75
|
+
// Widgets that pin a custom WidgetKit kind, so VoltraWidgetKind can map ids to kinds and back.
|
|
76
|
+
setOrDeleteVoltraKey(infoPlist, 'Voltra_WidgetKinds', iosWidgetKindOverrides(ios.widgets))
|
|
77
|
+
|
|
78
|
+
// Every server-driven widget gets an interval, so this dictionary's keys are the set of
|
|
79
|
+
// server-driven widget ids the runtime settings store validates against. A URL is written only
|
|
80
|
+
// when app.json set one; otherwise the app supplies it with setWidgetServerUpdate.
|
|
55
81
|
const serverWidgets = ios.widgets.filter((widget) => widget.serverUpdate)
|
|
56
|
-
const serverUrls = Object.fromEntries(
|
|
82
|
+
const serverUrls = Object.fromEntries(
|
|
83
|
+
serverWidgets
|
|
84
|
+
.filter((widget) => widget.serverUpdate?.url !== undefined)
|
|
85
|
+
.map((widget) => [widget.id, widget.serverUpdate?.url])
|
|
86
|
+
)
|
|
57
87
|
const serverIntervals = Object.fromEntries(
|
|
58
88
|
serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.intervalMinutes])
|
|
59
89
|
)
|
|
@@ -70,9 +100,8 @@ export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<
|
|
|
70
100
|
)
|
|
71
101
|
|
|
72
102
|
const nextContent = buildPlistXml(infoPlist, createInfoPlistError)
|
|
73
|
-
const change = await writePlistIfChanged(projectRoot, discovery.infoPlistPath, nextContent)
|
|
74
103
|
|
|
75
|
-
return
|
|
104
|
+
return await writePlistIfChanged(projectRoot, infoPlistPath, nextContent)
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
export async function parsePlistFile(
|
|
@@ -8,7 +8,7 @@ import { resolveIOSWidgetTargetName } from './targetName'
|
|
|
8
8
|
import { VOLTRA_MIN_IOS_DEPLOYMENT_TARGET, isIOSDeploymentTargetAtLeast } from './deploymentTarget'
|
|
9
9
|
|
|
10
10
|
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
11
|
-
import type {
|
|
11
|
+
import type { ResolvedVoltraIOSConfig } from '../../config/types'
|
|
12
12
|
import type { ReportedChange } from '../../reporting/summary'
|
|
13
13
|
|
|
14
14
|
const VOLTRA_MANAGED_BLOCK_START = '# >>> VOLTRA MANAGED BLOCK: widget extension target'
|
|
@@ -18,7 +18,7 @@ const LEGACY_BLOCK_NOTICE = '# DO NOT MODIFY THIS FILE - IT IS AUTO-GENERATED BY
|
|
|
18
18
|
|
|
19
19
|
export interface EnsurePodfileBlockOptions {
|
|
20
20
|
projectRoot: string
|
|
21
|
-
ios:
|
|
21
|
+
ios: ResolvedVoltraIOSConfig
|
|
22
22
|
discovery: IOSProjectDiscovery
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResolvedVoltraIOSConfig } from '../../config/types'
|
|
3
3
|
|
|
4
|
-
export function resolveIOSWidgetTargetName(ios:
|
|
4
|
+
export function resolveIOSWidgetTargetName(ios: ResolvedVoltraIOSConfig, discovery: IOSProjectDiscovery): string {
|
|
5
5
|
if (ios.targetName) {
|
|
6
6
|
return ios.targetName
|
|
7
7
|
}
|