@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.
@@ -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 = normalizeBuildTarget(readRequiredPositional(positionals, 0, '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 = normalizeBuildTarget(readRequiredPositional(positionals, 0, '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 = normalizeBuildTarget(readRequiredPositional(positionals, 0, '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
- const normalized = normalizeBuildTarget(target);
1995
- if (normalized === 'ait') {
2001
+ if (target === 'ait') {
1996
2002
  return input.aitVariant;
1997
2003
  }
1998
- if (normalized === 'ios') {
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
- const targets = new Set();
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 normalizeBuildTarget(target);
2120
+ return normalizeConfiguredTargetName(target);
2115
2121
  }
2116
2122
  catch {
2117
2123
  return undefined;
2118
2124
  }
2119
2125
  }
2120
- function normalizeBuildTarget(target) {
2121
- if (target === 'browser' || target === 'web') {
2122
- return 'web-preview';
2123
- }
2124
- if (target === 'msstore') {
2125
- return 'microsoft-store';
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
- return target;
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,2 @@
1
+ export declare const targetConfigExtensionsFileEnv = "MPGD_TARGET_CONFIG_EXTENSIONS_FILE";
2
+ export declare const targetConfigMatrixFileEnv = "MPGD_TARGET_CONFIG_MATRIX_FILE";
@@ -0,0 +1,2 @@
1
+ export const targetConfigExtensionsFileEnv = 'MPGD_TARGET_CONFIG_EXTENSIONS_FILE';
2
+ export const targetConfigMatrixFileEnv = 'MPGD_TARGET_CONFIG_MATRIX_FILE';
@@ -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.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
- "@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/game-services": "0.11.0",
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/catalog": "0.5.1",
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 type { AdPlacements } from '@mpgd/catalog';
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(_runtime: RuntimeConfig): Promise<PlatformGateway> {
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 = placement.platformPlacementIds.verse8;
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 = targetConfigMatrixJson as 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;