@playdrop/playdrop-cli 0.6.5 → 0.6.6
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/config/client-meta.json +7 -7
- package/dist/appUrls.d.ts +8 -0
- package/dist/appUrls.js +27 -0
- package/dist/apps/index.d.ts +11 -0
- package/dist/apps/index.js +45 -1
- package/dist/apps/launchCheck.d.ts +30 -0
- package/dist/apps/launchCheck.js +272 -0
- package/dist/apps/registration.d.ts +12 -0
- package/dist/apps/registration.js +49 -0
- package/dist/apps/upload.d.ts +5 -0
- package/dist/apps/upload.js +17 -0
- package/dist/apps/validate.js +41 -3
- package/dist/captureRuntime.d.ts +13 -0
- package/dist/captureRuntime.js +99 -21
- package/dist/commands/capture.js +52 -39
- package/dist/commands/devServer.js +10 -2
- package/dist/commands/upload.js +16 -5
- package/dist/commands/validate.js +58 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +21 -0
- package/node_modules/@playdrop/config/client-meta.json +7 -7
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/version.d.ts +37 -1
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.js +1 -0
- package/package.json +1 -1
package/dist/apps/validate.js
CHANGED
|
@@ -50,6 +50,9 @@ const SDK_INIT_PATTERNS = [
|
|
|
50
50
|
/\bplaydrop\s*\.\s*init\s*\(/g,
|
|
51
51
|
/\bsdk\s*\.\s*initialize\s*\(/g,
|
|
52
52
|
];
|
|
53
|
+
const SDK_READY_PATTERNS = [
|
|
54
|
+
/\bsdk\s*\??\.\s*host\s*\??\.\s*ready\s*\(/g,
|
|
55
|
+
];
|
|
53
56
|
const LOCAL_HTML_SCRIPT_PATTERN = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
54
57
|
const LOCAL_IMPORT_PATTERNS = [
|
|
55
58
|
/(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?["']([^"']+)["']/g,
|
|
@@ -124,6 +127,33 @@ function scanForLegacySdkSymbols(task) {
|
|
|
124
127
|
function stripSpecifierDecorators(specifier) {
|
|
125
128
|
return specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
|
|
126
129
|
}
|
|
130
|
+
function escapeRegexLiteral(value) {
|
|
131
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
132
|
+
}
|
|
133
|
+
function detectSdkInitInSource(source) {
|
|
134
|
+
if (SDK_INIT_PATTERNS.some((pattern) => {
|
|
135
|
+
pattern.lastIndex = 0;
|
|
136
|
+
return pattern.test(source);
|
|
137
|
+
})) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const playdropAliases = new Set();
|
|
141
|
+
const aliasAssignmentPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;\n]*\bplaydrop\b[^;\n]*/g;
|
|
142
|
+
let aliasMatch;
|
|
143
|
+
while ((aliasMatch = aliasAssignmentPattern.exec(source)) !== null) {
|
|
144
|
+
const alias = aliasMatch[1]?.trim();
|
|
145
|
+
if (alias) {
|
|
146
|
+
playdropAliases.add(alias);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const alias of playdropAliases) {
|
|
150
|
+
const aliasInitPattern = new RegExp(`\\b${escapeRegexLiteral(alias)}\\s*(?:\\.\\s*|\\?\\.\\s*)init\\s*\\(`, 'g');
|
|
151
|
+
if (aliasInitPattern.test(source)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
127
157
|
function safeReadFile(filePath) {
|
|
128
158
|
try {
|
|
129
159
|
return (0, node_fs_1.readFileSync)(filePath, 'utf8');
|
|
@@ -378,9 +408,10 @@ function collectSdkDetectionFiles(task, mode) {
|
|
|
378
408
|
function detectSdkUsage(task, mode) {
|
|
379
409
|
let hasSdkReference = false;
|
|
380
410
|
let hasSdkInit = false;
|
|
411
|
+
let hasSdkReady = false;
|
|
381
412
|
const files = collectSdkDetectionFiles(task, mode);
|
|
382
413
|
for (const filePath of files) {
|
|
383
|
-
if (hasSdkReference && hasSdkInit) {
|
|
414
|
+
if (hasSdkReference && hasSdkInit && hasSdkReady) {
|
|
384
415
|
break;
|
|
385
416
|
}
|
|
386
417
|
const source = safeReadFile(filePath);
|
|
@@ -394,13 +425,16 @@ function detectSdkUsage(task, mode) {
|
|
|
394
425
|
});
|
|
395
426
|
}
|
|
396
427
|
if (!hasSdkInit) {
|
|
397
|
-
hasSdkInit =
|
|
428
|
+
hasSdkInit = detectSdkInitInSource(source);
|
|
429
|
+
}
|
|
430
|
+
if (!hasSdkReady) {
|
|
431
|
+
hasSdkReady = SDK_READY_PATTERNS.some((pattern) => {
|
|
398
432
|
pattern.lastIndex = 0;
|
|
399
433
|
return pattern.test(source);
|
|
400
434
|
});
|
|
401
435
|
}
|
|
402
436
|
}
|
|
403
|
-
return { hasSdkReference, hasSdkInit };
|
|
437
|
+
return { hasSdkReference, hasSdkInit, hasSdkReady };
|
|
404
438
|
}
|
|
405
439
|
function collectOutdatedSdkVersionWarnings(task) {
|
|
406
440
|
if (!task.packageJsonPath) {
|
|
@@ -451,6 +485,7 @@ function collectAppValidationWarnings(task, mode = 'source') {
|
|
|
451
485
|
: primaryDetection;
|
|
452
486
|
const hasSdkReference = primaryDetection.hasSdkReference || sourceDetection.hasSdkReference;
|
|
453
487
|
const hasSdkInit = primaryDetection.hasSdkInit || sourceDetection.hasSdkInit;
|
|
488
|
+
const hasSdkReady = primaryDetection.hasSdkReady || sourceDetection.hasSdkReady;
|
|
454
489
|
const warnings = [];
|
|
455
490
|
if (!hasSdkReference) {
|
|
456
491
|
warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find https://assets.playdrop.ai/sdk/playdrop.js or @playdrop/sdk in the app files, so it might not work once uploaded.`);
|
|
@@ -458,6 +493,9 @@ function collectAppValidationWarnings(task, mode = 'source') {
|
|
|
458
493
|
if (!hasSdkInit) {
|
|
459
494
|
warnings.push(`[apps][validate] Could not detect Playdrop SDK initialization for ${task.name}. We could not find playdrop.init() in the app files, so it might not work once uploaded.`);
|
|
460
495
|
}
|
|
496
|
+
if (!hasSdkReady) {
|
|
497
|
+
warnings.push(`[apps][validate] Could not detect Playdrop host readiness for ${task.name}. We could not find sdk.host.ready() in the app files, so the app will fail to start correctly inside Playdrop.`);
|
|
498
|
+
}
|
|
461
499
|
return [...warnings, ...collectOutdatedSdkVersionWarnings(task)];
|
|
462
500
|
}
|
|
463
501
|
async function runFormatScript(task) {
|
package/dist/captureRuntime.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type RunCaptureOptions = {
|
|
|
12
12
|
targetUrl: string;
|
|
13
13
|
expectedUrl?: string | null;
|
|
14
14
|
timeoutMs: number;
|
|
15
|
+
settleAfterReadyMs?: number;
|
|
15
16
|
minimumLogLevel: CaptureLogLevel;
|
|
16
17
|
screenshotPath?: string | null;
|
|
17
18
|
logPath?: string | null;
|
|
@@ -21,10 +22,22 @@ export type RunCaptureOptions = {
|
|
|
21
22
|
login?: LoginOptions | null;
|
|
22
23
|
savedSessionBootstrap?: boolean;
|
|
23
24
|
enableCaptureBridge?: boolean;
|
|
25
|
+
requireHostedLaunchReady?: boolean;
|
|
26
|
+
expectedHostedLaunchState?: 'ready' | 'login_required';
|
|
27
|
+
};
|
|
28
|
+
export type CaptureHostedLaunchState = {
|
|
29
|
+
state: 'ready';
|
|
30
|
+
} | {
|
|
31
|
+
state: 'login_required';
|
|
32
|
+
} | {
|
|
33
|
+
state: 'error';
|
|
34
|
+
errorCode: string | null;
|
|
35
|
+
message: string | null;
|
|
24
36
|
};
|
|
25
37
|
export type CaptureRunResult = {
|
|
26
38
|
errorCount: number;
|
|
27
39
|
finalUrl: string;
|
|
40
|
+
hostedLaunchState: CaptureHostedLaunchState | null;
|
|
28
41
|
};
|
|
29
42
|
export declare function resolveCaptureLogLevel(value: string | undefined): CaptureLogLevel;
|
|
30
43
|
export declare function validateCaptureTimeout(value: number | undefined): number;
|
package/dist/captureRuntime.js
CHANGED
|
@@ -9,7 +9,6 @@ const node_path_1 = require("node:path");
|
|
|
9
9
|
const playwright_1 = require("./playwright");
|
|
10
10
|
const sessionCookie_1 = require("./sessionCookie");
|
|
11
11
|
const FRAME_SELECTOR = 'iframe[title="Game"]';
|
|
12
|
-
const LOGIN_BUTTON_NAME = 'Sign in to play';
|
|
13
12
|
exports.CAPTURE_LOG_LEVEL_VALUES = ['debug', 'info', 'warn', 'error'];
|
|
14
13
|
exports.MAX_CAPTURE_TIMEOUT_SECONDS = 600;
|
|
15
14
|
function formatConsoleValue(value) {
|
|
@@ -80,6 +79,44 @@ function normalizeComparableUrl(rawUrl) {
|
|
|
80
79
|
const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
|
|
81
80
|
return `${parsed.origin}${pathname}`;
|
|
82
81
|
}
|
|
82
|
+
function normalizeHostedLaunchStatePayload(payload) {
|
|
83
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const state = typeof payload.state === 'string'
|
|
87
|
+
? payload.state.trim().toLowerCase()
|
|
88
|
+
: '';
|
|
89
|
+
if (state === 'ready') {
|
|
90
|
+
return { state: 'ready' };
|
|
91
|
+
}
|
|
92
|
+
if (state === 'login_required') {
|
|
93
|
+
return { state: 'login_required' };
|
|
94
|
+
}
|
|
95
|
+
if (state !== 'error') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const errorCode = typeof payload.errorCode === 'string'
|
|
99
|
+
? payload.errorCode.trim() || null
|
|
100
|
+
: null;
|
|
101
|
+
const message = typeof payload.message === 'string'
|
|
102
|
+
? payload.message.trim() || null
|
|
103
|
+
: null;
|
|
104
|
+
return {
|
|
105
|
+
state: 'error',
|
|
106
|
+
errorCode,
|
|
107
|
+
message,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function formatHostedLaunchError(state) {
|
|
111
|
+
const parts = ['Hosted app failed to launch.'];
|
|
112
|
+
if (state.errorCode) {
|
|
113
|
+
parts.push(`errorCode=${state.errorCode}.`);
|
|
114
|
+
}
|
|
115
|
+
if (state.message) {
|
|
116
|
+
parts.push(state.message);
|
|
117
|
+
}
|
|
118
|
+
return parts.join(' ');
|
|
119
|
+
}
|
|
83
120
|
async function writeLogFile(logPath, lines) {
|
|
84
121
|
if (!logPath || logPath.trim().length === 0) {
|
|
85
122
|
return;
|
|
@@ -113,11 +150,15 @@ async function runCapture(options) {
|
|
|
113
150
|
const errors = [];
|
|
114
151
|
const outputLines = [];
|
|
115
152
|
const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
|
|
153
|
+
const expectedHostedLaunchState = options.requireHostedLaunchReady
|
|
154
|
+
? options.expectedHostedLaunchState ?? 'ready'
|
|
155
|
+
: null;
|
|
116
156
|
const hasExplicitLogin = Boolean(options.login?.username);
|
|
117
157
|
const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && !hasExplicitLogin;
|
|
118
158
|
const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
|
|
119
159
|
const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
|
|
120
160
|
let finalUrl = options.targetUrl;
|
|
161
|
+
let hostedLaunchState = null;
|
|
121
162
|
const record = (level, message) => {
|
|
122
163
|
const line = `[capture][${level}] ${message}`;
|
|
123
164
|
outputLines.push(line);
|
|
@@ -142,6 +183,21 @@ async function runCapture(options) {
|
|
|
142
183
|
try {
|
|
143
184
|
await (0, playwright_1.withChromiumPage)(async ({ context, page }) => {
|
|
144
185
|
const targetOrigin = new URL(options.targetUrl).origin;
|
|
186
|
+
let hostedLaunchWaiterSettled = false;
|
|
187
|
+
let resolveHostedLaunchWaiter = null;
|
|
188
|
+
const hostedLaunchWaiter = options.requireHostedLaunchReady
|
|
189
|
+
? new Promise((resolve) => {
|
|
190
|
+
resolveHostedLaunchWaiter = resolve;
|
|
191
|
+
})
|
|
192
|
+
: null;
|
|
193
|
+
const settleHostedLaunchWaiter = (state) => {
|
|
194
|
+
hostedLaunchState = state;
|
|
195
|
+
if (!hostedLaunchWaiter || hostedLaunchWaiterSettled) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
hostedLaunchWaiterSettled = true;
|
|
199
|
+
resolveHostedLaunchWaiter?.(state);
|
|
200
|
+
};
|
|
145
201
|
if (bootstrapToken) {
|
|
146
202
|
await context.addCookies((0, sessionCookie_1.buildCaptureAccessTokenCookies)(options.targetUrl, bootstrapToken));
|
|
147
203
|
}
|
|
@@ -173,6 +229,12 @@ async function runCapture(options) {
|
|
|
173
229
|
}
|
|
174
230
|
if (options.enableCaptureBridge) {
|
|
175
231
|
await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
|
|
232
|
+
if (type === 'hosted-launch-state') {
|
|
233
|
+
const normalizedHostedState = normalizeHostedLaunchStatePayload(payload);
|
|
234
|
+
if (normalizedHostedState) {
|
|
235
|
+
settleHostedLaunchWaiter(normalizedHostedState);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
176
238
|
const normalized = typeof type === 'string' ? type.toLowerCase() : 'info';
|
|
177
239
|
const level = normalized === 'error' || normalized === 'fatal'
|
|
178
240
|
? 'error'
|
|
@@ -216,7 +278,11 @@ async function runCapture(options) {
|
|
|
216
278
|
});
|
|
217
279
|
const bridgeWindow = window;
|
|
218
280
|
bridgeWindow.playdrop = bridgeWindow.playdrop || {};
|
|
281
|
+
bridgeWindow.__playdropCaptureHostedLaunchState = null;
|
|
219
282
|
const bridge = (type, payload) => {
|
|
283
|
+
if (type === 'hosted-launch-state') {
|
|
284
|
+
bridgeWindow.__playdropCaptureHostedLaunchState = payload;
|
|
285
|
+
}
|
|
220
286
|
try {
|
|
221
287
|
const binding = bridgeWindow.__playdropCaptureLog;
|
|
222
288
|
if (typeof binding === 'function') {
|
|
@@ -342,22 +408,20 @@ async function runCapture(options) {
|
|
|
342
408
|
recordError(`[navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
|
|
343
409
|
}
|
|
344
410
|
const readinessTimeoutMs = Math.min(Math.max(options.timeoutMs, 1000), 30000);
|
|
345
|
-
const readinessHandle = await page.waitForFunction(({ frameSelector,
|
|
411
|
+
const readinessHandle = await page.waitForFunction(({ frameSelector, waitForHostedLaunchState }) => {
|
|
346
412
|
const frame = document.querySelector(frameSelector);
|
|
347
413
|
if (frame) {
|
|
348
414
|
return 'frame';
|
|
349
415
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
416
|
+
if (waitForHostedLaunchState) {
|
|
417
|
+
const state = window.__playdropCaptureHostedLaunchState;
|
|
418
|
+
if (state && typeof state.state === 'string' && state.state.trim().length > 0) {
|
|
419
|
+
return `hosted:${state.state.trim().toLowerCase()}`;
|
|
420
|
+
}
|
|
354
421
|
}
|
|
355
422
|
return null;
|
|
356
|
-
}, { frameSelector: FRAME_SELECTOR,
|
|
357
|
-
|
|
358
|
-
if (readiness !== 'frame') {
|
|
359
|
-
throw new Error('The Playdrop app frame did not load. The page is still showing the login wall.');
|
|
360
|
-
}
|
|
423
|
+
}, { frameSelector: FRAME_SELECTOR, waitForHostedLaunchState: Boolean(expectedHostedLaunchState) }, { timeout: readinessTimeoutMs });
|
|
424
|
+
await readinessHandle.jsonValue();
|
|
361
425
|
const assertPageState = async () => {
|
|
362
426
|
const currentUrl = page.url();
|
|
363
427
|
if (!currentUrl) {
|
|
@@ -371,22 +435,35 @@ async function runCapture(options) {
|
|
|
371
435
|
if (finalPathname.startsWith('/login')) {
|
|
372
436
|
throw new Error(`Capture landed on the login page (${currentUrl}).`);
|
|
373
437
|
}
|
|
374
|
-
const loginVisible = await page
|
|
375
|
-
.getByRole('button', { name: LOGIN_BUTTON_NAME })
|
|
376
|
-
.first()
|
|
377
|
-
.isVisible()
|
|
378
|
-
.catch(() => false);
|
|
379
|
-
if (loginVisible) {
|
|
380
|
-
throw new Error('The Playdrop login wall is still visible.');
|
|
381
|
-
}
|
|
382
438
|
const frameCount = await page.locator(FRAME_SELECTOR).count();
|
|
383
|
-
if (frameCount === 0) {
|
|
439
|
+
if ((expectedHostedLaunchState ?? 'ready') === 'ready' && frameCount === 0) {
|
|
384
440
|
throw new Error('The Playdrop app frame never appeared.');
|
|
385
441
|
}
|
|
442
|
+
if (expectedHostedLaunchState === 'login_required' && frameCount > 0) {
|
|
443
|
+
throw new Error('The Playdrop auth gate booted the app frame unexpectedly.');
|
|
444
|
+
}
|
|
386
445
|
return currentUrl;
|
|
387
446
|
};
|
|
447
|
+
if (hostedLaunchWaiter) {
|
|
448
|
+
const launchState = await Promise.race([
|
|
449
|
+
hostedLaunchWaiter,
|
|
450
|
+
page.waitForTimeout(options.timeoutMs).then(() => {
|
|
451
|
+
throw new Error(expectedHostedLaunchState === 'login_required'
|
|
452
|
+
? 'Hosted app did not stop at the PlayDrop sign-in gate before the launch-check timeout.'
|
|
453
|
+
: 'Hosted app did not reach the ready state before the launch-check timeout.');
|
|
454
|
+
}),
|
|
455
|
+
]);
|
|
456
|
+
if (launchState.state === 'error') {
|
|
457
|
+
throw new Error(formatHostedLaunchError(launchState));
|
|
458
|
+
}
|
|
459
|
+
if (expectedHostedLaunchState && launchState.state !== expectedHostedLaunchState) {
|
|
460
|
+
throw new Error(expectedHostedLaunchState === 'login_required'
|
|
461
|
+
? 'Hosted app did not stop at the PlayDrop sign-in gate.'
|
|
462
|
+
: `Hosted app reported ${launchState.state} instead of ready.`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
388
465
|
finalUrl = await assertPageState();
|
|
389
|
-
await page.waitForTimeout(options.timeoutMs);
|
|
466
|
+
await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
|
|
390
467
|
finalUrl = await assertPageState();
|
|
391
468
|
if (options.screenshotPath) {
|
|
392
469
|
const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
|
|
@@ -404,5 +481,6 @@ async function runCapture(options) {
|
|
|
404
481
|
return {
|
|
405
482
|
errorCount: errors.length,
|
|
406
483
|
finalUrl,
|
|
484
|
+
hostedLaunchState,
|
|
407
485
|
};
|
|
408
486
|
}
|
package/dist/commands/capture.js
CHANGED
|
@@ -283,42 +283,6 @@ async function capture(targetArg, options = {}) {
|
|
|
283
283
|
process.exitCode = 1;
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
|
-
let registeredApp = null;
|
|
287
|
-
try {
|
|
288
|
-
registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
if (error instanceof http_1.CLIUnsupportedClientError) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (error instanceof types_1.UnsupportedClientError) {
|
|
295
|
-
(0, http_1.handleUnsupportedError)(error, 'Capture');
|
|
296
|
-
process.exitCode = 1;
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (error instanceof types_1.ApiError) {
|
|
300
|
-
if (error.status === 404) {
|
|
301
|
-
(0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
|
|
302
|
-
`Run "playdrop project create app ${appName}" to register the app before running capture.`,
|
|
303
|
-
'If you expected it to exist, ensure you are logged into the correct environment.',
|
|
304
|
-
], { command: 'project capture' });
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
(0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, [
|
|
308
|
-
'Retry in a moment.',
|
|
309
|
-
'If the issue persists, contact the Playdrop team.',
|
|
310
|
-
], { command: 'project capture' });
|
|
311
|
-
}
|
|
312
|
-
process.exitCode = 1;
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if ((0, devShared_1.isNetworkError)(error)) {
|
|
316
|
-
(0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project capture');
|
|
317
|
-
process.exitCode = 1;
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
throw error;
|
|
321
|
-
}
|
|
322
286
|
const entryLabel = (0, node_path_1.relative)(process.cwd(), filePath) || filePath;
|
|
323
287
|
console.log(`[capture] Preparing ${entryLabel} for ${env} (${appTypeSlug}).`);
|
|
324
288
|
const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
|
|
@@ -442,14 +406,61 @@ async function capture(targetArg, options = {}) {
|
|
|
442
406
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
443
407
|
}
|
|
444
408
|
const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
|
|
445
|
-
const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(`${webBase}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/dev`, devOptions.selection);
|
|
446
|
-
console.log(`[capture] Launching Playwright against ${frameUrl}`);
|
|
447
409
|
const shouldResetBefore = devOptions.selection.devAuth === 'player'
|
|
448
410
|
&& (devOptions.resetMode === 'before' || devOptions.resetMode === 'before-and-after');
|
|
449
411
|
const shouldResetAfter = devOptions.selection.devAuth === 'player'
|
|
450
412
|
&& (devOptions.resetMode === 'after' || devOptions.resetMode === 'before-and-after');
|
|
413
|
+
const requiresRegisteredApp = devOptions.selection.devAuth !== 'anonymous' || shouldResetBefore || shouldResetAfter;
|
|
414
|
+
let registeredApp = null;
|
|
415
|
+
if (requiresRegisteredApp) {
|
|
416
|
+
try {
|
|
417
|
+
registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (error instanceof http_1.CLIUnsupportedClientError) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (error instanceof types_1.UnsupportedClientError) {
|
|
424
|
+
(0, http_1.handleUnsupportedError)(error, 'Capture');
|
|
425
|
+
process.exitCode = 1;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (error instanceof types_1.ApiError) {
|
|
429
|
+
if (error.status === 404) {
|
|
430
|
+
(0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
|
|
431
|
+
`Run "playdrop project create app ${appName}" to register the app before using viewer or player capture.`,
|
|
432
|
+
'Use anonymous capture for local launch validation before upload.',
|
|
433
|
+
], { command: 'project capture' });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
(0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, [
|
|
437
|
+
'Retry in a moment.',
|
|
438
|
+
'If the issue persists, contact the Playdrop team.',
|
|
439
|
+
], { command: 'project capture' });
|
|
440
|
+
}
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if ((0, devShared_1.isNetworkError)(error)) {
|
|
445
|
+
(0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project capture');
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const captureBaseUrl = (0, appUrls_1.buildPlatformDevUrl)(webBase, {
|
|
453
|
+
creatorUsername: currentUsername,
|
|
454
|
+
appName,
|
|
455
|
+
appType: appTypeSlug,
|
|
456
|
+
devAuth: devOptions.selection.devAuth,
|
|
457
|
+
player: devOptions.selection.player ? String(devOptions.selection.player) : null,
|
|
458
|
+
launchCheck: devOptions.selection.devAuth === 'anonymous',
|
|
459
|
+
});
|
|
460
|
+
const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(captureBaseUrl, devOptions.selection);
|
|
461
|
+
console.log(`[capture] Launching Playwright against ${frameUrl}`);
|
|
451
462
|
const selectedPlayerSlot = devOptions.selection.player;
|
|
452
|
-
const registeredAppId = typeof registeredApp
|
|
463
|
+
const registeredAppId = typeof registeredApp?.id === 'number' ? registeredApp.id : null;
|
|
453
464
|
if ((shouldResetBefore || shouldResetAfter) && registeredAppId === null) {
|
|
454
465
|
(0, messages_1.printErrorWithHelp)('The registered app is missing a numeric id, so test-player reset is unavailable.', [], { command: 'project capture' });
|
|
455
466
|
process.exitCode = 1;
|
|
@@ -463,6 +474,7 @@ async function capture(targetArg, options = {}) {
|
|
|
463
474
|
targetUrl: frameUrl,
|
|
464
475
|
expectedUrl: frameUrl,
|
|
465
476
|
timeoutMs,
|
|
477
|
+
settleAfterReadyMs: timeoutMs,
|
|
466
478
|
minimumLogLevel,
|
|
467
479
|
screenshotPath,
|
|
468
480
|
contextOptions: surfaceContextOptions,
|
|
@@ -472,6 +484,7 @@ async function capture(targetArg, options = {}) {
|
|
|
472
484
|
} : undefined,
|
|
473
485
|
savedSessionBootstrap: false,
|
|
474
486
|
enableCaptureBridge: true,
|
|
487
|
+
requireHostedLaunchReady: true,
|
|
475
488
|
});
|
|
476
489
|
if (result.errorCount > 0) {
|
|
477
490
|
console.error(`[capture] Completed with ${result.errorCount} error(s) after ${timeoutSeconds} seconds.`);
|
|
@@ -396,15 +396,23 @@ async function startDevServer(options) {
|
|
|
396
396
|
};
|
|
397
397
|
}
|
|
398
398
|
async function isDevServerAvailable(input, timeoutMs = 1000) {
|
|
399
|
+
const port = input.port ?? exports.DEV_ROUTER_PORT;
|
|
400
|
+
const health = await fetchRouterHealth(port, Math.min(timeoutMs, 400));
|
|
401
|
+
if (!health || !isCompatibleRouterHealth(health)) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
399
404
|
const controller = new AbortController();
|
|
400
405
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
401
406
|
timeout.unref?.();
|
|
402
407
|
try {
|
|
403
|
-
await fetch(buildLocalDevAppUrl(
|
|
408
|
+
const response = await fetch(buildLocalDevAppUrl({
|
|
409
|
+
...input,
|
|
410
|
+
port,
|
|
411
|
+
}), {
|
|
404
412
|
method: 'GET',
|
|
405
413
|
signal: controller.signal,
|
|
406
414
|
});
|
|
407
|
-
return
|
|
415
|
+
return response.ok;
|
|
408
416
|
}
|
|
409
417
|
catch {
|
|
410
418
|
return false;
|
package/dist/commands/upload.js
CHANGED
|
@@ -150,18 +150,19 @@ async function fetchCurrentUserInfo(client, apiBase) {
|
|
|
150
150
|
return {
|
|
151
151
|
username: data?.user?.username ?? null,
|
|
152
152
|
role: data?.user?.role ?? null,
|
|
153
|
+
user: data?.user ?? null,
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
catch (error) {
|
|
156
157
|
if (error instanceof types_1.UnsupportedClientError) {
|
|
157
158
|
(0, http_1.handleUnsupportedError)(error, 'Upload');
|
|
158
|
-
return { username: null, role: null };
|
|
159
|
+
return { username: null, role: null, user: null };
|
|
159
160
|
}
|
|
160
161
|
if (error instanceof http_1.CLIUnsupportedClientError) {
|
|
161
162
|
throw error;
|
|
162
163
|
}
|
|
163
164
|
if (error instanceof types_1.ApiError) {
|
|
164
|
-
return { username: null, role: null };
|
|
165
|
+
return { username: null, role: null, user: null };
|
|
165
166
|
}
|
|
166
167
|
if (isConnectionRefusedError(error)) {
|
|
167
168
|
throw new Error(buildApiUnavailableMessage(apiBase));
|
|
@@ -282,6 +283,13 @@ async function uploadAppTask(state, task, taskCreator, options) {
|
|
|
282
283
|
skipReview: options?.skipReview,
|
|
283
284
|
clearTags: options?.clearTags,
|
|
284
285
|
creatorUsername: taskCreator,
|
|
286
|
+
apiBase: state.apiBase,
|
|
287
|
+
webBase: state.portalBase,
|
|
288
|
+
token: state.token,
|
|
289
|
+
user: state.currentUser,
|
|
290
|
+
runLocalLaunchCheck: true,
|
|
291
|
+
runStagedUploadLaunchCheck: true,
|
|
292
|
+
ensureRegisteredAppShell: true,
|
|
285
293
|
});
|
|
286
294
|
if (!upload.versionCreated || !upload.version) {
|
|
287
295
|
throw new Error(`App "${task.name}" upload did not return a created version.`);
|
|
@@ -476,7 +484,7 @@ async function flushGraphState(client, graphState, results) {
|
|
|
476
484
|
process.exitCode = process.exitCode || 1;
|
|
477
485
|
}
|
|
478
486
|
}
|
|
479
|
-
async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, warnings, webBase, options) {
|
|
487
|
+
async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, currentUser, token, warnings, apiBase, webBase, options) {
|
|
480
488
|
const portalBase = normalizePortalBase(webBase);
|
|
481
489
|
const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
|
|
482
490
|
const results = [];
|
|
@@ -494,6 +502,9 @@ async function processUploadTasks(client, tasks, owner, ownerUsername, currentUs
|
|
|
494
502
|
sortedTasks,
|
|
495
503
|
defaultCreator,
|
|
496
504
|
currentUserRole,
|
|
505
|
+
currentUser,
|
|
506
|
+
token,
|
|
507
|
+
apiBase,
|
|
497
508
|
portalBase,
|
|
498
509
|
uploadedAppsByName: new Map(),
|
|
499
510
|
uploadedAssetsByKey: new Map(),
|
|
@@ -539,7 +550,7 @@ async function upload(pathOrName, options) {
|
|
|
539
550
|
return;
|
|
540
551
|
}
|
|
541
552
|
const { client, env, envConfig } = ctx;
|
|
542
|
-
let userInfo = { username: null, role: null };
|
|
553
|
+
let userInfo = { username: null, role: null, user: null };
|
|
543
554
|
try {
|
|
544
555
|
userInfo = await fetchCurrentUserInfo(client, envConfig.apiBase);
|
|
545
556
|
}
|
|
@@ -589,7 +600,7 @@ async function upload(pathOrName, options) {
|
|
|
589
600
|
taxonomyEntries.forEach((entry) => pushLoggedEntry(results, entry));
|
|
590
601
|
}
|
|
591
602
|
if (tasks.length > 0) {
|
|
592
|
-
const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, warnings, envConfig.webBase, options);
|
|
603
|
+
const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, userInfo.user, ctx.token, warnings, envConfig.apiBase, envConfig.webBase, options);
|
|
593
604
|
results.push(...uploadEntries);
|
|
594
605
|
}
|
|
595
606
|
(0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'upload', environment: env });
|
|
@@ -79,6 +79,37 @@ async function loadAuthenticatedValidationContext(workspacePath) {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
async function loadHostedLaunchValidationContext(workspacePath) {
|
|
83
|
+
const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project validate', 'run hosted app launch validation', { workspacePath });
|
|
84
|
+
if (!ctx) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const username = typeof ctx.account?.username === 'string' ? ctx.account.username.trim() : '';
|
|
88
|
+
if (!username) {
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
let currentUserRole = null;
|
|
93
|
+
let currentUser = null;
|
|
94
|
+
try {
|
|
95
|
+
const me = await ctx.client.me();
|
|
96
|
+
currentUserRole = me.user.role ?? null;
|
|
97
|
+
currentUser = me.user ?? null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
currentUserRole = null;
|
|
101
|
+
currentUser = null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
client: ctx.client,
|
|
105
|
+
currentUserRole,
|
|
106
|
+
currentUser,
|
|
107
|
+
username,
|
|
108
|
+
apiBase: ctx.envConfig.apiBase,
|
|
109
|
+
webBase: ctx.envConfig.webBase ?? null,
|
|
110
|
+
token: ctx.token,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
82
113
|
function resolveValidationWorkspacePath(pathOrName, resolution) {
|
|
83
114
|
return resolution === 'name' ? process.cwd() : pathOrName;
|
|
84
115
|
}
|
|
@@ -147,6 +178,7 @@ async function collectLiveTagClearWarnings(tasks, workspacePath) {
|
|
|
147
178
|
}
|
|
148
179
|
async function validate(pathOrName) {
|
|
149
180
|
const selection = (0, taskSelection_1.selectTasks)(pathOrName);
|
|
181
|
+
const workspacePath = resolveValidationWorkspacePath(pathOrName, selection.resolution);
|
|
150
182
|
if (selection.errors.length > 0) {
|
|
151
183
|
(0, taskSelection_1.reportTaskErrors)(selection.errors, 'project validate');
|
|
152
184
|
process.exitCode = 1;
|
|
@@ -172,12 +204,38 @@ async function validate(pathOrName) {
|
|
|
172
204
|
const assetSpecTasks = tasks.filter((task) => task.kind === 'asset-spec');
|
|
173
205
|
const assetSpecLookups = buildLocalAssetSpecLookups(assetSpecTasks);
|
|
174
206
|
const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
|
|
207
|
+
const hostedLaunchValidationRequired = sortedTasks.some((task) => task.kind === 'app' && task.hostingMode !== 'EXTERNAL' && !task.externalUrl);
|
|
208
|
+
const hostedLaunchValidationContext = hostedLaunchValidationRequired
|
|
209
|
+
? await loadHostedLaunchValidationContext(workspacePath)
|
|
210
|
+
: null;
|
|
211
|
+
if (hostedLaunchValidationRequired && !hostedLaunchValidationContext) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
175
214
|
for (const task of sortedTasks) {
|
|
176
215
|
const entityId = task.name;
|
|
177
216
|
try {
|
|
178
217
|
if (task.kind === 'app') {
|
|
179
218
|
await (0, apps_1.validateAppTask)(task);
|
|
180
219
|
(0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
|
|
220
|
+
if (task.hostingMode !== 'EXTERNAL' && !task.externalUrl) {
|
|
221
|
+
const launchContext = hostedLaunchValidationContext;
|
|
222
|
+
const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, launchContext.username, launchContext.currentUserRole);
|
|
223
|
+
if (creatorResult.creatorTargetError) {
|
|
224
|
+
throw new Error(creatorResult.creatorTargetError);
|
|
225
|
+
}
|
|
226
|
+
const launchCheck = await (0, apps_1.runLocalHostedLaunchCheck)({
|
|
227
|
+
client: launchContext.client,
|
|
228
|
+
apiBase: launchContext.apiBase,
|
|
229
|
+
webBase: launchContext.webBase,
|
|
230
|
+
creatorUsername: creatorResult.taskCreator,
|
|
231
|
+
task,
|
|
232
|
+
token: launchContext.token,
|
|
233
|
+
currentUser: launchContext.currentUser,
|
|
234
|
+
});
|
|
235
|
+
if (launchCheck.status !== 'PASSED') {
|
|
236
|
+
throw new Error((0, apps_1.formatHostedLaunchCheckFailure)(task.name, launchCheck, 'local'));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
181
239
|
}
|
|
182
240
|
else if (task.kind === 'asset-spec') {
|
|
183
241
|
// Parsing already validates the contract, symbol, and documentation files.
|
|
@@ -215,7 +273,6 @@ async function validate(pathOrName) {
|
|
|
215
273
|
console.log((0, uploadLog_1.formatTaskLogLine)(entry));
|
|
216
274
|
}
|
|
217
275
|
}
|
|
218
|
-
const workspacePath = resolveValidationWorkspacePath(pathOrName, selection.resolution);
|
|
219
276
|
const liveTagWarnings = await collectLiveTagClearWarnings(sortedTasks, workspacePath);
|
|
220
277
|
liveTagWarnings.forEach((warning) => warnings.add(warning));
|
|
221
278
|
(0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'validate' });
|