@mpgd/cli 0.21.0 → 0.22.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.
Files changed (30) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +92 -8
  3. package/dist/microsoft-store-package-generation-contract.d.ts +5 -0
  4. package/dist/microsoft-store-package-generation-input.js +55 -0
  5. package/dist/microsoft-store-starter.d.ts +7 -0
  6. package/dist/microsoft-store-starter.js +176 -4
  7. package/dist/microsoft-store-submission.d.ts +24 -3
  8. package/dist/microsoft-store-submission.js +131 -4
  9. package/dist/offline-playtest.d.ts +31 -0
  10. package/dist/offline-playtest.js +5718 -0
  11. package/dist/production-target-readiness.js +38 -19
  12. package/package.json +15 -13
  13. package/templates/phaser-game/README.md +57 -33
  14. package/templates/phaser-game/apps/target-ait/apps-in-toss.config.ts +12 -0
  15. package/templates/phaser-game/apps/target-ait/package.json +5 -8
  16. package/templates/phaser-game/apps/target-ait/vite.config.ts +6 -15
  17. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +1 -1
  18. package/templates/phaser-game/apps/target-devvit/README.md +2 -1
  19. package/templates/phaser-game/apps/target-devvit/package.json +1 -1
  20. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +1 -0
  21. package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +11 -0
  22. package/templates/phaser-game/mpgd.targets.json +8 -1
  23. package/templates/phaser-game/package.json +2 -5
  24. package/templates/phaser-game/pnpm-workspace.yaml +3 -2
  25. package/templates/phaser-game/src/entry.ts +10 -4
  26. package/templates/phaser-game/src/gameEntry.ts +7 -1
  27. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +37 -0
  28. package/templates/phaser-game/src/platform/runtimeDetector.ts +1 -0
  29. package/templates/phaser-game/src/runtime/renderEntryFailure.ts +24 -0
  30. package/templates/phaser-game/vite.shared.ts +6 -2
