@mpgd/cli 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,19 +52,19 @@
52
52
  "@types/node": "^24.0.0",
53
53
  "ttsc": "0.18.4",
54
54
  "typescript": "7.0.2",
55
- "@mpgd/adapter-ait": "0.10.0",
56
- "@mpgd/adapter-browser": "0.7.1",
55
+ "@mpgd/adapter-ait": "0.11.0",
57
56
  "@mpgd/adapter-capacitor": "0.4.7",
58
- "@mpgd/adapter-devvit": "0.9.1",
57
+ "@mpgd/adapter-devvit": "0.9.3",
58
+ "@mpgd/adapter-browser": "0.7.1",
59
59
  "@mpgd/adapter-verse8": "0.3.1",
60
60
  "@mpgd/analytics": "0.3.10",
61
61
  "@mpgd/bridge": "0.9.0",
62
- "@mpgd/catalog": "0.7.0",
63
62
  "@mpgd/game-services": "0.12.1",
64
- "@mpgd/phaser-assets": "0.4.1",
65
- "@mpgd/platform": "0.10.0",
63
+ "@mpgd/catalog": "0.7.0",
66
64
  "@mpgd/i18n": "0.5.6",
67
- "@mpgd/target-config": "0.13.0"
65
+ "@mpgd/phaser-assets": "0.4.1",
66
+ "@mpgd/target-config": "0.13.0",
67
+ "@mpgd/platform": "0.10.0"
68
68
  },
69
69
  "main": "./dist/index.js",
70
70
  "types": "./dist/index.d.ts",
@@ -231,12 +231,16 @@ The generated `devvit.json` app name is a Devvit-safe slug derived from the game
231
231
  directory name; edit it before `devvit:init` if you need a different Reddit app
232
232
  name.
233
233
 
234
- The default post entry builds a lightweight inline mode launch screen without
235
- importing Phaser eagerly. Play here loads gameplay inside the post only after a
236
- user click; Open expanded mode requests the separate `game` entrypoint, whose
237
- `game.html` document starts the Phaser bundle directly. Keep these physical
238
- entries separate so feed cards meet Devvit's inline mode performance and
239
- gesture requirements while still supporting playable posts.
234
+ The default post entry builds a fixed, non-scrolling inline preview without
235
+ importing Phaser. Play full screen requests the separate `game` entrypoint,
236
+ whose `game.html` document starts the Phaser bundle directly. Keep these
237
+ physical entries separate so feed cards preserve Reddit-native gestures and do
238
+ not create scroll traps.
239
+
240
+ Inline gameplay remains an explicit opt-in through `startDevvitWebView`. Use it
241
+ only when the game interaction is tap/click based and the complete inline
242
+ surface has been reviewed against current Reddit requirements. Generated games
243
+ use `startDevvitPreviewWebView` by default.
240
244
 
241
245
  First-time Devvit setup:
242
246
 
@@ -312,8 +316,16 @@ Apps in Toss navigation chrome is target-owned. Configure the `ait` target's
312
316
  `navigationBar` in `mpgd.targets.json`; `mpgd target build ait` validates the
313
317
  supported appearance options and forwards them to `apps-in-toss.config.ts` at
314
318
  build time. The Phaser starter uses a transparent dark navigation bar without
315
- back, home, title, or app-icon chrome so game content can continue underneath it. Keep
316
- the game's own safe-area padding clear of Toss controls.
319
+ back, home, title, or app-icon chrome so game content can continue underneath
320
+ it. The wrapper installs `installAitSafeAreaCssVariables()` before the game
321
+ mount, mirrors the official `SafeArea.get()` and `SafeArea.subscribe()` values
322
+ into `--mpgd-safe-area-*`, and exposes
323
+ `--mpgd-ait-navigation-content-top` for a full-width HUD below the native
324
+ controls. CSS `env(safe-area-inset-*)` remains the local and older-host
325
+ fallback. Custom wrappers should import the installer from
326
+ `@mpgd/adapter-ait/safe-area`; keep SDK imports out of Phaser scenes.
327
+ The default 52px navigation band and 10px edge gap can be overridden through
328
+ the installer options when a custom host treatment has measured geometry.
317
329
 
318
330
  During the SDK 3 origin transition, allow all four exact origins on every API
319
331
  called by the mini-app:
@@ -32,9 +32,9 @@ continue to expose game-facing behavior through `PlatformGateway`. The Devvit
32
32
  app root is game-owned in `apps/target-devvit`; run `pnpm devvit:init` once
33
33
  after login before upload or playtest.
