@mpgd/cli 0.6.0 → 0.8.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.
Files changed (51) hide show
  1. package/dist/game-acceptance.d.ts +65 -0
  2. package/dist/game-acceptance.js +245 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +398 -32
  5. package/dist/production-target-readiness.d.ts +8 -0
  6. package/dist/production-target-readiness.js +207 -0
  7. package/package.json +15 -15
  8. package/templates/phaser-game/README.md +71 -1
  9. package/templates/phaser-game/agent/acceptance.md +12 -6
  10. package/templates/phaser-game/agent/brief.md +3 -1
  11. package/templates/phaser-game/agent/game-manifest.json +7 -0
  12. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +3 -3
  13. package/templates/phaser-game/apps/target-devvit/README.md +70 -4
  14. package/templates/phaser-game/apps/target-devvit/devvit.json +8 -1
  15. package/templates/phaser-game/apps/target-devvit/package.json +15 -20
  16. package/templates/phaser-game/apps/target-devvit/src/client/game.html +13 -0
  17. package/templates/phaser-game/apps/target-devvit/src/client/game.ts +3 -0
  18. package/templates/phaser-game/apps/target-devvit/src/client/index.html +13 -0
  19. package/templates/phaser-game/apps/target-devvit/src/client/preview.ts +3 -0
  20. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +228 -116
  21. package/templates/phaser-game/apps/target-devvit/src/server/postOperationStore.ts +9 -0
  22. package/templates/phaser-game/apps/target-devvit/tsconfig.json +7 -2
  23. package/templates/phaser-game/apps/target-devvit/vite.config.ts +41 -0
  24. package/templates/phaser-game/game.html +13 -0
  25. package/templates/phaser-game/gitignore +2 -0
  26. package/templates/phaser-game/index.html +2 -2
  27. package/templates/phaser-game/mpgd.game.json +8 -0
  28. package/templates/phaser-game/mpgd.targets.json +7 -0
  29. package/templates/phaser-game/package.json +10 -4
  30. package/templates/phaser-game/public/manifest.webmanifest +1 -0
  31. package/templates/phaser-game/src/entry.ts +7 -0
  32. package/templates/phaser-game/src/env.d.ts +3 -0
  33. package/templates/phaser-game/src/gameEntry.ts +3 -0
  34. package/templates/phaser-game/src/main.ts +11 -2
  35. package/templates/phaser-game/src/platform/buildGatewayModule.ts +5 -0
  36. package/templates/phaser-game/src/platform/buildGateways/ait.ts +11 -0
  37. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +12 -0
  38. package/templates/phaser-game/src/platform/buildGateways/browser.ts +8 -0
  39. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +12 -0
  40. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +12 -0
  41. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +11 -0
  42. package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +15 -0
  43. package/templates/phaser-game/src/platform/devvitEntrypoint.ts +57 -0
  44. package/templates/phaser-game/src/platform/devvitInlinePreview.css +75 -0
  45. package/templates/phaser-game/src/platform/gameServices.ts +18 -61
  46. package/templates/phaser-game/src/platform/installPlatform.ts +6 -50
  47. package/templates/phaser-game/src/platform/microsoftStorePwa.ts +99 -0
  48. package/templates/phaser-game/tools/run-game-acceptance.mjs +22 -0
  49. package/templates/phaser-game/vite.config.ts +24 -73
  50. package/templates/phaser-game/vite.shared.ts +217 -0
  51. package/templates/phaser-game/apps/target-devvit/vite.server.config.ts +0 -37
@@ -1,3 +1,5 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+
1
3
  import { createServer, context, getServerPort, reddit, redis } from '@devvit/web/server';
2
4
  import type { UiResponse } from '@devvit/web/shared';
