@mpgd/cli 0.21.0 → 0.22.1
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/index.d.ts +2 -1
- package/dist/index.js +92 -8
- package/dist/microsoft-store-package-generation-contract.d.ts +5 -0
- package/dist/microsoft-store-package-generation-input.js +55 -0
- package/dist/microsoft-store-starter.d.ts +7 -0
- package/dist/microsoft-store-starter.js +176 -4
- package/dist/microsoft-store-submission.d.ts +24 -3
- package/dist/microsoft-store-submission.js +131 -4
- package/dist/offline-playtest.d.ts +31 -0
- package/dist/offline-playtest.js +5718 -0
- package/dist/production-target-readiness.js +38 -19
- package/package.json +15 -13
- package/templates/phaser-game/README.md +57 -33
- package/templates/phaser-game/apps/target-ait/apps-in-toss.config.ts +12 -0
- package/templates/phaser-game/apps/target-ait/package.json +5 -8
- package/templates/phaser-game/apps/target-ait/vite.config.ts +6 -15
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +1 -1
- package/templates/phaser-game/apps/target-devvit/README.md +2 -1
- package/templates/phaser-game/apps/target-devvit/package.json +1 -1
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +1 -0
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +11 -0
- package/templates/phaser-game/mpgd.targets.json +8 -1
- package/templates/phaser-game/package.json +2 -5
- package/templates/phaser-game/pnpm-workspace.yaml +3 -2
- package/templates/phaser-game/src/entry.ts +10 -4
- package/templates/phaser-game/src/gameEntry.ts +7 -1
- package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +37 -0
- package/templates/phaser-game/src/platform/runtimeDetector.ts +1 -0
- package/templates/phaser-game/src/runtime/renderEntryFailure.ts +24 -0
- package/templates/phaser-game/vite.shared.ts +6 -2
|
@@ -41,6 +41,7 @@ export function runMicrosoftStoreSubmissionPreflight(input) {
|
|
|
41
41
|
const artifactRoot = readCanonicalDirectoryInside(gameRoot, input.artifactRoot, 'Microsoft Store artifact root');
|
|
42
42
|
const configFile = readCanonicalFileInside(gameRoot, input.configFile, 'Microsoft Store submission config');
|
|
43
43
|
const config = parseMicrosoftStoreSubmissionConfig(readJson(configFile, 'submission config'));
|
|
44
|
+
const effectiveTarget = readMicrosoftStoreEffectiveTarget(artifactRoot, config.commerce);
|
|
44
45
|
const manifestFile = readCanonicalFileInside(artifactRoot, path.join(artifactRoot, 'manifest.webmanifest'), 'Microsoft Store web app manifest');
|
|
45
46
|
const manifestSnapshot = readJsonSnapshot(manifestFile, 'web app manifest', 1024 * 1024);
|
|
46
47
|
const manifest = parseManifest(manifestSnapshot.value, artifactRoot);
|
|
@@ -48,6 +49,7 @@ export function runMicrosoftStoreSubmissionPreflight(input) {
|
|
|
48
49
|
const protectedFiles = [
|
|
49
50
|
{ file: configFile, label: 'Microsoft Store submission config' },
|
|
50
51
|
{ file: manifestFile, label: 'Microsoft Store web app manifest' },
|
|
52
|
+
{ file: effectiveTarget.file, label: 'Microsoft Store effective target config' },
|
|
51
53
|
...manifest.icons.map((icon) => ({
|
|
52
54
|
file: icon.file,
|
|
53
55
|
label: 'Microsoft Store web app manifest icon',
|
|
@@ -97,6 +99,10 @@ export function runMicrosoftStoreSubmissionPreflight(input) {
|
|
|
97
99
|
height: icon.height,
|
|
98
100
|
})),
|
|
99
101
|
},
|
|
102
|
+
effectiveTarget: {
|
|
103
|
+
file: relativeOrAbsolute(gameRoot, effectiveTarget.file),
|
|
104
|
+
sha256: effectiveTarget.sha256,
|
|
105
|
+
},
|
|
100
106
|
listing: {
|
|
101
107
|
category: config.listing.category,
|
|
102
108
|
supportUrl: config.listing.supportUrl,
|
|
@@ -117,6 +123,70 @@ export function runMicrosoftStoreSubmissionPreflight(input) {
|
|
|
117
123
|
writeFileSync(markdownFile, renderMicrosoftStoreSubmissionMarkdown(evidence));
|
|
118
124
|
return evidence;
|
|
119
125
|
}
|
|
126
|
+
function readMicrosoftStoreEffectiveTarget(artifactRoot, commerce) {
|
|
127
|
+
const file = readCanonicalFileInside(artifactRoot, path.join(artifactRoot, 'mpgd-effective-target.json'), 'Microsoft Store effective target config');
|
|
128
|
+
const snapshot = readJsonSnapshot(file, 'Microsoft Store effective target config', 4 * 1024 * 1024);
|
|
129
|
+
assertMicrosoftStoreEffectiveTarget(snapshot.value, commerce);
|
|
130
|
+
return { file, sha256: hashBytes(snapshot.bytes) };
|
|
131
|
+
}
|
|
132
|
+
export function assertMicrosoftStoreEffectiveTarget(input, commerce) {
|
|
133
|
+
const root = requireRecord(input, 'Microsoft Store effective target config');
|
|
134
|
+
if (root.target !== 'microsoft-store' || root.runtime !== 'microsoft-store-pwa') {
|
|
135
|
+
throw new Error('Microsoft Store effective target config must target the microsoft-store-pwa runtime.');
|
|
136
|
+
}
|
|
137
|
+
const monetization = requireRecord(root.monetization, 'Microsoft Store effective target config monetization');
|
|
138
|
+
const rawProducts = requireArray(monetization.products, 'Microsoft Store effective target config products');
|
|
139
|
+
if (commerce.mode === 'disabled') {
|
|
140
|
+
if (monetization.iap !== false) {
|
|
141
|
+
throw new Error('Microsoft Store effective target config must disable IAP when commerce mode is disabled.');
|
|
142
|
+
}
|
|
143
|
+
for (const [index, rawProduct] of rawProducts.entries()) {
|
|
144
|
+
const product = requireRecord(rawProduct, `Microsoft Store effective target config products[${String(index)}]`);
|
|
145
|
+
if (product.enabled !== false) {
|
|
146
|
+
throw new Error(`Microsoft Store effective target config products[${String(index)}] must be disabled when commerce mode is disabled.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (monetization.iap !== true) {
|
|
152
|
+
throw new Error('Microsoft Store effective target config must enable IAP.');
|
|
153
|
+
}
|
|
154
|
+
const seenProductIds = new Set();
|
|
155
|
+
const seenPlatformProductIds = new Set();
|
|
156
|
+
const products = rawProducts.flatMap((rawProduct, index) => {
|
|
157
|
+
const product = requireRecord(rawProduct, `Microsoft Store effective target config products[${String(index)}]`);
|
|
158
|
+
if (product.enabled !== true) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
if (product.type !== 'consumable') {
|
|
162
|
+
throw new Error('Microsoft Store effective target config can only enable consumables.');
|
|
163
|
+
}
|
|
164
|
+
const id = requireProductionString(product.id, `Microsoft Store effective target config products[${String(index)}].id`);
|
|
165
|
+
const platformProductId = requireProductionString(product.platformProductId, `Microsoft Store effective target config products[${String(index)}].platformProductId`);
|
|
166
|
+
if (seenProductIds.has(id)) {
|
|
167
|
+
throw new Error(`Microsoft Store effective target config duplicates product ${id}.`);
|
|
168
|
+
}
|
|
169
|
+
if (seenPlatformProductIds.has(platformProductId)) {
|
|
170
|
+
throw new Error(`Microsoft Store effective target config duplicates platformProductId ${platformProductId}.`);
|
|
171
|
+
}
|
|
172
|
+
seenProductIds.add(id);
|
|
173
|
+
seenPlatformProductIds.add(platformProductId);
|
|
174
|
+
return [{ id, platformProductId }];
|
|
175
|
+
});
|
|
176
|
+
if (products.length !== commerce.products.length) {
|
|
177
|
+
throw new Error('commerce.products must exactly match enabled Microsoft Store consumables in the built artifact.');
|
|
178
|
+
}
|
|
179
|
+
const productsById = new Map(products.map((product) => [product.id, product]));
|
|
180
|
+
for (const product of commerce.products) {
|
|
181
|
+
const effectiveProduct = productsById.get(product.logicalProductId);
|
|
182
|
+
if (effectiveProduct === undefined) {
|
|
183
|
+
throw new Error(`commerce product ${product.logicalProductId} is not present in the built artifact effective target.`);
|
|
184
|
+
}
|
|
185
|
+
if (effectiveProduct.platformProductId !== product.inAppOfferToken) {
|
|
186
|
+
throw new Error(`commerce product ${product.logicalProductId} InAppOfferToken ${product.inAppOfferToken} does not match built artifact platformProductId ${effectiveProduct.platformProductId}.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
120
190
|
export function parseMicrosoftStoreSubmissionConfig(input) {
|
|
121
191
|
const root = requireRecord(input, 'Microsoft Store submission config');
|
|
122
192
|
if (root.schemaVersion !== microsoftStoreSubmissionSchemaVersion) {
|
|
@@ -141,9 +211,7 @@ export function parseMicrosoftStoreSubmissionConfig(input) {
|
|
|
141
211
|
if (ageRating.questionnaireCompleted !== true) {
|
|
142
212
|
throw new Error('ageRating.questionnaireCompleted must be true before submission.');
|
|
143
213
|
}
|
|
144
|
-
|
|
145
|
-
throw new Error('commerce.mode must stay disabled until Microsoft Store commerce is backed by server-side ledger verification.');
|
|
146
|
-
}
|
|
214
|
+
const parsedCommerce = parseMicrosoftStoreSubmissionCommerce(commerce);
|
|
147
215
|
const iarcId = optionalProductionString(ageRating.iarcId, 'ageRating.iarcId');
|
|
148
216
|
return {
|
|
149
217
|
schemaVersion: microsoftStoreSubmissionSchemaVersion,
|
|
@@ -166,9 +234,56 @@ export function parseMicrosoftStoreSubmissionConfig(input) {
|
|
|
166
234
|
questionnaireCompleted: true,
|
|
167
235
|
...(iarcId === undefined ? {} : { iarcId }),
|
|
168
236
|
},
|
|
169
|
-
commerce:
|
|
237
|
+
commerce: parsedCommerce,
|
|
170
238
|
};
|
|
171
239
|
}
|
|
240
|
+
export function parseMicrosoftStoreSubmissionCommerce(input) {
|
|
241
|
+
if (input.mode === 'disabled') {
|
|
242
|
+
return { mode: 'disabled' };
|
|
243
|
+
}
|
|
244
|
+
if (input.mode !== 'microsoft-store') {
|
|
245
|
+
throw new Error('commerce.mode must be disabled or microsoft-store.');
|
|
246
|
+
}
|
|
247
|
+
if (input.productType !== 'developer-managed-consumable') {
|
|
248
|
+
throw new Error('commerce.productType must be developer-managed-consumable for Digital Goods purchases.');
|
|
249
|
+
}
|
|
250
|
+
if (input.fulfillment !== 'authoritative-server') {
|
|
251
|
+
throw new Error('commerce.fulfillment must be authoritative-server for server-side ledger verification.');
|
|
252
|
+
}
|
|
253
|
+
if (input.authoritativeGameServices !== true) {
|
|
254
|
+
throw new Error('commerce.authoritativeGameServices must be true.');
|
|
255
|
+
}
|
|
256
|
+
const rawProducts = requireArray(input.products, 'commerce.products');
|
|
257
|
+
if (rawProducts.length === 0) {
|
|
258
|
+
throw new Error('commerce.products must contain at least one product.');
|
|
259
|
+
}
|
|
260
|
+
const logicalProductIds = new Set();
|
|
261
|
+
const inAppOfferTokens = new Set();
|
|
262
|
+
const storeIds = new Set();
|
|
263
|
+
const products = rawProducts.map((rawProduct, index) => {
|
|
264
|
+
const product = requireRecord(rawProduct, `commerce.products[${String(index)}]`);
|
|
265
|
+
const logicalProductId = requireProductionString(product.logicalProductId, `commerce.products[${String(index)}].logicalProductId`);
|
|
266
|
+
const inAppOfferToken = requireProductionString(product.inAppOfferToken, `commerce.products[${String(index)}].inAppOfferToken`);
|
|
267
|
+
const storeId = requireProductionString(product.storeId, `commerce.products[${String(index)}].storeId`);
|
|
268
|
+
assertUniqueCommerceIdentifier(logicalProductIds, logicalProductId, 'logicalProductId');
|
|
269
|
+
assertUniqueCommerceIdentifier(inAppOfferTokens, inAppOfferToken, 'inAppOfferToken');
|
|
270
|
+
assertUniqueCommerceIdentifier(storeIds, storeId, 'storeId');
|
|
271
|
+
return { logicalProductId, inAppOfferToken, storeId };
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
mode: 'microsoft-store',
|
|
275
|
+
productType: 'developer-managed-consumable',
|
|
276
|
+
fulfillment: 'authoritative-server',
|
|
277
|
+
authoritativeGameServices: true,
|
|
278
|
+
products,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function assertUniqueCommerceIdentifier(seen, value, field) {
|
|
282
|
+
if (seen.has(value)) {
|
|
283
|
+
throw new Error(`commerce.products must use unique ${field} values; duplicate value "${value}" found.`);
|
|
284
|
+
}
|
|
285
|
+
seen.add(value);
|
|
286
|
+
}
|
|
172
287
|
export function renderMicrosoftStoreSubmissionMarkdown(evidence) {
|
|
173
288
|
const lines = [
|
|
174
289
|
'# Microsoft Store Submission Preflight',
|
|
@@ -178,11 +293,22 @@ export function renderMicrosoftStoreSubmissionMarkdown(evidence) {
|
|
|
178
293
|
`- Publisher ID: ${escapeMarkdownInline(evidence.productIdentity.publisherId)}`,
|
|
179
294
|
`- Reserved name: ${escapeMarkdownInline(evidence.productIdentity.reservedName)}`,
|
|
180
295
|
`- Manifest: ${escapeMarkdownInline(evidence.manifest.file)} (${evidence.manifest.sha256})`,
|
|
296
|
+
`- Effective target: ${escapeMarkdownInline(evidence.effectiveTarget.file)} (${evidence.effectiveTarget.sha256})`,
|
|
181
297
|
`- Manifest icons: ${evidence.manifest.iconCount}`,
|
|
182
298
|
`- Commerce: ${evidence.commerce.mode}`,
|
|
183
299
|
`- Personal data accessed or transmitted: ${String(evidence.listing.personalData.accessedOrTransmitted)}`,
|
|
184
300
|
`- Privacy policy: ${escapeMarkdownInline(evidence.listing.personalData.privacyPolicyUrl ?? 'Not required')}`,
|
|
185
301
|
'',
|
|
302
|
+
...(evidence.commerce.mode === 'microsoft-store'
|
|
303
|
+
? [
|
|
304
|
+
'## Commerce Products',
|
|
305
|
+
'',
|
|
306
|
+
'| Logical product | InAppOfferToken | Store ID |',
|
|
307
|
+
'| --- | --- | --- |',
|
|
308
|
+
...evidence.commerce.products.map((product) => (`| ${escapeMarkdownTable(product.logicalProductId)} | ${escapeMarkdownTable(product.inAppOfferToken)} | ${escapeMarkdownTable(product.storeId)} |`)),
|
|
309
|
+
'',
|
|
310
|
+
]
|
|
311
|
+
: []),
|
|
186
312
|
'## Store Listings',
|
|
187
313
|
'',
|
|
188
314
|
'| Locale | Screenshots | Description |',
|
|
@@ -495,6 +621,7 @@ function requireProductionString(input, label) {
|
|
|
495
621
|
const exactTemplateToken = /^(?:TODO|FIXME|PLACEHOLDER|DUMMY|(?:CHANGE|REPLACE)[-_ ]?ME|LOREM(?:_IPSUM)?)$/iu;
|
|
496
622
|
const templateIdentityField = label.startsWith('productIdentity.')
|
|
497
623
|
|| label.startsWith('web app manifest')
|
|
624
|
+
|| label.startsWith('commerce.products[')
|
|
498
625
|
|| label === 'ageRating.iarcId';
|
|
499
626
|
if (exactTemplateToken.test(value)
|
|
500
627
|
|| (templateIdentityField
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const defaultOfflinePlaytestArtifactDir = "artifacts/web-preview";
|
|
2
|
+
export declare const defaultOfflinePlaytestOutputDir = "artifacts/offline-playtest";
|
|
3
|
+
export declare const defaultOfflinePlaytestMaximumBytes: number;
|
|
4
|
+
export declare const offlinePlaytestSchemaVersion = "1";
|
|
5
|
+
export interface RunOfflinePlaytestPackagingInput {
|
|
6
|
+
readonly gameRoot: string;
|
|
7
|
+
readonly artifactDir?: string;
|
|
8
|
+
readonly outputDir?: string;
|
|
9
|
+
readonly maximumBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface OfflinePlaytestEvidence {
|
|
12
|
+
readonly schemaVersion: typeof offlinePlaytestSchemaVersion;
|
|
13
|
+
readonly purpose: 'test-play-only';
|
|
14
|
+
readonly releaseTarget: false;
|
|
15
|
+
readonly sourceTarget: 'web-preview';
|
|
16
|
+
readonly sourceRuntime: 'web-preview';
|
|
17
|
+
readonly networkPolicy: 'deny-network';
|
|
18
|
+
readonly entryFile: 'index.html';
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
readonly sha256: string;
|
|
21
|
+
readonly inlinedAssetCount: number;
|
|
22
|
+
readonly limitations: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface RunOfflinePlaytestPackagingResult {
|
|
25
|
+
readonly outputDir: string;
|
|
26
|
+
readonly entryFile: string;
|
|
27
|
+
readonly readmeFile: string;
|
|
28
|
+
readonly evidenceFile: string;
|
|
29
|
+
readonly evidence: OfflinePlaytestEvidence;
|
|
30
|
+
}
|
|
31
|
+
export declare function runOfflinePlaytestPackaging(input: RunOfflinePlaytestPackagingInput): Promise<RunOfflinePlaytestPackagingResult>;
|