34
34
  Verify the default post entry renders a lightweight inline mode launch screen,
35
- Play here starts gameplay inside the post, and the `game` entry opens the
36
- separate expanded mode Phaser document. Inline gameplay must remain tap/click
37
- only and must not trap feed scrolling.
35
+ does not load Phaser or create a scroll container, and Play full screen opens
36
+ the separate expanded mode Phaser document. Inline gameplay remains available
37
+ only as an explicit adapter opt-in and must preserve Reddit-native gestures.
38
38
 
39
39
  <!-- mpgd:microsoft-store:start -->
40
40
  For Microsoft Store changes, keep the first pass as a PWA/web target that uses
@@ -2,6 +2,7 @@ import './styles.css';
2
2
 
3
3
  import type { AitAdPlacementType } from '@mpgd/adapter-ait/ad-config';
4
4
  import { installAitHostBridge, type InstallAitHostBridgeOptions } from '@mpgd/adapter-ait/host';
5
+ import { installAitSafeAreaCssVariables } from '@mpgd/adapter-ait/safe-area';
5
6
  import { mountAitGameBundle } from '@mpgd/adapter-ait/wrapper';
6
7
 
7
8
  declare const __MPGD_AIT_APP_NAME__: string;
@@ -10,6 +11,7 @@ declare const __MPGD_AIT_AD_PLACEMENT_TYPES__: Readonly<
10
11
  Record<string, AitAdPlacementType>
11
12
  >;
12
13
 
14
+ installAitSafeAreaCssVariables();
13
15
  installAitHostBridge({
14
16
  appName: __MPGD_AIT_APP_NAME__,
15
17
  adGroupIds: __MPGD_AIT_AD_GROUP_IDS__,
@@ -41,11 +41,15 @@ replace the `devvit.json` scheduler endpoint and should not use process-local
41
41
  memory when delivery must cross instances. The generated target exposes only
42
42
  the oRPC bridge route; the former JSON compatibility route is not generated.
43
43
 
44
- The default post entry uses `index.html` for a lightweight inline mode launch
45
- screen. Play here loads Phaser inside the post after a user click. Open expanded
46
- mode requests the `game` entrypoint, which loads the separate `game.html`
47
- document. Keep launch UI free of eager game runtime imports, and keep inline
48
- gameplay tap/click only so the post does not trap feed scrolling.
44
+ The default post entry uses `index.html` for a fixed, non-scrolling preview.
45
+ Play full screen requests the `game` entrypoint, which loads Phaser from the
46
+ separate `game.html` document. Keep preview UI free of game runtime imports so
47
+ the post cannot trap feed scrolling.
48
+
49
+ Games that intentionally support inline gameplay can opt in through
50
+ `startDevvitWebView`; the generated default uses `startDevvitPreviewWebView`.
51
+ An opt-in inline surface must remain tap/click based and preserve Reddit-native
52
+ gestures across its complete gameplay flow.
49
53
 
50
54
  The generated bridge does not advertise or accept a generic platform
51
55
  leaderboard. Devvit ranking should be owned by a server completion handler that
@@ -19,17 +19,17 @@
19
19
  "fix": "ttsc fix"
20
20
  },
21
21
  "dependencies": {
22
- "@devvit/web": "0.13.8",
22
+ "@devvit/web": "0.14.1",
23
23
  "@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION_ADAPTER_DEVVIT__",
24
24
  "@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__"
25
25
  },
26
26
  "devDependencies": {
27
- "@devvit/start": "0.13.8",
27
+ "@devvit/start": "0.14.1",
28
28
  "@ttsc/unplugin": "0.18.4",
29
29
  "@types/node": "^24.0.0",
30
- "devvit": "0.13.8",
30
+ "devvit": "0.14.1",
31
31
  "ttsc": "0.18.4",
32
32
  "typescript": "7.0.2",
33
- "vite": "8.1.3"
33
+ "vite": "7.3.6"
34
34
  }
35
35
  }
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
4
  import { devvit } from '@devvit/start/vite';
5
- import { defineConfig } from 'vite';
5
+ import { defineConfig, type PluginOption } from 'vite';
6
6
 
7
7
  import { createGameViteSharedConfig } from '../../vite.shared';
8
8
 
@@ -10,7 +10,7 @@ const wrapperRoot = fileURLToPath(new URL('.', import.meta.url));
10
10
  const gameRoot = resolve(wrapperRoot, '../..');
11
11
 
