voltra 2.2.0 → 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 -5
- 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
package/src/discovery/ios.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import type { Stats } from 'node:fs'
|
|
5
5
|
|
|
6
6
|
import { VoltraCliError } from '../reporting/summary'
|
|
7
|
+
import { getValuesByBuildConfiguration, isPerConfigurationMap } from '../config/perConfiguration'
|
|
7
8
|
|
|
8
9
|
import type { NormalizedIOSProjectConfig } from '../config/types'
|
|
9
10
|
|
|
@@ -19,8 +20,18 @@ export interface IOSProjectDiscovery {
|
|
|
19
20
|
podfilePath: string
|
|
20
21
|
mainTargetName: string
|
|
21
22
|
mainTargetCandidates: string[]
|
|
23
|
+
/** Build configuration names of the app target, default build configuration first. */
|
|
24
|
+
buildConfigurationNames: string[]
|
|
25
|
+
/** Info.plist of the default build configuration. Always the first entry of `infoPlistPaths`. */
|
|
22
26
|
infoPlistPath: string
|
|
27
|
+
/** Every distinct Info.plist the app target references, default build configuration first. */
|
|
28
|
+
infoPlistPaths: string[]
|
|
29
|
+
/** Entitlements file of the default build configuration, when the project references one. */
|
|
23
30
|
entitlementsPath?: string
|
|
31
|
+
/** Every distinct entitlements file the app target references, default build configuration first. */
|
|
32
|
+
entitlementsPaths: string[]
|
|
33
|
+
/** Entitlements file per build configuration, when the config file names one per configuration. */
|
|
34
|
+
entitlementsPathByConfiguration?: Map<string, string>
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
interface ParsedPbxNativeTarget {
|
|
@@ -72,8 +83,13 @@ export async function discoverIOSProject(
|
|
|
72
83
|
const mainTargetCandidates = getMainTargetCandidates(parsedProject.targets)
|
|
73
84
|
const mainTarget = resolveMainTarget(mainTargetCandidates, config.mainTargetName, pbxprojPath)
|
|
74
85
|
const mainTargetBuildConfigurations = getTargetBuildConfigurations(parsedProject, mainTarget, pbxprojPath)
|
|
75
|
-
const
|
|
76
|
-
const
|
|
86
|
+
const infoPlistPaths = await resolveInfoPlistPaths(iosRoot, config, mainTarget, mainTargetBuildConfigurations)
|
|
87
|
+
const { entitlementsPaths, entitlementsPathByConfiguration } = await resolveEntitlements(
|
|
88
|
+
iosRoot,
|
|
89
|
+
config,
|
|
90
|
+
mainTarget,
|
|
91
|
+
mainTargetBuildConfigurations
|
|
92
|
+
)
|
|
77
93
|
|
|
78
94
|
return {
|
|
79
95
|
iosRoot,
|
|
@@ -82,8 +98,12 @@ export async function discoverIOSProject(
|
|
|
82
98
|
podfilePath,
|
|
83
99
|
mainTargetName: mainTarget.name,
|
|
84
100
|
mainTargetCandidates: mainTargetCandidates.map((target) => target.name).sort(),
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
buildConfigurationNames: mainTargetBuildConfigurations.map((configuration) => configuration.name),
|
|
102
|
+
infoPlistPath: infoPlistPaths[0],
|
|
103
|
+
infoPlistPaths,
|
|
104
|
+
entitlementsPath: entitlementsPaths[0],
|
|
105
|
+
entitlementsPaths,
|
|
106
|
+
entitlementsPathByConfiguration,
|
|
87
107
|
}
|
|
88
108
|
}
|
|
89
109
|
|
|
@@ -343,75 +363,102 @@ function getTargetBuildConfigurations(
|
|
|
343
363
|
: buildConfigurations
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
async function
|
|
366
|
+
async function resolveInfoPlistPaths(
|
|
347
367
|
iosRoot: string,
|
|
348
368
|
config: NormalizedIOSProjectConfig,
|
|
349
369
|
target: ParsedPbxNativeTarget,
|
|
350
370
|
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
351
|
-
): Promise<string> {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
): Promise<string[]> {
|
|
372
|
+
const discoveredPaths = config.infoPlistPath
|
|
373
|
+
? new Map<string, string[]>()
|
|
374
|
+
: resolveBuildSettingPaths(
|
|
375
|
+
iosRoot,
|
|
376
|
+
target,
|
|
377
|
+
buildConfigurations,
|
|
378
|
+
(configuration) => configuration.buildSettings.infoPlistFile
|
|
379
|
+
)
|
|
380
|
+
const infoPlistPaths = config.infoPlistPath ? [config.infoPlistPath] : [...discoveredPaths.keys()]
|
|
361
381
|
|
|
362
|
-
if (
|
|
382
|
+
if (infoPlistPaths.length === 0) {
|
|
363
383
|
throw new IOSProjectDiscoveryError(
|
|
364
384
|
`Could not determine Info.plist for target '${target.name}'. Set ios.project.infoPlistPath explicitly.`
|
|
365
385
|
)
|
|
366
386
|
}
|
|
367
387
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
for (const infoPlistPath of infoPlistPaths) {
|
|
389
|
+
await ensureFile(
|
|
390
|
+
infoPlistPath,
|
|
391
|
+
config.infoPlistPath
|
|
392
|
+
? `Configured Info.plist does not exist: ${infoPlistPath}`
|
|
393
|
+
: `Discovered Info.plist does not exist: ${infoPlistPath}${describeReferencingBuildConfigurations(
|
|
394
|
+
discoveredPaths,
|
|
395
|
+
infoPlistPath
|
|
396
|
+
)}. Set ios.project.infoPlistPath to override discovery.`
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return infoPlistPaths
|
|
401
|
+
}
|
|
374
402
|
|
|
375
|
-
|
|
403
|
+
interface ResolvedEntitlements {
|
|
404
|
+
entitlementsPaths: string[]
|
|
405
|
+
entitlementsPathByConfiguration?: Map<string, string>
|
|
376
406
|
}
|
|
377
407
|
|
|
378
|
-
async function
|
|
408
|
+
async function resolveEntitlements(
|
|
379
409
|
iosRoot: string,
|
|
380
410
|
config: NormalizedIOSProjectConfig,
|
|
381
411
|
target: ParsedPbxNativeTarget,
|
|
382
412
|
buildConfigurations: ParsedXCBuildConfiguration[]
|
|
383
|
-
): Promise<
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
413
|
+
): Promise<ResolvedEntitlements> {
|
|
414
|
+
const entitlementsPathByConfiguration = isPerConfigurationMap(config.entitlementsPath)
|
|
415
|
+
? getValuesByBuildConfiguration(
|
|
416
|
+
config.entitlementsPath,
|
|
417
|
+
buildConfigurations.map((configuration) => configuration.name),
|
|
418
|
+
'ios.project.entitlementsPath',
|
|
419
|
+
(message) => new IOSProjectDiscoveryError(message)
|
|
420
|
+
)
|
|
421
|
+
: undefined
|
|
422
|
+
const discoveredPaths = config.entitlementsPath
|
|
423
|
+
? new Map<string, string[]>()
|
|
424
|
+
: resolveBuildSettingPaths(
|
|
425
|
+
iosRoot,
|
|
426
|
+
target,
|
|
427
|
+
buildConfigurations,
|
|
428
|
+
(configuration) => configuration.buildSettings.codeSignEntitlements
|
|
429
|
+
)
|
|
430
|
+
const entitlementsPaths = entitlementsPathByConfiguration
|
|
431
|
+
? [...new Set(entitlementsPathByConfiguration.values())]
|
|
432
|
+
: typeof config.entitlementsPath === 'string'
|
|
433
|
+
? [config.entitlementsPath]
|
|
434
|
+
: [...discoveredPaths.keys()]
|
|
435
|
+
|
|
436
|
+
for (const entitlementsPath of entitlementsPaths) {
|
|
437
|
+
await ensureFile(
|
|
438
|
+
entitlementsPath,
|
|
439
|
+
config.entitlementsPath
|
|
440
|
+
? `Configured entitlements file does not exist: ${entitlementsPath}`
|
|
441
|
+
: `Discovered entitlements file does not exist: ${entitlementsPath}${describeReferencingBuildConfigurations(
|
|
442
|
+
discoveredPaths,
|
|
443
|
+
entitlementsPath
|
|
444
|
+
)}. Set ios.project.entitlementsPath to override discovery.`
|
|
392
445
|
)
|
|
393
|
-
|
|
394
|
-
if (!entitlementsPath) {
|
|
395
|
-
return undefined
|
|
396
446
|
}
|
|
397
447
|
|
|
398
|
-
|
|
399
|
-
entitlementsPath,
|
|
400
|
-
config.entitlementsPath
|
|
401
|
-
? `Configured entitlements file does not exist: ${entitlementsPath}`
|
|
402
|
-
: `Discovered entitlements file does not exist: ${entitlementsPath}`
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
return entitlementsPath
|
|
448
|
+
return { entitlementsPaths, entitlementsPathByConfiguration }
|
|
406
449
|
}
|
|
407
450
|
|
|
408
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Collects the distinct paths a build setting resolves to across the target's build configurations.
|
|
453
|
+
* Apps that ship several environments point each configuration at its own file, so divergence is
|
|
454
|
+
* expected rather than an error.
|
|
455
|
+
*/
|
|
456
|
+
function resolveBuildSettingPaths(
|
|
409
457
|
iosRoot: string,
|
|
410
458
|
target: ParsedPbxNativeTarget,
|
|
411
459
|
buildConfigurations: ParsedXCBuildConfiguration[],
|
|
412
|
-
settingName: string,
|
|
413
460
|
getSettingValue: (configuration: ParsedXCBuildConfiguration) => string | undefined
|
|
414
|
-
): string
|
|
461
|
+
): Map<string, string[]> {
|
|
415
462
|
const resolvedPaths = new Map<string, string[]>()
|
|
416
463
|
|
|
417
464
|
for (const configuration of buildConfigurations) {
|
|
@@ -423,25 +470,22 @@ function resolveConsistentBuildSettingPath(
|
|
|
423
470
|
|
|
424
471
|
const resolvedPath = resolveXcodePathValue(settingValue, iosRoot, target, configuration)
|
|
425
472
|
const configurationNames = resolvedPaths.get(resolvedPath) ?? []
|
|
473
|
+
|
|
426
474
|
configurationNames.push(configuration.name)
|
|
427
475
|
resolvedPaths.set(resolvedPath, configurationNames)
|
|
428
476
|
}
|
|
429
477
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
478
|
+
return resolvedPaths
|
|
479
|
+
}
|
|
433
480
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} explicitly.`
|
|
441
|
-
)
|
|
442
|
-
}
|
|
481
|
+
/** Names the build configurations that reference a path, so a missing file can be traced back. */
|
|
482
|
+
function describeReferencingBuildConfigurations(
|
|
483
|
+
pathsByBuildConfiguration: Map<string, string[]>,
|
|
484
|
+
filePath: string
|
|
485
|
+
): string {
|
|
486
|
+
const configurationNames = pathsByBuildConfiguration.get(filePath)
|
|
443
487
|
|
|
444
|
-
return
|
|
488
|
+
return configurationNames?.length ? ` (referenced by ${configurationNames.join(', ')})` : ''
|
|
445
489
|
}
|
|
446
490
|
|
|
447
491
|
function resolveXcodePathValue(
|
package/src/index.ts
CHANGED
|
@@ -117,6 +117,11 @@ export type { GenerateIOSFilesOptions, GenerateIOSFilesResult } from './platform
|
|
|
117
117
|
export { resolveIOSWidgetTargetName } from './platforms/ios/targetName'
|
|
118
118
|
export { IOSWidgetTargetMutationError, ensureIOSWidgetTarget } from './platforms/ios/xcodeTarget'
|
|
119
119
|
export { applyIOSPlatform, createIOSPreflightRunner } from './platforms/ios/apply'
|
|
120
|
+
export {
|
|
121
|
+
IOSBuildConfigurationValueError,
|
|
122
|
+
VOLTRA_BUILD_SETTING_PREFIX,
|
|
123
|
+
resolveIOSBuildConfigurationValues,
|
|
124
|
+
} from './platforms/ios/buildConfigurationValues'
|
|
120
125
|
export { IOSEntitlementsMutationError, ensureEntitlements } from './platforms/ios/entitlements'
|
|
121
126
|
export type { EnsureEntitlementsOptions, EnsureEntitlementsResult } from './platforms/ios/entitlements'
|
|
122
127
|
export { IOSInfoPlistMutationError, ensureInfoPlist } from './platforms/ios/plist'
|
|
@@ -187,6 +192,8 @@ export type {
|
|
|
187
192
|
NormalizedVoltraAndroidConfig,
|
|
188
193
|
NormalizedVoltraConfig,
|
|
189
194
|
NormalizedVoltraIOSConfig,
|
|
195
|
+
ResolvedIOSProjectConfig,
|
|
196
|
+
ResolvedVoltraIOSConfig,
|
|
190
197
|
VoltraAndroidConfig,
|
|
191
198
|
VoltraConfig,
|
|
192
199
|
VoltraIOSConfig,
|
|
@@ -195,6 +202,7 @@ export type {
|
|
|
195
202
|
WidgetLabel,
|
|
196
203
|
WidgetLocalizedValue,
|
|
197
204
|
} from './config/types'
|
|
205
|
+
export type { PerConfiguration } from './config/perConfiguration'
|
|
198
206
|
export type {
|
|
199
207
|
ApplySummary,
|
|
200
208
|
PreflightFailureReport,
|
|
@@ -4,11 +4,14 @@ import path from 'node:path'
|
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { vdConvert } from 'vd-tool'
|
|
6
6
|
|
|
7
|
-
import { requirePlatformPackage } from '../../dependencies/platformPackages'
|
|
7
|
+
import { getPlatformClientPackageVersion, requirePlatformPackage } from '../../dependencies/platformPackages'
|
|
8
8
|
import { ensureDirectory, pathExists, readTextFile, writeTextFile } from '../../fs/readWrite'
|
|
9
9
|
import { normalizeRelativePath, toRelativePath } from '../../fs/path'
|
|
10
10
|
import { VoltraCliError } from '../../reporting/summary'
|
|
11
|
-
import {
|
|
11
|
+
import { createDynamicWidgetBuildInfo, createGeneratedWidgetModuleLoader } from '../shared/widgetModule'
|
|
12
|
+
import { androidWidgetSizingAttributes, androidWidgetSizingWarnings } from './widgetSizing'
|
|
13
|
+
|
|
14
|
+
import type { WidgetModuleLoader } from '@use-voltra/compiler'
|
|
12
15
|
|
|
13
16
|
import type { AndroidProjectDiscovery } from '../../discovery/android'
|
|
14
17
|
import type {
|
|
@@ -27,6 +30,7 @@ const LOCALIZED_INITIAL_STATE_KEY = '__voltraLocales'
|
|
|
27
30
|
const DEFAULT_WIDGET_LOCALE_QUALIFIER = 'en'
|
|
28
31
|
const ANDROID_DYNAMIC_WIDGET_MANIFEST_PATH = path.join('.voltra', 'manifest.android.json')
|
|
29
32
|
const ANDROID_WIDGET_CONFIG_DEFAULTS_PATH = path.join('assets', 'voltra', 'widget_config_defaults.json')
|
|
33
|
+
const ANDROID_WIDGET_SERVER_DEFAULTS_PATH = path.join('assets', 'voltra', 'widget_server_defaults.json')
|
|
30
34
|
|
|
31
35
|
export interface GenerateAndroidFilesOptions {
|
|
32
36
|
projectRoot: string
|
|
@@ -76,11 +80,18 @@ type DetectedAndroidWidget =
|
|
|
76
80
|
|
|
77
81
|
export async function generateAndroidFiles(options: GenerateAndroidFilesOptions): Promise<GenerateAndroidFilesResult> {
|
|
78
82
|
const { projectRoot, android, discovery } = options
|
|
83
|
+
const voltraVersion = getPlatformClientPackageVersion(projectRoot, 'android')
|
|
79
84
|
const resourceRoot = path.join(discovery.appModuleRoot, 'src', 'main')
|
|
80
|
-
const detectedWidgets = detectClientRenderedWidgets(projectRoot, android.widgets)
|
|
81
85
|
const changes: ReportedChange[] = []
|
|
82
86
|
const warnings: string[] = []
|
|
83
87
|
const generatedFiles = new Set<string>()
|
|
88
|
+
const widgetModuleLoader = createGeneratedWidgetModuleLoader(
|
|
89
|
+
projectRoot,
|
|
90
|
+
'android',
|
|
91
|
+
createAndroidGeneratedFilesError,
|
|
92
|
+
(message) => warnings.push(message)
|
|
93
|
+
)
|
|
94
|
+
const detectedWidgets = detectClientRenderedWidgets(projectRoot, widgetModuleLoader, android.widgets)
|
|
84
95
|
|
|
85
96
|
mergeSingleResult(
|
|
86
97
|
await writeGeneratedTextFile(
|
|
@@ -104,7 +115,13 @@ export async function generateAndroidFiles(options: GenerateAndroidFilesOptions)
|
|
|
104
115
|
const fontFiles = await copyAndroidFonts(projectRoot, resourceRoot, android.fonts)
|
|
105
116
|
mergeResult(fontFiles, changes, warnings, generatedFiles)
|
|
106
117
|
|
|
107
|
-
const initialStateFiles = await generateAndroidInitialStates(
|
|
118
|
+
const initialStateFiles = await generateAndroidInitialStates(
|
|
119
|
+
projectRoot,
|
|
120
|
+
widgetModuleLoader,
|
|
121
|
+
resourceRoot,
|
|
122
|
+
detectedWidgets,
|
|
123
|
+
voltraVersion
|
|
124
|
+
)
|
|
108
125
|
mergeResult(initialStateFiles, changes, warnings, generatedFiles)
|
|
109
126
|
|
|
110
127
|
const configDefaultsResult = await generateAndroidConfigDefaults(projectRoot, resourceRoot, detectedWidgets)
|
|
@@ -112,6 +129,11 @@ export async function generateAndroidFiles(options: GenerateAndroidFilesOptions)
|
|
|
112
129
|
mergeSingleResult(configDefaultsResult, changes, generatedFiles)
|
|
113
130
|
}
|
|
114
131
|
|
|
132
|
+
const serverDefaultsResult = await generateAndroidServerDefaults(projectRoot, resourceRoot, detectedWidgets)
|
|
133
|
+
if (serverDefaultsResult) {
|
|
134
|
+
mergeSingleResult(serverDefaultsResult, changes, generatedFiles)
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
return {
|
|
116
138
|
changes,
|
|
117
139
|
files: [...generatedFiles].sort(),
|
|
@@ -216,6 +238,7 @@ async function generateAndroidXmlFiles(
|
|
|
216
238
|
const widgetInfoResult = await writeGeneratedTextFile(projectRoot, widgetInfoPath, widgetInfoContent)
|
|
217
239
|
pushChange(changes, widgetInfoResult.change)
|
|
218
240
|
generatedFiles.add(widgetInfoResult.relativePath)
|
|
241
|
+
warnings.push(...androidWidgetSizingWarnings(widget, widget.id))
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
return {
|
|
@@ -408,19 +431,21 @@ async function copyAndroidFonts(
|
|
|
408
431
|
|
|
409
432
|
async function generateAndroidInitialStates(
|
|
410
433
|
projectRoot: string,
|
|
434
|
+
loader: WidgetModuleLoader,
|
|
411
435
|
resourceRoot: string,
|
|
412
|
-
widgets: DetectedAndroidWidget[]
|
|
436
|
+
widgets: DetectedAndroidWidget[],
|
|
437
|
+
voltraVersion: string
|
|
413
438
|
): Promise<GenerateAndroidFilesResult> {
|
|
414
439
|
const serverWidgets = widgets.filter(
|
|
415
440
|
(widget): widget is Extract<DetectedAndroidWidget, { clientRendered: false }> => !widget.clientRendered
|
|
416
441
|
)
|
|
417
442
|
const prerenderableServerWidgets = serverWidgets.filter((widget) => widget.initialStatePath)
|
|
418
443
|
const serverStates = await prerenderWidgetStates(
|
|
419
|
-
|
|
444
|
+
loader,
|
|
420
445
|
prerenderableServerWidgets,
|
|
421
446
|
loadAndroidWidgetRenderer(projectRoot)
|
|
422
447
|
)
|
|
423
|
-
const clientStates = await prerenderClientRenderedAndroidWidgets(projectRoot, widgets)
|
|
448
|
+
const clientStates = await prerenderClientRenderedAndroidWidgets(projectRoot, loader, widgets, voltraVersion)
|
|
424
449
|
const prerenderedStates = new Map([...serverStates, ...clientStates])
|
|
425
450
|
|
|
426
451
|
if (prerenderedStates.size === 0) {
|
|
@@ -443,7 +468,7 @@ async function generateAndroidInitialStates(
|
|
|
443
468
|
}
|
|
444
469
|
|
|
445
470
|
async function prerenderWidgetStates(
|
|
446
|
-
|
|
471
|
+
loader: WidgetModuleLoader,
|
|
447
472
|
widgets: NormalizedAndroidWidgetConfig[],
|
|
448
473
|
renderer: AndroidWidgetRenderer
|
|
449
474
|
): Promise<PrerenderedWidgetStates> {
|
|
@@ -468,7 +493,7 @@ async function prerenderWidgetStates(
|
|
|
468
493
|
throw new AndroidGeneratedFilesError(`Initial state file not found for widget '${widget.id}' at ${modulePath}`)
|
|
469
494
|
}
|
|
470
495
|
|
|
471
|
-
const widgetVariants = evaluateLegacyWidgetModule(
|
|
496
|
+
const widgetVariants = evaluateLegacyWidgetModule(loader, modulePath)
|
|
472
497
|
localeStates.set(localeKey, renderer(widgetVariants))
|
|
473
498
|
}
|
|
474
499
|
|
|
@@ -478,11 +503,8 @@ async function prerenderWidgetStates(
|
|
|
478
503
|
return prerenderedStates
|
|
479
504
|
}
|
|
480
505
|
|
|
481
|
-
function evaluateLegacyWidgetModule(
|
|
482
|
-
const
|
|
483
|
-
default?: unknown
|
|
484
|
-
}
|
|
485
|
-
const widgetVariants = exports.default ?? exports
|
|
506
|
+
function evaluateLegacyWidgetModule(loader: WidgetModuleLoader, filePath: string): AndroidWidgetVariants {
|
|
507
|
+
const widgetVariants = loader.loadDefaultExport(filePath)
|
|
486
508
|
|
|
487
509
|
if (!widgetVariants || typeof widgetVariants !== 'object') {
|
|
488
510
|
throw new AndroidGeneratedFilesError(`Widget file must export widget variants: ${filePath}`)
|
|
@@ -530,12 +552,17 @@ function createDynamicWidgetsManifest(widgets: DetectedAndroidWidget[]): Dynamic
|
|
|
530
552
|
|
|
531
553
|
function detectClientRenderedWidgets(
|
|
532
554
|
projectRoot: string,
|
|
555
|
+
loader: WidgetModuleLoader,
|
|
533
556
|
widgets: NormalizedAndroidWidgetConfig[]
|
|
534
557
|
): DetectedAndroidWidget[] {
|
|
535
|
-
return widgets.map((widget) => detectSingleWidget(projectRoot, widget))
|
|
558
|
+
return widgets.map((widget) => detectSingleWidget(projectRoot, loader, widget))
|
|
536
559
|
}
|
|
537
560
|
|
|
538
|
-
function detectSingleWidget(
|
|
561
|
+
function detectSingleWidget(
|
|
562
|
+
projectRoot: string,
|
|
563
|
+
loader: WidgetModuleLoader,
|
|
564
|
+
widget: NormalizedAndroidWidgetConfig
|
|
565
|
+
): DetectedAndroidWidget {
|
|
539
566
|
if (!widget.entry) {
|
|
540
567
|
return {
|
|
541
568
|
...widget,
|
|
@@ -549,7 +576,7 @@ function detectSingleWidget(projectRoot: string, widget: NormalizedAndroidWidget
|
|
|
549
576
|
throw createAndroidGeneratedFilesError(`[voltra] Dynamic Widget "${widget.id}" entry not found at ${widget.entry}`)
|
|
550
577
|
}
|
|
551
578
|
|
|
552
|
-
const widgetModule = safelyEvaluateDynamicWidget(
|
|
579
|
+
const widgetModule = safelyEvaluateDynamicWidget(loader, widget.id, widget.entry, clientSourcePath)
|
|
553
580
|
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
554
581
|
|
|
555
582
|
if (typeof widgetFn !== 'function') {
|
|
@@ -568,7 +595,9 @@ function detectSingleWidget(projectRoot: string, widget: NormalizedAndroidWidget
|
|
|
568
595
|
|
|
569
596
|
async function prerenderClientRenderedAndroidWidgets(
|
|
570
597
|
projectRoot: string,
|
|
571
|
-
|
|
598
|
+
loader: WidgetModuleLoader,
|
|
599
|
+
widgets: DetectedAndroidWidget[],
|
|
600
|
+
voltraVersion: string
|
|
572
601
|
): Promise<PrerenderedWidgetStates> {
|
|
573
602
|
const clientWidgets = widgets.filter(
|
|
574
603
|
(widget): widget is Extract<DetectedAndroidWidget, { clientRendered: true }> => widget.clientRendered
|
|
@@ -585,13 +614,13 @@ async function prerenderClientRenderedAndroidWidgets(
|
|
|
585
614
|
colorScheme: 'light',
|
|
586
615
|
locale: 'en-US',
|
|
587
616
|
configuration: undefined,
|
|
588
|
-
build:
|
|
617
|
+
build: createDynamicWidgetBuildInfo(voltraVersion),
|
|
589
618
|
}
|
|
590
619
|
const prerenderedStates: PrerenderedWidgetStates = new Map()
|
|
591
620
|
|
|
592
621
|
for (const widget of clientWidgets) {
|
|
593
622
|
try {
|
|
594
|
-
const widgetModule = safelyEvaluateDynamicWidget(
|
|
623
|
+
const widgetModule = safelyEvaluateDynamicWidget(loader, widget.id, widget.entry, widget.clientSourcePath)
|
|
595
624
|
const widgetFn = readDynamicWidgetExport(widget.id, widget.entry, widgetModule)
|
|
596
625
|
const element = widgetFn({}, placeholderEnv)
|
|
597
626
|
prerenderedStates.set(widget.id, new Map([[DEFAULT_INITIAL_STATE_LOCALE, JSON.stringify(renderer(element))]]))
|
|
@@ -612,13 +641,13 @@ async function prerenderClientRenderedAndroidWidgets(
|
|
|
612
641
|
}
|
|
613
642
|
|
|
614
643
|
function safelyEvaluateDynamicWidget(
|
|
615
|
-
|
|
644
|
+
loader: WidgetModuleLoader,
|
|
616
645
|
widgetId: string,
|
|
617
646
|
widgetEntry: string,
|
|
618
647
|
clientSourcePath: string
|
|
619
648
|
): unknown {
|
|
620
649
|
try {
|
|
621
|
-
return
|
|
650
|
+
return loader.load(clientSourcePath)
|
|
622
651
|
} catch (error) {
|
|
623
652
|
if (error instanceof AndroidGeneratedFilesError) {
|
|
624
653
|
throw error
|
|
@@ -670,6 +699,59 @@ async function generateAndroidConfigDefaults(
|
|
|
670
699
|
)
|
|
671
700
|
}
|
|
672
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Emits the build-time `serverUpdate` defaults the runtime settings resolver reads as its lowest
|
|
704
|
+
* layer.
|
|
705
|
+
*
|
|
706
|
+
* These used to be Kotlin literals inside each generated receiver. They live in an asset now
|
|
707
|
+
* because the app can override the URL and the interval with `setWidgetServerUpdate`, and a
|
|
708
|
+
* receiver compiled at build time cannot be asked what the interval is today.
|
|
709
|
+
*
|
|
710
|
+
* A widget id present in this file is server-driven. `url` is omitted when app.json declared
|
|
711
|
+
* `serverUpdate` without one, which means the app supplies it at runtime.
|
|
712
|
+
*/
|
|
713
|
+
async function generateAndroidServerDefaults(
|
|
714
|
+
projectRoot: string,
|
|
715
|
+
resourceRoot: string,
|
|
716
|
+
widgets: DetectedAndroidWidget[]
|
|
717
|
+
): Promise<GeneratedFileResult | undefined> {
|
|
718
|
+
const defaults = createWidgetServerDefaults(widgets)
|
|
719
|
+
|
|
720
|
+
if (Object.keys(defaults).length === 0) {
|
|
721
|
+
return undefined
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return writeGeneratedTextFile(
|
|
725
|
+
projectRoot,
|
|
726
|
+
path.join(resourceRoot, ANDROID_WIDGET_SERVER_DEFAULTS_PATH),
|
|
727
|
+
`${JSON.stringify(defaults, null, 2)}\n`
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
interface WidgetServerDefaults {
|
|
732
|
+
url?: string
|
|
733
|
+
intervalMinutes: number
|
|
734
|
+
refresh: boolean
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function createWidgetServerDefaults(widgets: DetectedAndroidWidget[]): Record<string, WidgetServerDefaults> {
|
|
738
|
+
const defaults: Record<string, WidgetServerDefaults> = {}
|
|
739
|
+
|
|
740
|
+
for (const widget of widgets) {
|
|
741
|
+
if (!widget.serverUpdate) {
|
|
742
|
+
continue
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
defaults[widget.id] = {
|
|
746
|
+
...(widget.serverUpdate.url !== undefined ? { url: widget.serverUpdate.url } : {}),
|
|
747
|
+
intervalMinutes: widget.serverUpdate.intervalMinutes,
|
|
748
|
+
refresh: widget.serverUpdate.refresh,
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return defaults
|
|
753
|
+
}
|
|
754
|
+
|
|
673
755
|
function createWidgetConfigDefaults(widgets: DetectedAndroidWidget[]): WidgetConfigDefaults {
|
|
674
756
|
const defaults: WidgetConfigDefaults = {}
|
|
675
757
|
|
|
@@ -916,15 +998,41 @@ async function getLargeImageWarning(imagePath: string, fileName: string): Promis
|
|
|
916
998
|
return `Image '${fileName}' is ${stat.size} bytes. Large Android widget images may not display correctly.`
|
|
917
999
|
}
|
|
918
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* `entry` picks the render engine and `serverUpdate` picks where the data comes from, so the two
|
|
1003
|
+
* keys together select one of four base classes. Runtime code never asks "is this server-driven?" —
|
|
1004
|
+
* the answer is baked in here, once, at generate time.
|
|
1005
|
+
*/
|
|
919
1006
|
function generateWidgetReceiverContent(widget: DetectedAndroidWidget, packageName: string): string {
|
|
920
1007
|
const className = `VoltraWidget_${widget.id}Receiver`
|
|
921
1008
|
const labelForComment = widgetLabelEnglish(widget.displayName)
|
|
922
1009
|
|
|
1010
|
+
// A widget with an entry and a serverUpdate renders bundled JS from props fetched in the
|
|
1011
|
+
// background. The scheduling lives in the base class, so the generated receiver stays a name and
|
|
1012
|
+
// an id — the URL and the interval come from widget_server_defaults.json at runtime, where
|
|
1013
|
+
// setWidgetServerUpdate can override them.
|
|
1014
|
+
if (widget.clientRendered && widget.serverUpdate) {
|
|
1015
|
+
return [
|
|
1016
|
+
`package ${packageName}.widget`,
|
|
1017
|
+
'',
|
|
1018
|
+
'import voltra.dynamicwidget.serverupdate.VoltraServerDrivenClientWidgetReceiver',
|
|
1019
|
+
'',
|
|
1020
|
+
'/**',
|
|
1021
|
+
` * Auto-generated server-driven Dynamic Widget receiver for ${labelForComment}`,
|
|
1022
|
+
` * Widget ID: ${widget.id}`,
|
|
1023
|
+
' */',
|
|
1024
|
+
`class ${className} : VoltraServerDrivenClientWidgetReceiver() {`,
|
|
1025
|
+
` override val widgetId: String = "${widget.id}"`,
|
|
1026
|
+
'}',
|
|
1027
|
+
'',
|
|
1028
|
+
].join('\n')
|
|
1029
|
+
}
|
|
1030
|
+
|
|
923
1031
|
if (widget.clientRendered) {
|
|
924
1032
|
return [
|
|
925
1033
|
`package ${packageName}.widget`,
|
|
926
1034
|
'',
|
|
927
|
-
'import voltra.
|
|
1035
|
+
'import voltra.dynamicwidget.VoltraClientWidgetReceiver',
|
|
928
1036
|
'',
|
|
929
1037
|
'/**',
|
|
930
1038
|
` * Auto-generated Dynamic Widget receiver for ${labelForComment}`,
|
|
@@ -938,35 +1046,30 @@ function generateWidgetReceiverContent(widget: DetectedAndroidWidget, packageNam
|
|
|
938
1046
|
}
|
|
939
1047
|
|
|
940
1048
|
if (widget.serverUpdate) {
|
|
941
|
-
const refreshEnabled = widget.serverUpdate.refresh === true
|
|
942
|
-
|
|
943
1049
|
return [
|
|
944
1050
|
`package ${packageName}.widget`,
|
|
945
1051
|
'',
|
|
946
1052
|
'import android.appwidget.AppWidgetManager',
|
|
947
1053
|
'import android.content.Context',
|
|
948
|
-
'import
|
|
949
|
-
'import voltra.widget.
|
|
1054
|
+
'import kotlinx.coroutines.runBlocking',
|
|
1055
|
+
'import voltra.widget.payload.VoltraPayloadWidgetReceiver',
|
|
1056
|
+
'import voltra.widget.payload.VoltraWidgetUpdateScheduler',
|
|
950
1057
|
'',
|
|
951
1058
|
'/**',
|
|
952
1059
|
` * Auto-generated widget receiver for ${labelForComment}`,
|
|
953
1060
|
` * Widget ID: ${widget.id}`,
|
|
954
|
-
` * Server Update: ${widget.serverUpdate.url} (every ${widget.serverUpdate.intervalMinutes} minutes)`,
|
|
955
|
-
` * Refresh Button: ${String(refreshEnabled)}`,
|
|
956
1061
|
' */',
|
|
957
|
-
`class ${className} :
|
|
1062
|
+
`class ${className} : VoltraPayloadWidgetReceiver() {`,
|
|
958
1063
|
` override val widgetId: String = "${widget.id}"`,
|
|
959
1064
|
'',
|
|
960
1065
|
' override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {',
|
|
961
1066
|
' super.onUpdate(context, appWidgetManager, appWidgetIds)',
|
|
962
1067
|
'',
|
|
963
|
-
'
|
|
964
|
-
'
|
|
965
|
-
|
|
966
|
-
`
|
|
967
|
-
|
|
968
|
-
` refreshEnabled = ${String(refreshEnabled)}`,
|
|
969
|
-
' )',
|
|
1068
|
+
' // Blocking rather than launching: onReceive must not return before the work is',
|
|
1069
|
+
' // enqueued, or a widget added while the app is not running loses its schedule.',
|
|
1070
|
+
' runBlocking {',
|
|
1071
|
+
` VoltraWidgetUpdateScheduler.schedulePeriodicUpdate(context.applicationContext, "${widget.id}")`,
|
|
1072
|
+
' }',
|
|
970
1073
|
' }',
|
|
971
1074
|
'',
|
|
972
1075
|
' override fun onDeleted(context: Context, appWidgetIds: IntArray) {',
|
|
@@ -992,13 +1095,13 @@ function generateWidgetReceiverContent(widget: DetectedAndroidWidget, packageNam
|
|
|
992
1095
|
return [
|
|
993
1096
|
`package ${packageName}.widget`,
|
|
994
1097
|
'',
|
|
995
|
-
'import voltra.widget.
|
|
1098
|
+
'import voltra.widget.payload.VoltraPayloadWidgetReceiver',
|
|
996
1099
|
'',
|
|
997
1100
|
'/**',
|
|
998
1101
|
` * Auto-generated widget receiver for ${labelForComment}`,
|
|
999
1102
|
` * Widget ID: ${widget.id}`,
|
|
1000
1103
|
' */',
|
|
1001
|
-
`class ${className} :
|
|
1104
|
+
`class ${className} : VoltraPayloadWidgetReceiver() {`,
|
|
1002
1105
|
` override val widgetId: String = "${widget.id}"`,
|
|
1003
1106
|
'}',
|
|
1004
1107
|
'',
|
|
@@ -1010,18 +1113,12 @@ function generateWidgetInfoXml(
|
|
|
1010
1113
|
previewImageResourceName?: string,
|
|
1011
1114
|
previewLayoutResourceName?: string
|
|
1012
1115
|
): string {
|
|
1013
|
-
const minWidth = widget.minWidth ?? (widget.minCellWidth !== undefined ? widget.minCellWidth * 70 - 30 : undefined)
|
|
1014
|
-
const minHeight =
|
|
1015
|
-
widget.minHeight ?? (widget.minCellHeight !== undefined ? widget.minCellHeight * 70 - 30 : undefined)
|
|
1016
1116
|
const resizeMode = widget.resizeMode ?? 'horizontal|vertical'
|
|
1017
1117
|
const widgetCategory = widget.widgetCategory ?? 'home_screen'
|
|
1018
1118
|
const lines = [
|
|
1019
1119
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}${minHeight !== undefined ? ` android:minHeight="${minHeight}dp"` : ''}`,
|
|
1023
|
-
` android:targetCellWidth="${widget.targetCellWidth}"`,
|
|
1024
|
-
` android:targetCellHeight="${widget.targetCellHeight}"`,
|
|
1120
|
+
'<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"',
|
|
1121
|
+
...androidWidgetSizingAttributes(widget).map((attribute) => ` ${attribute}`),
|
|
1025
1122
|
' android:updatePeriodMillis="0"',
|
|
1026
1123
|
' android:initialLayout="@layout/voltra_widget_placeholder"',
|
|
1027
1124
|
` android:resizeMode="${resizeMode}"`,
|