3
5
  import {
@@ -7,17 +9,16 @@ import {
7
9
  type BridgeResponse,
8
10
  } from '@mpgd/bridge';
9
11
  import {
10
- createBridgeRpcFetchHandler,
11
12
  createBridgeRpcRouter,
12
13
  defaultBridgeRpcEndpoint,
13
14
  } from '@mpgd/bridge/orpc';
14
- import express, { type Request as ExpressRequest, type Response as ExpressResponse } from 'express';
15
- import helmet from 'helmet';
15
+ import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
16
16
 
17
- const app = express();
18
17
  const redisKeyComponentPattern = /^[A-Za-z0-9:_-]{1,128}$/;
19
18
  const maxStorageKeyLength = 128;
20
19
  const maxEncodedStorageKeyLength = 384;
20
+ const maxRequestBodySize = 1_000_000;
21
+ const legacyBridgeEndpoint = '/api/mpgd/bridge';
21
22
  const leaderboardUpdateMaxAttempts = 3;
22
23
  const leaderboardBackoffBaseMs = 25;
23
24
  const leaderboardLockTtlMs = 2_000;
@@ -25,51 +26,125 @@ const leaderboardLockTtlSeconds = Math.ceil(leaderboardLockTtlMs / 1_000);
25
26
  const leaderboardLockRetryBudgetMs = leaderboardLockTtlSeconds * 1_000;
26
27
  const gameName = '__GAME_NAME__';
27
28
  const gameTitle = __GAME_TITLE_TS_LITERAL__;
28
- const expressManagedResponseHeaders = new Set([
29
- 'connection',
30
- 'content-encoding',
31
- 'content-length',
32
- 'transfer-encoding',
33
- ]);
34
-
35
- app.disable('x-powered-by');
36
- app.use(helmet());
37
-
38
- const bridgeRpcFetchHandler = createBridgeRpcFetchHandler(
29
+ const bridgeRpcHandler = createBridgeRpcNodeHandler(
39
30
  createBridgeRpcRouter(handleBridgeRequest),
31
+ {
32
+ maxBodySize: maxRequestBodySize,
33
+ prefix: defaultBridgeRpcEndpoint,
34
+ },
40
35
  );
41
36
 
42
- app.use(defaultBridgeRpcEndpoint, express.raw({ type: '*/*', limit: '1mb' }), async (
43
- request: ExpressRequest,
44
- response: ExpressResponse,
45
- ): Promise<void> => {
37
+ async function handleHttpRequest(
38
+ request: IncomingMessage,
39
+ response: ServerResponse,
40
+ ): Promise<void> {
41
+ setResponseSecurityHeaders(response);
42
+
43
+ if (request.method === 'POST' && requestPathname(request) === '/internal/menu/create-post') {
44
+ try {
45
+ await drainRequestBody(request, maxRequestBodySize);
46
+ } catch (error) {
47
+ if (!(error instanceof RequestBodyTooLargeError)) {
48
+ throw error;
49
+ }
50
+
51
+ discardOversizedRequestBody(request, response);
52
+ sendJson(response, 413, {
53
+ error: 'REQUEST_BODY_TOO_LARGE',
54
+ });
55
+ return;
56
+ }
57
+
58
+ await handleCreatePostMenu(response);
59
+ return;
60
+ }
61
+
62
+ if (request.method === 'POST' && requestPathname(request) === legacyBridgeEndpoint) {
63
+ await handleLegacyBridgeRequest(request, response);
64
+ return;
65
+ }
66
+
67
+ if (await bridgeRpcHandler(request, response)) {
68
+ return;
69
+ }
70
+
71
+ sendJson(response, 404, {
72
+ error: 'NOT_FOUND',
73
+ });
74
+ }
75
+
76
+ async function handleLegacyBridgeRequest(
77
+ request: IncomingMessage,
78
+ response: ServerResponse,
79
+ ): Promise<void> {
80
+ let body: unknown;
81
+
82
+ try {
83
+ body = await readJsonRequestBody(request, maxRequestBodySize);
84
+ } catch (error) {
85
+ const responseError = describeLegacyBridgeBodyError(error);
86
+
87
+ if (error instanceof RequestBodyTooLargeError) {
88
+ discardOversizedRequestBody(request, response);
89
+ }
90
+
91
+ if (responseError.retryable) {
92
+ console.error(`devvit legacy bridge body read failed: ${errorMessage(error)}`, error);
93
+ }
94
+
95
+ sendJson(
96
+ response,
97
+ responseError.status,
98
+ createBridgeError(
99
+ 'unknown',
100
+ responseError.code,
101
+ responseError.message,
102
+ responseError.retryable,
103
+ ),
104
+ );
105
+ return;
106
+ }
107
+
108
+ let bridgeRequest: BridgeRequest;
109
+
110
+ try {
111
+ bridgeRequest = assertBridgeRequest(body);
112
+ } catch (error) {
113
+ console.warn(`devvit legacy bridge validation failed: ${errorMessage(error)}`);
114
+ sendJson(
115
+ response,
116
+ 400,
117
+ createBridgeError(
118
+ requestIdFromBody(body),
119
+ 'INVALID_BRIDGE_REQUEST',
120
+ 'Invalid bridge request.',
121
+ ),
122
+ );
123
+ return;
124
+ }
125
+
46
126
  try {
47
- const fetchResponse = await bridgeRpcFetchHandler(expressRequestToFetchRequest(request));
48
- await sendFetchResponse(response, fetchResponse);
127
+ sendJson(response, 200, await handleBridgeRequest(bridgeRequest));
49
128
  } catch (error) {
50
- const message = error instanceof Error ? error.message : 'Devvit oRPC request failed.';
51
- console.error(`devvit oRPC internal error: ${message}`, error);
52
- response.status(500).json(
129
+ console.error(`devvit legacy bridge request failed: ${errorMessage(error)}`, error);
130
+ sendJson(
131
+ response,
132
+ 500,
53
133
  createBridgeError(
54
- requestIdFromBody(request.body),
134
+ bridgeRequest.id,
55
135
  'DEVVIT_BRIDGE_INTERNAL_ERROR',
56
136
  'Devvit bridge request failed.',
57
137
  true,
58
138
  ),
59
139
  );
60
140
  }
61
- });
62
-
63
- app.use(express.json({ limit: '1mb' }));
141
+ }
64
142
 
65
- app.post('/internal/menu/create-post', async (
66
- _request: ExpressRequest,
67
- response: ExpressResponse,
68
- ): Promise<void> => {
143
+ async function handleCreatePostMenu(response: ServerResponse): Promise<void> {
69
144
  const subredditName = currentSubredditName();
70
145
 
71
146
  if (subredditName === undefined) {
72
- response.status(200).json({
147
+ sendJson(response, 200, {
73
148
  showToast: {
74
149
  text: 'Could not resolve the target subreddit for this menu action.',
75
150
  appearance: 'neutral',
@@ -92,7 +167,7 @@ app.post('/internal/menu/create-post', async (
92
167
  },
93
168
  });
94
169
 
95
- response.status(200).json({
170
+ sendJson(response, 200, {
96
171
  showToast: {
97
172
  text: `Created ${gameTitle} post ${post.id}.`,
98
173
  appearance: 'success',
@@ -100,53 +175,30 @@ app.post('/internal/menu/create-post', async (
100
175
  } satisfies UiResponse);
101
176
  } catch (error) {
102
177
  console.error(`devvit custom post creation failed: ${errorMessage(error)}`, error);
103
- response.status(200).json({
178
+ sendJson(response, 200, {
104
179
  showToast: {
105
180
  text: `Could not create the ${gameTitle} post.`,
106
181
  appearance: 'neutral',
107
182
  },
108
183
  } satisfies UiResponse);
109
184
  }
110
- });
111
-
112
- app.post('/api/mpgd/bridge', async (
113
- request: ExpressRequest,
114
- response: ExpressResponse,
115
- ): Promise<void> => {
116
- let bridgeRequest: BridgeRequest;
185
+ }
117
186
 
118
- try {
119
- bridgeRequest = assertBridgeRequest(request.body);
120
- } catch (error) {
121
- response.status(400).json(
122
- createBridgeError(
123
- requestIdFromBody(request.body),
124
- 'INVALID_BRIDGE_REQUEST',
125
- error instanceof Error ? error.message : 'Invalid bridge request.',
126
- ),
127
- );
128
- return;
129
- }
187
+ const server = createServer((request, response) => {
188
+ void handleHttpRequest(request, response).catch((error: unknown) => {
189
+ console.error(`devvit server request failed: ${errorMessage(error)}`, error);
130
190
 
131
- try {
132
- const bridgeResponse = await handleBridgeRequest(bridgeRequest);
133
- response.status(200).json(bridgeResponse);
134
- } catch (error) {
135
- const message = error instanceof Error ? error.message : 'Devvit bridge request failed.';
136
- console.error(`devvit bridge internal error: ${message}`, error);
191
+ if (response.headersSent) {
192
+ response.end();
193
+ return;
194
+ }
137
195
 
138
- response.status(500).json(
139
- createBridgeError(
140
- bridgeRequest.id,
141
- 'DEVVIT_BRIDGE_INTERNAL_ERROR',
142
- 'Devvit bridge request failed.',
143
- true,
144
- ),
145
- );
146
- }
196
+ sendJson(response, 500, {
197
+ error: 'DEVVIT_SERVER_INTERNAL_ERROR',
198
+ });
199
+ });
147
200
  });
148
201
 
149
- const server = createServer(app);
150
202
  const port = getServerPort();
151
203
 
152
204
  server.on('error', (error) => {
@@ -387,15 +439,6 @@ async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
387
439
  });
388
440
  }
389
441
 
390
- function requestIdFromBody(input: unknown): string {
391
- if (typeof input !== 'object' || input === null) {
392
- return 'unknown';
393
- }
394
-
395
- const candidate = input as { readonly id?: unknown };
396
- return typeof candidate.id === 'string' ? candidate.id : 'unknown';
397
- }
398
-
399
442
  function ok(input: BridgeRequest, data: unknown): BridgeResponse {
400
443
  return {
401
444
  id: input.id,
@@ -641,61 +684,130 @@ function optionalObjectPayload(payload: unknown): Record<string, unknown> {
641
684
  return payload as Record<string, unknown>;
642
685
  }
643
686
 
644
- function expressRequestToFetchRequest(request: ExpressRequest): Request {
645
- const headers = new Headers();
687
+ function requestPathname(request: IncomingMessage): string {
688
+ return new URL(request.url ?? '/', 'http://localhost').pathname;
689
+ }
646
690
 
647
- for (const [key, value] of Object.entries(request.headers)) {
648
- if (typeof value === 'string') {
649
- headers.set(key, value);
650
- } else if (Array.isArray(value)) {
651
- headers.set(key, value.join(', '));
652
- }
691
+ class RequestBodyTooLargeError extends Error {}
692
+
693
+ function describeLegacyBridgeBodyError(error: unknown): {
694
+ readonly status: number;
695
+ readonly code: string;
696
+ readonly message: string;
697
+ readonly retryable: boolean;
698
+ } {
699
+ if (error instanceof RequestBodyTooLargeError) {
700
+ return {
701
+ status: 413,
702
+ code: 'BRIDGE_REQUEST_TOO_LARGE',
703
+ message: 'Bridge request body is too large.',
704
+ retryable: false,
705
+ };
706
+ }
707
+
708
+ if (error instanceof SyntaxError) {
709
+ return {
710
+ status: 400,
711
+ code: 'INVALID_BRIDGE_REQUEST',
712
+ message: 'Bridge request body is not valid JSON.',
713
+ retryable: false,
714
+ };
653
715
  }
654
716
 
655
- const init: RequestInit = {
656
- method: request.method,
657
- headers,
717
+ return {
718
+ status: 500,
719
+ code: 'DEVVIT_BRIDGE_INTERNAL_ERROR',
720
+ message: 'Bridge request body could not be read.',
721
+ retryable: true,
658
722
  };
723
+ }
724
+
725
+ async function* readRequestBodyChunks(
726
+ request: IncomingMessage,
727
+ maxBodySize: number,
728
+ ): AsyncGenerator<Buffer, void, undefined> {
729
+ assertContentLengthWithinLimit(request, maxBodySize);
730
+
731
+ let bodySize = 0;
659
732
 
660
- if (request.method !== 'GET' && request.method !== 'HEAD') {
661
- init.body = requestBodyToBodyInit(request.body);
733
+ for await (const chunk of request) {
734
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
735
+ bodySize += buffer.byteLength;
662
736
 
663
- if (!headers.has('content-type') && !(request.body instanceof Uint8Array)) {
664
- headers.set('content-type', 'application/json');
737
+ if (bodySize > maxBodySize) {
738
+ throw new RequestBodyTooLargeError();
665
739
  }
740
+
741
+ yield buffer;
742
+ }
743
+ }
744
+
745
+ async function drainRequestBody(
746
+ request: IncomingMessage,
747
+ maxBodySize: number,
748
+ ): Promise<void> {
749
+ for await (const _chunk of readRequestBodyChunks(request, maxBodySize)) {
750
+ // Drain the bounded request stream without retaining its contents.
666
751
  }
752
+ }
753
+
754
+ async function readJsonRequestBody(
755
+ request: IncomingMessage,
756
+ maxBodySize: number,
757
+ ): Promise<unknown> {
758
+ const chunks: Buffer[] = [];
667
759
 
668
- return new Request(expressRequestUrl(request), init);
760
+ for await (const chunk of readRequestBodyChunks(request, maxBodySize)) {
761
+ chunks.push(chunk);
762
+ }
763
+
764
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
669
765
  }
670
766
 
671
- function requestBodyToBodyInit(input: unknown): BodyInit {
672
- if (input instanceof Uint8Array) {
673
- const body = new Uint8Array(input.byteLength);
674
- body.set(input);
767
+ function assertContentLengthWithinLimit(
768
+ request: IncomingMessage,
769
+ maxBodySize: number,
770
+ ): void {
771
+ const contentLength = request.headers['content-length'];
675
772
 
676
- return body.buffer as ArrayBuffer;
773
+ if (
774
+ contentLength !== undefined
775
+ && Number.isFinite(Number(contentLength))
776
+ && Number(contentLength) > maxBodySize
777
+ ) {
778
+ throw new RequestBodyTooLargeError();
677
779
  }
780
+ }
678
781
 
679
- return JSON.stringify(input ?? null);
782
+ function discardOversizedRequestBody(
783
+ request: IncomingMessage,
784
+ response: ServerResponse,
785
+ ): void {
786
+ response.setHeader('connection', 'close');
787
+ request.resume();
680
788
  }
681
789
 
682
- function expressRequestUrl(request: ExpressRequest): string {
683
- const host = request.get('host') ?? 'localhost';
684
- const protocol = request.protocol || 'https';
790
+ function requestIdFromBody(input: unknown): string {
791
+ if (typeof input !== 'object' || input === null || !('id' in input)) {
792
+ return 'unknown';
793
+ }
685
794
 
686
- return `${protocol}://${host}${request.originalUrl}`;
795
+ const id = (input as { readonly id?: unknown }).id;
796
+ return typeof id === 'string' && id.length > 0 ? id : 'unknown';
687
797
  }
688
798
 
689
- async function sendFetchResponse(
690
- response: ExpressResponse,
691
- fetchResponse: Response,
692
- ): Promise<void> {
693
- fetchResponse.headers.forEach((value: string, key: string) => {
694
- if (!expressManagedResponseHeaders.has(key.toLowerCase())) {
695
- response.setHeader(key, value);
696
- }
697
- });
799
+ function setResponseSecurityHeaders(response: ServerResponse): void {
800
+ response.setHeader('cache-control', 'no-store');
801
+ response.setHeader('content-security-policy', "default-src 'none'; frame-ancestors 'none'");
802
+ response.setHeader('referrer-policy', 'no-referrer');
803
+ response.setHeader('x-content-type-options', 'nosniff');
804
+ response.setHeader('x-frame-options', 'DENY');
805
+ }
698
806
 
699
- response.status(fetchResponse.status);
700
- response.send(await fetchResponse.text());
807
+ function sendJson(response: ServerResponse, status: number, body: unknown): void {
808
+ const payload = JSON.stringify(body);
809
+ response.statusCode = status;
810
+ response.setHeader('content-length', Buffer.byteLength(payload));
811
+ response.setHeader('content-type', 'application/json; charset=utf-8');
812
+ response.end(payload);
701
813
  }
@@ -0,0 +1,9 @@
1
+ import { redis } from '@devvit/web/server';
2
+ import {
3
+ createDevvitRedisPostOperationStore,
4
+ type DevvitDurableOperationStore,
5
+ } from '@mpgd/adapter-devvit/server';
6
+
7
+ export function createPostOperationStore(): DevvitDurableOperationStore {
8
+ return createDevvitRedisPostOperationStore(redis);
9
+ }
@@ -2,8 +2,13 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "allowSyntheticDefaultImports": true,
5
- "types": ["node"],
5
+ "types": ["node", "vite/client"],
6
6
  "noEmit": true
7
7
  },
8
- "include": ["src/**/*.ts", "vite.server.config.ts"]
8
+ "include": [
9
+ "src/**/*.ts",
10
+ "vite.config.ts",
11
+ "../../src/**/*.ts",
12
+ "../../vite.shared.ts"
13
+ ]
9
14
  }
@@ -0,0 +1,41 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { devvit } from '@devvit/start/vite';
5
+ import { defineConfig } from 'vite';
6
+
7
+ import { createGameViteSharedConfig } from '../../vite.shared';
8
+
9
+ const wrapperRoot = fileURLToPath(new URL('.', import.meta.url));
10
+ const gameRoot = resolve(wrapperRoot, '../..');
11
+
12
+ export default defineConfig(({ mode }) => {
13
+ const shared = createGameViteSharedConfig({
14
+ appTarget: 'reddit',
15
+ configTarget: 'reddit',
16
+ gameRoot,
17
+ mode,
18
+ platformTargetsFile: resolve(gameRoot, 'mpgd.targets.json'),
19
+ project: resolve(wrapperRoot, 'tsconfig.json'),
20
+ });
21
+
22
+ return {
23
+ ...shared,
24
+ publicDir: resolve(gameRoot, 'public'),
25
+ plugins: [
26
+ ...(shared.plugins ?? []),
27
+ devvit({
28
+ client: {
29
+ build: {
30
+ sourcemap: mode !== 'production',
31
+ },
32
+ },
33
+ server: {
34
+ build: {
35
+ sourcemap: mode !== 'production',
36
+ },
37
+ },
38
+ }),
39
+ ],
40
+ };
41
+ });
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0f172a" />
7
+ <title>__GAME_TITLE__</title>
8
+ </head>
9
+ <body>
10
+ <div id="game"></div>
11
+ <script type="module" src="/src/gameEntry.ts"></script>
12
+ </body>
13
+ </html>
@@ -1,9 +1,11 @@
1
1
  node_modules/
2
2
  dist/
3
3
  apps/*/dist/
4
+ apps/target-devvit/generated/
4
5
  artifacts/
5
6
  release-output/
6
7
  .mpgd.targets.generated.json
8
+ .mpgd/
7
9
  .env
8
10
  .env.*
9
11
  apps/*/.env
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#0f172a" />
7
- <link rel="manifest" href="/manifest.webmanifest" />
7
+ <link rel="manifest" href="./manifest.webmanifest" />
8
8
  <title>__GAME_TITLE__</title>
9
9
  </head>
10
10
  <body>
11
11
  <div id="game"></div>
12
- <script type="module" src="/src/main.ts"></script>
12
+ <script type="module" src="/src/entry.ts"></script>
13
13
  </body>
14
14
  </html>
@@ -0,0 +1,8 @@
1
+ {
2
+ "brand": {
3
+ "appIcon": {
4
+ "source": "public/icon.svg",
5
+ "backgroundColor": "#0f172a"
6
+ }
7
+ }
8
+ }
@@ -4,12 +4,14 @@
4
4
  "kind": "web",
5
5
  "gameApp": ".",
6
6
  "adapter": "browser",
7
+ "icon": { "profile": "web-preview" },
7
8
  "output": "artifacts/web-preview"
8
9
  },
9
10
  "microsoft-store": {
10
11
  "kind": "web",
11
12
  "gameApp": ".",
12
13
  "adapter": "browser",
14
+ "icon": { "profile": "microsoft-pwa" },
13
15
  "output": "artifacts/microsoft-store"
14
16
  },
15
17
  "android": {
@@ -17,6 +19,7 @@
17
19
  "gameApp": ".",
18
20
  "shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
19
21
  "adapter": "capacitor",
22
+ "icon": { "profile": "android" },
20
23
  "webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
21
24
  "artifact": "aab"
22
25
  },
@@ -25,6 +28,7 @@
25
28
  "gameApp": ".",
26
29
  "shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
27
30
  "adapter": "capacitor",
31
+ "icon": { "profile": "ios" },
28
32
  "webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
29
33
  "artifact": "ipa"
30
34
  },
@@ -33,14 +37,17 @@
33
37
  "gameApp": ".",
34
38
  "wrapperApp": "${MPGD_KIT_PATH}/apps/target-ait",
35
39
  "adapter": "ait",
40
+ "icon": { "profile": "ait" },
36
41
  "webDir": "${MPGD_KIT_PATH}/apps/target-ait/public/game",
37
42
  "artifact": ".ait"
38
43
  },
39
44
  "reddit": {
40
45
  "kind": "devvit-web",
46
+ "buildStrategy": "devvit-vite",
41
47
  "gameApp": ".",
42
48
  "wrapperApp": "apps/target-devvit",
43
49
  "adapter": "devvit",
50
+ "icon": { "profile": "devvit" },
44
51
  "webDir": "apps/target-devvit/dist/client",
45
52
  "artifact": "devvit"
46
53
  }
@@ -8,7 +8,13 @@
8
8
  "dev": "__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0",
9
9
  "dev:ait": "APP_TARGET=ait APP_VERSION=0.0.0-dev BUILD_ID=ait-sandbox sh -c '__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0'",
10
10
  "build": "__WORKSPACE_I18N_BUILD_PREFIX__vite build",
11
- "build:ait": "pnpm exec mpgd target build ait production --variant wrapper --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
11
+ "accept": "node ./tools/run-game-acceptance.mjs",
12
+ "icons:generate": "pnpm exec mpgd game icons generate . --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
13
+ "icons:generate:devvit": "pnpm exec mpgd game icons generate . --targets reddit --profile development --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
14
+ "icons:generate:devvit:production": "pnpm exec mpgd game icons generate . --targets reddit --profile production --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
15
+ "icons:verify": "pnpm exec mpgd game icons verify . --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
16
+ "icons:inspect": "pnpm exec mpgd game icons inspect . --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
17
+ "build:ait": "pnpm exec mpgd target build ait staging --variant wrapper --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
12
18
  "build:ait:package": "pnpm exec mpgd target build ait production --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
13
19
  "smoke:ait": "pnpm exec mpgd target smoke ait --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
14
20
  "ait:wrapper:dev": "pnpm --dir ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}/apps/target-ait run dev",
@@ -67,10 +73,10 @@
67
73
  "@ait-co/console-cli": "^0.1.45",
68
74
  "@cloudflare/workers-types": "^5.20260707.1",
69
75
  "@mpgd/cli": "__MPGD_DEPENDENCY_VERSION_CLI__",
70
- "@ttsc/unplugin": "0.16.9",
76
+ "@ttsc/unplugin": "0.18.4",
71
77
  "@types/node": "^24.0.0",
72
- "ttsc": "0.16.9",
73
- "typescript": "7.0.1-rc",
78
+ "ttsc": "0.18.4",
79
+ "typescript": "7.0.2",
74
80
  "vite": "^8.1.3",
75
81
  "wrangler": "^4.107.0"
76
82
  }
@@ -3,6 +3,7 @@
3
3
  "name": "__GAME_TITLE__",
4
4
  "short_name": "__GAME_NAME__",
5
5
  "description": "__GAME_TITLE__ built with mpgd-kit.",
6
+ "id": "./__GAME_NAME__",
6
7
  "start_url": "./",
7
8
  "scope": "./",
8
9
  "display": "standalone",
@@ -0,0 +1,7 @@
1
+ if (__APP_TARGET__ === 'reddit') {
2
+ await import('./platform/devvitEntrypoint');
3
+ } else {
4
+ await import('./main');
5
+ }
6
+
7
+ export {};
@@ -2,6 +2,9 @@
2
2
 
3
3
  declare const __APP_TARGET__: string;
4
4
  declare const __MPGD_CONFIG_TARGET__: string;
5
+ declare const __MPGD_PLATFORM_TARGET__:
6
+ | import('@mpgd/target-config').EffectivePlatformTargetMetadata
7
+ | undefined;
5
8
  declare const __APP_VERSION__: string;
6
9
  declare const __BUILD_ID__: string;
7
10
  declare const __SOURCE_GIT_SHA__: string;
@@ -0,0 +1,3 @@
1
+ await import('./main');
2
+
3
+ export {};