@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.
Files changed (29) hide show
  1. package/dist/capacitor-shell-starter.d.ts +22 -0
  2. package/dist/capacitor-shell-starter.js +395 -18
  3. package/dist/deploy-planning.js +2 -2
  4. package/dist/index.js +122 -34
  5. package/dist/kit-upgrade.js +2 -2
  6. package/dist/native-build-info.json +2 -2
  7. package/dist/native-build-target.js +433 -2
  8. package/dist/notifications-starter.d.ts +75 -0
  9. package/dist/notifications-starter.js +522 -0
  10. package/dist/offline-playtest.js +1 -1
  11. package/dist/tooling-migration.js +1 -1
  12. package/package.json +16 -16
  13. package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +104 -0
  14. package/templates/notifications-overlay/common/notifications/deployment.json +8 -0
  15. package/templates/notifications-overlay/common/notifications/topics.json +222 -0
  16. package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +59 -0
  17. package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +157 -0
  18. package/templates/notifications-overlay/common/src/platform/notificationOpens.ts +60 -0
  19. package/templates/notifications-overlay/common/src/platform/notificationSettings.ts +761 -0
  20. package/templates/notifications-overlay/common/src/platform/notifications.ts +163 -0
  21. package/templates/notifications-overlay/common/tools/validate-notifications.ts +75 -0
  22. package/templates/notifications-overlay/lite/src/lite/notificationSettingsPanel.ts +200 -0
  23. package/templates/notifications-overlay/phaser/src/scenes/NotificationSettingsScene.ts +301 -0
  24. package/templates/phaser-game/apps/target-ait/src/main.ts +15 -0
  25. package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +5 -0
  26. package/templates/phaser-game/apps/target-ait/vite.config.ts +22 -1
  27. package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +14 -0
  28. package/templates/phaser-game/src/platform/gameServices.ts +3 -0
  29. package/templates/phaser-game/src/platform/serverSession.ts +66 -0
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, verifyHost
24
24
  import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
25
25
  import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
26
26
  import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
27
+ import { applyNotificationsStarter, notificationsProviderId, readGameCreateProviders, } from './notifications-starter.js';
27
28
  import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
28
29
  import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
29
30
  import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
@@ -58,6 +59,7 @@ const packageRoot = resolvePackageRoot(sourceDir);
58
59
  const detectedKitRoot = resolveDefaultKitRoot();
59
60
  const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
60
61
  const liteTemplateOverlayDir = path.resolve(packageRoot, 'templates/lite-overlay');
62
+ const notificationsTemplateDir = path.resolve(packageRoot, 'templates/notifications-overlay');
61
63
  const supportedGameTemplates = ['phaser', 'lite'];
62
64
  /** Lite renderers implemented today. dom/svg are reserved extension points in @mpgd/lite-shell. */
63
65
  const supportedLiteRenderers = ['canvas'];
@@ -575,6 +577,10 @@ const gameCommand = defineI18n({
575
577
  en: 'Lite template renderer. Only canvas is implemented today.',
576
578
  ko: 'lite 템플릿 렌더러. 현재는 canvas만 지원합니다.',
577
579
  },
580
+ providers: {
581
+ en: 'Comma-separated game providers. notifications adds the notifications v2 starter: the topic catalog, the backend settings, the Android, iOS, Apps in Toss, and Devvit wiring, and a notification settings screen.',
582
+ ko: '쉼표로 구분한 게임 provider. notifications는 알림 v2 스타터(토픽 카탈로그, 백엔드 설정, Android·iOS·앱인토스·Devvit 연결, 알림 설정 화면)를 추가합니다.',
583
+ },
578
584
  }),