@@ -19,31 +19,30 @@ export function assertProductionTargetReadiness(input) {
19
19
  if (ownerPathKey === undefined && input.targetPolicy === undefined) {
20
20
  throw new Error(`Production target ${input.target} requires a target policy.`);
21
21
  }
22
- const requiresVerse8RewardAuthority = input.targetPolicy?.runtime === 'verse8-web'
23
- && (input.targetPolicy.features.rewardedAds
24
- || input.targetPolicy.monetization.rewardedAds);
25
- const requiresWebMonetizationAuthority = input.targetPolicy?.runtime === 'web'
26
- && (input.targetPolicy.features.iap
27
- || input.targetPolicy.features.rewardedAds
28
- || input.targetPolicy.features.interstitialAds
29
- || input.targetPolicy.monetization.iap
30
- || input.targetPolicy.monetization.rewardedAds
31
- || input.targetPolicy.monetization.interstitialAds);
22
+ const targetConfig = readTargets(input.targetsFile)[input.target];
23
+ if (!isRecord(targetConfig)) {
24
+ throw new Error(`Missing target configuration for ${input.target}.`);
25
+ }
26
+ const targetPolicy = targetConfig.authoritativeGameServices === false
27
+ ? disableMonetization(input.targetPolicy)
28
+ : input.targetPolicy;
29
+ const requiresVerse8RewardAuthority = targetPolicy?.runtime === 'verse8-web'
30
+ && (targetPolicy.features.rewardedAds
31
+ || targetPolicy.monetization.rewardedAds);
32
+ const requiresWebMonetizationAuthority = (targetPolicy?.runtime === 'web'
33
+ || targetPolicy?.runtime === 'microsoft-store-pwa')
34
+ && (targetPolicy.features.iap
35
+ || targetPolicy.features.rewardedAds
36
+ || targetPolicy.features.interstitialAds
37
+ || targetPolicy.monetization.iap
38
+ || targetPolicy.monetization.rewardedAds
39
+ || targetPolicy.monetization.interstitialAds);
32
40
  const requiresMonetizationAuthority = requiresVerse8RewardAuthority
33
41
  || requiresWebMonetizationAuthority;
34
42
  if (ownerPathKey === undefined && !requiresMonetizationAuthority) {
35
43
  return;
36
44
  }
37
- const targetConfig = readTargets(input.targetsFile)[input.target];
38
- if (!isRecord(targetConfig)) {
39
- throw new Error(`Missing target configuration for ${input.target}.`);
40
- }
41
45
  if (ownerPathKey === undefined) {
42
- if (requiresMonetizationAuthority
43
- && targetConfig.authoritativeGameServices === false) {
44
- throw new Error(`Production target ${input.target} cannot enable monetization without `
45
- + 'authoritative game services.');
46
- }
47
46
  if (requiresMonetizationAuthority || targetConfig.authoritativeGameServices !== false) {
48
47
  assertAuthoritativeGameServicesUrl(input.gameServicesUrl, input.target);
49
48
  }
@@ -82,6 +81,26 @@ export function assertProductionTargetReadiness(input) {
82
81
  assertAuthoritativeGameServicesUrl(input.gameServicesUrl, input.target);
83
82
  }
84
83
  }
84
+ function disableMonetization(policy) {
85
+ if (policy === undefined) {
86
+ return undefined;
87
+ }
88
+ return {
89
+ ...policy,
90
+ features: {
91
+ ...policy.features,
92
+ iap: false,
93
+ rewardedAds: false,
94
+ interstitialAds: false,
95
+ },
96
+ monetization: {
97
+ ...policy.monetization,
98
+ iap: false,
99
+ rewardedAds: false,
100
+ interstitialAds: false,
101
+ },
102
+ };
103
+ }
85
104
  function readTargets(targetsFile) {
86
105
  let parsed;
87
106
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,6 +43,8 @@
43
43
  "@gunshi/plugin-completion": "^0.35.1",
44
44
  "@gunshi/plugin-i18n": "^0.35.1",
45
45
  "@gunshi/resources": "^0.35.1",
46
+ "entities": "^8.0.0",
47
+ "esbuild": "^0.28.1",
46
48
  "gunshi": "^0.35.1",
47
49
  "undici": "^7.28.0"
48
50
  },
@@ -50,19 +52,19 @@
50
52
  "@types/node": "^24.0.0",
51
53
  "ttsc": "0.18.4",
52
54
  "typescript": "7.0.2",
53
- "@mpgd/adapter-ait": "0.8.0",
54
- "@mpgd/adapter-browser": "0.5.1",
55
- "@mpgd/adapter-capacitor": "0.4.5",
56
- "@mpgd/adapter-devvit": "0.8.6",
57
- "@mpgd/adapter-verse8": "0.2.3",
58
- "@mpgd/analytics": "0.3.8",
59
- "@mpgd/bridge": "0.7.0",
60
- "@mpgd/game-services": "0.11.1",
61
- "@mpgd/i18n": "0.5.4",
62
- "@mpgd/catalog": "0.5.2",
55
+ "@mpgd/adapter-capacitor": "0.4.6",
56
+ "@mpgd/adapter-ait": "0.9.0",
57
+ "@mpgd/adapter-devvit": "0.9.0",
58
+ "@mpgd/adapter-browser": "0.6.0",
59
+ "@mpgd/adapter-verse8": "0.3.0",
60
+ "@mpgd/catalog": "0.6.0",
61
+ "@mpgd/bridge": "0.8.0",
62
+ "@mpgd/game-services": "0.12.0",
63
+ "@mpgd/i18n": "0.5.5",
63
64
  "@mpgd/phaser-assets": "0.4.1",
64
- "@mpgd/platform": "0.8.0",
65
- "@mpgd/target-config": "0.11.0"
65
+ "@mpgd/analytics": "0.3.9",
66
+ "@mpgd/platform": "0.9.0",
67
+ "@mpgd/target-config": "0.12.0"
66
68
  },
67
69
  "main": "./dist/index.js",
68
70
  "types": "./dist/index.d.ts",
@@ -49,21 +49,36 @@ pnpm exec mpgd target doctor \
49
49
  --targets __RECOMMENDED_MATRIX_TARGETS__
50
50
  ```
51
51
 
52
- When developing the kit itself, the same commands work through the repository
53
- script:
52
+ `mpgd.targets.json` contains `${MPGD_KIT_PATH}` tokens. The CLI resolves them
53
+ into `.mpgd.targets.generated.json` before calling the kit's existing target
54
+ build and smoke scripts.
55
+
56
+ ### Offline test-play copy
57
+
58
+ When a tester needs to open one local file without a server, package the
59
+ existing browser preview:
54
60
 
55
61
  ```sh
