@mpgd/cli 0.36.0 → 0.36.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/dist/capacitor-shell-starter.d.ts +21 -0
- package/dist/capacitor-shell-starter.js +121 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +381 -292
- package/package.json +12 -12
- package/templates/phaser-game/README.md +7 -0
- package/templates/phaser-game/apps/target-devvit/README.md +12 -0
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +513 -0
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +8 -291
- package/templates/phaser-game/vite.shared.ts +5 -1
|
@@ -31,6 +31,27 @@ export interface CapacitorShellCommandRunner {
|
|
|
31
31
|
}
|
|
32
32
|
/** Materialize only missing native projects after the non-destructive plan is applied. */
|
|
33
33
|
export declare function materializeCapacitorShellStarter(plan: CapacitorShellStarterPlan, runner?: CapacitorShellCommandRunner): void;
|
|
34
|
+
/**
|
|
35
|
+
* Backup rules for API < 31 (android:fullBackupContent). Every data domain is
|
|
36
|
+
* excluded so nothing from the app's private storage reaches a backup even
|
|
37
|
+
* if a game later turns android:allowBackup back on.
|
|
38
|
+
*/
|
|
39
|
+
export declare const androidBackupRulesXml: string;
|
|
40
|
+
/**
|
|
41
|
+
* Data extraction rules for API 31+ (android:dataExtractionRules). Android 12
|
|
42
|
+
* ignores android:allowBackup="false" for device-to-device transfer on some
|
|
43
|
+
* devices, so cloud backup and device transfer each exclude every domain.
|
|
44
|
+
*/
|
|
45
|
+
export declare const androidDataExtractionRulesXml: string;
|
|
46
|
+
/** Manifest application attributes that opt the shell out of backups. */
|
|
47
|
+
export declare const androidManifestBackupAttributes: readonly (readonly [string, string])[];
|
|
48
|
+
/**
|
|
49
|
+
* Set the backup opt-out attributes on the manifest application node and bind
|
|
50
|
+
* the tools namespace on the manifest root. Existing values are replaced in
|
|
51
|
+
* place; the attribute group keeps the indentation of the first attribute so
|
|
52
|
+
* the result is stable across repeated runs.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyAndroidManifestBackupPolicy(manifest: string): string;
|
|
34
55
|
/** Fail closed when a Release plist hardcodes values independently of Xcode build settings. */
|
|
35
56
|
export declare function assertIosReleasePlistIdentity(source: string): void;
|
|
36
57
|
export declare function assertAndroidReleaseDisplayName(shellApp: string, expectedDisplayName: string): void;
|
|
@@ -450,10 +450,14 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
450
450
|
runner.run('pnpm', ['install', '--no-frozen-lockfile'], plan.gameRoot);
|
|
451
451
|
runner.run('pnpm', ['--dir', shellPath, 'install', '--ignore-workspace', '--no-frozen-lockfile'], plan.gameRoot);
|
|
452
452
|
for (const platform of plan.nativePlatformsToAdd) {
|
|
453
|
-
|
|
453
|
+
const generatedNow = !existsSync(path.join(plan.gameRoot, shellPath, platform));
|
|
454
|
+
if (generatedNow) {
|
|
454
455
|
runner.run('pnpm', ['--dir', shellPath, 'cap', 'add', platform], plan.gameRoot);
|
|
455
456
|
}
|
|
456
457
|
const manifest = readJsonObject(path.join(plan.gameRoot, shellPath, 'mpgd.native-shell.json'));
|
|
458
|
+
if (platform === 'android' && generatedNow) {
|
|
459
|
+
materializeAndroidBackupPolicy(plan.gameRoot);
|
|
460
|
+
}
|
|
457
461
|
if (platform === 'ios') {
|
|
458
462
|
materializeIosArchiveScheme(plan.gameRoot);
|
|
459
463
|
const relative = `${shellPath}/ios/App/App/Info-Smoke.plist`;
|
|
@@ -473,6 +477,122 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
473
477
|
throw new Error('Capacitor shell changed during installation; rerun the initializer.');
|
|
474
478
|
}
|
|
475
479
|
}
|
|
480
|
+
const androidToolsNamespace = 'http://schemas.android.com/tools';
|
|
481
|
+
const androidBackupDomains = ['root', 'file', 'database', 'sharedpref', 'external'];
|
|
482
|
+
const androidBackupExcludes = androidBackupDomains
|
|
483
|
+
.map((domain) => `<exclude domain="${domain}" path="." />`);
|
|
484
|
+
/**
|
|
485
|
+
* Backup rules for API < 31 (android:fullBackupContent). Every data domain is
|
|
486
|
+
* excluded so nothing from the app's private storage reaches a backup even
|
|
487
|
+
* if a game later turns android:allowBackup back on.
|
|
488
|
+
*/
|
|
489
|
+
export const androidBackupRulesXml = [
|
|
490
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
491
|
+
'<!--',
|
|
492
|
+
' Generated by mpgd target init capacitor. WebView storage and the',
|
|
493
|
+
' game-services SharedPreferences save store hold player data, so the whole',
|
|
494
|
+
' private data tree stays out of Android backups.',
|
|
495
|
+
'-->',
|
|
496
|
+
'<full-backup-content>',
|
|
497
|
+
...androidBackupExcludes.map((line) => ` ${line}`),
|
|
498
|
+
'</full-backup-content>',
|
|
499
|
+
'',
|
|
500
|
+
].join('\n');
|
|
501
|
+
/**
|
|
502
|
+
* Data extraction rules for API 31+ (android:dataExtractionRules). Android 12
|
|
503
|
+
* ignores android:allowBackup="false" for device-to-device transfer on some
|
|
504
|
+
* devices, so cloud backup and device transfer each exclude every domain.
|
|
505
|
+
*/
|
|
506
|
+
export const androidDataExtractionRulesXml = [
|
|
507
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
508
|
+
'<!--',
|
|
509
|
+
' Generated by mpgd target init capacitor. WebView storage and the',
|
|
510
|
+
' game-services SharedPreferences save store hold player data, so neither',
|
|
511
|
+
' cloud backup nor Android 12+ device-to-device transfer may copy them.',
|
|
512
|
+
'-->',
|
|
513
|
+
'<data-extraction-rules>',
|
|
514
|
+
' <cloud-backup>',
|
|
515
|
+
...androidBackupExcludes.map((line) => ` ${line}`),
|
|
516
|
+
' </cloud-backup>',
|
|
517
|
+
' <device-transfer>',
|
|
518
|
+
...androidBackupExcludes.map((line) => ` ${line}`),
|
|
519
|
+
' </device-transfer>',
|
|
520
|
+
'</data-extraction-rules>',
|
|
521
|
+
'',
|
|
522
|
+
].join('\n');
|
|
523
|
+
/** Manifest application attributes that opt the shell out of backups. */
|
|
524
|
+
export const androidManifestBackupAttributes = [
|
|
525
|
+
['android:allowBackup', 'false'],
|
|
526
|
+
['android:fullBackupContent', '@xml/backup_rules'],
|
|
527
|
+
['android:dataExtractionRules', '@xml/data_extraction_rules'],
|
|
528
|
+
['tools:targetApi', 's'],
|
|
529
|
+
];
|
|
530
|
+
/**
|
|
531
|
+
* Opt a freshly generated Android project out of cloud backups and Android
|
|
532
|
+
* 12+ device-to-device transfers. The Capacitor template enables
|
|
533
|
+
* android:allowBackup, which would copy WebView storage and the game-services
|
|
534
|
+
* SharedPreferences save store into Google backups; only a project created by
|
|
535
|
+
* this run is touched, never an existing game-owned shell.
|
|
536
|
+
*/
|
|
537
|
+
function materializeAndroidBackupPolicy(gameRoot) {
|
|
538
|
+
const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
|
|
539
|
+
if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const resources = [
|
|
543
|
+
['backup_rules.xml', androidBackupRulesXml],
|
|
544
|
+
['data_extraction_rules.xml', androidDataExtractionRulesXml],
|
|
545
|
+
];
|
|
546
|
+
for (const [name, content] of resources) {
|
|
547
|
+
const resource = safeDestination(gameRoot, `${shellPath}/android/app/src/main/res/xml/${name}`);
|
|
548
|
+
if (!existsSync(resource)) {
|
|
549
|
+
mkdirSync(path.dirname(resource), { recursive: true });
|
|
550
|
+
writeFileSync(resource, content);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const source = readFileSync(manifestFile, 'utf8');
|
|
554
|
+
const hardened = applyAndroidManifestBackupPolicy(source);
|
|
555
|
+
if (hardened !== source) {
|
|
556
|
+
writeFileSync(manifestFile, hardened);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function xmlAttributePattern(name) {
|
|
560
|
+
return new RegExp(`(\\s*)${name.replaceAll('.', '\\.')}\\s*=\\s*(?:"[^"]*"|'[^']*')`, 'u');
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Set the backup opt-out attributes on the manifest application node and bind
|
|
564
|
+
* the tools namespace on the manifest root. Existing values are replaced in
|
|
565
|
+
* place; the attribute group keeps the indentation of the first attribute so
|
|
566
|
+
* the result is stable across repeated runs.
|
|
567
|
+
*/
|
|
568
|
+
export function applyAndroidManifestBackupPolicy(manifest) {
|
|
569
|
+
const root = /<manifest\b[^>]*?>/u.exec(manifest);
|
|
570
|
+
const application = /<application\b[^>]*?\/?>/u.exec(manifest);
|
|
571
|
+
if (root === null || application === null || application.index < root.index) {
|
|
572
|
+
return manifest;
|
|
573
|
+
}
|
|
574
|
+
let rootTag = root[0];
|
|
575
|
+
if (!/\sxmlns:tools\s*=/u.test(rootTag)) {
|
|
576
|
+
const androidNamespace = /\sxmlns:android\s*=\s*(?:"[^"]*"|'[^']*')/u.exec(rootTag);
|
|
577
|
+
const toolsNamespace = ` xmlns:tools="${androidToolsNamespace}"`;
|
|
578
|
+
rootTag = androidNamespace === null
|
|
579
|
+
? rootTag.replace(/^<manifest\b/u, `<manifest${toolsNamespace}`)
|
|
580
|
+
: `${rootTag.slice(0, androidNamespace.index + androidNamespace[0].length)}${toolsNamespace}${rootTag.slice(androidNamespace.index + androidNamespace[0].length)}`;
|
|
581
|
+
}
|
|
582
|
+
let applicationTag = application[0];
|
|
583
|
+
const existing = xmlAttributePattern('android:allowBackup').exec(applicationTag);
|
|
584
|
+
const separator = existing?.[1] !== undefined && existing[1] !== ''
|
|
585
|
+
? existing[1]
|
|
586
|
+
: (/^<application(\s+)\S/u.exec(applicationTag)?.[1] ?? ' ');
|
|
587
|
+
for (const [name] of androidManifestBackupAttributes) {
|
|
588
|
+
applicationTag = applicationTag.replace(xmlAttributePattern(name), '');
|
|
589
|
+
}
|
|
590
|
+
const attributes = androidManifestBackupAttributes
|
|
591
|
+
.map(([name, value]) => `${separator}${name}="${value}"`)
|
|
592
|
+
.join('');
|
|
593
|
+
applicationTag = applicationTag.replace(/^<application\b/u, `<application${attributes}`);
|
|
594
|
+
return `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, application.index)}${applicationTag}${manifest.slice(application.index + application[0].length)}`;
|
|
595
|
+
}
|
|
476
596
|
function materializeIosArchiveScheme(gameRoot) {
|
|
477
597
|
const schemeRelative = `${shellPath}/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme`;
|
|
478
598
|
const schemeFile = safeDestination(gameRoot, schemeRelative);
|