@playdrop/playdrop-cli 0.17.5 → 0.17.15
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/README.md +12 -0
- package/config/client-meta.json +2 -2
- package/dist/apps/build.d.ts +2 -1
- package/dist/apps/build.js +6 -2
- package/dist/apps/index.d.ts +2 -3
- package/dist/apps/index.js +17 -49
- package/dist/apps/loadCheck.d.ts +3 -1
- package/dist/apps/loadCheck.js +123 -64
- package/dist/apps/publishingRuntime.d.ts +18 -0
- package/dist/apps/publishingRuntime.js +111 -0
- package/dist/apps/runtimeSyntax.d.ts +7 -0
- package/dist/apps/runtimeSyntax.js +154 -0
- package/dist/apps/serverBuild.d.ts +3 -3
- package/dist/apps/serverBuild.js +8 -43
- package/dist/apps/serverSdk.d.ts +23 -0
- package/dist/apps/serverSdk.js +157 -0
- package/dist/apps/upload.d.ts +9 -3
- package/dist/apps/upload.js +71 -25
- package/dist/apps/validate.d.ts +2 -1
- package/dist/apps/validate.js +3 -1
- package/dist/commandContext.js +34 -10
- package/dist/commands/build.js +3 -1
- package/dist/commands/create.js +35 -16
- package/dist/commands/dev.js +11 -0
- package/dist/commands/devGameServer.d.ts +8 -0
- package/dist/commands/devGameServer.js +12 -9
- package/dist/commands/devServer.d.ts +1 -0
- package/dist/commands/devServer.js +21 -3
- package/dist/commands/source.d.ts +11 -3
- package/dist/commands/source.js +82 -14
- package/dist/commands/taskCaptureSession.d.ts +1 -1
- package/dist/commands/taskCaptureSession.js +1 -1
- package/dist/commands/upload-content.js +13 -12
- package/dist/commands/upload.d.ts +2 -9
- package/dist/commands/upload.js +5 -21
- package/dist/commands/validate.js +1 -1
- package/dist/commands/worker/game-server-infra.d.ts +6 -0
- package/dist/commands/worker/game-server-infra.js +10 -0
- package/dist/commands/worker/game-source-git.d.ts +9 -1
- package/dist/commands/worker/game-source-git.js +65 -19
- package/dist/commands/worker/generation.d.ts +2 -0
- package/dist/commands/worker/generation.js +35 -5
- package/dist/commands/worker.d.ts +1 -1
- package/dist/commands/worker.js +57 -17
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +7 -0
- package/dist/index.js +10 -2
- package/dist/playwright.d.ts +6 -4
- package/dist/playwright.js +24 -9
- package/dist/proceduralValidation.d.ts +16 -0
- package/dist/proceduralValidation.js +227 -0
- package/dist/proceduralValidationBrowser.d.mts +3 -0
- package/dist/proceduralValidationBrowser.mjs +67 -0
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -1
- package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts +6 -1
- package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/chat.js +13 -1
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/procedural/dist/index.d.ts +201 -0
- package/node_modules/@playdrop/procedural/dist/index.d.ts.map +1 -0
- package/node_modules/@playdrop/procedural/dist/index.js +763 -0
- package/node_modules/@playdrop/procedural/package.json +36 -0
- package/node_modules/@playdrop/types/dist/api.d.ts +37 -4
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/chat.d.ts +7 -1
- package/node_modules/@playdrop/types/dist/chat.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/social.d.ts +20 -0
- package/node_modules/@playdrop/types/dist/social.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.d.ts +2 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -123,6 +123,18 @@ playdrop project dev my-app
|
|
|
123
123
|
playdrop project publish --env dev .
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
## Game server SDK
|
|
127
|
+
|
|
128
|
+
For a game with a `server` declaration, `project build`, `project check`, `project dev`, and upload preflight select the SDK reported by the environment's rooms runtime. The CLI verifies and downloads `playdrop-server-<version>.tgz` from the SDK CDN, installs its matching dependencies, and records them in `package.json`, `package-lock.json`, and `vendor/playdrop/`. Keep these files with the project. There is no npm registry release of `@playdrop/server` to install manually.
|
|
129
|
+
|
|
130
|
+
SDK selection is fixed for one CLI invocation. Start a new invocation to pick up a platform SDK release. Missing runtime metadata, unavailable artifacts, or integrity mismatches fail explicitly. Production uses the SDK included in the deployed rooms runtime; the SDK release lane publishes the matching client and server artifacts before promoting them.
|
|
131
|
+
|
|
132
|
+
Use Node.js 22 or later. Mixed client/server TypeScript projects should include both `src` and `server`, use `lib: ["ES2022", "DOM", "DOM.Iterable"]`, and retain `strict: true`. Set `skipLibCheck: true` for the Colyseus browser SDK's upstream declaration errors; game code remains checked. The CLI does not rewrite your TypeScript configuration.
|
|
133
|
+
|
|
134
|
+
Cloud and direct first-party uploads run the same checks against the staged files before publishing. Declared playtest tapes run too. Server games run the exact prepared server bundle on a private temporary port, with isolated matchmaking and the publishing job's database settings. Auth-required unpublished games use the creator's test-player account, not a token for a version that does not exist yet. A failed check stops publication with its error.
|
|
135
|
+
|
|
136
|
+
For direct first-party server uploads from this checkout, start `npm run dev:agent-platform` and load `infra/.env.dev` in the publishing process. Its `PLAYDROP_WORKER_GAME_SERVER_REDIS_ADMIN_URL` and `PLAYDROP_DEV_MONGO_URL` let the CLI create and clean up a temporary database lease for that upload. Cloud publishing receives the existing task lease directly and does not delete it until the task ends.
|
|
137
|
+
|
|
126
138
|
## Documentation
|
|
127
139
|
|
|
128
140
|
```bash
|
package/config/client-meta.json
CHANGED
package/dist/apps/build.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppPlaytestTapes, AppSurface, AppType } from "@playdrop/types";
|
|
2
2
|
import type { AppTask } from "../catalogue";
|
|
3
|
+
import { type ServerSdkOptions } from "./serverSdk";
|
|
3
4
|
export type AppBuildMetadata = {
|
|
4
5
|
displayName?: string;
|
|
5
6
|
subtitle?: string;
|
|
@@ -24,7 +25,7 @@ export type AppBuildArtifacts = {
|
|
|
24
25
|
sourceHash?: string;
|
|
25
26
|
metadata: AppBuildMetadata;
|
|
26
27
|
};
|
|
27
|
-
export type BuildAppOptions = {
|
|
28
|
+
export type BuildAppOptions = ServerSdkOptions & {
|
|
28
29
|
includeSourceArchive?: boolean;
|
|
29
30
|
};
|
|
30
31
|
export declare function runNpmScript(projectDir: string, script: string): Promise<void>;
|
package/dist/apps/build.js
CHANGED
|
@@ -9,6 +9,8 @@ const node_fs_1 = require("node:fs");
|
|
|
9
9
|
const promises_1 = require("node:fs/promises");
|
|
10
10
|
const node_path_1 = require("node:path");
|
|
11
11
|
const node_zlib_1 = require("node:zlib");
|
|
12
|
+
const runtimeSyntax_1 = require("./runtimeSyntax");
|
|
13
|
+
const serverSdk_1 = require("./serverSdk");
|
|
12
14
|
const BASE_SOURCE_IGNORE_PATTERNS = [
|
|
13
15
|
"node_modules/",
|
|
14
16
|
"dist/",
|
|
@@ -75,10 +77,11 @@ function readPackageJson(packageJsonPath) {
|
|
|
75
77
|
const raw = (0, node_fs_1.readFileSync)(packageJsonPath, "utf8");
|
|
76
78
|
return JSON.parse(raw);
|
|
77
79
|
}
|
|
78
|
-
async function ensureProjectBuild(task) {
|
|
80
|
+
async function ensureProjectBuild(task, options) {
|
|
79
81
|
if (!task.isTypescriptProject || !task.packageJsonPath) {
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
84
|
+
await (0, serverSdk_1.ensureGameServerSdk)(task, options);
|
|
82
85
|
if (!projectBuildCache.has(task.projectDir)) {
|
|
83
86
|
const promise = (async () => {
|
|
84
87
|
const pkg = readPackageJson(task.packageJsonPath);
|
|
@@ -614,7 +617,7 @@ function createSourceArchive(task) {
|
|
|
614
617
|
return { buffer, hash };
|
|
615
618
|
}
|
|
616
619
|
async function buildApp(task, options = {}) {
|
|
617
|
-
await ensureProjectBuild(task);
|
|
620
|
+
await ensureProjectBuild(task, options);
|
|
618
621
|
if (!(0, node_fs_1.existsSync)(task.filePath) || !(0, node_fs_1.statSync)(task.filePath).isFile()) {
|
|
619
622
|
const locationHint = task.cataloguePath || "catalogue.json";
|
|
620
623
|
const bundlePath = task.relativeFile || (0, node_path_1.basename)(task.filePath);
|
|
@@ -628,6 +631,7 @@ async function buildApp(task, options = {}) {
|
|
|
628
631
|
].join(" "));
|
|
629
632
|
}
|
|
630
633
|
const runtime = collectBundleFiles(task);
|
|
634
|
+
await (0, runtimeSyntax_1.validateRuntimeSyntax)(runtime.entries);
|
|
631
635
|
const runtimeHash = (0, node_crypto_1.createHash)("sha256");
|
|
632
636
|
const runtimeFiles = runtime.entries.map((entry) => {
|
|
633
637
|
const content = (0, node_fs_1.readFileSync)(entry.absolutePath);
|
package/dist/apps/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ApiClient } from "@playdrop/api-client";
|
|
|
2
2
|
import type { AgentTaskPlaytestProof, UserResponse } from "@playdrop/types";
|
|
3
3
|
import type { AppTask } from "../catalogue";
|
|
4
4
|
import type { TaskCaptureSession } from "../appUrls";
|
|
5
|
+
import type { GameServerDevServices } from "../commands/devGameServer";
|
|
5
6
|
import { collectAppValidationWarnings, validateAppTask, runFormatScript } from "./validate";
|
|
6
7
|
import { buildApp, type AppBuildArtifacts } from "./build";
|
|
7
8
|
import { prepareAppUploadFiles, uploadApp, type AppUploadResult, type AppUploadOptions } from "./upload";
|
|
@@ -18,14 +19,12 @@ export type AppPipelineOptions = {
|
|
|
18
19
|
webBase?: string | null;
|
|
19
20
|
token?: string | null;
|
|
20
21
|
user?: UserResponse | null;
|
|
21
|
-
runLocalLoadCheck?: boolean;
|
|
22
22
|
devRouterPort?: number;
|
|
23
|
-
|
|
23
|
+
gameServerDev?: GameServerDevServices;
|
|
24
24
|
loadCheckTimeoutMs?: number;
|
|
25
25
|
ensureRegisteredAppShell?: boolean;
|
|
26
26
|
agentTaskId?: number;
|
|
27
27
|
playtestProof?: AgentTaskPlaytestProof;
|
|
28
|
-
playtestSmokeCheckRequired?: boolean;
|
|
29
28
|
captureSession?: TaskCaptureSession | null;
|
|
30
29
|
artifacts?: AppBuildArtifacts;
|
|
31
30
|
prepareGitTransition?: AppUploadOptions["prepareGitTransition"];
|
package/dist/apps/index.js
CHANGED
|
@@ -12,67 +12,35 @@ const upload_1 = require("./upload");
|
|
|
12
12
|
Object.defineProperty(exports, "prepareAppUploadFiles", { enumerable: true, get: function () { return upload_1.prepareAppUploadFiles; } });
|
|
13
13
|
Object.defineProperty(exports, "uploadApp", { enumerable: true, get: function () { return upload_1.uploadApp; } });
|
|
14
14
|
const registration_1 = require("./registration");
|
|
15
|
-
const loadCheck_1 = require("./loadCheck");
|
|
16
15
|
async function runAppPipeline(client, task, options) {
|
|
17
|
-
await (0, validate_1.validateAppTask)(task);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error(`Missing creator username for required hosted app registration of "${task.name}".`);
|
|
25
|
-
}
|
|
26
|
-
await (0, registration_1.ensureRegisteredHostedAppShell)({
|
|
27
|
-
client,
|
|
28
|
-
creatorUsername,
|
|
29
|
-
task,
|
|
30
|
-
agentTaskId: options.agentTaskId,
|
|
31
|
-
});
|
|
16
|
+
await (0, validate_1.validateAppTask)(task, { apiBase: options?.apiBase });
|
|
17
|
+
const isExternal = task.hostingMode === "EXTERNAL" || Boolean(task.externalUrl);
|
|
18
|
+
if (!isExternal && options?.ensureRegisteredAppShell) {
|
|
19
|
+
const creatorUsername = options.creatorUsername?.trim();
|
|
20
|
+
if (!creatorUsername)
|
|
21
|
+
throw new Error(`Missing creator username for app "${task.name}" registration.`);
|
|
22
|
+
await (0, registration_1.ensureRegisteredHostedAppShell)({ client, creatorUsername, task, agentTaskId: options.agentTaskId });
|
|
32
23
|
}
|
|
33
|
-
const artifacts = isExternal
|
|
34
|
-
|
|
35
|
-
:
|
|
24
|
+
const artifacts = isExternal ? null : options?.artifacts ?? await (0, build_1.buildApp)(task, {
|
|
25
|
+
includeSourceArchive: options?.agentTaskId === undefined,
|
|
26
|
+
apiBase: options?.apiBase,
|
|
27
|
+
});
|
|
36
28
|
const warnings = (0, validate_1.collectAppValidationWarnings)(task, isExternal ? "source" : "bundle");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
const apiBase = typeof options.apiBase === "string" ? options.apiBase.trim() : "";
|
|
43
|
-
if (!apiBase) {
|
|
44
|
-
throw new Error(`Missing API base URL for local hosted load-check of "${task.name}".`);
|
|
45
|
-
}
|
|
46
|
-
const loadCheck = await (0, loadCheck_1.runLocalHostedLoadCheck)({
|
|
47
|
-
client,
|
|
48
|
-
apiBase,
|
|
49
|
-
webBase: options.webBase,
|
|
50
|
-
creatorUsername,
|
|
51
|
-
task,
|
|
52
|
-
devRouterPort: options.devRouterPort,
|
|
53
|
-
timeoutMs: options.loadCheckTimeoutMs,
|
|
54
|
-
token: options.token ?? null,
|
|
55
|
-
currentUser: options.user ?? null,
|
|
56
|
-
captureSession: options.captureSession,
|
|
57
|
-
});
|
|
58
|
-
if (loadCheck.status !== "PASSED") {
|
|
59
|
-
throw new Error((0, loadCheck_1.formatHostedLoadCheckFailure)(task.name, loadCheck, "local"));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const uploadOptions = {
|
|
29
|
+
// Cloud and direct publishing use the same uploaded-game checks. A separate
|
|
30
|
+
// local browser pass cannot validate the uploaded bytes or their server.
|
|
31
|
+
const upload = await (0, upload_1.uploadApp)(client, task, artifacts, {
|
|
32
|
+
apiBase: options?.apiBase,
|
|
63
33
|
skipReview: options?.skipReview,
|
|
64
34
|
clearTags: options?.clearTags,
|
|
65
35
|
creatorUsername: options?.creatorUsername,
|
|
66
36
|
token: options?.token,
|
|
67
37
|
user: options?.user ?? null,
|
|
68
|
-
|
|
38
|
+
gameServerDev: options?.gameServerDev,
|
|
69
39
|
loadCheckTimeoutMs: options?.loadCheckTimeoutMs,
|
|
70
40
|
agentTaskId: options?.agentTaskId,
|
|
71
41
|
playtestProof: options?.playtestProof,
|
|
72
|
-
playtestSmokeCheckRequired: options?.playtestSmokeCheckRequired,
|
|
73
42
|
captureSession: options?.captureSession,
|
|
74
43
|
prepareGitTransition: options?.prepareGitTransition,
|
|
75
|
-
};
|
|
76
|
-
const upload = await (0, upload_1.uploadApp)(client, task, artifacts, uploadOptions);
|
|
44
|
+
});
|
|
77
45
|
return { artifacts, upload, warnings };
|
|
78
46
|
}
|
package/dist/apps/loadCheck.d.ts
CHANGED
|
@@ -72,13 +72,14 @@ export declare function runLocalHostedLoadCheck(input: {
|
|
|
72
72
|
postReadyWaitMs?: number;
|
|
73
73
|
gameServerCredential?: DevGameServerCredentialResponse | null;
|
|
74
74
|
}): Promise<HostedLoadCheckResult>;
|
|
75
|
-
export declare function buildUploadedHostedLoadCheckUrl(rawUrl: string, phase?: "play" | "editor"): string;
|
|
75
|
+
export declare function buildUploadedHostedLoadCheckUrl(rawUrl: string, phase?: "play" | "editor", localDevPort?: number): string;
|
|
76
76
|
export declare function buildUploadedHostedLoadCheckScreenshotPath(task: Pick<AppTask, "name" | "projectDir">, sessionId: string, phase?: "play" | "editor"): string | null;
|
|
77
77
|
export declare function runUploadedHostedLoadCheck(input: {
|
|
78
78
|
client: ApiClient;
|
|
79
79
|
creatorUsername: string;
|
|
80
80
|
task: AppTask;
|
|
81
81
|
sessionId: string;
|
|
82
|
+
localDevPort?: number;
|
|
82
83
|
timeoutMs?: number;
|
|
83
84
|
token?: string | null;
|
|
84
85
|
currentUser?: UserResponse | null;
|
|
@@ -100,6 +101,7 @@ export declare function runUploadedHostedPlaytestSmokeCheck(input: {
|
|
|
100
101
|
creatorUsername: string;
|
|
101
102
|
task: AppTask;
|
|
102
103
|
sessionId: string;
|
|
104
|
+
localDevPort?: number;
|
|
103
105
|
timeoutMs?: number;
|
|
104
106
|
token?: string | null;
|
|
105
107
|
currentUser?: UserResponse | null;
|
package/dist/apps/loadCheck.js
CHANGED
|
@@ -292,14 +292,13 @@ function formatLoadCheckErrors(errors) {
|
|
|
292
292
|
return "";
|
|
293
293
|
}
|
|
294
294
|
const normalized = errors
|
|
295
|
-
.map((entry) => entry.
|
|
295
|
+
.map((entry) => entry.trim())
|
|
296
296
|
.filter(Boolean)
|
|
297
|
-
.slice(0, 3)
|
|
298
|
-
.map((entry) => (entry.length > 240 ? `${entry.slice(0, 237)}...` : entry));
|
|
297
|
+
.slice(0, 3);
|
|
299
298
|
if (normalized.length === 0) {
|
|
300
299
|
return "";
|
|
301
300
|
}
|
|
302
|
-
return
|
|
301
|
+
return `\nFirst error${normalized.length === 1 ? "" : "s"}:\n${normalized.join("\n\n")}`;
|
|
303
302
|
}
|
|
304
303
|
function isSoftwareWebGlRenderer(renderer) {
|
|
305
304
|
const normalized = typeof renderer === "string" ? renderer.trim() : "";
|
|
@@ -549,13 +548,17 @@ async function readWebGlRenderer(frame) {
|
|
|
549
548
|
}
|
|
550
549
|
return normalized;
|
|
551
550
|
}
|
|
552
|
-
async function saveScreenshot(page, screenshotPath) {
|
|
551
|
+
async function saveScreenshot(page, screenshotPath, timeoutMs = 30000) {
|
|
553
552
|
const normalizedPath = typeof screenshotPath === "string" ? screenshotPath.trim() : "";
|
|
554
553
|
if (!normalizedPath) {
|
|
555
554
|
return null;
|
|
556
555
|
}
|
|
557
556
|
await (0, promises_1.mkdir)((0, node_path_1.dirname)(normalizedPath), { recursive: true });
|
|
558
|
-
await page.screenshot({
|
|
557
|
+
await page.screenshot({
|
|
558
|
+
path: normalizedPath,
|
|
559
|
+
fullPage: true,
|
|
560
|
+
timeout: timeoutMs,
|
|
561
|
+
});
|
|
559
562
|
return normalizedPath;
|
|
560
563
|
}
|
|
561
564
|
async function saveGameFrameScreenshot(page, screenshotPath) {
|
|
@@ -632,11 +635,22 @@ async function runHostedLoadCheck(options) {
|
|
|
632
635
|
const withTapeAction = (line) => (currentTapeAction ? `${line} after ${currentTapeAction}` : line);
|
|
633
636
|
let hostedLaunchWaiterSettled = false;
|
|
634
637
|
let lastHostedLaunchState = null;
|
|
638
|
+
let fatalRuntimeError = null;
|
|
639
|
+
let rejectFatalRuntimeFailure;
|
|
640
|
+
const fatalRuntimeFailure = new Promise((_resolve, reject) => {
|
|
641
|
+
rejectFatalRuntimeFailure = reject;
|
|
642
|
+
});
|
|
643
|
+
// Browser events can arrive before navigation starts awaiting this signal.
|
|
644
|
+
void fatalRuntimeFailure.catch(() => { });
|
|
635
645
|
let resolveHostedLaunchWaiter = null;
|
|
636
646
|
const hostedLaunchWaiter = new Promise((resolve) => {
|
|
637
647
|
resolveHostedLaunchWaiter = resolve;
|
|
638
648
|
});
|
|
639
649
|
const settleHostedLaunchWaiter = (state) => {
|
|
650
|
+
if (state.state === "error" && !fatalRuntimeError) {
|
|
651
|
+
fatalRuntimeError = new Error(formatHostedLaunchError(state));
|
|
652
|
+
rejectFatalRuntimeFailure(fatalRuntimeError);
|
|
653
|
+
}
|
|
640
654
|
lastHostedLaunchState = state;
|
|
641
655
|
if (hostedLaunchWaiterSettled) {
|
|
642
656
|
return;
|
|
@@ -760,9 +774,14 @@ async function runHostedLoadCheck(options) {
|
|
|
760
774
|
pendingConsoleMessages.add(pending);
|
|
761
775
|
});
|
|
762
776
|
page.on("pageerror", (error) => {
|
|
763
|
-
const text = redactReportText(error?.
|
|
777
|
+
const text = redactReportText(error.stack?.trim() || `${error.name}: ${error.message}`);
|
|
764
778
|
const line = withTapeAction(`[check][pageerror] ${text}`);
|
|
765
779
|
errors.push(line);
|
|
780
|
+
settleHostedLaunchWaiter({
|
|
781
|
+
state: "error",
|
|
782
|
+
errorCode: "hosted_app_javascript_error",
|
|
783
|
+
message: line,
|
|
784
|
+
});
|
|
766
785
|
});
|
|
767
786
|
page.on("crash", () => {
|
|
768
787
|
const line = withTapeAction("[check][pagecrash] Hosted validator page crashed.");
|
|
@@ -811,7 +830,7 @@ async function runHostedLoadCheck(options) {
|
|
|
811
830
|
});
|
|
812
831
|
const captureFailureScreenshot = async () => {
|
|
813
832
|
try {
|
|
814
|
-
screenshotPath = await saveScreenshot(page, options.failureScreenshotPath ?? options.screenshotPath);
|
|
833
|
+
screenshotPath = await saveScreenshot(page, options.failureScreenshotPath ?? options.screenshotPath, HOSTED_LAUNCH_DIAGNOSTIC_TIMEOUT_MS);
|
|
815
834
|
}
|
|
816
835
|
catch (screenshotError) {
|
|
817
836
|
const detail = redactReportText(screenshotError instanceof Error ? screenshotError.message : String(screenshotError));
|
|
@@ -826,57 +845,81 @@ async function runHostedLoadCheck(options) {
|
|
|
826
845
|
screenshotPath = await saveScreenshot(page, options.screenshotPath);
|
|
827
846
|
};
|
|
828
847
|
try {
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
848
|
+
const check = async () => {
|
|
849
|
+
const readinessTimeoutMs = Math.min(Math.max(timeoutMs, 1000), 30000);
|
|
850
|
+
let response;
|
|
851
|
+
let launchState;
|
|
852
|
+
try {
|
|
853
|
+
[response, launchState] = await waitForHostedLaunchDeadline(async () => await Promise.race([
|
|
854
|
+
Promise.all([
|
|
855
|
+
page.goto(options.targetUrl, {
|
|
856
|
+
waitUntil: "domcontentloaded",
|
|
857
|
+
timeout: 30000,
|
|
858
|
+
}),
|
|
859
|
+
hostedLaunchWaiter,
|
|
860
|
+
]),
|
|
861
|
+
fatalRuntimeFailure,
|
|
862
|
+
]), expectedHostedLaunchState, timeoutMs);
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
866
|
+
if (!/^hosted_app_(?:missing_ready|gate_missing):/.test(detail))
|
|
867
|
+
throw error;
|
|
868
|
+
const timeoutContext = await describeHostedLaunchTimeout(page, lastHostedLaunchState);
|
|
869
|
+
throw new Error(`${detail} ${timeoutContext}`);
|
|
870
|
+
}
|
|
871
|
+
if (response && !response.ok()) {
|
|
872
|
+
errors.push(`[check][navigation] ${response.status()} ${response.statusText()} ${reportTargetUrl}`);
|
|
873
|
+
}
|
|
874
|
+
if (launchState.state === "error") {
|
|
875
|
+
throw new Error(formatHostedLaunchError(launchState));
|
|
876
|
+
}
|
|
877
|
+
finalUrl = (0, appUrls_1.redactTaskCaptureSession)(page.url());
|
|
878
|
+
if (expectedHostedLaunchState !== "ready") {
|
|
879
|
+
if (expectedHostedLaunchState === "login_required") {
|
|
880
|
+
try {
|
|
881
|
+
// Trial checks visibility and hit testing without opening the sign-in flow.
|
|
882
|
+
await page.locator('[data-hosted-play-gate="login_required"] button').click({
|
|
883
|
+
trial: true,
|
|
884
|
+
timeout: readinessTimeoutMs,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
throw new Error("hosted_app_gate_not_visible: PlayDrop reported that sign-in is required, but the game's sign-in panel could not be seen or used. " +
|
|
889
|
+
(error instanceof Error ? error.message : String(error)));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
await drainConsoleMessages();
|
|
893
|
+
await captureCompletionScreenshot();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const frame = await findGameFrame(page, readinessTimeoutMs);
|
|
897
|
+
webglRenderer = await readWebGlRenderer(frame);
|
|
898
|
+
if (isSoftwareWebGlRenderer(webglRenderer)) {
|
|
899
|
+
throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
|
|
900
|
+
}
|
|
901
|
+
if (options.verifyEditorPhaseRoundTrip) {
|
|
902
|
+
await verifyHostedEditorPhaseRoundTrip(page, frame, readinessTimeoutMs);
|
|
903
|
+
}
|
|
904
|
+
await saveGameFrameScreenshot(page, options.readyGameFrameScreenshotPath);
|
|
905
|
+
if (options.playtestTape) {
|
|
906
|
+
if (!options.playtestSurface) {
|
|
907
|
+
throw new Error("project_check_playtest_surface_required");
|
|
908
|
+
}
|
|
909
|
+
await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape, {
|
|
910
|
+
onBeforeEvent: (eventIndex, event) => {
|
|
911
|
+
currentTapeAction = `action ${eventIndex + 1} of ${options.playtestTape.events.length} (${event.type} at ${event.atMs} ms)`;
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
else if (options.postReadyWaitMs && options.postReadyWaitMs > 0) {
|
|
916
|
+
await page.waitForTimeout(options.postReadyWaitMs);
|
|
917
|
+
}
|
|
918
|
+
await saveGameFrameScreenshot(page, options.gameFrameScreenshotPath);
|
|
851
919
|
await drainConsoleMessages();
|
|
852
920
|
await captureCompletionScreenshot();
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const frame = await findGameFrame(page, readinessTimeoutMs);
|
|
856
|
-
webglRenderer = await readWebGlRenderer(frame);
|
|
857
|
-
if (isSoftwareWebGlRenderer(webglRenderer)) {
|
|
858
|
-
throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
|
|
859
|
-
}
|
|
860
|
-
if (options.verifyEditorPhaseRoundTrip) {
|
|
861
|
-
await verifyHostedEditorPhaseRoundTrip(page, frame, readinessTimeoutMs);
|
|
862
|
-
}
|
|
863
|
-
await saveGameFrameScreenshot(page, options.readyGameFrameScreenshotPath);
|
|
864
|
-
if (options.playtestTape) {
|
|
865
|
-
if (!options.playtestSurface) {
|
|
866
|
-
throw new Error("project_check_playtest_surface_required");
|
|
867
|
-
}
|
|
868
|
-
await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape, {
|
|
869
|
-
onBeforeEvent: (eventIndex, event) => {
|
|
870
|
-
currentTapeAction = `action ${eventIndex + 1} of ${options.playtestTape.events.length} (${event.type} at ${event.atMs} ms)`;
|
|
871
|
-
},
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
else if (options.postReadyWaitMs && options.postReadyWaitMs > 0) {
|
|
875
|
-
await page.waitForTimeout(options.postReadyWaitMs);
|
|
876
|
-
}
|
|
877
|
-
await saveGameFrameScreenshot(page, options.gameFrameScreenshotPath);
|
|
878
|
-
await drainConsoleMessages();
|
|
879
|
-
await captureCompletionScreenshot();
|
|
921
|
+
};
|
|
922
|
+
await Promise.race([check(), fatalRuntimeFailure]);
|
|
880
923
|
}
|
|
881
924
|
catch (error) {
|
|
882
925
|
await drainConsoleMessages();
|
|
@@ -887,7 +930,7 @@ async function runHostedLoadCheck(options) {
|
|
|
887
930
|
// Keep the last known URL when the renderer is unavailable.
|
|
888
931
|
}
|
|
889
932
|
await captureFailureScreenshot();
|
|
890
|
-
throw error;
|
|
933
|
+
throw fatalRuntimeError ?? error;
|
|
891
934
|
}
|
|
892
935
|
}, {
|
|
893
936
|
...options.contextOptions,
|
|
@@ -907,7 +950,9 @@ async function runHostedLoadCheck(options) {
|
|
|
907
950
|
targetUrl: finalUrl,
|
|
908
951
|
errors: errors.length > 0 ? errors : normalized.errors,
|
|
909
952
|
warnings,
|
|
910
|
-
message: normalized.message
|
|
953
|
+
message: normalized.message
|
|
954
|
+
? `${normalized.message}${formatLoadCheckErrors(errors.filter((entry) => !normalized.message.includes(entry)))}`
|
|
955
|
+
: normalized.message,
|
|
911
956
|
};
|
|
912
957
|
}
|
|
913
958
|
if (errors.length > 0) {
|
|
@@ -1028,6 +1073,8 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
1028
1073
|
creatorUsername: input.creatorUsername,
|
|
1029
1074
|
appName: input.task.name,
|
|
1030
1075
|
appType: input.task.type ?? "GAME",
|
|
1076
|
+
devAuth: "player",
|
|
1077
|
+
player: "1",
|
|
1031
1078
|
launchCheck: true,
|
|
1032
1079
|
localDevPort,
|
|
1033
1080
|
captureSession: input.captureSession,
|
|
@@ -1068,7 +1115,8 @@ async function runLocalHostedLoadCheck(input) {
|
|
|
1068
1115
|
creatorUsername: input.creatorUsername,
|
|
1069
1116
|
appName: input.task.name,
|
|
1070
1117
|
appType: input.task.type ?? "GAME",
|
|
1071
|
-
devAuth: "
|
|
1118
|
+
devAuth: "player",
|
|
1119
|
+
player: "1",
|
|
1072
1120
|
launchCheck: true,
|
|
1073
1121
|
localDevPort,
|
|
1074
1122
|
}),
|
|
@@ -1114,9 +1162,11 @@ function appendLoadCheckParams(targetUrl, devAuth) {
|
|
|
1114
1162
|
player: null,
|
|
1115
1163
|
});
|
|
1116
1164
|
}
|
|
1117
|
-
function buildUploadedHostedLoadCheckUrl(rawUrl, phase) {
|
|
1165
|
+
function buildUploadedHostedLoadCheckUrl(rawUrl, phase, localDevPort) {
|
|
1118
1166
|
const url = new URL(rawUrl);
|
|
1119
1167
|
url.searchParams.set("launchCheck", "1");
|
|
1168
|
+
if (localDevPort !== undefined)
|
|
1169
|
+
url.searchParams.set("localDevPort", String(localDevPort));
|
|
1120
1170
|
if (phase === "editor") {
|
|
1121
1171
|
url.searchParams.set("hostPhase", "editor");
|
|
1122
1172
|
}
|
|
@@ -1132,7 +1182,10 @@ function buildUploadedHostedLoadCheckScreenshotPath(task, sessionId, phase) {
|
|
|
1132
1182
|
}
|
|
1133
1183
|
async function runUploadedHostedLoadCheck(input) {
|
|
1134
1184
|
const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
|
|
1135
|
-
|
|
1185
|
+
if (input.task.server && (!Number.isInteger(input.localDevPort) || input.localDevPort <= 0 || input.localDevPort > 65535)) {
|
|
1186
|
+
throw new Error("publishing_game_server_port_missing: The uploaded game cannot be checked without its running game server.");
|
|
1187
|
+
}
|
|
1188
|
+
const withRequestedPhase = (rawUrl) => buildUploadedHostedLoadCheckUrl(rawUrl, input.phase, input.localDevPort);
|
|
1136
1189
|
const failureScreenshotPath = input.screenshotPath ?? buildUploadedHostedLoadCheckScreenshotPath(input.task, input.sessionId, input.phase);
|
|
1137
1190
|
const captureSurface = resolveLoadCheckSurface(preview.localAppMetadata.surfaceTargets, input.task.primarySurface);
|
|
1138
1191
|
const contextOptions = cloneLoadCheckContextOptions(captureSurface);
|
|
@@ -1142,7 +1195,10 @@ async function runUploadedHostedLoadCheck(input) {
|
|
|
1142
1195
|
});
|
|
1143
1196
|
let result;
|
|
1144
1197
|
if (input.captureSession) {
|
|
1145
|
-
const targetUrl = new URL(withRequestedPhase(preview.launchUrl)
|
|
1198
|
+
const targetUrl = new URL((0, devAuth_1.applyHostedDevAuthSelectionToUrl)(withRequestedPhase(preview.launchUrl), {
|
|
1199
|
+
devAuth: "player",
|
|
1200
|
+
player: 1,
|
|
1201
|
+
}));
|
|
1146
1202
|
(0, appUrls_1.appendTaskCaptureSession)(targetUrl, input.captureSession);
|
|
1147
1203
|
result = await runHostedLoadCheck({
|
|
1148
1204
|
targetUrl: targetUrl.toString(),
|
|
@@ -1179,7 +1235,9 @@ async function runUploadedHostedLoadCheck(input) {
|
|
|
1179
1235
|
}
|
|
1180
1236
|
else {
|
|
1181
1237
|
result = await runHostedLoadCheck({
|
|
1182
|
-
targetUrl: withRequestedPhase(
|
|
1238
|
+
targetUrl: (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(withRequestedPhase(preview.launchUrl), {
|
|
1239
|
+
devAuth: "player", player: 1,
|
|
1240
|
+
}),
|
|
1183
1241
|
artifactFingerprint: preview.session.manifestHash,
|
|
1184
1242
|
timeoutMs: input.timeoutMs,
|
|
1185
1243
|
expectedHostedLaunchState: resolvePostAuthHostedLaunchState(preview.localAppMetadata.controllerMode),
|
|
@@ -1285,6 +1343,7 @@ async function runUploadedHostedPlaytestSmokeCheck(input) {
|
|
|
1285
1343
|
creatorUsername: input.creatorUsername,
|
|
1286
1344
|
task: input.task,
|
|
1287
1345
|
sessionId: input.sessionId,
|
|
1346
|
+
localDevPort: input.localDevPort,
|
|
1288
1347
|
timeoutMs: input.timeoutMs,
|
|
1289
1348
|
token: input.token,
|
|
1290
1349
|
currentUser: input.currentUser,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApiClient } from "@playdrop/api-client";
|
|
2
|
+
import type { AppTask } from "../catalogue";
|
|
3
|
+
import { type GameServerDevServices } from "../commands/devGameServer";
|
|
4
|
+
import type { GameServerBuild } from "./serverBuild";
|
|
5
|
+
export type PublishingRuntime = {
|
|
6
|
+
localDevPort: number;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare function startPublishingRuntime(input: {
|
|
10
|
+
client: ApiClient;
|
|
11
|
+
task: AppTask;
|
|
12
|
+
creatorUsername: string;
|
|
13
|
+
appId: number;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
apiBase?: string;
|
|
16
|
+
gameServerBuild: GameServerBuild;
|
|
17
|
+
gameServerDev?: GameServerDevServices;
|
|
18
|
+
}): Promise<PublishingRuntime>;
|