56
- pnpm --dir ../mpgd-kit mpgd target build-all \
57
- --targets-file "$PWD/mpgd.targets.json" \
58
- --kit-path ../mpgd-kit \
59
- --targets __RECOMMENDED_MATRIX_TARGETS__ \
60
- --profile staging \
61
- --ait-variant wrapper
62
+ pnpm exec mpgd target build web-preview staging \
63
+ --targets-file ./mpgd.targets.json \
64
+ --kit-path "${MPGD_KIT_PATH:-../mpgd-kit}"
65
+ pnpm exec mpgd game offline-playtest .
62
66
  ```
63
67
 
64
- `mpgd.targets.json` contains `${MPGD_KIT_PATH}` tokens. The CLI resolves them
65
- into `.mpgd.targets.generated.json` before calling the kit's existing target
66
- build and smoke scripts.
68
+ Open `artifacts/offline-playtest/index.html` directly. This is a
69
+ network-blocked, test-play-only copy—not a target in `mpgd.targets.json`, PWA,
70
+ deployment artifact, release-manifest entry, or store-submission package.
71
+ Custom output directories must remain below this game's `artifacts` directory.
72
+ `README.txt` and `offline-playtest.json` preserve that boundary and record the
73
+ result hash. Server-backed login, purchases, ads, rewards, leaderboards, and
74
+ cloud saves do not work in this copy. Workers, service workers, WebAssembly
75
+ streaming, dynamic imports, import maps, HTML base elements, CSS `@import`,
76
+ runtime-computed asset URLs, retained inline-module imports, script-driven
77
+ navigation, and browser-dependent `file://` storage are outside this helper's
78
+ contract. Meta-refresh navigation is removed from the copy. glTF files must
79
+ embed their dependencies as data URIs; use GLB for models with external buffers
80
+ or textures. A successful package does not prove that every game path works
81
+ offline; keep unavailable capability fallbacks in the game.
67
82
 
68
83
  The starter owns its Apps in Toss wrapper under `apps/target-ait`. With
69
84
  `authoritativeGameServices: false`, production keeps native identity, storage,
@@ -96,7 +111,9 @@ Optional targets can add their own config, scripts, runtime wiring, and release
96
111
  skill. To enable Microsoft Store after creation, run
97
112
  `pnpm exec mpgd target init microsoft-store --game . --kit-path ../mpgd-kit`.
98
113
  The initializer is idempotent and stops before writing when it finds conflicting
99
- game-owned values.
114
+ game-owned values. It requires this generated starter's existing
115
+ `src/platform/runtimeDetector.ts` and `vite.shared.ts` files; custom layouts must
116
+ add equivalent runtime and Vite gateway routing manually.
100
117
 
101
118
  ## Acceptance Handoff
102
119
 
@@ -250,29 +267,19 @@ pnpm build:ait
250
267
  pnpm smoke:ait
251
268
  ```
252
269
 
253
- The game-owned wrapper includes the Apps in Toss community devtools package for
254
- SDK mocks and device debugging:
270
+ The game-owned wrapper provides an explicit local SDK mock for browser layout
271
+ and non-native flow checks:
255
272
 
256
273
  ```sh
257
274
  pnpm ait:wrapper:dev
258
275
  pnpm ait:wrapper:dev:plain
