@mpgd/cli 0.47.0 → 0.49.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/command-launcher.d.ts +13 -0
- package/dist/command-launcher.js +24 -0
- package/dist/devvit-output-collisions.d.ts +49 -0
- package/dist/devvit-output-collisions.js +120 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +186 -10
- package/dist/microsoft-store-submission.js +11 -6
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +2134 -351
- package/dist/tooling-migration.d.ts +24 -0
- package/dist/tooling-migration.js +497 -0
- package/dist/windows-host/Core/BridgeCommand.cs +17 -0
- package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
- package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
- package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
- package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
- package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
- package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
- package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
- package/dist/windows-host/README.md +118 -0
- package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
- package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
- package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
- package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
- package/dist/windows-native-tools.d.ts +4 -0
- package/dist/windows-native-tools.js +21 -0
- package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
- package/package.json +30 -18
- package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
- package/templates/lite-overlay/AGENTS.md +32 -0
- package/templates/lite-overlay/README.md +37 -0
- package/templates/lite-overlay/agent/acceptance.md +27 -0
- package/templates/lite-overlay/agent/brief.md +17 -0
- package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
- package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
- package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
- package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
- package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
- package/templates/phaser-game/AGENTS.md +5 -0
- package/templates/phaser-game/README.md +5 -3
- package/templates/phaser-game/apps/target-ait/package.json +4 -4
- package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
- package/templates/phaser-game/apps/target-devvit/vite.config.ts +2 -0
- package/templates/phaser-game/package.json +8 -8
- package/templates/phaser-game/pnpm-workspace.yaml +2 -0
- package/templates/phaser-game/src/env.d.ts +2 -0
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
- package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
- package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
- package/templates/phaser-game/tools/{run-game-acceptance.mjs → run-game-acceptance.ts} +1 -8
- package/templates/phaser-game/tsconfig.tools.json +12 -0
- package/templates/phaser-game/vite.shared.ts +40 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface NativeCommandLaunch {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
readonly shell?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Launch pnpm directly; Windows .cmd shims require a shell and lose argument boundaries. */
|
|
7
|
+
export declare function resolveNativeCommandLaunch(input: {
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly args: readonly string[];
|
|
10
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
11
|
+
readonly platform?: NodeJS.Platform;
|
|
12
|
+
readonly nodeExecutable?: string;
|
|
13
|
+
}): NativeCommandLaunch;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Launch pnpm directly; Windows .cmd shims require a shell and lose argument boundaries. */
|
|
2
|
+
export function resolveNativeCommandLaunch(input) {
|
|
3
|
+
if ((input.platform ?? process.platform) !== 'win32') {
|
|
4
|
+
return { command: input.command, args: input.args };
|
|
5
|
+
}
|
|
6
|
+
if (input.command === './gradlew') {
|
|
7
|
+
return { command: 'gradlew.bat', args: input.args, shell: true };
|
|
8
|
+
}
|
|
9
|
+
if (input.command !== 'pnpm') {
|
|
10
|
+
return { command: input.command, args: input.args };
|
|
11
|
+
}
|
|
12
|
+
const pnpmEntry = input.environment.npm_execpath;
|
|
13
|
+
const pnpmBase = pnpmEntry?.split(/[\\/]/u).at(-1) ?? '';
|
|
14
|
+
if (pnpmEntry === undefined || !/^pnpm(?:\.exe|\.[cm]?js)$/iu.test(pnpmBase)) {
|
|
15
|
+
throw new Error('Windows builds require pnpm exec mpgd so npm_execpath resolves the pnpm executable or JavaScript entrypoint.');
|
|
16
|
+
}
|
|
17
|
+
if (/\.exe$/iu.test(pnpmEntry)) {
|
|
18
|
+
return { command: pnpmEntry, args: input.args };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
command: input.nodeExecutable ?? process.execPath,
|
|
22
|
+
args: [pnpmEntry, ...input.args],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One output path written by more than one source in a Devvit web build.
|
|
3
|
+
*
|
|
4
|
+
* `public` means a file from a public directory was silently replaced by a
|
|
5
|
+
* Vite-emitted file of the same name (Vite copies `publicDir` first and then
|
|
6
|
+
* writes the bundle over it). `case` means two output files differ only by
|
|
7
|
+
* letter case. The build plugin checks bundle names before writing so this
|
|
8
|
+
* also works on case-insensitive filesystems.
|
|
9
|
+
*/
|
|
10
|
+
export interface DevvitOutputCollision {
|
|
11
|
+
readonly kind: 'public' | 'case';
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly sources: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Validate logical output names before the filesystem can collapse them. */
|
|
16
|
+
export declare function assertDevvitOutputNames(bundleNames: readonly string[], publicNames: readonly string[], outDir: string): void;
|
|
17
|
+
/** Shared Vite 7/8 guard for production and staging watch builds. */
|
|
18
|
+
export declare function devvitOutputCollisionPlugin(): {
|
|
19
|
+
name: string;
|
|
20
|
+
apply: "build";
|
|
21
|
+
enforce: "post";
|
|
22
|
+
configResolved(config: {
|
|
23
|
+
publicDir: string;
|
|
24
|
+
}): void;
|
|
25
|
+
generateBundle: {
|
|
26
|
+
order: "post";
|
|
27
|
+
handler(this: {
|
|
28
|
+
environment: {
|
|
29
|
+
config: {
|
|
30
|
+
build: {
|
|
31
|
+
copyPublicDir: boolean;
|
|
32
|
+
outDir: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}, options: {
|
|
37
|
+
dir?: string | undefined;
|
|
38
|
+
}, bundle: Record<string, {
|
|
39
|
+
fileName: string;
|
|
40
|
+
}>): void;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Finds output files of a Devvit client build that were overwritten or that
|
|
45
|
+
* collide by case. `publicDirs` are the directories Vite copied verbatim.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findDevvitOutputCollisions(outDir: string, publicDirs: readonly string[]): DevvitOutputCollision[];
|
|
48
|
+
/** Throws a single readable error listing every collision. */
|
|
49
|
+
export declare function assertNoDevvitOutputCollisions(outDir: string, publicDirs: readonly string[]): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
/** Validate logical output names before the filesystem can collapse them. */
|
|
4
|
+
export function assertDevvitOutputNames(bundleNames, publicNames, outDir) {
|
|
5
|
+
const collisions = [];
|
|
6
|
+
const names = new Map();
|
|
7
|
+
for (const name of [...publicNames, ...bundleNames]) {
|
|
8
|
+
const previous = names.get(name.toLowerCase());
|
|
9
|
+
if (previous !== undefined) {
|
|
10
|
+
collisions.push({
|
|
11
|
+
kind: previous === name ? 'public' : 'case',
|
|
12
|
+
path: name,
|
|
13
|
+
sources: [previous, name],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
names.set(name.toLowerCase(), name);
|
|
17
|
+
}
|
|
18
|
+
throwCollisions(collisions, outDir);
|
|
19
|
+
}
|
|
20
|
+
/** Shared Vite 7/8 guard for production and staging watch builds. */
|
|
21
|
+
export function devvitOutputCollisionPlugin() {
|
|
22
|
+
let publicDir = '';
|
|
23
|
+
return {
|
|
24
|
+
name: 'mpgd-devvit-output-collisions',
|
|
25
|
+
apply: 'build',
|
|
26
|
+
enforce: 'post',
|
|
27
|
+
configResolved(config) {
|
|
28
|
+
publicDir = config.publicDir;
|
|
29
|
+
},
|
|
30
|
+
generateBundle: {
|
|
31
|
+
order: 'post',
|
|
32
|
+
handler(options, bundle) {
|
|
33
|
+
const publicNames = this.environment.config.build.copyPublicDir
|
|
34
|
+
&& publicDir !== '' && existsSync(publicDir) ? listFiles(publicDir) : [];
|
|
35
|
+
assertDevvitOutputNames(Object.values(bundle).map((output) => output.fileName), publicNames, options.dir ?? this.environment.config.build.outDir);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function listFiles(root) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const pending = [root];
|
|
43
|
+
while (pending.length > 0) {
|
|
44
|
+
const directory = pending.pop();
|
|
45
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
46
|
+
const absolute = join(directory, entry.name);
|
|
47
|
+
// Follow symlinks: a linked directory is traversed, a linked file listed.
|
|
48
|
+
const stats = entry.isSymbolicLink() ? statSync(absolute, { throwIfNoEntry: false }) : entry;
|
|
49
|
+
if (stats === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (stats.isDirectory()) {
|
|
53
|
+
pending.push(absolute);
|
|
54
|
+
}
|
|
55
|
+
else if (stats.isFile()) {
|
|
56
|
+
out.push(relative(root, absolute).split(sep).join('/'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out.sort();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Finds output files of a Devvit client build that were overwritten or that
|
|
64
|
+
* collide by case. `publicDirs` are the directories Vite copied verbatim.
|
|
65
|
+
*/
|
|
66
|
+
export function findDevvitOutputCollisions(outDir, publicDirs) {
|
|
67
|
+
if (!existsSync(outDir)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const outputs = listFiles(outDir);
|
|
71
|
+
const collisions = [];
|
|
72
|
+
for (const publicDir of publicDirs) {
|
|
73
|
+
if (!existsSync(publicDir)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const file of listFiles(publicDir)) {
|
|
77
|
+
const output = join(outDir, file);
|
|
78
|
+
if (!existsSync(output)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!readFileSync(output).equals(readFileSync(join(publicDir, file)))) {
|
|
82
|
+
collisions.push({
|
|
83
|
+
kind: 'public',
|
|
84
|
+
path: file,
|
|
85
|
+
sources: [join(publicDir, file), 'vite bundle output'],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const byLowerCase = new Map();
|
|
91
|
+
for (const file of outputs) {
|
|
92
|
+
const key = file.toLowerCase();
|
|
93
|
+
byLowerCase.set(key, [...(byLowerCase.get(key) ?? []), file]);
|
|
94
|
+
}
|
|
95
|
+
for (const files of byLowerCase.values()) {
|
|
96
|
+
if (files.length > 1) {
|
|
97
|
+
collisions.push({ kind: 'case', path: files[0], sources: files });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return collisions;
|
|
101
|
+
}
|
|
102
|
+
/** Throws a single readable error listing every collision. */
|
|
103
|
+
export function assertNoDevvitOutputCollisions(outDir, publicDirs) {
|
|
104
|
+
const collisions = findDevvitOutputCollisions(outDir, publicDirs);
|
|
105
|
+
throwCollisions(collisions, outDir);
|
|
106
|
+
}
|
|
107
|
+
function throwCollisions(collisions, outDir) {
|
|
108
|
+
if (collisions.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const lines = collisions.map((collision) => collision.kind === 'public'
|
|
112
|
+
? ` - ${collision.path}: public and bundle files use the same output name`
|
|
113
|
+
: ` - ${collision.sources.join(', ')}: names differ only by case`);
|
|
114
|
+
throw new Error([
|
|
115
|
+
`Devvit build output has ${collisions.length} conflicting file name(s) in ${outDir}:`,
|
|
116
|
+
...lines,
|
|
117
|
+
'Rename the public file or the entry/chunk (Devvit client output uses unhashed [name].js names),'
|
|
118
|
+
+ ' then rebuild.',
|
|
119
|
+
].join('\n'));
|
|
120
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupporte
|
|
|
15
15
|
export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, type OfflinePlaytestEvidence, type RunOfflinePlaytestPackagingInput, type RunOfflinePlaytestPackagingResult, } from './offline-playtest.js';
|
|
16
16
|
export { assertMicrosoftStorePwaCacheTransition, assertMicrosoftStorePwaPrecacheUrl, assertMicrosoftStorePwaReleaseEvidence, inspectMicrosoftStorePwaBrowserCacheTransition, microsoftStorePwaCacheSchema, readMicrosoftStorePwaBrowserReleaseEvidence, requestMicrosoftStorePwaBrowserUpdate, resolveMicrosoftStorePwaScopedCacheName, type AssertMicrosoftStorePwaCacheTransitionInput, type InspectMicrosoftStorePwaCacheTransitionInput, type MicrosoftStorePwaBrowserPage, type MicrosoftStorePwaCachedIndexObservation, type MicrosoftStorePwaCacheTransitionEvidence, type MicrosoftStorePwaCacheTransitionObservation, type MicrosoftStorePwaReleaseEvidence, } from './microsoft-store-pwa-e2e.js';
|
|
17
17
|
export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, type CreateMicrosoftStorePackageAcceptanceRuntimeInput, type MicrosoftStorePackageAcceptanceEvidence, type MicrosoftStorePackageAcceptanceRuntime, type MicrosoftStorePackageIdentity, type RunMicrosoftStorePackageAcceptanceInput, } from './microsoft-store-package-acceptance.js';
|
|
18
|
+
export { getMicrosoftStoreNativeAcceptanceModulePath, getMicrosoftStoreNativeHostSourceDirectory, } from './windows-native-tools.js';
|
|
18
19
|
export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress, type RunMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation.js';
|
|
19
20
|
export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, type MicrosoftStoreSubmissionConfig, type MicrosoftStoreSubmissionCommerce, type MicrosoftStoreSubmissionCommerceProduct, type MicrosoftStoreSubmissionEvidence, type RunMicrosoftStoreSubmissionPreflightInput, } from './microsoft-store-submission.js';
|
|
20
21
|
export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
|
7
7
|
import resources from '@gunshi/resources';
|
|
8
8
|
import { cli } from 'gunshi';
|
|
9
9
|
import { assertPlatformVersionLedger } from '@mpgd/target-config';
|
|
10
|
+
import { resolveNativeCommandLaunch } from './command-launcher.js';
|
|
10
11
|
import { buildAssetPacks } from './asset-pack-build.js';
|
|
11
12
|
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
12
13
|
import { buildAssetPackTarget } from './asset-pack-target.js';
|
|
@@ -24,6 +25,7 @@ import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageA
|
|
|
24
25
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
25
26
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
26
27
|
import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
|
|
28
|
+
import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
|
|
27
29
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
28
30
|
import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
29
31
|
import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
|
|
@@ -47,6 +49,7 @@ export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupporte
|
|
|
47
49
|
export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
48
50
|
export { assertMicrosoftStorePwaCacheTransition, assertMicrosoftStorePwaPrecacheUrl, assertMicrosoftStorePwaReleaseEvidence, inspectMicrosoftStorePwaBrowserCacheTransition, microsoftStorePwaCacheSchema, readMicrosoftStorePwaBrowserReleaseEvidence, requestMicrosoftStorePwaBrowserUpdate, resolveMicrosoftStorePwaScopedCacheName, } from './microsoft-store-pwa-e2e.js';
|
|
49
51
|
export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
52
|
+
export { getMicrosoftStoreNativeAcceptanceModulePath, getMicrosoftStoreNativeHostSourceDirectory, } from './windows-native-tools.js';
|
|
50
53
|
export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, } from './microsoft-store-package-generation.js';
|
|
51
54
|
export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, } from './microsoft-store-submission.js';
|
|
52
55
|
export { assertProductionTargetReadiness, } from './production-target-readiness.js';
|
|
@@ -54,6 +57,18 @@ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
54
57
|
const packageRoot = resolvePackageRoot(sourceDir);
|
|
55
58
|
const detectedKitRoot = resolveDefaultKitRoot();
|
|
56
59
|
const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
|
|
60
|
+
const liteTemplateOverlayDir = path.resolve(packageRoot, 'templates/lite-overlay');
|
|
61
|
+
const supportedGameTemplates = ['phaser', 'lite'];
|
|
62
|
+
/** Lite renderers implemented today. dom/svg are reserved extension points in @mpgd/lite-shell. */
|
|
63
|
+
const supportedLiteRenderers = ['canvas'];
|
|
64
|
+
/** Phaser-only base template files that the lite template does not emit. */
|
|
65
|
+
const liteTemplateRemovedFiles = new Set([
|
|
66
|
+
'src/scenes/BootScene.ts',
|
|
67
|
+
'src/scenes/LobbyScene.ts',
|
|
68
|
+
'src/scenes/PlayScene.ts',
|
|
69
|
+
'src/assets/manifest.ts',
|
|
70
|
+
'src/assets/marker.svg',
|
|
71
|
+
]);
|
|
57
72
|
const cliVersion = readPackageVersion(packageRoot);
|
|
58
73
|
const recommendedMatrixTargets = 'web,microsoft-store,verse8,ait,reddit';
|
|
59
74
|
const recommendedMatrixTargetsWithoutMicrosoftStore = recommendedMatrixTargets
|
|
@@ -80,6 +95,7 @@ const mpgdTemplateDependencyPackages = [
|
|
|
80
95
|
{ name: '@mpgd/game-services', packageDir: 'packages/game-services' },
|
|
81
96
|
{ name: '@mpgd/game-runtime', packageDir: 'packages/game-runtime' },
|
|
82
97
|
{ name: '@mpgd/i18n', packageDir: 'packages/i18n' },
|
|
98
|
+
{ name: '@mpgd/lite-shell', packageDir: 'packages/lite-shell' },
|
|
83
99
|
{ name: '@mpgd/phaser-assets', packageDir: 'packages/phaser-assets' },
|
|
84
100
|
{ name: '@mpgd/platform', packageDir: 'packages/platform' },
|
|
85
101
|
{ name: '@mpgd/target-config', packageDir: 'packages/target-config' },
|
|
@@ -162,7 +178,7 @@ export async function runMpgdCli(args) {
|
|
|
162
178
|
// partial document. Every other command keeps the default rendering.
|
|
163
179
|
renderHeader: async (ctx) => {
|
|
164
180
|
const values = ctx.values;
|
|
165
|
-
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
|
|
181
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade' || ctx.name === 'migrate-tools') && values.json === true) {
|
|
166
182
|
return '';
|
|
167
183
|
}
|
|
168
184
|
const title = ctx.env.description || ctx.env.name || '';
|
|
@@ -173,7 +189,7 @@ export async function runMpgdCli(args) {
|
|
|
173
189
|
renderValidationErrors: async (ctx, error) => {
|
|
174
190
|
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
175
191
|
const values = ctx.values;
|
|
176
|
-
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
|
|
192
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade' || ctx.name === 'migrate-tools') && values.json === true) {
|
|
177
193
|
process.stderr.write(`${messages}\n`);
|
|
178
194
|
return '';
|
|
179
195
|
}
|
|
@@ -551,8 +567,26 @@ const gameCommand = defineI18n({
|
|
|
551
567
|
en: 'Include the Microsoft Store PWA target and release workflow.',
|
|
552
568
|
ko: 'Microsoft Store PWA 타깃과 출시 워크플로를 포함합니다.',
|
|
553
569
|
},
|
|
570
|
+
template: {
|
|
571
|
+
en: 'Game template: phaser (default) or lite (no Phaser, @mpgd/lite-shell).',
|
|
572
|
+
ko: '게임 템플릿: phaser(기본) 또는 lite(Phaser 없음, @mpgd/lite-shell).',
|
|
573
|
+
},
|
|
574
|
+
renderer: {
|
|
575
|
+
en: 'Lite template renderer. Only canvas is implemented today.',
|
|
576
|
+
ko: 'lite 템플릿 렌더러. 현재는 canvas만 지원합니다.',
|
|
577
|
+
},
|
|
554
578
|
}),
|
|
555
579
|
args: {
|
|
580
|
+
template: {
|
|
581
|
+
type: 'string',
|
|
582
|
+
required: false,
|
|
583
|
+
description: 'Game template: phaser (default) or lite.',
|
|
584
|
+
},
|
|
585
|
+
renderer: {
|
|
586
|
+
type: 'string',
|
|
587
|
+
required: false,
|
|
588
|
+
description: 'Lite template renderer: canvas (default).',
|
|
589
|
+
},
|
|
556
590
|
directory: {
|
|
557
591
|
type: 'positional',
|
|
558
592
|
required: true,
|
|
@@ -603,7 +637,10 @@ const gameCommand = defineI18n({
|
|
|
603
637
|
? 'workspace:*'
|
|
604
638
|
: readOptionalString(ctx.values['dependency-version']);
|
|
605
639
|
const kitPath = readGameCreateKitPath(ctx.values, ctx.values.workspace === true);
|
|
640
|
+
const { template, renderer } = readGameCreateTemplate(readOptionalString(ctx.values.template), readOptionalString(ctx.values.renderer));
|
|
606
641
|
createGameApp({
|
|
642
|
+
template,
|
|
643
|
+
...(renderer === undefined ? {} : { renderer }),
|
|
607
644
|
directory,
|
|
608
645
|
...(title === undefined ? {} : { title }),
|
|
609
646
|
...(packageName === undefined ? {} : { packageName }),
|
|
@@ -2540,6 +2577,58 @@ const kitCommand = defineI18n({
|
|
|
2540
2577
|
}
|
|
2541
2578
|
},
|
|
2542
2579
|
}),
|
|
2580
|
+
'migrate-tools': defineI18n({
|
|
2581
|
+
name: 'migrate-tools',
|
|
2582
|
+
description: 'Plan or apply the TypeScript tools migration for an existing generated game.',
|
|
2583
|
+
resource: commandResource({
|
|
2584
|
+
base: {
|
|
2585
|
+
en: 'Plan or apply the TypeScript tools migration for an existing generated game.',
|
|
2586
|
+
ko: '기존 생성 게임의 TypeScript 도구 이전을 계획하거나 적용합니다.',
|
|
2587
|
+
},
|
|
2588
|
+
args: {
|
|
2589
|
+
game: { en: 'Existing generated game directory.', ko: '기존 생성 게임 디렉터리.' },
|
|
2590
|
+
apply: { en: 'Apply the reviewed local migration plan.', ko: '검토한 로컬 이전 계획을 적용합니다.' },
|
|
2591
|
+
json: { en: 'Print the complete plan as JSON.', ko: '전체 계획을 JSON으로 출력합니다.' },
|
|
2592
|
+
},
|
|
2593
|
+
}),
|
|
2594
|
+
args: {
|
|
2595
|
+
game: { type: 'string', required: false, default: '.', description: 'Game directory.' },
|
|
2596
|
+
apply: { type: 'boolean', required: false, description: 'Apply the local plan.' },
|
|
2597
|
+
json: { type: 'boolean', required: false, description: 'Print JSON.' },
|
|
2598
|
+
},
|
|
2599
|
+
run: (ctx) => {
|
|
2600
|
+
const plan = planToolingMigration(readOptionalString(ctx.values.game) ?? '.', gameTemplateDir);
|
|
2601
|
+
const result = ctx.values.apply === true && plan.blockers.length === 0
|
|
2602
|
+
? applyToolingMigration(plan) : undefined;
|
|
2603
|
+
if (ctx.values.json === true) {
|
|
2604
|
+
process.stdout.write(JSON.stringify({ plan, result: result ?? null }, null, 2) + '\n');
|
|
2605
|
+
}
|
|
2606
|
+
else {
|
|
2607
|
+
console.info('TypeScript tooling plan for ' + plan.gameRoot);
|
|
2608
|
+
for (const edit of plan.edits) {
|
|
2609
|
+
console.info(' ' + (edit.after === null ? 'Remove ' : 'Write ') + edit.file);
|
|
2610
|
+
}
|
|
2611
|
+
for (const note of plan.notes) {
|
|
2612
|
+
console.info('Note: ' + note);
|
|
2613
|
+
}
|
|
2614
|
+
for (const blocker of plan.blockers) {
|
|
2615
|
+
console.error('Blocked: ' + blocker);
|
|
2616
|
+
}
|
|
2617
|
+
if (result !== undefined) {
|
|
2618
|
+
console.info('Applied ' + result.length + ' file change(s).');
|
|
2619
|
+
}
|
|
2620
|
+
else if (!plan.edits.length && !plan.blockers.length) {
|
|
2621
|
+
console.info('TypeScript tooling is already current.');
|
|
2622
|
+
}
|
|
2623
|
+
else if (!plan.blockers.length) {
|
|
2624
|
+
console.info('Run again with --apply after reviewing the plan.');
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (plan.blockers.length) {
|
|
2628
|
+
process.exitCode = 1;
|
|
2629
|
+
}
|
|
2630
|
+
},
|
|
2631
|
+
}),
|
|
2543
2632
|
doctor: defineI18n({
|
|
2544
2633
|
name: 'doctor',
|
|
2545
2634
|
description: 'Print CLI, kit, template, and target-wrapper status.',
|
|
@@ -2558,7 +2647,7 @@ const kitCommand = defineI18n({
|
|
|
2558
2647
|
}),
|
|
2559
2648
|
},
|
|
2560
2649
|
run: () => {
|
|
2561
|
-
console.info('Use "mpgd kit doctor" or "mpgd kit
|
|
2650
|
+
console.info('Use "mpgd kit doctor", "mpgd kit upgrade --game <directory> [--apply]", or "mpgd kit migrate-tools --game <directory> [--apply]".');
|
|
2562
2651
|
},
|
|
2563
2652
|
});
|
|
2564
2653
|
function commandResource(description, args = {}) {
|
|
@@ -2755,9 +2844,88 @@ function normalizeLegalHtml(content, sourceFile) {
|
|
|
2755
2844
|
}
|
|
2756
2845
|
return content.endsWith('\n') ? content : `${content}\n`;
|
|
2757
2846
|
}
|
|
2847
|
+
function readGameCreateTemplate(template, renderer) {
|
|
2848
|
+
const resolvedTemplate = template ?? 'phaser';
|
|
2849
|
+
if (!supportedGameTemplates.includes(resolvedTemplate)) {
|
|
2850
|
+
throw new Error(`Unsupported --template ${resolvedTemplate}; expected one of: ${supportedGameTemplates.join(', ')}.`);
|
|
2851
|
+
}
|
|
2852
|
+
if (resolvedTemplate !== 'lite') {
|
|
2853
|
+
if (renderer !== undefined) {
|
|
2854
|
+
throw new Error('--renderer is only available with --template lite.');
|
|
2855
|
+
}
|
|
2856
|
+
return { template: resolvedTemplate };
|
|
2857
|
+
}
|
|
2858
|
+
const resolvedRenderer = renderer ?? 'canvas';
|
|
2859
|
+
if (!supportedLiteRenderers.includes(resolvedRenderer)) {
|
|
2860
|
+
throw new Error(`Unsupported --renderer ${resolvedRenderer}; implemented lite renderers: ${supportedLiteRenderers.join(', ')}. `
|
|
2861
|
+
+ 'dom and svg are planned LiteRenderer implementations.');
|
|
2862
|
+
}
|
|
2863
|
+
return { template: 'lite', renderer: resolvedRenderer };
|
|
2864
|
+
}
|
|
2865
|
+
/** Apply the lite overlay to base template files: drop Phaser-only files and swap renderers. */
|
|
2866
|
+
function applyLiteTemplateOverlay(baseFiles, overlayFiles) {
|
|
2867
|
+
const overlay = new Map(overlayFiles.map((file) => [file.relativePath, file]));
|
|
2868
|
+
const merged = baseFiles.flatMap((file) => {
|
|
2869
|
+
if (liteTemplateRemovedFiles.has(file.relativePath)) {
|
|
2870
|
+
return [];
|
|
2871
|
+
}
|
|
2872
|
+
const replacement = overlay.get(file.relativePath);
|
|
2873
|
+
overlay.delete(file.relativePath);
|
|
2874
|
+
if (replacement !== undefined) {
|
|
2875
|
+
return [replacement];
|
|
2876
|
+
}
|
|
2877
|
+
if (file.relativePath === 'package.json') {
|
|
2878
|
+
return [{ ...file, content: liteTemplatePackageJson(file.content) }];
|
|
2879
|
+
}
|
|
2880
|
+
if (file.relativePath === 'agent/game-manifest.json') {
|
|
2881
|
+
return [{ ...file, content: liteTemplateGameManifest(file.content) }];
|
|
2882
|
+
}
|
|
2883
|
+
if (file.relativePath === 'mpgd.targets.json') {
|
|
2884
|
+
return [{ ...file, content: liteTemplateTargets(file.content) }];
|
|
2885
|
+
}
|
|
2886
|
+
return [file];
|
|
2887
|
+
});
|
|
2888
|
+
return [...merged, ...overlay.values()];
|
|
2889
|
+
}
|
|
2890
|
+
function liteTemplateTargets(content) {
|
|
2891
|
+
const value = JSON.parse(content);
|
|
2892
|
+
const reddit = value.targets.reddit;
|
|
2893
|
+
if (reddit !== undefined) {
|
|
2894
|
+
reddit.inlineEntry = 'index.html';
|
|
2895
|
+
reddit.inlineBudget = {
|
|
2896
|
+
maxGzipKb: 40,
|
|
2897
|
+
forbidModules: ['phaser', 'matter-js'],
|
|
2898
|
+
deferredChunks: ['main.js', 'matter.js', 'assets/main-*.js', 'assets/matter-*.js'],
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2902
|
+
}
|
|
2903
|
+
function liteTemplatePackageJson(content) {
|
|
2904
|
+
const value = JSON.parse(content);
|
|
2905
|
+
value.description = (value.description ?? '').replace('mpgd Phaser game', 'mpgd lite (Canvas2D) game');
|
|
2906
|
+
const dependencies = { ...value.dependencies };
|
|
2907
|
+
delete dependencies.phaser;
|
|
2908
|
+
delete dependencies['@mpgd/phaser-assets'];
|
|
2909
|
+
dependencies['@mpgd/lite-shell'] = '__MPGD_DEPENDENCY_VERSION_LITE_SHELL__';
|
|
2910
|
+
value.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
|
|
2911
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2912
|
+
}
|
|
2913
|
+
function liteTemplateGameManifest(content) {
|
|
2914
|
+
const value = JSON.parse(content);
|
|
2915
|
+
if (value.game !== undefined) {
|
|
2916
|
+
value.game.engine = 'lite-canvas';
|
|
2917
|
+
}
|
|
2918
|
+
value.blocks = (value.blocks ?? []).map((block) => block.id === 'assets.manifest.phaser-loader'
|
|
2919
|
+
? { id: 'render.lite-shell.canvas', owner: 'src/lite/tapGame.ts' }
|
|
2920
|
+
: block);
|
|
2921
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2922
|
+
}
|
|
2758
2923
|
function createGameApp(input) {
|
|
2759
2924
|
const appDir = resolveAppDirectory(input.directory);
|
|
2760
2925
|
const gameName = path.basename(appDir);
|
|
2926
|
+
if (input.template === 'lite' && !input.workspace) {
|
|
2927
|
+
throw new Error('The lite template requires --workspace until @mpgd/lite-shell is published.');
|
|
2928
|
+
}
|
|
2761
2929
|
assertValidGameName(gameName);
|
|
2762
2930
|
const packageName = input.packageName ?? gameName;
|
|
2763
2931
|
assertValidPackageName(packageName);
|
|
@@ -2777,7 +2945,11 @@ function createGameApp(input) {
|
|
|
2777
2945
|
...(input.title === undefined ? {} : { title: input.title }),
|
|
2778
2946
|
microsoftStore: input.microsoftStore,
|
|
2779
2947
|
});
|
|
2780
|
-
const
|
|
2948
|
+
const baseTemplateFiles = collectTemplateFiles(gameTemplateDir);
|
|
2949
|
+
const selectedTemplateFiles = input.template === 'lite'
|
|
2950
|
+
? applyLiteTemplateOverlay(baseTemplateFiles, collectTemplateFiles(liteTemplateOverlayDir))
|
|
2951
|
+
: baseTemplateFiles;
|
|
2952
|
+
const files = selectedTemplateFiles.flatMap((file) => {
|
|
2781
2953
|
const prepared = prepareBaseGameTemplateFile({
|
|
2782
2954
|
relativePath: file.relativePath,
|
|
2783
2955
|
content: file.content,
|
|
@@ -2923,6 +3095,9 @@ function createTemplateContext(input) {
|
|
|
2923
3095
|
workspaceI18nBuildPrefix: input.workspace
|
|
2924
3096
|
? `pnpm --dir ${workspacePrefix} i18n:build && `
|
|
2925
3097
|
: '',
|
|
3098
|
+
devvitOutputPluginImport: input.workspace
|
|
3099
|
+
? toJavaScriptStringLiteral(`./${toTemplatePath(path.relative(path.join(input.appDir, 'apps/target-devvit'), path.resolve(kitPath ?? input.appDir, 'packages/cli/src/devvit-output-collisions')))}`)
|
|
3100
|
+
: "'@mpgd/cli/devvit'",
|
|
2926
3101
|
defaultKitPath: kitPath === undefined ? defaultMpgdKitPath : workspacePrefix,
|
|
2927
3102
|
recommendedMatrixTargets: input.microsoftStore
|
|
2928
3103
|
? recommendedMatrixTargets
|
|
@@ -3014,6 +3189,7 @@ function walkTemplateDir(templateDir, relativeDir, files) {
|
|
|
3014
3189
|
}
|
|
3015
3190
|
function renderTemplate(content, context) {
|
|
3016
3191
|
let rendered = content
|
|
3192
|
+
.replaceAll("from '@mpgd/cli/devvit'", `from ${context.devvitOutputPluginImport}`)
|
|
3017
3193
|
.replaceAll('__GAME_NAME__', context.gameName)
|
|
3018
3194
|
.replaceAll('__DEVVIT_APP_NAME__', context.devvitAppName)
|
|
3019
3195
|
.replaceAll('__GAME_TITLE_TS_LITERAL__', context.gameTitleTsLiteral)
|
|
@@ -3037,9 +3213,10 @@ function renderTemplate(content, context) {
|
|
|
3037
3213
|
return rendered;
|
|
3038
3214
|
}
|
|
3039
3215
|
function templateOutputPath(relativePath) {
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3216
|
+
const normalized = relativePath.replace(/\\/gu, '/');
|
|
3217
|
+
return path.posix.basename(normalized) === 'gitignore'
|
|
3218
|
+
? path.posix.join(path.posix.dirname(normalized), '.gitignore')
|
|
3219
|
+
: normalized;
|
|
3043
3220
|
}
|
|
3044
3221
|
function toPascalCase(value) {
|
|
3045
3222
|
return value
|
|
@@ -3506,12 +3683,11 @@ function runPnpm(args, commandEnv) {
|
|
|
3506
3683
|
if (kitPath === undefined || kitPath.length === 0) {
|
|
3507
3684
|
throw new Error('Missing MPGD_KIT_PATH. Pass --kit-path or set MPGD_KIT_PATH.');
|
|
3508
3685
|
}
|
|
3509
|
-
const
|
|
3510
|
-
const result = spawnSync(command, [...args], {
|
|
3686
|
+
const launch = resolveNativeCommandLaunch({ command: 'pnpm', args, environment: commandEnv });
|
|
3687
|
+
const result = spawnSync(launch.command, [...launch.args], {
|
|
3511
3688
|
cwd: kitPath,
|
|
3512
3689
|
env: commandEnv,
|
|
3513
3690
|
stdio: 'inherit',
|
|
3514
|
-
shell: process.platform === 'win32',
|
|
3515
3691
|
});
|
|
3516
3692
|
if (result.error !== undefined) {
|
|
3517
3693
|
throw result.error;
|
|
@@ -3,6 +3,7 @@ import { closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, re
|
|
|
3
3
|
import { isIP } from 'node:net';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { inflateSync } from 'node:zlib';
|
|
6
|
+
import { normalizeRootRelativeRoute } from '@mpgd/platform/routes';
|
|
6
7
|
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
7
8
|
import { isMicrosoftStoreSupportedListingLocale } from './microsoft-store-supported-locales.js';
|
|
8
9
|
export const microsoftStoreSubmissionSchemaVersion = 1;
|
|
@@ -445,14 +446,18 @@ function parseListings(input) {
|
|
|
445
446
|
}];
|
|
446
447
|
}));
|
|
447
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* An artifact-relative URL: `.` and then a root-relative route already in the
|
|
451
|
+
* form `normalizeRootRelativeRoute` from `@mpgd/platform/routes` returns, the
|
|
452
|
+
* rule share links and notification routes follow, so no client reads it as
|
|
453
|
+
* another host, even once it percent-decodes the path. The manifest ships in
|
|
454
|
+
* the artifact and the Store resolves these against the manifest's own URL,
|
|
455
|
+
* so an absolute URL is never needed.
|
|
456
|
+
*/
|
|
448
457
|
function requireManifestUrl(input, label) {
|
|
449
458
|
const value = requireNonEmptyString(input, label);
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
if (!value.startsWith('./')
|
|
453
|
-
|| url.origin !== base.origin
|
|
454
|
-
|| url.pathname.startsWith('//')
|
|
455
|
-
|| value !== `.${url.pathname}${url.search}${url.hash}`) {
|
|
459
|
+
const route = value.startsWith('./') ? value.slice(1) : undefined;
|
|
460
|
+
if (route === undefined || normalizeRootRelativeRoute(route) !== route) {
|
|
456
461
|
throw new Error(`${label} must be an artifact-relative URL beginning with ./`);
|
|
457
462
|
}
|
|
458
463
|
return value;
|