pixifact 0.5.0 → 0.6.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/compiler/pixiNodeSchema.d.ts +1 -0
- package/dist/compiler/pixiNodeSchema.js +1 -1
- package/dist/compiler-node/index.d.ts +1 -0
- package/dist/compiler-node/index.js +1 -0
- package/dist/compiler-node/runtimeVite.d.ts +89 -0
- package/dist/compiler-node/runtimeVite.js +420 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +4 -1
- package/dist/runtime-dev/index.d.ts +5 -0
- package/dist/runtime-dev/index.js +22 -0
- package/dist/runtime-dev/protocol.d.ts +58 -0
- package/dist/runtime-dev/protocol.js +1 -0
- package/dist/runtime-dev/runtimeClient.d.ts +22 -0
- package/dist/runtime-dev/runtimeClient.js +382 -0
- package/package.json +6 -1
|
@@ -4,6 +4,7 @@ export type PixiSceneFieldType = 'string' | 'number' | 'boolean' | 'color' | 'en
|
|
|
4
4
|
export type PixiScenePropGroup = 'transform' | 'display' | 'sprite' | 'nineSlice' | 'tiling' | 'text' | 'graphics' | 'stack' | 'props';
|
|
5
5
|
export interface PixiSceneFieldSchema {
|
|
6
6
|
key: string;
|
|
7
|
+
resource?: 'image';
|
|
7
8
|
type: PixiSceneFieldType;
|
|
8
9
|
options?: readonly (string | number)[];
|
|
9
10
|
}
|
|
@@ -218,7 +218,7 @@ const pixiSceneFieldSchemas = {
|
|
|
218
218
|
eventMode: { key: 'eventMode', type: 'enum', options: ['none', 'passive', 'auto', 'static', 'dynamic'] },
|
|
219
219
|
cursor: { key: 'cursor', type: 'enum', options: ['default', 'pointer', 'text', 'grab', 'grabbing'] },
|
|
220
220
|
label: { key: 'label', type: 'string' },
|
|
221
|
-
texture: { key: 'texture', type: 'string' },
|
|
221
|
+
texture: { key: 'texture', type: 'string', resource: 'image' },
|
|
222
222
|
fit: { key: 'fit', type: 'enum', options: ['stretch', 'contain', 'cover', 'none'] },
|
|
223
223
|
anchorX: { key: 'anchorX', type: 'number' },
|
|
224
224
|
anchorY: { key: 'anchorY', type: 'number' },
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { IncomingMessage, Server, ServerResponse } from 'node:http';
|
|
2
|
+
export declare const runtimeSessionProtocolVersion = 1;
|
|
3
|
+
export interface RuntimeSessionDescriptor {
|
|
4
|
+
protocolVersion: typeof runtimeSessionProtocolVersion;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
origin: string;
|
|
8
|
+
token: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RuntimeHostBrowserSocket {
|
|
11
|
+
send(event: string, data: unknown): void;
|
|
12
|
+
}
|
|
13
|
+
interface RuntimeHostSessionOptions {
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
token: string;
|
|
17
|
+
now?: () => Date;
|
|
18
|
+
}
|
|
19
|
+
interface RuntimeSessionLookupOptions {
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
sessionsRoot?: string;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
interface ClaimRuntimeSessionOptions extends RuntimeSessionLookupOptions {
|
|
25
|
+
descriptor: RuntimeSessionDescriptor;
|
|
26
|
+
}
|
|
27
|
+
export interface PixifactRuntimePluginOptions {
|
|
28
|
+
projectRoot?: string | URL;
|
|
29
|
+
}
|
|
30
|
+
interface RuntimeViteClient extends RuntimeHostBrowserSocket {
|
|
31
|
+
socket: {
|
|
32
|
+
once(event: 'close', listener: () => void): void;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
interface RuntimeViteServer {
|
|
36
|
+
config: {
|
|
37
|
+
server: {
|
|
38
|
+
https?: unknown;
|
|
39
|
+
};
|
|
40
|
+
logger: {
|
|
41
|
+
error(message: string): void;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
httpServer: Server | null;
|
|
45
|
+
middlewares: {
|
|
46
|
+
use(handler: (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void): void;
|
|
47
|
+
};
|
|
48
|
+
ws: {
|
|
49
|
+
on(event: string, listener: (data: unknown, client: RuntimeViteClient) => void): void;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function createRuntimeHostSession(options: RuntimeHostSessionOptions): {
|
|
53
|
+
open: (socket: RuntimeHostBrowserSocket) => void;
|
|
54
|
+
close: (socket: RuntimeHostBrowserSocket) => void;
|
|
55
|
+
message: (socket: RuntimeHostBrowserSocket, event: string, data: unknown) => void;
|
|
56
|
+
fetch: (request: Request) => Promise<Response | undefined>;
|
|
57
|
+
};
|
|
58
|
+
export declare function defaultRuntimeSessionsRoot(): string;
|
|
59
|
+
export declare function runtimeSessionDescriptorPath(projectRoot: string, sessionsRoot?: string): string;
|
|
60
|
+
export declare function readRuntimeSessionDescriptor(projectRoot: string, sessionsRoot?: string): RuntimeSessionDescriptor | undefined;
|
|
61
|
+
export declare function writeRuntimeSessionDescriptor(descriptor: RuntimeSessionDescriptor, sessionsRoot?: string): void;
|
|
62
|
+
export declare function removeRuntimeSessionDescriptor(descriptor: RuntimeSessionDescriptor, sessionsRoot?: string): void;
|
|
63
|
+
export declare function findActiveRuntimeSession(options: RuntimeSessionLookupOptions): Promise<RuntimeSessionDescriptor | undefined>;
|
|
64
|
+
export declare function claimRuntimeSession(options: ClaimRuntimeSessionOptions): Promise<{
|
|
65
|
+
descriptor: RuntimeSessionDescriptor;
|
|
66
|
+
owned: false;
|
|
67
|
+
} | {
|
|
68
|
+
descriptor: RuntimeSessionDescriptor;
|
|
69
|
+
owned: true;
|
|
70
|
+
}>;
|
|
71
|
+
export declare function pixifactRuntimePlugin(options?: PixifactRuntimePluginOptions): {
|
|
72
|
+
name: string;
|
|
73
|
+
apply: string;
|
|
74
|
+
configResolved(config: {
|
|
75
|
+
root: string;
|
|
76
|
+
}): void;
|
|
77
|
+
resolveId(id: string): "\0virtual:pixifact-runtime-client" | undefined;
|
|
78
|
+
load(id: string): string | undefined;
|
|
79
|
+
transformIndexHtml(): {
|
|
80
|
+
tag: string;
|
|
81
|
+
attrs: {
|
|
82
|
+
type: string;
|
|
83
|
+
src: string;
|
|
84
|
+
};
|
|
85
|
+
injectTo: "head-prepend";
|
|
86
|
+
}[];
|
|
87
|
+
configureServer(server: RuntimeViteServer): void;
|
|
88
|
+
};
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { pixifactRuntimeProtocolVersion, } from '../runtime-dev/protocol.js';
|
|
7
|
+
export const runtimeSessionProtocolVersion = pixifactRuntimeProtocolVersion;
|
|
8
|
+
const runtimeAnnounceEvent = 'pixifact:runtime:announce';
|
|
9
|
+
const runtimeRequestEvent = 'pixifact:runtime:request';
|
|
10
|
+
const runtimeResponseEvent = 'pixifact:runtime:response';
|
|
11
|
+
const runtimeApiRoot = '/__pixifact_runtime__';
|
|
12
|
+
const virtualRuntimeClientId = 'virtual:pixifact-runtime-client';
|
|
13
|
+
const resolvedVirtualRuntimeClientId = `\0${virtualRuntimeClientId}`;
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function jsonResponse(value, status = 200) {
|
|
18
|
+
return Response.json(value, { status });
|
|
19
|
+
}
|
|
20
|
+
function authorizationMatches(request, token) {
|
|
21
|
+
return request.headers.get('authorization') === `Bearer ${token}`;
|
|
22
|
+
}
|
|
23
|
+
function isRuntimePageDescriptor(value) {
|
|
24
|
+
return isRecord(value)
|
|
25
|
+
&& typeof value.runtimeId === 'string'
|
|
26
|
+
&& value.runtimeId.length > 0
|
|
27
|
+
&& typeof value.url === 'string'
|
|
28
|
+
&& typeof value.title === 'string'
|
|
29
|
+
&& typeof value.ready === 'boolean';
|
|
30
|
+
}
|
|
31
|
+
function isRuntimeHmrResponse(value) {
|
|
32
|
+
if (!isRecord(value)
|
|
33
|
+
|| typeof value.requestId !== 'string'
|
|
34
|
+
|| typeof value.runtimeId !== 'string'
|
|
35
|
+
|| typeof value.ok !== 'boolean') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return value.ok ? 'result' in value : typeof value.error === 'string';
|
|
39
|
+
}
|
|
40
|
+
function isRuntimeRequest(value) {
|
|
41
|
+
if (!isRecord(value) || typeof value.type !== 'string')
|
|
42
|
+
return false;
|
|
43
|
+
if (value.type === 'tree' || value.type === 'state')
|
|
44
|
+
return true;
|
|
45
|
+
if (value.type === 'node')
|
|
46
|
+
return Number.isInteger(value.uid) && Number(value.uid) >= 0;
|
|
47
|
+
if (value.type === 'logs') {
|
|
48
|
+
return (value.after === undefined || (Number.isInteger(value.after) && Number(value.after) >= 0))
|
|
49
|
+
&& (value.level === undefined || ['debug', 'log', 'info', 'warn', 'error'].includes(String(value.level)));
|
|
50
|
+
}
|
|
51
|
+
if (value.type !== 'input' || typeof value.action !== 'string')
|
|
52
|
+
return false;
|
|
53
|
+
if (value.action === 'click' || value.action === 'move') {
|
|
54
|
+
return typeof value.x === 'number' && typeof value.y === 'number';
|
|
55
|
+
}
|
|
56
|
+
return ['key', 'keydown', 'keyup'].includes(value.action)
|
|
57
|
+
&& typeof value.key === 'string'
|
|
58
|
+
&& value.key.length > 0;
|
|
59
|
+
}
|
|
60
|
+
export function createRuntimeHostSession(options) {
|
|
61
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 5_000;
|
|
62
|
+
const now = options.now ?? (() => new Date());
|
|
63
|
+
const sockets = new Set();
|
|
64
|
+
const runtimeIdBySocket = new Map();
|
|
65
|
+
const runtimes = new Map();
|
|
66
|
+
const pending = new Map();
|
|
67
|
+
let requestSequence = 0;
|
|
68
|
+
function settle(requestId, response) {
|
|
69
|
+
const current = pending.get(requestId);
|
|
70
|
+
if (!current)
|
|
71
|
+
return;
|
|
72
|
+
pending.delete(requestId);
|
|
73
|
+
clearTimeout(current.timeout);
|
|
74
|
+
current.resolve(response);
|
|
75
|
+
}
|
|
76
|
+
function removeSocketRuntime(socket) {
|
|
77
|
+
const runtimeId = runtimeIdBySocket.get(socket);
|
|
78
|
+
if (!runtimeId)
|
|
79
|
+
return;
|
|
80
|
+
runtimeIdBySocket.delete(socket);
|
|
81
|
+
if (runtimes.get(runtimeId)?.socket === socket)
|
|
82
|
+
runtimes.delete(runtimeId);
|
|
83
|
+
}
|
|
84
|
+
function open(socket) {
|
|
85
|
+
sockets.add(socket);
|
|
86
|
+
}
|
|
87
|
+
function close(socket) {
|
|
88
|
+
sockets.delete(socket);
|
|
89
|
+
removeSocketRuntime(socket);
|
|
90
|
+
for (const [requestId, current] of pending) {
|
|
91
|
+
if (current.socket === socket) {
|
|
92
|
+
settle(requestId, jsonResponse({
|
|
93
|
+
ok: false,
|
|
94
|
+
error: 'Pixifact Runtime page disconnected during the request.',
|
|
95
|
+
}, 409));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function message(socket, event, data) {
|
|
100
|
+
if (!sockets.has(socket))
|
|
101
|
+
return;
|
|
102
|
+
if (event === runtimeAnnounceEvent) {
|
|
103
|
+
if (!isRuntimePageDescriptor(data)) {
|
|
104
|
+
throw new Error('Pixifact Runtime page descriptor is invalid.');
|
|
105
|
+
}
|
|
106
|
+
const previousId = runtimeIdBySocket.get(socket);
|
|
107
|
+
const previous = runtimes.get(data.runtimeId);
|
|
108
|
+
if (previousId && previousId !== data.runtimeId)
|
|
109
|
+
runtimes.delete(previousId);
|
|
110
|
+
if (previous && previous.socket !== socket)
|
|
111
|
+
runtimeIdBySocket.delete(previous.socket);
|
|
112
|
+
runtimeIdBySocket.set(socket, data.runtimeId);
|
|
113
|
+
runtimes.set(data.runtimeId, {
|
|
114
|
+
...data,
|
|
115
|
+
connectedAt: previous?.connectedAt ?? now().toISOString(),
|
|
116
|
+
socket,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (event !== runtimeResponseEvent || !isRuntimeHmrResponse(data))
|
|
121
|
+
return;
|
|
122
|
+
const current = pending.get(data.requestId);
|
|
123
|
+
if (!current || current.socket !== socket || current.runtimeId !== data.runtimeId)
|
|
124
|
+
return;
|
|
125
|
+
settle(data.requestId, data.ok
|
|
126
|
+
? jsonResponse(data.result)
|
|
127
|
+
: jsonResponse({ ok: false, error: data.error }, 422));
|
|
128
|
+
}
|
|
129
|
+
async function fetchRequest(request) {
|
|
130
|
+
const pathname = new URL(request.url).pathname;
|
|
131
|
+
if (!pathname.startsWith(`${runtimeApiRoot}/`))
|
|
132
|
+
return undefined;
|
|
133
|
+
if (!authorizationMatches(request, options.token)) {
|
|
134
|
+
return jsonResponse({
|
|
135
|
+
ok: false,
|
|
136
|
+
error: 'Pixifact Runtime session token is invalid.',
|
|
137
|
+
}, 401);
|
|
138
|
+
}
|
|
139
|
+
if (pathname === `${runtimeApiRoot}/health`) {
|
|
140
|
+
return jsonResponse({
|
|
141
|
+
protocolVersion: runtimeSessionProtocolVersion,
|
|
142
|
+
projectRoot: options.projectRoot,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (pathname === `${runtimeApiRoot}/list`) {
|
|
146
|
+
return jsonResponse({
|
|
147
|
+
ok: true,
|
|
148
|
+
runtimes: [...runtimes.values()]
|
|
149
|
+
.map(({ socket: _socket, ...runtime }) => runtime)
|
|
150
|
+
.sort((left, right) => left.connectedAt.localeCompare(right.connectedAt)),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (pathname !== `${runtimeApiRoot}/request`)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (request.method !== 'POST') {
|
|
156
|
+
return jsonResponse({ ok: false, error: 'Pixifact Runtime requests require POST.' }, 405);
|
|
157
|
+
}
|
|
158
|
+
const body = await request.json();
|
|
159
|
+
if (!isRecord(body) || typeof body.runtimeId !== 'string' || !isRuntimeRequest(body.request)) {
|
|
160
|
+
return jsonResponse({ ok: false, error: 'Pixifact Runtime request is invalid.' }, 400);
|
|
161
|
+
}
|
|
162
|
+
const runtime = runtimes.get(body.runtimeId);
|
|
163
|
+
if (!runtime) {
|
|
164
|
+
return jsonResponse({
|
|
165
|
+
ok: false,
|
|
166
|
+
error: `Pixifact Runtime "${body.runtimeId}" is not connected.`,
|
|
167
|
+
}, 404);
|
|
168
|
+
}
|
|
169
|
+
const requestId = `runtime-${++requestSequence}`;
|
|
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
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
open,
|
|
192
|
+
close,
|
|
193
|
+
message,
|
|
194
|
+
fetch: fetchRequest,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function defaultRuntimeSessionsRoot() {
|
|
198
|
+
return path.join(os.tmpdir(), 'pixifact', 'runtime-sessions');
|
|
199
|
+
}
|
|
200
|
+
export function runtimeSessionDescriptorPath(projectRoot, sessionsRoot = defaultRuntimeSessionsRoot()) {
|
|
201
|
+
const canonicalRoot = fs.realpathSync(projectRoot);
|
|
202
|
+
const projectHash = createHash('sha256').update(canonicalRoot).digest('hex');
|
|
203
|
+
return path.join(sessionsRoot, `${projectHash}.json`);
|
|
204
|
+
}
|
|
205
|
+
function isLoopbackOrigin(origin) {
|
|
206
|
+
const hostname = new URL(origin).hostname;
|
|
207
|
+
return hostname === 'localhost' || hostname === '::1' || hostname.startsWith('127.');
|
|
208
|
+
}
|
|
209
|
+
function validateRuntimeSessionDescriptor(value) {
|
|
210
|
+
if (!isRecord(value)
|
|
211
|
+
|| value.protocolVersion !== runtimeSessionProtocolVersion
|
|
212
|
+
|| typeof value.projectRoot !== 'string'
|
|
213
|
+
|| typeof value.pid !== 'number'
|
|
214
|
+
|| typeof value.origin !== 'string'
|
|
215
|
+
|| typeof value.token !== 'string'
|
|
216
|
+
|| !isLoopbackOrigin(value.origin)) {
|
|
217
|
+
throw new Error('Pixifact Runtime session descriptor is invalid.');
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
export function readRuntimeSessionDescriptor(projectRoot, sessionsRoot = defaultRuntimeSessionsRoot()) {
|
|
222
|
+
const descriptorPath = runtimeSessionDescriptorPath(projectRoot, sessionsRoot);
|
|
223
|
+
if (!fs.existsSync(descriptorPath))
|
|
224
|
+
return undefined;
|
|
225
|
+
return validateRuntimeSessionDescriptor(JSON.parse(fs.readFileSync(descriptorPath, 'utf8')));
|
|
226
|
+
}
|
|
227
|
+
export function writeRuntimeSessionDescriptor(descriptor, sessionsRoot = defaultRuntimeSessionsRoot()) {
|
|
228
|
+
const canonicalRoot = fs.realpathSync(descriptor.projectRoot);
|
|
229
|
+
const stored = validateRuntimeSessionDescriptor({ ...descriptor, projectRoot: canonicalRoot });
|
|
230
|
+
fs.mkdirSync(sessionsRoot, { recursive: true, mode: 0o700 });
|
|
231
|
+
fs.writeFileSync(runtimeSessionDescriptorPath(canonicalRoot, sessionsRoot), `${JSON.stringify(stored, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
232
|
+
}
|
|
233
|
+
export function removeRuntimeSessionDescriptor(descriptor, sessionsRoot = defaultRuntimeSessionsRoot()) {
|
|
234
|
+
const descriptorPath = runtimeSessionDescriptorPath(descriptor.projectRoot, sessionsRoot);
|
|
235
|
+
if (!fs.existsSync(descriptorPath))
|
|
236
|
+
return;
|
|
237
|
+
const current = readRuntimeSessionDescriptor(descriptor.projectRoot, sessionsRoot);
|
|
238
|
+
if (current?.token === descriptor.token)
|
|
239
|
+
fs.unlinkSync(descriptorPath);
|
|
240
|
+
}
|
|
241
|
+
function validateRuntimeHealth(value) {
|
|
242
|
+
if (!isRecord(value)
|
|
243
|
+
|| value.protocolVersion !== runtimeSessionProtocolVersion
|
|
244
|
+
|| typeof value.projectRoot !== 'string') {
|
|
245
|
+
throw new Error('Pixifact Runtime Host health response is invalid.');
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
export async function findActiveRuntimeSession(options) {
|
|
250
|
+
const projectRoot = fs.realpathSync(options.projectRoot);
|
|
251
|
+
const descriptor = readRuntimeSessionDescriptor(projectRoot, options.sessionsRoot);
|
|
252
|
+
if (!descriptor)
|
|
253
|
+
return undefined;
|
|
254
|
+
if (descriptor.projectRoot !== projectRoot) {
|
|
255
|
+
throw new Error('Pixifact Runtime session belongs to a different project.');
|
|
256
|
+
}
|
|
257
|
+
const fetcher = options.fetch ?? fetch;
|
|
258
|
+
try {
|
|
259
|
+
const response = await fetcher(`${descriptor.origin}${runtimeApiRoot}/health`, {
|
|
260
|
+
headers: { authorization: `Bearer ${descriptor.token}` },
|
|
261
|
+
});
|
|
262
|
+
if (!response.ok)
|
|
263
|
+
return undefined;
|
|
264
|
+
const health = validateRuntimeHealth(await response.json());
|
|
265
|
+
if (health.projectRoot !== projectRoot) {
|
|
266
|
+
throw new Error('Pixifact Runtime Host health check returned a different project.');
|
|
267
|
+
}
|
|
268
|
+
return descriptor;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error instanceof Error && error.message.includes('different project'))
|
|
272
|
+
throw error;
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export async function claimRuntimeSession(options) {
|
|
277
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
278
|
+
const current = readRuntimeSessionDescriptor(options.projectRoot, options.sessionsRoot);
|
|
279
|
+
const active = await findActiveRuntimeSession(options);
|
|
280
|
+
if (active)
|
|
281
|
+
return { descriptor: active, owned: false };
|
|
282
|
+
if (current)
|
|
283
|
+
removeRuntimeSessionDescriptor(current, options.sessionsRoot);
|
|
284
|
+
try {
|
|
285
|
+
writeRuntimeSessionDescriptor(options.descriptor, options.sessionsRoot);
|
|
286
|
+
return { descriptor: options.descriptor, owned: true };
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (!isRecord(error) || error.code !== 'EEXIST')
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw new Error('Pixifact Runtime session could not be registered.');
|
|
294
|
+
}
|
|
295
|
+
function projectRootPath(projectRoot, viteRoot) {
|
|
296
|
+
if (typeof projectRoot === 'string')
|
|
297
|
+
return projectRoot;
|
|
298
|
+
if (projectRoot)
|
|
299
|
+
return fileURLToPath(projectRoot);
|
|
300
|
+
return viteRoot;
|
|
301
|
+
}
|
|
302
|
+
function serverOrigin(server) {
|
|
303
|
+
const address = server.httpServer?.address();
|
|
304
|
+
if (!address || typeof address === 'string') {
|
|
305
|
+
throw new Error('Pixifact Runtime requires a listening Vite HTTP server.');
|
|
306
|
+
}
|
|
307
|
+
if (!(address.address === '::1' || address.address.startsWith('127.'))) {
|
|
308
|
+
throw new Error('Pixifact Runtime requires the Vite server to bind to a loopback address.');
|
|
309
|
+
}
|
|
310
|
+
const hostname = address.address === '::1' ? '[::1]' : address.address;
|
|
311
|
+
const protocol = server.config.server.https ? 'https' : 'http';
|
|
312
|
+
return `${protocol}://${hostname}:${address.port}`;
|
|
313
|
+
}
|
|
314
|
+
async function webRequestFromNode(request, origin) {
|
|
315
|
+
const chunks = [];
|
|
316
|
+
for await (const chunk of request) {
|
|
317
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
318
|
+
}
|
|
319
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
|
|
320
|
+
return new Request(new URL(request.url ?? '/', origin), {
|
|
321
|
+
method: request.method,
|
|
322
|
+
headers: request.headers,
|
|
323
|
+
...(body ? { body } : {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
async function writeNodeResponse(response, result) {
|
|
327
|
+
response.statusCode = result.status;
|
|
328
|
+
result.headers.forEach((value, name) => response.setHeader(name, value));
|
|
329
|
+
response.end(Buffer.from(await result.arrayBuffer()));
|
|
330
|
+
}
|
|
331
|
+
export function pixifactRuntimePlugin(options = {}) {
|
|
332
|
+
let projectRoot = '';
|
|
333
|
+
const token = randomBytes(32).toString('hex');
|
|
334
|
+
return {
|
|
335
|
+
name: 'pixifact-runtime',
|
|
336
|
+
apply: 'serve',
|
|
337
|
+
configResolved(config) {
|
|
338
|
+
projectRoot = fs.realpathSync(projectRootPath(options.projectRoot, config.root));
|
|
339
|
+
},
|
|
340
|
+
resolveId(id) {
|
|
341
|
+
return id === virtualRuntimeClientId ? resolvedVirtualRuntimeClientId : undefined;
|
|
342
|
+
},
|
|
343
|
+
load(id) {
|
|
344
|
+
if (id !== resolvedVirtualRuntimeClientId)
|
|
345
|
+
return undefined;
|
|
346
|
+
return [
|
|
347
|
+
"const runtimeHotKey = Symbol.for('pixifact.runtime.hot');",
|
|
348
|
+
'globalThis[runtimeHotKey] = import.meta.hot;',
|
|
349
|
+
"await import('pixifact/runtime-dev');",
|
|
350
|
+
'delete globalThis[runtimeHotKey];',
|
|
351
|
+
].join('\n');
|
|
352
|
+
},
|
|
353
|
+
transformIndexHtml() {
|
|
354
|
+
return [{
|
|
355
|
+
tag: 'script',
|
|
356
|
+
attrs: {
|
|
357
|
+
type: 'module',
|
|
358
|
+
src: `/@id/${virtualRuntimeClientId}`,
|
|
359
|
+
},
|
|
360
|
+
injectTo: 'head-prepend',
|
|
361
|
+
}];
|
|
362
|
+
},
|
|
363
|
+
configureServer(server) {
|
|
364
|
+
const host = createRuntimeHostSession({ projectRoot, token });
|
|
365
|
+
const openedClients = new WeakSet();
|
|
366
|
+
function openClient(client) {
|
|
367
|
+
if (openedClients.has(client))
|
|
368
|
+
return;
|
|
369
|
+
openedClients.add(client);
|
|
370
|
+
host.open(client);
|
|
371
|
+
client.socket.once('close', () => host.close(client));
|
|
372
|
+
}
|
|
373
|
+
server.ws.on(runtimeAnnounceEvent, (data, client) => {
|
|
374
|
+
openClient(client);
|
|
375
|
+
host.message(client, runtimeAnnounceEvent, data);
|
|
376
|
+
});
|
|
377
|
+
server.ws.on(runtimeResponseEvent, (data, client) => {
|
|
378
|
+
openClient(client);
|
|
379
|
+
host.message(client, runtimeResponseEvent, data);
|
|
380
|
+
});
|
|
381
|
+
server.middlewares.use((request, response, next) => {
|
|
382
|
+
void (async () => {
|
|
383
|
+
const webRequest = await webRequestFromNode(request, serverOrigin(server));
|
|
384
|
+
const result = await host.fetch(webRequest);
|
|
385
|
+
if (!result) {
|
|
386
|
+
next();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await writeNodeResponse(response, result);
|
|
390
|
+
})().catch(next);
|
|
391
|
+
});
|
|
392
|
+
const httpServer = server.httpServer;
|
|
393
|
+
if (!httpServer)
|
|
394
|
+
throw new Error('Pixifact Runtime requires the Vite HTTP server.');
|
|
395
|
+
let descriptor;
|
|
396
|
+
httpServer.once('listening', () => {
|
|
397
|
+
void (async () => {
|
|
398
|
+
const candidate = {
|
|
399
|
+
protocolVersion: runtimeSessionProtocolVersion,
|
|
400
|
+
projectRoot,
|
|
401
|
+
pid: process.pid,
|
|
402
|
+
origin: serverOrigin(server),
|
|
403
|
+
token,
|
|
404
|
+
};
|
|
405
|
+
const claim = await claimRuntimeSession({ projectRoot, descriptor: candidate });
|
|
406
|
+
if (!claim.owned) {
|
|
407
|
+
throw new Error('Another Pixifact Runtime Vite server is already running for this project.');
|
|
408
|
+
}
|
|
409
|
+
descriptor = candidate;
|
|
410
|
+
})().catch((error) => {
|
|
411
|
+
server.config.logger.error(error instanceof Error ? error.message : String(error));
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
httpServer.once('close', () => {
|
|
415
|
+
if (descriptor)
|
|
416
|
+
removeRuntimeSessionDescriptor(descriptor);
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function hintForCommandError(error: string): "Use a project-relative scene path that stays inside projectRoot." | "Choose a different scene path or inspect the existing file before writing." | "Use one of: button, progressBar, scrollView, loginForm." | "
|
|
1
|
+
export declare function hintForCommandError(error: string): "Use a project-relative scene path that stays inside projectRoot." | "Choose a different scene path or inspect the existing file before writing." | "Use one of: button, progressBar, scrollView, loginForm." | "Run scene inspect --scene <scene-path> and pass one of the returned node locator values." | "Inspect the current project state again before regenerating the command." | "Choose a container node as the parent, or create a container template before adding children." | "Verify that the field belongs to the target node or component schema before retrying." | "Check the target schema and send a value with the expected type." | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -8,8 +8,11 @@ export function hintForCommandError(error) {
|
|
|
8
8
|
if (error.includes('Unknown template kind')) {
|
|
9
9
|
return 'Use one of: button, progressBar, scrollView, loginForm.';
|
|
10
10
|
}
|
|
11
|
+
if (error.includes('Node locator') && error.includes('was not found')) {
|
|
12
|
+
return 'Run scene inspect --scene <scene-path> and pass one of the returned node locator values.';
|
|
13
|
+
}
|
|
11
14
|
if (error.includes('was not found')) {
|
|
12
|
-
return '
|
|
15
|
+
return 'Inspect the current project state again before regenerating the command.';
|
|
13
16
|
}
|
|
14
17
|
if (error.includes('Only container nodes')) {
|
|
15
18
|
return 'Choose a container node as the parent, or create a container template before adding children.';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Application } from 'pixi.js';
|
|
2
|
+
import { type RegisterPixiRuntimeOptions } from './runtimeClient.js';
|
|
3
|
+
export declare function registerPixiRuntime(app: Application, options?: RegisterPixiRuntimeOptions): void;
|
|
4
|
+
export type { RegisterPixiRuntimeOptions } from './runtimeClient.js';
|
|
5
|
+
export type { RuntimeJsonValue, RuntimeLogLevel, RuntimePageDescriptor, RuntimeRequest, } from './protocol.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createPixifactRuntimeClient, } from './runtimeClient.js';
|
|
2
|
+
const runtimeHotKey = Symbol.for('pixifact.runtime.hot');
|
|
3
|
+
const runtimeClientKey = Symbol.for('pixifact.runtime.client');
|
|
4
|
+
const runtimeGlobal = globalThis;
|
|
5
|
+
const injectedHot = runtimeGlobal[runtimeHotKey];
|
|
6
|
+
const existingRuntimeClient = runtimeGlobal[runtimeClientKey];
|
|
7
|
+
const runtimeClient = existingRuntimeClient ?? (injectedHot
|
|
8
|
+
? createPixifactRuntimeClient({
|
|
9
|
+
runtimeId: crypto.randomUUID(),
|
|
10
|
+
hot: injectedHot,
|
|
11
|
+
console,
|
|
12
|
+
window,
|
|
13
|
+
})
|
|
14
|
+
: undefined);
|
|
15
|
+
if (runtimeClient && !existingRuntimeClient)
|
|
16
|
+
runtimeGlobal[runtimeClientKey] = runtimeClient;
|
|
17
|
+
export function registerPixiRuntime(app, options = {}) {
|
|
18
|
+
if (!runtimeClient) {
|
|
19
|
+
throw new Error('registerPixiRuntime requires pixifactRuntimePlugin in the Vite development server.');
|
|
20
|
+
}
|
|
21
|
+
runtimeClient.register(app, options);
|
|
22
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export declare const pixifactRuntimeProtocolVersion = 1;
|
|
2
|
+
export type RuntimeJsonPrimitive = boolean | number | string | null;
|
|
3
|
+
export type RuntimeJsonValue = RuntimeJsonPrimitive | RuntimeJsonValue[] | {
|
|
4
|
+
[key: string]: RuntimeJsonValue;
|
|
5
|
+
};
|
|
6
|
+
export type RuntimeLogLevel = 'debug' | 'log' | 'info' | 'warn' | 'error';
|
|
7
|
+
export interface RuntimeLogEntry {
|
|
8
|
+
seq: number;
|
|
9
|
+
time: string;
|
|
10
|
+
level: RuntimeLogLevel;
|
|
11
|
+
message: string;
|
|
12
|
+
args: RuntimeJsonValue[];
|
|
13
|
+
stack?: string;
|
|
14
|
+
}
|
|
15
|
+
export type RuntimeInputRequest = {
|
|
16
|
+
type: 'input';
|
|
17
|
+
action: 'click' | 'move';
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'input';
|
|
22
|
+
action: 'key' | 'keydown' | 'keyup';
|
|
23
|
+
key: string;
|
|
24
|
+
};
|
|
25
|
+
export type RuntimeRequest = {
|
|
26
|
+
type: 'tree';
|
|
27
|
+
} | {
|
|
28
|
+
type: 'node';
|
|
29
|
+
uid: number;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'state';
|
|
32
|
+
} | {
|
|
33
|
+
type: 'logs';
|
|
34
|
+
after?: number;
|
|
35
|
+
level?: RuntimeLogLevel;
|
|
36
|
+
} | RuntimeInputRequest;
|
|
37
|
+
export interface RuntimePageDescriptor {
|
|
38
|
+
runtimeId: string;
|
|
39
|
+
url: string;
|
|
40
|
+
title: string;
|
|
41
|
+
ready: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface RuntimeHmrRequest {
|
|
44
|
+
requestId: string;
|
|
45
|
+
runtimeId: string;
|
|
46
|
+
request: RuntimeRequest;
|
|
47
|
+
}
|
|
48
|
+
export type RuntimeHmrResponse = {
|
|
49
|
+
requestId: string;
|
|
50
|
+
runtimeId: string;
|
|
51
|
+
ok: true;
|
|
52
|
+
result: RuntimeJsonValue;
|
|
53
|
+
} | {
|
|
54
|
+
requestId: string;
|
|
55
|
+
runtimeId: string;
|
|
56
|
+
ok: false;
|
|
57
|
+
error: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const pixifactRuntimeProtocolVersion = 1;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Application } from 'pixi.js';
|
|
2
|
+
import { type RuntimeJsonValue, type RuntimeRequest } from './protocol.js';
|
|
3
|
+
export interface RegisterPixiRuntimeOptions {
|
|
4
|
+
getState?: () => RuntimeJsonValue;
|
|
5
|
+
}
|
|
6
|
+
export interface PixifactRuntimeHotContext {
|
|
7
|
+
on(event: string, listener: (data: unknown) => void): void;
|
|
8
|
+
send(event: string, data: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
interface RuntimeClientOptions {
|
|
11
|
+
runtimeId: string;
|
|
12
|
+
hot: PixifactRuntimeHotContext;
|
|
13
|
+
console: Console;
|
|
14
|
+
window: Window;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
}
|
|
17
|
+
export declare function createPixifactRuntimeClient(options: RuntimeClientOptions): {
|
|
18
|
+
register(application: Application, registerOptions?: RegisterPixiRuntimeOptions): void;
|
|
19
|
+
handleRequest: (request: RuntimeRequest) => Promise<RuntimeJsonValue>;
|
|
20
|
+
dispose: () => void;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { BitmapText, Graphics, Sprite, Text, } from 'pixi.js';
|
|
2
|
+
const maxRetainedLogs = 500;
|
|
3
|
+
const runtimeRequestEvent = 'pixifact:runtime:request';
|
|
4
|
+
const runtimeResponseEvent = 'pixifact:runtime:response';
|
|
5
|
+
const runtimeAnnounceEvent = 'pixifact:runtime:announce';
|
|
6
|
+
function pointSnapshot(point) {
|
|
7
|
+
return { x: point.x, y: point.y };
|
|
8
|
+
}
|
|
9
|
+
function boundsSnapshot(bounds) {
|
|
10
|
+
return {
|
|
11
|
+
x: bounds.x,
|
|
12
|
+
y: bounds.y,
|
|
13
|
+
width: bounds.width,
|
|
14
|
+
height: bounds.height,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function basicNodeSnapshot(node, childIndex) {
|
|
18
|
+
return {
|
|
19
|
+
uid: node.uid,
|
|
20
|
+
type: node.constructor.name,
|
|
21
|
+
label: node.label,
|
|
22
|
+
childIndex,
|
|
23
|
+
position: pointSnapshot(node.position),
|
|
24
|
+
scale: pointSnapshot(node.scale),
|
|
25
|
+
rotation: node.rotation,
|
|
26
|
+
alpha: node.alpha,
|
|
27
|
+
visible: node.visible,
|
|
28
|
+
renderable: node.renderable,
|
|
29
|
+
zIndex: node.zIndex,
|
|
30
|
+
eventMode: node.eventMode ?? null,
|
|
31
|
+
children: node.children.map((child, index) => basicNodeSnapshot(child, index)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function supportedFill(fill) {
|
|
35
|
+
return typeof fill === 'number' || typeof fill === 'string' ? fill : null;
|
|
36
|
+
}
|
|
37
|
+
function specificNodeSnapshot(node) {
|
|
38
|
+
if (node instanceof Sprite) {
|
|
39
|
+
return {
|
|
40
|
+
kind: 'sprite',
|
|
41
|
+
anchor: pointSnapshot(node.anchor),
|
|
42
|
+
texture: {
|
|
43
|
+
uid: node.texture.uid,
|
|
44
|
+
label: node.texture.label ?? null,
|
|
45
|
+
width: node.texture.width,
|
|
46
|
+
height: node.texture.height,
|
|
47
|
+
},
|
|
48
|
+
tint: node.tint,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (node instanceof BitmapText || node instanceof Text) {
|
|
52
|
+
return {
|
|
53
|
+
kind: node instanceof BitmapText ? 'bitmapText' : 'text',
|
|
54
|
+
text: node.text,
|
|
55
|
+
anchor: pointSnapshot(node.anchor),
|
|
56
|
+
style: {
|
|
57
|
+
align: node.style.align,
|
|
58
|
+
fill: supportedFill(node.style.fill),
|
|
59
|
+
fontFamily: Array.isArray(node.style.fontFamily)
|
|
60
|
+
? [...node.style.fontFamily]
|
|
61
|
+
: node.style.fontFamily,
|
|
62
|
+
fontSize: node.style.fontSize,
|
|
63
|
+
fontStyle: node.style.fontStyle,
|
|
64
|
+
fontWeight: node.style.fontWeight,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (node instanceof Graphics) {
|
|
69
|
+
return { kind: 'graphics' };
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function findStageNode(stage, uid) {
|
|
74
|
+
if (stage.uid === uid)
|
|
75
|
+
return stage;
|
|
76
|
+
for (const child of stage.children) {
|
|
77
|
+
const result = findStageNode(child, uid);
|
|
78
|
+
if (result)
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function detailedNodeSnapshot(node) {
|
|
84
|
+
const childIndex = node.parent ? node.parent.children.indexOf(node) : null;
|
|
85
|
+
const specific = specificNodeSnapshot(node);
|
|
86
|
+
return {
|
|
87
|
+
uid: node.uid,
|
|
88
|
+
type: node.constructor.name,
|
|
89
|
+
label: node.label,
|
|
90
|
+
parentUid: node.parent?.uid ?? null,
|
|
91
|
+
childIndex,
|
|
92
|
+
childCount: node.children.length,
|
|
93
|
+
position: pointSnapshot(node.position),
|
|
94
|
+
scale: pointSnapshot(node.scale),
|
|
95
|
+
pivot: pointSnapshot(node.pivot),
|
|
96
|
+
skew: pointSnapshot(node.skew),
|
|
97
|
+
rotation: node.rotation,
|
|
98
|
+
angle: node.angle,
|
|
99
|
+
width: node.width,
|
|
100
|
+
height: node.height,
|
|
101
|
+
localBounds: boundsSnapshot(node.getLocalBounds()),
|
|
102
|
+
globalBounds: boundsSnapshot(node.getBounds()),
|
|
103
|
+
alpha: node.alpha,
|
|
104
|
+
worldAlpha: node.getGlobalAlpha(),
|
|
105
|
+
visible: node.visible,
|
|
106
|
+
renderable: node.renderable,
|
|
107
|
+
zIndex: node.zIndex,
|
|
108
|
+
tint: node.tint,
|
|
109
|
+
blendMode: String(node.blendMode),
|
|
110
|
+
eventMode: node.eventMode ?? null,
|
|
111
|
+
cursor: node.cursor === undefined ? null : String(node.cursor),
|
|
112
|
+
...(specific ? { specific } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function snapshotLogValue(value, seen = new WeakSet(), depth = 0) {
|
|
116
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === 'bigint')
|
|
120
|
+
return `${value}n`;
|
|
121
|
+
if (typeof value === 'undefined')
|
|
122
|
+
return '[undefined]';
|
|
123
|
+
if (typeof value === 'symbol')
|
|
124
|
+
return String(value);
|
|
125
|
+
if (typeof value === 'function')
|
|
126
|
+
return `[Function ${value.name || 'anonymous'}]`;
|
|
127
|
+
if (value instanceof Error) {
|
|
128
|
+
return {
|
|
129
|
+
name: value.name,
|
|
130
|
+
message: value.message,
|
|
131
|
+
stack: value.stack ?? '',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (value instanceof Date)
|
|
135
|
+
return value.toISOString();
|
|
136
|
+
if (depth >= 4)
|
|
137
|
+
return `[${value.constructor?.name ?? 'Object'}]`;
|
|
138
|
+
if (seen.has(value))
|
|
139
|
+
return '[Circular]';
|
|
140
|
+
seen.add(value);
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
return value.map((item) => snapshotLogValue(item, seen, depth + 1));
|
|
143
|
+
}
|
|
144
|
+
const snapshot = {};
|
|
145
|
+
for (const key of Object.keys(value)) {
|
|
146
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
147
|
+
snapshot[key] = descriptor && 'value' in descriptor
|
|
148
|
+
? snapshotLogValue(descriptor.value, seen, depth + 1)
|
|
149
|
+
: '[Getter]';
|
|
150
|
+
}
|
|
151
|
+
return snapshot;
|
|
152
|
+
}
|
|
153
|
+
function logMessage(args) {
|
|
154
|
+
const first = args[0];
|
|
155
|
+
if (typeof first === 'string')
|
|
156
|
+
return first;
|
|
157
|
+
if (first instanceof Error)
|
|
158
|
+
return first.message;
|
|
159
|
+
return JSON.stringify(snapshotLogValue(first));
|
|
160
|
+
}
|
|
161
|
+
function createRuntimeLogCapture(targetConsole, targetWindow, now) {
|
|
162
|
+
const retained = [];
|
|
163
|
+
const originals = new Map();
|
|
164
|
+
let sequence = 0;
|
|
165
|
+
function append(level, args, explicitStack) {
|
|
166
|
+
const stack = explicitStack ?? args.find((value) => value instanceof Error)?.stack;
|
|
167
|
+
retained.push({
|
|
168
|
+
seq: ++sequence,
|
|
169
|
+
time: now().toISOString(),
|
|
170
|
+
level,
|
|
171
|
+
message: logMessage(args),
|
|
172
|
+
args: args.slice(typeof args[0] === 'string' ? 1 : 0).map((value) => snapshotLogValue(value)),
|
|
173
|
+
...(stack ? { stack } : {}),
|
|
174
|
+
});
|
|
175
|
+
if (retained.length > maxRetainedLogs)
|
|
176
|
+
retained.splice(0, retained.length - maxRetainedLogs);
|
|
177
|
+
}
|
|
178
|
+
for (const level of ['debug', 'log', 'info', 'warn', 'error']) {
|
|
179
|
+
const original = targetConsole[level];
|
|
180
|
+
originals.set(level, original);
|
|
181
|
+
targetConsole[level] = ((...args) => {
|
|
182
|
+
original.apply(targetConsole, args);
|
|
183
|
+
append(level, args);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const onWindowError = (event) => {
|
|
187
|
+
append('error', [event.message, event.error ?? null], event.error instanceof Error ? event.error.stack : undefined);
|
|
188
|
+
};
|
|
189
|
+
const onUnhandledRejection = (event) => {
|
|
190
|
+
const reason = event.reason;
|
|
191
|
+
append('error', ['Unhandled promise rejection', reason], reason instanceof Error ? reason.stack : undefined);
|
|
192
|
+
};
|
|
193
|
+
targetWindow.addEventListener('error', onWindowError);
|
|
194
|
+
targetWindow.addEventListener('unhandledrejection', onUnhandledRejection);
|
|
195
|
+
return {
|
|
196
|
+
entries: () => retained,
|
|
197
|
+
latestSeq: () => sequence,
|
|
198
|
+
dispose() {
|
|
199
|
+
for (const [level, original] of originals) {
|
|
200
|
+
targetConsole[level] = original;
|
|
201
|
+
}
|
|
202
|
+
targetWindow.removeEventListener('error', onWindowError);
|
|
203
|
+
targetWindow.removeEventListener('unhandledrejection', onUnhandledRejection);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function pointerEvent(targetWindow, type, point, buttons) {
|
|
208
|
+
const eventWindow = targetWindow;
|
|
209
|
+
return new eventWindow.PointerEvent(type, {
|
|
210
|
+
bubbles: true,
|
|
211
|
+
cancelable: true,
|
|
212
|
+
pointerId: 1,
|
|
213
|
+
pointerType: 'mouse',
|
|
214
|
+
isPrimary: true,
|
|
215
|
+
button: 0,
|
|
216
|
+
buttons,
|
|
217
|
+
...point,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function clientPoint(app, x, y) {
|
|
221
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
222
|
+
throw new Error('Runtime pointer coordinates must be finite numbers.');
|
|
223
|
+
}
|
|
224
|
+
if (x < 0 || y < 0 || x > app.screen.width || y > app.screen.height) {
|
|
225
|
+
throw new Error(`Runtime pointer coordinates must stay inside ${app.screen.width} x ${app.screen.height}.`);
|
|
226
|
+
}
|
|
227
|
+
const bounds = app.canvas.getBoundingClientRect();
|
|
228
|
+
return {
|
|
229
|
+
clientX: bounds.left + (x / app.screen.width) * bounds.width,
|
|
230
|
+
clientY: bounds.top + (y / app.screen.height) * bounds.height,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function keyboardIdentity(key) {
|
|
234
|
+
if (key === 'Space')
|
|
235
|
+
return { key: ' ', code: 'Space' };
|
|
236
|
+
if (/^Key[A-Z]$/.test(key))
|
|
237
|
+
return { key: key.slice(3).toLowerCase(), code: key };
|
|
238
|
+
if (/^Digit[0-9]$/.test(key))
|
|
239
|
+
return { key: key.slice(5), code: key };
|
|
240
|
+
return { key, code: key };
|
|
241
|
+
}
|
|
242
|
+
function dispatchRuntimeInput(targetWindow, app, request) {
|
|
243
|
+
const eventWindow = targetWindow;
|
|
244
|
+
if (request.action === 'move' || request.action === 'click') {
|
|
245
|
+
const point = clientPoint(app, request.x, request.y);
|
|
246
|
+
app.canvas.dispatchEvent(pointerEvent(targetWindow, 'pointermove', point, 0));
|
|
247
|
+
if (request.action === 'click') {
|
|
248
|
+
app.canvas.dispatchEvent(pointerEvent(targetWindow, 'pointerdown', point, 1));
|
|
249
|
+
app.canvas.dispatchEvent(pointerEvent(targetWindow, 'pointerup', point, 0));
|
|
250
|
+
app.canvas.dispatchEvent(new eventWindow.MouseEvent('click', {
|
|
251
|
+
bubbles: true,
|
|
252
|
+
cancelable: true,
|
|
253
|
+
button: 0,
|
|
254
|
+
...point,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!('key' in request))
|
|
260
|
+
throw new Error('Runtime keyboard input requires a key.');
|
|
261
|
+
const identity = keyboardIdentity(request.key);
|
|
262
|
+
const dispatchKey = (type) => {
|
|
263
|
+
targetWindow.dispatchEvent(new eventWindow.KeyboardEvent(type, {
|
|
264
|
+
bubbles: true,
|
|
265
|
+
cancelable: true,
|
|
266
|
+
...identity,
|
|
267
|
+
}));
|
|
268
|
+
};
|
|
269
|
+
if (request.action === 'key' || request.action === 'keydown')
|
|
270
|
+
dispatchKey('keydown');
|
|
271
|
+
if (request.action === 'key' || request.action === 'keyup')
|
|
272
|
+
dispatchKey('keyup');
|
|
273
|
+
}
|
|
274
|
+
function cloneState(value) {
|
|
275
|
+
return JSON.parse(JSON.stringify(value));
|
|
276
|
+
}
|
|
277
|
+
function isHmrRequest(value) {
|
|
278
|
+
return typeof value === 'object'
|
|
279
|
+
&& value !== null
|
|
280
|
+
&& typeof value.requestId === 'string'
|
|
281
|
+
&& typeof value.runtimeId === 'string'
|
|
282
|
+
&& typeof value.request === 'object'
|
|
283
|
+
&& value.request !== null;
|
|
284
|
+
}
|
|
285
|
+
export function createPixifactRuntimeClient(options) {
|
|
286
|
+
const now = options.now ?? (() => new Date());
|
|
287
|
+
const logs = createRuntimeLogCapture(options.console, options.window, now);
|
|
288
|
+
let app;
|
|
289
|
+
let getState;
|
|
290
|
+
function descriptor() {
|
|
291
|
+
return {
|
|
292
|
+
runtimeId: options.runtimeId,
|
|
293
|
+
url: options.window.location.href,
|
|
294
|
+
title: options.window.document.title,
|
|
295
|
+
ready: app !== undefined,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function announce() {
|
|
299
|
+
options.hot.send(runtimeAnnounceEvent, descriptor());
|
|
300
|
+
}
|
|
301
|
+
function requireApplication() {
|
|
302
|
+
if (!app)
|
|
303
|
+
throw new Error('PixiJS Application has not been registered in this runtime.');
|
|
304
|
+
return app;
|
|
305
|
+
}
|
|
306
|
+
async function handleRequest(request) {
|
|
307
|
+
if (request.type === 'logs') {
|
|
308
|
+
return {
|
|
309
|
+
runtimeId: options.runtimeId,
|
|
310
|
+
latestSeq: logs.latestSeq(),
|
|
311
|
+
entries: logs.entries()
|
|
312
|
+
.filter((entry) => request.after === undefined || entry.seq > request.after)
|
|
313
|
+
.filter((entry) => request.level === undefined || entry.level === request.level)
|
|
314
|
+
.map((entry) => ({ ...entry })),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const currentApp = requireApplication();
|
|
318
|
+
if (request.type === 'tree') {
|
|
319
|
+
return {
|
|
320
|
+
runtimeId: options.runtimeId,
|
|
321
|
+
root: basicNodeSnapshot(currentApp.stage, null),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (request.type === 'node') {
|
|
325
|
+
const node = findStageNode(currentApp.stage, request.uid);
|
|
326
|
+
if (!node)
|
|
327
|
+
throw new Error(`PixiJS node uid ${request.uid} is not in app.stage.`);
|
|
328
|
+
return {
|
|
329
|
+
runtimeId: options.runtimeId,
|
|
330
|
+
node: detailedNodeSnapshot(node),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (request.type === 'state') {
|
|
334
|
+
return getState
|
|
335
|
+
? {
|
|
336
|
+
runtimeId: options.runtimeId,
|
|
337
|
+
available: true,
|
|
338
|
+
state: cloneState(getState()),
|
|
339
|
+
}
|
|
340
|
+
: {
|
|
341
|
+
runtimeId: options.runtimeId,
|
|
342
|
+
available: false,
|
|
343
|
+
state: null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (request.type === 'input') {
|
|
347
|
+
dispatchRuntimeInput(options.window, currentApp, request);
|
|
348
|
+
return {
|
|
349
|
+
runtimeId: options.runtimeId,
|
|
350
|
+
dispatched: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
throw new Error('Unknown Pixifact Runtime request.');
|
|
354
|
+
}
|
|
355
|
+
options.hot.on(runtimeRequestEvent, (data) => {
|
|
356
|
+
if (!isHmrRequest(data) || data.runtimeId !== options.runtimeId)
|
|
357
|
+
return;
|
|
358
|
+
void handleRequest(data.request).then((result) => options.hot.send(runtimeResponseEvent, {
|
|
359
|
+
requestId: data.requestId,
|
|
360
|
+
runtimeId: options.runtimeId,
|
|
361
|
+
ok: true,
|
|
362
|
+
result,
|
|
363
|
+
}), (error) => options.hot.send(runtimeResponseEvent, {
|
|
364
|
+
requestId: data.requestId,
|
|
365
|
+
runtimeId: options.runtimeId,
|
|
366
|
+
ok: false,
|
|
367
|
+
error: error instanceof Error ? error.message : String(error),
|
|
368
|
+
}));
|
|
369
|
+
});
|
|
370
|
+
announce();
|
|
371
|
+
return {
|
|
372
|
+
register(application, registerOptions = {}) {
|
|
373
|
+
if (app)
|
|
374
|
+
throw new Error('This page already has a registered PixiJS Application.');
|
|
375
|
+
app = application;
|
|
376
|
+
getState = registerOptions.getState;
|
|
377
|
+
announce();
|
|
378
|
+
},
|
|
379
|
+
handleRequest,
|
|
380
|
+
dispose: logs.dispose,
|
|
381
|
+
};
|
|
382
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixifact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Pixifact 2D UI 和轻量游戏 Scene 语义框架。",
|
|
5
5
|
"packageManager": "bun@1.3.13",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"import": "./dist/runtime/index.js",
|
|
43
43
|
"require": "./dist/runtime/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./runtime-dev": {
|
|
46
|
+
"types": "./dist/runtime-dev/index.d.ts",
|
|
47
|
+
"import": "./dist/runtime-dev/index.js",
|
|
48
|
+
"require": "./dist/runtime-dev/index.js"
|
|
49
|
+
},
|
|
45
50
|
"./scene": {
|
|
46
51
|
"types": "./dist/scene/index.d.ts",
|
|
47
52
|
"import": "./dist/scene/index.js",
|