pixifact 0.7.0 → 0.9.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.
|
@@ -9,8 +9,10 @@ const runtimeAnnounceEvent = 'pixifact:runtime:announce';
|
|
|
9
9
|
const runtimeRequestEvent = 'pixifact:runtime:request';
|
|
10
10
|
const runtimeResponseEvent = 'pixifact:runtime:response';
|
|
11
11
|
const runtimeApiRoot = '/__pixifact_runtime__';
|
|
12
|
+
const runtimeScreenshotPath = `${runtimeApiRoot}/screenshot`;
|
|
12
13
|
const virtualRuntimeClientId = 'virtual:pixifact-runtime-client';
|
|
13
14
|
const resolvedVirtualRuntimeClientId = `\0${virtualRuntimeClientId}`;
|
|
15
|
+
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
14
16
|
function isRecord(value) {
|
|
15
17
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
18
|
}
|
|
@@ -57,6 +59,27 @@ function isRuntimeRequest(value) {
|
|
|
57
59
|
&& typeof value.key === 'string'
|
|
58
60
|
&& value.key.length > 0;
|
|
59
61
|
}
|
|
62
|
+
function runtimeScreenshotResult(value) {
|
|
63
|
+
if (!isRecord(value)
|
|
64
|
+
|| typeof value.runtimeId !== 'string'
|
|
65
|
+
|| !Number.isInteger(value.width)
|
|
66
|
+
|| Number(value.width) <= 0
|
|
67
|
+
|| !Number.isInteger(value.height)
|
|
68
|
+
|| Number(value.height) <= 0
|
|
69
|
+
|| typeof value.dataUrl !== 'string') {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const match = value.dataUrl.match(/^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/);
|
|
73
|
+
const png = match ? Buffer.from(match[1], 'base64') : undefined;
|
|
74
|
+
if (!png || !png.subarray(0, pngSignature.length).equals(pngSignature))
|
|
75
|
+
return undefined;
|
|
76
|
+
return {
|
|
77
|
+
runtimeId: value.runtimeId,
|
|
78
|
+
width: Number(value.width),
|
|
79
|
+
height: Number(value.height),
|
|
80
|
+
png,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
60
83
|
export function createRuntimeHostSession(options) {
|
|
61
84
|
const requestTimeoutMs = options.requestTimeoutMs ?? 5_000;
|
|
62
85
|
const now = options.now ?? (() => new Date());
|
|
@@ -96,6 +119,29 @@ export function createRuntimeHostSession(options) {
|
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
}
|
|
122
|
+
function forwardRequest(runtime, request, responseType) {
|
|
123
|
+
const requestId = `runtime-${++requestSequence}`;
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const timeout = setTimeout(() => {
|
|
126
|
+
settle(requestId, jsonResponse({
|
|
127
|
+
ok: false,
|
|
128
|
+
error: 'Pixifact Runtime request timed out.',
|
|
129
|
+
}, 504));
|
|
130
|
+
}, requestTimeoutMs);
|
|
131
|
+
pending.set(requestId, {
|
|
132
|
+
resolve,
|
|
133
|
+
runtimeId: runtime.runtimeId,
|
|
134
|
+
socket: runtime.socket,
|
|
135
|
+
responseType,
|
|
136
|
+
timeout,
|
|
137
|
+
});
|
|
138
|
+
runtime.socket.send(runtimeRequestEvent, {
|
|
139
|
+
requestId,
|
|
140
|
+
runtimeId: runtime.runtimeId,
|
|
141
|
+
request,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
99
145
|
function message(socket, event, data) {
|
|
100
146
|
if (!sockets.has(socket))
|
|
101
147
|
return;
|
|
@@ -122,9 +168,38 @@ export function createRuntimeHostSession(options) {
|
|
|
122
168
|
const current = pending.get(data.requestId);
|
|
123
169
|
if (!current || current.socket !== socket || current.runtimeId !== data.runtimeId)
|
|
124
170
|
return;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
if (!data.ok) {
|
|
172
|
+
settle(data.requestId, jsonResponse({ ok: false, error: data.error }, 422));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (current.responseType === 'json') {
|
|
176
|
+
settle(data.requestId, jsonResponse(data.result));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const screenshot = runtimeScreenshotResult(data.result);
|
|
180
|
+
if (!screenshot) {
|
|
181
|
+
settle(data.requestId, jsonResponse({
|
|
182
|
+
ok: false,
|
|
183
|
+
error: 'Pixifact Runtime returned invalid screenshot data.',
|
|
184
|
+
}, 502));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (screenshot.runtimeId !== data.runtimeId) {
|
|
188
|
+
settle(data.requestId, jsonResponse({
|
|
189
|
+
ok: false,
|
|
190
|
+
error: 'Pixifact Runtime returned a screenshot for a different runtime.',
|
|
191
|
+
}, 502));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
settle(data.requestId, new Response(screenshot.png, {
|
|
195
|
+
headers: {
|
|
196
|
+
'cache-control': 'no-store',
|
|
197
|
+
'content-type': 'image/png',
|
|
198
|
+
'x-pixifact-height': String(screenshot.height),
|
|
199
|
+
'x-pixifact-runtime': screenshot.runtimeId,
|
|
200
|
+
'x-pixifact-width': String(screenshot.width),
|
|
201
|
+
},
|
|
202
|
+
}));
|
|
128
203
|
}
|
|
129
204
|
async function fetchRequest(request) {
|
|
130
205
|
const pathname = new URL(request.url).pathname;
|
|
@@ -150,6 +225,23 @@ export function createRuntimeHostSession(options) {
|
|
|
150
225
|
.sort((left, right) => left.connectedAt.localeCompare(right.connectedAt)),
|
|
151
226
|
});
|
|
152
227
|
}
|
|
228
|
+
if (pathname === runtimeScreenshotPath) {
|
|
229
|
+
if (request.method !== 'POST') {
|
|
230
|
+
return jsonResponse({ ok: false, error: 'Pixifact Runtime screenshot requests require POST.' }, 405);
|
|
231
|
+
}
|
|
232
|
+
const body = await request.json();
|
|
233
|
+
if (!isRecord(body) || typeof body.runtimeId !== 'string') {
|
|
234
|
+
return jsonResponse({ ok: false, error: 'Pixifact Runtime screenshot request is invalid.' }, 400);
|
|
235
|
+
}
|
|
236
|
+
const runtime = runtimes.get(body.runtimeId);
|
|
237
|
+
if (!runtime) {
|
|
238
|
+
return jsonResponse({
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Pixifact Runtime "${body.runtimeId}" is not connected.`,
|
|
241
|
+
}, 404);
|
|
242
|
+
}
|
|
243
|
+
return forwardRequest(runtime, { type: 'screenshot' }, 'screenshot');
|
|
244
|
+
}
|
|
153
245
|
if (pathname !== `${runtimeApiRoot}/request`)
|
|
154
246
|
return undefined;
|
|
155
247
|
if (request.method !== 'POST') {
|
|
@@ -166,26 +258,7 @@ export function createRuntimeHostSession(options) {
|
|
|
166
258
|
error: `Pixifact Runtime "${body.runtimeId}" is not connected.`,
|
|
167
259
|
}, 404);
|
|
168
260
|
}
|
|
169
|
-
|
|
170
|
-
return new Promise((resolve) => {
|
|
171
|
-
const timeout = setTimeout(() => {
|
|
172
|
-
settle(requestId, jsonResponse({
|
|
173
|
-
ok: false,
|
|
174
|
-
error: 'Pixifact Runtime request timed out.',
|
|
175
|
-
}, 504));
|
|
176
|
-
}, requestTimeoutMs);
|
|
177
|
-
pending.set(requestId, {
|
|
178
|
-
resolve,
|
|
179
|
-
runtimeId: runtime.runtimeId,
|
|
180
|
-
socket: runtime.socket,
|
|
181
|
-
timeout,
|
|
182
|
-
});
|
|
183
|
-
runtime.socket.send(runtimeRequestEvent, {
|
|
184
|
-
requestId,
|
|
185
|
-
runtimeId: runtime.runtimeId,
|
|
186
|
-
request: body.request,
|
|
187
|
-
});
|
|
188
|
-
});
|
|
261
|
+
return forwardRequest(runtime, body.request, 'json');
|
|
189
262
|
}
|
|
190
263
|
return {
|
|
191
264
|
open,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { access, readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { loadEnv, } from 'vite';
|
|
@@ -204,7 +205,7 @@ export function loadPixifactEnv(projectRoot, mode) {
|
|
|
204
205
|
}
|
|
205
206
|
async function configureState(state, options, config, env) {
|
|
206
207
|
const configuredRoot = options.projectRoot ?? config.root;
|
|
207
|
-
const projectRoot = path.resolve(projectRootPath(configuredRoot));
|
|
208
|
+
const projectRoot = realpathSync(path.resolve(projectRootPath(configuredRoot)));
|
|
208
209
|
const loaded = loadPixifactEnv(projectRoot, env.mode);
|
|
209
210
|
state.projectRoot = projectRoot;
|
|
210
211
|
state.mode = env.mode;
|
|
@@ -2,4 +2,4 @@ import type { Application } from 'pixi.js';
|
|
|
2
2
|
import { type RegisterPixiRuntimeOptions } from './runtimeClient.js';
|
|
3
3
|
export declare function registerPixiRuntime(app: Application, options?: RegisterPixiRuntimeOptions): void;
|
|
4
4
|
export type { RegisterPixiRuntimeOptions } from './runtimeClient.js';
|
|
5
|
-
export type { RuntimeJsonValue, RuntimeLogLevel, RuntimePageDescriptor, RuntimeRequest, } from './protocol.js';
|
|
5
|
+
export type { RuntimeJsonValue, RuntimeLogLevel, RuntimePageDescriptor, RuntimeObservationRequest, RuntimeRequest, RuntimeScreenshotRequest, RuntimeScreenshotResult, } from './protocol.js';
|
|
@@ -22,7 +22,7 @@ export type RuntimeInputRequest = {
|
|
|
22
22
|
action: 'key' | 'keydown' | 'keyup';
|
|
23
23
|
key: string;
|
|
24
24
|
};
|
|
25
|
-
export type
|
|
25
|
+
export type RuntimeObservationRequest = {
|
|
26
26
|
type: 'tree';
|
|
27
27
|
} | {
|
|
28
28
|
type: 'node';
|
|
@@ -34,6 +34,17 @@ export type RuntimeRequest = {
|
|
|
34
34
|
after?: number;
|
|
35
35
|
level?: RuntimeLogLevel;
|
|
36
36
|
} | RuntimeInputRequest;
|
|
37
|
+
export type RuntimeScreenshotRequest = {
|
|
38
|
+
type: 'screenshot';
|
|
39
|
+
};
|
|
40
|
+
export type RuntimeRequest = RuntimeObservationRequest | RuntimeScreenshotRequest;
|
|
41
|
+
export interface RuntimeScreenshotResult {
|
|
42
|
+
[key: string]: RuntimeJsonValue;
|
|
43
|
+
runtimeId: string;
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
dataUrl: string;
|
|
47
|
+
}
|
|
37
48
|
export interface RuntimePageDescriptor {
|
|
38
49
|
runtimeId: string;
|
|
39
50
|
url: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BitmapText, Graphics, Sprite, Text, } from 'pixi.js';
|
|
1
|
+
import { BitmapText, Graphics, Rectangle, Sprite, Text, } from 'pixi.js';
|
|
2
2
|
const maxRetainedLogs = 500;
|
|
3
3
|
const runtimeRequestEvent = 'pixifact:runtime:request';
|
|
4
4
|
const runtimeResponseEvent = 'pixifact:runtime:response';
|
|
@@ -303,6 +303,23 @@ export function createPixifactRuntimeClient(options) {
|
|
|
303
303
|
throw new Error('PixiJS Application has not been registered in this runtime.');
|
|
304
304
|
return app;
|
|
305
305
|
}
|
|
306
|
+
async function captureScreenshot(currentApp) {
|
|
307
|
+
const width = Math.max(1, Math.floor(currentApp.screen.width));
|
|
308
|
+
const height = Math.max(1, Math.floor(currentApp.screen.height));
|
|
309
|
+
const dataUrl = await currentApp.renderer.extract.base64({
|
|
310
|
+
target: currentApp.stage,
|
|
311
|
+
frame: new Rectangle(0, 0, width, height),
|
|
312
|
+
resolution: 1,
|
|
313
|
+
format: 'png',
|
|
314
|
+
clearColor: currentApp.renderer.background.colorRgba,
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
runtimeId: options.runtimeId,
|
|
318
|
+
width,
|
|
319
|
+
height,
|
|
320
|
+
dataUrl,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
306
323
|
async function handleRequest(request) {
|
|
307
324
|
if (request.type === 'logs') {
|
|
308
325
|
return {
|
|
@@ -343,6 +360,9 @@ export function createPixifactRuntimeClient(options) {
|
|
|
343
360
|
state: null,
|
|
344
361
|
};
|
|
345
362
|
}
|
|
363
|
+
if (request.type === 'screenshot') {
|
|
364
|
+
return captureScreenshot(currentApp);
|
|
365
|
+
}
|
|
346
366
|
if (request.type === 'input') {
|
|
347
367
|
dispatchRuntimeInput(options.window, currentApp, request);
|
|
348
368
|
return {
|