@mpgd/cli 0.34.0 → 0.36.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/android-bundle-signer.d.ts +2 -0
- package/dist/android-bundle-signer.js +38 -0
- package/dist/android-signing-session.d.ts +22 -0
- package/dist/android-signing-session.js +198 -0
- package/dist/artifact-inspection-environment.d.ts +2 -0
- package/dist/artifact-inspection-environment.js +31 -0
- package/dist/asc-pin.json +26 -0
- package/dist/capacitor-shell-starter.d.ts +1 -0
- package/dist/capacitor-shell-starter.js +9 -6
- package/dist/deploy-planning.d.ts +64 -0
- package/dist/deploy-planning.js +471 -0
- package/dist/deploy-process.d.ts +28 -0
- package/dist/deploy-process.js +197 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +483 -13
- package/dist/ios-ipa-inspection.js +151 -0
- package/dist/ios-keychain-import.swift +92 -0
- package/dist/ios-plist.d.ts +2 -0
- package/dist/ios-plist.js +75 -0
- package/dist/ios-signing-profile.d.ts +12 -0
- package/dist/ios-signing-profile.js +60 -0
- package/dist/ios-signing-session.d.ts +22 -0
- package/dist/ios-signing-session.js +256 -0
- package/dist/kit-upgrade-target-readiness.d.ts +8 -0
- package/dist/kit-upgrade-target-readiness.js +119 -0
- package/dist/kit-upgrade.d.ts +41 -0
- package/dist/kit-upgrade.js +489 -0
- package/dist/native-build-info.json +5 -0
- package/dist/native-build-target.js +12032 -0
- package/dist/native-deploy-run.d.ts +35 -0
- package/dist/native-deploy-run.js +462 -0
- package/dist/native-deploy-submission.d.ts +40 -0
- package/dist/native-deploy-submission.js +244 -0
- package/dist/native-deploy-workflow.d.ts +11 -0
- package/dist/native-deploy-workflow.js +328 -0
- package/dist/play-internal-submission.d.ts +32 -0
- package/dist/play-internal-submission.js +247 -0
- package/dist/play-publisher-port.d.ts +30 -0
- package/dist/play-publisher-port.js +1 -0
- package/dist/play-sdk-adapter.js +78 -0
- package/dist/release-state.d.ts +140 -0
- package/dist/release-state.js +1046 -0
- package/dist/release-workspace.d.ts +61 -0
- package/dist/release-workspace.js +504 -0
- package/dist/testflight-submission.d.ts +49 -0
- package/dist/testflight-submission.js +462 -0
- package/package.json +20 -13
- package/templates/phaser-game/README.md +23 -8
- package/templates/phaser-game/src/main.ts +34 -33
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface KitUpgradeTargetAdvisory {
|
|
2
|
+
readonly target: string;
|
|
3
|
+
readonly kind: 'missing-product-id' | 'missing-placement-id' | 'not-assessed';
|
|
4
|
+
readonly logicalId?: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
/** Read-only hints, not a substitute for target validation after an upgrade. */
|
|
8
|
+
export declare function inspectKitUpgradeTargetReadiness(gameRoot: string, targetsFile: string): readonly KitUpgradeTargetAdvisory[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { assertAdPlacements, assertProductCatalog } from '@mpgd/catalog';
|
|
5
|
+
import { assertTargetConfigMatrix, createEffectiveTargetConfig } from '@mpgd/target-config';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
/** Read-only hints, not a substitute for target validation after an upgrade. */
|
|
8
|
+
export function inspectKitUpgradeTargetReadiness(gameRoot, targetsFile) {
|
|
9
|
+
if (!existsSync(targetsFile)) {
|
|
10
|
+
return [notAssessed('*', 'No game-owned targets file is available.')];
|
|
11
|
+
}
|
|
12
|
+
if (process.env.MPGD_PRODUCT_CATALOG_FILE?.trim()
|
|
13
|
+
|| process.env.MPGD_AD_PLACEMENTS_FILE?.trim()) {
|
|
14
|
+
const message = 'Custom catalog or placement paths are not inspected; '
|
|
15
|
+
+ 'validate targets after upgrading.';
|
|
16
|
+
return [notAssessed('*', message)];
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const configured = asRecord(readGameOwnedJsonFile(gameRoot, targetsFile), 'targets file');
|
|
20
|
+
const targets = asRecord(configured.targets, 'targets');
|
|
21
|
+
const baseFile = require.resolve('@mpgd/target-config/targets.json');
|
|
22
|
+
const base = assertTargetConfigMatrix(JSON.parse(readFileSync(baseFile, 'utf8')));
|
|
23
|
+
const catalogFile = path.join(gameRoot, 'mpgd.catalog.json');
|
|
24
|
+
const placementsFile = path.join(gameRoot, 'mpgd.ad-placements.json');
|
|
25
|
+
if (existsSync(catalogFile) !== existsSync(placementsFile)) {
|
|
26
|
+
return [notAssessed('*', 'Default mpgd.catalog.json and mpgd.ad-placements.json must be present together; '
|
|
27
|
+
+ 'custom paths are not assessed.')];
|
|
28
|
+
}
|
|
29
|
+
const catalog = existsSync(catalogFile)
|
|
30
|
+
? assertProductCatalog(readGameOwnedJsonFile(gameRoot, catalogFile))
|
|
31
|
+
: assertProductCatalog({ version: 'none', products: [] });
|
|
32
|
+
const placements = existsSync(placementsFile)
|
|
33
|
+
? assertAdPlacements(readGameOwnedJsonFile(gameRoot, placementsFile))
|
|
34
|
+
: assertAdPlacements({ version: 'none', placements: [] });
|
|
35
|
+
const advisories = [];
|
|
36
|
+
for (const target of Object.keys(targets).sort()) {
|
|
37
|
+
const policy = base.targets[target];
|
|
38
|
+
if (policy === undefined) {
|
|
39
|
+
const message = 'No built-in runtime policy is available; '
|
|
40
|
+
+ 'validate this custom target after upgrading.';
|
|
41
|
+
advisories.push(notAssessed(target, message));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const platform = asRecord(targets[target], `target ${target}`);
|
|
46
|
+
const effective = createEffectiveTargetConfig({
|
|
47
|
+
target,
|
|
48
|
+
targetConfigVersion: base.version,
|
|
49
|
+
config: policy,
|
|
50
|
+
catalog,
|
|
51
|
+
adPlacements: placements,
|
|
52
|
+
platformTarget: {
|
|
53
|
+
kind: String(platform.kind ?? ''),
|
|
54
|
+
adapter: String(platform.adapter ?? ''),
|
|
55
|
+
...(platform.authoritativeGameServices === false
|
|
56
|
+
? { authoritativeGameServices: false }
|
|
57
|
+
: {}),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
// Devvit validates product SKUs against its wrapper contract, not catalog platform IDs.
|
|
61
|
+
if (effective.runtime !== 'devvit-web') {
|
|
62
|
+
for (const product of effective.monetization.products) {
|
|
63
|
+
if (product.reason === 'missing-platform-id') {
|
|
64
|
+
advisories.push({
|
|
65
|
+
target,
|
|
66
|
+
kind: 'missing-product-id',
|
|
67
|
+
logicalId: product.id,
|
|
68
|
+
message: `Product ${product.id} needs platformProductIds.${target}.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const placement of effective.ads.placements) {
|
|
74
|
+
if (placement.reason === 'missing-platform-id') {
|
|
75
|
+
advisories.push({
|
|
76
|
+
target,
|
|
77
|
+
kind: 'missing-placement-id',
|
|
78
|
+
logicalId: placement.id,
|
|
79
|
+
message: `Ad placement ${placement.id} needs platformPlacementIds.${target}.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
advisories.push(notAssessed(target, `Could not inspect target: ${formatError(error)}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return advisories;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return [notAssessed('*', `Target readiness could not be assessed: ${formatError(error)}`)];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function readGameOwnedJsonFile(gameRoot, file) {
|
|
95
|
+
if (!existsSync(file)) {
|
|
96
|
+
throw new Error(`Missing game-owned ${path.basename(file)}.`);
|
|
97
|
+
}
|
|
98
|
+
if (lstatSync(file).isSymbolicLink() || !inside(gameRoot, realpathSync(file))) {
|
|
99
|
+
throw new Error(`Refusing to inspect a linked or external ${path.basename(file)}.`);
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
function asRecord(value, label) {
|
|
104
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
105
|
+
throw new Error(`${label} must be a JSON object`);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function inside(root, candidate) {
|
|
110
|
+
const relative = path.relative(root, candidate);
|
|
111
|
+
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`)
|
|
112
|
+
&& !path.isAbsolute(relative));
|
|
113
|
+
}
|
|
114
|
+
function notAssessed(target, message) {
|
|
115
|
+
return { target, kind: 'not-assessed', message };
|
|
116
|
+
}
|
|
117
|
+
function formatError(error) {
|
|
118
|
+
return error instanceof Error ? error.message : String(error);
|
|
119
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type KitUpgradeTargetAdvisory } from './kit-upgrade-target-readiness.js';
|
|
2
|
+
declare const dependencySections: readonly ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
3
|
+
type DependencySection = (typeof dependencySections)[number];
|
|
4
|
+
export interface PublishedKitPackage {
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly peerDependencies?: Readonly<Record<string, string>>;
|
|
7
|
+
readonly optionalPeerDependencies?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export type LatestKitPackageLookup = (name: string, gameRoot: string) => Promise<PublishedKitPackage>;
|
|
10
|
+
export interface KitUpgradeUpdate {
|
|
11
|
+
readonly manifest: string;
|
|
12
|
+
readonly section: DependencySection;
|
|
13
|
+
readonly packageName: string;
|
|
14
|
+
readonly current: string;
|
|
15
|
+
readonly next: string;
|
|
16
|
+
readonly latestVersion: string;
|
|
17
|
+
}
|
|
18
|
+
export interface KitUpgradePlan {
|
|
19
|
+
readonly gameRoot: string;
|
|
20
|
+
readonly updates: readonly KitUpgradeUpdate[];
|
|
21
|
+
readonly blockers: readonly string[];
|
|
22
|
+
readonly notes: readonly string[];
|
|
23
|
+
readonly targetAdvisories: readonly KitUpgradeTargetAdvisory[];
|
|
24
|
+
readonly manifestDigests: Readonly<Record<string, string>>;
|
|
25
|
+
readonly targetConfigDigest: {
|
|
26
|
+
readonly file: string;
|
|
27
|
+
readonly sha256: string | null;
|
|
28
|
+
};
|
|
29
|
+
readonly lockfileDigests: Readonly<Record<string, string>>;
|
|
30
|
+
readonly workspaceRoots: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
export interface KitUpgradeApplyResult {
|
|
33
|
+
readonly manifests: readonly string[];
|
|
34
|
+
readonly lockfiles: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export type KitUpgradeLockfileRunner = (workspaceRoot: string) => void;
|
|
37
|
+
export declare function planKitUpgrade(gameDirectory: string, lookup?: LatestKitPackageLookup, targetsFileName?: string): Promise<KitUpgradePlan>;
|
|
38
|
+
export declare function applyKitUpgrade(plan: KitUpgradePlan, runLockfileUpdate?: KitUpgradeLockfileRunner): KitUpgradeApplyResult;
|
|
39
|
+
export declare function lookupPublishedKitPackage(packageName: string, gameRoot: string): Promise<PublishedKitPackage>;
|
|
40
|
+
export declare function parsePublishedKitPackageResponse(source: string, packageName: string): PublishedKitPackage;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { execFile as execFileCallback, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { compare, intersects, minVersion, satisfies, valid, validRange } from 'semver';
|
|
7
|
+
import { inspectKitUpgradeTargetReadiness, } from './kit-upgrade-target-readiness.js';
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
const dependencySections = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
10
|
+
export async function planKitUpgrade(gameDirectory, lookup = lookupPublishedKitPackage, targetsFileName) {
|
|
11
|
+
const gameRoot = realpathSync(path.resolve(gameDirectory));
|
|
12
|
+
const gameManifest = path.join(gameRoot, 'package.json');
|
|
13
|
+
if (!existsSync(gameManifest)) {
|
|
14
|
+
throw new Error('Game package.json is missing: ' + gameManifest);
|
|
15
|
+
}
|
|
16
|
+
if (lstatSync(gameManifest).isSymbolicLink() || !inside(gameRoot, realpathSync(gameManifest))) {
|
|
17
|
+
throw new Error('Refusing to update a linked game package.json: ' + gameManifest);
|
|
18
|
+
}
|
|
19
|
+
const notes = [];
|
|
20
|
+
const blockers = [];
|
|
21
|
+
const { manifests, targetConfigDigest } = discoverGameManifests(gameRoot, notes, blockers, targetsFileName);
|
|
22
|
+
const targetAdvisories = inspectKitUpgradeTargetReadiness(gameRoot, targetConfigDigest.file);
|
|
23
|
+
const manifestDigests = {};
|
|
24
|
+
const requested = new Set();
|
|
25
|
+
const parsedManifests = new Map();
|
|
26
|
+
for (const manifest of manifests) {
|
|
27
|
+
const source = readFileSync(manifest, 'utf8');
|
|
28
|
+
manifestDigests[manifest] = digest(source);
|
|
29
|
+
const parsed = parseJsonObject(source, manifest);
|
|
30
|
+
parsedManifests.set(manifest, parsed);
|
|
31
|
+
for (const section of dependencySections) {
|
|
32
|
+
for (const [name, value] of Object.entries(asObject(parsed[section]))) {
|
|
33
|
+
if (!name.startsWith('@mpgd/') || typeof value !== 'string') {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!isKitPackageName(name)) {
|
|
37
|
+
blockers.push('Invalid Kit package name in ' + manifest + ': ' + name);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (parseVersionSpecifier(value) === undefined) {
|
|
41
|
+
if (validRange(value) !== null) {
|
|
42
|
+
blockers.push('Unsupported Kit version range for ' + name + ' in ' + manifest
|
|
43
|
+
+ ': ' + value + '. Pin an exact version, ^version, or ~version before upgrading.');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
notes.push('Skipped non-SemVer dependency ' + name + ' in ' + manifest + ': ' + value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
requested.add(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const packages = new Map();
|
|
56
|
+
const names = [...requested].sort();
|
|
57
|
+
for (let index = 0; index < names.length; index += 4) {
|
|
58
|
+
await Promise.all(names.slice(index, index + 4).map(async (name) => {
|
|
59
|
+
try {
|
|
60
|
+
const published = await lookup(name, gameRoot);
|
|
61
|
+
if (valid(published.version) === null) {
|
|
62
|
+
throw new Error('registry returned a non-SemVer latest version');
|
|
63
|
+
}
|
|
64
|
+
packages.set(name, published);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
blockers.push('Cannot resolve npm latest for ' + name + ': ' + formatError(error));
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
const updates = [];
|
|
72
|
+
const peerSubjects = [];
|
|
73
|
+
for (const manifest of manifests) {
|
|
74
|
+
const parsed = parsedManifests.get(manifest);
|
|
75
|
+
if (parsed === undefined) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const section of dependencySections) {
|
|
79
|
+
for (const [name, value] of Object.entries(asObject(parsed[section]))) {
|
|
80
|
+
if (!name.startsWith('@mpgd/') || typeof value !== 'string') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const current = parseVersionSpecifier(value);
|
|
84
|
+
const published = packages.get(name);
|
|
85
|
+
if (current === undefined || published === undefined) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (compare(published.version, current.version) < 0) {
|
|
89
|
+
notes.push('Kept ' + name + ' in ' + manifest + ': current version is newer than npm latest.');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const next = compare(published.version, current.version) > 0
|
|
93
|
+
? current.prefix + published.version
|
|
94
|
+
: value;
|
|
95
|
+
const subject = {
|
|
96
|
+
manifest,
|
|
97
|
+
section,
|
|
98
|
+
packageName: name,
|
|
99
|
+
current: value,
|
|
100
|
+
next,
|
|
101
|
+
latestVersion: published.version,
|
|
102
|
+
};
|
|
103
|
+
peerSubjects.push(subject);
|
|
104
|
+
if (next !== value) {
|
|
105
|
+
updates.push(subject);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
validatePeers(peerSubjects, updates, packages, parsedManifests, gameManifest, blockers, notes);
|
|
112
|
+
const { lockfileDigests, workspaceRoots } = discoverLockfiles(gameRoot, manifests, blockers, notes);
|
|
113
|
+
const cliUpdate = updates.find((update) => update.packageName === '@mpgd/cli');
|
|
114
|
+
if (process.env.MPGD_KIT_PATH !== undefined && cliUpdate !== undefined) {
|
|
115
|
+
const kitPackageFile = path.resolve(process.env.MPGD_KIT_PATH, 'packages/cli/package.json');
|
|
116
|
+
if (existsSync(kitPackageFile)) {
|
|
117
|
+
const kitPackage = parseJsonObject(readFileSync(kitPackageFile, 'utf8'), kitPackageFile);
|
|
118
|
+
if (kitPackage.version !== cliUpdate.latestVersion) {
|
|
119
|
+
notes.push('MPGD_KIT_PATH still has CLI ' + String(kitPackage.version)
|
|
120
|
+
+ '; select the published ' + cliUpdate.latestVersion + ' Kit tag before a release.');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
gameRoot,
|
|
126
|
+
updates,
|
|
127
|
+
blockers,
|
|
128
|
+
notes: [...new Set(notes)],
|
|
129
|
+
targetAdvisories,
|
|
130
|
+
manifestDigests,
|
|
131
|
+
targetConfigDigest,
|
|
132
|
+
lockfileDigests,
|
|
133
|
+
workspaceRoots,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function applyKitUpgrade(plan, runLockfileUpdate = updatePnpmLockfile) {
|
|
137
|
+
if (plan.blockers.length > 0) {
|
|
138
|
+
throw new Error('Kit upgrade is blocked:\n' + plan.blockers.join('\n'));
|
|
139
|
+
}
|
|
140
|
+
if (plan.updates.length === 0) {
|
|
141
|
+
return { manifests: [], lockfiles: [] };
|
|
142
|
+
}
|
|
143
|
+
const { file: targetsFile, sha256: targetsDigest } = plan.targetConfigDigest;
|
|
144
|
+
if (targetsDigest === null) {
|
|
145
|
+
if (existsSync(targetsFile)) {
|
|
146
|
+
throw new Error('File changed after upgrade planning: ' + targetsFile);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (!existsSync(targetsFile) || digest(readFileSync(targetsFile, 'utf8')) !== targetsDigest) {
|
|
150
|
+
throw new Error('File changed after upgrade planning: ' + targetsFile);
|
|
151
|
+
}
|
|
152
|
+
const snapshots = new Map();
|
|
153
|
+
for (const [file, expected] of Object.entries({
|
|
154
|
+
...plan.manifestDigests,
|
|
155
|
+
...plan.lockfileDigests,
|
|
156
|
+
})) {
|
|
157
|
+
const current = readFileSync(file, 'utf8');
|
|
158
|
+
if (digest(current) !== expected) {
|
|
159
|
+
throw new Error('File changed after upgrade planning: ' + file);
|
|
160
|
+
}
|
|
161
|
+
snapshots.set(file, current);
|
|
162
|
+
}
|
|
163
|
+
const changedManifests = [...new Set(plan.updates.map((update) => update.manifest))];
|
|
164
|
+
try {
|
|
165
|
+
for (const manifest of changedManifests) {
|
|
166
|
+
const source = snapshots.get(manifest);
|
|
167
|
+
if (source === undefined) {
|
|
168
|
+
throw new Error('Missing manifest snapshot: ' + manifest);
|
|
169
|
+
}
|
|
170
|
+
const parsed = parseJsonObject(source, manifest);
|
|
171
|
+
for (const update of plan.updates.filter((item) => item.manifest === manifest)) {
|
|
172
|
+
const section = asObject(parsed[update.section]);
|
|
173
|
+
if (section[update.packageName] !== update.current) {
|
|
174
|
+
throw new Error('Dependency changed after planning: ' + update.packageName);
|
|
175
|
+
}
|
|
176
|
+
section[update.packageName] = update.next;
|
|
177
|
+
parsed[update.section] = section;
|
|
178
|
+
}
|
|
179
|
+
writeFileSync(manifest, stringifyLike(source, parsed));
|
|
180
|
+
}
|
|
181
|
+
for (const root of plan.workspaceRoots) {
|
|
182
|
+
runLockfileUpdate(root);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const restoreFailures = [];
|
|
187
|
+
for (const [file, original] of snapshots) {
|
|
188
|
+
try {
|
|
189
|
+
writeFileSync(file, original);
|
|
190
|
+
}
|
|
191
|
+
catch (restoreError) {
|
|
192
|
+
restoreFailures.push(file + ': ' + formatError(restoreError));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const restoration = restoreFailures.length === 0
|
|
196
|
+
? 'Original manifests and lockfiles were rolled back.'
|
|
197
|
+
: 'Rollback incomplete:\n' + restoreFailures.join('\n');
|
|
198
|
+
throw new Error('Kit upgrade failed during lockfile update: ' + formatError(error)
|
|
199
|
+
+ '\n' + restoration);
|
|
200
|
+
}
|
|
201
|
+
return { manifests: changedManifests, lockfiles: Object.keys(plan.lockfileDigests) };
|
|
202
|
+
}
|
|
203
|
+
export async function lookupPublishedKitPackage(packageName, gameRoot) {
|
|
204
|
+
if (!isKitPackageName(packageName)) {
|
|
205
|
+
throw new Error('Invalid Kit package name: ' + packageName);
|
|
206
|
+
}
|
|
207
|
+
const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
208
|
+
const { stdout } = await execFile(executable, [
|
|
209
|
+
'view',
|
|
210
|
+
packageName + '@latest',
|
|
211
|
+
'version',
|
|
212
|
+
'peerDependencies',
|
|
213
|
+
'peerDependenciesMeta',
|
|
214
|
+
'--json',
|
|
215
|
+
], {
|
|
216
|
+
cwd: gameRoot,
|
|
217
|
+
encoding: 'utf8',
|
|
218
|
+
maxBuffer: 512 * 1024,
|
|
219
|
+
timeout: 20_000,
|
|
220
|
+
shell: process.platform === 'win32',
|
|
221
|
+
});
|
|
222
|
+
return parsePublishedKitPackageResponse(String(stdout), packageName);
|
|
223
|
+
}
|
|
224
|
+
export function parsePublishedKitPackageResponse(source, packageName) {
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(source);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
throw new Error('Invalid npm view JSON for ' + packageName + ': ' + formatError(error));
|
|
231
|
+
}
|
|
232
|
+
const response = typeof parsed === 'string' ? { version: parsed } : asObject(parsed);
|
|
233
|
+
if (typeof response.version !== 'string') {
|
|
234
|
+
throw new Error('Registry did not return a latest version for ' + packageName);
|
|
235
|
+
}
|
|
236
|
+
const peers = asObject(response.peerDependencies);
|
|
237
|
+
const peerMetadata = asObject(response.peerDependenciesMeta);
|
|
238
|
+
const declaredPeers = Object.entries(peers).filter((entry) => typeof entry[1] === 'string');
|
|
239
|
+
const optionalPeerDependencies = declaredPeers
|
|
240
|
+
.filter(([name]) => asObject(peerMetadata[name]).optional === true)
|
|
241
|
+
.map(([name]) => name);
|
|
242
|
+
return {
|
|
243
|
+
version: response.version,
|
|
244
|
+
peerDependencies: Object.fromEntries(declaredPeers),
|
|
245
|
+
...(optionalPeerDependencies.length > 0 ? { optionalPeerDependencies } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function discoverGameManifests(gameRoot, notes, blockers, targetsFileName) {
|
|
249
|
+
const manifests = new Set([path.join(gameRoot, 'package.json')]);
|
|
250
|
+
const targetsFile = path.resolve(gameRoot, targetsFileName ?? 'mpgd.targets.json');
|
|
251
|
+
if (!inside(gameRoot, targetsFile)) {
|
|
252
|
+
throw new Error('Targets file must be inside the game directory: ' + targetsFile);
|
|
253
|
+
}
|
|
254
|
+
if (!existsSync(targetsFile)) {
|
|
255
|
+
if (targetsFileName === undefined) {
|
|
256
|
+
notes.push('No mpgd.targets.json found; only the game manifest will be updated.');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
blockers.push('Requested targets file is missing: ' + targetsFile);
|
|
260
|
+
}
|
|
261
|
+
return { manifests: [...manifests], targetConfigDigest: { file: targetsFile, sha256: null } };
|
|
262
|
+
}
|
|
263
|
+
if (lstatSync(targetsFile).isSymbolicLink() || !inside(gameRoot, realpathSync(targetsFile))) {
|
|
264
|
+
throw new Error('Refusing to read a linked targets file: ' + targetsFile);
|
|
265
|
+
}
|
|
266
|
+
const targetsSource = readFileSync(targetsFile, 'utf8');
|
|
267
|
+
const config = parseJsonObject(targetsSource, targetsFile);
|
|
268
|
+
const targetsDirectory = path.dirname(targetsFile);
|
|
269
|
+
for (const value of Object.values(asObject(config.targets))) {
|
|
270
|
+
const target = asObject(value);
|
|
271
|
+
for (const key of ['wrapperApp', 'shellApp']) {
|
|
272
|
+
const location = target[key];
|
|
273
|
+
if (typeof location !== 'string') {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (location.includes('${MPGD_KIT_PATH}')) {
|
|
277
|
+
notes.push('Skipped external target ' + key + ': ' + location);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const resolvedLocation = location
|
|
281
|
+
.replaceAll('${MPGD_GAME_ROOT}', targetsDirectory)
|
|
282
|
+
.replaceAll('${MPGD_GAME_APP_ROOT}', targetsDirectory);
|
|
283
|
+
if (resolvedLocation.includes('$' + '{')) {
|
|
284
|
+
notes.push('Skipped target with unresolved path token: ' + location);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const directory = path.resolve(targetsDirectory, resolvedLocation);
|
|
288
|
+
if (!inside(gameRoot, directory)) {
|
|
289
|
+
notes.push('Skipped target outside the game directory: ' + location);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const manifest = path.join(directory, 'package.json');
|
|
293
|
+
if (!existsSync(manifest)) {
|
|
294
|
+
notes.push('Skipped target without package.json: ' + location);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (lstatSync(manifest).isSymbolicLink() || !inside(gameRoot, realpathSync(manifest))) {
|
|
298
|
+
notes.push('Skipped linked target manifest: ' + location);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
manifests.add(manifest);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
manifests: [...manifests].sort(),
|
|
306
|
+
targetConfigDigest: { file: targetsFile, sha256: digest(targetsSource) },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function discoverLockfiles(gameRoot, manifests, blockers, notes) {
|
|
310
|
+
const roots = new Set();
|
|
311
|
+
let boundary = gameRoot;
|
|
312
|
+
for (;;) {
|
|
313
|
+
if (existsSync(path.join(boundary, '.git'))) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
const parent = path.dirname(boundary);
|
|
317
|
+
if (parent === boundary) {
|
|
318
|
+
boundary = gameRoot;
|
|
319
|
+
notes.push('No git boundary found; lockfile discovery is limited to the game directory.');
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
boundary = parent;
|
|
323
|
+
}
|
|
324
|
+
for (const manifest of manifests) {
|
|
325
|
+
let directory = path.dirname(manifest);
|
|
326
|
+
while (inside(boundary, directory)) {
|
|
327
|
+
const lockfile = path.join(directory, 'pnpm-lock.yaml');
|
|
328
|
+
if (existsSync(lockfile)) {
|
|
329
|
+
if (lstatSync(lockfile).isSymbolicLink()) {
|
|
330
|
+
blockers.push('Refusing to rewrite a linked pnpm lockfile: ' + lockfile);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
roots.add(directory);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (directory === boundary) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
directory = path.dirname(directory);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (roots.size === 0) {
|
|
343
|
+
blockers.push('No pnpm-lock.yaml found for the game and its target manifests.');
|
|
344
|
+
}
|
|
345
|
+
const workspaceRoots = [...roots].sort((left, right) => left.length - right.length);
|
|
346
|
+
return {
|
|
347
|
+
workspaceRoots,
|
|
348
|
+
lockfileDigests: Object.fromEntries(workspaceRoots.map((root) => {
|
|
349
|
+
const file = path.join(root, 'pnpm-lock.yaml');
|
|
350
|
+
return [file, digest(readFileSync(file, 'utf8'))];
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function validatePeers(subjects, updates, packages, manifests, gameManifest, blockers, notes) {
|
|
355
|
+
const replacements = new Map(updates.map((update) => [update.manifest + '\0' + update.packageName, update.next]));
|
|
356
|
+
for (const subject of subjects) {
|
|
357
|
+
const published = packages.get(subject.packageName);
|
|
358
|
+
const peers = published?.peerDependencies ?? {};
|
|
359
|
+
const optionalPeers = new Set(published?.optionalPeerDependencies ?? []);
|
|
360
|
+
for (const [peerName, range] of Object.entries(peers)) {
|
|
361
|
+
if (validRange(range) === null) {
|
|
362
|
+
blockers.push('Invalid published peer range for ' + subject.packageName + ': '
|
|
363
|
+
+ peerName + ' ' + range);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const owner = findPeerOwner(manifests, subject.manifest, gameManifest, peerName);
|
|
367
|
+
if (owner === undefined) {
|
|
368
|
+
if (!optionalPeers.has(peerName)) {
|
|
369
|
+
notes.push(subject.packageName + ' requires peer ' + peerName + ' ' + range
|
|
370
|
+
+ '; no direct version was declared. Check the resolved lockfile.');
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const specifier = replacements.get(owner.manifest + '\0' + peerName) ?? owner.specifier;
|
|
375
|
+
const parsed = parseVersionSpecifier(specifier);
|
|
376
|
+
if (parsed !== undefined) {
|
|
377
|
+
if (!intersects(specifier, range)) {
|
|
378
|
+
blockers.push(subject.packageName + '@' + subject.latestVersion + ' requires '
|
|
379
|
+
+ peerName + ' ' + range + ', incompatible with ' + specifier + '.');
|
|
380
|
+
}
|
|
381
|
+
else if (!satisfies(parsed.version, range)) {
|
|
382
|
+
notes.push(subject.packageName + ' peer ' + peerName + ' requires ' + range
|
|
383
|
+
+ '; confirm the installed version selected by ' + specifier + '.');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else if (validRange(specifier) !== null && !intersects(specifier, range)) {
|
|
387
|
+
blockers.push(subject.packageName + '@' + subject.latestVersion + ' requires '
|
|
388
|
+
+ peerName + ' ' + range + ', incompatible with ' + specifier + '.');
|
|
389
|
+
}
|
|
390
|
+
else if (validRange(specifier) !== null) {
|
|
391
|
+
const minimum = minVersion(specifier);
|
|
392
|
+
if (minimum !== null && !satisfies(minimum, range)) {
|
|
393
|
+
notes.push(subject.packageName + ' peer ' + peerName + ' requires ' + range
|
|
394
|
+
+ '; verify the resolved version inside ' + specifier + '.');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
notes.push('Could not verify peer ' + peerName + ' for ' + subject.packageName
|
|
399
|
+
+ ' from specifier ' + specifier + '.');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function findPeerOwner(manifests, packageManifest, gameManifest, peerName) {
|
|
405
|
+
for (const manifest of [packageManifest, gameManifest]) {
|
|
406
|
+
const specifier = dependencySpecifier(manifests.get(manifest), peerName);
|
|
407
|
+
if (specifier !== undefined) {
|
|
408
|
+
return { manifest, specifier };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const candidates = [...manifests].flatMap(([manifest, content]) => {
|
|
412
|
+
const specifier = dependencySpecifier(content, peerName);
|
|
413
|
+
return specifier === undefined ? [] : [{ manifest, specifier }];
|
|
414
|
+
});
|
|
415
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
416
|
+
}
|
|
417
|
+
function dependencySpecifier(manifest, name) {
|
|
418
|
+
if (manifest === undefined) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
for (const section of dependencySections) {
|
|
422
|
+
const value = asObject(manifest[section])[name];
|
|
423
|
+
if (typeof value === 'string') {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
function parseVersionSpecifier(value) {
|
|
430
|
+
if (valid(value) !== null) {
|
|
431
|
+
return { prefix: '', version: value };
|
|
432
|
+
}
|
|
433
|
+
const prefix = value[0];
|
|
434
|
+
if ((prefix === '^' || prefix === '~') && valid(value.slice(1)) !== null) {
|
|
435
|
+
return { prefix, version: value.slice(1) };
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
function updatePnpmLockfile(workspaceRoot) {
|
|
440
|
+
const result = spawnSync(process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm', ['install', '--lockfile-only', '--ignore-scripts'], {
|
|
441
|
+
cwd: workspaceRoot,
|
|
442
|
+
encoding: 'utf8',
|
|
443
|
+
timeout: 180_000,
|
|
444
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
445
|
+
shell: process.platform === 'win32',
|
|
446
|
+
});
|
|
447
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
448
|
+
throw new Error('pnpm lockfile update failed in ' + workspaceRoot + ': '
|
|
449
|
+
+ formatError(result.error ?? (result.stderr || result.stdout)));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function parseJsonObject(source, label) {
|
|
453
|
+
let parsed;
|
|
454
|
+
try {
|
|
455
|
+
parsed = JSON.parse(source);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
throw new Error('Invalid JSON in ' + label + ': ' + formatError(error));
|
|
459
|
+
}
|
|
460
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
461
|
+
throw new Error('Expected a JSON object in ' + label);
|
|
462
|
+
}
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
function asObject(value) {
|
|
466
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
467
|
+
? value
|
|
468
|
+
: {};
|
|
469
|
+
}
|
|
470
|
+
function stringifyLike(original, value) {
|
|
471
|
+
const indent = original.match(/\r?\n([ \t]+)"/u)?.[1] ?? ' ';
|
|
472
|
+
const newline = original.includes('\r\n') ? '\r\n' : '\n';
|
|
473
|
+
return JSON.stringify(value, null, indent).replaceAll('\n', newline)
|
|
474
|
+
+ (original.endsWith('\n') ? newline : '');
|
|
475
|
+
}
|
|
476
|
+
function inside(root, candidate) {
|
|
477
|
+
const relative = path.relative(root, candidate);
|
|
478
|
+
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..'
|
|
479
|
+
&& !path.isAbsolute(relative));
|
|
480
|
+
}
|
|
481
|
+
function digest(source) {
|
|
482
|
+
return createHash('sha256').update(source).digest('hex');
|
|
483
|
+
}
|
|
484
|
+
function isKitPackageName(name) {
|
|
485
|
+
return /^@mpgd\/[a-z0-9][a-z0-9._-]*$/u.test(name);
|
|
486
|
+
}
|
|
487
|
+
function formatError(error) {
|
|
488
|
+
return error instanceof Error ? error.message : String(error);
|
|
489
|
+
}
|