@mpgd/cli 0.8.0 → 0.9.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 +5 -5
- package/templates/phaser-game/apps/target-devvit/README.md +2 -3
- package/templates/phaser-game/apps/target-devvit/package.json +3 -3
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +1 -129
- package/templates/phaser-game/mpgd.targets.json +0 -1
- package/templates/phaser-game/vite.shared.ts +6 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
"typescript": "7.0.2",
|
|
52
52
|
"@mpgd/adapter-ait": "0.4.2",
|
|
53
53
|
"@mpgd/adapter-browser": "0.4.2",
|
|
54
|
-
"@mpgd/adapter-capacitor": "0.4.2",
|
|
55
|
-
"@mpgd/adapter-devvit": "0.6.0",
|
|
56
54
|
"@mpgd/analytics": "0.3.5",
|
|
55
|
+
"@mpgd/adapter-capacitor": "0.4.2",
|
|
56
|
+
"@mpgd/adapter-devvit": "0.7.0",
|
|
57
57
|
"@mpgd/bridge": "0.6.0",
|
|
58
|
-
"@mpgd/game-services": "0.6.0",
|
|
59
58
|
"@mpgd/catalog": "0.3.5",
|
|
60
|
-
"@mpgd/
|
|
59
|
+
"@mpgd/game-services": "0.7.0",
|
|
61
60
|
"@mpgd/phaser-assets": "0.4.1",
|
|
61
|
+
"@mpgd/i18n": "0.5.1",
|
|
62
62
|
"@mpgd/platform": "0.5.1",
|
|
63
63
|
"@mpgd/target-config": "0.6.1"
|
|
64
64
|
},
|
|
@@ -38,9 +38,8 @@ not required. Devvit-owned menu, scheduler, trigger, and form callbacks remain
|
|
|
38
38
|
thin `/internal/...` routes and delegate to shared service functions. oRPC
|
|
39
39
|
Publisher helpers can broadcast results after a task completes, but they do not
|
|
40
40
|
replace the `devvit.json` scheduler endpoint and should not use process-local
|
|
41
|
-
memory when delivery must cross instances.
|
|
42
|
-
|
|
43
|
-
`endpoint` option; new traffic uses oRPC by default.
|
|
41
|
+
memory when delivery must cross instances. The generated target exposes only
|
|
42
|
+
the oRPC bridge route; the former JSON compatibility route is not generated.
|
|
44
43
|
|
|
45
44
|
The default post entry uses `index.html` for a lightweight inline preview. Its
|
|
46
45
|
Play button requests the `game` entrypoint, which loads the separate
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
"fix": "ttsc fix"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@devvit/web": "0.13.
|
|
22
|
+
"@devvit/web": "0.13.8",
|
|
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.
|
|
27
|
+
"@devvit/start": "0.13.8",
|
|
28
28
|
"@ttsc/unplugin": "0.18.4",
|
|
29
29
|
"@types/node": "^24.0.0",
|
|
30
|
-
"devvit": "0.13.
|
|
30
|
+
"devvit": "0.13.8",
|
|
31
31
|
"ttsc": "0.18.4",
|
|
32
32
|
"typescript": "7.0.2",
|
|
33
33
|
"vite": "^8.1.3"
|
|
@@ -3,7 +3,6 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
3
3
|
import { createServer, context, getServerPort, reddit, redis } from '@devvit/web/server';
|
|
4
4
|
import type { UiResponse } from '@devvit/web/shared';
|
|
5
5
|
import {
|
|
6
|
-
assertBridgeRequest,
|
|
7
6
|
createBridgeError,
|
|
8
7
|
type BridgeRequest,
|
|
9
8
|
type BridgeResponse,
|
|
@@ -17,8 +16,7 @@ import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
|
|
|
17
16
|
const redisKeyComponentPattern = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
18
17
|
const maxStorageKeyLength = 128;
|
|
19
18
|
const maxEncodedStorageKeyLength = 384;
|
|
20
|
-
const maxRequestBodySize =
|
|
21
|
-
const legacyBridgeEndpoint = '/api/mpgd/bridge';
|
|
19
|
+
const maxRequestBodySize = 1_048_576;
|
|
22
20
|
const leaderboardUpdateMaxAttempts = 3;
|
|
23
21
|
const leaderboardBackoffBaseMs = 25;
|
|
24
22
|
const leaderboardLockTtlMs = 2_000;
|
|
@@ -59,11 +57,6 @@ async function handleHttpRequest(
|
|
|
59
57
|
return;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
if (request.method === 'POST' && requestPathname(request) === legacyBridgeEndpoint) {
|
|
63
|
-
await handleLegacyBridgeRequest(request, response);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
60
|
if (await bridgeRpcHandler(request, response)) {
|
|
68
61
|
return;
|
|
69
62
|
}
|
|
@@ -73,73 +66,6 @@ async function handleHttpRequest(
|
|
|
73
66
|
});
|
|
74
67
|
}
|
|
75
68
|
|
|
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
|
-
|
|
126
|
-
try {
|
|
127
|
-
sendJson(response, 200, await handleBridgeRequest(bridgeRequest));
|
|
128
|
-
} catch (error) {
|
|
129
|
-
console.error(`devvit legacy bridge request failed: ${errorMessage(error)}`, error);
|
|
130
|
-
sendJson(
|
|
131
|
-
response,
|
|
132
|
-
500,
|
|
133
|
-
createBridgeError(
|
|
134
|
-
bridgeRequest.id,
|
|
135
|
-
'DEVVIT_BRIDGE_INTERNAL_ERROR',
|
|
136
|
-
'Devvit bridge request failed.',
|
|
137
|
-
true,
|
|
138
|
-
),
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
69
|
async function handleCreatePostMenu(response: ServerResponse): Promise<void> {
|
|
144
70
|
const subredditName = currentSubredditName();
|
|
145
71
|
|
|
@@ -690,38 +616,6 @@ function requestPathname(request: IncomingMessage): string {
|
|
|
690
616
|
|
|
691
617
|
class RequestBodyTooLargeError extends Error {}
|
|
692
618
|
|
|
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
|
-
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
status: 500,
|
|
719
|
-
code: 'DEVVIT_BRIDGE_INTERNAL_ERROR',
|
|
720
|
-
message: 'Bridge request body could not be read.',
|
|
721
|
-
retryable: true,
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
619
|
async function* readRequestBodyChunks(
|
|
726
620
|
request: IncomingMessage,
|
|
727
621
|
maxBodySize: number,
|
|
@@ -751,19 +645,6 @@ async function drainRequestBody(
|
|
|
751
645
|
}
|
|
752
646
|
}
|
|
753
647
|
|
|
754
|
-
async function readJsonRequestBody(
|
|
755
|
-
request: IncomingMessage,
|
|
756
|
-
maxBodySize: number,
|
|
757
|
-
): Promise<unknown> {
|
|
758
|
-
const chunks: Buffer[] = [];
|
|
759
|
-
|
|
760
|
-
for await (const chunk of readRequestBodyChunks(request, maxBodySize)) {
|
|
761
|
-
chunks.push(chunk);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
648
|
function assertContentLengthWithinLimit(
|
|
768
649
|
request: IncomingMessage,
|
|
769
650
|
maxBodySize: number,
|
|
@@ -787,15 +668,6 @@ function discardOversizedRequestBody(
|
|
|
787
668
|
request.resume();
|
|
788
669
|
}
|
|
789
670
|
|
|
790
|
-
function requestIdFromBody(input: unknown): string {
|
|
791
|
-
if (typeof input !== 'object' || input === null || !('id' in input)) {
|
|
792
|
-
return 'unknown';
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const id = (input as { readonly id?: unknown }).id;
|
|
796
|
-
return typeof id === 'string' && id.length > 0 ? id : 'unknown';
|
|
797
|
-
}
|
|
798
|
-
|
|
799
671
|
function setResponseSecurityHeaders(response: ServerResponse): void {
|
|
800
672
|
response.setHeader('cache-control', 'no-store');
|
|
801
673
|
response.setHeader('content-security-policy', "default-src 'none'; frame-ancestors 'none'");
|
|
@@ -51,12 +51,8 @@ export function createGameViteSharedConfig(
|
|
|
51
51
|
alias: {
|
|
52
52
|
...createCatalogAliases({
|
|
53
53
|
gameRoot: input.gameRoot,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: { productCatalogFile: input.productCatalogFile }),
|
|
57
|
-
...(input.adPlacementsFile === undefined
|
|
58
|
-
? {}
|
|
59
|
-
: { adPlacementsFile: input.adPlacementsFile }),
|
|
54
|
+
productCatalogFile: input.productCatalogFile,
|
|
55
|
+
adPlacementsFile: input.adPlacementsFile,
|
|
60
56
|
}),
|
|
61
57
|
'#mpgd-platform-gateway': resolve(input.gameRoot, buildGatewayModule),
|
|
62
58
|
},
|
|
@@ -99,8 +95,8 @@ export function resolveBuildGatewayModule(input: {
|
|
|
99
95
|
|
|
100
96
|
function createCatalogAliases(input: {
|
|
101
97
|
readonly gameRoot: string;
|
|
102
|
-
readonly productCatalogFile
|
|
103
|
-
readonly adPlacementsFile
|
|
98
|
+
readonly productCatalogFile: string | undefined;
|
|
99
|
+
readonly adPlacementsFile: string | undefined;
|
|
104
100
|
}): Record<string, string> {
|
|
105
101
|
const productCatalogFile = readConfiguredPath(
|
|
106
102
|
input.productCatalogFile ?? process.env.MPGD_PRODUCT_CATALOG_FILE,
|
|
@@ -209,9 +205,8 @@ function resolveCatalogBaseDir(path: string, pairedPath: string, gameRoot: strin
|
|
|
209
205
|
}
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
|
|
208
|
+
throw new Error(
|
|
213
209
|
`Could not locate catalog files (${path}, ${pairedPath}) in any expected directory; `
|
|
214
|
-
|
|
210
|
+
+ `checked: ${candidates.filter((candidate) => candidate !== undefined).join(', ')}.`,
|
|
215
211
|
);
|
|
216
|
-
return gameRoot;
|
|
217
212
|
}
|