@mpgd/cli 0.33.1 → 0.34.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-verify.js +74 -17
- package/dist/capacitor-shell-starter.d.ts +40 -0
- package/dist/capacitor-shell-starter.js +2130 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +97 -5
- package/dist/native-shell-identity.d.ts +19 -0
- package/dist/native-shell-identity.js +490 -0
- package/dist/production-target-readiness.d.ts +1 -0
- package/dist/production-target-readiness.js +1 -1
- package/package.json +14 -13
- package/templates/phaser-game/package.json +2 -0
- package/templates/phaser-game/src/styles.css +6 -4
- package/templates/phaser-game/vite.runtime-target-config.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
2
3
|
import { lstat, open, opendir, realpath, stat } from 'node:fs/promises';
|
|
3
4
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
5
|
import { verifyZipV1Archive, ZipDecodeError, } from '@mpgd/phaser-assets/archive-validation';
|
|
@@ -6,6 +7,7 @@ import { defaultArchiveWorkerLimits } from '@mpgd/phaser-assets/archives';
|
|
|
6
7
|
import { validatePhaserPackDeliveryManifest, } from '@mpgd/phaser-assets/pack-format';
|
|
7
8
|
const DEFAULT_MANIFEST_BYTE_CAP = 32 * 1024 * 1024;
|
|
8
9
|
const DEFAULT_VERIFY_TIMEOUT_MS = 120_000;
|
|
10
|
+
const FIFO_WRITER_GRACE_MS = 1000;
|
|
9
11
|
/** The watchdog arms one millisecond after the deadline. Leave one
|
|
10
12
|
* millisecond of headroom so the largest accepted timeout plus that offset
|
|
11
13
|
* still fits the platform timer range instead of clamping to 1 ms. */
|
|
@@ -255,10 +257,10 @@ const inventoryRoot = async (root, deadline) => {
|
|
|
255
257
|
return { files, bytes, largest, truncated };
|
|
256
258
|
};
|
|
257
259
|
const readManifestCapped = async (manifestPath, cap, deadline) => {
|
|
258
|
-
// Open through the deadline
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
const handle = await deadline.race(open(resolve(manifestPath),
|
|
260
|
+
// Open through the deadline. POSIX O_NONBLOCK keeps FIFO open/read calls
|
|
261
|
+
// bounded; Windows retains the stream fallback under the same deadline.
|
|
262
|
+
const flags = process.platform === 'win32' ? 'r' : constants.O_RDONLY | constants.O_NONBLOCK;
|
|
263
|
+
const handle = await deadline.race(open(resolve(manifestPath), flags));
|
|
262
264
|
const chunks = [];
|
|
263
265
|
let received = 0;
|
|
264
266
|
let stalled = false;
|
|
@@ -292,30 +294,85 @@ const readManifestCapped = async (manifestPath, cap, deadline) => {
|
|
|
292
294
|
throw new Error(`Delivery manifest changed while reading (expected ${expectedBytes} bytes, received more)`);
|
|
293
295
|
}
|
|
294
296
|
}
|
|
297
|
+
else if (process.platform !== 'win32') {
|
|
298
|
+
// A blocking FIFO read can remain pending after stream.destroy() on
|
|
299
|
+
// some Linux hosts. O_NONBLOCK bounds each read: EAGAIN means a writer
|
|
300
|
+
// is connected but has no data; a later zero-byte read is its EOF.
|
|
301
|
+
const buffer = Buffer.allocUnsafe(Math.min(STREAM_CHUNK_BYTES, cap + 1));
|
|
302
|
+
const isFifo = info.isFIFO();
|
|
303
|
+
// A writer can open and close between read polls without ever yielding
|
|
304
|
+
// EAGAIN. Bound the initial connection wait so that empty EOF cannot
|
|
305
|
+
// consume the default two-minute verification budget.
|
|
306
|
+
const writerGraceUntil = performance.now() + Math.min(FIFO_WRITER_GRACE_MS, deadline.remainingMs());
|
|
307
|
+
let writerObserved = false;
|
|
308
|
+
while (true) {
|
|
309
|
+
deadline.sample();
|
|
310
|
+
let bytesRead;
|
|
311
|
+
let wouldBlock = false;
|
|
312
|
+
try {
|
|
313
|
+
({ bytesRead } = await deadline.race(handle.read(buffer, 0, buffer.byteLength, null)));
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (typeof error !== 'object'
|
|
317
|
+
|| error === null
|
|
318
|
+
|| !('code' in error)
|
|
319
|
+
|| (error.code !== 'EAGAIN' && error.code !== 'EWOULDBLOCK')) {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
writerObserved = true;
|
|
323
|
+
wouldBlock = true;
|
|
324
|
+
bytesRead = 0;
|
|
325
|
+
}
|
|
326
|
+
if (bytesRead === 0) {
|
|
327
|
+
// Other sources have ordinary EOF. An unconnected FIFO waits only
|
|
328
|
+
// for its bounded startup grace; an observed writer has hung up.
|
|
329
|
+
if (!wouldBlock
|
|
330
|
+
&& (!isFifo || writerObserved || performance.now() >= writerGraceUntil)) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
const waitMs = Math.min(10, Math.max(1, deadline.remainingMs()));
|
|
334
|
+
await deadline.race(new Promise((resume) => setTimeout(resume, waitMs)));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
writerObserved = true;
|
|
338
|
+
received += bytesRead;
|
|
339
|
+
if (received > cap) {
|
|
340
|
+
throw new Error(`Delivery manifest exceeds ${cap} bytes`);
|
|
341
|
+
}
|
|
342
|
+
chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
|
|
343
|
+
deadline.sample();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
295
346
|
else {
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
// rejected race.
|
|
347
|
+
// Windows non-regular sources retain the stream watchdog. A pending
|
|
348
|
+
// read must be destroyed at the deadline, then the race can return
|
|
349
|
+
// without waiting for that read to settle.
|
|
300
350
|
const stream = handle.createReadStream({
|
|
301
351
|
highWaterMark: STREAM_CHUNK_BYTES,
|
|
302
352
|
autoClose: false,
|
|
303
353
|
});
|
|
304
354
|
const timer = deadline.armStream(stream);
|
|
305
355
|
try {
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
356
|
+
await deadline.race((async () => {
|
|
357
|
+
for await (const chunk of stream) {
|
|
358
|
+
const view = chunk;
|
|
359
|
+
received += view.byteLength;
|
|
360
|
+
if (received > cap) {
|
|
361
|
+
stream.destroy();
|
|
362
|
+
throw new Error(`Delivery manifest exceeds ${cap} bytes`);
|
|
363
|
+
}
|
|
364
|
+
chunks.push(Buffer.from(view));
|
|
365
|
+
deadline.sample();
|
|
312
366
|
}
|
|
313
|
-
|
|
314
|
-
deadline.sample();
|
|
315
|
-
}
|
|
367
|
+
})());
|
|
316
368
|
}
|
|
317
369
|
finally {
|
|
370
|
+
// Destroy may not wake a pending FIFO read until its writer closes.
|
|
371
|
+
// The raced deadline returns the report without waiting for that read.
|
|
318
372
|
clearTimeout(timer);
|
|
373
|
+
if (!stream.destroyed) {
|
|
374
|
+
stream.destroy();
|
|
375
|
+
}
|
|
319
376
|
}
|
|
320
377
|
}
|
|
321
378
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
interface PlannedFile {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CapacitorShellStarterInput {
|
|
6
|
+
readonly gameRoot: string;
|
|
7
|
+
readonly appId: string;
|
|
8
|
+
readonly displayName: string;
|
|
9
|
+
readonly iconSource?: string;
|
|
10
|
+
readonly backendUrl?: string;
|
|
11
|
+
/** Selection metadata only; a provider SDK is not installed or enabled here. */
|
|
12
|
+
readonly providerIds?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface CapacitorShellStarterPlan {
|
|
15
|
+
readonly gameRoot: string;
|
|
16
|
+
readonly input: CapacitorShellStarterInput;
|
|
17
|
+
readonly changedFiles: readonly string[];
|
|
18
|
+
readonly nativePlatformsToAdd: readonly ('android' | 'ios')[];
|
|
19
|
+
readonly files: readonly PlannedFile[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Produce the complete game-owned shell change list before writing anything.
|
|
23
|
+
* Existing Android/iOS projects, signing files, and custom controllers are
|
|
24
|
+
* never rewritten by this initializer.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planCapacitorShellStarter(input: CapacitorShellStarterInput): CapacitorShellStarterPlan;
|
|
27
|
+
export declare function applyCapacitorShellStarter(plan: CapacitorShellStarterPlan, installStagedFile?: (temporary: string, destination: string) => void): void;
|
|
28
|
+
export interface CapacitorShellCommandRunner {
|
|
29
|
+
run(command: string, args: readonly string[], cwd: string): void;
|
|
30
|
+
}
|
|
31
|
+
/** Materialize only missing native projects after the non-destructive plan is applied. */
|
|
32
|
+
export declare function materializeCapacitorShellStarter(plan: CapacitorShellStarterPlan, runner?: CapacitorShellCommandRunner): void;
|
|
33
|
+
/** Fail closed when a Release plist hardcodes values independently of Xcode build settings. */
|
|
34
|
+
export declare function assertIosReleasePlistIdentity(source: string): void;
|
|
35
|
+
export declare function assertAndroidReleaseDisplayName(shellApp: string, expectedDisplayName: string): void;
|
|
36
|
+
export declare function assertAndroidReleaseLauncherIntegrity(shellApp: string): void;
|
|
37
|
+
export declare function assertIosReleaseDisplayName(shellApp: string, expectedDisplayName: string): void;
|
|
38
|
+
export declare function assertIosArchiveScheme(shellApp: string): void;
|
|
39
|
+
export declare function decodeAndroidStringResource(value: string): string;
|
|
40
|
+
export {};
|