@mpgd/cli 0.52.0 → 0.54.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/dist/capacitor-shell-starter.d.ts +22 -0
- package/dist/capacitor-shell-starter.js +395 -18
- package/dist/deploy-planning.js +2 -2
- package/dist/index.js +122 -34
- package/dist/kit-upgrade.js +2 -2
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +433 -2
- package/dist/notifications-starter.d.ts +75 -0
- package/dist/notifications-starter.js +522 -0
- package/dist/offline-playtest.js +1 -1
- package/dist/tooling-migration.js +1 -1
- package/package.json +16 -16
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +104 -0
- package/templates/notifications-overlay/common/notifications/deployment.json +8 -0
- package/templates/notifications-overlay/common/notifications/topics.json +222 -0
- package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +59 -0
- package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +157 -0
- package/templates/notifications-overlay/common/src/platform/notificationOpens.ts +60 -0
- package/templates/notifications-overlay/common/src/platform/notificationSettings.ts +761 -0
- package/templates/notifications-overlay/common/src/platform/notifications.ts +163 -0
- package/templates/notifications-overlay/common/tools/validate-notifications.ts +75 -0
- package/templates/notifications-overlay/lite/src/lite/notificationSettingsPanel.ts +200 -0
- package/templates/notifications-overlay/phaser/src/scenes/NotificationSettingsScene.ts +301 -0
- package/templates/phaser-game/apps/target-ait/src/main.ts +15 -0
- package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +5 -0
- package/templates/phaser-game/apps/target-ait/vite.config.ts +22 -1
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +14 -0
- package/templates/phaser-game/src/platform/gameServices.ts +3 -0
- package/templates/phaser-game/src/platform/serverSession.ts +66 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notifications starter (`mpgd game create --providers notifications`):
|
|
3
|
+
* the files `templates/notifications-overlay` adds to a generated game, and the
|
|
4
|
+
* anchored edits it makes to the base template, so a game without
|
|
5
|
+
* notifications ships none of it (notifications v2 RFC, PR C1).
|
|
6
|
+
*
|
|
7
|
+
* Every edit names an anchor that must occur exactly once in the base file,
|
|
8
|
+
* so a template change that moves an anchor fails the CLI's tests instead of
|
|
9
|
+
* generating a half-wired game.
|
|
10
|
+
*/
|
|
11
|
+
/** The provider id that selects the notifications starter. */
|
|
12
|
+
export const notificationsProviderId = 'notifications';
|
|
13
|
+
/** The push plugin the notifications starter adds (FCM on Android and iOS). */
|
|
14
|
+
export const notificationsMessagingPlugin = '@capacitor-firebase/messaging';
|
|
15
|
+
/**
|
|
16
|
+
* The npm packages a game with notifications declares. The two Capacitor
|
|
17
|
+
* plugins are the optional peers `@mpgd/adapter-capacitor/notifications`
|
|
18
|
+
* loads, at the versions the adapter is tested with; `firebase` is the
|
|
19
|
+
* package the messaging plugin's web fallback imports, which the bundler
|
|
20
|
+
* must resolve even though a native shell never loads it.
|
|
21
|
+
*/
|
|
22
|
+
export const notificationsGameDependencies = Object.freeze({
|
|
23
|
+
[notificationsMessagingPlugin]: '8.5.2',
|
|
24
|
+
'@capacitor/local-notifications': '8.3.1',
|
|
25
|
+
firebase: '12.19.0',
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* The `firebase` range the starter's `@capacitor-firebase/messaging` version
|
|
29
|
+
* declares as its peer (its web fallback imports the SDK). A game that keeps
|
|
30
|
+
* its own `firebase` must be in it before the starter adds the plugin; the
|
|
31
|
+
* starter test pins it to the plugin's published metadata.
|
|
32
|
+
*/
|
|
33
|
+
export const notificationsMessagingFirebaseRange = '^12.6.0';
|
|
34
|
+
/** The plugins `mpgd target init capacitor --providers notifications` installs into the shell. */
|
|
35
|
+
export const notificationsShellPlugins = Object.freeze([
|
|
36
|
+
notificationsMessagingPlugin,
|
|
37
|
+
'@capacitor/local-notifications',
|
|
38
|
+
]);
|
|
39
|
+
/** Never installed next to the messaging plugin: on iOS both claim Capacitor's one push handler. */
|
|
40
|
+
export const conflictingPushPlugin = '@capacitor/push-notifications';
|
|
41
|
+
const gatewayImport = {
|
|
42
|
+
anchor: "import type { RuntimeConfig } from '../runtimeDetector';\n",
|
|
43
|
+
replacement: [
|
|
44
|
+
"import { createStarterCapacitorNotifications } from '../capacitorNotifications';",
|
|
45
|
+
"import type { RuntimeConfig } from '../runtimeDetector';",
|
|
46
|
+
'',
|
|
47
|
+
].join('\n'),
|
|
48
|
+
};
|
|
49
|
+
const gatewayProviders = (platform) => ({
|
|
50
|
+
anchor: ' const providers = createStoreProviders(runtime, () => gateway);\n',
|
|
51
|
+
replacement: [
|
|
52
|
+
' const providers = [',
|
|
53
|
+
' ...createStoreProviders(runtime, () => gateway),',
|
|
54
|
+
' // Notifications v2 (FCM and local reminders); `notifications/topics.json`',
|
|
55
|
+
' // maps the topics, and the shell needs the two plugins',
|
|
56
|
+
' // (`mpgd target init capacitor --providers notifications`).',
|
|
57
|
+
` ...(await createStarterCapacitorNotifications('${platform}', runtime.configTarget)),`,
|
|
58
|
+
' ];',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
});
|
|
62
|
+
/** Edits to base template files, by path, for both templates unless noted. */
|
|
63
|
+
const commonEdits = {
|
|
64
|
+
'src/platform/buildGateways/capacitorAndroid.ts': [gatewayImport, gatewayProviders('android')],
|
|
65
|
+
'src/platform/buildGateways/capacitorIos.ts': [gatewayImport, gatewayProviders('ios')],
|
|
66
|
+
'src/platform/buildGateways/reddit.ts': [
|
|
67
|
+
{
|
|
68
|
+
anchor: "import { createDevvitWebPaymentsClient } from '@mpgd/adapter-devvit/web';\n",
|
|
69
|
+
replacement: [
|
|
70
|
+
'import {',
|
|
71
|
+
' createDevvitWebLoginPrompt,',
|
|
72
|
+
' createDevvitWebPaymentsClient,',
|
|
73
|
+
"} from '@mpgd/adapter-devvit/web';",
|
|
74
|
+
'',
|
|
75
|
+
].join('\n'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
anchor: "import type { RuntimeConfig } from '../runtimeDetector';\n",
|
|
79
|
+
replacement: [
|
|
80
|
+
"import { starterNotificationTopics } from '../notifications';",
|
|
81
|
+
"import type { RuntimeConfig } from '../runtimeDetector';",
|
|
82
|
+
'',
|
|
83
|
+
].join('\n'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
anchor: ' ...(commerce === undefined ? {} : { commerce }),\n',
|
|
87
|
+
replacement: [
|
|
88
|
+
' ...(commerce === undefined ? {} : { commerce }),',
|
|
89
|
+
' // Notifications v2: the game-rendered opt-in the server bridge serves',
|
|
90
|
+
' // (a gated beta: `integrations.notifications` stays approval-required',
|
|
91
|
+
' // until Reddit approves the app), and Reddit login for a logged-out',
|
|
92
|
+
' // player, who must log in before opting in.',
|
|
93
|
+
' ...notificationOptions(runtime),',
|
|
94
|
+
' loginPrompt: createDevvitWebLoginPrompt(),',
|
|
95
|
+
'',
|
|
96
|
+
].join('\n'),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
anchor: 'function createCommerceOptions(): DevvitGatewayCommerceOptions | undefined {\n',
|
|
100
|
+
replacement: [
|
|
101
|
+
'function notificationOptions(',
|
|
102
|
+
' runtime: RuntimeConfig,',
|
|
103
|
+
"): Pick<Parameters<typeof createDevvitPlatformGateway>[0], 'notifications'> {",
|
|
104
|
+
' const topics = starterNotificationTopics(runtime.configTarget).map((topic) => topic.id);',
|
|
105
|
+
' return topics.length === 0 ? {} : { notifications: { topics } };',
|
|
106
|
+
'}',
|
|
107
|
+
'',
|
|
108
|
+
'function createCommerceOptions(): DevvitGatewayCommerceOptions | undefined {',
|
|
109
|
+
'',
|
|
110
|
+
].join('\n'),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
'apps/target-devvit/src/server/index.ts': [
|
|
114
|
+
{
|
|
115
|
+
anchor: "import { context, createServer, getServerPort, payments, reddit, redis } from '@devvit/web/server';\n",
|
|
116
|
+
replacement: [
|
|
117
|
+
'import {',
|
|
118
|
+
' context,',
|
|
119
|
+
' createServer,',
|
|
120
|
+
' getServerPort,',
|
|
121
|
+
' notifications,',
|
|
122
|
+
' payments,',
|
|
123
|
+
' reddit,',
|
|
124
|
+
' redis,',
|
|
125
|
+
"} from '@devvit/web/server';",
|
|
126
|
+
'',
|
|
127
|
+
].join('\n'),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
anchor: [
|
|
131
|
+
'import {',
|
|
132
|
+
' createDevvitCommerceBridgeProducts,',
|
|
133
|
+
' createDevvitCommerceV2BridgeHandler,',
|
|
134
|
+
"} from '@mpgd/adapter-devvit/server';",
|
|
135
|
+
'',
|
|
136
|
+
].join('\n'),
|
|
137
|
+
replacement: [
|
|
138
|
+
'import {',
|
|
139
|
+
' createDevvitCommerceBridgeProducts,',
|
|
140
|
+
' createDevvitCommerceV2BridgeHandler,',
|
|
141
|
+
' createDevvitNotificationService,',
|
|
142
|
+
' createDevvitNotificationsV2BridgeHandler,',
|
|
143
|
+
"} from '@mpgd/adapter-devvit/server';",
|
|
144
|
+
'',
|
|
145
|
+
].join('\n'),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
anchor: "import type { ProductCatalog } from '@mpgd/catalog';\n",
|
|
149
|
+
replacement: [
|
|
150
|
+
"import { resolveNotificationTopicsForTarget, type NotificationTopicCatalog, type ProductCatalog } from '@mpgd/catalog';",
|
|
151
|
+
"import { assertNotificationDeployment } from '@mpgd/platform/notifications';",
|
|
152
|
+
'',
|
|
153
|
+
].join('\n'),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
anchor: "import devvitManifest from '../../devvit.json';\n",
|
|
157
|
+
replacement: [
|
|
158
|
+
"import notificationDeploymentJson from '../../../../notifications/deployment.json';",
|
|
159
|
+
"import notificationTopicsJson from '../../../../notifications/topics.json';",
|
|
160
|
+
"import devvitManifest from '../../devvit.json';",
|
|
161
|
+
'',
|
|
162
|
+
].join('\n'),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
anchor: [
|
|
166
|
+
'const handleBridgeRequest = createDevvitBridgeHandler({',
|
|
167
|
+
' redis,',
|
|
168
|
+
' currentPlayerId,',
|
|
169
|
+
' currentDisplayName,',
|
|
170
|
+
' storageKeyNamespace: gameName,',
|
|
171
|
+
' ...(commerce === undefined ? {} : { commerce }),',
|
|
172
|
+
'});',
|
|
173
|
+
'',
|
|
174
|
+
].join('\n'),
|
|
175
|
+
replacement: [
|
|
176
|
+
'/**',
|
|
177
|
+
' * Notifications v2 (`notifications/topics.json`, mapped for the `reddit`',
|
|
178
|
+
' * target): the game-rendered opt-in for the logged-in requester, and the',
|
|
179
|
+
' * service that records consent and sends server reminders over the Devvit',
|
|
180
|
+
' * notifications plugin. Devvit notifications are a gated beta: the game',
|
|
181
|
+
' * shows them only once `integrations.notifications` is `available`.',
|
|
182
|
+
' */',
|
|
183
|
+
'const notificationTopicCatalog = notificationTopicsJson as NotificationTopicCatalog;',
|
|
184
|
+
'const redditNotificationTopics = readRedditNotificationTopics();',
|
|
185
|
+
"const notificationService: Parameters<typeof handleNotificationTask>[0]['notifications'] =",
|
|
186
|
+
' redditNotificationTopics.length === 0 ? undefined : createNotificationService();',
|
|
187
|
+
'// The opt-in is served only with the service that sends; a refused catalog turns both off.',
|
|
188
|
+
'const notificationsBridge = notificationService === undefined',
|
|
189
|
+
' ? undefined',
|
|
190
|
+
' : createDevvitNotificationsV2BridgeHandler({',
|
|
191
|
+
' notifications,',
|
|
192
|
+
' currentUserId: currentPlayerId,',
|
|
193
|
+
' topics: redditNotificationTopics,',
|
|
194
|
+
' });',
|
|
195
|
+
'const handleBridgeRequest = createDevvitBridgeHandler({',
|
|
196
|
+
' redis,',
|
|
197
|
+
' currentPlayerId,',
|
|
198
|
+
' currentDisplayName,',
|
|
199
|
+
' storageKeyNamespace: gameName,',
|
|
200
|
+
' ...(commerce === undefined ? {} : { commerce }),',
|
|
201
|
+
' ...(notificationsBridge === undefined ? {} : { notifications: notificationsBridge }),',
|
|
202
|
+
'});',
|
|
203
|
+
'',
|
|
204
|
+
].join('\n'),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
anchor: [
|
|
208
|
+
'/**',
|
|
209
|
+
' * The notifications v2 service (`createDevvitNotificationService` from',
|
|
210
|
+
' * `@mpgd/adapter-devvit/server`). None until the game declares a topic',
|
|
211
|
+
' * catalog, so the scheduler tasks below do no work.',
|
|
212
|
+
' */',
|
|
213
|
+
"const notificationService: Parameters<typeof handleNotificationTask>[0]['notifications'] = undefined;",
|
|
214
|
+
'',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
replacement: [
|
|
217
|
+
'/** The topic ids the catalog maps for Reddit; none when the catalog cannot be read. */',
|
|
218
|
+
'function readRedditNotificationTopics(): readonly string[] {',
|
|
219
|
+
' try {',
|
|
220
|
+
" return resolveNotificationTopicsForTarget(notificationTopicCatalog, 'reddit').map((topic) => topic.id);",
|
|
221
|
+
' } catch (error) {',
|
|
222
|
+
' console.error(`devvit notifications disabled: ${errorMessage(error)}`);',
|
|
223
|
+
' return [];',
|
|
224
|
+
' }',
|
|
225
|
+
'}',
|
|
226
|
+
'',
|
|
227
|
+
'/**',
|
|
228
|
+
' * The notifications v2 service over Redis and the Devvit plugin; the',
|
|
229
|
+
' * scheduler tasks below run its reminders and purges. A catalog or',
|
|
230
|
+
' * deployment the service refuses turns notifications off, never the game',
|
|
231
|
+
' * server.',
|
|
232
|
+
' */',
|
|
233
|
+
"function createNotificationService(): Parameters<typeof handleNotificationTask>[0]['notifications'] {",
|
|
234
|
+
' try {',
|
|
235
|
+
' return createDevvitNotificationService({',
|
|
236
|
+
' redis,',
|
|
237
|
+
' notifications,',
|
|
238
|
+
' catalog: notificationTopicCatalog,',
|
|
239
|
+
' deployment: assertNotificationDeployment(notificationDeploymentJson),',
|
|
240
|
+
' }).service;',
|
|
241
|
+
' } catch (error) {',
|
|
242
|
+
' console.error(`devvit notifications disabled: ${errorMessage(error)}`);',
|
|
243
|
+
' return undefined;',
|
|
244
|
+
' }',
|
|
245
|
+
'}',
|
|
246
|
+
'',
|
|
247
|
+
].join('\n'),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
const phaserEdits = {
|
|
252
|
+
'src/runtime/createGame.ts': [
|
|
253
|
+
{
|
|
254
|
+
anchor: "import { LobbyScene } from '../scenes/LobbyScene';\n",
|
|
255
|
+
replacement: [
|
|
256
|
+
"import { LobbyScene } from '../scenes/LobbyScene';",
|
|
257
|
+
'import {',
|
|
258
|
+
' NotificationSettingsScene,',
|
|
259
|
+
' routeNotificationSettingsOpens,',
|
|
260
|
+
"} from '../scenes/NotificationSettingsScene';",
|
|
261
|
+
'',
|
|
262
|
+
].join('\n'),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
anchor: ' scene: [BootScene, LobbyScene, PlayScene],\n',
|
|
266
|
+
replacement: ' scene: [BootScene, LobbyScene, PlayScene, NotificationSettingsScene],\n',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
anchor: ' ownGameAudio(game, input.context);\n',
|
|
270
|
+
replacement: [
|
|
271
|
+
' ownGameAudio(game, input.context);',
|
|
272
|
+
' // The advertising opt-out action and consent notices open the settings screen.',
|
|
273
|
+
' routeNotificationSettingsOpens(game, input.context);',
|
|
274
|
+
'',
|
|
275
|
+
].join('\n'),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
'src/scenes/LobbyScene.ts': [
|
|
279
|
+
{
|
|
280
|
+
anchor: "import { starterContextKey, type StarterContext } from '../runtime/gameContext';\n",
|
|
281
|
+
replacement: [
|
|
282
|
+
"import { starterContextKey, type StarterContext } from '../runtime/gameContext';",
|
|
283
|
+
"import { notificationSettingsSceneKey } from './NotificationSettingsScene';",
|
|
284
|
+
'',
|
|
285
|
+
].join('\n'),
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
anchor: " this.input.once('pointerdown', () => this.scene.start('PlayScene'));\n",
|
|
289
|
+
replacement: [
|
|
290
|
+
' // The settings button shows only where this target has notifications.',
|
|
291
|
+
' const notificationsButton = context.platform.notificationsV2 === undefined',
|
|
292
|
+
' ? undefined',
|
|
293
|
+
' : this.add',
|
|
294
|
+
" .text(940, 16, locale === 'ko' ? '알림 설정' : 'Notifications', {",
|
|
295
|
+
" color: '#5eead4',",
|
|
296
|
+
" fontFamily: 'Arial, sans-serif',",
|
|
297
|
+
" fontSize: '22px',",
|
|
298
|
+
' })',
|
|
299
|
+
' .setOrigin(1, 0)',
|
|
300
|
+
' .setPadding(16, 12, 16, 12)',
|
|
301
|
+
' .setInteractive({ useHandCursor: true });',
|
|
302
|
+
" notificationsButton?.on('pointerup', () => this.scene.start(notificationSettingsSceneKey));",
|
|
303
|
+
' // A tap anywhere else starts the game; the scene drops this listener on shutdown.',
|
|
304
|
+
" this.input.on('pointerdown', (_pointer: Phaser.Input.Pointer, over: Phaser.GameObjects.GameObject[]) => {",
|
|
305
|
+
' if (notificationsButton === undefined || !over.includes(notificationsButton)) {',
|
|
306
|
+
" this.scene.start('PlayScene');",
|
|
307
|
+
' }',
|
|
308
|
+
' });',
|
|
309
|
+
'',
|
|
310
|
+
].join('\n'),
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
const liteEdits = {
|
|
315
|
+
'src/runtime/createGame.ts': [
|
|
316
|
+
{
|
|
317
|
+
anchor: "import { createTapGame } from '../lite/tapGame';\n",
|
|
318
|
+
replacement: [
|
|
319
|
+
"import { mountNotificationSettingsPanel } from '../lite/notificationSettingsPanel';",
|
|
320
|
+
"import { createTapGame } from '../lite/tapGame';",
|
|
321
|
+
'',
|
|
322
|
+
].join('\n'),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
anchor: " const shell = createTapGame({ mount, title: '__GAME_TITLE__' });\n",
|
|
326
|
+
replacement: [
|
|
327
|
+
" const shell = createTapGame({ mount, title: '__GAME_TITLE__' });",
|
|
328
|
+
' const disposeNotificationSettings = mountNotificationSettingsPanel(mount, input.context);',
|
|
329
|
+
'',
|
|
330
|
+
].join('\n'),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
anchor: ' audio.dispose();\n',
|
|
334
|
+
replacement: [' audio.dispose();', ' disposeNotificationSettings();', ''].join('\n'),
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
const readmeSection = [
|
|
339
|
+
'',
|
|
340
|
+
'## Notifications',
|
|
341
|
+
'',
|
|
342
|
+
'This game was created with `--providers notifications`. Its topics live in',
|
|
343
|
+
'`notifications/topics.json` and the backend settings (`advertisingRules`,',
|
|
344
|
+
'the advertising sender) in `notifications/deployment.json`; run',
|
|
345
|
+
'`pnpm validate:notifications` after every change and before every release.',
|
|
346
|
+
'Replace the sender contact placeholder in `notifications/deployment.json`',
|
|
347
|
+
'before any advertising is sent. The notification settings screen asks for the',
|
|
348
|
+
'OS or host permission and records reminder, advertising (광고성 정보',
|
|
349
|
+
'수신동의), and night consent through the notifications runtime. See',
|
|
350
|
+
'`docs/NOTIFICATIONS.md` and, for Android and iOS, the mpgd-kit Firebase setup',
|
|
351
|
+
'guide; `mpgd target init capacitor --providers notifications` installs the two',
|
|
352
|
+
'Capacitor plugins into a game-owned shell.',
|
|
353
|
+
'',
|
|
354
|
+
].join('\n');
|
|
355
|
+
function applyEdits(path, content, edits) {
|
|
356
|
+
let next = content;
|
|
357
|
+
for (const { anchor, replacement } of edits) {
|
|
358
|
+
const first = next.indexOf(anchor);
|
|
359
|
+
if (first === -1 || next.indexOf(anchor, first + anchor.length) !== -1) {
|
|
360
|
+
throw new Error(`The notifications starter could not find exactly one anchor in ${path}; the template changed: ${JSON.stringify(anchor.split('\n')[0])}`);
|
|
361
|
+
}
|
|
362
|
+
next = `${next.slice(0, first)}${replacement}${next.slice(first + anchor.length)}`;
|
|
363
|
+
}
|
|
364
|
+
return next;
|
|
365
|
+
}
|
|
366
|
+
function editPackageJson(content) {
|
|
367
|
+
const value = JSON.parse(content);
|
|
368
|
+
value.scripts = {
|
|
369
|
+
...value.scripts,
|
|
370
|
+
'validate:notifications': 'ttsx --project tsconfig.tools.json tools/validate-notifications.ts',
|
|
371
|
+
};
|
|
372
|
+
value.dependencies = Object.fromEntries(Object.entries({ ...value.dependencies, ...notificationsGameDependencies })
|
|
373
|
+
.sort(([left], [right]) => left.localeCompare(right)));
|
|
374
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
375
|
+
}
|
|
376
|
+
/** The Devvit server reads the deployment settings through `@mpgd/platform/notifications`. */
|
|
377
|
+
function editDevvitPackageJson(content) {
|
|
378
|
+
const value = JSON.parse(content);
|
|
379
|
+
value.dependencies = Object.fromEntries(Object.entries({
|
|
380
|
+
...value.dependencies,
|
|
381
|
+
'@mpgd/platform': '__MPGD_DEPENDENCY_VERSION_PLATFORM__',
|
|
382
|
+
}).sort(([left], [right]) => left.localeCompare(right)));
|
|
383
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
384
|
+
}
|
|
385
|
+
function editTargets(content) {
|
|
386
|
+
const value = JSON.parse(content);
|
|
387
|
+
// Android, iOS, and Apps in Toss expose notifications once their provider
|
|
388
|
+
// is ready; the provider still reports its own readiness (no plugin, no
|
|
389
|
+
// mapped topic), which the target config follows. Reddit keeps the
|
|
390
|
+
// default `approval-required` until Reddit approves the app, and exposes
|
|
391
|
+
// the login prompt the opt-in needs.
|
|
392
|
+
for (const name of ['android', 'ios', 'ait']) {
|
|
393
|
+
const target = value.targets[name];
|
|
394
|
+
if (target !== undefined) {
|
|
395
|
+
target.integrations = { ...target.integrations, notifications: 'available' };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const reddit = value.targets.reddit;
|
|
399
|
+
if (reddit !== undefined) {
|
|
400
|
+
reddit.integrations = { ...reddit.integrations, identityUpgrade: 'available' };
|
|
401
|
+
}
|
|
402
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Applies the notifications starter to a generated game's template files:
|
|
406
|
+
* adds the overlay's files (`common` and the template's own) and edits the
|
|
407
|
+
* base files at their anchors. Throws when an anchor is missing, so a
|
|
408
|
+
* template change cannot silently drop the wiring.
|
|
409
|
+
*/
|
|
410
|
+
export function applyNotificationsStarter(baseFiles, overlay) {
|
|
411
|
+
const edits = {
|
|
412
|
+
...commonEdits,
|
|
413
|
+
...(overlay.template === 'phaser' ? phaserEdits : liteEdits),
|
|
414
|
+
};
|
|
415
|
+
const pending = new Set(Object.keys(edits));
|
|
416
|
+
const edited = baseFiles.map((file) => {
|
|
417
|
+
const fileEdits = edits[file.relativePath];
|
|
418
|
+
let content = file.content;
|
|
419
|
+
if (fileEdits !== undefined) {
|
|
420
|
+
pending.delete(file.relativePath);
|
|
421
|
+
content = applyEdits(file.relativePath, content, fileEdits);
|
|
422
|
+
}
|
|
423
|
+
else if (file.relativePath === 'package.json') {
|
|
424
|
+
content = editPackageJson(content);
|
|
425
|
+
}
|
|
426
|
+
else if (file.relativePath === 'apps/target-devvit/package.json') {
|
|
427
|
+
content = editDevvitPackageJson(content);
|
|
428
|
+
}
|
|
429
|
+
else if (file.relativePath === 'mpgd.targets.json') {
|
|
430
|
+
content = editTargets(content);
|
|
431
|
+
}
|
|
432
|
+
else if (file.relativePath === 'README.md') {
|
|
433
|
+
content = `${content.trimEnd()}\n${readmeSection}`;
|
|
434
|
+
}
|
|
435
|
+
return content === file.content ? file : { ...file, content };
|
|
436
|
+
});
|
|
437
|
+
if (pending.size > 0) {
|
|
438
|
+
throw new Error(`The notifications starter is missing template files: ${[...pending].sort().join(', ')}`);
|
|
439
|
+
}
|
|
440
|
+
const added = [
|
|
441
|
+
...overlay.common,
|
|
442
|
+
...(overlay.template === 'phaser' ? overlay.phaser : overlay.lite),
|
|
443
|
+
];
|
|
444
|
+
const existing = new Set(edited.map((file) => file.relativePath));
|
|
445
|
+
for (const file of added) {
|
|
446
|
+
// Also refuses a path the overlay's parts both carry, so neither wins silently.
|
|
447
|
+
if (existing.has(file.relativePath)) {
|
|
448
|
+
throw new Error(`The notifications starter would overwrite ${file.relativePath}.`);
|
|
449
|
+
}
|
|
450
|
+
existing.add(file.relativePath);
|
|
451
|
+
}
|
|
452
|
+
return [...edited, ...added];
|
|
453
|
+
}
|
|
454
|
+
/** The Capacitor gateways the notifications starter wires, by native platform. */
|
|
455
|
+
export const notificationsCapacitorGateways = Object.freeze({
|
|
456
|
+
android: 'src/platform/buildGateways/capacitorAndroid.ts',
|
|
457
|
+
ios: 'src/platform/buildGateways/capacitorIos.ts',
|
|
458
|
+
});
|
|
459
|
+
/** The starter modules a wired Capacitor gateway imports, directly or through one another. */
|
|
460
|
+
export const notificationsCapacitorGatewayModules = Object.freeze([
|
|
461
|
+
'src/platform/capacitorNotifications.ts',
|
|
462
|
+
'src/platform/notifications.ts',
|
|
463
|
+
'src/platform/notificationBackend.ts',
|
|
464
|
+
'src/platform/notificationSettings.ts',
|
|
465
|
+
'src/platform/notificationOpens.ts',
|
|
466
|
+
]);
|
|
467
|
+
/**
|
|
468
|
+
* The files `mpgd target init capacitor --providers notifications` writes
|
|
469
|
+
* with a wired gateway when the game has none: the modules it imports, the
|
|
470
|
+
* catalog files they read, the validator, and the game's notifications doc.
|
|
471
|
+
*/
|
|
472
|
+
export const notificationsCapacitorSupportFiles = Object.freeze([
|
|
473
|
+
...notificationsCapacitorGatewayModules,
|
|
474
|
+
'notifications/topics.json',
|
|
475
|
+
'notifications/deployment.json',
|
|
476
|
+
'tools/validate-notifications.ts',
|
|
477
|
+
'docs/NOTIFICATIONS.md',
|
|
478
|
+
]);
|
|
479
|
+
/**
|
|
480
|
+
* Files of the base game template the notifications modules import, which a
|
|
481
|
+
* game created by an earlier CLI release may not have: written from the
|
|
482
|
+
* template when missing, kept when the game has one.
|
|
483
|
+
*/
|
|
484
|
+
export const notificationsCapacitorTemplateSupportFiles = Object.freeze([
|
|
485
|
+
'src/platform/serverSession.ts',
|
|
486
|
+
]);
|
|
487
|
+
/** Whether a Capacitor gateway already registers a notifications provider. */
|
|
488
|
+
export function capacitorGatewayRegistersNotifications(source) {
|
|
489
|
+
// One pass over strings and comments: neither registers anything, and a
|
|
490
|
+
// string that holds `//` or `/*` does not start a comment. Regex literals
|
|
491
|
+
// are not tokenized, so a quote or `/*` inside one can hide a call or
|
|
492
|
+
// expose a commented one; such a gateway gets a warning or keeps its
|
|
493
|
+
// wiring as it is, and is never rewritten wrongly.
|
|
494
|
+
const code = source.replace(/'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\.|[^`\\])*`|\/\/[^\n]*|\/\*[\s\S]*?\*\//gu, (token) => (token.startsWith('/') ? ' ' : '""'));
|
|
495
|
+
return /\b(?:createStarterCapacitorNotifications|createCapacitorNotificationServiceProvider)\s*\(/u
|
|
496
|
+
.test(code);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The template's Capacitor gateway with the notifications provider wired in:
|
|
500
|
+
* the same anchored edits `--providers notifications` makes at generation.
|
|
501
|
+
*/
|
|
502
|
+
export function wireCapacitorGatewayNotifications(relativePath, source) {
|
|
503
|
+
const edits = commonEdits[relativePath];
|
|
504
|
+
if (edits === undefined || !Object.values(notificationsCapacitorGateways).includes(relativePath)) {
|
|
505
|
+
throw new Error(`${relativePath} is not a Capacitor gateway the notifications starter wires.`);
|
|
506
|
+
}
|
|
507
|
+
return applyEdits(relativePath, source, edits);
|
|
508
|
+
}
|
|
509
|
+
/** Reads `--providers` for `mpgd game create`: only `notifications` is a game-level provider today. */
|
|
510
|
+
export function readGameCreateProviders(value) {
|
|
511
|
+
if (value === undefined) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
const ids = [...new Set(value.split(',').map((id) => id.trim()).filter((id) => id.length > 0))];
|
|
515
|
+
for (const id of ids) {
|
|
516
|
+
if (id !== notificationsProviderId) {
|
|
517
|
+
throw new Error(`Unsupported --providers ${id} for mpgd game create; expected: ${notificationsProviderId}. `
|
|
518
|
+
+ 'Store plugins (play-billing, storekit) are installed with "mpgd target init capacitor --providers ...".');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return ids;
|
|
522
|
+
}
|
package/dist/offline-playtest.js
CHANGED
|
@@ -3125,7 +3125,7 @@ function assembleOfflineHtml(html, bundledEntry, entryAttributes) {
|
|
|
3125
3125
|
|| output.indexOf(offlineEntryPlaceholder) !== output.lastIndexOf(offlineEntryPlaceholder)) {
|
|
3126
3126
|
throw new Error('Offline playtest could not preserve the module entry position.');
|
|
3127
3127
|
}
|
|
3128
|
-
output = output.replace(offlineEntryPlaceholder, inlineScript);
|
|
3128
|
+
output = output.replace(offlineEntryPlaceholder, () => inlineScript);
|
|
3129
3129
|
if (findActiveHtmlStartTag(output, 'body') !== undefined) {
|
|
3130
3130
|
return `${output.trim()}\n`;
|
|
3131
3131
|
}
|
|
@@ -399,7 +399,7 @@ function migrateKnownRunner(source, template) {
|
|
|
399
399
|
if (defaults.length !== 1) {
|
|
400
400
|
return undefined;
|
|
401
401
|
}
|
|
402
|
-
const rendered = template.replace("'__DEFAULT_KIT_PATH__'", JSON.stringify(defaults[0]));
|
|
402
|
+
const rendered = template.replace("'__DEFAULT_KIT_PATH__'", () => JSON.stringify(defaults[0]));
|
|
403
403
|
const canonical = (value) => JSON.stringify(value, (key, item) => ['start', 'end', 'raw'].includes(key) ? undefined : item);
|
|
404
404
|
return canonical(ast) === canonical(parse(rendered, { ecmaVersion: 'latest', sourceType: 'module' }))
|
|
405
405
|
? rendered
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"parse5": "7.3.0",
|
|
71
71
|
"postcss": "8.5.28",
|
|
72
72
|
"postcss-value-parser": "4.2.0",
|
|
73
|
-
"@mpgd/catalog": "0.10.
|
|
74
|
-
"@mpgd/platform": "0.
|
|
73
|
+
"@mpgd/catalog": "0.10.5",
|
|
74
|
+
"@mpgd/platform": "0.27.0",
|
|
75
75
|
"@mpgd/phaser-assets": "0.9.1",
|
|
76
|
-
"@mpgd/target-config": "0.23.
|
|
76
|
+
"@mpgd/target-config": "0.23.5"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/node": "^24.0.0",
|
|
@@ -81,18 +81,18 @@
|
|
|
81
81
|
"@types/yauzl": "3.4.0",
|
|
82
82
|
"ttsc": "0.32.1",
|
|
83
83
|
"typescript": "7.0.2",
|
|
84
|
-
"@mpgd/adapter-ait": "0.
|
|
85
|
-
"@mpgd/adapter-browser": "0.11.
|
|
86
|
-
"@mpgd/adapter-
|
|
87
|
-
"@mpgd/adapter-
|
|
88
|
-
"@mpgd/
|
|
89
|
-
"@mpgd/
|
|
90
|
-
"@mpgd/
|
|
91
|
-
"@mpgd/
|
|
92
|
-
"@mpgd/capacitor-storekit": "0.3.
|
|
93
|
-
"@mpgd/game-services": "0.
|
|
94
|
-
"@mpgd/
|
|
95
|
-
"@mpgd/
|
|
84
|
+
"@mpgd/adapter-ait": "0.20.1",
|
|
85
|
+
"@mpgd/adapter-browser": "0.11.5",
|
|
86
|
+
"@mpgd/adapter-devvit": "0.13.3",
|
|
87
|
+
"@mpgd/adapter-verse8": "0.7.5",
|
|
88
|
+
"@mpgd/bridge": "0.14.2",
|
|
89
|
+
"@mpgd/adapter-capacitor": "0.12.1",
|
|
90
|
+
"@mpgd/analytics": "0.4.14",
|
|
91
|
+
"@mpgd/capacitor-play-billing": "0.3.5",
|
|
92
|
+
"@mpgd/capacitor-storekit": "0.3.5",
|
|
93
|
+
"@mpgd/game-services": "0.27.0",
|
|
94
|
+
"@mpgd/i18n": "0.6.19",
|
|
95
|
+
"@mpgd/game-runtime": "0.7.5",
|
|
96
96
|
"@mpgd/lite-shell": "0.1.0"
|
|
97
97
|
},
|
|
98
98
|
"main": "./dist/index.js",
|