579
585
  args: {
580
586
  template: {
@@ -627,6 +633,11 @@ const gameCommand = defineI18n({
627
633
  required: false,
628
634
  description: 'Include the Microsoft Store PWA target and release workflow.',
629
635
  },
636
+ providers: {
637
+ type: 'string',
638
+ required: false,
639
+ description: 'Comma-separated game providers. notifications adds the notifications v2 starter.',
640
+ },
630
641
  },
631
642
  run: (ctx) => {
632
643
  const positionals = readLocalPositionals(ctx.positionals, ['game', 'create']);
@@ -649,6 +660,7 @@ const gameCommand = defineI18n({
649
660
  ...(kitPath === undefined ? {} : { kitPath }),
650
661
  dryRun: ctx.values['dry-run'] === true,
651
662
  microsoftStore: ctx.values['microsoft-store'] === true,
663
+ providers: readGameCreateProviders(readOptionalString(ctx.values.providers)),
652
664
  });
653
665
  },
654
666
  }),
@@ -1597,8 +1609,8 @@ const targetCommand = defineI18n({
1597
1609
  ko: '프로덕션 HTTPS Game Services 기본 URL.',
1598
1610
  },
1599
1611
  providers: {
1600
- en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
1601
- ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
1612
+ en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
1613
+ ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. notifications는 Android·iOS용 Firebase 메시징과 로컬 알림 플러그인을 설치하고 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
1602
1614
  },
1603
1615
  }),
