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,100 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { cosmiconfig } from 'cosmiconfig'
|
|
4
|
+
|
|
5
|
+
import type { CosmiconfigResult } from 'cosmiconfig'
|
|
6
|
+
|
|
7
|
+
import type { LoadedVoltraConfig, VoltraConfig } from './types'
|
|
8
|
+
|
|
9
|
+
const MODULE_NAME = 'voltra'
|
|
10
|
+
|
|
11
|
+
export class VoltraConfigLoadError extends Error {
|
|
12
|
+
cause?: Error
|
|
13
|
+
|
|
14
|
+
constructor(message: string, cause?: Error) {
|
|
15
|
+
super(message)
|
|
16
|
+
this.name = 'VoltraConfigLoadError'
|
|
17
|
+
this.cause = cause
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoadVoltraConfigOptions {
|
|
22
|
+
configPath?: string
|
|
23
|
+
cwd?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getExplorer() {
|
|
27
|
+
return cosmiconfig(MODULE_NAME, {
|
|
28
|
+
searchPlaces: [
|
|
29
|
+
'package.json',
|
|
30
|
+
'.voltrarc',
|
|
31
|
+
'.voltrarc.json',
|
|
32
|
+
'.voltrarc.yaml',
|
|
33
|
+
'.voltrarc.yml',
|
|
34
|
+
'.voltrarc.js',
|
|
35
|
+
'.voltrarc.cjs',
|
|
36
|
+
'.voltrarc.mjs',
|
|
37
|
+
'.voltrarc.ts',
|
|
38
|
+
'voltra.config.json',
|
|
39
|
+
'voltra.config.yaml',
|
|
40
|
+
'voltra.config.yml',
|
|
41
|
+
'voltra.config.js',
|
|
42
|
+
'voltra.config.cjs',
|
|
43
|
+
'voltra.config.mjs',
|
|
44
|
+
'voltra.config.ts',
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toLoadedConfig(result: CosmiconfigResult): LoadedVoltraConfig {
|
|
50
|
+
if (result.isEmpty) {
|
|
51
|
+
throw new VoltraConfigLoadError(`Voltra config file is empty: ${result.filepath}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!result.config || typeof result.config !== 'object' || Array.isArray(result.config)) {
|
|
55
|
+
throw new VoltraConfigLoadError(`Voltra config must be an object: ${result.filepath}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
config: result.config as VoltraConfig,
|
|
60
|
+
configPath: result.filepath,
|
|
61
|
+
configDir: dirname(result.filepath),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function loadVoltraConfig(options: LoadVoltraConfigOptions = {}): Promise<LoadedVoltraConfig> {
|
|
66
|
+
const explorer = getExplorer()
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (options.configPath) {
|
|
70
|
+
const configPath = resolve(options.cwd ?? process.cwd(), options.configPath)
|
|
71
|
+
const result = await explorer.load(configPath)
|
|
72
|
+
|
|
73
|
+
if (!result) {
|
|
74
|
+
throw new VoltraConfigLoadError(`Voltra config not found at ${configPath}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return toLoadedConfig(result)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = await explorer.search(options.cwd)
|
|
81
|
+
|
|
82
|
+
if (!result) {
|
|
83
|
+
throw new VoltraConfigLoadError(
|
|
84
|
+
'No Voltra config found. Checked package.json, .voltrarc*, and voltra.config.* files.'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return toLoadedConfig(result)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof VoltraConfigLoadError) {
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
95
|
+
throw new VoltraConfigLoadError(
|
|
96
|
+
`Failed to load Voltra config: ${message}`,
|
|
97
|
+
error instanceof Error ? error : undefined
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { resolveFromRoot } from '../fs/path'
|
|
4
|
+
import { CLI_DEFAULTS } from './defaults'
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AndroidWidgetAppIntentConfig,
|
|
8
|
+
AndroidWidgetConfig,
|
|
9
|
+
IOSWidgetAppIntentConfig,
|
|
10
|
+
IOSWidgetConfig,
|
|
11
|
+
LoadedVoltraConfig,
|
|
12
|
+
NormalizedAndroidWidgetConfig,
|
|
13
|
+
NormalizedVoltraAndroidConfig,
|
|
14
|
+
NormalizedVoltraConfig,
|
|
15
|
+
NormalizedVoltraIOSConfig,
|
|
16
|
+
NormalizedIOSWidgetConfig,
|
|
17
|
+
WidgetInitialStatePath,
|
|
18
|
+
WidgetLabel,
|
|
19
|
+
WidgetLocalizedValue,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
const VALID_IOS_WIDGET_FAMILIES = new Set([
|
|
23
|
+
'systemSmall',
|
|
24
|
+
'systemMedium',
|
|
25
|
+
'systemLarge',
|
|
26
|
+
'systemExtraLarge',
|
|
27
|
+
'accessoryCircular',
|
|
28
|
+
'accessoryRectangular',
|
|
29
|
+
'accessoryInline',
|
|
30
|
+
])
|
|
31
|
+
const WIDGET_ENTRY_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'])
|
|
32
|
+
|
|
33
|
+
export class VoltraConfigNormalizationError extends Error {
|
|
34
|
+
constructor(message: string) {
|
|
35
|
+
super(message)
|
|
36
|
+
this.name = 'VoltraConfigNormalizationError'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertObject(value: unknown, context: string): void {
|
|
41
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
42
|
+
throw new VoltraConfigNormalizationError(`${context} must be an object`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertRecord(value: unknown, context: string): asserts value is Record<string, unknown> {
|
|
47
|
+
assertObject(value, context)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertOptionalString(value: unknown, context: string): asserts value is string | undefined {
|
|
51
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
52
|
+
throw new VoltraConfigNormalizationError(`${context} must be a string`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertOptionalBoolean(value: unknown, context: string): asserts value is boolean | undefined {
|
|
57
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
58
|
+
throw new VoltraConfigNormalizationError(`${context} must be a boolean`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertPositiveInteger(value: unknown, context: string): asserts value is number {
|
|
63
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
64
|
+
throw new VoltraConfigNormalizationError(`${context} must be a positive integer`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertOptionalPositiveInteger(value: unknown, context: string): asserts value is number | undefined {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
assertPositiveInteger(value, context)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertNonEmptyString(value: unknown, context: string): asserts value is string {
|
|
75
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
76
|
+
throw new VoltraConfigNormalizationError(`${context} must be a non-empty string`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertOptionalStringArray(value: unknown, context: string): asserts value is string[] | undefined {
|
|
81
|
+
if (value === undefined) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(value)) {
|
|
86
|
+
throw new VoltraConfigNormalizationError(`${context} must be an array of strings`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const entry of value) {
|
|
90
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
91
|
+
throw new VoltraConfigNormalizationError(`${context} must contain only non-empty strings`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolvePathFromProjectRoot(projectRoot: string, filePath: string): string {
|
|
97
|
+
return path.isAbsolute(filePath) ? filePath : resolveFromRoot(projectRoot, filePath)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveOptionalPathFromProjectRoot(projectRoot: string, filePath: string | undefined): string | undefined {
|
|
101
|
+
if (!filePath) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolvePathFromProjectRoot(projectRoot, filePath)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAbsoluteWidgetPath(value: string): boolean {
|
|
109
|
+
return path.isAbsolute(value) || path.win32.isAbsolute(value) || /^[a-zA-Z]:[\\/]/.test(value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeWidgetEntry(entry: string): string {
|
|
113
|
+
return path.posix.normalize(entry.replace(/\\/g, '/'))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeWidgetEntryPath(entry: string): string {
|
|
117
|
+
return normalizeWidgetEntry(entry)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeOptionalWidgetEntry(value: unknown, context: string): string | undefined {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assertNonEmptyString(value, context)
|
|
126
|
+
|
|
127
|
+
if (isAbsoluteWidgetPath(value)) {
|
|
128
|
+
throw new VoltraConfigNormalizationError(`${context} must be a relative path, not an absolute path`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const normalizedEntry = normalizeWidgetEntryPath(value)
|
|
132
|
+
|
|
133
|
+
if (!normalizedEntry || normalizedEntry === '.') {
|
|
134
|
+
throw new VoltraConfigNormalizationError(`${context} must point to a file inside the project root`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (normalizedEntry === '..' || normalizedEntry.startsWith('../')) {
|
|
138
|
+
throw new VoltraConfigNormalizationError(`${context} must stay within the project root after normalization`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const extension = path.posix.extname(normalizedEntry)
|
|
142
|
+
if (!WIDGET_ENTRY_EXTENSIONS.has(extension)) {
|
|
143
|
+
throw new VoltraConfigNormalizationError(
|
|
144
|
+
`${context} must use a Metro-importable source extension (${Array.from(WIDGET_ENTRY_EXTENSIONS).join(
|
|
145
|
+
', '
|
|
146
|
+
)}); received '${extension || '(none)'}'`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return normalizedEntry
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeLocalizedPathMap(
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
value: WidgetLocalizedValue,
|
|
156
|
+
context: string
|
|
157
|
+
): WidgetLocalizedValue {
|
|
158
|
+
const entries = Object.entries(value)
|
|
159
|
+
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
throw new VoltraConfigNormalizationError(`${context} must not be empty`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Object.fromEntries(
|
|
165
|
+
entries.map(([locale, localePath]) => {
|
|
166
|
+
assertNonEmptyString(locale, `${context} locale key`)
|
|
167
|
+
assertNonEmptyString(localePath, `${context}.${locale}`)
|
|
168
|
+
return [locale, resolvePathFromProjectRoot(projectRoot, localePath)]
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeLabel(value: WidgetLabel, context: string): WidgetLabel {
|
|
174
|
+
if (typeof value === 'string') {
|
|
175
|
+
assertNonEmptyString(value, context)
|
|
176
|
+
return value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
assertRecord(value, context)
|
|
180
|
+
|
|
181
|
+
const entries = Object.entries(value)
|
|
182
|
+
if (entries.length === 0) {
|
|
183
|
+
throw new VoltraConfigNormalizationError(`${context} must not be empty`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Object.fromEntries(
|
|
187
|
+
entries.map(([locale, label]) => {
|
|
188
|
+
assertNonEmptyString(locale, `${context} locale key`)
|
|
189
|
+
if (typeof label !== 'string') {
|
|
190
|
+
throw new VoltraConfigNormalizationError(`${context}.${locale} must be a string`)
|
|
191
|
+
}
|
|
192
|
+
assertNonEmptyString(label, `${context}.${locale}`)
|
|
193
|
+
return [locale, label]
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeInitialStatePath(
|
|
199
|
+
projectRoot: string,
|
|
200
|
+
value: WidgetInitialStatePath | undefined,
|
|
201
|
+
context: string
|
|
202
|
+
): WidgetInitialStatePath | undefined {
|
|
203
|
+
if (value === undefined) {
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof value === 'string') {
|
|
208
|
+
assertNonEmptyString(value, context)
|
|
209
|
+
return resolvePathFromProjectRoot(projectRoot, value)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
assertRecord(value, context)
|
|
213
|
+
return normalizeLocalizedPathMap(projectRoot, value, context)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeWidgetParameterName(value: unknown, context: string): string {
|
|
217
|
+
assertNonEmptyString(value, context)
|
|
218
|
+
return value
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeAndroidAppIntent(
|
|
222
|
+
value: AndroidWidgetAppIntentConfig | undefined,
|
|
223
|
+
context: string
|
|
224
|
+
): AndroidWidgetAppIntentConfig | undefined {
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assertObject(value, context)
|
|
230
|
+
|
|
231
|
+
if (!Array.isArray(value.parameters)) {
|
|
232
|
+
throw new VoltraConfigNormalizationError(`${context}.parameters must be an array`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const seenNames = new Set<string>()
|
|
236
|
+
const parameters = value.parameters.map((parameter, index) => {
|
|
237
|
+
const parameterContext = `${context}.parameters[${index}]`
|
|
238
|
+
assertObject(parameter, parameterContext)
|
|
239
|
+
const name = normalizeWidgetParameterName(parameter.name, `${parameterContext}.name`)
|
|
240
|
+
assertOptionalString(parameter.title, `${parameterContext}.title`)
|
|
241
|
+
assertOptionalString(parameter.default, `${parameterContext}.default`)
|
|
242
|
+
|
|
243
|
+
if (seenNames.has(name)) {
|
|
244
|
+
throw new VoltraConfigNormalizationError(`${context}.parameters contains duplicate name '${name}'`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
seenNames.add(name)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
name,
|
|
251
|
+
title: parameter.title,
|
|
252
|
+
default: parameter.default,
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return { parameters }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeIOSAppIntent(
|
|
260
|
+
value: IOSWidgetAppIntentConfig | undefined,
|
|
261
|
+
context: string
|
|
262
|
+
): IOSWidgetAppIntentConfig | undefined {
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
assertObject(value, context)
|
|
268
|
+
|
|
269
|
+
if (!Array.isArray(value.parameters)) {
|
|
270
|
+
throw new VoltraConfigNormalizationError(`${context}.parameters must be an array`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const seenNames = new Set<string>()
|
|
274
|
+
const parameters = value.parameters.map((parameter, index) => {
|
|
275
|
+
const parameterContext = `${context}.parameters[${index}]`
|
|
276
|
+
assertObject(parameter, parameterContext)
|
|
277
|
+
const name = normalizeWidgetParameterName(parameter.name, `${parameterContext}.name`)
|
|
278
|
+
assertNonEmptyString(parameter.title, `${parameterContext}.title`)
|
|
279
|
+
assertOptionalString(parameter.default, `${parameterContext}.default`)
|
|
280
|
+
|
|
281
|
+
if (seenNames.has(name)) {
|
|
282
|
+
throw new VoltraConfigNormalizationError(`${context}.parameters contains duplicate name '${name}'`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
seenNames.add(name)
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
name,
|
|
289
|
+
title: parameter.title,
|
|
290
|
+
default: parameter.default,
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return { parameters }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeServerUpdate(
|
|
298
|
+
serverUpdate: { url: string; intervalMinutes?: number; refresh?: boolean },
|
|
299
|
+
context: string,
|
|
300
|
+
defaultIntervalMinutes: number,
|
|
301
|
+
defaultRefresh: boolean,
|
|
302
|
+
minimumIntervalMinutes: number
|
|
303
|
+
): { url: string; intervalMinutes: number; refresh: boolean } {
|
|
304
|
+
assertObject(serverUpdate, context)
|
|
305
|
+
assertNonEmptyString(serverUpdate.url, `${context}.url`)
|
|
306
|
+
|
|
307
|
+
if (serverUpdate.intervalMinutes !== undefined) {
|
|
308
|
+
if (typeof serverUpdate.intervalMinutes !== 'number' || !Number.isFinite(serverUpdate.intervalMinutes)) {
|
|
309
|
+
throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be a number`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!Number.isInteger(serverUpdate.intervalMinutes)) {
|
|
313
|
+
throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be an integer`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (serverUpdate.intervalMinutes < minimumIntervalMinutes) {
|
|
317
|
+
throw new VoltraConfigNormalizationError(`${context}.intervalMinutes must be at least ${minimumIntervalMinutes}`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (serverUpdate.refresh !== undefined && typeof serverUpdate.refresh !== 'boolean') {
|
|
322
|
+
throw new VoltraConfigNormalizationError(`${context}.refresh must be a boolean`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
url: serverUpdate.url,
|
|
327
|
+
intervalMinutes: serverUpdate.intervalMinutes ?? defaultIntervalMinutes,
|
|
328
|
+
refresh: serverUpdate.refresh ?? defaultRefresh,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeAndroidWidget(projectRoot: string, widget: AndroidWidgetConfig): NormalizedAndroidWidgetConfig {
|
|
333
|
+
assertObject(widget, 'android.widgets[]')
|
|
334
|
+
assertNonEmptyString(widget.id, 'android.widgets[].id')
|
|
335
|
+
assertValidWidgetId(widget.id, 'android.widgets[].id')
|
|
336
|
+
assertPositiveInteger(widget.targetCellWidth, `android.widgets[${widget.id}].targetCellWidth`)
|
|
337
|
+
assertPositiveInteger(widget.targetCellHeight, `android.widgets[${widget.id}].targetCellHeight`)
|
|
338
|
+
assertOptionalPositiveInteger(widget.minCellWidth, `android.widgets[${widget.id}].minCellWidth`)
|
|
339
|
+
assertOptionalPositiveInteger(widget.minCellHeight, `android.widgets[${widget.id}].minCellHeight`)
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...widget,
|
|
343
|
+
displayName: normalizeLabel(widget.displayName, `android.widgets[${widget.id}].displayName`),
|
|
344
|
+
description: normalizeLabel(widget.description, `android.widgets[${widget.id}].description`),
|
|
345
|
+
entry: normalizeOptionalWidgetEntry(widget.entry, `android.widgets[${widget.id}].entry`),
|
|
346
|
+
initialStatePath: normalizeInitialStatePath(
|
|
347
|
+
projectRoot,
|
|
348
|
+
widget.initialStatePath,
|
|
349
|
+
`android.widgets[${widget.id}].initialStatePath`
|
|
350
|
+
),
|
|
351
|
+
previewImage: resolveOptionalPathFromProjectRoot(projectRoot, widget.previewImage),
|
|
352
|
+
previewLayout: resolveOptionalPathFromProjectRoot(projectRoot, widget.previewLayout),
|
|
353
|
+
appIntent: normalizeAndroidAppIntent(widget.appIntent, `android.widgets[${widget.id}].appIntent`),
|
|
354
|
+
serverUpdate: widget.serverUpdate
|
|
355
|
+
? normalizeServerUpdate(
|
|
356
|
+
widget.serverUpdate,
|
|
357
|
+
`android.widgets[${widget.id}].serverUpdate`,
|
|
358
|
+
CLI_DEFAULTS.android.serverUpdateIntervalMinutes,
|
|
359
|
+
CLI_DEFAULTS.android.serverUpdateRefresh,
|
|
360
|
+
15
|
|
361
|
+
)
|
|
362
|
+
: undefined,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function normalizeIOSWidget(projectRoot: string, widget: IOSWidgetConfig): NormalizedIOSWidgetConfig {
|
|
367
|
+
assertObject(widget, 'ios.widgets[]')
|
|
368
|
+
assertNonEmptyString(widget.id, 'ios.widgets[].id')
|
|
369
|
+
assertValidWidgetId(widget.id, 'ios.widgets[].id')
|
|
370
|
+
|
|
371
|
+
if (widget.supportedFamilies !== undefined) {
|
|
372
|
+
if (!Array.isArray(widget.supportedFamilies)) {
|
|
373
|
+
throw new VoltraConfigNormalizationError(`ios.widgets[${widget.id}].supportedFamilies must be an array`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const family of widget.supportedFamilies) {
|
|
377
|
+
if (!VALID_IOS_WIDGET_FAMILIES.has(family)) {
|
|
378
|
+
throw new VoltraConfigNormalizationError(
|
|
379
|
+
`ios.widgets[${widget.id}].supportedFamilies contains invalid family '${family}'`
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...widget,
|
|
387
|
+
displayName: normalizeLabel(widget.displayName, `ios.widgets[${widget.id}].displayName`),
|
|
388
|
+
description: normalizeLabel(widget.description, `ios.widgets[${widget.id}].description`),
|
|
389
|
+
supportedFamilies: widget.supportedFamilies ?? [...CLI_DEFAULTS.ios.widgetFamilies],
|
|
390
|
+
entry: normalizeOptionalWidgetEntry(widget.entry, `ios.widgets[${widget.id}].entry`),
|
|
391
|
+
initialStatePath: normalizeInitialStatePath(
|
|
392
|
+
projectRoot,
|
|
393
|
+
widget.initialStatePath,
|
|
394
|
+
`ios.widgets[${widget.id}].initialStatePath`
|
|
395
|
+
),
|
|
396
|
+
appIntent: normalizeIOSAppIntent(widget.appIntent, `ios.widgets[${widget.id}].appIntent`),
|
|
397
|
+
serverUpdate: widget.serverUpdate
|
|
398
|
+
? normalizeServerUpdate(
|
|
399
|
+
widget.serverUpdate,
|
|
400
|
+
`ios.widgets[${widget.id}].serverUpdate`,
|
|
401
|
+
CLI_DEFAULTS.ios.serverUpdateIntervalMinutes,
|
|
402
|
+
CLI_DEFAULTS.ios.serverUpdateRefresh,
|
|
403
|
+
1
|
|
404
|
+
)
|
|
405
|
+
: undefined,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function assertUniqueWidgetIds(widgetIds: string[], context: string): void {
|
|
410
|
+
const seen = new Set<string>()
|
|
411
|
+
|
|
412
|
+
for (const widgetId of widgetIds) {
|
|
413
|
+
if (seen.has(widgetId)) {
|
|
414
|
+
throw new VoltraConfigNormalizationError(`Duplicate ${context} widget ID '${widgetId}'`)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
seen.add(widgetId)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function assertValidWidgetId(widgetId: string, context: string): void {
|
|
422
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(widgetId)) {
|
|
423
|
+
throw new VoltraConfigNormalizationError(
|
|
424
|
+
`${context} must start with a letter or underscore and contain only alphanumeric characters and underscores`
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function assertValidIOSTargetName(targetName: string, context: string): void {
|
|
430
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(targetName)) {
|
|
431
|
+
throw new VoltraConfigNormalizationError(
|
|
432
|
+
`${context} must start with a letter and contain only letters, numbers, and underscores`
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeAndroidConfig(
|
|
438
|
+
projectRoot: string,
|
|
439
|
+
config: LoadedVoltraConfig['config']['android']
|
|
440
|
+
): NormalizedVoltraAndroidConfig | undefined {
|
|
441
|
+
if (config === undefined) {
|
|
442
|
+
return undefined
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
assertObject(config, 'android')
|
|
446
|
+
assertOptionalBoolean(config.enableNotifications, 'android.enableNotifications')
|
|
447
|
+
assertOptionalStringArray(config.fonts, 'android.fonts')
|
|
448
|
+
assertOptionalString(config.userImagesPath, 'android.userImagesPath')
|
|
449
|
+
|
|
450
|
+
if (config.project !== undefined) {
|
|
451
|
+
assertObject(config.project, 'android.project')
|
|
452
|
+
assertOptionalString(config.project.rootDir, 'android.project.rootDir')
|
|
453
|
+
assertOptionalString(config.project.appModuleName, 'android.project.appModuleName')
|
|
454
|
+
assertOptionalString(config.project.manifestPath, 'android.project.manifestPath')
|
|
455
|
+
assertOptionalString(config.project.packageName, 'android.project.packageName')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (config.widgets !== undefined && !Array.isArray(config.widgets)) {
|
|
459
|
+
throw new VoltraConfigNormalizationError('android.widgets must be an array')
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const widgets = (config.widgets ?? []).map((widget) => normalizeAndroidWidget(projectRoot, widget))
|
|
463
|
+
assertUniqueWidgetIds(
|
|
464
|
+
widgets.map((widget) => widget.id),
|
|
465
|
+
'android'
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
enableNotifications: config.enableNotifications ?? CLI_DEFAULTS.android.enableNotifications,
|
|
470
|
+
widgets,
|
|
471
|
+
fonts: (config.fonts ?? []).map((fontPath) => resolvePathFromProjectRoot(projectRoot, fontPath)),
|
|
472
|
+
userImagesPath: resolvePathFromProjectRoot(
|
|
473
|
+
projectRoot,
|
|
474
|
+
config.userImagesPath ?? CLI_DEFAULTS.android.userImagesPath
|
|
475
|
+
),
|
|
476
|
+
project: {
|
|
477
|
+
rootDir: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.rootDir),
|
|
478
|
+
appModuleName: config.project?.appModuleName,
|
|
479
|
+
manifestPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.manifestPath),
|
|
480
|
+
packageName: config.project?.packageName,
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function normalizeIOSConfig(
|
|
486
|
+
projectRoot: string,
|
|
487
|
+
config: LoadedVoltraConfig['config']['ios']
|
|
488
|
+
): NormalizedVoltraIOSConfig | undefined {
|
|
489
|
+
if (config === undefined) {
|
|
490
|
+
return undefined
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
assertObject(config, 'ios')
|
|
494
|
+
assertOptionalBoolean(config.enablePushNotifications, 'ios.enablePushNotifications')
|
|
495
|
+
assertOptionalString(config.groupIdentifier, 'ios.groupIdentifier')
|
|
496
|
+
assertOptionalString(config.deploymentTarget, 'ios.deploymentTarget')
|
|
497
|
+
assertOptionalString(config.targetName, 'ios.targetName')
|
|
498
|
+
assertOptionalStringArray(config.fonts, 'ios.fonts')
|
|
499
|
+
assertOptionalString(config.userImagesPath, 'ios.userImagesPath')
|
|
500
|
+
assertOptionalString(config.keychainGroup, 'ios.keychainGroup')
|
|
501
|
+
|
|
502
|
+
if (config.project !== undefined) {
|
|
503
|
+
assertObject(config.project, 'ios.project')
|
|
504
|
+
assertOptionalString(config.project.rootDir, 'ios.project.rootDir')
|
|
505
|
+
assertOptionalString(config.project.xcodeprojPath, 'ios.project.xcodeprojPath')
|
|
506
|
+
assertOptionalString(config.project.mainTargetName, 'ios.project.mainTargetName')
|
|
507
|
+
assertOptionalString(config.project.infoPlistPath, 'ios.project.infoPlistPath')
|
|
508
|
+
assertOptionalString(config.project.entitlementsPath, 'ios.project.entitlementsPath')
|
|
509
|
+
assertOptionalString(config.project.podfilePath, 'ios.project.podfilePath')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (config.widgets !== undefined && !Array.isArray(config.widgets)) {
|
|
513
|
+
throw new VoltraConfigNormalizationError('ios.widgets must be an array')
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (config.targetName !== undefined) {
|
|
517
|
+
assertValidIOSTargetName(config.targetName, 'ios.targetName')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const widgets = (config.widgets ?? []).map((widget) => normalizeIOSWidget(projectRoot, widget))
|
|
521
|
+
assertUniqueWidgetIds(
|
|
522
|
+
widgets.map((widget) => widget.id),
|
|
523
|
+
'ios'
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
enablePushNotifications: config.enablePushNotifications ?? CLI_DEFAULTS.ios.enablePushNotifications,
|
|
528
|
+
groupIdentifier: config.groupIdentifier,
|
|
529
|
+
widgets,
|
|
530
|
+
deploymentTarget: config.deploymentTarget ?? CLI_DEFAULTS.ios.deploymentTarget,
|
|
531
|
+
targetName: config.targetName,
|
|
532
|
+
fonts: (config.fonts ?? []).map((fontPath) => resolvePathFromProjectRoot(projectRoot, fontPath)),
|
|
533
|
+
userImagesPath: resolvePathFromProjectRoot(projectRoot, config.userImagesPath ?? CLI_DEFAULTS.ios.userImagesPath),
|
|
534
|
+
keychainGroup: config.keychainGroup,
|
|
535
|
+
project: {
|
|
536
|
+
rootDir: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.rootDir),
|
|
537
|
+
xcodeprojPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.xcodeprojPath),
|
|
538
|
+
mainTargetName: config.project?.mainTargetName,
|
|
539
|
+
infoPlistPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.infoPlistPath),
|
|
540
|
+
entitlementsPath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.entitlementsPath),
|
|
541
|
+
podfilePath: resolveOptionalPathFromProjectRoot(projectRoot, config.project?.podfilePath),
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function normalizeVoltraConfig(loadedConfig: LoadedVoltraConfig): NormalizedVoltraConfig {
|
|
547
|
+
assertObject(loadedConfig.config, 'config')
|
|
548
|
+
assertOptionalString(loadedConfig.config.projectRoot, 'projectRoot')
|
|
549
|
+
|
|
550
|
+
const projectRoot = resolvePathFromProjectRoot(
|
|
551
|
+
loadedConfig.configDir,
|
|
552
|
+
loadedConfig.config.projectRoot ?? loadedConfig.configDir
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
configPath: loadedConfig.configPath,
|
|
557
|
+
configDir: loadedConfig.configDir,
|
|
558
|
+
projectRoot,
|
|
559
|
+
android: normalizeAndroidConfig(projectRoot, loadedConfig.config.android),
|
|
560
|
+
ios: normalizeIOSConfig(projectRoot, loadedConfig.config.ios),
|
|
561
|
+
}
|
|
562
|
+
}
|