259
- pnpm ait:wrapper:dev:phone
260
- pnpm ait:devtools:mcp
261
- pnpm ait:devtools:mcp:mobile
262
276
  ```
263
277
 
264
- `ait:wrapper:dev` runs `apps/target-ait` with the community Vite plugin enabled.
265
- It loads the last game bundle copied by `pnpm build:ait` from the wrapper's
266
- `public/game` directory, so run `pnpm build:ait` again after game changes before
267
- opening a wrapper devtools session. Use `ait:wrapper:dev:plain` to disable the
268
- devtools plugin. `ait:wrapper:dev:phone` uses the devtools tunnel and CDP relay
269
- for phone sandbox checks, and `ait:devtools:mcp:mobile` starts the community MCP
270
- bridge in the phone tunnel mode after the wrapper prints the tunnel URLs. Use
271
- `ait:devtools:mcp` for the default local or relay debugging mode. The kit
272
- intentionally leaves
273
- `cloudflared` postinstall disabled; the first phone tunnel session downloads the
274
- binary lazily through the community devtools package instead of during ordinary
275
- workspace install.
278
+ `ait:wrapper:dev` loads the last game bundle copied by `pnpm build:ait` from the
279
+ wrapper's `public/game` directory, so run `pnpm build:ait` again after game
280
+ changes before opening it. The local mock deliberately keeps ads, promotions,
281
+ and IAP unavailable; test those flows from the Apps in Toss console QR sandbox
282
+ or a real device after uploading the `.ait` bundle.
276
283
 
277
284
  The starter includes `@ait-co/console-cli`, the Apps in Toss community console
278
285
  CLI, as a dev dependency for project-local console automation:
@@ -301,6 +308,13 @@ Apps in Toss console, while the file owns `appName`, `brand.primaryColor`,
301
308
  permissions, `webView`, and `webBundleDir`. Do not restore the removed SDK 2
302
309
  `granite.config.ts` fields.
303
310
 
311
+ Apps in Toss navigation chrome is target-owned. Configure the `ait` target's
312
+ `navigationBar` in `mpgd.targets.json`; `mpgd target build ait` validates the
313
+ supported appearance options and forwards them to `apps-in-toss.config.ts` at
314
+ build time. The Phaser starter uses a transparent dark navigation bar without
315
+ back or home chrome so game content can continue underneath it. Keep
316
+ the game's own safe-area padding clear of Toss controls.
317
+
304
318
  Before the first SDK 3 release, allow both of these exact origins on every API
305
319
  called by the mini-app:
306
320
 
@@ -334,7 +348,7 @@ HTTPS game-services backend that verifies purchase or reward evidence.
334
348
 
335
349
  - Browser preview is fully game-owned and writes to `artifacts/web-preview`.
336
350
  <!-- mpgd:microsoft-store:start -->
337
- - Microsoft Store is a game-owned PWA target that reuses the browser adapter and
351
+ - Microsoft Store is a game-owned PWA target with a dedicated Digital Goods adapter and
338
352
  writes to `artifacts/microsoft-store` with `manifest.webmanifest` for
339
353
  PWABuilder packaging. Replace the starter icon and manifest metadata before
340
354
  Store submission. Production builds also emit `pwa-release.json` and an
@@ -345,6 +359,16 @@ HTTPS game-services backend that verifies purchase or reward evidence.
345
359
  app; do not force activation with `skipWaiting()`. Keep the web manifest `id`
346
360
  game-specific because the cache namespace uses it to isolate apps sharing an
347
361
  origin.
362
+ The generated target starts with `authoritativeGameServices: false` while
363
+ `mpgd.microsoft-store.json` uses `commerce.mode: "disabled"`, so its effective
364
+ target contains no enabled IAP products. Switch both settings to their enabled
365
+ values only after the game supplies consumable
366
+ `InAppOfferToken` mappings plus an authoritative HTTPS backend. The backend
367
+ must resolve the renewable User Store ID and a separate stable account-link
368
+ ID from trusted player identity, query Collections v9, record the grant
369
+ idempotently, and consume through Collections v8. Never derive the account-link
370
+ ID from the User Store ID or treat the browser `purchaseToken` as a unique
371
+ transaction ID.
348
372
  Before Partner Center submission, replace every placeholder in
349
373
  `mpgd.microsoft-store.json`, add at least one real PNG screenshot per listing
350
374
  locale (maximum 50 MB and at least 1366 x 768 in either orientation), and run
@@ -405,9 +429,9 @@ HTTPS game-services backend that verifies purchase or reward evidence.
405
429
  bridge fails closed on identity, provider, serialization, and quota errors;
406
430
  it never switches progress to browser `localStorage`.
407
431
  - Apps in Toss is game-owned in `apps/target-ait`. The wrapper keeps the app
408
- name, console state, review metadata, and `@ait-co/devtools` development
409
- integration local to the game while reusing `@mpgd/adapter-ait` for the
410
- production bridge and game-bundle loader.
432
+ name, console state, review metadata, and local SDK mock configuration in the
433
+ game while reusing `@mpgd/adapter-ait` for the production bridge and
434
+ game-bundle loader.
411
435
  - Android and iOS currently use the kit reference Capacitor shell at
412
436
  `${MPGD_KIT_PATH}/apps/mobile-capacitor`. That is useful for local artifact
413
437
  checks, and final artifacts are copied back to this game's `release-output/`
@@ -1,11 +1,14 @@
1
1
  import { defineConfig, type AppsInTossConfig } from '@apps-in-toss/web-framework/config';
2
2
 
3
+ const navigationBar = readNavigationBar(process.env.MPGD_AIT_NAVIGATION_BAR);
4
+
3
5
  const config: AppsInTossConfig = {
4
6
  appName: readEnvString(process.env.MPGD_AIT_APP_NAME) ?? '__GAME_NAME__',
5
7
  brand: {
6
8
  primaryColor: readEnvString(process.env.MPGD_AIT_PRIMARY_COLOR) ?? '#101820',
7
9
  },
8
10
  permissions: [],
11
+ ...(navigationBar === undefined ? {} : { navigationBar }),
9
12
  webView: {
10
13
  bounces: false,
11
14
  pullToRefreshEnabled: false,
@@ -24,3 +27,12 @@ function readEnvString(input: string | undefined): string | undefined {
24
27
  const trimmed = input?.trim();
25
28
  return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed;
26
29
  }
30
+
31
+ function readNavigationBar(
32
+ input: string | undefined,
33
+ ): AppsInTossConfig['navigationBar'] | undefined {
34
+ const encoded = readEnvString(input);
35
+ return encoded === undefined
36
+ ? undefined
37
+ : JSON.parse(encoded) as AppsInTossConfig['navigationBar'];
38
+ }
@@ -4,30 +4,27 @@
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "vite --host 0.0.0.0",
8
- "dev:plain": "MPGD_AIT_DEVTOOLS=0 VITE_MPGD_AIT_MOCK_IDENTITY=1 vite --host 0.0.0.0",
9
- "dev:phone": "AIT_TUNNEL=1 AIT_TUNNEL_CDP=1 vite --host 0.0.0.0",
7
+ "dev": "MPGD_AIT_LOCAL_MOCK=1 VITE_MPGD_AIT_MOCK_IDENTITY=1 vite --host 0.0.0.0",
8
+ "dev:plain": "pnpm dev",
10
9
  "build": "vite build && ait build",
11
10
  "ait:build": "ait build",
12
11
  "ait:deploy": "ait deploy --api-key \"$AIT_API_KEY\"",
13
- "devtools:mcp": "pnpm exec devtools-mcp",
14
- "devtools:mcp:mobile": "pnpm exec devtools-mcp --target=mobile",
15
12
  "check": "ttsc --noEmit -p tsconfig.json",
16
13
  "lint": "ttsc --noEmit -p tsconfig.json",
17
14
  "format": "ttsc format",
18
15
  "fix": "ttsc fix"
19
16
  },
20
17
  "dependencies": {
21
- "@apps-in-toss/web-framework": "3.0.0-beta.da14818",
18
+ "@apps-in-toss/web-framework": "3.0.2",
22
19
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__"
23
20
  },
24
21
  "devDependencies": {
25
22
  "@ait-co/devtools": "^0.1.140",
26
- "@apps-in-toss/cli": "3.0.0-beta.66a479a",
23
+ "@apps-in-toss/cli": "3.0.2",
27
24
  "@ttsc/unplugin": "0.18.4",
28
25
  "@types/node": "^24.0.0",
29
26
  "ttsc": "0.18.4",
30
27
  "typescript": "7.0.2",
31
- "vite": "^8.1.3"
28
+ "vite": "8.1.3"
32
29
  }
33
30
  }
@@ -1,22 +1,16 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
 
3
- import aitDevtools from '@ait-co/devtools/unplugin';
4
3
  import { extractAitAdBridgeConfig } from '@mpgd/adapter-ait/ad-config';
5
4
  import ttsc from '@ttsc/unplugin/vite';
6
5
  import { defineConfig } from 'vite';
7
6
 
8
- const isTruthyEnv = (value: string | undefined): boolean =>
9
- value !== undefined && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
10
- const aitDevtoolsTunnel = isTruthyEnv(process.env.AIT_TUNNEL)
11
- ? { cdp: isTruthyEnv(process.env.AIT_TUNNEL_CDP) }
12
- : false;
13
7
  const aitAppName = process.env.MPGD_AIT_APP_NAME?.trim() || '__GAME_NAME__';
14
8
  const aitAdConfig = readAitAdConfig(process.env.MPGD_AD_PLACEMENTS_FILE);
15
9
 
16
10
  export default defineConfig(({ command, isPreview }) => {
17
- const enableAitDevtools = command === 'serve'
11
+ const enableLocalAitMock = command === 'serve'
18
12
  && !isPreview
19
- && process.env.MPGD_AIT_DEVTOOLS !== '0';
13
+ && process.env.MPGD_AIT_LOCAL_MOCK !== '0';
20
14
 
21
15
  return {
22
16
  define: {
@@ -24,21 +18,18 @@ export default defineConfig(({ command, isPreview }) => {
24
18
  __MPGD_AIT_AD_GROUP_IDS__: JSON.stringify(aitAdConfig.adGroupIds),
25
19
  __MPGD_AIT_AD_PLACEMENT_TYPES__: JSON.stringify(aitAdConfig.adPlacementTypes),
26
20
  },
27
- // Vite prebundles the Kit host before plugin resolve hooks run. An explicit development
28
- // alias keeps that optimized host while replacing its transitive native SDK import.
29
- ...(enableAitDevtools
21
+ // Browser-only local development must not pretend that native rewards or
22
+ // purchases are available. Production and console builds use the real SDK.
23
+ ...(enableLocalAitMock
30
24
  ? {
31
25
  resolve: {
32
26
  alias: {
33
- '@apps-in-toss/web-framework': '@ait-co/devtools/mock',
27
+ '@apps-in-toss/web-framework': '@mpgd/adapter-ait/local-mock',
34
28
  },
35
29
  },
36
30
  }
37
31
  : {}),
38
32
  plugins: [
39
- ...(enableAitDevtools
40
- ? [aitDevtools.vite({ mcp: true, tunnel: aitDevtoolsTunnel })]
41
- : []),
42
33
  ttsc({ project: 'tsconfig.json', plugins: false }),
43
34
  ],
44
35
  build: {
@@ -22,7 +22,7 @@
22
22
  "@ttsc/unplugin": "0.18.4",
23
23
  "ttsc": "0.18.4",
24
24
  "typescript": "7.0.2",
25
- "vite": "^8.1.3",
25
+ "vite": "8.1.3",
26
26
  "wrangler": "^4.107.0"
27
27
  }
28
28
  }
@@ -52,7 +52,8 @@ leaderboard. Devvit ranking should be owned by a server completion handler that
52
52
  validates the game-specific attempt, records it through the verified leaderboard
53
53
  provider from `@mpgd/adapter-devvit/server`, and exposes only an authenticated
54
54
  game-scoped snapshot route. Do not enable `nativeLeaderboard` or trust a raw
55
- client score submission for that flow.
55
+ client score submission for that flow. Keep `remoteLeaderboard` disabled too;
56
+ the verified completion route is not the generic `PlatformGateway` leaderboard.
56
57
 
57
58
  `devvit playtest` runs the official unified Vite build in watch mode, so a
58
59
  separate client/server watcher or staging prebuild is not required.
@@ -30,6 +30,6 @@
30
30
  "devvit": "0.13.8",
31
31
  "ttsc": "0.18.4",
32
32
  "typescript": "7.0.2",
33
- "vite": "^8.1.3"
33
+ "vite": "8.1.3"
34
34
  }
35
35
  }
@@ -141,6 +141,7 @@ async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse
141
141
  rewardedAds: false,
142
142
  interstitialAds: false,
143
143
  nativeLeaderboard: false,
144
+ remoteLeaderboard: false,
144
145
  achievements: false,
145
146
  cloudSave: true,
146
147
  socialShare: false,
@@ -74,6 +74,17 @@ Use the browser adapter for local and hosted web behavior. Verify installed-PWA
74
74
  updates without forcing service-worker activation while old windows remain
75
75
  open.
76
76
 
77
+ For direct-file test play, build `web-preview` and run
78
+ `pnpm exec mpgd game offline-playtest .`. Open
79
+ `artifacts/offline-playtest/index.html`. This output is deliberately a
80
+ network-blocked test copy, not another release target, deployable PWA,
81
+ release-manifest entry, or store package. Its README and JSON evidence describe
82
+ server-feature, Worker, dynamic-import, import-map, HTML-base, CSS `@import`,
83
+ dynamic-asset, inline-module-import, script-navigation, and `file://` storage
84
+ limitations. Meta-refresh navigation is removed. Keep server calls behind
85
+ platform or game-service helpers and expose a disabled or local fallback when
86
+ those capabilities are unavailable.
87
+
77
88
  ### Microsoft Store
