@mpgd/cli 0.5.0 → 0.7.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/game-acceptance.d.ts +65 -0
- package/dist/game-acceptance.js +245 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +469 -2
- package/dist/production-target-readiness.d.ts +8 -0
- package/dist/production-target-readiness.js +207 -0
- package/package.json +12 -12
- package/templates/phaser-game/README.md +71 -1
- package/templates/phaser-game/agent/acceptance.md +14 -6
- package/templates/phaser-game/agent/brief.md +3 -1
- package/templates/phaser-game/agent/game-manifest.json +11 -0
- package/templates/phaser-game/apps/target-devvit/README.md +30 -0
- package/templates/phaser-game/apps/target-devvit/devvit.json +7 -0
- package/templates/phaser-game/apps/target-devvit/package.json +8 -5
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +46 -1
- package/templates/phaser-game/apps/target-devvit/src/server/postOperationStore.ts +9 -0
- package/templates/phaser-game/apps/target-devvit/vite.server.config.ts +2 -1
- package/templates/phaser-game/game.html +13 -0
- package/templates/phaser-game/gitignore +2 -0
- package/templates/phaser-game/index.html +2 -2
- package/templates/phaser-game/mpgd.game.json +8 -0
- package/templates/phaser-game/mpgd.targets.json +6 -0
- package/templates/phaser-game/package.json +7 -1
- package/templates/phaser-game/public/manifest.webmanifest +1 -0
- package/templates/phaser-game/src/entry.ts +7 -0
- package/templates/phaser-game/src/env.d.ts +4 -0
- package/templates/phaser-game/src/gameEntry.ts +3 -0
- package/templates/phaser-game/src/main.ts +48 -3
- package/templates/phaser-game/src/platform/buildGatewayModule.ts +5 -0
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +11 -0
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/browser.ts +8 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +11 -0
- package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +15 -0
- package/templates/phaser-game/src/platform/devvitEntrypoint.ts +57 -0
- package/templates/phaser-game/src/platform/devvitInlinePreview.css +75 -0
- package/templates/phaser-game/src/platform/gameServices.ts +18 -61
- package/templates/phaser-game/src/platform/installPlatform.ts +6 -50
- package/templates/phaser-game/src/platform/microsoftStorePwa.ts +99 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +3 -1
- package/templates/phaser-game/tools/run-game-acceptance.mjs +22 -0
- package/templates/phaser-game/vite.config.ts +161 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const microsoftStorePwaUpdateReadyEvent = 'mpgd:pwa-update-ready';
|
|
2
|
+
|
|
3
|
+
export interface MicrosoftStorePwaUpdateReadyDetail {
|
|
4
|
+
readonly registration: ServiceWorkerRegistration;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function shouldInstallMicrosoftStorePwa(input: {
|
|
8
|
+
readonly configTarget: string;
|
|
9
|
+
readonly debug: boolean;
|
|
10
|
+
}): boolean {
|
|
11
|
+
return input.configTarget === 'microsoft-store' && !input.debug;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function installMicrosoftStorePwa(input: {
|
|
15
|
+
readonly configTarget: string;
|
|
16
|
+
readonly debug: boolean;
|
|
17
|
+
readonly onUpdateReady?: (detail: MicrosoftStorePwaUpdateReadyDetail) => void;
|
|
18
|
+
readonly onRegistrationError?: (error: unknown) => void;
|
|
19
|
+
}): () => void {
|
|
20
|
+
if (
|
|
21
|
+
!shouldInstallMicrosoftStorePwa(input)
|
|
22
|
+
|| typeof navigator === 'undefined'
|
|
23
|
+
|| !('serviceWorker' in navigator)
|
|
24
|
+
|| typeof document === 'undefined'
|
|
25
|
+
) {
|
|
26
|
+
return () => {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let disposed = false;
|
|
30
|
+
let registration: ServiceWorkerRegistration | null = null;
|
|
31
|
+
let installingWorker: ServiceWorker | null = null;
|
|
32
|
+
const announceWaitingUpdate = () => {
|
|
33
|
+
if (
|
|
34
|
+
disposed
|
|
35
|
+
|| registration === null
|
|
36
|
+
|| registration.waiting === null
|
|
37
|
+
|| navigator.serviceWorker.controller === null
|
|
38
|
+
) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const detail = { registration } satisfies MicrosoftStorePwaUpdateReadyDetail;
|
|
43
|
+
|
|
44
|
+
if (input.onUpdateReady !== undefined) {
|
|
45
|
+
input.onUpdateReady(detail);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
globalThis.dispatchEvent(new CustomEvent(microsoftStorePwaUpdateReadyEvent, { detail }));
|
|
50
|
+
};
|
|
51
|
+
const handleInstallingStateChange = () => {
|
|
52
|
+
if (installingWorker?.state === 'installed') {
|
|
53
|
+
announceWaitingUpdate();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const handleUpdateFound = () => {
|
|
57
|
+
installingWorker?.removeEventListener('statechange', handleInstallingStateChange);
|
|
58
|
+
installingWorker = registration?.installing ?? null;
|
|
59
|
+
installingWorker?.addEventListener('statechange', handleInstallingStateChange);
|
|
60
|
+
};
|
|
61
|
+
const register = async () => {
|
|
62
|
+
try {
|
|
63
|
+
registration = await navigator.serviceWorker.register('./service-worker.js', {
|
|
64
|
+
scope: './',
|
|
65
|
+
updateViaCache: 'none',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (disposed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registration.addEventListener('updatefound', handleUpdateFound);
|
|
73
|
+
handleUpdateFound();
|
|
74
|
+
announceWaitingUpdate();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (input.onRegistrationError !== undefined) {
|
|
77
|
+
input.onRegistrationError(error);
|
|
78
|
+
} else {
|
|
79
|
+
console.warn('[mpgd] Microsoft Store service worker registration failed.', error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const handleWindowLoad = () => {
|
|
84
|
+
void register();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (document.readyState === 'complete') {
|
|
88
|
+
void register();
|
|
89
|
+
} else {
|
|
90
|
+
globalThis.addEventListener('load', handleWindowLoad, { once: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
disposed = true;
|
|
95
|
+
globalThis.removeEventListener('load', handleWindowLoad);
|
|
96
|
+
installingWorker?.removeEventListener('statechange', handleInstallingStateChange);
|
|
97
|
+
registration?.removeEventListener('updatefound', handleUpdateFound);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
|
|
2
2
|
import type { Locale } from '@mpgd/i18n';
|
|
3
|
-
import type { PlayerIdentity } from '@mpgd/platform';
|
|
3
|
+
import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platform';
|
|
4
4
|
import type {
|
|
5
5
|
TargetConfiguredGateway,
|
|
6
6
|
TargetRuntimeSnapshot,
|
|
@@ -16,6 +16,8 @@ export interface StarterContext {
|
|
|
16
16
|
readonly runtime: TargetRuntimeSnapshot;
|
|
17
17
|
readonly viewport: TargetViewportPlan;
|
|
18
18
|
readonly player: PlayerIdentity;
|
|
19
|
+
readonly identitySession: IdentitySession;
|
|
20
|
+
readonly launchIntent: LaunchIntent;
|
|
19
21
|
readonly locale: Locale;
|
|
20
22
|
readonly gameServices: StarterGameServices;
|
|
21
23
|
readonly analytics: AnalyticsReporter;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { runMpgdCli } from '@mpgd/cli';
|
|
5
|
+
|
|
6
|
+
const gameRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
7
|
+
const configuredKitPath = process.env.MPGD_KIT_PATH;
|
|
8
|
+
const kitPath = path.resolve(
|
|
9
|
+
gameRoot,
|
|
10
|
+
configuredKitPath === undefined || configuredKitPath.length === 0
|
|
11
|
+
? '__DEFAULT_KIT_PATH__'
|
|
12
|
+
: configuredKitPath,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
await runMpgdCli([
|
|
16
|
+
'game',
|
|
17
|
+
'accept',
|
|
18
|
+
gameRoot,
|
|
19
|
+
'--kit-path',
|
|
20
|
+
kitPath,
|
|
21
|
+
...process.argv.slice(2),
|
|
22
|
+
]);
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
1
4
|
import ttsc from '@ttsc/unplugin/vite';
|
|
2
5
|
import { defineConfig } from 'vite';
|
|
3
6
|
|
|
7
|
+
interface RuntimePlatformTargetMetadata {
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
readonly adapter: string;
|
|
10
|
+
readonly integrations?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export default defineConfig(({ mode }) => {
|
|
5
14
|
const isProduction = mode === 'production';
|
|
15
|
+
const platformTarget = readRuntimePlatformTarget();
|
|
16
|
+
const appTarget = process.env.APP_TARGET ?? 'browser';
|
|
17
|
+
const isDevvitBuild = appTarget === 'reddit';
|
|
18
|
+
const buildGatewayModule = resolveBuildGatewayModule({
|
|
19
|
+
target: appTarget,
|
|
20
|
+
debug: !isProduction,
|
|
21
|
+
buildId: process.env.BUILD_ID ?? 'local',
|
|
22
|
+
});
|
|
6
23
|
|
|
7
24
|
return {
|
|
8
25
|
base: './',
|
|
@@ -12,11 +29,20 @@ export default defineConfig(({ mode }) => {
|
|
|
12
29
|
plugins: false,
|
|
13
30
|
}),
|
|
14
31
|
],
|
|
32
|
+
resolve: {
|
|
33
|
+
alias: {
|
|
34
|
+
...createCatalogAliases(),
|
|
35
|
+
'#mpgd-platform-gateway': resolve(buildGatewayModule),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
15
38
|
define: {
|
|
16
|
-
__APP_TARGET__: JSON.stringify(
|
|
39
|
+
__APP_TARGET__: JSON.stringify(appTarget),
|
|
17
40
|
__MPGD_CONFIG_TARGET__: JSON.stringify(process.env.MPGD_CONFIG_TARGET ?? ''),
|
|
41
|
+
__MPGD_PLATFORM_TARGET__:
|
|
42
|
+
platformTarget === undefined ? 'undefined' : JSON.stringify(platformTarget),
|
|
18
43
|
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
|
|
19
44
|
__BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
|
|
45
|
+
__SOURCE_GIT_SHA__: JSON.stringify(process.env.MPGD_SOURCE_GIT_SHA ?? 'uncommitted'),
|
|
20
46
|
__DEBUG_BUILD__: JSON.stringify(!isProduction),
|
|
21
47
|
},
|
|
22
48
|
build: {
|
|
@@ -26,8 +52,16 @@ export default defineConfig(({ mode }) => {
|
|
|
26
52
|
assetsDir: 'assets',
|
|
27
53
|
emptyOutDir: true,
|
|
28
54
|
rolldownOptions: {
|
|
55
|
+
...(isDevvitBuild
|
|
56
|
+
? {
|
|
57
|
+
input: {
|
|
58
|
+
preview: resolve('index.html'),
|
|
59
|
+
game: resolve('game.html'),
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
29
63
|
output: {
|
|
30
|
-
entryFileNames: 'assets/game.js',
|
|
64
|
+
entryFileNames: isDevvitBuild ? 'assets/[name].js' : 'assets/game.js',
|
|
31
65
|
chunkFileNames: 'assets/[name].js',
|
|
32
66
|
assetFileNames: 'assets/[name][extname]',
|
|
33
67
|
},
|
|
@@ -35,3 +69,128 @@ export default defineConfig(({ mode }) => {
|
|
|
35
69
|
},
|
|
36
70
|
};
|
|
37
71
|
});
|
|
72
|
+
|
|
73
|
+
export function resolveBuildGatewayModule(input: {
|
|
74
|
+
readonly target: string;
|
|
75
|
+
readonly debug: boolean;
|
|
76
|
+
readonly buildId: string;
|
|
77
|
+
}): string {
|
|
78
|
+
switch (input.target) {
|
|
79
|
+
case 'android':
|
|
80
|
+
return 'src/platform/buildGateways/capacitorAndroid.ts';
|
|
81
|
+
case 'ios':
|
|
82
|
+
return 'src/platform/buildGateways/capacitorIos.ts';
|
|
83
|
+
case 'ait':
|
|
84
|
+
return input.debug
|
|
85
|
+
? 'src/platform/buildGateways/aitSandbox.ts'
|
|
86
|
+
: 'src/platform/buildGateways/ait.ts';
|
|
87
|
+
case 'reddit':
|
|
88
|
+
return input.debug && input.buildId === 'devvit-sandbox'
|
|
89
|
+
? 'src/platform/buildGateways/redditSandbox.ts'
|
|
90
|
+
: 'src/platform/buildGateways/reddit.ts';
|
|
91
|
+
default:
|
|
92
|
+
return 'src/platform/buildGateways/browser.ts';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createCatalogAliases(): Record<string, string> {
|
|
97
|
+
const productCatalogFile = readConfiguredPath(process.env.MPGD_PRODUCT_CATALOG_FILE);
|
|
98
|
+
const adPlacementsFile = readConfiguredPath(process.env.MPGD_AD_PLACEMENTS_FILE);
|
|
99
|
+
|
|
100
|
+
if ((productCatalogFile === undefined) !== (adPlacementsFile === undefined)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'MPGD_PRODUCT_CATALOG_FILE and MPGD_AD_PLACEMENTS_FILE must be configured together.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (productCatalogFile === undefined || adPlacementsFile === undefined) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
'@mpgd/catalog/catalog.json': resolveCatalogPath(productCatalogFile, adPlacementsFile),
|
|
112
|
+
'@mpgd/catalog/placements.json': resolveCatalogPath(adPlacementsFile, productCatalogFile),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readConfiguredPath(value: string | undefined): string | undefined {
|
|
117
|
+
const normalized = value?.trim();
|
|
118
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readRuntimePlatformTarget(): RuntimePlatformTargetMetadata | undefined {
|
|
122
|
+
const targetsFile = readConfiguredPath(process.env.MPGD_PLATFORM_TARGETS_FILE);
|
|
123
|
+
const configTarget = readConfiguredPath(process.env.MPGD_CONFIG_TARGET);
|
|
124
|
+
|
|
125
|
+
if (targetsFile === undefined || configTarget === undefined) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const resolvedTargetsFile = resolve(targetsFile);
|
|
130
|
+
let parsed: unknown;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(readFileSync(resolvedTargetsFile, 'utf8'));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Failed to read or parse MPGD_PLATFORM_TARGETS_FILE at ${resolvedTargetsFile}: ${formatError(error)}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!isRecord(parsed) || !isRecord(parsed.targets)) {
|
|
141
|
+
throw new Error('MPGD_PLATFORM_TARGETS_FILE must contain a targets object.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const target = parsed.targets[configTarget];
|
|
145
|
+
|
|
146
|
+
if (!isRecord(target)) {
|
|
147
|
+
throw new Error(`Missing platform target metadata for ${configTarget}.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof target.kind !== 'string' || typeof target.adapter !== 'string') {
|
|
151
|
+
throw new Error(`Platform target ${configTarget} must define kind and adapter.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (target.integrations !== undefined && !isRecord(target.integrations)) {
|
|
155
|
+
throw new Error(`Platform target ${configTarget} integrations must be an object.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
kind: target.kind,
|
|
160
|
+
adapter: target.adapter,
|
|
161
|
+
...(target.integrations === undefined ? {} : { integrations: target.integrations }),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(input: unknown): input is Record<string, unknown> {
|
|
166
|
+
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatError(error: unknown): string {
|
|
170
|
+
return error instanceof Error ? error.message : String(error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveCatalogPath(path: string, pairedPath: string): string {
|
|
174
|
+
return resolve(resolveCatalogBaseDir(path, pairedPath), path);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveCatalogBaseDir(path: string, pairedPath: string): string {
|
|
178
|
+
const fallbackBaseDir = process.cwd();
|
|
179
|
+
const candidates = [
|
|
180
|
+
fallbackBaseDir,
|
|
181
|
+
readConfiguredPath(process.env.INIT_CWD),
|
|
182
|
+
readConfiguredPath(process.env.PWD),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
if (
|
|
187
|
+
candidate !== undefined
|
|
188
|
+
&& existsSync(resolve(candidate, path))
|
|
189
|
+
&& existsSync(resolve(candidate, pairedPath))
|
|
190
|
+
) {
|
|
191
|
+
return candidate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return fallbackBaseDir;
|
|
196
|
+
}
|