pixifact 0.4.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/builtinScenes.d.ts +0 -5
- package/dist/compiler/builtinScenes.js +0 -26
- package/dist/compiler/commands/index.js +8 -5
- package/dist/compiler/compileScenes.d.ts +3 -1
- package/dist/compiler/compileScenes.js +32 -2
- package/dist/compiler/index.d.ts +0 -1
- package/dist/compiler/index.js +0 -1
- package/dist/compiler/pixiNodeSchema.d.ts +5 -3
- package/dist/compiler/pixiNodeSchema.js +61 -7
- package/dist/compiler/sceneValidation.d.ts +3 -1
- package/dist/compiler/sceneValidation.js +105 -6
- package/dist/compiler/spec.d.ts +18 -7
- package/dist/compiler/spec.js +3 -1
- package/dist/compiler/templateParser.js +19 -0
- package/dist/compiler/templateSerializer.js +7 -1
- package/dist/compiler/typescriptCompiler.js +66 -8
- package/dist/compiler-node/builtinSceneInterfaces.d.ts +6 -0
- package/dist/compiler-node/builtinSceneInterfaces.js +27 -0
- package/dist/compiler-node/index.d.ts +3 -0
- package/dist/compiler-node/index.js +3 -0
- package/dist/compiler-node/runtimeVite.d.ts +89 -0
- package/dist/compiler-node/runtimeVite.js +420 -0
- package/dist/{compiler → compiler-node}/scriptInterfaceExtractor.d.ts +1 -1
- package/dist/{compiler → compiler-node}/scriptInterfaceExtractor.js +157 -25
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +4 -1
- package/dist/runtime/BitmapLabel.d.ts +9 -0
- package/dist/runtime/BitmapLabel.js +7 -0
- package/dist/runtime/Label.d.ts +9 -0
- package/dist/runtime/Label.js +7 -0
- package/dist/runtime/ScrollContainer.d.ts +2 -0
- package/dist/runtime/ScrollContainer.js +12 -2
- package/dist/runtime/TextBoxControl.d.ts +48 -0
- package/dist/runtime/TextBoxControl.js +162 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/runtimeBehavior.d.ts +3 -0
- package/dist/runtime/runtimeBehavior.js +11 -0
- 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/dist/scene/decorators.d.ts +2 -1
- package/dist/scene/decorators.js +12 -7
- package/dist/scene/index.d.ts +1 -0
- package/dist/scene/index.js +1 -0
- package/dist/scene/sceneBindingRuntime.d.ts +10 -0
- package/dist/scene/sceneBindingRuntime.js +124 -0
- package/package.json +6 -1
|
@@ -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
|
+
}
|
|
@@ -15,6 +15,7 @@ function extractSceneScriptClasses(source, fileName, options) {
|
|
|
15
15
|
const sourceId = normalizeScriptSourceId(fileName);
|
|
16
16
|
const imports = collectNamedImports(sourceFile, sourceId);
|
|
17
17
|
const structClasses = collectStructClasses(sourceFile);
|
|
18
|
+
const variantDeclarations = collectVariantDeclarations(sourceFile);
|
|
18
19
|
const classes = [];
|
|
19
20
|
for (const statement of sourceFile.statements) {
|
|
20
21
|
if (!ts.isClassDeclaration(statement)) {
|
|
@@ -42,7 +43,8 @@ function extractSceneScriptClasses(source, fileName, options) {
|
|
|
42
43
|
}
|
|
43
44
|
const prop = memberDecoratorOptions(member, 'prop');
|
|
44
45
|
if (prop) {
|
|
45
|
-
|
|
46
|
+
assertStaticPropMember(member, name);
|
|
47
|
+
props[name] = propContract(prop, member, structClasses, variantDeclarations);
|
|
46
48
|
}
|
|
47
49
|
const event = memberDecoratorOptions(member, 'event');
|
|
48
50
|
if (event) {
|
|
@@ -318,6 +320,88 @@ function memberName(name) {
|
|
|
318
320
|
}
|
|
319
321
|
return undefined;
|
|
320
322
|
}
|
|
323
|
+
function collectVariantDeclarations(sourceFile) {
|
|
324
|
+
const variants = new Map();
|
|
325
|
+
for (const statement of sourceFile.statements) {
|
|
326
|
+
if (!ts.isVariableStatement(statement)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
330
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
334
|
+
if (!ts.isCallExpression(initializer)
|
|
335
|
+
|| !ts.isIdentifier(initializer.expression)
|
|
336
|
+
|| initializer.expression.text !== 'defineVariants') {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (initializer.arguments.length !== 1) {
|
|
340
|
+
throw new Error('defineVariants accepts exactly one argument.');
|
|
341
|
+
}
|
|
342
|
+
variants.set(declaration.name.text, variantObjectValue(initializer.arguments[0], 'defineVariants argument'));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return variants;
|
|
346
|
+
}
|
|
347
|
+
function variantObjectValue(expression, label) {
|
|
348
|
+
const object = unwrapExpression(expression);
|
|
349
|
+
if (!ts.isObjectLiteralExpression(object)) {
|
|
350
|
+
throw new Error(`${label} must be an object literal.`);
|
|
351
|
+
}
|
|
352
|
+
const variants = {};
|
|
353
|
+
let expectedFields;
|
|
354
|
+
for (const property of object.properties) {
|
|
355
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
356
|
+
throw new Error(`${label} only supports property assignments.`);
|
|
357
|
+
}
|
|
358
|
+
const name = memberName(property.name);
|
|
359
|
+
if (!name) {
|
|
360
|
+
throw new Error(`${label} only supports literal case names.`);
|
|
361
|
+
}
|
|
362
|
+
const caseExpression = unwrapExpression(property.initializer);
|
|
363
|
+
if (!ts.isObjectLiteralExpression(caseExpression)) {
|
|
364
|
+
throw new Error(`${label}.${name} must be an object literal.`);
|
|
365
|
+
}
|
|
366
|
+
const fields = {};
|
|
367
|
+
for (const field of caseExpression.properties) {
|
|
368
|
+
if (!ts.isPropertyAssignment(field)) {
|
|
369
|
+
throw new Error(`${label}.${name} only supports property assignments.`);
|
|
370
|
+
}
|
|
371
|
+
const fieldName = memberName(field.name);
|
|
372
|
+
if (!fieldName) {
|
|
373
|
+
throw new Error(`${label}.${name} only supports literal field names.`);
|
|
374
|
+
}
|
|
375
|
+
fields[fieldName] = literalValue(field.initializer, `${label}.${name}.${fieldName}`);
|
|
376
|
+
}
|
|
377
|
+
const fieldTypes = Object.fromEntries(Object.entries(fields).map(([field, value]) => [field, typeof value]));
|
|
378
|
+
if (!expectedFields) {
|
|
379
|
+
expectedFields = fieldTypes;
|
|
380
|
+
}
|
|
381
|
+
else if (!sameFieldTypes(expectedFields, fieldTypes)) {
|
|
382
|
+
throw new Error(`defineVariants case "${name}" must contain the same fields and field types as the first case.`);
|
|
383
|
+
}
|
|
384
|
+
variants[name] = fields;
|
|
385
|
+
}
|
|
386
|
+
if (Object.keys(variants).length === 0) {
|
|
387
|
+
throw new Error('defineVariants requires at least one case.');
|
|
388
|
+
}
|
|
389
|
+
return variants;
|
|
390
|
+
}
|
|
391
|
+
function sameFieldTypes(left, right) {
|
|
392
|
+
const leftEntries = Object.entries(left);
|
|
393
|
+
return leftEntries.length === Object.keys(right).length
|
|
394
|
+
&& leftEntries.every(([name, type]) => right[name] === type);
|
|
395
|
+
}
|
|
396
|
+
function unwrapExpression(expression) {
|
|
397
|
+
if (ts.isAsExpression(expression)
|
|
398
|
+
|| ts.isTypeAssertionExpression(expression)
|
|
399
|
+
|| ts.isSatisfiesExpression(expression)
|
|
400
|
+
|| ts.isParenthesizedExpression(expression)) {
|
|
401
|
+
return unwrapExpression(expression.expression);
|
|
402
|
+
}
|
|
403
|
+
return expression;
|
|
404
|
+
}
|
|
321
405
|
function collectStructClasses(sourceFile) {
|
|
322
406
|
const classes = new Map();
|
|
323
407
|
for (const statement of sourceFile.statements) {
|
|
@@ -355,65 +439,113 @@ function structFields(node) {
|
|
|
355
439
|
}
|
|
356
440
|
return fields;
|
|
357
441
|
}
|
|
358
|
-
function
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
442
|
+
function assertStaticPropMember(member, name) {
|
|
443
|
+
const isDeclareProperty = ts.isPropertyDeclaration(member)
|
|
444
|
+
&& member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword)
|
|
445
|
+
&& !member.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.AccessorKeyword)
|
|
446
|
+
&& !member.initializer;
|
|
447
|
+
if (!isDeclareProperty) {
|
|
448
|
+
throw new Error(`@prop "${name}" must decorate a declare property without an initializer.`);
|
|
449
|
+
}
|
|
450
|
+
if (!member.type) {
|
|
451
|
+
throw new Error(`@prop "${name}" requires an explicit TypeScript property type.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function propContract(prop, member, structClasses, variantDeclarations) {
|
|
455
|
+
if (!ts.isPropertyDeclaration(member) || !member.type) {
|
|
456
|
+
throw new Error('@prop requires a typed declare property.');
|
|
363
457
|
}
|
|
364
|
-
if (
|
|
365
|
-
throw new Error('@prop type
|
|
458
|
+
if (prop.type !== undefined) {
|
|
459
|
+
throw new Error('@prop type is inferred from the TypeScript property declaration; remove the type option.');
|
|
460
|
+
}
|
|
461
|
+
const defaultValue = prop.default;
|
|
462
|
+
const variantsReference = prop.variants;
|
|
463
|
+
if (variantsReference !== undefined) {
|
|
464
|
+
if (!isIdentifierValue(variantsReference)) {
|
|
465
|
+
throw new Error('@prop variants must reference a local defineVariants declaration.');
|
|
466
|
+
}
|
|
467
|
+
const variants = variantDeclarations.get(variantsReference.text);
|
|
468
|
+
if (!variants) {
|
|
469
|
+
throw new Error(`@prop variants "${variantsReference.text}" must reference a local defineVariants declaration.`);
|
|
470
|
+
}
|
|
471
|
+
if (typeof defaultValue !== 'string' || !variants[defaultValue]) {
|
|
472
|
+
throw new Error('@prop Variant default must name an existing defineVariants case.');
|
|
473
|
+
}
|
|
474
|
+
assertVariantPropertyType(member.type, variantsReference.text);
|
|
475
|
+
return {
|
|
476
|
+
type: 'variant',
|
|
477
|
+
default: defaultValue,
|
|
478
|
+
variants,
|
|
479
|
+
};
|
|
366
480
|
}
|
|
367
|
-
const primitiveType =
|
|
481
|
+
const primitiveType = primitivePropertyType(member.type);
|
|
368
482
|
if (primitiveType) {
|
|
369
483
|
if (defaultValue !== undefined && (!isSceneTemplateScalarValue(defaultValue) || sceneTemplateScalarType(defaultValue) !== primitiveType)) {
|
|
370
|
-
throw new Error(`@prop default
|
|
484
|
+
throw new Error(`@prop default must be a ${primitiveType} literal.`);
|
|
371
485
|
}
|
|
372
486
|
return {
|
|
373
487
|
type: primitiveType,
|
|
374
488
|
...(defaultValue !== undefined ? { default: defaultValue } : {}),
|
|
375
489
|
};
|
|
376
490
|
}
|
|
377
|
-
const
|
|
491
|
+
const structName = typeReferenceName(member.type);
|
|
492
|
+
if (!structName) {
|
|
493
|
+
throw new Error('@prop type must be string, number, boolean, a local struct class, or keyof a local defineVariants declaration.');
|
|
494
|
+
}
|
|
495
|
+
const structClass = structClasses.get(structName);
|
|
378
496
|
if (!structClass) {
|
|
379
|
-
throw new Error(`Struct prop type ${
|
|
497
|
+
throw new Error(`Struct prop type ${structName} was not found.`);
|
|
380
498
|
}
|
|
381
499
|
if (!structClass.exported) {
|
|
382
|
-
throw new Error(`Struct prop type ${
|
|
500
|
+
throw new Error(`Struct prop type ${structName} must be exported.`);
|
|
383
501
|
}
|
|
384
502
|
if (defaultValue !== undefined) {
|
|
385
503
|
throw new Error('@prop default is only supported for primitive props.');
|
|
386
504
|
}
|
|
387
505
|
const struct = structContractInfo(structClass.node);
|
|
388
506
|
if (struct.hasRequiredConstructorParameters) {
|
|
389
|
-
throw new Error(`Struct prop type ${
|
|
507
|
+
throw new Error(`Struct prop type ${structName} must be constructable with no required parameters.`);
|
|
390
508
|
}
|
|
391
509
|
return {
|
|
392
510
|
type: 'struct',
|
|
393
|
-
struct:
|
|
511
|
+
struct: structName,
|
|
394
512
|
fields: struct.fields,
|
|
395
513
|
};
|
|
396
514
|
}
|
|
397
|
-
function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
515
|
+
function assertVariantPropertyType(type, variantsName) {
|
|
516
|
+
if (!ts.isTypeOperatorNode(type)
|
|
517
|
+
|| type.operator !== ts.SyntaxKind.KeyOfKeyword
|
|
518
|
+
|| !ts.isTypeQueryNode(type.type)
|
|
519
|
+
|| !ts.isIdentifier(type.type.exprName)
|
|
520
|
+
|| type.type.exprName.text !== variantsName) {
|
|
521
|
+
throw new Error(`@prop Variant type must be "keyof typeof ${variantsName}".`);
|
|
522
|
+
}
|
|
402
523
|
}
|
|
403
|
-
function
|
|
404
|
-
if (
|
|
524
|
+
function primitivePropertyType(type) {
|
|
525
|
+
if (type.kind === ts.SyntaxKind.StringKeyword) {
|
|
405
526
|
return 'string';
|
|
406
527
|
}
|
|
407
|
-
if (
|
|
528
|
+
if (type.kind === ts.SyntaxKind.NumberKeyword) {
|
|
408
529
|
return 'number';
|
|
409
530
|
}
|
|
410
|
-
if (
|
|
531
|
+
if (type.kind === ts.SyntaxKind.BooleanKeyword) {
|
|
411
532
|
return 'boolean';
|
|
412
533
|
}
|
|
413
534
|
return undefined;
|
|
414
535
|
}
|
|
536
|
+
function typeReferenceName(type) {
|
|
537
|
+
return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)
|
|
538
|
+
? type.typeName.text
|
|
539
|
+
: undefined;
|
|
540
|
+
}
|
|
541
|
+
function structContractInfo(node) {
|
|
542
|
+
return {
|
|
543
|
+
fields: structFields(node),
|
|
544
|
+
hasRequiredConstructorParameters: node.members.some((member) => ts.isConstructorDeclaration(member) && member.parameters.some((parameter) => !parameter.questionToken && !parameter.initializer)),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
415
547
|
function isIdentifierValue(value) {
|
|
416
|
-
return ts.isIdentifier(value);
|
|
548
|
+
return typeof value === 'object' && ts.isIdentifier(value);
|
|
417
549
|
}
|
|
418
550
|
function isSceneTemplateScalarValue(value) {
|
|
419
551
|
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
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;
|