78
89
 
79
90
  This target is optional. Enable it in an existing generated game with:
@@ -10,7 +10,8 @@
10
10
  "microsoft-store": {
11
11
  "kind": "web",
12
12
  "gameApp": ".",
13
- "adapter": "browser",
13
+ "adapter": "microsoft-store",
14
+ "authoritativeGameServices": false,
14
15
  "icon": { "profile": "microsoft-pwa" },
15
16
  "output": "artifacts/microsoft-store"
16
17
  },
@@ -51,6 +52,12 @@
51
52
  "primaryColor": "#101820",
52
53
  "sdkMajor": 3
53
54
  },
55
+ "navigationBar": {
56
+ "withBackButton": false,
57
+ "withHomeButton": false,
58
+ "transparentBackground": true,
59
+ "theme": "dark"
60
+ },
54
61
  "icon": { "profile": "ait" },
55
62
  "webDir": "apps/target-ait/public/game",
56
63
  "artifact": ".ait"
@@ -19,9 +19,6 @@
19
19
  "smoke:ait": "pnpm exec mpgd target smoke ait --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
20
20
  "ait:wrapper:dev": "pnpm --dir apps/target-ait run dev",
21
21
  "ait:wrapper:dev:plain": "pnpm --dir apps/target-ait run dev:plain",
