@mpgd/cli 0.48.0 → 0.50.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/asset-pack-build.js +3 -3
- package/dist/devvit-output-collisions.d.ts +49 -0
- package/dist/devvit-output-collisions.js +120 -0
- package/dist/index.js +60 -6
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +31 -19
- package/dist/tooling-migration.d.ts +24 -0
- package/dist/tooling-migration.js +497 -0
- package/package.json +23 -18
- package/templates/lite-overlay/AGENTS.md +5 -0
- package/templates/lite-overlay/README.md +2 -2
- package/templates/phaser-game/AGENTS.md +5 -0
- package/templates/phaser-game/apps/target-ait/package.json +2 -2
- 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/vite.config.ts +2 -0
- package/templates/phaser-game/package.json +7 -7
- 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/dist/asset-pack-build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { deflateRawSync } from 'node:zlib';
|
|
5
5
|
import { PHASER_PACK_DELIVERY_FORMAT, PHASER_PACK_DELIVERY_VERSION, phaserPackMediaTypeForPath, validatePhaserPackBuildConfig, validatePhaserPackDeliveryManifest, } from '@mpgd/phaser-assets/pack-format';
|
|
6
6
|
import { createDeterministicZip } from './asset-pack-zip.js';
|
|
@@ -92,7 +92,7 @@ function readSourceFile(rootPath, entryPath) {
|
|
|
92
92
|
}
|
|
93
93
|
const relativeToRoot = relative(realpathSync(rootPath), realpathSync(current));
|
|
94
94
|
// Exact '..' components only: names like '..dots' stay inside the root.
|
|
95
|
-
if (relativeToRoot === '..' || relativeToRoot.startsWith(
|
|
95
|
+
if (relativeToRoot === '..' || relativeToRoot.startsWith(`..${sep}`) || isAbsolute(relativeToRoot)) {
|
|
96
96
|
throw new Error(`Pack source path escapes the source root: ${entryPath}`);
|
|
97
97
|
}
|
|
98
98
|
const data = readFileSync(current);
|
|
@@ -185,7 +185,7 @@ export function buildAssetPacks(options) {
|
|
|
185
185
|
if (isAbsolute(step)) {
|
|
186
186
|
return false;
|
|
187
187
|
}
|
|
188
|
-
return step === '' || (step !== '..' && !step.startsWith(
|
|
188
|
+
return step === '' || (step !== '..' && !step.startsWith(`..${sep}`));
|
|
189
189
|
};
|
|
190
190
|
if (overlaps(realOutPath, realRootPath) || overlaps(realRootPath, realOutPath)) {
|
|
191
191
|
throw new Error(`Output directory must be outside the pack source root: ${outPath} vs ${rootPath}`);
|
|
@@ -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.js
CHANGED
|
@@ -25,6 +25,7 @@ import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageA
|
|
|
25
25
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
26
26
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
27
27
|
import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
|
|
28
|
+
import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
|
|
28
29
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
29
30
|
import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
30
31
|
import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
|
|
@@ -177,7 +178,7 @@ export async function runMpgdCli(args) {
|
|
|
177
178
|
// partial document. Every other command keeps the default rendering.
|
|
178
179
|
renderHeader: async (ctx) => {
|
|
179
180
|
const values = ctx.values;
|
|
180
|
-
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) {
|
|
181
182
|
return '';
|
|
182
183
|
}
|
|
183
184
|
const title = ctx.env.description || ctx.env.name || '';
|
|
@@ -188,7 +189,7 @@ export async function runMpgdCli(args) {
|
|
|
188
189
|
renderValidationErrors: async (ctx, error) => {
|
|
189
190
|
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
190
191
|
const values = ctx.values;
|
|
191
|
-
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) {
|
|
192
193
|
process.stderr.write(`${messages}\n`);
|
|
193
194
|
return '';
|
|
194
195
|
}
|
|
@@ -2576,6 +2577,58 @@ const kitCommand = defineI18n({
|
|
|
2576
2577
|
}
|
|
2577
2578
|
},
|
|
2578
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
|
+
}),
|
|
2579
2632
|
doctor: defineI18n({
|
|
2580
2633
|
name: 'doctor',
|
|
2581
2634
|
description: 'Print CLI, kit, template, and target-wrapper status.',
|
|
@@ -2594,7 +2647,7 @@ const kitCommand = defineI18n({
|
|
|
2594
2647
|
}),
|
|
2595
2648
|
},
|
|
2596
2649
|
run: () => {
|
|
2597
|
-
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]".');
|
|
2598
2651
|
},
|
|
2599
2652
|
});
|
|
2600
2653
|
function commandResource(description, args = {}) {
|
|
@@ -2870,9 +2923,6 @@ function liteTemplateGameManifest(content) {
|
|
|
2870
2923
|
function createGameApp(input) {
|
|
2871
2924
|
const appDir = resolveAppDirectory(input.directory);
|
|
2872
2925
|
const gameName = path.basename(appDir);
|
|
2873
|
-
if (input.template === 'lite' && !input.workspace) {
|
|
2874
|
-
throw new Error('The lite template requires --workspace until @mpgd/lite-shell is published.');
|
|
2875
|
-
}
|
|
2876
2926
|
assertValidGameName(gameName);
|
|
2877
2927
|
const packageName = input.packageName ?? gameName;
|
|
2878
2928
|
assertValidPackageName(packageName);
|
|
@@ -3042,6 +3092,9 @@ function createTemplateContext(input) {
|
|
|
3042
3092
|
workspaceI18nBuildPrefix: input.workspace
|
|
3043
3093
|
? `pnpm --dir ${workspacePrefix} i18n:build && `
|
|
3044
3094
|
: '',
|
|
3095
|
+
devvitOutputPluginImport: input.workspace
|
|
3096
|
+
? toJavaScriptStringLiteral(`./${toTemplatePath(path.relative(path.join(input.appDir, 'apps/target-devvit'), path.resolve(kitPath ?? input.appDir, 'packages/cli/src/devvit-output-collisions')))}`)
|
|
3097
|
+
: "'@mpgd/cli/devvit'",
|
|
3045
3098
|
defaultKitPath: kitPath === undefined ? defaultMpgdKitPath : workspacePrefix,
|
|
3046
3099
|
recommendedMatrixTargets: input.microsoftStore
|
|
3047
3100
|
? recommendedMatrixTargets
|
|
@@ -3133,6 +3186,7 @@ function walkTemplateDir(templateDir, relativeDir, files) {
|
|
|
3133
3186
|
}
|
|
3134
3187
|
function renderTemplate(content, context) {
|
|
3135
3188
|
let rendered = content
|
|
3189
|
+
.replaceAll("from '@mpgd/cli/devvit'", `from ${context.devvitOutputPluginImport}`)
|
|
3136
3190
|
.replaceAll('__GAME_NAME__', context.gameName)
|
|
3137
3191
|
.replaceAll('__DEVVIT_APP_NAME__', context.devvitAppName)
|
|
3138
3192
|
.replaceAll('__GAME_TITLE_TS_LITERAL__', context.gameTitleTsLiteral)
|
|
@@ -16,7 +16,7 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
16
16
|
import { createReadStream } from "node:fs";
|
|
17
17
|
import { copyFile, lstat as lstat2, mkdir, mkdtemp, readdir, readFile, realpath as realpath2, rename, rm, writeFile } from "node:fs/promises";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve3, sep as
|
|
19
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
|
|
20
20
|
|
|
21
21
|
// node_modules/.cache/mpgd-native-build/packages/phaser-assets/src/pack-format.js
|
|
22
22
|
var PHASER_PACK_DELIVERY_FORMAT = "mpgd-asset-packs";
|
|
@@ -588,7 +588,7 @@ function resolveAssetPackLocations(policy, packs) {
|
|
|
588
588
|
// node_modules/.cache/mpgd-native-build/packages/cli/src/asset-pack-build.js
|
|
589
589
|
import { createHash } from "node:crypto";
|
|
590
590
|
import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
591
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
591
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
592
592
|
import { deflateRawSync as deflateRawSync2 } from "node:zlib";
|
|
593
593
|
|
|
594
594
|
// node_modules/.cache/mpgd-native-build/packages/cli/src/asset-pack-zip.js
|
|
@@ -749,7 +749,7 @@ function readSourceFile(rootPath, entryPath) {
|
|
|
749
749
|
throw new Error(`Pack source path is not a regular file: ${entryPath}`);
|
|
750
750
|
}
|
|
751
751
|
const relativeToRoot = relative(realpathSync(rootPath), realpathSync(current));
|
|
752
|
-
if (relativeToRoot === ".." || relativeToRoot.startsWith(
|
|
752
|
+
if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${sep}`) || isAbsolute(relativeToRoot)) {
|
|
753
753
|
throw new Error(`Pack source path escapes the source root: ${entryPath}`);
|
|
754
754
|
}
|
|
755
755
|
const data = readFileSync(current);
|
|
@@ -831,7 +831,7 @@ function buildAssetPacks(options) {
|
|
|
831
831
|
if (isAbsolute(step)) {
|
|
832
832
|
return false;
|
|
833
833
|
}
|
|
834
|
-
return step === "" || step !== ".." && !step.startsWith(
|
|
834
|
+
return step === "" || step !== ".." && !step.startsWith(`..${sep}`);
|
|
835
835
|
};
|
|
836
836
|
if (overlaps(realOutPath, realRootPath) || overlaps(realRootPath, realOutPath)) {
|
|
837
837
|
throw new Error(`Output directory must be outside the pack source root: ${outPath} vs ${rootPath}`);
|
|
@@ -1032,7 +1032,7 @@ function buildAssetPacks(options) {
|
|
|
1032
1032
|
import { createHash as createHash2 } from "node:crypto";
|
|
1033
1033
|
import { constants } from "node:fs";
|
|
1034
1034
|
import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
|
|
1035
|
-
import { isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
1035
|
+
import { isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1036
1036
|
|
|
1037
1037
|
// node_modules/.cache/mpgd-native-build/packages/phaser-assets/src/archive-errors.js
|
|
1038
1038
|
var ZipDecodeError = class extends Error {
|
|
@@ -1627,7 +1627,7 @@ var resolveArtifactPath = async (root, realRoot, artifactPath, deadline) => {
|
|
|
1627
1627
|
throw new Error(`Artifact path is not a regular file: ${artifactPath}`);
|
|
1628
1628
|
}
|
|
1629
1629
|
const rel = relative2(realRoot, await deadline.race(realpath(current)));
|
|
1630
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
1630
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
1631
1631
|
throw new Error(`Artifact path escapes the root: ${artifactPath}`);
|
|
1632
1632
|
}
|
|
1633
1633
|
return { path: current, dev: stat2.dev, ino: stat2.ino };
|
|
@@ -2278,7 +2278,7 @@ async function fileDigest(path10) {
|
|
|
2278
2278
|
}
|
|
2279
2279
|
function inside(parent, child) {
|
|
2280
2280
|
const step = relative3(parent, child);
|
|
2281
|
-
return step === "" || !isAbsolute3(step) && step !== ".." && !step.startsWith(`..${
|
|
2281
|
+
return step === "" || !isAbsolute3(step) && step !== ".." && !step.startsWith(`..${sep3}`);
|
|
2282
2282
|
}
|
|
2283
2283
|
async function noSymlink(path10) {
|
|
2284
2284
|
let current = resolve3(path10);
|
|
@@ -3646,7 +3646,7 @@ import sharp3 from "sharp";
|
|
|
3646
3646
|
|
|
3647
3647
|
// node_modules/.cache/mpgd-native-build/tools/target/web-artifact.js
|
|
3648
3648
|
import { cpSync, existsSync as existsSync5, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, realpathSync as realpathSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3649
|
-
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute5, join as join6, relative as relative7, resolve as resolve7, sep as
|
|
3649
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute5, join as join6, relative as relative7, resolve as resolve7, sep as sep4 } from "node:path";
|
|
3650
3650
|
var installableManifestNames = /* @__PURE__ */ new Set(["manifest.json", "manifest.webmanifest"]);
|
|
3651
3651
|
var generatedWebArtifactEvidenceNames = [
|
|
3652
3652
|
"mpgd-effective-target.json",
|
|
@@ -4197,7 +4197,7 @@ function assertExistingWebOutputsAreGeneratedArtifacts(outputs) {
|
|
|
4197
4197
|
}
|
|
4198
4198
|
function isPathWithin(root, candidate) {
|
|
4199
4199
|
const path10 = relative7(root, candidate);
|
|
4200
|
-
return path10 === "" || path10 !== ".." && !path10.startsWith(`..${
|
|
4200
|
+
return path10 === "" || path10 !== ".." && !path10.startsWith(`..${sep4}`) && !isAbsolute5(path10);
|
|
4201
4201
|
}
|
|
4202
4202
|
function pathsOverlap(first, second) {
|
|
4203
4203
|
return isPathWithin(first, second) || isPathWithin(second, first);
|
|
@@ -7039,7 +7039,7 @@ function isValidHostname(hostname) {
|
|
|
7039
7039
|
|
|
7040
7040
|
// node_modules/.cache/mpgd-native-build/tools/target/minigame-package-budget.js
|
|
7041
7041
|
import { lstatSync as lstatSync6, readdirSync as readdirSync3 } from "node:fs";
|
|
7042
|
-
import { isAbsolute as isAbsolute6, relative as relative8, resolve as resolve10, sep as
|
|
7042
|
+
import { isAbsolute as isAbsolute6, relative as relative8, resolve as resolve10, sep as sep5 } from "node:path";
|
|
7043
7043
|
var miniGameMainPackageLimitBytes = 4 * 1024 * 1024;
|
|
7044
7044
|
function assertMiniGamePackageBudget(input) {
|
|
7045
7045
|
const artifactRoot = resolve10(input.artifactRoot);
|
|
@@ -7186,7 +7186,7 @@ function assertSubpackageRoots(subpackages) {
|
|
|
7186
7186
|
function resolveArtifactPath2(root, path10) {
|
|
7187
7187
|
const resolved = resolve10(root, path10);
|
|
7188
7188
|
const relativePath = relative8(root, resolved);
|
|
7189
|
-
if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${
|
|
7189
|
+
if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${sep5}`) || isAbsolute6(relativePath)) {
|
|
7190
7190
|
throw new Error(`Mini-game path escapes its artifact root: ${path10}`);
|
|
7191
7191
|
}
|
|
7192
7192
|
try {
|
|
@@ -10371,7 +10371,7 @@ function requireNonEmptyString(input, label) {
|
|
|
10371
10371
|
// node_modules/.cache/mpgd-native-build/packages/cli/src/microsoft-store-pwa-release.js
|
|
10372
10372
|
import { createHash as createHash9 } from "node:crypto";
|
|
10373
10373
|
import { closeSync as closeSync2, existsSync as existsSync11, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync15, readSync, statSync as statSync2 } from "node:fs";
|
|
10374
|
-
import { relative as relative10, resolve as resolve13, sep as
|
|
10374
|
+
import { relative as relative10, resolve as resolve13, sep as sep6 } from "node:path";
|
|
10375
10375
|
function assertMicrosoftStorePwaProvenance(input) {
|
|
10376
10376
|
return {
|
|
10377
10377
|
appVersion: requireNonEmptyString2(input.appVersion, "PWA app version"),
|
|
@@ -10553,7 +10553,7 @@ function listPrecacheEntries(artifactRoot) {
|
|
|
10553
10553
|
}
|
|
10554
10554
|
}
|
|
10555
10555
|
}
|
|
10556
|
-
return files.filter((path10) => !path10.endsWith(".map") && !path10.endsWith(`${
|
|
10556
|
+
return files.filter((path10) => !path10.endsWith(".map") && !path10.endsWith(`${sep6}pwa-release.json`) && !path10.endsWith(`${sep6}service-worker.js`)).map((path10) => ({
|
|
10557
10557
|
url: toPrecacheUrl(artifactRoot, path10),
|
|
10558
10558
|
source: readFileSync15(path10)
|
|
10559
10559
|
})).sort((left, right) => compareCodeUnits(left.url, right.url));
|
|
@@ -10580,7 +10580,7 @@ function normalizePrecacheUrls(urls) {
|
|
|
10580
10580
|
return normalized.sort(compareCodeUnits);
|
|
10581
10581
|
}
|
|
10582
10582
|
function toPrecacheUrl(root, path10) {
|
|
10583
|
-
const portablePath = relative10(root, path10).split(
|
|
10583
|
+
const portablePath = relative10(root, path10).split(sep6).join("/");
|
|
10584
10584
|
return assertMicrosoftStorePwaPrecacheUrl(`./${portablePath}`);
|
|
10585
10585
|
}
|
|
10586
10586
|
function requireGameSpecificPwaId(value) {
|
|
@@ -10657,7 +10657,7 @@ function readPwaId(input) {
|
|
|
10657
10657
|
// node_modules/.cache/mpgd-native-build/tools/target/minigame-artifact.js
|
|
10658
10658
|
import { createHash as createHash10 } from "node:crypto";
|
|
10659
10659
|
import { copyFileSync as copyFileSync2, cpSync as cpSync2, existsSync as existsSync12, constants as fsConstants, lstatSync as lstatSync8, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync16, realpathSync as realpathSync7, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "node:fs";
|
|
10660
|
-
import { basename as basename4, dirname as dirname8, extname as extname2, isAbsolute as isAbsolute9, join as join11, relative as relative11, resolve as resolve15, sep as
|
|
10660
|
+
import { basename as basename4, dirname as dirname8, extname as extname2, isAbsolute as isAbsolute9, join as join11, relative as relative11, resolve as resolve15, sep as sep7 } from "node:path";
|
|
10661
10661
|
import { parse } from "acorn";
|
|
10662
10662
|
var miniGameArtifactEvidenceFileName = "mpgd-minigame-artifact.json";
|
|
10663
10663
|
var miniGameEffectiveTargetConfigFileName = "mpgd-effective-target-config.json";
|
|
@@ -10753,9 +10753,9 @@ function assertMiniGameArtifactOutputDirectory(artifactRoot, projectRoot) {
|
|
|
10753
10753
|
const project = resolve15(projectRoot);
|
|
10754
10754
|
const artifact = resolve15(artifactRoot);
|
|
10755
10755
|
const projectRelativePath = relative11(project, artifact);
|
|
10756
|
-
const portableProjectRelativePath = projectRelativePath.replaceAll(
|
|
10756
|
+
const portableProjectRelativePath = projectRelativePath.replaceAll(sep7, "/");
|
|
10757
10757
|
const segments = portableProjectRelativePath.split("/");
|
|
10758
|
-
if (projectRelativePath.length === 0 || projectRelativePath === ".." || projectRelativePath.startsWith(`..${
|
|
10758
|
+
if (projectRelativePath.length === 0 || projectRelativePath === ".." || projectRelativePath.startsWith(`..${sep7}`) || sep7 === "/" && projectRelativePath.includes("\\") || isAbsolute9(projectRelativePath) || segments[0] !== "artifacts" || segments.length < 2) {
|
|
10759
10759
|
throw new Error(`Mini-game artifact output must be a dedicated artifacts/ child: ${artifactRoot}`);
|
|
10760
10760
|
}
|
|
10761
10761
|
assertMiniGameArtifactRelativePath(portableProjectRelativePath, "Mini-game artifact output");
|
|
@@ -10982,7 +10982,7 @@ function pathsOverlap2(first, second) {
|
|
|
10982
10982
|
}
|
|
10983
10983
|
function isPathWithin2(root, candidate) {
|
|
10984
10984
|
const path10 = relative11(root, candidate);
|
|
10985
|
-
return path10 === "" || path10 !== ".." && !path10.startsWith(`..${
|
|
10985
|
+
return path10 === "" || path10 !== ".." && !path10.startsWith(`..${sep7}`) && !isAbsolute9(path10);
|
|
10986
10986
|
}
|
|
10987
10987
|
function portablePathComparisonKey(path10) {
|
|
10988
10988
|
const portablePath = path10.replaceAll("\\", "/").split("/").map((segment) => {
|
|
@@ -16525,6 +16525,12 @@ var releaseManifestEnvKeys = [
|
|
|
16525
16525
|
];
|
|
16526
16526
|
var platformTargets = loadPlatformTargetsConfig();
|
|
16527
16527
|
var configBaseDir = platformTargets.baseDir;
|
|
16528
|
+
var kitLinkCheck = resolve18("tools/workspace/relink-kit-workspace.mjs");
|
|
16529
|
+
if (existsSync24(kitLinkCheck)) {
|
|
16530
|
+
spawnSync5(process.execPath, [kitLinkCheck, "--warn-unless-owner", configBaseDir], {
|
|
16531
|
+
stdio: "inherit"
|
|
16532
|
+
});
|
|
16533
|
+
}
|
|
16528
16534
|
var config = platformTargets.config;
|
|
16529
16535
|
var target = config.targets[targetName];
|
|
16530
16536
|
if (target === void 0) {
|
|
@@ -16784,7 +16790,13 @@ try {
|
|
|
16784
16790
|
const wrapperAppConfigPath = requireString2(target.wrapperApp, `${targetName}.wrapperApp`);
|
|
16785
16791
|
const wrapperApp = targetPath(wrapperAppConfigPath);
|
|
16786
16792
|
stageWrapperIcon(generatedIcons, wrapperApp);
|
|
16787
|
-
run(
|
|
16793
|
+
run(process.execPath, [
|
|
16794
|
+
resolve18("tools/run-ttsx.mjs"),
|
|
16795
|
+
"--project",
|
|
16796
|
+
resolve18("tsconfig.tools.json"),
|
|
16797
|
+
resolve18("tools/target/build-devvit.ts"),
|
|
16798
|
+
profile
|
|
16799
|
+
], env, wrapperApp);
|
|
16788
16800
|
embedEffectiveTargetConfig(targetName, webDir, env);
|
|
16789
16801
|
await stageTargetAssetPacks(webDir);
|
|
16790
16802
|
stageWebIconEvidence(generatedIcons, webDir);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const managedPaths: readonly ['package.json', 'tsconfig.tools.json', 'tools/run-game-acceptance.mjs', 'tools/run-game-acceptance.ts', 'pnpm-lock.yaml'];
|
|
2
|
+
type ManagedPath = typeof managedPaths[number];
|
|
3
|
+
declare const snapshotPaths: readonly ["package.json", "tsconfig.tools.json", "tools/run-game-acceptance.mjs", "tools/run-game-acceptance.ts", "pnpm-lock.yaml", "pnpm-workspace.yaml", "tsconfig.json"];
|
|
4
|
+
type SnapshotPath = typeof snapshotPaths[number];
|
|
5
|
+
export interface ToolingMigrationEdit {
|
|
6
|
+
readonly file: ManagedPath;
|
|
7
|
+
readonly before: string | null;
|
|
8
|
+
readonly after: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolingMigrationPlan {
|
|
11
|
+
readonly gameRoot: string;
|
|
12
|
+
readonly edits: readonly ToolingMigrationEdit[];
|
|
13
|
+
readonly snapshots: Readonly<Record<SnapshotPath, string | null>>;
|
|
14
|
+
readonly childManifests: Readonly<Record<string, string>>;
|
|
15
|
+
readonly updateLockfile: boolean;
|
|
16
|
+
readonly blockers: readonly string[];
|
|
17
|
+
readonly notes: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/** Plans only known generated-tool changes; custom scripts and source stay untouched. */
|
|
20
|
+
export declare function planToolingMigration(gameDirectory: string, templateRoot?: string): ToolingMigrationPlan;
|
|
21
|
+
/** Applies a fresh local plan, rolling back managed files and the game lockfile on failure. */
|
|
22
|
+
export declare function applyToolingMigration(plan: ToolingMigrationPlan, updateLockfile?: typeof refreshLockfile): readonly string[];
|
|
23
|
+
declare function refreshLockfile(gameRoot: string): void;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parse } from 'acorn';
|
|
8
|
+
import { gte, minVersion, valid } from 'semver';
|
|
9
|
+
import { resolveNativeCommandLaunch } from './command-launcher.js';
|
|
10
|
+
const managedPaths = [
|
|
11
|
+
'package.json',
|
|
12
|
+
'tsconfig.tools.json',
|
|
13
|
+
'tools/run-game-acceptance.mjs',
|
|
14
|
+
'tools/run-game-acceptance.ts',
|
|
15
|
+
'pnpm-lock.yaml',
|
|
16
|
+
];
|
|
17
|
+
const snapshotPaths = [...managedPaths, 'pnpm-workspace.yaml', 'tsconfig.json'];
|
|
18
|
+
const defaultTemplateRoot = fileURLToPath(new URL('../templates/phaser-game/', import.meta.url));
|
|
19
|
+
/** Plans only known generated-tool changes; custom scripts and source stay untouched. */
|
|
20
|
+
export function planToolingMigration(gameDirectory, templateRoot = defaultTemplateRoot) {
|
|
21
|
+
const gameRoot = realpathSync(path.resolve(gameDirectory));
|
|
22
|
+
const snapshots = Object.fromEntries(snapshotPaths.map((file) => [file, readOwnedFile(gameRoot, file)]));
|
|
23
|
+
const blockers = [];
|
|
24
|
+
const childManifests = readChildManifests(gameRoot);
|
|
25
|
+
const notes = [];
|
|
26
|
+
const edits = [];
|
|
27
|
+
const add = (file, after) => {
|
|
28
|
+
if (snapshots[file] !== after) {
|
|
29
|
+
edits.push({ file, before: snapshots[file], after });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const manifest = parseObject(snapshots['package.json'], 'package.json');
|
|
33
|
+
const template = parseObject(readFileSync(path.join(templateRoot, 'package.json'), 'utf8'), 'template package.json');
|
|
34
|
+
if (manifest.type !== 'module') {
|
|
35
|
+
blockers.push('Only generated ESM games (package.json type: module) can be migrated automatically.');
|
|
36
|
+
}
|
|
37
|
+
const scripts = objectField(manifest, 'scripts');
|
|
38
|
+
const templateScripts = objectField(template, 'scripts');
|
|
39
|
+
const oldCommands = {
|
|
40
|
+
accept: ['node ./tools/run-game-acceptance.mjs', 'node tools/run-game-acceptance.mjs'],
|
|
41
|
+
check: ['ttsc --noEmit -p tsconfig.json'],
|
|
42
|
+
lint: ['ttsc --noEmit -p tsconfig.json'],
|
|
43
|
+
format: ['ttsc format'],
|
|
44
|
+
fix: ['ttsc fix'],
|
|
45
|
+
};
|
|
46
|
+
for (const [name, old] of Object.entries(oldCommands)) {
|
|
47
|
+
const desired = templateScripts[name];
|
|
48
|
+
if (typeof desired !== 'string') {
|
|
49
|
+
throw new Error(`Template script ${name} is missing.`);
|
|
50
|
+
}
|
|
51
|
+
if (scripts[name] === undefined || scripts[name] === desired || old.includes(String(scripts[name]))) {
|
|
52
|
+
scripts[name] = desired;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
blockers.push(`Custom scripts.${name}: preserve it and migrate that command manually.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
manifest.scripts = scripts;
|
|
59
|
+
const oldRunner = snapshots['tools/run-game-acceptance.mjs'];
|
|
60
|
+
const newRunner = snapshots['tools/run-game-acceptance.ts'];
|
|
61
|
+
if (oldRunner !== null && newRunner !== null) {
|
|
62
|
+
blockers.push('Both acceptance runners exist; resolve their contents before migrating.');
|
|
63
|
+
}
|
|
64
|
+
else if (oldRunner !== null) {
|
|
65
|
+
const templateRunner = readFileSync(path.join(templateRoot, 'tools/run-game-acceptance.ts'), 'utf8');
|
|
66
|
+
const rendered = migrateKnownRunner(oldRunner, templateRunner);
|
|
67
|
+
if (rendered === undefined) {
|
|
68
|
+
blockers.push('The JavaScript acceptance runner is customized; preserve it and migrate it manually.');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
add('tools/run-game-acceptance.ts', rendered);
|
|
72
|
+
add('tools/run-game-acceptance.mjs', null);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (newRunner === null) {
|
|
76
|
+
blockers.push('No generated acceptance runner was found.');
|
|
77
|
+
}
|
|
78
|
+
else if (migrateKnownRunner(newRunner, readFileSync(path.join(templateRoot, 'tools/run-game-acceptance.ts'), 'utf8')) === undefined) {
|
|
79
|
+
blockers.push('The TypeScript acceptance runner is customized; preserve it and migrate it manually.');
|
|
80
|
+
}
|
|
81
|
+
const toolsTemplate = parseObject(readFileSync(path.join(templateRoot, 'tsconfig.tools.json'), 'utf8'), 'template tools project');
|
|
82
|
+
const desiredTools = {
|
|
83
|
+
...toolsTemplate,
|
|
84
|
+
compilerOptions: { ...objectField(toolsTemplate, 'compilerOptions'), strict: true },
|
|
85
|
+
};
|
|
86
|
+
const currentTools = snapshots['tsconfig.tools.json'];
|
|
87
|
+
if (currentTools === null) {
|
|
88
|
+
add('tsconfig.tools.json', stringify(desiredTools));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const current = parseObject(currentTools, 'tsconfig.tools.json');
|
|
92
|
+
if (stable(current) === stable(toolsTemplate)) {
|
|
93
|
+
add('tsconfig.tools.json', stringify(desiredTools, currentTools));
|
|
94
|
+
}
|
|
95
|
+
else if (stable(current) !== stable(desiredTools)) {
|
|
96
|
+
blockers.push('Custom tsconfig.tools.json: preserve its options and migrate it manually.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const dev = objectField(manifest, 'devDependencies');
|
|
100
|
+
const templateDev = objectField(template, 'devDependencies');
|
|
101
|
+
for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
102
|
+
for (const name of ['ttsc', 'typescript', '@types/node']) {
|
|
103
|
+
if (objectField(manifest, section)[name] !== undefined) {
|
|
104
|
+
blockers.push(`Custom ${section}.${name}: keep toolchain declarations in devDependencies before migrating.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const compilerFloor = templateDev.ttsc;
|
|
109
|
+
if (typeof compilerFloor !== 'string' || !valid(compilerFloor)) {
|
|
110
|
+
throw new Error('Template ttsc must have an exact version.');
|
|
111
|
+
}
|
|
112
|
+
const currentCompiler = dev.ttsc;
|
|
113
|
+
if (!existsSync(path.join(gameRoot, 'tsconfig.json'))) {
|
|
114
|
+
blockers.push('Missing game tsconfig.json.');
|
|
115
|
+
}
|
|
116
|
+
let compilerVersion = compilerFloor;
|
|
117
|
+
if (typeof currentCompiler === 'string' && valid(currentCompiler)) {
|
|
118
|
+
if (gte(currentCompiler, compilerFloor)) {
|
|
119
|
+
compilerVersion = currentCompiler;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (currentCompiler !== undefined) {
|
|
123
|
+
blockers.push('Custom ttsc version specifier: align the shared toolchain manually first.');
|
|
124
|
+
}
|
|
125
|
+
let updateLockfile = false;
|
|
126
|
+
for (const name of ['ttsc', 'typescript', '@types/node']) {
|
|
127
|
+
const desired = name === 'ttsc' ? compilerVersion : templateDev[name];
|
|
128
|
+
if (typeof desired !== 'string') {
|
|
129
|
+
throw new Error(`Template dependency ${name} is missing.`);
|
|
130
|
+
}
|
|
131
|
+
const existing = versionFloor(dev[name]);
|
|
132
|
+
const minimum = minVersion(desired);
|
|
133
|
+
if (dev[name] !== undefined && existing === null) {
|
|
134
|
+
blockers.push(`Custom ${name} declaration: update it manually first.`);
|
|
135
|
+
}
|
|
136
|
+
if ((name === 'ttsc' || existing === null || (minimum !== null && !gte(existing, minimum))) && dev[name] !== desired) {
|
|
137
|
+
dev[name] = desired;
|
|
138
|
+
updateLockfile = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
142
|
+
const dependencies = section === 'devDependencies' ? dev : objectField(manifest, section);
|
|
143
|
+
for (const name of Object.keys(dependencies)) {
|
|
144
|
+
if (!name.startsWith('@ttsc/')) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (dependencies[name] !== compilerVersion) {
|
|
148
|
+
if (typeof dependencies[name] !== 'string' || !valid(dependencies[name]) || gte(dependencies[name], compilerVersion)) {
|
|
149
|
+
blockers.push(`Custom or newer ${section}.${name}: align it with ttsc manually first.`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
dependencies[name] = compilerVersion;
|
|
153
|
+
updateLockfile = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (section === 'devDependencies' || manifest[section] !== undefined) {
|
|
158
|
+
manifest[section] = dependencies;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [name, version] of Object.entries(objectField(manifest, 'peerDependencies'))) {
|
|
162
|
+
if (name.startsWith('@ttsc/') && version !== compilerVersion) {
|
|
163
|
+
blockers.push(`Custom peerDependencies.${name}: align the peer toolchain manually first.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const [file, source] of Object.entries(childManifests)) {
|
|
167
|
+
const child = parseObject(source, file);
|
|
168
|
+
for (const section of [
|
|
169
|
+
'dependencies',
|
|
170
|
+
'devDependencies',
|
|
171
|
+
'optionalDependencies',
|
|
172
|
+
'peerDependencies',
|
|
173
|
+
]) {
|
|
174
|
+
for (const [name, version] of Object.entries(objectField(child, section))) {
|
|
175
|
+
if (name === 'ttsc' || name.startsWith('@ttsc/')) {
|
|
176
|
+
if (version !== compilerVersion) {
|
|
177
|
+
blockers.push(`${file}: align ${section}.${name} with ttsc ${compilerVersion} manually first.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (name === 'typescript' || name === '@types/node') {
|
|
181
|
+
const minimum = versionFloor(templateDev[name]);
|
|
182
|
+
const current = versionFloor(version);
|
|
183
|
+
if (minimum === null || current === null || !gte(current, minimum)) {
|
|
184
|
+
blockers.push(`${file}: align ${section}.${name} with the template toolchain manually first.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (hasAncestorWorkspace(gameRoot)) {
|
|
191
|
+
blockers.push('This game belongs to a parent pnpm workspace. Automatic migration cannot verify its shared toolchain and lockfile; migrate the tools manually after aligning the owning workspace.');
|
|
192
|
+
}
|
|
193
|
+
if (updateLockfile) {
|
|
194
|
+
notes.push('Applying updates the game lockfile with pnpm install --lockfile-only --ignore-scripts.');
|
|
195
|
+
}
|
|
196
|
+
notes.push('Requires Node 24.11 or newer. Run pnpm check and pnpm accept --help after applying.');
|
|
197
|
+
if (stable(manifest) !== stable(parseObject(snapshots['package.json'], 'package.json'))) {
|
|
198
|
+
add('package.json', stringify(manifest, snapshots['package.json'] ?? undefined));
|
|
199
|
+
}
|
|
200
|
+
return { gameRoot, edits, snapshots, childManifests, updateLockfile, blockers, notes };
|
|
201
|
+
}
|
|
202
|
+
/** Applies a fresh local plan, rolling back managed files and the game lockfile on failure. */
|
|
203
|
+
export function applyToolingMigration(plan, updateLockfile = refreshLockfile) {
|
|
204
|
+
if (plan.blockers.length) {
|
|
205
|
+
throw new Error(`Tooling migration blocked:\n${plan.blockers.join('\n')}`);
|
|
206
|
+
}
|
|
207
|
+
assertFreshPlan(plan);
|
|
208
|
+
for (const edit of plan.edits) {
|
|
209
|
+
if (!managedPaths.includes(edit.file) || edit.before !== plan.snapshots[edit.file]) {
|
|
210
|
+
throw new Error('Invalid tooling migration edit.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!plan.edits.length) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const lockPath = path.join(plan.gameRoot, '.mpgd-tooling-migration.lock');
|
|
217
|
+
const lock = JSON.stringify({
|
|
218
|
+
token: randomUUID(),
|
|
219
|
+
pid: process.pid,
|
|
220
|
+
host: hostname(),
|
|
221
|
+
startedAt: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
writeFileSync(lockPath, lock, { flag: 'wx', mode: 0o600 });
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (isObject(error) && error.code === 'EEXIST') {
|
|
228
|
+
throw new Error(`Migration lock exists (EEXIST): ${lockPath}. If an earlier run was interrupted, verify its process and pnpm child have exited, inspect partial changes, then remove only this lock and run a fresh preview.`, { cause: error });
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
let writesStarted = false;
|
|
233
|
+
let lockfileUpdateAttempted = false;
|
|
234
|
+
const written = [];
|
|
235
|
+
try {
|
|
236
|
+
// Another migration may have completed between the initial check and lock acquisition.
|
|
237
|
+
assertFreshPlan(plan);
|
|
238
|
+
writesStarted = true;
|
|
239
|
+
for (const edit of plan.edits) {
|
|
240
|
+
if (readOwnedFile(plan.gameRoot, edit.file) !== edit.before) {
|
|
241
|
+
throw new Error(`File changed during migration: ${edit.file}`);
|
|
242
|
+
}
|
|
243
|
+
writeOwnedFile(plan.gameRoot, edit.file, edit.after);
|
|
244
|
+
written.push(edit);
|
|
245
|
+
}
|
|
246
|
+
if (plan.updateLockfile) {
|
|
247
|
+
lockfileUpdateAttempted = true;
|
|
248
|
+
updateLockfile(plan.gameRoot);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (!writesStarted) {
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
const failures = [];
|
|
256
|
+
for (const edit of [...written].reverse()) {
|
|
257
|
+
try {
|
|
258
|
+
if (readOwnedFile(plan.gameRoot, edit.file) !== edit.after) {
|
|
259
|
+
throw new Error('A concurrent writer changed the migration output.');
|
|
260
|
+
}
|
|
261
|
+
writeOwnedFile(plan.gameRoot, edit.file, edit.before);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
failures.push(edit.file);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (lockfileUpdateAttempted) {
|
|
268
|
+
try {
|
|
269
|
+
// pnpm does not honor our migration lock. A changed lockfile might be
|
|
270
|
+
// another install's output; preserve it and require manual inspection.
|
|
271
|
+
if (readOwnedFile(plan.gameRoot, 'pnpm-lock.yaml') !== plan.snapshots['pnpm-lock.yaml']) {
|
|
272
|
+
failures.push('pnpm-lock.yaml');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
failures.push('pnpm-lock.yaml');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
let rollbackStatus = lockfileUpdateAttempted
|
|
280
|
+
? 'Managed files restored; lockfile unchanged.'
|
|
281
|
+
: 'Managed files restored; lockfile untouched.';
|
|
282
|
+
if (failures.length) {
|
|
283
|
+
rollbackStatus = `Rollback incomplete: ${failures.join(', ')}`;
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`Tooling migration failed. ${rollbackStatus}`, { cause: error });
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
if (existsSync(lockPath) && readFileSync(lockPath, 'utf8') === lock) {
|
|
289
|
+
rmSync(lockPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return plan.edits.map((edit) => edit.file);
|
|
293
|
+
}
|
|
294
|
+
function assertFreshPlan(plan) {
|
|
295
|
+
if (realpathSync(plan.gameRoot) !== plan.gameRoot) {
|
|
296
|
+
throw new Error('Game root changed after planning.');
|
|
297
|
+
}
|
|
298
|
+
for (const file of snapshotPaths) {
|
|
299
|
+
if (readOwnedFile(plan.gameRoot, file) !== plan.snapshots[file]) {
|
|
300
|
+
throw new Error(`File changed after planning: ${file}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (stable(readChildManifests(plan.gameRoot)) !== stable(plan.childManifests)) {
|
|
304
|
+
throw new Error('Child package manifests changed after planning.');
|
|
305
|
+
}
|
|
306
|
+
if (hasAncestorWorkspace(plan.gameRoot)) {
|
|
307
|
+
throw new Error('Parent workspace changed after planning.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Conservatively inspect game-owned nested packages, including custom workspace
|
|
311
|
+
// layouts. Never traverse dependency installs, generated builds or linked trees.
|
|
312
|
+
function readChildManifests(root) {
|
|
313
|
+
const manifests = {};
|
|
314
|
+
const ignored = new Set(['node_modules', 'dist', 'build', 'coverage']);
|
|
315
|
+
function visit(directory) {
|
|
316
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
317
|
+
if (entry.name.startsWith('.') || ignored.has(entry.name)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const full = path.join(directory, entry.name);
|
|
321
|
+
if (entry.isSymbolicLink()) {
|
|
322
|
+
if (entry.name === 'package.json' || existsSync(path.join(full, 'package.json'))) {
|
|
323
|
+
throw new Error(`Refusing linked child package: ${path.relative(root, full)}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else if (entry.isDirectory()) {
|
|
327
|
+
visit(full);
|
|
328
|
+
}
|
|
329
|
+
else if (directory !== root && entry.name === 'package.json') {
|
|
330
|
+
manifests[path.relative(root, full)] = readFileSync(full, 'utf8');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
visit(root);
|
|
335
|
+
return manifests;
|
|
336
|
+
}
|
|
337
|
+
function parseObject(source, label) {
|
|
338
|
+
if (source === null) {
|
|
339
|
+
throw new Error(`Missing ${label}.`);
|
|
340
|
+
}
|
|
341
|
+
const parsed = JSON.parse(source);
|
|
342
|
+
if (!isObject(parsed)) {
|
|
343
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
344
|
+
}
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
function isObject(value) {
|
|
348
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
349
|
+
}
|
|
350
|
+
function objectField(value, key) {
|
|
351
|
+
if (value[key] === undefined) {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
if (!isObject(value[key])) {
|
|
355
|
+
throw new Error(`${key} must be an object.`);
|
|
356
|
+
}
|
|
357
|
+
return value[key];
|
|
358
|
+
}
|
|
359
|
+
function stable(value) {
|
|
360
|
+
return JSON.stringify(value, (_key, entry) => isObject(entry)
|
|
361
|
+
? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)))
|
|
362
|
+
: entry);
|
|
363
|
+
}
|
|
364
|
+
function stringify(value, original = '') {
|
|
365
|
+
const indent = /^([\t ]+)"/m.exec(original)?.[1] ?? ' ';
|
|
366
|
+
const text = JSON.stringify(value, null, indent) + '\n';
|
|
367
|
+
return original.includes('\r\n') ? text.replace(/\n/g, '\r\n') : text;
|
|
368
|
+
}
|
|
369
|
+
function migrateKnownRunner(source, template) {
|
|
370
|
+
try {
|
|
371
|
+
let comments = 0;
|
|
372
|
+
const ast = parse(source, {
|
|
373
|
+
ecmaVersion: 'latest',
|
|
374
|
+
sourceType: 'module',
|
|
375
|
+
onComment: () => {
|
|
376
|
+
comments++;
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
if (comments) {
|
|
380
|
+
return undefined;
|
|
381
|
+
} // Preserve user-authored comments rather than silently replacing them.
|
|
382
|
+
const defaults = [];
|
|
383
|
+
function walk(value) {
|
|
384
|
+
if (Array.isArray(value)) {
|
|
385
|
+
for (const entry of value) {
|
|
386
|
+
walk(entry);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else if (isObject(value)) {
|
|
390
|
+
if (value.type === 'ConditionalExpression' && isObject(value.consequent) && typeof value.consequent.value === 'string') {
|
|
391
|
+
defaults.push(value.consequent.value);
|
|
392
|
+
}
|
|
393
|
+
for (const entry of Object.values(value)) {
|
|
394
|
+
walk(entry);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
walk(ast);
|
|
399
|
+
if (defaults.length !== 1) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const rendered = template.replace("'__DEFAULT_KIT_PATH__'", JSON.stringify(defaults[0]));
|
|
403
|
+
const canonical = (value) => JSON.stringify(value, (key, item) => ['start', 'end', 'raw'].includes(key) ? undefined : item);
|
|
404
|
+
return canonical(ast) === canonical(parse(rendered, { ecmaVersion: 'latest', sourceType: 'module' }))
|
|
405
|
+
? rendered
|
|
406
|
+
: undefined;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function readOwnedFile(root, file) {
|
|
413
|
+
const full = ownedPath(root, file);
|
|
414
|
+
return existsSync(full) ? readFileSync(full, 'utf8') : null;
|
|
415
|
+
}
|
|
416
|
+
function ownedPath(root, file) {
|
|
417
|
+
if (!snapshotPaths.includes(file)) {
|
|
418
|
+
throw new Error('Path is not owned by tooling migration.');
|
|
419
|
+
}
|
|
420
|
+
let current = root;
|
|
421
|
+
for (const part of file.split('/')) {
|
|
422
|
+
current = path.join(current, part);
|
|
423
|
+
try {
|
|
424
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
425
|
+
throw new Error(`Refusing linked migration path: ${file}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (!isObject(error) || error.code !== 'ENOENT') {
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return current;
|
|
435
|
+
}
|
|
436
|
+
function writeOwnedFile(root, file, source) {
|
|
437
|
+
const full = ownedPath(root, file);
|
|
438
|
+
if (source === null) {
|
|
439
|
+
rmSync(full, { force: true });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const temporary = `${full}.mpgd-${randomUUID()}.tmp`;
|
|
443
|
+
try {
|
|
444
|
+
writeFileSync(temporary, source, {
|
|
445
|
+
flag: 'wx',
|
|
446
|
+
mode: existsSync(full) ? lstatSync(full).mode & 0o777 : 0o644,
|
|
447
|
+
});
|
|
448
|
+
renameSync(temporary, full);
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
rmSync(temporary, { force: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function hasAncestorWorkspace(root) {
|
|
455
|
+
if (existsSync(path.join(root, 'pnpm-workspace.yaml'))) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
for (let parent = path.dirname(root);; parent = path.dirname(parent)) {
|
|
459
|
+
if (existsSync(path.join(parent, 'pnpm-workspace.yaml'))) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (path.dirname(parent) === parent) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function refreshLockfile(gameRoot) {
|
|
468
|
+
const environment = { ...process.env };
|
|
469
|
+
for (const key of Object.keys(environment)) {
|
|
470
|
+
if (key.toLowerCase() === 'npm_config_workspace_dir') {
|
|
471
|
+
delete environment[key];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const launch = resolveNativeCommandLaunch({
|
|
475
|
+
command: 'pnpm',
|
|
476
|
+
args: ['install', '--lockfile-only', '--ignore-scripts', '--lockfile-dir', gameRoot],
|
|
477
|
+
environment,
|
|
478
|
+
});
|
|
479
|
+
const result = spawnSync(launch.command, [...launch.args], {
|
|
480
|
+
cwd: gameRoot,
|
|
481
|
+
env: environment,
|
|
482
|
+
encoding: 'utf8',
|
|
483
|
+
timeout: 120_000,
|
|
484
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
485
|
+
});
|
|
486
|
+
if (result.error || result.status !== 0) {
|
|
487
|
+
throw new Error(`Could not refresh game lockfile: ${result.error?.message ?? result.stderr}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function versionFloor(value) {
|
|
491
|
+
try {
|
|
492
|
+
return typeof value === 'string' ? minVersion(value) : null;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
".": {
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./devvit": {
|
|
39
|
+
"types": "./dist/devvit-output-collisions.d.ts",
|
|
40
|
+
"default": "./dist/devvit-output-collisions.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"files": [
|
|
@@ -66,29 +70,30 @@
|
|
|
66
70
|
"parse5": "7.3.0",
|
|
67
71
|
"postcss": "8.5.28",
|
|
68
72
|
"postcss-value-parser": "4.2.0",
|
|
69
|
-
"@mpgd/catalog": "0.10.
|
|
70
|
-
"@mpgd/phaser-assets": "0.9.
|
|
71
|
-
"@mpgd/platform": "0.23.
|
|
72
|
-
"@mpgd/target-config": "0.23.
|
|
73
|
+
"@mpgd/catalog": "0.10.1",
|
|
74
|
+
"@mpgd/phaser-assets": "0.9.1",
|
|
75
|
+
"@mpgd/platform": "0.23.1",
|
|
76
|
+
"@mpgd/target-config": "0.23.1"
|
|
73
77
|
},
|
|
74
78
|
"devDependencies": {
|
|
75
79
|
"@types/node": "^24.0.0",
|
|
76
80
|
"@types/semver": "^7.8.0",
|
|
77
81
|
"@types/yauzl": "3.4.0",
|
|
78
|
-
"ttsc": "0.
|
|
82
|
+
"ttsc": "0.32.1",
|
|
79
83
|
"typescript": "7.0.2",
|
|
80
|
-
"@mpgd/adapter-ait": "0.17.
|
|
81
|
-
"@mpgd/adapter-browser": "0.11.
|
|
82
|
-
"@mpgd/adapter-capacitor": "0.10.
|
|
83
|
-
"@mpgd/adapter-devvit": "0.12.
|
|
84
|
-
"@mpgd/
|
|
85
|
-
"@mpgd/
|
|
86
|
-
"@mpgd/capacitor-play-billing": "0.3.
|
|
87
|
-
"@mpgd/
|
|
88
|
-
"@mpgd/
|
|
89
|
-
"@mpgd/game-services": "0.24.
|
|
90
|
-
"@mpgd/game-runtime": "0.7.
|
|
91
|
-
"@mpgd/
|
|
84
|
+
"@mpgd/adapter-ait": "0.17.1",
|
|
85
|
+
"@mpgd/adapter-browser": "0.11.1",
|
|
86
|
+
"@mpgd/adapter-capacitor": "0.10.1",
|
|
87
|
+
"@mpgd/adapter-devvit": "0.12.1",
|
|
88
|
+
"@mpgd/adapter-verse8": "0.7.1",
|
|
89
|
+
"@mpgd/analytics": "0.4.10",
|
|
90
|
+
"@mpgd/capacitor-play-billing": "0.3.1",
|
|
91
|
+
"@mpgd/capacitor-storekit": "0.3.1",
|
|
92
|
+
"@mpgd/bridge": "0.13.1",
|
|
93
|
+
"@mpgd/game-services": "0.24.1",
|
|
94
|
+
"@mpgd/game-runtime": "0.7.1",
|
|
95
|
+
"@mpgd/lite-shell": "0.1.0",
|
|
96
|
+
"@mpgd/i18n": "0.6.15"
|
|
92
97
|
},
|
|
93
98
|
"main": "./dist/index.js",
|
|
94
99
|
"types": "./dist/index.d.ts",
|
|
@@ -21,6 +21,11 @@ This is a Canvas2D game generated with the mpgd lite template.
|
|
|
21
21
|
|
|
22
22
|
## Tooling and checks
|
|
23
23
|
|
|
24
|
+
- Author game code, tests, and development tools in TypeScript (`.ts`). Include every tool in
|
|
25
|
+
`tsconfig.tools.json` and run it with `ttsx --project tsconfig.tools.json`.
|
|
26
|
+
- Use `.mjs`, `.cjs`, or authored `.js` only when a concrete consumer cannot run TypeScript
|
|
27
|
+
(for example a browser fixture or a JavaScript-only config loader). Document that constraint
|
|
28
|
+
beside the file. Convenience or avoiding type errors is not an exception.
|
|
24
29
|
- Use `ttsx`, `ttsc`, and `@ttsc/lint`; do not add `tsx`, ESLint, or Prettier.
|
|
25
30
|
- Run focused checks, then `pnpm check` and `pnpm build`.
|
|
26
31
|
- Run `pnpm accept` for a target handoff; preserve its generated evidence.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# __GAME_TITLE__
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Canvas2D lite game generated by `@mpgd/create-game --template lite`.
|
|
4
4
|
It uses `@mpgd/lite-shell`, `PlatformGateway`, and the shared mpgd game runtime.
|
|
5
5
|
|
|
6
6
|
## Start
|
|
@@ -13,7 +13,7 @@ pnpm build
|
|
|
13
13
|
pnpm accept
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Standalone games install the public `@mpgd/lite-shell` package. Add `--workspace --kit-path <mpgd-kit>` to generate a game linked to a local Kit checkout.
|
|
17
17
|
The CLI supports the canvas renderer; DOM and SVG renderers are planned.
|
|
18
18
|
|
|
19
19
|
## Ownership
|
|
@@ -27,6 +27,11 @@ This repository is a standalone Phaser game generated with `@mpgd/create-game`.
|
|
|
27
27
|
|
|
28
28
|
## Tooling and checks
|
|
29
29
|
|
|
30
|
+
- Author game code, tests, and development tools in TypeScript (`.ts`). Include every tool in
|
|
31
|
+
`tsconfig.tools.json` and run it with `ttsx --project tsconfig.tools.json`.
|
|
32
|
+
- Use `.mjs`, `.cjs`, or authored `.js` only when a concrete consumer cannot run TypeScript
|
|
33
|
+
(for example a browser fixture or a JavaScript-only config loader). Document that constraint
|
|
34
|
+
beside the file. Convenience or avoiding type errors is not an exception.
|
|
30
35
|
- Use `ttsx`, not `tsx`, for TypeScript scripts.
|
|
31
36
|
- Use `ttsc` and `@ttsc/lint`; do not add ESLint or Prettier for TypeScript.
|
|
32
37
|
- Run focused checks, then `pnpm check` and `pnpm build`.
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@devvit/start": "0.14.1",
|
|
29
|
-
"@ttsc/unplugin": "0.
|
|
29
|
+
"@ttsc/unplugin": "0.32.1",
|
|
30
30
|
"@types/node": "^24.0.0",
|
|
31
31
|
"devvit": "0.14.1",
|
|
32
|
-
"ttsc": "0.
|
|
32
|
+
"ttsc": "0.32.1",
|
|
33
33
|
"typescript": "7.0.2",
|
|
34
34
|
"vite": "7.3.6"
|
|
35
35
|
}
|
|
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
|
|
4
4
|
import { devvit } from '@devvit/start/vite';
|
|
5
|
+
import { devvitOutputCollisionPlugin } from '@mpgd/cli/devvit';
|
|
5
6
|
import { defineConfig, type PluginOption } from 'vite';
|
|
6
7
|
|
|
7
8
|
import { createGameViteSharedConfig } from '../../vite.shared';
|
|
@@ -24,6 +25,7 @@ export default defineConfig(({ mode }) => {
|
|
|
24
25
|
publicDir: resolve(gameRoot, 'public'),
|
|
25
26
|
plugins: [
|
|
26
27
|
...(shared.plugins ?? []),
|
|
28
|
+
devvitOutputCollisionPlugin(),
|
|
27
29
|
devvit({
|
|
28
30
|
client: {
|
|
29
31
|
build: {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dev": "__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0",
|
|
9
9
|
"dev:ait": "APP_TARGET=ait MPGD_CONFIG_TARGET=ait MPGD_PLATFORM_TARGETS_FILE=./mpgd.targets.json APP_VERSION=0.0.0-dev BUILD_ID=ait-sandbox sh -c '__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0'",
|
|
10
10
|
"build": "__WORKSPACE_I18N_BUILD_PREFIX__vite build",
|
|
11
|
-
"accept": "
|
|
11
|
+
"accept": "ttsx --project tsconfig.tools.json tools/run-game-acceptance.ts --",
|
|
12
12
|
"icons:generate": "pnpm exec mpgd game icons generate . --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
|
|
13
13
|
"icons:generate:devvit": "pnpm exec mpgd game icons generate . --targets reddit --profile development --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
|
|
14
14
|
"icons:generate:devvit:production": "pnpm exec mpgd game icons generate . --targets reddit --profile production --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"pages:dev": "pnpm legal:build && pnpm --dir apps/target-cloudflare-pages run dev",
|
|
50
50
|
"pages:preview": "pnpm legal:build && pnpm --dir apps/target-cloudflare-pages run preview",
|
|
51
51
|
"pages:types": "pnpm --dir apps/target-cloudflare-pages run types",
|
|
52
|
-
"check": "ttsc --noEmit -p tsconfig.json",
|
|
53
|
-
"lint": "ttsc --noEmit -p tsconfig.json",
|
|
54
|
-
"format": "ttsc format",
|
|
55
|
-
"fix": "ttsc fix"
|
|
52
|
+
"check": "ttsc --noEmit -p tsconfig.json && ttsc --noEmit -p tsconfig.tools.json",
|
|
53
|
+
"lint": "ttsc --noEmit -p tsconfig.json && ttsc --noEmit -p tsconfig.tools.json",
|
|
54
|
+
"format": "ttsc format -p tsconfig.json && ttsc format -p tsconfig.tools.json",
|
|
55
|
+
"fix": "ttsc fix -p tsconfig.json && ttsc fix -p tsconfig.tools.json"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@ait-co/polyfill": "^0.1.19",
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
"@ait-co/console-cli": "^0.1.49",
|
|
82
82
|
"@cloudflare/workers-types": "^5.20260707.1",
|
|
83
83
|
"@mpgd/cli": "__MPGD_DEPENDENCY_VERSION_CLI__",
|
|
84
|
-
"@ttsc/unplugin": "0.
|
|
84
|
+
"@ttsc/unplugin": "0.32.1",
|
|
85
85
|
"@types/node": "^24.0.0",
|
|
86
|
-
"ttsc": "0.
|
|
86
|
+
"ttsc": "0.32.1",
|
|
87
87
|
"typescript": "7.0.2",
|
|
88
88
|
"vite": "8.1.3",
|
|
89
89
|
"wrangler": "^4.107.0"
|
|
@@ -12,11 +12,4 @@ const kitPath = path.resolve(
|
|
|
12
12
|
: configuredKitPath,
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
await runMpgdCli([
|
|
16
|
-
'game',
|
|
17
|
-
'accept',
|
|
18
|
-
gameRoot,
|
|
19
|
-
'--kit-path',
|
|
20
|
-
kitPath,
|
|
21
|
-
...process.argv.slice(2),
|
|
22
|
-
]);
|
|
15
|
+
await runMpgdCli(['game', 'accept', gameRoot, '--kit-path', kitPath, ...process.argv.slice(2)]);
|