1604
1616
  args: {
@@ -1646,7 +1658,7 @@ const targetCommand = defineI18n({
1646
1658
  providers: {
1647
1659
  type: 'string',
1648
1660
  required: false,
1649
- description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
1661
+ description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
1650
1662
  },
1651
1663
  },
1652
1664
  run: (ctx) => {
@@ -1693,6 +1705,11 @@ const targetCommand = defineI18n({
1693
1705
  + `${plugin.versionSource === 'kit' ? ' and the game' : ''}`
1694
1706
  + `${dryRun ? '; cap add or cap sync registers it' : ''}.`);
1695
1707
  }
1708
+ if (plan.notificationPlugins.length > 0) {
1709
+ console.info(`${dryRun ? 'Would install' : 'Installed'} notifications plugins `
1710
+ + plan.notificationPlugins.map((plugin) => `${plugin.packageName}@${plugin.version}`).join(', ')
1711
+ + ` into the shell for android and ios${dryRun ? '; cap add or cap sync registers them' : ''}.`);
1712
+ }
1696
1713
  for (const warning of plan.warnings) {
1697
1714
  console.warn(`Warning: ${warning}`);
1698
1715
  }
@@ -2943,15 +2960,30 @@ function createGameApp(input) {
2943
2960
  microsoftStore: input.microsoftStore,
2944
2961
  });
2945
2962
  const baseTemplateFiles = collectTemplateFiles(gameTemplateDir);
2946
- const selectedTemplateFiles = input.template === 'lite'
2963
+ const templateFiles = input.template === 'lite'
2947
2964
  ? applyLiteTemplateOverlay(baseTemplateFiles, collectTemplateFiles(liteTemplateOverlayDir))
2948
2965
  : baseTemplateFiles;
2966
+ const selectedTemplateFiles = input.providers?.includes(notificationsProviderId) === true
2967
+ ? applyNotificationsStarter(templateFiles, {
2968
+ template: input.template === 'lite' ? 'lite' : 'phaser',
2969
+ common: collectTemplateFiles(path.join(notificationsTemplateDir, 'common')),
2970
+ phaser: collectTemplateFiles(path.join(notificationsTemplateDir, 'phaser')),
2971
+ lite: collectTemplateFiles(path.join(notificationsTemplateDir, 'lite')),
2972
+ })
2973
+ : templateFiles;
2949
2974
  const files = selectedTemplateFiles.flatMap((file) => {
2950
2975
  const prepared = prepareBaseGameTemplateFile({
2951
2976
  relativePath: file.relativePath,
2952
2977
  content: file.content,
2953
2978
  });
2954
- return prepared === undefined ? [] : [{ ...file, content: renderTemplate(prepared, context) }];
2979
+ return prepared === undefined
2980
+ ? []
2981
+ : [
2982
+ {
2983
+ ...file,
2984
+ content: renderTemplate(prepared, context, file.relativePath),
2985
+ },
2986
+ ];
2955
2987
  });
2956
2988
  if (input.dryRun) {
2957
2989
  console.info(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
@@ -3162,8 +3194,10 @@ function assertValidGameTitle(title) {
3162
3194
  if (title.trim().length === 0) {
3163
3195
  throw new Error('Game title cannot be empty.');
3164
3196
  }
3165
- if (/[\u0000-\u001f<>"'\\]/u.test(title)) {
3166
- throw new Error('Game title cannot contain control characters, quotes, backslashes, or angle brackets.');
3197
+ // Quotes, backslashes, and `$` are allowed: JSON files and JS string literals
3198
+ // escape them. `<` would break the generated HTML title.
3199
+ if (/[\u0000-\u001f<>]/u.test(title)) {
3200
+ throw new Error('Game title cannot contain control characters or angle brackets.');
3167
3201
  }
3168
3202
  }
3169
3203
  function collectTemplateFiles(templateDir) {
@@ -3185,28 +3219,79 @@ function walkTemplateDir(templateDir, relativeDir, files) {
3185
3219
  });
3186
3220
  }
3187
3221
  }
3188
- function renderTemplate(content, context) {
3189
- let rendered = content
3190
- .replaceAll("from '@mpgd/cli/devvit'", `from ${context.devvitOutputPluginImport}`)
3191
- .replaceAll('__GAME_NAME__', context.gameName)
3192
- .replaceAll('__DEVVIT_APP_NAME__', context.devvitAppName)
3193
- .replaceAll('__GAME_TITLE_TS_LITERAL__', context.gameTitleTsLiteral)
3194
- .replaceAll('__GAME_TITLE__', context.gameTitle)
3195
- .replaceAll('__LEGAL_LAST_UPDATED__', context.legalLastUpdated)
3196
- .replaceAll('__PACKAGE_NAME__', context.packageName)
3197
- .replaceAll('__MPGD_DEPENDENCY_VERSION__', context.defaultDependencyVersion)
3198
- .replaceAll('__TSCONFIG_EXTENDS_LINE__', context.tsconfigExtendsLine)
3199
- .replaceAll('__TSCONFIG_ROOT_DIR__', context.tsconfigRootDir)
3200
- .replaceAll('__TSCONFIG_WORKSPACE_INCLUDES__', context.tsconfigWorkspaceIncludes)
3201
- .replaceAll('__TSCONFIG_WORKSPACE_EXCLUDES__', context.tsconfigWorkspaceExcludes)
3202
- .replaceAll('__WORKSPACE_I18N_BUILD_PREFIX__', context.workspaceI18nBuildPrefix)
3203
- .replaceAll('__DEFAULT_KIT_PATH__', context.defaultKitPath)
3204
- .replaceAll('__RECOMMENDED_MATRIX_TARGETS__', context.recommendedMatrixTargets)
3205
- .replaceAll('__PNPM_WORKSPACE_KIT_PACKAGES__', context.pnpmWorkspaceKitPackages)
3206
- .replaceAll('__PASCAL_NAME__', context.pascalName)
3207
- .replaceAll('__CAMEL_NAME__', context.camelName);
3222
+ /**
3223
+ * String replacements treat `$&`, `$'`, `` $` ``, and `$$` as patterns.
3224
+ * A function replacer inserts `value` literally.
3225
+ */
3226
+ function replaceLiteral(content, placeholder, value) {
3227
+ return content.replaceAll(placeholder, () => value);
3228
+ }
3229
+ /** Interior of a JSON string, without the surrounding quotes. */
3230
+ function jsonStringContents(value) {
3231
+ return JSON.stringify(value).slice(1, -1);
3232
+ }
3233
+ /**
3234
+ * Placeholders substituted inside an existing JSON string. Structural
3235
+ * fragments (`__TSCONFIG_EXTENDS_LINE__` and the workspace include/exclude
3236
+ * lists) stay raw so their quotes remain JSON syntax.
3237
+ */
3238
+ function jsonEscapedPlaceholder(placeholder) {
3239
+ return placeholder === '__GAME_TITLE__'
3240
+ || placeholder === '__GAME_NAME__'
3241
+ || placeholder === '__PACKAGE_NAME__'
3242
+ || placeholder === '__DEVVIT_APP_NAME__'
3243
+ || placeholder === '__DEFAULT_KIT_PATH__'
3244
+ || placeholder === '__WORKSPACE_I18N_BUILD_PREFIX__'
3245
+ || placeholder === '__TSCONFIG_ROOT_DIR__'
3246
+ || placeholder === '__LEGAL_LAST_UPDATED__'
3247
+ || placeholder === '__MPGD_DEPENDENCY_VERSION__'
3248
+ || placeholder.startsWith('__MPGD_DEPENDENCY_VERSION_');
3249
+ }
3250
+ /** Interior of a single-quoted JS string. Backslash first, then quote. */
3251
+ function singleQuotedContents(value) {
3252
+ return value.replaceAll('\\', () => '\\\\').replaceAll("'", () => "\\'");
3253
+ }
3254
+ function renderTemplate(content, context, relativePath) {
3255
+ // `.webmanifest` is JSON. Hosted PWA deploy and Microsoft Store submission parse it.
3256
+ const jsonFile = relativePath.endsWith('.json') || relativePath.endsWith('.webmanifest');
3257
+ const typescriptFile = relativePath.endsWith('.ts');
3258
+ const insert = (placeholder, value) => {
3259
+ if (jsonFile && jsonEscapedPlaceholder(placeholder)) {
3260
+ return jsonStringContents(value);
3261
+ }
3262
+ // Template titles sit in single quotes (`title: '__GAME_TITLE__'`).
3263
+ if (typescriptFile && placeholder === '__GAME_TITLE__') {
3264
+ return singleQuotedContents(value);
3265
+ }
3266
+ return value;
3267
+ };
3268
+ const pairs = [
3269
+ [`from '@mpgd/cli/devvit'`, `from ${context.devvitOutputPluginImport}`],
3270
+ ['__GAME_NAME__', context.gameName],
3271
+ ['__DEVVIT_APP_NAME__', context.devvitAppName],
3272
+ // Longer than `__GAME_TITLE__`, so a title is not written into the literal name.
3273
+ ['__GAME_TITLE_TS_LITERAL__', context.gameTitleTsLiteral],
3274
+ ['__GAME_TITLE__', context.gameTitle],
3275
+ ['__LEGAL_LAST_UPDATED__', context.legalLastUpdated],
3276
+ ['__PACKAGE_NAME__', context.packageName],
3277
+ ['__MPGD_DEPENDENCY_VERSION__', context.defaultDependencyVersion],
3278
+ ['__TSCONFIG_EXTENDS_LINE__', context.tsconfigExtendsLine],
3279
+ ['__TSCONFIG_ROOT_DIR__', context.tsconfigRootDir],
3280
+ ['__TSCONFIG_WORKSPACE_INCLUDES__', context.tsconfigWorkspaceIncludes],
3281
+ ['__TSCONFIG_WORKSPACE_EXCLUDES__', context.tsconfigWorkspaceExcludes],
3282
+ ['__WORKSPACE_I18N_BUILD_PREFIX__', context.workspaceI18nBuildPrefix],
3283
+ ['__DEFAULT_KIT_PATH__', context.defaultKitPath],
3284
+ ['__RECOMMENDED_MATRIX_TARGETS__', context.recommendedMatrixTargets],
3285
+ ['__PNPM_WORKSPACE_KIT_PACKAGES__', context.pnpmWorkspaceKitPackages],
3286
+ ['__PASCAL_NAME__', context.pascalName],
3287
+ ['__CAMEL_NAME__', context.camelName],
3288
+ ];
3289
+ let rendered = content;
3290
+ for (const [placeholder, value] of pairs) {
3291
+ rendered = replaceLiteral(rendered, placeholder, insert(placeholder, value));
3292
+ }
3208
3293
  for (const replacement of context.mpgdDependencyVersionReplacements) {
3209
- rendered = rendered.replaceAll(replacement.placeholder, replacement.version);
3294
+ rendered = replaceLiteral(rendered, replacement.placeholder, insert(replacement.placeholder, replacement.version));
3210
3295
  }
3211
3296
  return rendered;
3212
3297
  }
@@ -3311,8 +3396,8 @@ function resolveStandaloneNativeBuild(targetsFile, requestedTarget, values) {
3311
3396
  return undefined;
3312
3397
  }
3313
3398
  const candidate = path.resolve(gameRoot, configuredPath
3314
- .replaceAll('${MPGD_GAME_ROOT}', gameRoot)
3315
- .replaceAll('${MPGD_GAME_APP_ROOT}', gameRoot));
3399
+ .replaceAll('${MPGD_GAME_ROOT}', () => gameRoot)
3400
+ .replaceAll('${MPGD_GAME_APP_ROOT}', () => gameRoot));
3316
3401
  let existingAncestor = candidate;
3317
3402
  while (!existsSync(existingAncestor)) {
3318
3403
  const parent = path.dirname(existingAncestor);
@@ -3371,8 +3456,14 @@ function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
3371
3456
  ?? readSourceGitSha(gameRoot);
3372
3457
  const appVersion = readOptionalString(baseEnv.APP_VERSION)
3373
3458
  ?? readGamePackageVersion(gameRoot);
3459
+ const notificationTopicsFile = readConfiguredPath(baseEnv.MPGD_NOTIFICATION_TOPICS_FILE);
3374
3460
  return {
3375
3461
  ...monetizationFiles,
3462
+ // The Apps in Toss wrapper builds in its own directory; a relative path
3463
+ // names a file of the game, as `pnpm validate:notifications` reads it.
3464
+ ...(notificationTopicsFile === undefined
3465
+ ? {}
3466
+ : { MPGD_NOTIFICATION_TOPICS_FILE: path.resolve(gameRoot, notificationTopicsFile) }),
3376
3467
  MPGD_SOURCE_GIT_SHA: sourceGitSha,
3377
3468
  ...(appVersion === undefined ? {} : { APP_VERSION: appVersion }),
3378
3469
  };
@@ -3464,10 +3555,7 @@ function formatError(error) {
3464
3555
  }
3465
3556
  function replaceTargetTokens(value, context) {
3466
3557
  if (typeof value === 'string') {
3467
- return value
3468
- .replaceAll('${MPGD_KIT_PATH}', context.kitPath)
3469
- .replaceAll('${MPGD_GAME_ROOT}', context.gameRoot)
3470
- .replaceAll('${MPGD_GAME_APP_ROOT}', context.gameRoot);
3558
+ return replaceLiteral(replaceLiteral(replaceLiteral(value, '${MPGD_KIT_PATH}', context.kitPath), '${MPGD_GAME_ROOT}', context.gameRoot), '${MPGD_GAME_APP_ROOT}', context.gameRoot);
3471
3559
  }
3472
3560
  if (Array.isArray(value)) {
3473
3561
  return value.map((entry) => replaceTargetTokens(entry, context));
@@ -278,8 +278,8 @@ function discoverGameManifests(gameRoot, notes, blockers, targetsFileName) {
278
278
  continue;
279
279
  }
280
280
  const resolvedLocation = location
281
- .replaceAll('${MPGD_GAME_ROOT}', targetsDirectory)
282
- .replaceAll('${MPGD_GAME_APP_ROOT}', targetsDirectory);
281
+ .replaceAll('${MPGD_GAME_ROOT}', () => targetsDirectory)
282
+ .replaceAll('${MPGD_GAME_APP_ROOT}', () => targetsDirectory);
283
283
  if (resolvedLocation.includes('$' + '{')) {
284
284
  notes.push('Skipped target with unresolved path token: ' + location);
285
285
  continue;
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "48bd874fd96f6dbca38c4ff5f0783088b61fab58",
2
+ "kitGitSha": "95ea69d3e9755256b27a3b0ea230812a632ed03b",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.52.0"
4
+ "packageVersion": "0.54.0"
5
5
  }