22
- "ait:wrapper:dev:phone": "pnpm --dir apps/target-ait run dev:phone",
23
- "ait:devtools:mcp": "pnpm --dir apps/target-ait run devtools:mcp",
24
- "ait:devtools:mcp:mobile": "pnpm --dir apps/target-ait run devtools:mcp:mobile",
25
22
  "ait:console:login": "aitcc login",
26
23
  "ait:console:whoami": "aitcc whoami",
27
24
  "ait:console:init": "aitcc app init",
@@ -58,7 +55,7 @@
58
55
  },
59
56
  "dependencies": {
60
57
  "@ait-co/polyfill": "^0.1.17",
61
- "@apps-in-toss/web-framework": "3.0.0-beta.da14818",
58
+ "@apps-in-toss/web-framework": "3.0.2",
62
59
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__",
63
60
  "@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION_ADAPTER_BROWSER__",
64
61
  "@mpgd/adapter-capacitor": "__MPGD_DEPENDENCY_VERSION_ADAPTER_CAPACITOR__",
@@ -82,7 +79,7 @@
82
79
  "@types/node": "^24.0.0",
83
80
  "ttsc": "0.18.4",
84
81
  "typescript": "7.0.2",
85
- "vite": "^8.1.3",
82
+ "vite": "8.1.3",
86
83
  "wrangler": "^4.107.0"
87
84
  }
