@mpgd/cli 0.19.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/README.md +15 -3
- package/templates/phaser-game/agent/acceptance.md +3 -0
- package/templates/phaser-game/agent/brief.md +2 -0
- package/templates/phaser-game/agent/game-manifest.json +4 -0
- package/templates/phaser-game/apps/target-ait/index.html +1 -1
- package/templates/phaser-game/apps/target-devvit/src/client/game.html +4 -1
- package/templates/phaser-game/apps/target-devvit/src/client/index.html +4 -1
- package/templates/phaser-game/game.html +4 -1
- package/templates/phaser-game/index.html +4 -1
- package/templates/phaser-game/src/env.d.ts +3 -0
- package/templates/phaser-game/src/main.ts +13 -3
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +9 -3
- package/templates/phaser-game/src/platform/gameServices.ts +9 -0
- package/templates/phaser-game/src/platform/installPlatform.ts +2 -1
- package/templates/phaser-game/src/runtime/gameContext.ts +2 -2
- package/templates/phaser-game/src/scenes/PlayScene.ts +9 -12
- package/templates/phaser-game/src/styles.css +12 -5
- 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.
|
|
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
54
|
"@mpgd/adapter-capacitor": "0.4.5",
|
|
57
|
-
"@mpgd/adapter-
|
|
55
|
+
"@mpgd/adapter-devvit": "0.8.6",
|
|
56
|
+
"@mpgd/adapter-verse8": "0.2.3",
|
|
58
57
|
"@mpgd/analytics": "0.3.8",
|
|
59
|
-
"@mpgd/
|
|
58
|
+
"@mpgd/adapter-browser": "0.5.1",
|
|
60
59
|
"@mpgd/bridge": "0.7.0",
|
|
61
|
-
"@mpgd/
|
|
62
|
-
"@mpgd/
|
|
60
|
+
"@mpgd/catalog": "0.5.2",
|
|
61
|
+
"@mpgd/game-services": "0.11.1",
|
|
63
62
|
"@mpgd/i18n": "0.5.4",
|
|
63
|
+
"@mpgd/phaser-assets": "0.4.1",
|
|
64
64
|
"@mpgd/platform": "0.8.0",
|
|
65
|
-
"@mpgd/target-config": "0.
|
|
65
|
+
"@mpgd/target-config": "0.10.1"
|
|
66
66
|
},
|
|
67
67
|
"main": "./dist/index.js",
|
|
68
68
|
"types": "./dist/index.d.ts",
|
|
@@ -127,13 +127,15 @@ preview, Apps in Toss, Devvit, Android, and iOS bundles.
|
|
|
127
127
|
|
|
128
128
|
## Viewport And Controls
|
|
129
129
|
|
|
130
|
-
The starter computes an initial viewport
|
|
130
|
+
The starter computes an initial viewport snapshot with `@mpgd/target-config`.
|
|
131
131
|
Measure the game container first, then fall back to `visualViewport` or
|
|
132
132
|
`window.innerWidth`:
|
|
133
133
|
|
|
134
134
|
```ts
|
|
135
|
+
import { resolveTargetViewportSnapshot } from '@mpgd/target-config';
|
|
136
|
+
|
|
135
137
|
const measured = measureGameViewport();
|
|
136
|
-
const viewport =
|
|
138
|
+
const viewport = resolveTargetViewportSnapshot({
|
|
137
139
|
width: measured.width,
|
|
138
140
|
height: measured.height,
|
|
139
141
|
source: measured.source,
|
|
@@ -146,7 +148,17 @@ const viewport = resolveTargetViewportPlan({
|
|
|
146
148
|
|
|
147
149
|
Use this as a starting point for game-specific layout. The returned
|
|
148
150
|
recommendation is not a hard target rule; override it when your playfield or
|
|
149
|
-
input model needs a different UI arrangement.
|
|
151
|
+
input model needs a different UI arrangement. The snapshot also includes
|
|
152
|
+
`safeArea.contentBounds` for a full-bleed game that places its own persistent
|
|
153
|
+
HUD.
|
|
154
|
+
|
|
155
|
+
The generated HTML uses `viewport-fit=cover`, and the shared stylesheet exposes
|
|
156
|
+
the browser values as `--mpgd-safe-area-*`. The starter's `#game` mount is
|
|
157
|
+
already inside that CSS padding, so its default snapshot has zero local insets.
|
|
158
|
+
If a game instead measures the full browser viewport, pass
|
|
159
|
+
`readTargetViewportSafeAreaInsets(getComputedStyle(document.documentElement))`
|
|
160
|
+
to `resolveTargetViewportSnapshot`. Do not reserve the same inset in both the
|
|
161
|
+
outer CSS and the Phaser layout; that would shrink content twice.
|
|
150
162
|
|
|
151
163
|
- `compact` is `<= 599px`: phone and narrow Devvit/card layouts. Prefer bottom
|
|
152
164
|
controls and drawers.
|
|
@@ -60,3 +60,6 @@ game server or replace game-owned production evidence.
|
|
|
60
60
|
|
|
61
61
|
Verify the first screen reports the viewport orientation policy, and treat
|
|
62
62
|
locked orientation modes as soft prompts instead of unsafe WebView hard locks.
|
|
63
|
+
Verify safe-area geometry is reserved exactly once: CSS-padded containers must
|
|
64
|
+
not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
|
|
65
|
+
the snapshot bounds for persistent controls.
|
|
@@ -15,6 +15,8 @@ Boundaries:
|
|
|
15
15
|
- Platform SDK imports belong in adapters or target wrappers.
|
|
16
16
|
- Orientation policy should be chosen before adding resize behavior; treat
|
|
17
17
|
locked modes as soft prompts unless a platform adapter supports hard locks.
|
|
18
|
+
- Safe-area geometry must be reserved exactly once: use outer CSS safe-area
|
|
19
|
+
padding or the viewport snapshot `safeArea.contentBounds`, never both.
|
|
18
20
|
|
|
19
21
|
Useful commands:
|
|
20
22
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta
|
|
6
6
|
name="viewport"
|
|
7
|
-
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
|
7
|
+
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
8
8
|
/>
|
|
9
9
|
<title>__GAME_TITLE__</title>
|
|
10
10
|
</head>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
|
8
|
+
/>
|
|
6
9
|
<meta name="theme-color" content="#0f172a" />
|
|
7
10
|
<title>__GAME_TITLE__</title>
|
|
8
11
|
</head>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
|
8
|
+
/>
|
|
6
9
|
<meta name="theme-color" content="#0f172a" />
|
|
7
10
|
<title>__GAME_TITLE__</title>
|
|
8
11
|
</head>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
|
8
|
+
/>
|
|
6
9
|
<meta name="theme-color" content="#0f172a" />
|
|
7
10
|
<title>__GAME_TITLE__</title>
|
|
8
11
|
</head>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
|
8
|
+
/>
|
|
6
9
|
<meta name="theme-color" content="#0f172a" />
|
|
7
10
|
<link rel="manifest" href="./manifest.webmanifest" />
|
|
8
11
|
<title>__GAME_TITLE__</title>
|
|
@@ -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;
|
|
@@ -4,7 +4,7 @@ import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/anal
|
|
|
4
4
|
import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
|
|
5
5
|
import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
resolveTargetViewportSnapshot,
|
|
8
8
|
type TargetViewportOrientationPolicy,
|
|
9
9
|
} from '@mpgd/target-config';
|
|
10
10
|
|
|
@@ -18,6 +18,7 @@ import { installMicrosoftStorePwa } from './platform/microsoftStorePwa';
|
|
|
18
18
|
|
|
19
19
|
await bootstrap();
|
|
20
20
|
|
|
21
|
+
/** Install target services, analytics, and viewport state before starting the game. */
|
|
21
22
|
async function bootstrap(): Promise<void> {
|
|
22
23
|
let locale: Locale = 'en';
|
|
23
24
|
let disposeMicrosoftStorePwa: (() => void) | undefined;
|
|
@@ -35,7 +36,7 @@ async function bootstrap(): Promise<void> {
|
|
|
35
36
|
const orientationPolicy = {
|
|
36
37
|
mode: 'prefer-landscape',
|
|
37
38
|
} as const satisfies TargetViewportOrientationPolicy;
|
|
38
|
-
const viewport =
|
|
39
|
+
const viewport = resolveTargetViewportSnapshot({
|
|
39
40
|
...measureGameViewport(),
|
|
40
41
|
runtime: runtime.config.runtime,
|
|
41
42
|
orientationPolicy,
|
|
@@ -56,14 +57,18 @@ async function bootstrap(): Promise<void> {
|
|
|
56
57
|
?? runtime.config.localization.fallbackLocale,
|
|
57
58
|
});
|
|
58
59
|
const analyticsSink = createBufferedAnalyticsSink();
|
|
60
|
+
const analyticsSessionId = createClientId('session');
|
|
59
61
|
const analytics = createAnalyticsReporter({
|
|
60
62
|
target: platform.target,
|
|
61
|
-
sessionId:
|
|
63
|
+
sessionId: analyticsSessionId,
|
|
62
64
|
sink: analyticsSink,
|
|
63
65
|
});
|
|
64
66
|
const gameServices = createStarterGameServices({
|
|
65
67
|
gateway: platform,
|
|
66
68
|
playerId: identitySession.playerId ?? player.playerId,
|
|
69
|
+
configTarget: runtime.configTarget,
|
|
70
|
+
analytics: analyticsSink,
|
|
71
|
+
analyticsSessionId,
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
await analytics.track({
|
|
@@ -98,6 +103,7 @@ async function bootstrap(): Promise<void> {
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
/** Resolve a verified platform session or fall back to a local guest. */
|
|
101
107
|
async function resolveIdentitySession(
|
|
102
108
|
platform: PlatformGateway,
|
|
103
109
|
fallbackPlayerId: string,
|
|
@@ -110,6 +116,7 @@ async function resolveIdentitySession(
|
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
/** Resolve the host launch intent while preserving a home-entry fallback. */
|
|
113
120
|
async function resolveLaunchIntent(platform: PlatformGateway): Promise<LaunchIntent> {
|
|
114
121
|
try {
|
|
115
122
|
return (await platform.presentation?.getLaunchIntent()) ?? { entry: 'home' };
|
|
@@ -119,6 +126,7 @@ async function resolveLaunchIntent(platform: PlatformGateway): Promise<LaunchInt
|
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
/** Create the minimal local identity used when a host session is unavailable. */
|
|
122
130
|
function createGuestSession(playerId: string): IdentitySession {
|
|
123
131
|
return {
|
|
124
132
|
identityLevel: 'guest',
|
|
@@ -127,6 +135,7 @@ function createGuestSession(playerId: string): IdentitySession {
|
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/** Render a localized bootstrap failure inside the game mount. */
|
|
130
139
|
function renderBootstrapError(error: unknown, locale: Locale): void {
|
|
131
140
|
const message = error instanceof Error ? error.message : String(error);
|
|
132
141
|
const root = document.querySelector<HTMLDivElement>('#game');
|
|
@@ -142,6 +151,7 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
|
|
|
142
151
|
root.append(panel);
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
/** Measure the game surface before falling back to browser viewport geometry. */
|
|
145
155
|
function measureGameViewport(): {
|
|
146
156
|
readonly width: number;
|
|
147
157
|
readonly height: number;
|
|
@@ -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
|
? []
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AnalyticsSink } from '@mpgd/analytics';
|
|
1
2
|
import {
|
|
2
3
|
createGameServicesRuntime,
|
|
3
4
|
resolveGameServicesAuthorityMode,
|
|
@@ -13,12 +14,20 @@ export type StarterGameServices = GameServicesRuntime;
|
|
|
13
14
|
export function createStarterGameServices(input: {
|
|
14
15
|
readonly gateway: PlatformGateway;
|
|
15
16
|
readonly playerId: string;
|
|
17
|
+
readonly configTarget: string;
|
|
18
|
+
readonly analytics?: AnalyticsSink;
|
|
19
|
+
readonly analyticsSessionId?: string;
|
|
16
20
|
}): StarterGameServices {
|
|
17
21
|
return createGameServicesRuntime({
|
|
18
22
|
gateway: input.gateway,
|
|
19
23
|
playerId: input.playerId,
|
|
24
|
+
...(input.analytics === undefined ? {} : { analytics: input.analytics }),
|
|
25
|
+
...(input.analyticsSessionId === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { analyticsSessionId: input.analyticsSessionId }),
|
|
20
28
|
authorityMode: resolveGameServicesAuthorityMode(import.meta.env.MODE),
|
|
21
29
|
target: import.meta.env.VITE_MPGD_GAME_SERVICES_TARGET ?? input.gateway.target,
|
|
30
|
+
deploymentTarget: input.configTarget,
|
|
22
31
|
...(import.meta.env.VITE_MPGD_GAME_SERVICES_URL === undefined
|
|
23
32
|
? {}
|
|
24
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) => ({
|
|
@@ -4,7 +4,7 @@ import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platfo
|
|
|
4
4
|
import type {
|
|
5
5
|
TargetConfiguredGateway,
|
|
6
6
|
TargetRuntimeSnapshot,
|
|
7
|
-
|
|
7
|
+
TargetViewportSnapshot,
|
|
8
8
|
} from '@mpgd/target-config';
|
|
9
9
|
|
|
10
10
|
import type { StarterGameServices } from '../platform/gameServices';
|
|
@@ -14,7 +14,7 @@ export const starterContextKey = 'starterContext';
|
|
|
14
14
|
export interface StarterContext {
|
|
15
15
|
readonly platform: TargetConfiguredGateway;
|
|
16
16
|
readonly runtime: TargetRuntimeSnapshot;
|
|
17
|
-
readonly viewport:
|
|
17
|
+
readonly viewport: TargetViewportSnapshot;
|
|
18
18
|
readonly player: PlayerIdentity;
|
|
19
19
|
readonly identitySession: IdentitySession;
|
|
20
20
|
readonly launchIntent: LaunchIntent;
|
|
@@ -77,7 +77,7 @@ export class PlayScene extends Phaser.Scene {
|
|
|
77
77
|
private rewardHint(): string {
|
|
78
78
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
79
79
|
|
|
80
|
-
if (!rewardedAds.enabled) {
|
|
80
|
+
if (!rewardedAds.enabled || this.context.gameServices.client === undefined) {
|
|
81
81
|
return t(this.context.locale, 'rewardUnavailable');
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -86,27 +86,24 @@ export class PlayScene extends Phaser.Scene {
|
|
|
86
86
|
|
|
87
87
|
private async requestRewardedAd(): Promise<void> {
|
|
88
88
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
89
|
+
const gameServicesClient = this.context.gameServices.client;
|
|
89
90
|
|
|
90
|
-
if (!rewardedAds.enabled) {
|
|
91
|
+
if (!rewardedAds.enabled || gameServicesClient === undefined) {
|
|
91
92
|
this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
try {
|
|
96
|
-
const result = await
|
|
97
|
+
const result = await gameServicesClient.claimRewardedAd({
|
|
97
98
|
placementId: rewardedPlacementId,
|
|
98
99
|
idempotencyKey: createClientId('starter-reward'),
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
this.rewardText.setText(t(this.context.locale, 'reward', { status: result.status }));
|
|
102
|
+
this.rewardText.setText(
|
|
103
|
+
result.claim?.granted === true
|
|
104
|
+
? t(this.context.locale, 'reward', { status: result.status })
|
|
105
|
+
: t(this.context.locale, 'rewardError'),
|
|
106
|
+
);
|
|
110
107
|
} catch (error) {
|
|
111
108
|
await this.context.analytics.track({
|
|
112
109
|
name: 'rewarded_ad_rejected',
|
|
@@ -1,3 +1,10 @@
|
|
|
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);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
html,
|
|
2
9
|
body {
|
|
3
10
|
width: 100%;
|
|
@@ -13,10 +20,10 @@ body {
|
|
|
13
20
|
min-height: 100dvh;
|
|
14
21
|
overflow: hidden;
|
|
15
22
|
padding:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
var(--mpgd-safe-area-top)
|
|
24
|
+
var(--mpgd-safe-area-right)
|
|
25
|
+
var(--mpgd-safe-area-bottom)
|
|
26
|
+
var(--mpgd-safe-area-left);
|
|
20
27
|
background: #07111f;
|
|
21
28
|
color: #ffffff;
|
|
22
29
|
font-family: Arial, sans-serif;
|
|
@@ -24,7 +31,7 @@ body {
|
|
|
24
31
|
|
|
25
32
|
#game {
|
|
26
33
|
--mpgd-game-max-height: calc(
|
|
27
|
-
100dvh -
|
|
34
|
+
100dvh - var(--mpgd-safe-area-top) - var(--mpgd-safe-area-bottom)
|
|
28
35
|
);
|
|
29
36
|
|
|
30
37
|
box-sizing: border-box;
|
|
@@ -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;
|