@mpgd/cli 0.33.2 → 0.34.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.
@@ -0,0 +1,490 @@
1
+ /** Inspect only the effective App target, not unrelated Xcode extensions. */
2
+ export function assertNativeShellIdentity(platform, source, expectedAppId) {
3
+ if (platform === 'android') {
4
+ assertAndroidIdentity(source, expectedAppId);
5
+ }
6
+ else {
7
+ assertIosAppIdentity(source, expectedAppId);
8
+ }
9
+ }
10
+ function assertAndroidIdentity(source, expectedAppId) {
11
+ const clean = stripGradleComments(source);
12
+ const assignments = [...clean.matchAll(/\bapplicationId\s*(?:=\s*)?["']([^"']+)["'](?=\s*(?:;|\r?\n|\}|$))/gu)]
13
+ .map((match) => match[1]);
14
+ const mentions = countGradleIdentityWrites(clean, 'applicationId');
15
+ if (assignments.length === 0 || assignments.length !== mentions
16
+ || assignments.some((value) => value !== expectedAppId)
17
+ || hasAndroidReleaseIdSuffix(clean) || hasGradleBracketIdentityWrite(clean)) {
18
+ throw new Error('Existing android project app ID differs or cannot be read safely.');
19
+ }
20
+ }
21
+ function hasAndroidReleaseIdSuffix(source) {
22
+ const code = maskGradleStrings(source);
23
+ const suffix = '(?:applicationIdSuffix|setApplicationIdSuffix)';
24
+ const prefixes = [
25
+ '\\bbuildTypes\\s*\\.\\s*release',
26
+ '\\bbuildTypes\\s*\\[\\s*["\']release["\']\\s*\\]',
27
+ '\\bbuildTypes\\s*\\.\\s*(?:getByName|named)\\s*\\(\\s*["\']release["\']\\s*\\)',
28
+ '\\b(?:getByName|named)\\s*\\(\\s*["\']release["\']\\s*\\)',
29
+ '\\brelease',
30
+ ];
31
+ const qualifiedSuffix = prefixes.some((prefix) => {
32
+ const expression = new RegExp(`${prefix}\\s*\\.\\s*${suffix}\\b`, 'gu');
33
+ return [...source.matchAll(expression)].some((match) => {
34
+ const field = new RegExp(`${suffix}$`, 'u').exec(match[0])?.[0] ?? '';
35
+ const offset = (match.index ?? 0) + match[0].lastIndexOf(field);
36
+ return field !== '' && code.slice(offset, offset + field.length) === field;
37
+ });
38
+ });
39
+ if (qualifiedSuffix) {
40
+ return true;
41
+ }
42
+ const releaseBlocks = [
43
+ /\brelease\s*\{/gu,
44
+ /\brelease\s+by\s+getting\s*\{/gu,
45
+ /\brelease\s*\.\s*(?:apply|configure)\s*\{/gu,
46
+ /\b(?:getByName|named)\s*\(\s*["']release["']\s*\)\s*\{/gu,
47
+ /\b(?:getByName|named)\s*\(\s*["']release["']\s*\)\s*\.\s*(?:apply|configure)\s*\{/gu,
48
+ /\bbuildTypes\s*\[\s*["']release["']\s*\]\s*\{/gu,
49
+ /\bbuildTypes\s*\[\s*["']release["']\s*\]\s*\.\s*(?:apply|configure)\s*\{/gu,
50
+ ];
51
+ return releaseBlocks.some((expression) => [...source.matchAll(expression)]
52
+ .some((match) => match.index !== undefined
53
+ && code[(match.index ?? 0) + match[0].length - 1] === '{'
54
+ && /\b(?:applicationIdSuffix|setApplicationIdSuffix)\b/u.test(maskGradleStrings(readBracedText(source, source.indexOf('{', match.index))))));
55
+ }
56
+ export function assertAndroidSettingsAppProject(source) {
57
+ const clean = stripGradleComments(source);
58
+ const code = maskGradleStrings(clean);
59
+ const includes = [...clean.matchAll(/\binclude\s*(?:\(([^)]*)\)|([^\r\n;]+))/gu)];
60
+ const includesApp = includes.some((match) => code.slice(match.index, match.index + 7)
61
+ === 'include' && /(?:^|,)\s*["']:app["']\s*(?:,|$)/u.test(match[1] ?? match[2] ?? ''));
62
+ if (!includesApp) {
63
+ throw new Error('Android settings must include :app at android/app without remapping it.');
64
+ }
65
+ assertAndroidSettingsNoAppRemap(clean);
66
+ }
67
+ export function assertAndroidSettingsNoAppRemap(source) {
68
+ const clean = stripGradleComments(source);
69
+ const code = maskGradleStrings(clean);
70
+ const appRemap = [...clean.matchAll(/\b(project|findProject)\s*\(\s*["']:app["']\s*\)/gu)]
71
+ .some((match) => code.slice(match.index, match.index + (match[1]?.length ?? 0))
72
+ === match[1]);
73
+ if (appRemap) {
74
+ throw new Error('Android settings must include :app at android/app without remapping it.');
75
+ }
76
+ }
77
+ export function stripGradleComments(source) {
78
+ let output = '';
79
+ let quote;
80
+ for (let index = 0; index < source.length; index += 1) {
81
+ const character = source[index] ?? '';
82
+ const next = source[index + 1];
83
+ if (quote !== undefined) {
84
+ output += character;
85
+ if (character === '\\' && next !== undefined) {
86
+ output += next;
87
+ index += 1;
88
+ }
89
+ else if (character === quote) {
90
+ quote = undefined;
91
+ }
92
+ continue;
93
+ }
94
+ if (character === '"' || character === "'") {
95
+ quote = character;
96
+ output += character;
97
+ continue;
98
+ }
99
+ if (character === '/' && next === '/') {
100
+ index += 2;
101
+ while (index < source.length && source[index] !== '\n') {
102
+ index += 1;
103
+ }
104
+ output += '\n';
105
+ continue;
106
+ }
107
+ if (character === '/' && next === '*') {
108
+ index += 2;
109
+ while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
110
+ output += source[index] === '\n' ? '\n' : ' ';
111
+ index += 1;
112
+ }
113
+ index += 1;
114
+ continue;
115
+ }
116
+ output += character;
117
+ }
118
+ return output;
119
+ }
120
+ export function hasGradleIdentityMutation(source) {
121
+ const clean = stripGradleComments(source);
122
+ const masked = maskGradleStrings(clean);
123
+ if (hasGradlePropertySetter(clean) || hasGradleBracketIdentityWrite(clean)) {
124
+ return true;
125
+ }
126
+ const setter = /\bset(?:ApplicationId|ApplicationIdSuffix|VersionCode|VersionName|VersionNameSuffix)\s*\(/u;
127
+ const field = '(?:applicationId|applicationIdSuffix|versionCode|versionName|versionNameSuffix)';
128
+ const assignment = new RegExp(`\\b${field}(?:\\s*(?:\\+?=|\\(|["'\\d]|\\.set\\s*\\()|[ \\t]+[A-Za-z_$])`, 'u');
129
+ return setter.test(masked) || assignment.test(masked);
130
+ }
131
+ export function hasGradleBracketIdentityWrite(source) {
132
+ const clean = stripGradleComments(source);
133
+ const code = maskGradleStrings(clean);
134
+ const key = '(?:applicationId|applicationIdSuffix|versionCode|versionName|versionNameSuffix)';
135
+ const bracketWrite = `\\[\\s*["']${key}["']\\s*\\]\\s*(?:\\+?=|\\.\\s*set\\s*\\(|\\()`;
136
+ const expression = new RegExp(bracketWrite, 'gu');
137
+ return [...clean.matchAll(expression)].some((match) => code[match.index] === '[');
138
+ }
139
+ export function hasAndroidDisplayNameResourceOverride(source) {
140
+ const clean = stripGradleComments(source);
141
+ const code = maskGradleStrings(clean);
142
+ return [...clean.matchAll(/\bresValue\b/gu)].some((match) => {
143
+ if (code.slice(match.index, match.index + 8) !== 'resValue') {
144
+ return false;
145
+ }
146
+ const tail = clean.slice(match.index);
147
+ const literal = /^resValue\s*(?:\(\s*)?["'][^"']+["']\s*,\s*["']([^"']+)["']/u.exec(tail);
148
+ return literal === null || literal[1] === 'app_name';
149
+ });
150
+ }
151
+ export function hasAndroidResourceSourceSetOverride(source) {
152
+ const code = maskGradleStrings(stripGradleComments(source));
153
+ return /\bsourceSets\b/u.test(code) && /\bres\b/u.test(code);
154
+ }
155
+ export function hasAndroidManifestSourceSetOverride(source) {
156
+ const code = maskGradleStrings(stripGradleComments(source));
157
+ return /\bsourceSets\b/u.test(code) && /\bmanifest\b/u.test(code);
158
+ }
159
+ export function hasGradleTaskAction(source) {
160
+ const code = maskGradleStrings(stripGradleComments(source));
161
+ const withoutStandardClean = code.replace(/\btask\s+clean\s*\(\s*type\s*:\s*Delete\s*\)\s*\{\s*delete\s+rootProject\.buildDir\s*;?\s*\}/gu, '');
162
+ return /\b(?:doFirst|doLast|whenTaskAdded|whenReady)\s*(?:\(|\{)/u.test(withoutStandardClean)
163
+ || /\btasks\s*(?:\.|\[|\{)/u.test(withoutStandardClean)
164
+ || /\btask\s*(?:\([^)]*\)|[A-Za-z_]\w*(?:\s*\([^)]*\))?)\s*\{/u.test(withoutStandardClean);
165
+ }
166
+ export function hasGradlePropertySetter(source) {
167
+ const clean = stripGradleComments(source);
168
+ const masked = maskGradleStrings(clean);
169
+ // A computed key can target any release identity; do not try to evaluate Groovy here.
170
+ return /\bsetProperty\s*\(/u.test(masked);
171
+ }
172
+ export function countGradleIdentityWrites(source, key) {
173
+ const masked = maskGradleStrings(stripGradleComments(source));
174
+ const expression = new RegExp(`\\b${key}\\b`, 'gu');
175
+ return [...masked.matchAll(expression)].filter((match) => {
176
+ const index = match.index ?? 0;
177
+ const rest = masked.slice(index + key.length);
178
+ if (masked[index - 1] === '.') {
179
+ return /^\s*(?:\+?=|\(|\.set\s*\()/u.test(rest);
180
+ }
181
+ return /^\s*(?:\+?=|\(|["'\d]|[A-Za-z_$])/u.test(rest);
182
+ }).length;
183
+ }
184
+ export function maskGradleStrings(clean) {
185
+ let masked = '';
186
+ let quote;
187
+ for (let index = 0; index < clean.length; index += 1) {
188
+ const character = clean[index] ?? '';
189
+ if (quote !== undefined) {
190
+ if (character === '\\' && index + 1 < clean.length) {
191
+ masked += ' ';
192
+ index += 1;
193
+ }
194
+ else if (character === quote) {
195
+ masked += character;
196
+ quote = undefined;
197
+ }
198
+ else {
199
+ masked += character === '\n' ? '\n' : ' ';
200
+ }
201
+ }
202
+ else if (character === '"' || character === "'") {
203
+ quote = character;
204
+ masked += character;
205
+ }
206
+ else {
207
+ masked += character;
208
+ }
209
+ }
210
+ return masked;
211
+ }
212
+ function assertIosAppIdentity(source, expectedAppId) {
213
+ const listId = readIosAppConfigurationList(source);
214
+ let actual = readIosReleaseBundleId(source, listId);
215
+ if (actual === undefined || actual === '$(inherited)') {
216
+ const projectListId = readIosProjectConfigurationList(source);
217
+ if (projectListId === undefined) {
218
+ throw new Error('Existing ios project inherited App ID has no project configuration.');
219
+ }
220
+ actual = readIosReleaseBundleId(source, projectListId);
221
+ }
222
+ if (actual !== expectedAppId) {
223
+ throw new Error('Existing ios project app ID differs or cannot be read safely.');
224
+ }
225
+ }
226
+ export function readIosReleaseInfoPlist(source) {
227
+ const target = readIosReleaseConfiguration(source, readIosAppConfigurationList(source));
228
+ let value = readIosInfoPlistSetting(target);
229
+ if (value === undefined) {
230
+ if (/\bbaseConfigurationReference\s*=/u.test(target)) {
231
+ throw new Error('Existing ios project Release Info.plist xcconfig cannot be read safely.');
232
+ }
233
+ const projectListId = readIosProjectConfigurationList(source);
234
+ if (projectListId === undefined) {
235
+ throw new Error('Existing ios project Release Info.plist has no project configuration.');
236
+ }
237
+ const project = readIosReleaseConfiguration(source, projectListId);
238
+ value = readIosInfoPlistSetting(project);
239
+ if (value === undefined && /\bbaseConfigurationReference\s*=/u.test(project)) {
240
+ throw new Error('Existing ios project Release Info.plist xcconfig cannot be read safely.');
241
+ }
242
+ }
243
+ if (value === undefined) {
244
+ throw new Error('Existing ios project Release Info.plist cannot be read safely.');
245
+ }
246
+ return value;
247
+ }
248
+ export function assertIosReleaseProductName(source) {
249
+ const target = readIosReleaseConfiguration(source, readIosAppConfigurationList(source));
250
+ let name = readIosProductName(target);
251
+ if (name === undefined || name === '$(inherited)') {
252
+ if (/\bbaseConfigurationReference\s*=/u.test(target)) {
253
+ throw new Error('Existing ios project Release product name xcconfig is unsupported.');
254
+ }
255
+ const projectList = readIosProjectConfigurationList(source);
256
+ if (projectList !== undefined) {
257
+ const project = readIosReleaseConfiguration(source, projectList);
258
+ name = readIosProductName(project);
259
+ if ((name === undefined || name === '$(inherited)')
260
+ && /\bbaseConfigurationReference\s*=/u.test(project)) {
261
+ throw new Error('Existing ios project Release product name xcconfig is unsupported.');
262
+ }
263
+ }
264
+ }
265
+ if (name !== undefined && name !== '$(inherited)'
266
+ && name !== 'App' && name !== '$(TARGET_NAME)') {
267
+ throw new Error('Existing ios project Release PRODUCT_NAME must build App.app.');
268
+ }
269
+ }
270
+ export function assertIosReleaseInfoPlistExpansion(source) {
271
+ const target = readIosReleaseConfiguration(source, readIosAppConfigurationList(source));
272
+ assertIosInfoPlistExpansionSetting(target);
273
+ const projectList = readIosProjectConfigurationList(source);
274
+ if (projectList !== undefined) {
275
+ assertIosInfoPlistExpansionSetting(readIosReleaseConfiguration(source, projectList));
276
+ }
277
+ }
278
+ export function assertIosReleasePackagingSettings(source) {
279
+ const target = readIosReleaseConfiguration(source, readIosAppConfigurationList(source));
280
+ const projectList = readIosProjectConfigurationList(source);
281
+ const project = projectList === undefined
282
+ ? undefined
283
+ : readIosReleaseConfiguration(source, projectList);
284
+ const setting = (configuration, key) => {
285
+ if (configuration === undefined) {
286
+ return undefined;
287
+ }
288
+ const conditional = new RegExp(`\\b${key}\\[[^\\]\\r\\n]+\\]`, 'u');
289
+ if (conditional.test(configuration)) {
290
+ throw new Error(`Existing ios project conditional Release ${key} is unsupported.`);
291
+ }
292
+ const expression = `(?:^|[\\s{;])["']?${key}["']?\\s*=\\s*([^;]+);`;
293
+ const pattern = new RegExp(expression, 'gu');
294
+ const values = [...configuration.matchAll(pattern)]
295
+ .map((match) => match[1]?.trim().replace(/^["']|["']$/gu, ''));
296
+ if (values.length > 1 || /\bbaseConfigurationReference\s*=/u.test(configuration)) {
297
+ throw new Error(`Existing ios project Release ${key} cannot be read safely.`);
298
+ }
299
+ return values[0];
300
+ };
301
+ const effective = (key) => {
302
+ const targetValue = setting(target, key);
303
+ return targetValue === undefined || targetValue === '$(inherited)'
304
+ ? setting(project, key)
305
+ : targetValue;
306
+ };
307
+ const preprocessing = effective('INFOPLIST_PREPROCESS');
308
+ if (preprocessing !== undefined && preprocessing !== 'NO') {
309
+ throw new Error('Existing ios project Release Info.plist preprocessing is unsupported.');
310
+ }
311
+ for (const key of ['INFOPLIST_PREPROCESSOR_DEFINITIONS', 'INFOPLIST_PREFIX_HEADER']) {
312
+ if (effective(key) !== undefined) {
313
+ throw new Error(`Existing ios project Release ${key} is unsupported.`);
314
+ }
315
+ }
316
+ const skipInstall = effective('SKIP_INSTALL');
317
+ if (skipInstall !== undefined && skipInstall !== 'NO') {
318
+ throw new Error('Existing ios project Release SKIP_INSTALL must be NO.');
319
+ }
320
+ }
321
+ function assertIosInfoPlistExpansionSetting(configuration) {
322
+ const key = 'INFOPLIST_EXPAND_BUILD_SETTINGS';
323
+ const conditional = /(?:^|[\s{;])["']?INFOPLIST_EXPAND_BUILD_SETTINGS(?:\[[^\]\r\n]+\])+["']?\s*=/u;
324
+ if (conditional.test(configuration)) {
325
+ throw new Error('Existing ios project conditional Release Info.plist expansion is unsupported.');
326
+ }
327
+ const values = [...configuration.matchAll(/(?:^|[\s{;])["']?INFOPLIST_EXPAND_BUILD_SETTINGS["']?\s*=\s*([^;]+);/gu)].map((match) => match[1]?.trim().replace(/^["']|["']$/gu, ''));
328
+ if (values.length === 0 && /\bbaseConfigurationReference\s*=/u.test(configuration)) {
329
+ throw new Error('Existing ios project Release Info.plist expansion xcconfig is unsupported.');
330
+ }
331
+ if (values.length > 1 || values.some((value) => value !== 'YES')) {
332
+ throw new Error(`Existing ios project Release ${key} must be enabled.`);
333
+ }
334
+ }
335
+ function readIosProductName(configuration) {
336
+ if (/(?:^|[\s{;])["']?PRODUCT_NAME(?:\[[^\]\r\n]+\])+["']?\s*=/u
337
+ .test(configuration)) {
338
+ throw new Error('Existing ios project conditional Release PRODUCT_NAME is unsupported.');
339
+ }
340
+ const values = [...configuration.matchAll(/(?:^|[\s{;])["']?PRODUCT_NAME["']?\s*=\s*([^;]+);/gu)].map((match) => match[1]?.trim().replace(/^["']|["']$/gu, ''));
341
+ if (values.length > 1) {
342
+ throw new Error('Existing ios project Release PRODUCT_NAME is ambiguous.');
343
+ }
344
+ return values[0];
345
+ }
346
+ function readIosInfoPlistSetting(configuration) {
347
+ if (/(?:^|[\s{;])["']?INFOPLIST_FILE(?:\[[^\]\r\n]+\])+["']?\s*=/u.test(configuration)) {
348
+ throw new Error('Existing ios project conditional Release Info.plist is unsupported.');
349
+ }
350
+ const values = [...configuration.matchAll(/(?:^|[\s{;])["']?INFOPLIST_FILE["']?\s*=\s*([^;]+);/gu)]
351
+ .map((match) => match[1]?.trim().replace(/^["']|["']$/gu, ''));
352
+ if (values.length > 1) {
353
+ throw new Error('Existing ios project Release Info.plist is ambiguous.');
354
+ }
355
+ const value = values[0];
356
+ if (value === undefined || value === '$(inherited)') {
357
+ return undefined;
358
+ }
359
+ if (value.includes('$') || value.includes('\\') || pathIsUnsafe(value)) {
360
+ throw new Error('Existing ios project Release Info.plist cannot be read safely.');
361
+ }
362
+ return value;
363
+ }
364
+ function pathIsUnsafe(value) {
365
+ return value.startsWith('/') || value.split('/').some((part) => part === '..' || part === '');
366
+ }
367
+ function readIosAppConfigurationList(source) {
368
+ const target = readPbxObject(source, readIosAppTargetId(source));
369
+ const listId = /\bbuildConfigurationList\s*=\s*([A-F0-9]+)\b/u.exec(target)?.[1];
370
+ if (listId === undefined) {
371
+ throw new Error('Existing ios project App target configuration is missing.');
372
+ }
373
+ return listId;
374
+ }
375
+ export function readIosAppTargetId(source) {
376
+ const appTargets = [...source.matchAll(/\b([A-F0-9]+)\s*\/\*\s*App\s*\*\/\s*=\s*\{/gu)]
377
+ .map((match) => match[1] ?? '')
378
+ .filter((id) => {
379
+ const block = readPbxObject(source, id);
380
+ return /\bisa\s*=\s*PBXNativeTarget;/u.test(block)
381
+ && /\bname\s*=\s*"?App"?\s*;/u.test(block);
382
+ });
383
+ if (appTargets.length !== 1) {
384
+ throw new Error('Existing ios project App target could not be read safely.');
385
+ }
386
+ const targetId = appTargets[0] ?? '';
387
+ const target = readPbxObject(source, targetId);
388
+ const productId = /\bproductReference\s*=\s*([A-F0-9]+)\b/u.exec(target)?.[1];
389
+ if (!/\bproductType\s*=\s*"?com\.apple\.product-type\.application"?\s*;/u.test(target)
390
+ || productId === undefined) {
391
+ throw new Error('Existing ios App target must produce an application.');
392
+ }
393
+ const product = readPbxObject(source, productId);
394
+ if (!/\bisa\s*=\s*PBXFileReference\s*;/u.test(product)
395
+ || !/\b(?:explicitFileType|lastKnownFileType)\s*=\s*wrapper\.application\s*;/u.test(product)
396
+ || !/\bpath\s*=\s*"?App\.app"?\s*;/u.test(product)
397
+ || !/\bsourceTree\s*=\s*BUILT_PRODUCTS_DIR\s*;/u.test(product)) {
398
+ throw new Error('Existing ios App target product must be App.app.');
399
+ }
400
+ return targetId;
401
+ }
402
+ function readIosProjectConfigurationList(source) {
403
+ const projects = [...source.matchAll(/\b([A-F0-9]+)\s*\/\*[^*]+\*\/\s*=\s*\{/gu)]
404
+ .map((match) => readPbxObject(source, match[1] ?? ''))
405
+ .filter((block) => /\bisa\s*=\s*PBXProject;/u.test(block));
406
+ return projects.length === 1
407
+ ? /\bbuildConfigurationList\s*=\s*([A-F0-9]+)\b/u.exec(projects[0] ?? '')?.[1]
408
+ : undefined;
409
+ }
410
+ function readIosReleaseBundleId(source, listId) {
411
+ const configuration = readIosReleaseConfiguration(source, listId);
412
+ const hasBaseConfiguration = /\bbaseConfigurationReference\s*=/u.test(configuration);
413
+ const conditionalId = /(?:^|[\s{;])["']?PRODUCT_BUNDLE_IDENTIFIER(?:\[[^\]\r\n]+\])+["']?\s*=/u;
414
+ if (conditionalId.test(configuration)) {
415
+ throw new Error('Existing ios project conditional Release bundle ID is unsupported.');
416
+ }
417
+ const values = [...configuration.matchAll(/(?:^|[\s{;])["']?PRODUCT_BUNDLE_IDENTIFIER["']?\s*=\s*([^;]+);/gu)]
418
+ .map((match) => match[1]?.trim().replace(/^["']|["']$/gu, ''));
419
+ if (values.length > 1) {
420
+ throw new Error('Existing ios project Release bundle ID is ambiguous.');
421
+ }
422
+ if ((values[0] === undefined || values[0] === '$(inherited)') && hasBaseConfiguration) {
423
+ throw new Error('Existing ios project Release xcconfig identity cannot be read safely.');
424
+ }
425
+ return values[0];
426
+ }
427
+ function readIosReleaseConfiguration(source, listId) {
428
+ const list = readPbxObject(source, listId);
429
+ const configurations = /\bbuildConfigurations\s*=\s*\(([^)]*)\)/u.exec(list)?.[1];
430
+ const releaseIds = configurations === undefined ? []
431
+ : [...configurations.matchAll(/\b([A-F0-9]+)\s*\/\*\s*Release\s*\*\//gu)]
432
+ .map((match) => match[1]);
433
+ if (releaseIds.length !== 1) {
434
+ throw new Error('Existing ios project App Release configuration is missing or ambiguous.');
435
+ }
436
+ return stripGradleComments(readPbxObject(source, releaseIds[0] ?? ''));
437
+ }
438
+ function readPbxObject(source, id) {
439
+ const marker = new RegExp(`\\b${id}\\s*\\/\\*[^*]+\\*\\/\\s*=\\s*\\{`, 'u').exec(source);
440
+ if (marker?.index === undefined) {
441
+ throw new Error(`Existing ios project object ${id} could not be read safely.`);
442
+ }
443
+ const opening = source.indexOf('{', marker.index);
444
+ return readBracedText(source, opening);
445
+ }
446
+ function readBracedText(source, opening) {
447
+ if (opening < 0) {
448
+ throw new Error('Existing native project block is missing.');
449
+ }
450
+ let depth = 0;
451
+ let quote = false;
452
+ let blockComment = false;
453
+ for (let index = opening; index < source.length; index += 1) {
454
+ const character = source[index];
455
+ const next = source[index + 1];
456
+ if (blockComment) {
457
+ if (character === '*' && next === '/') {
458
+ blockComment = false;
459
+ index += 1;
460
+ }
461
+ continue;
462
+ }
463
+ if (quote) {
464
+ if (character === '\\') {
465
+ index += 1;
466
+ }
467
+ else if (character === '"') {
468
+ quote = false;
469
+ }
470
+ continue;
471
+ }
472
+ if (character === '/' && next === '*') {
473
+ blockComment = true;
474
+ index += 1;
475
+ }
476
+ else if (character === '"') {
477
+ quote = true;
478
+ }
479
+ else if (character === '{') {
480
+ depth += 1;
481
+ }
482
+ else if (character === '}') {
483
+ depth -= 1;
484
+ if (depth === 0) {
485
+ return source.slice(opening, index + 1);
486
+ }
487
+ }
488
+ }
489
+ throw new Error('Existing native project block is incomplete.');
490
+ }
@@ -23,3 +23,4 @@ export interface ProductionTargetPolicy {
23
23
  };
24
24
  }
25
25
  export declare function assertProductionTargetReadiness(input: ProductionTargetReadinessInput): void;
26
+ export declare function isNonPublicServiceHostname(hostname: string): boolean;
@@ -187,7 +187,7 @@ function assertAuthoritativeGameServicesUrl(value, target) {
187
187
  + 'without embedded credentials.');
188
188
  }
189
189
  }
190
- function isNonPublicServiceHostname(hostname) {
190
+ export function isNonPublicServiceHostname(hostname) {
191
191
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/u, '');
192
192
  if (normalized === 'localhost'
193
193
  || normalized.endsWith('.localhost')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.33.2",
3
+ "version": "0.34.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "@gunshi/plugin-completion": "^0.35.1",
44
44
  "@gunshi/plugin-i18n": "^0.35.1",
45
45
  "@gunshi/resources": "^0.35.1",
46
+ "@xmldom/xmldom": "0.9.12",
46
47
  "entities": "^8.0.0",
47
48
  "esbuild": "^0.28.1",
48
49
  "gunshi": "^0.35.1",
@@ -53,18 +54,18 @@
53
54
  "@types/node": "^24.0.0",
54
55
  "ttsc": "0.30.4",
55
56
  "typescript": "7.0.2",
56
- "@mpgd/adapter-capacitor": "0.4.12",
57
- "@mpgd/adapter-ait": "0.12.4",
58
- "@mpgd/adapter-browser": "0.7.5",
59
- "@mpgd/adapter-devvit": "0.9.10",
60
- "@mpgd/adapter-verse8": "0.3.8",
61
- "@mpgd/analytics": "0.3.14",
62
- "@mpgd/bridge": "0.9.1",
63
- "@mpgd/catalog": "0.7.4",
64
- "@mpgd/game-services": "0.15.3",
65
- "@mpgd/platform": "0.12.2",
66
- "@mpgd/i18n": "0.6.3",
67
- "@mpgd/target-config": "0.15.2"
57
+ "@mpgd/adapter-ait": "0.12.5",
58
+ "@mpgd/adapter-browser": "0.7.6",
59
+ "@mpgd/adapter-devvit": "0.9.11",
60
+ "@mpgd/adapter-capacitor": "0.5.0",
61
+ "@mpgd/adapter-verse8": "0.3.9",
62
+ "@mpgd/bridge": "0.10.0",
63
+ "@mpgd/catalog": "0.7.5",
64
+ "@mpgd/i18n": "0.6.4",
65
+ "@mpgd/platform": "0.13.0",
66
+ "@mpgd/target-config": "0.16.0",
67
+ "@mpgd/analytics": "0.3.15",
68
+ "@mpgd/game-services": "0.16.0"
68
69
  },
69
70
  "main": "./dist/index.js",
70
71
  "types": "./dist/index.d.ts",
@@ -57,6 +57,7 @@
57
57
  "dependencies": {
58
58
  "@ait-co/polyfill": "^0.1.19",
59
59
  "@apps-in-toss/web-framework": "3.5.0",
60
+ "@capacitor/app": "8.1.1",
60
61
  "@capacitor/core": "8.5.2",
61
62
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__",
62
63
  "@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION_ADAPTER_BROWSER__",
@@ -1,8 +1,10 @@
1
1
  :root {
2
- --mpgd-safe-area-top: env(safe-area-inset-top, 0px);
3
- --mpgd-safe-area-right: env(safe-area-inset-right, 0px);
4
- --mpgd-safe-area-bottom: env(safe-area-inset-bottom, 0px);
5
- --mpgd-safe-area-left: env(safe-area-inset-left, 0px);
2
+ /* SystemBars injects valid pixel values on documentElement. A value set only
3
+ on body or a nested game container does not feed these root aliases. */
4
+ --mpgd-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
5
+ --mpgd-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
6
+ --mpgd-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
7
+ --mpgd-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
6
8
  }
7
9
 
8
10
  html,
@@ -44,7 +44,9 @@ const integrationAvailabilityValues = valueSet({
44
44
  available: true,
45
45
  disabled: true,
46
46
  'approval-required': true,
47
+ 'action-required': true,
47
48
  'configuration-required': true,
49
+ 'temporarily-unavailable': true,
48
50
  unsupported: true,
49
51
  } satisfies Record<IntegrationAvailabilityState, true>);
50
52
  const presentationModeValues = valueSet({
@@ -83,6 +85,9 @@ function assertTargetConfig(input: unknown, label: string): asserts input is Tar
83
85
  `${label}.features`,
84
86
  );
85
87
  assertOptionalBoolean(input.features.bannerAds, `${label}.features.bannerAds`);
88
+ assertOptionalBoolean(input.features.subscriptions, `${label}.features.subscriptions`);
89
+ assertOptionalBoolean(input.features.nativeLeaderboard, `${label}.features.nativeLeaderboard`);
90
+ assertOptionalBoolean(input.features.remoteLeaderboard, `${label}.features.remoteLeaderboard`);
86
91
 
87
92
  assertRecord(input.capabilities, `${label}.capabilities`);
88
93
  assertOneOf(input.capabilities.storage, storageSupportValues, `${label}.capabilities.storage`);