88
85
  }
@@ -5,10 +5,11 @@ __PNPM_WORKSPACE_KIT_PACKAGES__
5
5
  allowBuilds:
6
6
  '@sentry/cli': true
7
7
  '@swc/core': true
8
- # @ait-co/devtools lazily installs cloudflared when the phone tunnel starts;
9
- # keep default workspace installs free of external binary downloads.
10
8
  cloudflared: false
11
9
  esbuild: true
12
10
  protobufjs: true
13
11
  sharp: true
14
12
  workerd: true
13
+ peerDependencyRules:
14
+ allowedVersions:
15
+ '@ait-co/devtools>@apps-in-toss/web-framework': '3.0.2'
@@ -1,7 +1,13 @@
1
- if (__APP_TARGET__ === 'reddit') {
2
- await import('./platform/devvitEntrypoint');
3
- } else {
4
- await import('./main');
1
+ import { renderEntryFailure } from './runtime/renderEntryFailure';
2
+
3
+ try {
4
+ if (__APP_TARGET__ === 'reddit') {
5
+ await import('./platform/devvitEntrypoint');
6
+ } else {
7
+ await import('./main');
8
+ }
9
+ } catch (error) {
10
+ renderEntryFailure(error);
5
11
  }
6
12
 
7
13
  export {};
@@ -1,3 +1,9 @@
1
- await import('./main');
1
+ import { renderEntryFailure } from './runtime/renderEntryFailure';
2
+
3
+ try {
4
+ await import('./main');
5
+ } catch (error) {
6
+ renderEntryFailure(error);
7
+ }
2
8
 
3
9
  export {};
@@ -0,0 +1,37 @@
1
+ import { createBrowserPlatformGateway } from '@mpgd/adapter-browser';
2
+ import {
3
+ createMicrosoftStoreCommerceAdapter,
4
+ withMicrosoftStoreCommerceAdapter,
5
+ } from '@mpgd/adapter-browser/microsoft-store';
6
+ import type { PlatformGateway } from '@mpgd/platform';
7
+
8
+ import type { RuntimeConfig } from '../runtimeDetector';
9
+
10
+ const browser = createBrowserPlatformGateway();
11
+ const base: PlatformGateway = { ...browser, target: 'microsoft-store' };
12
+ const commerce = createMicrosoftStoreCommerceAdapter({
13
+ getRecoveryScope: () => 'configuration-required',
14
+ products: [],
15
+ authority: {
16
+ async getAvailability() {
17
+ return 'configuration-required';
18
+ },
19
+ async claimRecoveryOwnership() {
20
+ return { status: 'denied' } as const;
21
+ },
22
+ async hasRecoveryOwnership() {
23
+ return { status: 'denied' } as const;
24
+ },
25
+ async verifyAndGrant() {
26
+ return { status: 'failed' };
27
+ },
28
+ async getEntitlements() {
29
+ return [];
30
+ },
31
+ },
32
+ });
33
+
34
+ /** Configure Store products, authenticated recovery scope, and authority before checkout. */
35
+ export async function createBuildGateway(_runtime: RuntimeConfig): Promise<PlatformGateway> {
36
+ return withMicrosoftStoreCommerceAdapter(base, commerce);
37
+ }
@@ -4,6 +4,7 @@ const validTargets = new Set<string>([
4
4
  'android',
5
5
  'ios',
6
6
  'ait',
7
+ 'microsoft-store',
7
8
  'reddit',
8
9
  'verse8',
9
10
  'telegram',
@@ -0,0 +1,24 @@
1
+ export function renderEntryFailure(error: unknown): void {
2
+ console.error('[entry] Failed to load the game module.', error);
3
+
4
+ const root = document.querySelector<HTMLElement>('#game');
5
+
6
+ if (root === null) {
7
+ return;
8
+ }
9
+
10
+ root.replaceChildren();
11
+ const panel = document.createElement('div');
12
+ panel.setAttribute('role', 'alert');
13
+ panel.textContent = 'Failed to load the game. Please refresh and try again.';
14
+ panel.style.boxSizing = 'border-box';
15
+ panel.style.width = 'min(720px, calc(100% - 32px))';
16
+ panel.style.margin = '20dvh auto 0';
17
+ panel.style.padding = '20px';
18
+ panel.style.border = '1px solid #dc2626';
19
+ panel.style.borderRadius = '8px';
20
+ panel.style.background = '#1f1111';
21
+ panel.style.color = '#fecaca';
22
+ panel.style.font = '16px/1.5 Arial, sans-serif';
23
+ root.append(panel);
24
+ }
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  import ttsc from '@ttsc/unplugin/vite';
5
- import type { UserConfig } from 'vite';
5
+ import type { PluginOption, UserConfig } from 'vite';
6
6
 
7
7
  import {
8
8
  assertRuntimeTargetConfigMatrix,
@@ -51,10 +51,12 @@ export function createGameViteSharedConfig(
51
51
  return {
52
52
  base: './',
53
53
  plugins: [
54
+ // The unplugin runtime is Vite-compatible, but its recursive generic
55
+ // plugin type can exceed TypeScript's comparison depth in a monorepo.
54
56
  ttsc({
55
57
  project: input.project,
56
58
  plugins: false,
57
- }),
59
+ }) as unknown as PluginOption,
58
60
  ],
59
61
  resolve: {
60
62
  alias: {
@@ -124,6 +126,8 @@ export function resolveBuildGatewayModule(input: {
124
126
  : 'src/platform/buildGateways/reddit.ts';
125
127
  case 'verse8':
126
128
  return 'src/platform/buildGateways/verse8.ts';
129
+ case 'microsoft-store':
130
+ return 'src/platform/buildGateways/microsoftStore.ts';
127
131
  default:
128
132
  return 'src/platform/buildGateways/browser.ts';
129
133
  }