@mpgd/cli 0.45.0 → 0.47.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/dist/capacitor-shell-starter.d.ts +57 -1
- package/dist/capacitor-shell-starter.js +311 -34
- package/dist/index.js +23 -5
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +613 -316
- package/package.json +17 -15
- package/templates/phaser-game/apps/target-ait/src/main.ts +17 -0
- package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +2 -0
- package/templates/phaser-game/apps/target-ait/vite.config.ts +27 -0
- package/templates/phaser-game/apps/target-devvit/README.md +8 -1
- package/templates/phaser-game/apps/target-devvit/package.json +2 -1
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
- package/templates/phaser-game/package.json +2 -0
- package/templates/phaser-game/src/main.ts +8 -1
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +9 -0
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +7 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +51 -2
- package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +51 -2
- package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +62 -26
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +40 -2
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +79 -0
- package/templates/phaser-game/src/platform/commerceProducts.ts +69 -0
- package/templates/phaser-game/src/platform/gameServices.ts +22 -2
- package/templates/phaser-game/src/platform/monetizationJournal.ts +286 -0
- package/templates/phaser-game/src/platform/verse8Agent8Client.ts +23 -0
|
@@ -2,14 +2,61 @@ interface PlannedFile {
|
|
|
2
2
|
readonly path: string;
|
|
3
3
|
readonly content: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* A provider id that names a kit native store plugin. `--providers` installs
|
|
7
|
+
* the plugin package into the game-owned shell (where `cap add` or `cap sync`
|
|
8
|
+
* registers its native code), declares it in the game (where the Vite bundle
|
|
9
|
+
* imports it from the gateway file), and wires the gateway when the game still
|
|
10
|
+
* has the unwired template gateway; any other id stays selection metadata.
|
|
11
|
+
*/
|
|
12
|
+
export interface CapacitorShellProviderPlugin {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly packageName: string;
|
|
15
|
+
/** The plugin's directory in a kit checkout, read when the game has not installed it yet. */
|
|
16
|
+
readonly packageDir: string;
|
|
17
|
+
/** The native platform, which is also the canonical `mpgd.targets.json` target name. */
|
|
18
|
+
readonly platform: 'android' | 'ios';
|
|
19
|
+
/** The game gateway file expected to register the plugin's service provider. */
|
|
20
|
+
readonly gatewayFile: string;
|
|
21
|
+
readonly gatewayFactory: string;
|
|
22
|
+
/**
|
|
23
|
+
* Gateway sources the initializer may replace with the template's wired
|
|
24
|
+
* gateway: the unwired gateway earlier templates generated. Any other
|
|
25
|
+
* source is game-owned and only checked.
|
|
26
|
+
*/
|
|
27
|
+
readonly legacyGatewaySources: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export declare const capacitorShellProviderPlugins: readonly CapacitorShellProviderPlugin[];
|
|
30
|
+
/**
|
|
31
|
+
* Game modules the wired template gateways import. They are written next to a
|
|
32
|
+
* replaced legacy gateway when missing; a game-owned copy that differs from
|
|
33
|
+
* the template keeps the legacy gateway in place.
|
|
34
|
+
*/
|
|
35
|
+
export declare const capacitorShellProviderSupportFiles: readonly string[];
|
|
36
|
+
/** A plugin the plan installs, with the exact version the shell and the game pin. */
|
|
37
|
+
export interface CapacitorShellPlannedProviderPlugin extends CapacitorShellProviderPlugin {
|
|
38
|
+
readonly version: string;
|
|
39
|
+
/** `installed`: the game's copy; `kit`: the version this CLI release ships with. */
|
|
40
|
+
readonly versionSource: 'installed' | 'kit';
|
|
41
|
+
}
|
|
5
42
|
export interface CapacitorShellStarterInput {
|
|
6
43
|
readonly gameRoot: string;
|
|
7
44
|
readonly appId: string;
|
|
8
45
|
readonly displayName: string;
|
|
9
46
|
readonly iconSource?: string;
|
|
10
47
|
readonly backendUrl?: string;
|
|
11
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Requested provider ids. The ids in `capacitorShellProviderPlugins`
|
|
50
|
+
* (`play-billing`, `storekit`) install their native plugin package into the
|
|
51
|
+
* shell and the game; other ids are recorded as selection metadata only.
|
|
52
|
+
*/
|
|
12
53
|
readonly providerIds?: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* The CLI's game template. With it a legacy, unwired Android or iOS gateway
|
|
56
|
+
* is replaced by the template's store-provider wiring; without it the plan
|
|
57
|
+
* only checks the gateway and warns.
|
|
58
|
+
*/
|
|
59
|
+
readonly templateRoot?: string;
|
|
13
60
|
}
|
|
14
61
|
export interface CapacitorShellStarterPlan {
|
|
15
62
|
readonly gameRoot: string;
|
|
@@ -17,6 +64,15 @@ export interface CapacitorShellStarterPlan {
|
|
|
17
64
|
readonly changedFiles: readonly string[];
|
|
18
65
|
readonly nativePlatformsToAdd: readonly ('android' | 'ios')[];
|
|
19
66
|
readonly files: readonly PlannedFile[];
|
|
67
|
+
/** Native store plugins the plan installs into the shell and declares in the game. */
|
|
68
|
+
readonly providerPlugins: readonly CapacitorShellPlannedProviderPlugin[];
|
|
69
|
+
/**
|
|
70
|
+
* Store wiring the plan could not write or confirm: a missing or game-owned
|
|
71
|
+
* gateway that does not register the plugin, or a store plugin the shell
|
|
72
|
+
* keeps although it is no longer requested. Printed; game source is never
|
|
73
|
+
* rewritten for these.
|
|
74
|
+
*/
|
|
75
|
+
readonly warnings: readonly string[];
|
|
20
76
|
}
|
|
21
77
|
/**
|
|
22
78
|
* Produce the complete game-owned shell change list before writing anything.
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { DOMParser } from '@xmldom/xmldom';
|
|
7
|
+
import { satisfies, valid } from 'semver';
|
|
7
8
|
import { isNonPublicServiceHostname } from './production-target-readiness.js';
|
|
8
9
|
import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
|
|
9
10
|
const shellPath = 'apps/mobile-capacitor';
|
|
@@ -18,6 +19,50 @@ const javaKeywords = new Set(`
|
|
|
18
19
|
synchronized this throw throws transient try void volatile while true false null record sealed
|
|
19
20
|
permits var yield
|
|
20
21
|
`.trim().split(/\s+/u));
|
|
22
|
+
const unwiredCapacitorGateway = (target) => [
|
|
23
|
+
"import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';",
|
|
24
|
+
"import type { PlatformGateway } from '@mpgd/platform';",
|
|
25
|
+
'',
|
|
26
|
+
"import type { RuntimeConfig } from '../runtimeDetector';",
|
|
27
|
+
'',
|
|
28
|
+
'export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {',
|
|
29
|
+
' return createCapacitorPlatformGateway({',
|
|
30
|
+
` target: '${target}',`,
|
|
31
|
+
' appVersion: runtime.appVersion,',
|
|
32
|
+
' buildId: runtime.buildId,',
|
|
33
|
+
' });',
|
|
34
|
+
'}',
|
|
35
|
+
'',
|
|
36
|
+
].join('\n');
|
|
37
|
+
export const capacitorShellProviderPlugins = Object.freeze([
|
|
38
|
+
{
|
|
39
|
+
id: 'play-billing',
|
|
40
|
+
packageName: '@mpgd/capacitor-play-billing',
|
|
41
|
+
packageDir: 'native-plugins/capacitor-play-billing',
|
|
42
|
+
platform: 'android',
|
|
43
|
+
gatewayFile: 'src/platform/buildGateways/capacitorAndroid.ts',
|
|
44
|
+
gatewayFactory: 'createCapacitorPlayBillingServiceProvider',
|
|
45
|
+
legacyGatewaySources: [unwiredCapacitorGateway('android')],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'storekit',
|
|
49
|
+
packageName: '@mpgd/capacitor-storekit',
|
|
50
|
+
packageDir: 'native-plugins/capacitor-storekit',
|
|
51
|
+
platform: 'ios',
|
|
52
|
+
gatewayFile: 'src/platform/buildGateways/capacitorIos.ts',
|
|
53
|
+
gatewayFactory: 'createCapacitorStoreKitServiceProvider',
|
|
54
|
+
legacyGatewaySources: [unwiredCapacitorGateway('ios')],
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Game modules the wired template gateways import. They are written next to a
|
|
59
|
+
* replaced legacy gateway when missing; a game-owned copy that differs from
|
|
60
|
+
* the template keeps the legacy gateway in place.
|
|
61
|
+
*/
|
|
62
|
+
export const capacitorShellProviderSupportFiles = Object.freeze([
|
|
63
|
+
'src/platform/commerceProducts.ts',
|
|
64
|
+
'src/platform/commerceAccountBinding.ts',
|
|
65
|
+
]);
|
|
21
66
|
/**
|
|
22
67
|
* Produce the complete game-owned shell change list before writing anything.
|
|
23
68
|
* Existing Android/iOS projects, signing files, and custom controllers are
|
|
@@ -107,6 +152,13 @@ export function planCapacitorShellStarter(input) {
|
|
|
107
152
|
const cliDevelopmentDependencies = requireObject(cliPackage.devDependencies, 'CLI dev dependencies');
|
|
108
153
|
const typescriptVersion = requireString(cliDevelopmentDependencies.typescript, 'CLI TypeScript version');
|
|
109
154
|
const targetMap = requireObject(targets.targets, 'targets');
|
|
155
|
+
for (const plugin of capacitorShellProviderPlugins) {
|
|
156
|
+
if (providerIds.includes(plugin.id) && targetMap[plugin.platform] === undefined) {
|
|
157
|
+
throw new Error(`--providers ${plugin.id} installs the ${plugin.platform} store plugin into the native `
|
|
158
|
+
+ `shell, but mpgd.targets.json has no ${plugin.platform} target. Add the `
|
|
159
|
+
+ `capacitor-${plugin.platform} target before requesting store providers.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
110
162
|
for (const [targetName, kind, identityKey] of [
|
|
111
163
|
['android', 'capacitor-android', 'packageId'],
|
|
112
164
|
['ios', 'capacitor-ios', 'bundleId'],
|
|
@@ -174,11 +226,41 @@ export function planCapacitorShellStarter(input) {
|
|
|
174
226
|
}
|
|
175
227
|
const requestedProviderIds = input.providerIds === undefined
|
|
176
228
|
&& Array.isArray(existingManifest?.requestedProviderIds)
|
|
177
|
-
? existingManifest.requestedProviderIds
|
|
229
|
+
? existingManifest.requestedProviderIds.filter((id) => typeof id === 'string')
|
|
230
|
+
: providerIds;
|
|
178
231
|
const requestedBackendUrl = backendUrl ?? (typeof existingManifest?.backendUrl === 'string'
|
|
179
232
|
? existingManifest.backendUrl : undefined);
|
|
233
|
+
// Store plugins are pinned to one exact version in the shell and the game:
|
|
234
|
+
// the game's installed copy when it has one, otherwise the version this CLI
|
|
235
|
+
// release ships with, which the plan adds to the game as well. The Vite
|
|
236
|
+
// bundle imports the plugin from the gateway file, and its native code must
|
|
237
|
+
// match that JavaScript.
|
|
238
|
+
const providerPlugins = capacitorShellProviderPlugins
|
|
239
|
+
.filter((plugin) => requestedProviderIds.includes(plugin.id))
|
|
240
|
+
.map((plugin) => resolveProviderPlugin(gameRoot, gameDependencies, cliDevelopmentDependencies, plugin));
|
|
241
|
+
const warnings = [];
|
|
242
|
+
const gatewayFiles = planProviderGatewayWiring(gameRoot, input.templateRoot, providerPlugins, warnings);
|
|
243
|
+
if (packageContent !== undefined) {
|
|
244
|
+
const existingShellDependencies = requireObject(parseJsonObject(packageContent, 'existing shell package').dependencies, 'existing shell dependencies');
|
|
245
|
+
for (const plugin of capacitorShellProviderPlugins) {
|
|
246
|
+
if (existingShellDependencies[plugin.packageName] !== undefined
|
|
247
|
+
&& !providerPlugins.some((planned) => planned.id === plugin.id)) {
|
|
248
|
+
warnings.push(`The shell still lists ${plugin.packageName} although ${plugin.id} is no longer `
|
|
249
|
+
+ 'requested; the initializer never removes a store plugin, so remove it from the '
|
|
250
|
+
+ 'shell package and resync if the game stopped selling there.');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const shellDependencies = {
|
|
255
|
+
'@capacitor/android': capacitorVersion,
|
|
256
|
+
'@capacitor/app': appVersion,
|
|
257
|
+
'@capacitor/core': capacitorVersion,
|
|
258
|
+
'@capacitor/ios': capacitorVersion,
|
|
259
|
+
'@mpgd/capacitor-game-services': pluginVersion,
|
|
260
|
+
...Object.fromEntries(providerPlugins.map((plugin) => [plugin.packageName, plugin.version])),
|
|
261
|
+
};
|
|
180
262
|
if (packageContent !== undefined) {
|
|
181
|
-
assertCompatibleExistingPackage(packageContent,
|
|
263
|
+
assertCompatibleExistingPackage(packageContent, shellDependencies, capacitorVersion, typescriptVersion);
|
|
182
264
|
}
|
|
183
265
|
if (configContent !== undefined) {
|
|
184
266
|
const safeConfig = requireStaticCapacitorConfig(configContent);
|
|
@@ -205,28 +287,24 @@ export function planCapacitorShellStarter(input) {
|
|
|
205
287
|
{ path: 'mpgd.targets.json', content: json(targets) },
|
|
206
288
|
{
|
|
207
289
|
path: `${shellPath}/package.json`,
|
|
208
|
-
content: packageContent
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
'@capacitor/cli': capacitorVersion,
|
|
227
|
-
typescript: typescriptVersion,
|
|
228
|
-
},
|
|
229
|
-
}),
|
|
290
|
+
content: packageContent === undefined
|
|
291
|
+
? json({
|
|
292
|
+
name: `${gamePackageName}-native-shell`,
|
|
293
|
+
private: true,
|
|
294
|
+
version: '0.0.0',
|
|
295
|
+
type: 'module',
|
|
296
|
+
scripts: {
|
|
297
|
+
cap: 'cap',
|
|
298
|
+
'sync:android': 'cap sync android',
|
|
299
|
+
'sync:ios': 'cap sync ios',
|
|
300
|
+
},
|
|
301
|
+
dependencies: shellDependencies,
|
|
302
|
+
devDependencies: {
|
|
303
|
+
'@capacitor/cli': capacitorVersion,
|
|
304
|
+
typescript: typescriptVersion,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
: withPackageDependencies(packageContent, providerPlugins),
|
|
230
308
|
},
|
|
231
309
|
{
|
|
232
310
|
path: `${shellPath}/capacitor.config.ts`,
|
|
@@ -253,6 +331,18 @@ export function planCapacitorShellStarter(input) {
|
|
|
253
331
|
appId: input.appId,
|
|
254
332
|
displayName: input.displayName,
|
|
255
333
|
requestedProviderIds,
|
|
334
|
+
...(providerPlugins.length === 0
|
|
335
|
+
? {}
|
|
336
|
+
: {
|
|
337
|
+
providers: Object.fromEntries(providerPlugins.map((plugin) => [
|
|
338
|
+
plugin.id,
|
|
339
|
+
{
|
|
340
|
+
package: plugin.packageName,
|
|
341
|
+
version: plugin.version,
|
|
342
|
+
platform: plugin.platform,
|
|
343
|
+
},
|
|
344
|
+
])),
|
|
345
|
+
}),
|
|
256
346
|
...(requestedBackendUrl === undefined ? {} : { backendUrl: requestedBackendUrl }),
|
|
257
347
|
}),
|
|
258
348
|
},
|
|
@@ -269,8 +359,11 @@ export function planCapacitorShellStarter(input) {
|
|
|
269
359
|
'Existing native files, signing settings, and custom',
|
|
270
360
|
'ViewControllers are never replaced by mpgd target init capacitor.',
|
|
271
361
|
'',
|
|
272
|
-
'requestedProviderIds is selection metadata
|
|
273
|
-
'
|
|
362
|
+
'requestedProviderIds is selection metadata, except play-billing and',
|
|
363
|
+
'storekit: those install the kit store plugin into this shell and the game',
|
|
364
|
+
'and register it with cap sync. A plugin never reports a feature as',
|
|
365
|
+
'available on its own; the gateway registers the store provider, and the',
|
|
366
|
+
'store answers configuration-required until the plugin and products exist.',
|
|
274
367
|
'Production builds require a real game-owned backend when grants are enabled.',
|
|
275
368
|
'',
|
|
276
369
|
].join('\n'),
|
|
@@ -318,6 +411,24 @@ export function planCapacitorShellStarter(input) {
|
|
|
318
411
|
: current,
|
|
319
412
|
});
|
|
320
413
|
}
|
|
414
|
+
requestedFiles.push(...gatewayFiles);
|
|
415
|
+
// The game bundle imports the plugin from its gateway file, so the game
|
|
416
|
+
// declares it too, at the shell's exact version; the materializer's game
|
|
417
|
+
// install picks it up.
|
|
418
|
+
const missingGameDependencies = providerPlugins.filter((plugin) => gameDependencies[plugin.packageName] === undefined);
|
|
419
|
+
if (missingGameDependencies.length > 0) {
|
|
420
|
+
safeDestination(gameRoot, 'package.json');
|
|
421
|
+
requestedFiles.push({
|
|
422
|
+
path: 'package.json',
|
|
423
|
+
content: json({
|
|
424
|
+
...gamePackage,
|
|
425
|
+
dependencies: {
|
|
426
|
+
...gameDependencies,
|
|
427
|
+
...Object.fromEntries(missingGameDependencies.map((plugin) => [plugin.packageName, plugin.version])),
|
|
428
|
+
},
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
321
432
|
const nativePlatformsToAdd = ['android', 'ios'].filter((platform) => {
|
|
322
433
|
const nativeDirectory = path.join(gameRoot, shellPath, platform);
|
|
323
434
|
if (!existsSync(nativeDirectory)) {
|
|
@@ -342,6 +453,8 @@ export function planCapacitorShellStarter(input) {
|
|
|
342
453
|
files,
|
|
343
454
|
changedFiles: files.map((file) => file.path),
|
|
344
455
|
nativePlatformsToAdd,
|
|
456
|
+
providerPlugins,
|
|
457
|
+
warnings,
|
|
345
458
|
};
|
|
346
459
|
}
|
|
347
460
|
export function assertCapacitorAppId(appId) {
|
|
@@ -472,6 +585,15 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
472
585
|
}
|
|
473
586
|
assertNativePlatformComplete(plan.gameRoot, platform, requireString(manifest.appId, 'shell app ID'), requireString(manifest.displayName, 'shell display name'));
|
|
474
587
|
}
|
|
588
|
+
// `cap add` syncs a new project itself; a preserved project needs an explicit
|
|
589
|
+
// sync so the store plugin's native code joins the shell.
|
|
590
|
+
for (const platform of ['android', 'ios']) {
|
|
591
|
+
if (plan.providerPlugins.some((plugin) => plugin.platform === platform)
|
|
592
|
+
&& !plan.nativePlatformsToAdd.includes(platform)
|
|
593
|
+
&& existsSync(path.join(plan.gameRoot, shellPath, platform))) {
|
|
594
|
+
runner.run('pnpm', ['--dir', shellPath, 'cap', 'sync', platform], plan.gameRoot);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
475
597
|
const afterInstall = planCapacitorShellStarter(plan.input);
|
|
476
598
|
if (afterInstall.changedFiles.length > 0 || afterInstall.nativePlatformsToAdd.length > 0) {
|
|
477
599
|
throw new Error('Capacitor shell changed during installation; rerun the initializer.');
|
|
@@ -1915,19 +2037,24 @@ function runCommand(command, args, cwd) {
|
|
|
1915
2037
|
throw new Error(`${command} ${args.join(' ')} failed. The shell files remain for a safe retry.`);
|
|
1916
2038
|
}
|
|
1917
2039
|
}
|
|
1918
|
-
function assertCompatibleExistingPackage(source,
|
|
2040
|
+
function assertCompatibleExistingPackage(source, shellDependencies, capacitorVersion, typescriptVersion) {
|
|
1919
2041
|
const value = parseJsonObject(source, 'existing shell package');
|
|
1920
2042
|
if (value.private !== true) {
|
|
1921
2043
|
throw new Error('Existing Capacitor shell package must stay private.');
|
|
1922
2044
|
}
|
|
1923
2045
|
const dependencies = requireObject(value.dependencies, 'existing shell dependencies');
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
'
|
|
1929
|
-
|
|
1930
|
-
|
|
2046
|
+
const providerPackages = new Set(capacitorShellProviderPlugins.map((plugin) => plugin.packageName));
|
|
2047
|
+
for (const [name, version] of Object.entries(shellDependencies)) {
|
|
2048
|
+
// A store plugin the shell does not list yet is added by the plan. One it
|
|
2049
|
+
// lists at another version would compile native code that does not match
|
|
2050
|
+
// the game's JavaScript, so it is refused like any other pinned package.
|
|
2051
|
+
if (providerPackages.has(name) && dependencies[name] === undefined) {
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (providerPackages.has(name) && dependencies[name] !== version) {
|
|
2055
|
+
throw new Error(`Existing shell ${name} is ${String(dependencies[name])} but the game uses ${version}; `
|
|
2056
|
+
+ 'align the shell package with the game before rerunning.');
|
|
2057
|
+
}
|
|
1931
2058
|
if (dependencies[name] !== version) {
|
|
1932
2059
|
throw new Error(`Existing shell ${name} differs; refusing to rewrite dependencies.`);
|
|
1933
2060
|
}
|
|
@@ -2209,6 +2336,156 @@ function assertNotSymlink(file) {
|
|
|
2209
2336
|
}
|
|
2210
2337
|
}
|
|
2211
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Adds the requested store plugins to an existing shell package, keeping every
|
|
2341
|
+
* other field as the game wrote it. The content is unchanged when nothing is
|
|
2342
|
+
* missing, so an untouched shell stays out of the change list.
|
|
2343
|
+
*/
|
|
2344
|
+
function withPackageDependencies(source, plugins) {
|
|
2345
|
+
const value = parseJsonObject(source, 'existing shell package');
|
|
2346
|
+
const dependencies = requireObject(value.dependencies, 'existing shell dependencies');
|
|
2347
|
+
const missing = plugins.filter((plugin) => dependencies[plugin.packageName] === undefined);
|
|
2348
|
+
if (missing.length === 0) {
|
|
2349
|
+
return source;
|
|
2350
|
+
}
|
|
2351
|
+
return json({
|
|
2352
|
+
...value,
|
|
2353
|
+
dependencies: {
|
|
2354
|
+
...dependencies,
|
|
2355
|
+
...Object.fromEntries(missing.map((plugin) => [plugin.packageName, plugin.version])),
|
|
2356
|
+
},
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Resolves one requested store plugin to the exact version the shell and the
|
|
2361
|
+
* game pin: the game's installed copy, or, when the game does not declare the
|
|
2362
|
+
* plugin yet, the version this CLI release ships with (its published dev
|
|
2363
|
+
* dependency, or the plugin package of the kit checkout it runs from). A
|
|
2364
|
+
* declared but uninstalled plugin, or one whose peers the game's installed
|
|
2365
|
+
* kit packages do not satisfy, is refused before anything is written.
|
|
2366
|
+
*/
|
|
2367
|
+
function resolveProviderPlugin(gameRoot, gameDependencies, cliDevelopmentDependencies, plugin) {
|
|
2368
|
+
const installedFile = path.join(gameRoot, 'node_modules', plugin.packageName, 'package.json');
|
|
2369
|
+
let version;
|
|
2370
|
+
let versionSource;
|
|
2371
|
+
let peerDependencies;
|
|
2372
|
+
if (existsSync(installedFile)) {
|
|
2373
|
+
const installed = readJsonObject(installedFile);
|
|
2374
|
+
version = requireString(installed.version, `installed ${plugin.packageName} version`);
|
|
2375
|
+
versionSource = 'installed';
|
|
2376
|
+
peerDependencies = installed.peerDependencies;
|
|
2377
|
+
}
|
|
2378
|
+
else if (gameDependencies[plugin.packageName] !== undefined) {
|
|
2379
|
+
throw new Error(`The game declares ${plugin.packageName} but it is not installed; install game `
|
|
2380
|
+
+ 'dependencies before initializing the shell.');
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
const kitPackageFile = fileURLToPath(new URL(`../../../${plugin.packageDir}/package.json`, import.meta.url));
|
|
2384
|
+
const kitPackage = existsSync(kitPackageFile) ? readJsonObject(kitPackageFile) : undefined;
|
|
2385
|
+
const published = cliDevelopmentDependencies[plugin.packageName];
|
|
2386
|
+
const known = typeof published === 'string' && valid(published) !== null
|
|
2387
|
+
? published
|
|
2388
|
+
: kitPackage?.name === plugin.packageName
|
|
2389
|
+
? kitPackage.version
|
|
2390
|
+
: undefined;
|
|
2391
|
+
if (typeof known !== 'string') {
|
|
2392
|
+
throw new Error(`Provider ${plugin.id} needs ${plugin.packageName}, and this CLI does not know which `
|
|
2393
|
+
+ `version to install; add it to the game (pnpm add ${plugin.packageName}) and rerun.`);
|
|
2394
|
+
}
|
|
2395
|
+
version = known;
|
|
2396
|
+
versionSource = 'kit';
|
|
2397
|
+
peerDependencies = kitPackage?.peerDependencies;
|
|
2398
|
+
}
|
|
2399
|
+
if (valid(version) === null || version.includes('+')) {
|
|
2400
|
+
throw new Error(`${plugin.packageName} must resolve to a concrete version, not ${version}.`);
|
|
2401
|
+
}
|
|
2402
|
+
if (typeof peerDependencies === 'object' && peerDependencies !== null) {
|
|
2403
|
+
// Every declared peer the game has installed must satisfy the plugin's
|
|
2404
|
+
// range (Capacitor core, the bridge, the adapter, and the platform alike),
|
|
2405
|
+
// so an incompatible host is refused before the shell or gateway is written.
|
|
2406
|
+
for (const [peer, range] of Object.entries(peerDependencies)) {
|
|
2407
|
+
const peerFile = path.join(gameRoot, 'node_modules', peer, 'package.json');
|
|
2408
|
+
if (typeof range !== 'string' || !existsSync(peerFile)) {
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const peerVersion = readJsonObject(peerFile).version;
|
|
2412
|
+
if (typeof peerVersion === 'string' && valid(peerVersion) !== null
|
|
2413
|
+
&& !satisfies(peerVersion, range, { includePrerelease: true })) {
|
|
2414
|
+
throw new Error(`Provider ${plugin.id} (${plugin.packageName}@${version}) needs ${peer} ${range}, but `
|
|
2415
|
+
+ `the game has ${peerVersion}; upgrade the game's kit packages (mpgd kit upgrade) first.`);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return { ...plugin, version, versionSource };
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Plans the gateway wiring for the requested store plugins. A gateway that
|
|
2423
|
+
* already imports the plugin is left alone; a gateway that is still the
|
|
2424
|
+
* unwired template gateway is replaced by the template's wired gateway, with
|
|
2425
|
+
* the support modules it imports written when the game has none; every other
|
|
2426
|
+
* gateway is game-owned, so the plan only warns. Nothing here can enable a
|
|
2427
|
+
* purchase: the wired gateway registers the store provider, and grants still
|
|
2428
|
+
* come from the backend ledger through the reconciler.
|
|
2429
|
+
*/
|
|
2430
|
+
function planProviderGatewayWiring(gameRoot, templateRoot, plugins, warnings) {
|
|
2431
|
+
const planned = new Map();
|
|
2432
|
+
for (const plugin of plugins) {
|
|
2433
|
+
const current = readExisting(safeDestination(gameRoot, plugin.gatewayFile));
|
|
2434
|
+
// Wired means the gateway calls the plugin's service-provider factory; a
|
|
2435
|
+
// type-only or unused import of the package does not register anything.
|
|
2436
|
+
if (current !== undefined && new RegExp(`\\b${plugin.gatewayFactory}\\s*\\(`, 'u').test(current)) {
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
const register = `register ${plugin.gatewayFactory} from ${plugin.packageName} there so the `
|
|
2440
|
+
+ 'shell plugin is used';
|
|
2441
|
+
if (current === undefined) {
|
|
2442
|
+
warnings.push(`${plugin.gatewayFile} is missing; ${register}.`);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
const legacy = plugin.legacyGatewaySources.includes(current.replace(/\r\n/gu, '\n'));
|
|
2446
|
+
if (!legacy || templateRoot === undefined) {
|
|
2447
|
+
warnings.push(`${plugin.gatewayFile} does not call ${plugin.gatewayFactory}; ${register}.`);
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
const support = new Map();
|
|
2451
|
+
let kept;
|
|
2452
|
+
for (const file of capacitorShellProviderSupportFiles) {
|
|
2453
|
+
const existing = readExisting(safeDestination(gameRoot, file));
|
|
2454
|
+
const template = readTemplateFile(templateRoot, file);
|
|
2455
|
+
if (existing === undefined) {
|
|
2456
|
+
support.set(file, template);
|
|
2457
|
+
}
|
|
2458
|
+
else if (existing !== template) {
|
|
2459
|
+
kept = file;
|
|
2460
|
+
break;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (kept !== undefined) {
|
|
2464
|
+
warnings.push(`${plugin.gatewayFile} was not rewritten because the game-owned ${kept} differs from `
|
|
2465
|
+
+ `the template; ${register}.`);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
planned.set(plugin.gatewayFile, readTemplateFile(templateRoot, plugin.gatewayFile));
|
|
2469
|
+
for (const [file, content] of support) {
|
|
2470
|
+
planned.set(file, content);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return [...planned].map(([file, content]) => ({ path: file, content }));
|
|
2474
|
+
}
|
|
2475
|
+
/** A regular file of the CLI's game template, read without leaving the template root. */
|
|
2476
|
+
function readTemplateFile(templateRoot, relative) {
|
|
2477
|
+
const root = realpathSync(templateRoot);
|
|
2478
|
+
const file = path.resolve(root, relative);
|
|
2479
|
+
const within = path.relative(root, file);
|
|
2480
|
+
if (within === '' || within.startsWith('..') || path.isAbsolute(within)) {
|
|
2481
|
+
throw new Error(`Game template path escapes the template root: ${relative}`);
|
|
2482
|
+
}
|
|
2483
|
+
if (!existsSync(file) || !lstatSync(file).isFile()
|
|
2484
|
+
|| path.relative(root, realpathSync(file)).startsWith('..')) {
|
|
2485
|
+
throw new Error(`Game template file is missing: ${relative}`);
|
|
2486
|
+
}
|
|
2487
|
+
return readFileSync(file, 'utf8');
|
|
2488
|
+
}
|
|
2212
2489
|
function resolveInstalledPluginVersion(gameRoot, requirement) {
|
|
2213
2490
|
const adapterRoot = path.dirname(realpathSync(path.join(gameRoot, 'node_modules/@mpgd/adapter-capacitor/package.json')));
|
|
2214
2491
|
const installedPackage = path.join(adapterRoot, 'node_modules/@mpgd/capacitor-game-services/package.json');
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,8 @@ const mpgdTemplateDependencyPackages = [
|
|
|
73
73
|
{ name: '@mpgd/adapter-verse8', packageDir: 'adapters/verse8' },
|
|
74
74
|
{ name: '@mpgd/analytics', packageDir: 'packages/analytics' },
|
|
75
75
|
{ name: '@mpgd/bridge', packageDir: 'packages/bridge' },
|
|
76
|
+
{ name: '@mpgd/capacitor-play-billing', packageDir: 'native-plugins/capacitor-play-billing' },
|
|
77
|
+
{ name: '@mpgd/capacitor-storekit', packageDir: 'native-plugins/capacitor-storekit' },
|
|
76
78
|
{ name: '@mpgd/catalog', packageDir: 'packages/catalog' },
|
|
77
79
|
{ name: '@mpgd/cli', packageDir: 'packages/cli' },
|
|
78
80
|
{ name: '@mpgd/game-services', packageDir: 'packages/game-services' },
|
|
@@ -1558,8 +1560,8 @@ const targetCommand = defineI18n({
|
|
|
1558
1560
|
ko: '프로덕션 HTTPS Game Services 기본 URL.',
|
|
1559
1561
|
},
|
|
1560
1562
|
providers: {
|
|
1561
|
-
en: 'Comma-separated
|
|
1562
|
-
ko: '쉼표로 구분한
|
|
1563
|
+
en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
|
|
1564
|
+
ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
|
|
1563
1565
|
},
|
|
1564
1566
|
}),
|
|
1565
1567
|
args: {
|
|
@@ -1607,7 +1609,7 @@ const targetCommand = defineI18n({
|
|
|
1607
1609
|
providers: {
|
|
1608
1610
|
type: 'string',
|
|
1609
1611
|
required: false,
|
|
1610
|
-
description: 'Comma-separated
|
|
1612
|
+
description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
|
|
1611
1613
|
},
|
|
1612
1614
|
},
|
|
1613
1615
|
run: (ctx) => {
|
|
@@ -1631,6 +1633,7 @@ const targetCommand = defineI18n({
|
|
|
1631
1633
|
gameRoot,
|
|
1632
1634
|
appId,
|
|
1633
1635
|
displayName,
|
|
1636
|
+
templateRoot: gameTemplateDir,
|
|
1634
1637
|
...(iconSource === undefined ? {} : { iconSource }),
|
|
1635
1638
|
...(backendUrl === undefined ? {} : { backendUrl }),
|
|
1636
1639
|
...(selectedProviders === undefined ? {} : {
|
|
@@ -1647,18 +1650,33 @@ const targetCommand = defineI18n({
|
|
|
1647
1650
|
for (const platform of plan.nativePlatformsToAdd) {
|
|
1648
1651
|
console.info(`${dryRun ? 'Would add' : 'Added'} native platform: ${platform}`);
|
|
1649
1652
|
}
|
|
1650
|
-
|
|
1653
|
+
for (const plugin of plan.providerPlugins) {
|
|
1654
|
+
console.info(`${dryRun ? 'Would install' : 'Installed'} ${plugin.platform} store plugin `
|
|
1655
|
+
+ `${plugin.packageName}@${plugin.version} (${plugin.id}) into the shell`
|
|
1656
|
+
+ `${plugin.versionSource === 'kit' ? ' and the game' : ''}`
|
|
1657
|
+
+ `${dryRun ? '; cap add or cap sync registers it' : ''}.`);
|
|
1658
|
+
}
|
|
1659
|
+
for (const warning of plan.warnings) {
|
|
1660
|
+
console.warn(`Warning: ${warning}`);
|
|
1661
|
+
}
|
|
1662
|
+
console.info('File count covers managed shell and game files only; pnpm may also update root or shell lockfiles.');
|
|
1651
1663
|
if (!dryRun) {
|
|
1652
1664
|
console.info('Production native builds still require release identity and signing checks.');
|
|
1653
1665
|
}
|
|
1654
1666
|
return;
|
|
1655
1667
|
}
|
|
1668
|
+
if (ctx.values.providers !== undefined) {
|
|
1669
|
+
throw new Error('--providers is only available for capacitor initialization: it installs the '
|
|
1670
|
+
+ 'play-billing (Android) and storekit (iOS) store plugins into the game-owned '
|
|
1671
|
+
+ `native shell, which ${requestedTarget} does not initialize. Run `
|
|
1672
|
+
+ '"mpgd target init capacitor --providers ..." instead.');
|
|
1673
|
+
}
|
|
1656
1674
|
const target = normalizeConfiguredBuildTarget(requestedTarget);
|
|
1657
1675
|
if (target !== 'microsoft-store') {
|
|
1658
1676
|
throw new Error(`Target initialization is only available for microsoft-store; received: ${requestedTarget}`);
|
|
1659
1677
|
}
|
|
1660
1678
|
for (const option of [
|
|
1661
|
-
'app-id', 'display-name', 'icon-source', 'backend-url',
|
|
1679
|
+
'app-id', 'display-name', 'icon-source', 'backend-url',
|
|
1662
1680
|
]) {
|
|
1663
1681
|
if (ctx.values[option] !== undefined) {
|
|
1664
1682
|
throw new Error(`--${option} is only available for capacitor initialization.`);
|