@kubohiroya/turbowarp-runtime-adapter 0.2.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/index.js ADDED
@@ -0,0 +1,500 @@
1
+ const broadcastHatOpcode = 'event_whenbroadcastreceived';
2
+ const broadcastMessageType = 'broadcast_msg';
3
+ const afterExecuteEvent = 'AFTER_EXECUTE';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function prefixedCode(prefix, suffix) {
8
+ if (!/^[A-Z][A-Z0-9]*$/u.test(prefix)) {
9
+ throw new TypeError('errorCodePrefix must be an uppercase diagnostic prefix.');
10
+ }
11
+ return `${prefix}-${suffix}`;
12
+ }
13
+ function hostError(code, message, cause) {
14
+ const error = new Error(message);
15
+ Object.defineProperty(error, 'code', { value: code });
16
+ if (cause !== undefined)
17
+ Object.defineProperty(error, 'cause', { value: cause });
18
+ return error;
19
+ }
20
+ function cancelledError(prefix, message) {
21
+ const error = hostError(prefixedCode(prefix, 'BROADCAST-CANCELLED'), message);
22
+ error.name = 'AbortError';
23
+ return error;
24
+ }
25
+ function removeRuntimeListener(runtime, event, listener) {
26
+ if (typeof runtime.off === 'function') {
27
+ runtime.off(event, listener);
28
+ return;
29
+ }
30
+ if (typeof runtime.removeListener === 'function') {
31
+ runtime.removeListener(event, listener);
32
+ return;
33
+ }
34
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime must provide off or removeListener to dispose listeners.');
35
+ }
36
+ function validateRuntime(value) {
37
+ if (!isRecord(value)) {
38
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime must be an object.');
39
+ }
40
+ if (typeof value['on'] !== 'function') {
41
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime must provide on.');
42
+ }
43
+ if (typeof value['startHats'] !== 'function') {
44
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime must provide startHats.');
45
+ }
46
+ return value;
47
+ }
48
+ function validateAbortSignal(value, prefix = 'TWRH') {
49
+ if (!isRecord(value) ||
50
+ typeof value['aborted'] !== 'boolean' ||
51
+ typeof value['addEventListener'] !== 'function' ||
52
+ typeof value['removeEventListener'] !== 'function') {
53
+ throw hostError(prefixedCode(prefix, 'BROADCAST-CONTEXT-001'), 'broadcastMessageAndWait context must provide an AbortSignal.');
54
+ }
55
+ return value;
56
+ }
57
+ function requireMethod(runtime, method) {
58
+ const operation = runtime[method];
59
+ if (typeof operation !== 'function') {
60
+ throw hostError('TWRH-RUNTIME-001', `TurboWarp runtime must provide ${String(method)}.`);
61
+ }
62
+ return operation;
63
+ }
64
+ export function createTurboWarpRuntimeHost(options = {}) {
65
+ if (options.requireUnsandboxed === true && options.Scratch?.extensions?.unsandboxed !== true) {
66
+ throw hostError('TWRH-SCRATCH-001', 'TurboWarp runtime host requires unsandboxed Scratch.');
67
+ }
68
+ const runtime = validateRuntime(options.runtime ?? options.Scratch?.vm?.runtime);
69
+ return Object.freeze({
70
+ runtime,
71
+ onRuntimeEvent(event, listener) {
72
+ runtime.on(event, listener);
73
+ return () => removeRuntimeListener(runtime, event, listener);
74
+ },
75
+ startHats(opcode, matchFields) {
76
+ const threads = runtime.startHats(opcode, matchFields);
77
+ if (threads === undefined)
78
+ return [];
79
+ if (!Array.isArray(threads)) {
80
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp startHats must return an array or undefined.');
81
+ }
82
+ return threads;
83
+ },
84
+ stopThread(thread) {
85
+ requireMethod(runtime, '_stopThread').call(runtime, thread);
86
+ },
87
+ getStageTarget() {
88
+ return requireMethod(runtime, 'getTargetForStage').call(runtime);
89
+ },
90
+ currentThreads() {
91
+ if (runtime.threads === undefined)
92
+ return [];
93
+ if (!Array.isArray(runtime.threads)) {
94
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime threads must be an array.');
95
+ }
96
+ return [...runtime.threads];
97
+ },
98
+ targets() {
99
+ return resolveTargets(runtime);
100
+ },
101
+ spriteTargets() {
102
+ return resolveTargets(runtime).filter((target) => !(isRecord(target) && target['isStage'] === true));
103
+ },
104
+ getRenderer() {
105
+ const renderer = runtime.renderer;
106
+ if (!isRecord(renderer)) {
107
+ throw hostError('TWRH-RENDERER-001', 'TurboWarp runtime has no attached renderer.');
108
+ }
109
+ return renderer;
110
+ },
111
+ requestRedraw() {
112
+ // A runtime without a renderer redraws on its own tick, so this stays a no-op rather than
113
+ // forcing every caller to feature-test before asking for a frame.
114
+ if (typeof runtime.requestRedraw === 'function')
115
+ runtime.requestRedraw();
116
+ },
117
+ getMonitorBlocks() {
118
+ return resolveMonitorBlocks(runtime);
119
+ },
120
+ getMonitorState() {
121
+ return resolveMonitorState(runtime);
122
+ }
123
+ });
124
+ }
125
+ function resolveTargets(runtime) {
126
+ if (runtime.targets === undefined)
127
+ return [];
128
+ if (!Array.isArray(runtime.targets)) {
129
+ throw hostError('TWRH-RUNTIME-001', 'TurboWarp runtime targets must be an array.');
130
+ }
131
+ return [...runtime.targets];
132
+ }
133
+ function resolveMonitorBlocks(runtime) {
134
+ const monitorBlocks = runtime.monitorBlocks;
135
+ if (!isRecord(monitorBlocks) ||
136
+ typeof monitorBlocks['getBlock'] !== 'function' ||
137
+ typeof monitorBlocks['getScripts'] !== 'function' ||
138
+ typeof monitorBlocks['changeBlock'] !== 'function') {
139
+ throw hostError('TWRH-MONITOR-001', 'TurboWarp monitor blocks are unavailable.');
140
+ }
141
+ return monitorBlocks;
142
+ }
143
+ function resolveMonitorState(runtime) {
144
+ const getMonitorState = runtime.getMonitorState;
145
+ if (typeof getMonitorState !== 'function') {
146
+ throw hostError('TWRH-MONITOR-001', 'TurboWarp runtime must provide getMonitorState.');
147
+ }
148
+ const monitorState = getMonitorState.call(runtime);
149
+ if (!isRecord(monitorState) ||
150
+ typeof monitorState['has'] !== 'function' ||
151
+ typeof monitorState['get'] !== 'function' ||
152
+ typeof monitorState['valueSeq'] !== 'function') {
153
+ throw hostError('TWRH-MONITOR-001', 'TurboWarp monitor state is unavailable.');
154
+ }
155
+ return monitorState;
156
+ }
157
+ function validateBroadcastRuntime(runtime, prefix) {
158
+ try {
159
+ requireMethod(runtime, 'getTargetForStage');
160
+ requireMethod(runtime, '_stopThread');
161
+ }
162
+ catch {
163
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp runtime is missing required broadcast methods.');
164
+ }
165
+ if (!Array.isArray(runtime.threads)) {
166
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp runtime threads must be an array.');
167
+ }
168
+ if (typeof runtime.off !== 'function' && typeof runtime.removeListener !== 'function') {
169
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp runtime must provide off or removeListener.');
170
+ }
171
+ return runtime;
172
+ }
173
+ function resolveExactBroadcast(runtime, message, prefix) {
174
+ let stage;
175
+ try {
176
+ stage = requireMethod(runtime, 'getTargetForStage').call(runtime);
177
+ }
178
+ catch (cause) {
179
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp Stage broadcasts could not be read.', cause);
180
+ }
181
+ if (!isRecord(stage) || !isRecord(stage['variables'])) {
182
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp Stage broadcasts are unavailable.');
183
+ }
184
+ const declared = Object.values(stage['variables']).find((variable) => isRecord(variable) &&
185
+ variable['type'] === broadcastMessageType &&
186
+ variable['name'] === message);
187
+ return declared ? message : null;
188
+ }
189
+ function validateBroadcastPayload(payload, prefix) {
190
+ if (!isRecord(payload) ||
191
+ Object.keys(payload).length !== 1 ||
192
+ typeof payload['message'] !== 'string' ||
193
+ payload['message'].length === 0) {
194
+ throw hostError(prefixedCode(prefix, 'BROADCAST-PAYLOAD-001'), 'broadcastMessageAndWait payload must contain one non-empty message string.');
195
+ }
196
+ return payload['message'];
197
+ }
198
+ export function createTurboWarpBroadcastPort(options) {
199
+ if (!isRecord(options)) {
200
+ throw hostError('TWRH-BROADCAST-RUNTIME-001', 'TurboWarp broadcast port options must be an object.');
201
+ }
202
+ const prefix = options.errorCodePrefix ?? 'TWRH';
203
+ let baseRuntime;
204
+ try {
205
+ baseRuntime = validateRuntime(options.runtime);
206
+ }
207
+ catch (cause) {
208
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp broadcast port requires a valid runtime object.', cause);
209
+ }
210
+ const runtime = validateBroadcastRuntime(baseRuntime, prefix);
211
+ const activeInvocations = new Set();
212
+ let disposed = false;
213
+ function threadIsActive(thread) {
214
+ return runtime.threads?.includes(thread) === true;
215
+ }
216
+ function stopOwnedThreads(threads, reason) {
217
+ const errors = [];
218
+ for (const thread of threads) {
219
+ if (!threadIsActive(thread))
220
+ continue;
221
+ try {
222
+ requireMethod(runtime, '_stopThread').call(runtime, thread);
223
+ }
224
+ catch (error) {
225
+ errors.push(error);
226
+ }
227
+ }
228
+ if (errors.length > 0) {
229
+ throw hostError(prefixedCode(prefix, 'BROADCAST-CLEANUP-001'), `TurboWarp broadcast receiver cleanup failed: ${reason}`, errors.length === 1 ? errors[0] : new AggregateError(errors));
230
+ }
231
+ }
232
+ return Object.freeze({
233
+ broadcastMessageAndWait(payload, context) {
234
+ if (disposed) {
235
+ return Promise.reject(hostError(prefixedCode(prefix, 'BROADCAST-DISPOSED'), 'TurboWarp broadcast port is disposed.'));
236
+ }
237
+ const message = validateBroadcastPayload(payload, prefix);
238
+ const signal = validateAbortSignal(context?.signal, prefix);
239
+ if (signal.aborted) {
240
+ return Promise.reject(cancelledError(prefix, 'broadcastMessageAndWait was cancelled.'));
241
+ }
242
+ const broadcastName = resolveExactBroadcast(runtime, message, prefix);
243
+ if (broadcastName === null)
244
+ return Promise.resolve();
245
+ let started;
246
+ try {
247
+ started = runtime.startHats(broadcastHatOpcode, { BROADCAST_OPTION: broadcastName });
248
+ }
249
+ catch (cause) {
250
+ throw hostError(prefixedCode(prefix, 'BROADCAST-START-001'), 'TurboWarp broadcast receiver threads could not be started.', cause);
251
+ }
252
+ const threads = started ?? [];
253
+ if (!Array.isArray(threads)) {
254
+ throw hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp startHats must return an array or undefined.');
255
+ }
256
+ if (threads.length === 0)
257
+ return Promise.resolve();
258
+ return new Promise((resolve, reject) => {
259
+ let settled = false;
260
+ let runtimeListenerAttached = false;
261
+ let abortListenerAttached = false;
262
+ const invocation = {
263
+ cancel(messageText) {
264
+ settleCancelled(messageText);
265
+ }
266
+ };
267
+ const cleanup = (reason) => {
268
+ const errors = [];
269
+ if (runtimeListenerAttached) {
270
+ runtimeListenerAttached = false;
271
+ try {
272
+ removeRuntimeListener(runtime, afterExecuteEvent, handleAfterExecute);
273
+ }
274
+ catch (error) {
275
+ errors.push(error);
276
+ }
277
+ }
278
+ if (abortListenerAttached) {
279
+ abortListenerAttached = false;
280
+ signal.removeEventListener('abort', handleAbort);
281
+ }
282
+ activeInvocations.delete(invocation);
283
+ try {
284
+ stopOwnedThreads(threads, reason);
285
+ }
286
+ catch (error) {
287
+ errors.push(error);
288
+ }
289
+ if (errors.length > 0) {
290
+ throw hostError(prefixedCode(prefix, 'BROADCAST-CLEANUP-001'), `TurboWarp broadcast cleanup failed: ${reason}`, errors.length === 1 ? errors[0] : new AggregateError(errors));
291
+ }
292
+ };
293
+ const settle = (operation, reason) => {
294
+ if (settled)
295
+ return;
296
+ settled = true;
297
+ try {
298
+ cleanup(reason);
299
+ operation();
300
+ }
301
+ catch (error) {
302
+ reject(error);
303
+ }
304
+ };
305
+ const settleCancelled = (messageText) => {
306
+ settle(() => reject(cancelledError(prefix, messageText)), 'cancelled');
307
+ };
308
+ const handleAbort = () => {
309
+ settleCancelled('broadcastMessageAndWait was cancelled.');
310
+ };
311
+ const handleAfterExecute = () => {
312
+ if (threads.every((thread) => !threadIsActive(thread))) {
313
+ settle(resolve, 'completed');
314
+ }
315
+ };
316
+ activeInvocations.add(invocation);
317
+ try {
318
+ runtime.on(afterExecuteEvent, handleAfterExecute);
319
+ runtimeListenerAttached = true;
320
+ signal.addEventListener('abort', handleAbort, { once: true });
321
+ abortListenerAttached = true;
322
+ }
323
+ catch (cause) {
324
+ settled = true;
325
+ const errors = [
326
+ hostError(prefixedCode(prefix, 'BROADCAST-RUNTIME-001'), 'TurboWarp broadcast receiver completion could not be observed.', cause)
327
+ ];
328
+ try {
329
+ cleanup('observer setup failure');
330
+ }
331
+ catch (error) {
332
+ errors.push(error);
333
+ }
334
+ reject(errors.length === 1
335
+ ? errors[0]
336
+ : hostError(prefixedCode(prefix, 'BROADCAST-CLEANUP-001'), 'TurboWarp broadcast observer setup cleanup failed.', new AggregateError(errors)));
337
+ return;
338
+ }
339
+ if (signal.aborted)
340
+ handleAbort();
341
+ else
342
+ handleAfterExecute();
343
+ });
344
+ },
345
+ dispose() {
346
+ if (disposed)
347
+ return;
348
+ disposed = true;
349
+ for (const invocation of [...activeInvocations]) {
350
+ invocation.cancel('TurboWarp broadcast port was disposed.');
351
+ }
352
+ }
353
+ });
354
+ }
355
+ function requireBlockType(vocabulary, name) {
356
+ const value = vocabulary.BlockType[name];
357
+ if (typeof value !== 'string') {
358
+ throw hostError('TWRH-BLOCK-001', `Scratch.BlockType.${name} is required.`);
359
+ }
360
+ return value;
361
+ }
362
+ function requireArgumentType(vocabulary, name) {
363
+ const value = vocabulary.ArgumentType[name];
364
+ if (typeof value !== 'string') {
365
+ throw hostError('TWRH-BLOCK-001', `Scratch.ArgumentType.${name} is required.`);
366
+ }
367
+ return value;
368
+ }
369
+ function validateBlockSpec(spec) {
370
+ if (!isRecord(spec) || typeof spec.opcode !== 'string' || spec.opcode.length === 0) {
371
+ throw hostError('TWRH-BLOCK-002', 'A block spec must provide a non-empty opcode.');
372
+ }
373
+ if (typeof spec.text !== 'string' || spec.text.length === 0) {
374
+ throw hostError('TWRH-BLOCK-002', `Block ${spec.opcode} must provide non-empty text.`);
375
+ }
376
+ return spec;
377
+ }
378
+ /**
379
+ * Build the repetitive parts of an extension's `getInfo()` block list.
380
+ *
381
+ * The builder owns record shape and palette visibility only. Opcodes, block text, menu items, and
382
+ * every other app-specific decision stay with the caller, so nothing about a concrete app leaks
383
+ * into this package.
384
+ */
385
+ export function createBlockSurfaceBuilder(vocabulary, options = {}) {
386
+ if (!isRecord(vocabulary) ||
387
+ !isRecord(vocabulary.ArgumentType) ||
388
+ !isRecord(vocabulary.BlockType)) {
389
+ throw hostError('TWRH-BLOCK-001', 'Block surface requires Scratch ArgumentType and BlockType records.');
390
+ }
391
+ if (options.visible !== undefined && typeof options.visible !== 'boolean') {
392
+ throw hostError('TWRH-BLOCK-001', 'Block surface visible must be boolean.');
393
+ }
394
+ const defaultVisible = options.visible ?? true;
395
+ function build(spec, blockTypeName, monitorable) {
396
+ validateBlockSpec(spec);
397
+ const visible = spec.visible ?? defaultVisible;
398
+ const record = {
399
+ opcode: spec.opcode,
400
+ blockType: requireBlockType(vocabulary, blockTypeName),
401
+ text: spec.text,
402
+ hideFromPalette: !visible
403
+ };
404
+ if (spec.arguments !== undefined)
405
+ record.arguments = Object.freeze({ ...spec.arguments });
406
+ if (monitorable && (spec.monitor ?? false) === false)
407
+ record.disableMonitor = true;
408
+ return Object.freeze(record);
409
+ }
410
+ return Object.freeze({
411
+ reporter(spec) {
412
+ return build(spec, 'REPORTER', true);
413
+ },
414
+ boolean(spec) {
415
+ return build(spec, 'BOOLEAN', false);
416
+ },
417
+ command(spec) {
418
+ return build(spec, 'COMMAND', false);
419
+ },
420
+ hat(spec) {
421
+ return build(spec, 'HAT', false);
422
+ },
423
+ reporters(entries, reporterOptions = {}) {
424
+ if (!Array.isArray(entries)) {
425
+ throw hostError('TWRH-BLOCK-002', 'Block reporters must be an array of entries.');
426
+ }
427
+ return Object.freeze(entries.map(([opcode, text]) => build({
428
+ opcode,
429
+ text,
430
+ ...(reporterOptions.visible === undefined ? {} : { visible: reporterOptions.visible }),
431
+ ...(reporterOptions.monitor === undefined ? {} : { monitor: reporterOptions.monitor })
432
+ }, 'REPORTER', true)));
433
+ },
434
+ stringArgument(defaultValue = '') {
435
+ return Object.freeze({ type: requireArgumentType(vocabulary, 'STRING'), defaultValue });
436
+ },
437
+ numberArgument(defaultValue = 0) {
438
+ return Object.freeze({ type: requireArgumentType(vocabulary, 'NUMBER'), defaultValue });
439
+ },
440
+ menuArgument(menu) {
441
+ if (typeof menu !== 'string' || menu.length === 0) {
442
+ throw hostError('TWRH-BLOCK-002', 'A menu argument must name a non-empty menu.');
443
+ }
444
+ return Object.freeze({ type: requireArgumentType(vocabulary, 'STRING'), menu });
445
+ },
446
+ menu(items, menuOptions = {}) {
447
+ if (!Array.isArray(items) || items.length === 0) {
448
+ throw hostError('TWRH-BLOCK-002', 'A block menu must list at least one item.');
449
+ }
450
+ return Object.freeze({
451
+ acceptReporters: menuOptions.acceptReporters ?? false,
452
+ items: Object.freeze([...items])
453
+ });
454
+ },
455
+ surface(blocks, menus = {}) {
456
+ if (!Array.isArray(blocks)) {
457
+ throw hostError('TWRH-BLOCK-002', 'A block surface must provide an array of blocks.');
458
+ }
459
+ const opcodes = new Set();
460
+ for (const block of blocks) {
461
+ if (opcodes.has(block.opcode)) {
462
+ throw hostError('TWRH-BLOCK-002', `Duplicate block opcode: ${block.opcode}`);
463
+ }
464
+ opcodes.add(block.opcode);
465
+ }
466
+ return Object.freeze({
467
+ blocks: Object.freeze([...blocks]),
468
+ menus: Object.freeze({ ...menus })
469
+ });
470
+ }
471
+ });
472
+ }
473
+ /**
474
+ * Coerce one Scratch block argument to a scalar of the named type.
475
+ *
476
+ * Scratch hands every argument over as a string, so a writable scalar surface has to decide what
477
+ * counts as a number or a boolean. That decision is the same in every app, while the diagnostic
478
+ * codes are not, so the prefix is injectable the way {@link createTurboWarpBroadcastPort} does it.
479
+ */
480
+ export function coerceScalarBlockValue(value, type, options = {}) {
481
+ const prefix = options.errorCodePrefix ?? 'TWRH';
482
+ if (type === 'string')
483
+ return Object.freeze({ ok: true, value: String(value ?? '') });
484
+ if (type === 'number') {
485
+ const number = typeof value === 'number' ? value : Number(value);
486
+ return Number.isFinite(number)
487
+ ? Object.freeze({ ok: true, value: number })
488
+ : Object.freeze({ ok: false, code: prefixedCode(prefix, 'VARIABLE-WRITE-VALUE') });
489
+ }
490
+ if (type === 'boolean') {
491
+ if (typeof value === 'boolean')
492
+ return Object.freeze({ ok: true, value });
493
+ if (value === 'true')
494
+ return Object.freeze({ ok: true, value: true });
495
+ if (value === 'false')
496
+ return Object.freeze({ ok: true, value: false });
497
+ return Object.freeze({ ok: false, code: prefixedCode(prefix, 'VARIABLE-WRITE-VALUE') });
498
+ }
499
+ return Object.freeze({ ok: false, code: prefixedCode(prefix, 'VARIABLE-WRITE-TYPE') });
500
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@kubohiroya/turbowarp-runtime-adapter",
3
+ "version": "0.2.0",
4
+ "description": "App-neutral adapters for interacting with the TurboWarp runtime.",
5
+ "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
6
+ "license": "MPL-2.0",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "homepage": "https://github.com/kubohiroya/turbowarp-runtime-adapter#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/kubohiroya/turbowarp-runtime-adapter.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/kubohiroya/turbowarp-runtime-adapter/issues"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist/",
28
+ "src/",
29
+ "README.md",
30
+ "README.ja.md",
31
+ "CHANGELOG.md",
32
+ "LICENSE"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "packageManager": "pnpm@11.11.0",
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "build": "tsc --project tsconfig.build.json",
46
+ "pack:check": "pnpm pack --dry-run",
47
+ "release:check": "npm publish --dry-run --access public",
48
+ "check": "pnpm run typecheck && pnpm run test && pnpm run build && pnpm run pack:check"
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.10"
53
+ },
54
+ "keywords": [
55
+ "turbowarp",
56
+ "scratch",
57
+ "runtime",
58
+ "adapter"
59
+ ]
60
+ }