@mpgd/cli 0.20.0 → 0.20.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/build-targets.d.ts +4 -0
- package/dist/build-targets.js +80 -0
- package/dist/index.js +47 -43
- package/dist/production-target-readiness.d.ts +10 -0
- package/dist/production-target-readiness.js +18 -1
- package/dist/target-config-env.d.ts +2 -0
- package/dist/target-config-env.js +2 -0
- package/dist/target-name.d.ts +1 -0
- package/dist/target-name.js +23 -0
- package/package.json +8 -8
- package/templates/phaser-game/src/env.d.ts +3 -0
- package/templates/phaser-game/src/main.ts +1 -0
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +9 -3
- package/templates/phaser-game/src/platform/gameServices.ts +2 -0
- package/templates/phaser-game/src/platform/installPlatform.ts +2 -1
- package/templates/phaser-game/vite.runtime-target-config.ts +176 -0
- package/templates/phaser-game/vite.shared.ts +33 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ConfiguredBuildTargets = Readonly<Record<string, unknown>>;
|
|
2
|
+
export declare const supportedBuildTargets: readonly ['browser', 'web', 'web-preview', 'microsoft-store', 'msstore', 'verse8', 'android', 'ios', 'ait', 'devvit', 'reddit'];
|
|
3
|
+
export declare function normalizeBuildTarget(target: string, configuredTargets?: ConfiguredBuildTargets): string;
|
|
4
|
+
export declare function normalizeConfiguredBuildTargets(configuredTargets: ConfiguredBuildTargets): readonly string[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { assertDeploymentTargetName } from './target-name.js';
|
|
2
|
+
export const supportedBuildTargets = [
|
|
3
|
+
'browser',
|
|
4
|
+
'web',
|
|
5
|
+
'web-preview',
|
|
6
|
+
'microsoft-store',
|
|
7
|
+
'msstore',
|
|
8
|
+
'verse8',
|
|
9
|
+
'android',
|
|
10
|
+
'ios',
|
|
11
|
+
'ait',
|
|
12
|
+
'devvit',
|
|
13
|
+
'reddit',
|
|
14
|
+
];
|
|
15
|
+
const canonicalBuildTargets = new Set([
|
|
16
|
+
'web-preview',
|
|
17
|
+
'microsoft-store',
|
|
18
|
+
'verse8',
|
|
19
|
+
'android',
|
|
20
|
+
'ios',
|
|
21
|
+
'ait',
|
|
22
|
+
'reddit',
|
|
23
|
+
]);
|
|
24
|
+
export function normalizeBuildTarget(target, configuredTargets = {}) {
|
|
25
|
+
const normalizedTarget = resolveBuildTarget(target, configuredTargets);
|
|
26
|
+
if (normalizedTarget !== undefined) {
|
|
27
|
+
return normalizedTarget;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported target: ${target}`);
|
|
30
|
+
}
|
|
31
|
+
export function normalizeConfiguredBuildTargets(configuredTargets) {
|
|
32
|
+
const targets = new Set();
|
|
33
|
+
for (const target of Object.keys(configuredTargets)) {
|
|
34
|
+
const normalizedTarget = resolveBuildTarget(target, configuredTargets);
|
|
35
|
+
if (normalizedTarget !== undefined) {
|
|
36
|
+
targets.add(normalizedTarget);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...targets];
|
|
40
|
+
}
|
|
41
|
+
function resolveBuildTarget(target, configuredTargets) {
|
|
42
|
+
const configuredWebTarget = isConfiguredWebTarget(target, configuredTargets);
|
|
43
|
+
if (configuredWebTarget) {
|
|
44
|
+
assertDeploymentTargetName(target);
|
|
45
|
+
}
|
|
46
|
+
if (target === 'web' && configuredWebTarget) {
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
const builtInTarget = normalizeBuiltInBuildTarget(target);
|
|
50
|
+
if (builtInTarget !== undefined) {
|
|
51
|
+
return builtInTarget;
|
|
52
|
+
}
|
|
53
|
+
if (configuredWebTarget) {
|
|
54
|
+
return target;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function normalizeBuiltInBuildTarget(target) {
|
|
59
|
+
if (target === 'browser' || target === 'web') {
|
|
60
|
+
return 'web-preview';
|
|
61
|
+
}
|
|
62
|
+
if (target === 'msstore') {
|
|
63
|
+
return 'microsoft-store';
|
|
64
|
+
}
|
|
65
|
+
if (target === 'devvit') {
|
|
66
|
+
return 'reddit';
|
|
67
|
+
}
|
|
68
|
+
return canonicalBuildTargets.has(target) ? target : undefined;
|
|
69
|
+
}
|
|
70
|
+
function isConfiguredWebTarget(target, configuredTargets) {
|
|
71
|
+
if (!Object.prototype.hasOwnProperty.call(configuredTargets, target)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const configuredTarget = configuredTargets[target];
|
|
75
|
+
return typeof configuredTarget === 'object'
|
|
76
|
+
&& configuredTarget !== null
|
|
77
|
+
&& !Array.isArray(configuredTarget)
|
|
78
|
+
&& 'kind' in configuredTarget
|
|
79
|
+
&& configuredTarget.kind === 'web';
|
|
80
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,14 @@ import completion from '@gunshi/plugin-completion';
|
|
|
6
6
|
import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
7
7
|
import resources from '@gunshi/resources';
|
|
8
8
|
import { cli } from 'gunshi';
|
|
9
|
+
import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
|
|
9
10
|
import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
|
|
10
11
|
import { resolveGameplayE2EReportFile } from './gameplay-e2e.js';
|
|
11
12
|
import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
12
13
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
13
14
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, } from './microsoft-store-starter.js';
|
|
14
15
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
16
|
+
import { targetConfigExtensionsFileEnv } from './target-config-env.js';
|
|
15
17
|
export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
|
|
16
18
|
export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
|
|
17
19
|
export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
|
|
@@ -67,19 +69,6 @@ const allMatrixTargetOrder = [
|
|
|
67
69
|
'ait',
|
|
68
70
|
'reddit',
|
|
69
71
|
];
|
|
70
|
-
const supportedBuildTargets = [
|
|
71
|
-
'browser',
|
|
72
|
-
'web',
|
|
73
|
-
'web-preview',
|
|
74
|
-
'microsoft-store',
|
|
75
|
-
'msstore',
|
|
76
|
-
'verse8',
|
|
77
|
-
'android',
|
|
78
|
-
'ios',
|
|
79
|
-
'ait',
|
|
80
|
-
'devvit',
|
|
81
|
-
'reddit',
|
|
82
|
-
];
|
|
83
72
|
const supportedVariants = ['wrapper', 'sync', 'simulator', 'archive'];
|
|
84
73
|
const acceptanceStepIds = {
|
|
85
74
|
check: 'check',
|
|
@@ -1024,7 +1013,7 @@ const targetCommand = defineI18n({
|
|
|
1024
1013
|
},
|
|
1025
1014
|
run: async (ctx) => {
|
|
1026
1015
|
const positionals = readLocalPositionals(ctx.positionals, ['target', 'generate-package']);
|
|
1027
|
-
const target =
|
|
1016
|
+
const target = normalizeConfiguredTargetName(readRequiredPositional(positionals, 0, 'target'));
|
|
1028
1017
|
if (target !== 'microsoft-store') {
|
|
1029
1018
|
throw new Error(`Package generation is not available for target: ${target}`);
|
|
1030
1019
|
}
|
|
@@ -1102,7 +1091,7 @@ const targetCommand = defineI18n({
|
|
|
1102
1091
|
},
|
|
1103
1092
|
run: (ctx) => {
|
|
1104
1093
|
const positionals = readLocalPositionals(ctx.positionals, ['target', 'preflight']);
|
|
1105
|
-
const target =
|
|
1094
|
+
const target = normalizeConfiguredTargetName(readRequiredPositional(positionals, 0, 'target'));
|
|
1106
1095
|
if (target !== 'microsoft-store') {
|
|
1107
1096
|
throw new Error(`Submission preflight is not available for target: ${target}`);
|
|
1108
1097
|
}
|
|
@@ -1204,7 +1193,7 @@ const targetCommand = defineI18n({
|
|
|
1204
1193
|
},
|
|
1205
1194
|
run: (ctx) => {
|
|
1206
1195
|
const positionals = readLocalPositionals(ctx.positionals, ['target', 'accept-package']);
|
|
1207
|
-
const target =
|
|
1196
|
+
const target = normalizeConfiguredTargetName(readRequiredPositional(positionals, 0, 'target'));
|
|
1208
1197
|
if (target !== 'microsoft-store') {
|
|
1209
1198
|
throw new Error(`Package acceptance is not available for target: ${target}`);
|
|
1210
1199
|
}
|
|
@@ -1397,6 +1386,11 @@ function targetConfigArgs() {
|
|
|
1397
1386
|
required: false,
|
|
1398
1387
|
description: 'Output path for the resolved target config file.',
|
|
1399
1388
|
},
|
|
1389
|
+
'target-config-extensions-file': {
|
|
1390
|
+
type: 'string',
|
|
1391
|
+
required: false,
|
|
1392
|
+
description: 'Optional game-owned target config extensions file.',
|
|
1393
|
+
},
|
|
1400
1394
|
};
|
|
1401
1395
|
}
|
|
1402
1396
|
function matrixArgResources() {
|
|
@@ -1836,6 +1830,7 @@ function createTargetCommandEnv(values) {
|
|
|
1836
1830
|
const gameRoot = path.dirname(targetsFile);
|
|
1837
1831
|
const kitPath = resolveKitPathForTarget(values);
|
|
1838
1832
|
const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
|
|
1833
|
+
const targetConfigExtensionsFile = resolveTargetConfigExtensionsFile(gameRoot, readOptionalString(values['target-config-extensions-file']));
|
|
1839
1834
|
const preparedTargetsFile = preparePlatformTargetsFile({
|
|
1840
1835
|
targetsFile,
|
|
1841
1836
|
kitPath,
|
|
@@ -1848,8 +1843,20 @@ function createTargetCommandEnv(values) {
|
|
|
1848
1843
|
...createGameOwnedReleaseEnv(gameRoot, process.env),
|
|
1849
1844
|
MPGD_KIT_PATH: kitPath,
|
|
1850
1845
|
MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
|
|
1846
|
+
...(targetConfigExtensionsFile === undefined
|
|
1847
|
+
? {}
|
|
1848
|
+
: { [targetConfigExtensionsFileEnv]: targetConfigExtensionsFile }),
|
|
1851
1849
|
};
|
|
1852
1850
|
}
|
|
1851
|
+
function resolveTargetConfigExtensionsFile(gameRoot, configuredFile) {
|
|
1852
|
+
const inheritedFile = readConfiguredPath(process.env[targetConfigExtensionsFileEnv]);
|
|
1853
|
+
const candidate = configuredFile ?? inheritedFile;
|
|
1854
|
+
if (candidate !== undefined) {
|
|
1855
|
+
return path.resolve(gameRoot, candidate);
|
|
1856
|
+
}
|
|
1857
|
+
const gameOwnedFile = path.join(gameRoot, 'mpgd.target-config.json');
|
|
1858
|
+
return existsSync(gameOwnedFile) ? gameOwnedFile : undefined;
|
|
1859
|
+
}
|
|
1853
1860
|
function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
|
|
1854
1861
|
const monetizationFiles = resolveGameOwnedMonetizationFiles(gameRoot, baseEnv);
|
|
1855
1862
|
const sourceGitSha = readOptionalString(baseEnv.MPGD_SOURCE_GIT_SHA)
|
|
@@ -1970,7 +1977,7 @@ function replaceTargetTokens(value, context) {
|
|
|
1970
1977
|
return value;
|
|
1971
1978
|
}
|
|
1972
1979
|
function runTargetCommand(input) {
|
|
1973
|
-
const target = normalizeBuildTarget(input.target);
|
|
1980
|
+
const target = normalizeBuildTarget(input.target, input.env);
|
|
1974
1981
|
const env = withTargetVariantEnv(target, input.variant, input.env);
|
|
1975
1982
|
const profile = input.profile ?? 'production';
|
|
1976
1983
|
const args = input.action === 'build'
|
|
@@ -1991,11 +1998,10 @@ function runTargetMatrix(input) {
|
|
|
1991
1998
|
}
|
|
1992
1999
|
}
|
|
1993
2000
|
function variantForTarget(target, input) {
|
|
1994
|
-
|
|
1995
|
-
if (normalized === 'ait') {
|
|
2001
|
+
if (target === 'ait') {
|
|
1996
2002
|
return input.aitVariant;
|
|
1997
2003
|
}
|
|
1998
|
-
if (
|
|
2004
|
+
if (target === 'ios') {
|
|
1999
2005
|
return input.iosVariant;
|
|
2000
2006
|
}
|
|
2001
2007
|
return undefined;
|
|
@@ -2033,7 +2039,7 @@ function parseTargetList(value, env) {
|
|
|
2033
2039
|
.split(',')
|
|
2034
2040
|
.map((target) => target.trim())
|
|
2035
2041
|
.filter((target) => target.length > 0)
|
|
2036
|
-
.map((target) => normalizeBuildTarget(target));
|
|
2042
|
+
.map((target) => normalizeBuildTarget(target, env));
|
|
2037
2043
|
if (targets.length === 0) {
|
|
2038
2044
|
throw new Error('Target list cannot be empty.');
|
|
2039
2045
|
}
|
|
@@ -2042,6 +2048,13 @@ function parseTargetList(value, env) {
|
|
|
2042
2048
|
function configuredTargetsForOrder(env, order, label) {
|
|
2043
2049
|
const configuredTargets = readConfiguredBuildTargets(env);
|
|
2044
2050
|
const targets = order.filter((target) => configuredTargets.has(target));
|
|
2051
|
+
if (label === 'all') {
|
|
2052
|
+
for (const target of configuredTargets) {
|
|
2053
|
+
if (!targets.includes(target)) {
|
|
2054
|
+
targets.push(target);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2045
2058
|
if (targets.length === 0) {
|
|
2046
2059
|
throw new Error(`No ${label} targets are configured in ${readConfiguredTargetsFile(env)}.`);
|
|
2047
2060
|
}
|
|
@@ -2052,14 +2065,7 @@ function readConfiguredBuildTargets(env) {
|
|
|
2052
2065
|
const parsed = readJsonForCli(file);
|
|
2053
2066
|
assertJsonObject(parsed, `targets file ${file}`);
|
|
2054
2067
|
assertJsonObject(parsed.targets, `targets in ${file}`);
|
|
2055
|
-
|
|
2056
|
-
for (const target of Object.keys(parsed.targets)) {
|
|
2057
|
-
const normalized = normalizeConfiguredBuildTarget(target);
|
|
2058
|
-
if (normalized !== undefined) {
|
|
2059
|
-
targets.add(normalized);
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
return targets;
|
|
2068
|
+
return new Set(normalizeConfiguredBuildTargets(parsed.targets));
|
|
2063
2069
|
}
|
|
2064
2070
|
function readConfiguredTargetsFile(env) {
|
|
2065
2071
|
const file = env.MPGD_PLATFORM_TARGETS_FILE;
|
|
@@ -2111,26 +2117,24 @@ function resolvePotentiallyMissingPath(input) {
|
|
|
2111
2117
|
}
|
|
2112
2118
|
function normalizeConfiguredBuildTarget(target) {
|
|
2113
2119
|
try {
|
|
2114
|
-
return
|
|
2120
|
+
return normalizeConfiguredTargetName(target);
|
|
2115
2121
|
}
|
|
2116
2122
|
catch {
|
|
2117
2123
|
return undefined;
|
|
2118
2124
|
}
|
|
2119
2125
|
}
|
|
2120
|
-
function normalizeBuildTarget(target) {
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
if (target === 'devvit') {
|
|
2128
|
-
return 'reddit';
|
|
2129
|
-
}
|
|
2130
|
-
if (!supportedBuildTargets.includes(target)) {
|
|
2131
|
-
throw new Error(`Unsupported target: ${target}`);
|
|
2126
|
+
function normalizeBuildTarget(target, env = process.env) {
|
|
2127
|
+
return normalizeConfiguredTargetName(target, readConfiguredTargetMap(env));
|
|
2128
|
+
}
|
|
2129
|
+
function readConfiguredTargetMap(env) {
|
|
2130
|
+
const file = env.MPGD_PLATFORM_TARGETS_FILE;
|
|
2131
|
+
if (file === undefined || file.length === 0 || !existsSync(file)) {
|
|
2132
|
+
return {};
|
|
2132
2133
|
}
|
|
2133
|
-
|
|
2134
|
+
const parsed = readJsonForCli(file);
|
|
2135
|
+
assertJsonObject(parsed, `targets file ${file}`);
|
|
2136
|
+
assertJsonObject(parsed.targets, `targets in ${file}`);
|
|
2137
|
+
return parsed.targets;
|
|
2134
2138
|
}
|
|
2135
2139
|
function assertJsonObject(input, label) {
|
|
2136
2140
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
@@ -4,5 +4,15 @@ export interface ProductionTargetReadinessInput {
|
|
|
4
4
|
readonly targetsFile: string;
|
|
5
5
|
readonly gameRoot: string;
|
|
6
6
|
readonly gameServicesUrl?: string;
|
|
7
|
+
readonly targetPolicy?: ProductionTargetPolicy;
|
|
8
|
+
}
|
|
9
|
+
export interface ProductionTargetPolicy {
|
|
10
|
+
readonly runtime: string;
|
|
11
|
+
readonly features: {
|
|
12
|
+
readonly rewardedAds: boolean;
|
|
13
|
+
};
|
|
14
|
+
readonly monetization: {
|
|
15
|
+
readonly rewardedAds: boolean;
|
|
16
|
+
};
|
|
7
17
|
}
|
|
8
18
|
export declare function assertProductionTargetReadiness(input: ProductionTargetReadinessInput): void;
|
|
@@ -16,13 +16,30 @@ export function assertProductionTargetReadiness(input) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const ownerPathKey = ownerPathByTarget[input.target];
|
|
19
|
-
if (ownerPathKey === undefined) {
|
|
19
|
+
if (ownerPathKey === undefined && input.targetPolicy === undefined) {
|
|
20
|
+
throw new Error(`Production target ${input.target} requires a target policy.`);
|
|
21
|
+
}
|
|
22
|
+
const requiresVerse8RewardAuthority = input.targetPolicy?.runtime === 'verse8-web'
|
|
23
|
+
&& (input.targetPolicy.features.rewardedAds
|
|
24
|
+
|| input.targetPolicy.monetization.rewardedAds);
|
|
25
|
+
if (ownerPathKey === undefined && !requiresVerse8RewardAuthority) {
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
28
|
const targetConfig = readTargets(input.targetsFile)[input.target];
|
|
23
29
|
if (!isRecord(targetConfig)) {
|
|
24
30
|
throw new Error(`Missing target configuration for ${input.target}.`);
|
|
25
31
|
}
|
|
32
|
+
if (ownerPathKey === undefined) {
|
|
33
|
+
if (requiresVerse8RewardAuthority
|
|
34
|
+
&& targetConfig.authoritativeGameServices === false) {
|
|
35
|
+
throw new Error(`Production target ${input.target} cannot enable rewarded ads without `
|
|
36
|
+
+ 'authoritative game services.');
|
|
37
|
+
}
|
|
38
|
+
if (requiresVerse8RewardAuthority || targetConfig.authoritativeGameServices !== false) {
|
|
39
|
+
assertAuthoritativeGameServicesUrl(input.gameServicesUrl, input.target);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
26
43
|
const ownerPath = targetConfig[ownerPathKey];
|
|
27
44
|
const webDir = targetConfig.webDir;
|
|
28
45
|
if (typeof ownerPath !== 'string' || ownerPath.length === 0) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertDeploymentTargetName(target: string): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const deploymentTargetNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2
|
+
const reservedDeploymentTargetNames = new Set([
|
|
3
|
+
'aux',
|
|
4
|
+
'browser',
|
|
5
|
+
'con',
|
|
6
|
+
'devvit',
|
|
7
|
+
'index',
|
|
8
|
+
'msstore',
|
|
9
|
+
'nul',
|
|
10
|
+
'prn',
|
|
11
|
+
'constructor',
|
|
12
|
+
'prototype',
|
|
13
|
+
...Array.from({ length: 9 }, (_, index) => `com${String(index + 1)}`),
|
|
14
|
+
...Array.from({ length: 9 }, (_, index) => `lpt${String(index + 1)}`),
|
|
15
|
+
]);
|
|
16
|
+
const maximumDeploymentTargetNameLength = 64;
|
|
17
|
+
export function assertDeploymentTargetName(target) {
|
|
18
|
+
if (!deploymentTargetNamePattern.test(target)
|
|
19
|
+
|| target.length > maximumDeploymentTargetNameLength
|
|
20
|
+
|| reservedDeploymentTargetNames.has(target)) {
|
|
21
|
+
throw new Error(`Invalid deployment target name: ${target}. Use lowercase kebab-case and avoid reserved names.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,18 +51,18 @@
|
|
|
51
51
|
"ttsc": "0.18.4",
|
|
52
52
|
"typescript": "7.0.2",
|
|
53
53
|
"@mpgd/adapter-ait": "0.8.0",
|
|
54
|
-
"@mpgd/adapter-browser": "0.5.1",
|
|
55
|
-
"@mpgd/adapter-devvit": "0.8.5",
|
|
56
|
-
"@mpgd/adapter-verse8": "0.2.2",
|
|
57
54
|
"@mpgd/adapter-capacitor": "0.4.5",
|
|
55
|
+
"@mpgd/adapter-devvit": "0.8.6",
|
|
56
|
+
"@mpgd/adapter-verse8": "0.2.3",
|
|
57
|
+
"@mpgd/analytics": "0.3.8",
|
|
58
|
+
"@mpgd/adapter-browser": "0.5.1",
|
|
58
59
|
"@mpgd/bridge": "0.7.0",
|
|
59
|
-
"@mpgd/
|
|
60
|
+
"@mpgd/catalog": "0.5.2",
|
|
61
|
+
"@mpgd/game-services": "0.11.1",
|
|
60
62
|
"@mpgd/i18n": "0.5.4",
|
|
61
|
-
"@mpgd/analytics": "0.3.8",
|
|
62
63
|
"@mpgd/phaser-assets": "0.4.1",
|
|
63
64
|
"@mpgd/platform": "0.8.0",
|
|
64
|
-
"@mpgd/
|
|
65
|
-
"@mpgd/target-config": "0.10.0"
|
|
65
|
+
"@mpgd/target-config": "0.10.1"
|
|
66
66
|
},
|
|
67
67
|
"main": "./dist/index.js",
|
|
68
68
|
"types": "./dist/index.d.ts",
|
|
@@ -5,6 +5,9 @@ declare const __MPGD_CONFIG_TARGET__: string;
|
|
|
5
5
|
declare const __MPGD_PLATFORM_TARGET__:
|
|
6
6
|
| import('@mpgd/target-config').EffectivePlatformTargetMetadata
|
|
7
7
|
| undefined;
|
|
8
|
+
declare const __MPGD_TARGET_CONFIG_MATRIX__:
|
|
9
|
+
| import('@mpgd/target-config').TargetConfigMatrix
|
|
10
|
+
| undefined;
|
|
8
11
|
declare const __APP_VERSION__: string;
|
|
9
12
|
declare const __BUILD_ID__: string;
|
|
10
13
|
declare const __SOURCE_GIT_SHA__: string;
|
|
@@ -66,6 +66,7 @@ async function bootstrap(): Promise<void> {
|
|
|
66
66
|
const gameServices = createStarterGameServices({
|
|
67
67
|
gateway: platform,
|
|
68
68
|
playerId: identitySession.playerId ?? player.playerId,
|
|
69
|
+
configTarget: runtime.configTarget,
|
|
69
70
|
analytics: analyticsSink,
|
|
70
71
|
analyticsSessionId,
|
|
71
72
|
});
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { createVerse8PlatformGateway } from '@mpgd/adapter-verse8';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
resolveAdPlacementPlatformId,
|
|
4
|
+
type AdPlacements,
|
|
5
|
+
} from '@mpgd/catalog';
|
|
3
6
|
import adPlacementsJson from '@mpgd/catalog/placements.json';
|
|
4
7
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
5
8
|
|
|
6
9
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
7
10
|
|
|
8
|
-
export async function createBuildGateway(
|
|
11
|
+
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
9
12
|
const adPlacements = adPlacementsJson as AdPlacements;
|
|
10
13
|
const placementIds = new Map(
|
|
11
14
|
adPlacements.placements.flatMap((placement) => {
|
|
12
|
-
const platformPlacementId =
|
|
15
|
+
const platformPlacementId = resolveAdPlacementPlatformId(
|
|
16
|
+
placement,
|
|
17
|
+
runtime.configTarget,
|
|
18
|
+
);
|
|
13
19
|
|
|
14
20
|
return platformPlacementId === undefined
|
|
15
21
|
? []
|
|
@@ -14,6 +14,7 @@ export type StarterGameServices = GameServicesRuntime;
|
|
|
14
14
|
export function createStarterGameServices(input: {
|
|
15
15
|
readonly gateway: PlatformGateway;
|
|
16
16
|
readonly playerId: string;
|
|
17
|
+
readonly configTarget: string;
|
|
17
18
|
readonly analytics?: AnalyticsSink;
|
|
18
19
|
readonly analyticsSessionId?: string;
|
|
19
20
|
}): StarterGameServices {
|
|
@@ -26,6 +27,7 @@ export function createStarterGameServices(input: {
|
|
|
26
27
|
: { analyticsSessionId: input.analyticsSessionId }),
|
|
27
28
|
authorityMode: resolveGameServicesAuthorityMode(import.meta.env.MODE),
|
|
28
29
|
target: import.meta.env.VITE_MPGD_GAME_SERVICES_TARGET ?? input.gateway.target,
|
|
30
|
+
deploymentTarget: input.configTarget,
|
|
29
31
|
...(import.meta.env.VITE_MPGD_GAME_SERVICES_URL === undefined
|
|
30
32
|
? {}
|
|
31
33
|
: { baseUrl: import.meta.env.VITE_MPGD_GAME_SERVICES_URL }),
|
|
@@ -15,7 +15,8 @@ import targetConfigMatrixJson from '@mpgd/target-config/targets.json';
|
|
|
15
15
|
|
|
16
16
|
import type { RuntimeConfig } from './runtimeDetector';
|
|
17
17
|
|
|
18
|
-
const targetConfigMatrix =
|
|
18
|
+
const targetConfigMatrix = __MPGD_TARGET_CONFIG_MATRIX__
|
|
19
|
+
?? targetConfigMatrixJson as TargetConfigMatrix;
|
|
19
20
|
const productCatalog = productCatalogJson as ProductCatalog;
|
|
20
21
|
const adPlacements = adPlacementsJson as AdPlacements;
|
|
21
22
|
const targetAdPlacements = adPlacements.placements.map((placement) => ({
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IntegrationAvailabilityState,
|
|
3
|
+
PresentationMode,
|
|
4
|
+
ReleaseProfile,
|
|
5
|
+
StorageSupport,
|
|
6
|
+
TargetConfig,
|
|
7
|
+
TargetConfigMatrix,
|
|
8
|
+
TargetIntegration,
|
|
9
|
+
TargetRuntimeKind,
|
|
10
|
+
} from '@mpgd/target-config';
|
|
11
|
+
|
|
12
|
+
export type { TargetConfigMatrix } from '@mpgd/target-config';
|
|
13
|
+
|
|
14
|
+
const runtimeKinds = valueSet({
|
|
15
|
+
'web-preview': true,
|
|
16
|
+
'microsoft-store-pwa': true,
|
|
17
|
+
'capacitor-android': true,
|
|
18
|
+
'capacitor-ios': true,
|
|
19
|
+
'apps-in-toss': true,
|
|
20
|
+
'devvit-web': true,
|
|
21
|
+
'verse8-web': true,
|
|
22
|
+
} satisfies Record<TargetRuntimeKind, true>);
|
|
23
|
+
const releaseProfiles = valueSet({
|
|
24
|
+
'web-preview': true,
|
|
25
|
+
'microsoft-store': true,
|
|
26
|
+
'google-play': true,
|
|
27
|
+
'app-store': true,
|
|
28
|
+
'apps-in-toss': true,
|
|
29
|
+
'devvit': true,
|
|
30
|
+
'verse8': true,
|
|
31
|
+
} satisfies Record<ReleaseProfile, true>);
|
|
32
|
+
const storageSupportValues = valueSet({
|
|
33
|
+
local: true,
|
|
34
|
+
native: true,
|
|
35
|
+
none: true,
|
|
36
|
+
} satisfies Record<StorageSupport, true>);
|
|
37
|
+
const integrationAvailabilityValues = valueSet({
|
|
38
|
+
available: true,
|
|
39
|
+
disabled: true,
|
|
40
|
+
'approval-required': true,
|
|
41
|
+
'configuration-required': true,
|
|
42
|
+
unsupported: true,
|
|
43
|
+
} satisfies Record<IntegrationAvailabilityState, true>);
|
|
44
|
+
const presentationModeValues = valueSet({
|
|
45
|
+
fullscreen: true,
|
|
46
|
+
'inline-expanded': true,
|
|
47
|
+
} satisfies Record<PresentationMode, true>);
|
|
48
|
+
const targetIntegrations = Object.keys({
|
|
49
|
+
identityUpgrade: true,
|
|
50
|
+
presentation: true,
|
|
51
|
+
sharing: true,
|
|
52
|
+
inboundShare: true,
|
|
53
|
+
notifications: true,
|
|
54
|
+
} satisfies Record<TargetIntegration, true>);
|
|
55
|
+
|
|
56
|
+
export function assertRuntimeTargetConfigMatrix(input: unknown): TargetConfigMatrix {
|
|
57
|
+
assertRecord(input, 'target config matrix');
|
|
58
|
+
assertNonEmptyString(input.version, 'target config matrix.version');
|
|
59
|
+
assertRecord(input.targets, 'target config matrix.targets');
|
|
60
|
+
|
|
61
|
+
for (const [target, config] of Object.entries(input.targets)) {
|
|
62
|
+
assertNonEmptyString(target, 'target config matrix target name');
|
|
63
|
+
assertTargetConfig(config, `target config matrix.targets.${target}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return input as unknown as TargetConfigMatrix;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertTargetConfig(input: unknown, label: string): asserts input is TargetConfig {
|
|
70
|
+
assertRecord(input, label);
|
|
71
|
+
assertOneOf(input.runtime, runtimeKinds, `${label}.runtime`);
|
|
72
|
+
|
|
73
|
+
assertRecord(input.features, `${label}.features`);
|
|
74
|
+
assertBooleanFields(
|
|
75
|
+
input.features,
|
|
76
|
+
['iap', 'rewardedAds', 'interstitialAds', 'leaderboard', 'localization'],
|
|
77
|
+
`${label}.features`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
assertRecord(input.capabilities, `${label}.capabilities`);
|
|
81
|
+
assertOneOf(input.capabilities.storage, storageSupportValues, `${label}.capabilities.storage`);
|
|
82
|
+
assertBoolean(input.capabilities.localization, `${label}.capabilities.localization`);
|
|
83
|
+
|
|
84
|
+
assertRecord(input.localization, `${label}.localization`);
|
|
85
|
+
assertNonEmptyString(input.localization.fallbackLocale, `${label}.localization.fallbackLocale`);
|
|
86
|
+
|
|
87
|
+
assertRecord(input.monetization, `${label}.monetization`);
|
|
88
|
+
assertBooleanFields(
|
|
89
|
+
input.monetization,
|
|
90
|
+
['iap', 'rewardedAds', 'interstitialAds'],
|
|
91
|
+
`${label}.monetization`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
assertRecord(input.leaderboard, `${label}.leaderboard`);
|
|
95
|
+
assertBoolean(input.leaderboard.native, `${label}.leaderboard.native`);
|
|
96
|
+
|
|
97
|
+
assertRecord(input.release, `${label}.release`);
|
|
98
|
+
assertOneOf(input.release.profile, releaseProfiles, `${label}.release.profile`);
|
|
99
|
+
|
|
100
|
+
assertRecord(input.policy, `${label}.policy`);
|
|
101
|
+
assertBooleanFields(
|
|
102
|
+
input.policy,
|
|
103
|
+
[
|
|
104
|
+
'externalPaymentAllowed',
|
|
105
|
+
'remoteExecutableCodeAllowed',
|
|
106
|
+
'installOtherAppCTAAllowed',
|
|
107
|
+
'requiresStoreReview',
|
|
108
|
+
'requiresAitReview',
|
|
109
|
+
],
|
|
110
|
+
`${label}.policy`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (input.integrations !== undefined) {
|
|
114
|
+
assertRecord(input.integrations, `${label}.integrations`);
|
|
115
|
+
for (const key of targetIntegrations) {
|
|
116
|
+
assertOneOf(
|
|
117
|
+
input.integrations[key],
|
|
118
|
+
integrationAvailabilityValues,
|
|
119
|
+
`${label}.integrations.${key}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
assertOneOf(
|
|
123
|
+
input.integrations.presentationMode,
|
|
124
|
+
presentationModeValues,
|
|
125
|
+
`${label}.integrations.presentationMode`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertBooleanFields(
|
|
131
|
+
input: Record<string, unknown>,
|
|
132
|
+
fields: readonly string[],
|
|
133
|
+
label: string,
|
|
134
|
+
): void {
|
|
135
|
+
for (const field of fields) {
|
|
136
|
+
assertBoolean(input[field], `${label}.${field}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assertBoolean(input: unknown, label: string): asserts input is boolean {
|
|
141
|
+
if (typeof input !== 'boolean') {
|
|
142
|
+
throw new Error(`${label} must be a boolean.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertNonEmptyString(input: unknown, label: string): asserts input is string {
|
|
147
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
148
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertOneOf(input: unknown, values: ReadonlySet<string>, label: string): void {
|
|
153
|
+
if (typeof input !== 'string' || !values.has(input)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${label} has unsupported value ${formatValue(input)}; expected one of ${[...values].join(', ')}.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatValue(input: unknown): string {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.stringify(input) ?? String(input);
|
|
163
|
+
} catch {
|
|
164
|
+
return String(input);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function valueSet<T extends string>(values: Record<T, true>): ReadonlySet<string> {
|
|
169
|
+
return new Set(Object.keys(values));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function assertRecord(input: unknown, label: string): asserts input is Record<string, unknown> {
|
|
173
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
174
|
+
throw new Error(`${label} must be an object.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -4,6 +4,11 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import ttsc from '@ttsc/unplugin/vite';
|
|
5
5
|
import type { UserConfig } from 'vite';
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
assertRuntimeTargetConfigMatrix,
|
|
9
|
+
type TargetConfigMatrix,
|
|
10
|
+
} from './vite.runtime-target-config';
|
|
11
|
+
|
|
7
12
|
interface RuntimePlatformTargetMetadata {
|
|
8
13
|
readonly kind: string;
|
|
9
14
|
readonly adapter: string;
|
|
@@ -39,6 +44,9 @@ export function createGameViteSharedConfig(
|
|
|
39
44
|
debug: !isProduction,
|
|
40
45
|
buildId: process.env.BUILD_ID ?? 'local',
|
|
41
46
|
});
|
|
47
|
+
const runtimeTargetConfigMatrix = readRuntimeTargetConfigMatrix(
|
|
48
|
+
process.env.MPGD_TARGET_CONFIG_MATRIX_FILE,
|
|
49
|
+
);
|
|
42
50
|
|
|
43
51
|
return {
|
|
44
52
|
base: './',
|
|
@@ -63,6 +71,10 @@ export function createGameViteSharedConfig(
|
|
|
63
71
|
__MPGD_CONFIG_TARGET__: JSON.stringify(configTarget),
|
|
64
72
|
__MPGD_PLATFORM_TARGET__:
|
|
65
73
|
platformTarget === undefined ? 'undefined' : JSON.stringify(platformTarget),
|
|
74
|
+
__MPGD_TARGET_CONFIG_MATRIX__:
|
|
75
|
+
runtimeTargetConfigMatrix === undefined
|
|
76
|
+
? 'undefined'
|
|
77
|
+
: JSON.stringify(runtimeTargetConfigMatrix),
|
|
66
78
|
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
|
|
67
79
|
__BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
|
|
68
80
|
__SOURCE_GIT_SHA__: JSON.stringify(process.env.MPGD_SOURCE_GIT_SHA ?? 'uncommitted'),
|
|
@@ -71,6 +83,27 @@ export function createGameViteSharedConfig(
|
|
|
71
83
|
};
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
function readRuntimeTargetConfigMatrix(
|
|
87
|
+
source: string | undefined,
|
|
88
|
+
): TargetConfigMatrix | undefined {
|
|
89
|
+
const matrixFile = source?.trim();
|
|
90
|
+
if (matrixFile === undefined || matrixFile.length === 0) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resolvedMatrixFile = resolve(matrixFile);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return assertRuntimeTargetConfigMatrix(
|
|
98
|
+
JSON.parse(readFileSync(resolvedMatrixFile, 'utf8')) as unknown,
|
|
99
|
+
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Failed to read or validate MPGD_TARGET_CONFIG_MATRIX_FILE at ${resolvedMatrixFile}: ${formatError(error)}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
74
107
|
export function resolveBuildGatewayModule(input: {
|
|
75
108
|
readonly target: string;
|
|
76
109
|
readonly debug: boolean;
|