12
12
  export default defineConfig(({ mode }) => {
13
- const shared = createGameViteSharedConfig({
13
+ const shared = createGameViteSharedConfig<PluginOption>({
14
14
  appTarget: 'reddit',
15
15
  configTarget: 'reddit',
16
16
  gameRoot,
@@ -1,28 +1,26 @@
1
1
  import {
2
2
  requestDevvitExpandedMode,
3
- startDevvitWebView,
4
- type DevvitInlineModeContext,
3
+ startDevvitPreviewWebView,
5
4
  } from '@mpgd/adapter-devvit/web';
6
5
 
7
- await startDevvitWebView({
8
- async mountInlineMode(context) {
6
+ const expandedRequestRecoveryMs = 8_000;
7
+
8
+ await startDevvitPreviewWebView({
9
+ async mountInlinePreview() {
9
10
  await import('./devvitInlineMode.css');
10
- renderInlineLaunchScreen(context);
11
+ renderInlineLaunchScreen();
11
12
  },
12
- async loadGameplay() {
13
+ async loadExpandedGame() {
13
14
  await import('../main');
14
15
  },
15
16
  onModeUnavailable(error) {
16
17
  if (!(error instanceof ReferenceError)) {
17
- console.warn('[devvit] web view mode unavailable; loading gameplay.', error);
18
+ console.warn('[devvit] web view mode unavailable; loading expanded gameplay.', error);
18
19
  }
19
20
  },
20
21
  });
21
22
 
22
- function renderInlineLaunchScreen(
23
- context: DevvitInlineModeContext,
24
- errorMessage?: string,
25
- ): void {
23
+ function renderInlineLaunchScreen(): void {
26
24
  const launchScreen = document.createElement('main');
27
25
  launchScreen.className = 'devvit-launch-screen';
28
26
 
@@ -35,90 +33,69 @@ function renderInlineLaunchScreen(
35
33
 
36
34
  const description = document.createElement('p');
37
35
  description.className = 'devvit-launch-screen__description';
38
- description.textContent = 'Play directly in the post or open expanded mode.';
36
+ description.textContent = 'Open the full game without interrupting your Reddit feed.';
39
37
 
40
38
  const actions = document.createElement('div');
41
39
  actions.className = 'devvit-launch-screen__actions';
42
40
 
43
- const playInlineButton = document.createElement('button');
44
- playInlineButton.className = 'devvit-launch-screen__button';
45
- playInlineButton.type = 'button';
46
- playInlineButton.textContent = 'Play here';
47
-
48
41
  const expandButton = document.createElement('button');
49
- expandButton.className =
50
- 'devvit-launch-screen__button devvit-launch-screen__button--secondary';
42
+ expandButton.className = 'devvit-launch-screen__button';
51
43
  expandButton.type = 'button';
52
- expandButton.textContent = 'Open expanded mode';
44
+ expandButton.textContent = 'Play full screen';
53
45
 
54
46
  const status = document.createElement('p');
55
47
  status.className = 'devvit-launch-screen__status';
56
48
  status.setAttribute('aria-live', 'polite');
57
- status.textContent = errorMessage ?? '';
49
+ status.setAttribute('role', 'status');
50
+
51
+ let nextRequestId = 0;
52
+ let activeRequestId: number | undefined;
53
+ let recoveryTimeout: ReturnType<typeof globalThis.setTimeout> | undefined;
54
+ const recover = (requestId: number, message: string) => {
55
+ if (activeRequestId !== requestId) {
56
+ return;
57
+ }
58
58
 
59
- playInlineButton.addEventListener('click', () => {
60
- setBusy(true, 'Loading gameplay…');
61
- const loading = mountGameplayDocument();
59
+ activeRequestId = undefined;
60
+ if (recoveryTimeout !== undefined) {
61
+ globalThis.clearTimeout(recoveryTimeout);
62
+ recoveryTimeout = undefined;
63
+ }
62
64
 
63
- void context.startGameplay()
64
- .then(() => {
65
- loading.remove();
66
- })
67
- .catch((error: unknown) => {
68
- console.error('[devvit] inline mode gameplay failed to load.', error);
69
- renderInlineLaunchScreen(context, 'Gameplay could not start. Try again.');
70
- });
71
- });
65
+ expandButton.disabled = false;
66
+ status.textContent = message;
67
+ };
72
68
  expandButton.addEventListener('click', (event) => {
73
- setBusy(true, 'Opening expanded mode…');
69
+ const requestId = ++nextRequestId;
70
+
71
+ activeRequestId = requestId;
72
+ expandButton.disabled = true;
73
+ status.textContent = 'Opening full screen…';
74
+ recoveryTimeout = globalThis.setTimeout(() => {
75
+ recover(requestId, 'Full screen did not open. Try again.');
76
+ }, expandedRequestRecoveryMs);
74
77
 
75
78
  void requestDevvitExpandedMode(event, 'game')
76
- .then(() => {
77
- setBusy(false, '');
78
- })
79
79
  .catch((error: unknown) => {
80
80
  console.error('[devvit] expanded mode request failed.', error);
81
- setBusy(false, 'Expanded mode is unavailable. Try again.');
81
+ recover(requestId, 'Full screen is unavailable. Try again.');
82
82
  });
83
83
  });
84
+ const recoverAfterReturn = () => {
85
+ if (activeRequestId !== undefined) {
86
+ recover(activeRequestId, '');
87
+ }
88
+ };
89
+ globalThis.addEventListener('focus', recoverAfterReturn);
90
+ globalThis.addEventListener('pageshow', recoverAfterReturn);
84
91
 
85
- actions.append(playInlineButton, expandButton);
92
+ actions.append(expandButton);
86
93
  launchScreen.append(eyebrow, title, description, actions, status);
87
94
  const body = requireDocumentBody();
88
95
 
89
- body.classList.remove('devvit-inline-mode-gameplay');
90
96
  body.classList.add('devvit-inline-mode-host');
91
97
  delete body.dataset.mpgdPreserveBrowserTouchGestures;
92
98
  body.replaceChildren(launchScreen);
93
-
94
- function setBusy(busy: boolean, message: string): void {
95
- playInlineButton.disabled = busy;
96
- expandButton.disabled = busy;
97
- status.textContent = message;
98
- }
99
- }
100
-
101
- function mountGameplayDocument(): HTMLElement {
102
- const body = requireDocumentBody();
103
- const app = document.createElement('main');
104
- const game = document.createElement('div');
105
- const loading = document.createElement('p');
106
-
107
- app.id = 'app';
108
- game.id = 'game';
109
- loading.className = 'devvit-inline-gameplay-loading';
110
- loading.setAttribute('aria-live', 'polite');
111
- loading.setAttribute('role', 'status');
112
- loading.textContent = 'Loading gameplay…';
113
- game.append(loading);
114
- app.append(game);
115
-
116
- body.classList.remove('devvit-inline-mode-host');
117
- body.classList.add('devvit-inline-mode-gameplay');
118
- body.dataset.mpgdPreserveBrowserTouchGestures = 'true';
119
- body.replaceChildren(app);
120
-
121
- return loading;
122
99
  }
123
100
 
124
101
  function requireDocumentBody(): HTMLElement {
@@ -1,35 +1,15 @@
1
- body.devvit-inline-mode-host {
2
- min-width: 320px;
3
- min-height: 100vh;
1
+ html,
2
+ body {
3
+ width: 100%;
4
+ height: 100%;
4
5
  margin: 0;
5
- background: #0f172a;
6
- }
7
-
8
- body.devvit-inline-mode-gameplay {
9
- overflow-y: auto;
10
- overscroll-behavior-y: auto;
11
- }
12
-
13
- body.devvit-inline-mode-gameplay #game {
14
- position: relative;
15
- }
16
-
17
- body.devvit-inline-mode-gameplay #game,
18
- body.devvit-inline-mode-gameplay canvas {
19
- touch-action: pan-y !important;
6
+ overflow: hidden;
7
+ overscroll-behavior: auto;
8
+ touch-action: pan-y;
20
9
  }
21
10
 
22
- .devvit-inline-gameplay-loading {
23
- position: fixed;
24
- z-index: 1;
25
- inset: 0;
26
- display: grid;
27
- place-items: center;
28
- margin: 0;
11
+ body.devvit-inline-mode-host {
29
12
  background: #0f172a;
30
- color: #cbd5e1;
31
- font: 700 0.875rem/1.5 Inter, ui-sans-serif, system-ui, sans-serif;
32
- pointer-events: none;
33
13
  }
34
14
 
35
15
  .devvit-launch-screen,
@@ -38,10 +18,14 @@ body.devvit-inline-mode-gameplay canvas {
38
18
  }
39
19
 
40
20
  .devvit-launch-screen {
41
- min-height: 100vh;
21
+ position: fixed;
22
+ inset: 0;
23
+ width: 100%;
24
+ height: 100dvh;
25
+ overflow: hidden;
42
26
  display: grid;
43
- place-content: center;
44
- justify-items: start;
27
+ place-content: safe center;
28
+ justify-items: center;
45
29
  gap: 0.75rem;
46
30
  padding: clamp(1.25rem, 5vw, 3rem);
47
31
  background:
@@ -49,6 +33,8 @@ body.devvit-inline-mode-gameplay canvas {
49
33
  linear-gradient(145deg, #0f172a, #172554);
50
34
  color: #f8fafc;
51
35
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
36
+ text-align: center;
37
+ touch-action: pan-y;
52
38
  }
53
39
 
54
40
  .devvit-launch-screen__eyebrow,
@@ -67,9 +53,16 @@ body.devvit-inline-mode-gameplay canvas {
67
53
  }
68
54
 
69
55
  .devvit-launch-screen h1 {
56
+ display: -webkit-box;
70
57
  max-width: 18ch;
58
+ max-height: 1.9em;
59
+ overflow: hidden;
71
60
  font-size: clamp(2rem, 9vw, 4.5rem);
72
61
  line-height: 0.95;
62
+ overflow-wrap: anywhere;
63
+ -webkit-box-orient: vertical;
64
+ -webkit-line-clamp: 2;
65
+ line-clamp: 2;
73
66
  }
74
67
 
75
68
  .devvit-launch-screen__description {
@@ -79,9 +72,8 @@ body.devvit-inline-mode-gameplay canvas {
79
72
  }
80
73
 
81
74
  .devvit-launch-screen__actions {
82
- display: flex;
83
- flex-wrap: wrap;
84
- gap: 0.75rem;
75
+ width: min(100%, 22rem);
76
+ display: grid;
85
77
  margin-top: 0.5rem;
86
78
  }
87
79
 
@@ -97,12 +89,6 @@ body.devvit-inline-mode-gameplay canvas {
97
89
  cursor: pointer;
98
90
  }
99
91
 
100
- .devvit-launch-screen__button--secondary {
101
- border-color: #7dd3fc;
102
- color: #e0f2fe;
103
- background: rgb(15 23 42 / 0.64);
104
- }
105
-
106
92
  .devvit-launch-screen__button:disabled {
107
93
  cursor: wait;
108
94
  opacity: 0.65;
@@ -131,3 +117,23 @@ body.devvit-inline-mode-gameplay canvas {
131
117
  width: 100%;
132
118
  }
133
119
  }
120
+
121
+ @media (max-height: 440px) {
122
+ .devvit-launch-screen {
123
+ gap: 0.45rem;
124
+ padding-block: 0.5rem;
125
+ }
126
+
127
+ .devvit-launch-screen h1 {
128
+ font-size: clamp(1.75rem, 8vw, 2.5rem);
129
+ }
130
+
131
+ .devvit-launch-screen__description {
132
+ font-size: 0.875rem;
133
+ line-height: 1.3;
134
+ }
135
+
136
+ .devvit-launch-screen__actions {
137
+ margin-top: 0.2rem;
138
+ }
139
+ }
@@ -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 { PluginOption, UserConfig } from 'vite';
5
+ import type { PluginOption } from 'vite';
6
6
 
7
7
  import {
8
8
  assertRuntimeTargetConfigMatrix,
@@ -29,9 +29,19 @@ export interface CreateGameViteSharedConfigInput {
29
29
  readonly adPlacementsFile?: string;
30
30
  }
31
31
 
32
- export function createGameViteSharedConfig(
32
+ /** Vite-major-neutral subset shared by every generated target wrapper. */
33
+ export interface GameViteSharedConfig<SharedPluginOption> {
34
+ readonly base: string;
35
+ readonly plugins: SharedPluginOption[];
36
+ readonly resolve: {
37
+ readonly alias: Record<string, string>;
38
+ };
39
+ readonly define: Record<string, string>;
40
+ }
41
+
42
+ export function createGameViteSharedConfig<SharedPluginOption = PluginOption>(
33
43
  input: CreateGameViteSharedConfigInput,
34
- ): UserConfig {
44
+ ): GameViteSharedConfig<SharedPluginOption> {
35
45
  const isProduction = input.mode === 'production';
36
46
  const appTarget = input.appTarget ?? process.env.APP_TARGET ?? 'browser';
37
47
  const configTarget = input.configTarget ?? process.env.MPGD_CONFIG_TARGET ?? '';
@@ -56,7 +66,7 @@ export function createGameViteSharedConfig(
56
66
  ttsc({
57
67
  project: input.project,
58
68
  plugins: false,
59
- }) as unknown as PluginOption,
69
+ }) as unknown as SharedPluginOption,
60
70
  ],
61
71
  resolve: {
62
72
  alias: {