@geastack/core 0.1.41 → 0.1.43
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/README.md +36 -9
- package/events.cpp +11 -0
- package/gea_app_entry.cpp +4 -0
- package/gea_sources.mjs +12 -2
- package/gea_sources.sh +6 -1
- package/include/canvas.h +1 -0
- package/include/display.h +1 -0
- package/include/event_listener_options.h +26 -0
- package/include/events.h +109 -5
- package/include/gea/embedded.h +1 -0
- package/include/graphics/font.h +11 -0
- package/include/native_identity.h +65 -0
- package/include/platform/asset_bundle.h +71 -0
- package/include/platform/esp_now.h +113 -0
- package/include/platform/magnetometer.h +60 -0
- package/include/platform/uart.h +58 -0
- package/include/runtime.h +14 -0
- package/index.d.ts +183 -8
- package/package.json +5 -2
- package/runtime/host.ts +95 -0
- package/runtime-surface.ts +7 -0
- package/runtime.cpp +145 -16
- package/scripts/build-gea-vite-geatsc.mjs +27 -3
- package/scripts/gea-file-asset-bundle.mjs +82 -0
- package/scripts/gea-native-style-plugin.mjs +3 -1
- package/scripts/gea-vite-plugin-resolution.mjs +49 -0
- package/scripts/generate-gea-embedded-assets.mjs +20 -3
- package/scripts/generate-gea-embedded-fonts.mjs +25 -4
- package/scripts/tests/gea-vite-plugin-resolution.test.mjs +46 -0
package/runtime/host.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// below. lib.dom's GeolocationPositionError also carries PERMISSION_DENIED /
|
|
8
8
|
// POSITION_UNAVAILABLE / TIMEOUT, which our host object never sets.
|
|
9
9
|
import type {
|
|
10
|
+
UartOptions,
|
|
11
|
+
MagnetometerReading,
|
|
10
12
|
CanvasRenderingContext2D,
|
|
11
13
|
GeolocationCoordinates,
|
|
12
14
|
GeolocationPosition,
|
|
@@ -354,6 +356,32 @@ declare const __gea_Gpio: {
|
|
|
354
356
|
write(pin: number, level: boolean): boolean
|
|
355
357
|
read(pin: number): boolean
|
|
356
358
|
}
|
|
359
|
+
declare const __gea_Magnetometer: {
|
|
360
|
+
open(): boolean
|
|
361
|
+
close(): void
|
|
362
|
+
count(): number
|
|
363
|
+
read(index: number, values: Float64Array): boolean
|
|
364
|
+
}
|
|
365
|
+
declare const __gea_Uart: {
|
|
366
|
+
open(port: number, tx: number, rx: number, baud: number, rxBytes: number, txBytes: number): boolean
|
|
367
|
+
isOpen(port: number): boolean
|
|
368
|
+
close(port: number): void
|
|
369
|
+
read(port: number, buffer: Uint8Array): number
|
|
370
|
+
write(port: number, buffer: Uint8Array): number
|
|
371
|
+
available(port: number): number
|
|
372
|
+
writable(port: number): number
|
|
373
|
+
}
|
|
374
|
+
declare const __gea_EspNow: {
|
|
375
|
+
open(channel: number, rate: number): boolean
|
|
376
|
+
setMaxTxPower(quarterDbm: number): boolean
|
|
377
|
+
getMaxTxPower(): number
|
|
378
|
+
isOpen(): boolean
|
|
379
|
+
canSend(): boolean
|
|
380
|
+
close(): void
|
|
381
|
+
addPeer(address: Uint8Array, key: Uint8Array): boolean
|
|
382
|
+
send(address: Uint8Array, bytes: Uint8Array): number
|
|
383
|
+
receive(source: Uint8Array, bytes: Uint8Array): number
|
|
384
|
+
}
|
|
357
385
|
declare const __gea_Led: {
|
|
358
386
|
// A WS2812/NeoPixel strand on one pin — what "the onboard LED" is on most
|
|
359
387
|
// modern ESP32 modules. set() is the one-call default (attach if needed,
|
|
@@ -1031,6 +1059,73 @@ export const Gpio = {
|
|
|
1031
1059
|
return __gea_Gpio.read(pin)
|
|
1032
1060
|
},
|
|
1033
1061
|
}
|
|
1062
|
+
export const Uart = {
|
|
1063
|
+
open(port: number, options: UartOptions): boolean {
|
|
1064
|
+
if (typeof __gea_Uart === 'undefined') return false
|
|
1065
|
+
return __gea_Uart.open(port, options.tx, options.rx, options.baudRate,
|
|
1066
|
+
options.rxBufferSize ?? 4096, options.txBufferSize ?? 4096)
|
|
1067
|
+
},
|
|
1068
|
+
isOpen(port: number): boolean {
|
|
1069
|
+
return typeof __gea_Uart !== 'undefined' && __gea_Uart.isOpen(port)
|
|
1070
|
+
},
|
|
1071
|
+
close(port: number): void {
|
|
1072
|
+
if (typeof __gea_Uart !== 'undefined') __gea_Uart.close(port)
|
|
1073
|
+
},
|
|
1074
|
+
read(port: number, buffer: Uint8Array): number {
|
|
1075
|
+
return typeof __gea_Uart !== 'undefined' ? __gea_Uart.read(port, buffer) : -1
|
|
1076
|
+
},
|
|
1077
|
+
write(port: number, buffer: Uint8Array): number {
|
|
1078
|
+
return typeof __gea_Uart !== 'undefined' ? __gea_Uart.write(port, buffer) : -1
|
|
1079
|
+
},
|
|
1080
|
+
available(port: number): number {
|
|
1081
|
+
return typeof __gea_Uart !== 'undefined' ? __gea_Uart.available(port) : -1
|
|
1082
|
+
},
|
|
1083
|
+
writable(port: number): number {
|
|
1084
|
+
return typeof __gea_Uart !== 'undefined' ? __gea_Uart.writable(port) : -1
|
|
1085
|
+
},
|
|
1086
|
+
}
|
|
1087
|
+
export const Magnetometer = {
|
|
1088
|
+
open(): boolean {
|
|
1089
|
+
return typeof __gea_Magnetometer !== 'undefined' && __gea_Magnetometer.open()
|
|
1090
|
+
},
|
|
1091
|
+
close(): void {
|
|
1092
|
+
if (typeof __gea_Magnetometer !== 'undefined') __gea_Magnetometer.close()
|
|
1093
|
+
},
|
|
1094
|
+
count(): number {
|
|
1095
|
+
return typeof __gea_Magnetometer !== 'undefined' ? __gea_Magnetometer.count() : 0
|
|
1096
|
+
},
|
|
1097
|
+
read(index: number): MagnetometerReading {
|
|
1098
|
+
const values = new Float64Array(5)
|
|
1099
|
+
const valid = typeof __gea_Magnetometer !== 'undefined' && __gea_Magnetometer.read(index, values)
|
|
1100
|
+
return { x: values[0], y: values[1], z: values[2], valid, saturatedAxes: values[3], timestampUs: values[4] }
|
|
1101
|
+
},
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export const EspNow = {
|
|
1105
|
+
// 115 selects S31 HE20 MCS9 short GI (114.7 Mbps); target validation owns
|
|
1106
|
+
// availability. Other rates and the default retain their existing meaning.
|
|
1107
|
+
open(channel: number, rateMbps: number = 54): boolean {
|
|
1108
|
+
return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.open(channel, rateMbps)
|
|
1109
|
+
},
|
|
1110
|
+
setMaxTxPower(quarterDbm: number): boolean {
|
|
1111
|
+
return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.setMaxTxPower(quarterDbm)
|
|
1112
|
+
},
|
|
1113
|
+
getMaxTxPower(): number {
|
|
1114
|
+
return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.getMaxTxPower() : -1
|
|
1115
|
+
},
|
|
1116
|
+
isOpen(): boolean { return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.isOpen() },
|
|
1117
|
+
canSend(): boolean { return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.canSend() },
|
|
1118
|
+
close(): void { if (typeof __gea_EspNow !== 'undefined') __gea_EspNow.close() },
|
|
1119
|
+
addPeer(address: Uint8Array, key: Uint8Array = new Uint8Array(0)): boolean {
|
|
1120
|
+
return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.addPeer(address, key)
|
|
1121
|
+
},
|
|
1122
|
+
send(address: Uint8Array, bytes: Uint8Array): number {
|
|
1123
|
+
return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.send(address, bytes) : -1
|
|
1124
|
+
},
|
|
1125
|
+
receive(source: Uint8Array, bytes: Uint8Array): number {
|
|
1126
|
+
return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.receive(source, bytes) : -1
|
|
1127
|
+
},
|
|
1128
|
+
}
|
|
1034
1129
|
export const Led = {
|
|
1035
1130
|
set(pin: number, r: number, g: number, b: number): boolean {
|
|
1036
1131
|
return __gea_Led.set(pin, r, g, b)
|
package/runtime-surface.ts
CHANGED
|
@@ -57,8 +57,13 @@ export type {
|
|
|
57
57
|
* gets today.
|
|
58
58
|
*/
|
|
59
59
|
export type {
|
|
60
|
+
UartOptions,
|
|
61
|
+
UartController,
|
|
62
|
+
EspNowController,
|
|
60
63
|
AccelerometerController,
|
|
61
64
|
AccelerometerReading,
|
|
65
|
+
MagnetometerController,
|
|
66
|
+
MagnetometerReading,
|
|
62
67
|
AppsController,
|
|
63
68
|
AudioBuffer,
|
|
64
69
|
AudioBufferSourceNode,
|
|
@@ -128,6 +133,8 @@ export type {
|
|
|
128
133
|
InputElementProps,
|
|
129
134
|
InputEvent,
|
|
130
135
|
InputEventTarget,
|
|
136
|
+
InputElement,
|
|
137
|
+
ButtonElement,
|
|
131
138
|
KeyEvent,
|
|
132
139
|
MediaDevices,
|
|
133
140
|
MediaRecorder,
|
package/runtime.cpp
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "input.h"
|
|
8
8
|
#include "memory.h"
|
|
9
9
|
#include "events.h"
|
|
10
|
+
#include "touch.h"
|
|
10
11
|
#include "power.h"
|
|
11
12
|
#include "services/app_runner.h"
|
|
12
13
|
#include "services/app_state.h"
|
|
@@ -48,10 +49,89 @@
|
|
|
48
49
|
#include "ui/document.h"
|
|
49
50
|
#include "ui/tree_internal.h"
|
|
50
51
|
|
|
52
|
+
#include <atomic>
|
|
53
|
+
#include <cstdint>
|
|
54
|
+
|
|
55
|
+
extern "C" void gea_cycle_collection_defer_begin() __attribute__((weak));
|
|
56
|
+
extern "C" void gea_cycle_collection_defer_end() __attribute__((weak));
|
|
57
|
+
|
|
51
58
|
namespace gea::framework {
|
|
52
59
|
|
|
53
60
|
namespace {
|
|
54
61
|
|
|
62
|
+
// The low bit is the requested state; the remaining bits distinguish each
|
|
63
|
+
// transition. An acknowledgement of an earlier pause cannot satisfy a new
|
|
64
|
+
// pause after a quick resume. These survive a request made by native boot,
|
|
65
|
+
// before either the UI task or its queue exists.
|
|
66
|
+
std::atomic<std::uint32_t> uiPauseRequest{0};
|
|
67
|
+
std::atomic<std::uint32_t> uiPauseAcknowledged{0};
|
|
68
|
+
|
|
69
|
+
void discardPausedInput()
|
|
70
|
+
{
|
|
71
|
+
#if !defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) || !GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
|
|
72
|
+
services::AppState::lock();
|
|
73
|
+
events::TouchRuntime::resetGestureState();
|
|
74
|
+
services::AppState::unlock();
|
|
75
|
+
gea::platform::touch::Touchscreen::consumeLatestMove(nullptr, nullptr);
|
|
76
|
+
(void)input::consumeRotaryDelta();
|
|
77
|
+
input::HardwareKeyEvent key{};
|
|
78
|
+
while (input::consumeKeyEvent(key)) {}
|
|
79
|
+
#endif
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void discardPausedEvent(const events::Event &event)
|
|
83
|
+
{
|
|
84
|
+
#if !defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) || !GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
|
|
85
|
+
// A move marker must re-arm the controller even though its callback is
|
|
86
|
+
// dropped; otherwise the first drag after resume can stop producing moves.
|
|
87
|
+
if (event.type == events::EventType::Touch && event.touchPhase == events::TouchPhase::Move)
|
|
88
|
+
gea::platform::touch::Touchscreen::consumeLatestMove(nullptr, nullptr);
|
|
89
|
+
#else
|
|
90
|
+
(void)event;
|
|
91
|
+
#endif
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
bool acknowledgeUiPause(bool &paused)
|
|
95
|
+
{
|
|
96
|
+
const auto request = uiPauseRequest.load(std::memory_order_acquire);
|
|
97
|
+
if ((request & 1U) == 0) return false;
|
|
98
|
+
if (!paused) {
|
|
99
|
+
discardPausedInput();
|
|
100
|
+
paused = true;
|
|
101
|
+
}
|
|
102
|
+
// Called only by the event-loop owner, outside dispatch_event/runFrame and
|
|
103
|
+
// after discardPausedInput has released AppState. No lock is held while the
|
|
104
|
+
// runtime waits on its queue in the paused state.
|
|
105
|
+
uiPauseAcknowledged.store(request, std::memory_order_release);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Input can allocate just like a frame callback, but touch dispatch and the
|
|
110
|
+
// frame's poll/rotary/key prelude run outside Application::frame's lifetime
|
|
111
|
+
// scope. Do not collect while generated callbacks still hold transient native
|
|
112
|
+
// references or the dispatcher is borrowing its current listener.
|
|
113
|
+
class InputCycleCollectionDeferral {
|
|
114
|
+
public:
|
|
115
|
+
InputCycleCollectionDeferral()
|
|
116
|
+
: active_(gea_cycle_collection_defer_begin && gea_cycle_collection_defer_end)
|
|
117
|
+
{
|
|
118
|
+
if (active_) gea_cycle_collection_defer_begin();
|
|
119
|
+
}
|
|
120
|
+
~InputCycleCollectionDeferral()
|
|
121
|
+
{
|
|
122
|
+
if (!active_) return;
|
|
123
|
+
const auto previous = app::applicationFramePhaseRead();
|
|
124
|
+
app::applicationFramePhaseSet(app::ApplicationFramePhase::CycleCollection);
|
|
125
|
+
gea_cycle_collection_defer_end();
|
|
126
|
+
app::applicationFramePhaseSet(previous);
|
|
127
|
+
}
|
|
128
|
+
InputCycleCollectionDeferral(const InputCycleCollectionDeferral &) = delete;
|
|
129
|
+
InputCycleCollectionDeferral &operator=(const InputCycleCollectionDeferral &) = delete;
|
|
130
|
+
|
|
131
|
+
private:
|
|
132
|
+
bool active_;
|
|
133
|
+
};
|
|
134
|
+
|
|
55
135
|
#if defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) && GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
|
|
56
136
|
|
|
57
137
|
void run_app_frame(int timestampMs, void *context)
|
|
@@ -96,19 +176,8 @@ void dispatch_key_input()
|
|
|
96
176
|
event.bubbles = true;
|
|
97
177
|
event.cancelable = true;
|
|
98
178
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (inputId >= 0) {
|
|
102
|
-
event.targetId = inputId;
|
|
103
|
-
event.currentTargetId = inputId;
|
|
104
|
-
tree.dispatchEvent(event);
|
|
105
|
-
stopped = event.propagationStopped;
|
|
106
|
-
}
|
|
107
|
-
if (!stopped) {
|
|
108
|
-
event.propagationStopped = false;
|
|
109
|
-
if (key.pressed) gea::embedded::ui::dispatchDocumentKeyDown(event);
|
|
110
|
-
else gea::embedded::ui::dispatchDocumentKeyUp(event);
|
|
111
|
-
}
|
|
179
|
+
event.targetId = tree.activeInputId();
|
|
180
|
+
gea::embedded::ui::dispatchRootKeyEvent(event);
|
|
112
181
|
}
|
|
113
182
|
}
|
|
114
183
|
|
|
@@ -116,6 +185,7 @@ void run_app_frame(int timestampMs, void *context)
|
|
|
116
185
|
{
|
|
117
186
|
(void)context;
|
|
118
187
|
const uint32_t nowMs = static_cast<uint32_t>(timestampMs);
|
|
188
|
+
InputCycleCollectionDeferral deferCycleCollection;
|
|
119
189
|
// Low-rate platform work shares the runtime event loop. This keeps input and
|
|
120
190
|
// battery observation deterministic without dedicating several mostly-idle
|
|
121
191
|
// FreeRTOS tasks (and their stacks) to polling.
|
|
@@ -153,7 +223,11 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
|
|
|
153
223
|
break;
|
|
154
224
|
}
|
|
155
225
|
services::AppState::lock();
|
|
156
|
-
|
|
226
|
+
bool touchChangedScene;
|
|
227
|
+
{
|
|
228
|
+
InputCycleCollectionDeferral deferCycleCollection;
|
|
229
|
+
touchChangedScene = events::TouchRuntime::dispatchEvent(event);
|
|
230
|
+
}
|
|
157
231
|
services::AppState::unlock();
|
|
158
232
|
// Input-triggered render: a touch that actually ran handlers (down / real
|
|
159
233
|
// move / up) likely changed the scene, so post a Frame now instead of
|
|
@@ -198,6 +272,7 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
|
|
|
198
272
|
// A finger held still posts no moves, so gestureActiveWithin() lapses to
|
|
199
273
|
// false after the window and the loop quiesces — no busy-render on a hold.
|
|
200
274
|
for (;;) {
|
|
275
|
+
if (Runtime::uiPauseRequested()) break;
|
|
201
276
|
#if defined(ESP_PLATFORM) && defined(GEA_NATIVE_DEBUGGER) && GEA_NATIVE_DEBUGGER
|
|
202
277
|
if (services::FrameScheduler::debuggerFrameRate() > 0) break;
|
|
203
278
|
#endif
|
|
@@ -213,11 +288,12 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
|
|
|
213
288
|
// keep going, instead of returning to the timer-paced outer loop.
|
|
214
289
|
// drainFramesAndCheckInput() leaves a non-Frame event at the queue
|
|
215
290
|
// front, so receiveEvent() returns it immediately without blocking.
|
|
216
|
-
while (services::FrameScheduler::drainFramesAndCheckInput()) {
|
|
291
|
+
while (!Runtime::uiPauseRequested() && services::FrameScheduler::drainFramesAndCheckInput()) {
|
|
217
292
|
events::Event pending{};
|
|
218
293
|
if (!services::FrameScheduler::receiveEvent(&pending)) break;
|
|
219
294
|
dispatch_event(pending, options);
|
|
220
295
|
}
|
|
296
|
+
if (Runtime::uiPauseRequested()) break;
|
|
221
297
|
services::FrameScheduler::runFrame(frameCallbacks);
|
|
222
298
|
}
|
|
223
299
|
// Persist any localStorage writes made this frame (or from a store's
|
|
@@ -242,6 +318,40 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
|
|
|
242
318
|
|
|
243
319
|
} // namespace
|
|
244
320
|
|
|
321
|
+
void Runtime::requestUiPause(bool paused)
|
|
322
|
+
{
|
|
323
|
+
auto observed = uiPauseRequest.load(std::memory_order_relaxed);
|
|
324
|
+
for (;;) {
|
|
325
|
+
if (((observed & 1U) != 0) == paused) return;
|
|
326
|
+
const auto next = ((observed + 2U) & ~1U) | (paused ? 1U : 0U);
|
|
327
|
+
if (uiPauseRequest.compare_exchange_weak(observed, next, std::memory_order_acq_rel,
|
|
328
|
+
std::memory_order_relaxed)) break;
|
|
329
|
+
}
|
|
330
|
+
#if !GEA_EMBEDDED_NO_DISPLAY
|
|
331
|
+
// Before queue creation this harmlessly fails. A full queue is already a
|
|
332
|
+
// wake-up; an idle runtime receives this event and observes the new request.
|
|
333
|
+
events::Event wake{};
|
|
334
|
+
wake.type = events::EventType::Timeout;
|
|
335
|
+
(void)services::FrameScheduler::sendEvent(wake, 0);
|
|
336
|
+
#endif
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
bool Runtime::uiPauseRequested()
|
|
340
|
+
{
|
|
341
|
+
return (uiPauseRequest.load(std::memory_order_acquire) & 1U) != 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
bool Runtime::uiPaused()
|
|
345
|
+
{
|
|
346
|
+
#if GEA_EMBEDDED_NO_DISPLAY
|
|
347
|
+
return uiPauseRequested();
|
|
348
|
+
#else
|
|
349
|
+
const auto request = uiPauseRequest.load(std::memory_order_acquire);
|
|
350
|
+
return (request & 1U) != 0 && uiPauseAcknowledged.load(std::memory_order_acquire) == request &&
|
|
351
|
+
uiPauseRequest.load(std::memory_order_acquire) == request;
|
|
352
|
+
#endif
|
|
353
|
+
}
|
|
354
|
+
|
|
245
355
|
void Runtime::runNativeBoot()
|
|
246
356
|
{
|
|
247
357
|
// An app with native hardware of its own (audio, USB host, sensors) brings
|
|
@@ -464,10 +574,29 @@ void Runtime::run(const RuntimeOptions &options)
|
|
|
464
574
|
// which starves whatever else shares it. boot() was the work; return.
|
|
465
575
|
(void)options;
|
|
466
576
|
#else
|
|
577
|
+
bool paused = false;
|
|
467
578
|
while (true) {
|
|
579
|
+
if (acknowledgeUiPause(paused)) {
|
|
580
|
+
events::Event discarded{};
|
|
581
|
+
if (services::FrameScheduler::receiveEvent(&discarded)) discardPausedEvent(discarded);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (paused) {
|
|
585
|
+
discardPausedInput();
|
|
586
|
+
paused = false;
|
|
587
|
+
(void)services::FrameScheduler::takeCatchUpRequest();
|
|
588
|
+
// A paused Frame was consumed without runFrame clearing the target's
|
|
589
|
+
// coalesced-pending flag. Force one ordinary frame to re-arm its clock;
|
|
590
|
+
// relying on a timer or a best-effort queue post could leave it stuck.
|
|
591
|
+
events::Event refresh{};
|
|
592
|
+
refresh.type = events::EventType::Frame;
|
|
593
|
+
dispatch_event(refresh, options);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
468
596
|
events::Event event{};
|
|
469
597
|
if (services::FrameScheduler::receiveEvent(&event)) {
|
|
470
|
-
|
|
598
|
+
if (uiPauseRequested()) discardPausedEvent(event);
|
|
599
|
+
else dispatch_event(event, options);
|
|
471
600
|
}
|
|
472
601
|
}
|
|
473
602
|
#endif
|
|
@@ -5,6 +5,7 @@ import process from 'node:process'
|
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
import { resolveAppGeaVitePlugin } from './gea-vite-plugin-resolution.mjs'
|
|
8
9
|
import { inlineJsonImports, transformGeaEmbeddedCompatSource } from './gea-embedded-compat-transform.mjs'
|
|
9
10
|
import { normalizeEmbeddedJsxOptions, shouldIgnoreCompatStagingDirectory } from './gea-embedded-compat-staging.mjs'
|
|
10
11
|
import { dotEnvDefines, inlineProcessEnv } from './dotenv-defines.mjs'
|
|
@@ -24,7 +25,8 @@ import {
|
|
|
24
25
|
const nativeModules = new Map()
|
|
25
26
|
let nativeModuleSources = new Map()
|
|
26
27
|
const args = process.argv.slice(2)
|
|
27
|
-
const
|
|
28
|
+
const corePackageJson = new URL('../package.json', import.meta.url)
|
|
29
|
+
const requireFromLib = createRequire(corePackageJson)
|
|
28
30
|
const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
29
31
|
const elementsRoot = path.resolve(coreRoot, '..', 'elements')
|
|
30
32
|
const engineRoot = path.resolve(coreRoot, '..', 'engine')
|
|
@@ -74,7 +76,6 @@ function readPackageName(dir) {
|
|
|
74
76
|
// `paths` entry, so both consumers of the staged tree agree.
|
|
75
77
|
const runtimePath = path.join(coreRoot, 'runtime.ts')
|
|
76
78
|
const tryResolve = (m) => { try { return requireFromLib.resolve(m) } catch { return '' } }
|
|
77
|
-
const geaPluginResolved = fileURLToPath(import.meta.resolve('@geajs/vite-plugin'))
|
|
78
79
|
const { parse } = requireFromLib('@babel/parser')
|
|
79
80
|
const traverseModule = requireFromLib('@babel/traverse')
|
|
80
81
|
const t = requireFromLib('@babel/types')
|
|
@@ -211,7 +212,29 @@ function copyCssUrlAssets(srcCss, dstCss) {
|
|
|
211
212
|
const dstAsset = path.resolve(dstDir, clean)
|
|
212
213
|
if (dstAsset === srcAsset) continue
|
|
213
214
|
fs.mkdirSync(path.dirname(dstAsset), { recursive: true })
|
|
214
|
-
|
|
215
|
+
copyAssetAtomically(srcAsset, dstAsset)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A url() that climbs out of the app lands outside this build's own directory
|
|
220
|
+
// (`../../../assets/fonts/x.ttf` staged under `<build>/<app>/` is
|
|
221
|
+
// `<build>/assets/fonts/x.ttf`), where other builds sharing the parent stage
|
|
222
|
+
// the same file at the same time -- core's app pipelines all do. copyFileSync
|
|
223
|
+
// truncates and rewrites in place, so a concurrent reader saw a partial font
|
|
224
|
+
// ("Offset is outside the bounds of the DataView"). Identical bytes are left
|
|
225
|
+
// alone, and new bytes appear in one rename, never half-written.
|
|
226
|
+
function copyAssetAtomically(srcAsset, dstAsset) {
|
|
227
|
+
const bytes = fs.readFileSync(srcAsset)
|
|
228
|
+
if (fs.existsSync(dstAsset) && fs.readFileSync(dstAsset).equals(bytes)) return
|
|
229
|
+
const staged = `${dstAsset}.${process.pid}.tmp`
|
|
230
|
+
fs.writeFileSync(staged, bytes)
|
|
231
|
+
try {
|
|
232
|
+
fs.renameSync(staged, dstAsset)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Windows refuses to replace a file another process has open; that
|
|
235
|
+
// process staged it first, so the bytes it holds are these bytes.
|
|
236
|
+
fs.rmSync(staged, { force: true })
|
|
237
|
+
if (!(fs.existsSync(dstAsset) && fs.readFileSync(dstAsset).equals(bytes))) throw error
|
|
215
238
|
}
|
|
216
239
|
}
|
|
217
240
|
|
|
@@ -528,6 +551,7 @@ function resolveGeaThreeAliasModules(appDir) {
|
|
|
528
551
|
}
|
|
529
552
|
|
|
530
553
|
function writeCompatViteConfig({ configPath, compatSrcDir, appDir, stagedAppDir, entry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines }) {
|
|
554
|
+
const geaPluginResolved = resolveAppGeaVitePlugin(appDir, corePackageJson)
|
|
531
555
|
const engineComponentsPath = path.join(engineRoot, 'components/index.ts')
|
|
532
556
|
const engineComponentSubpath = path.join(engineRoot, 'components/$1')
|
|
533
557
|
const elementsPath = path.join(elementsRoot, 'components/index.ts')
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export function fileAssetDirectory(manifest) {
|
|
6
|
+
const storage = manifest?.gea?.embeddedAssets?.storage;
|
|
7
|
+
if (storage === undefined) return null;
|
|
8
|
+
if (
|
|
9
|
+
storage?.type !== "file" ||
|
|
10
|
+
typeof storage.directory !== "string" ||
|
|
11
|
+
!storage.directory.startsWith("/") ||
|
|
12
|
+
storage.directory === "/" ||
|
|
13
|
+
storage.directory !== path.posix.normalize(storage.directory) ||
|
|
14
|
+
/[\\\0\r\n]/.test(storage.directory)
|
|
15
|
+
) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'gea.embeddedAssets.storage must specify type="file" and a normalized absolute directory',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return storage.directory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function fileAssetBundle(appDir, outCpp, kind, entries) {
|
|
24
|
+
const manifestFile = path.join(appDir, "package.json");
|
|
25
|
+
const manifest = fs.existsSync(manifestFile)
|
|
26
|
+
? JSON.parse(fs.readFileSync(manifestFile, "utf8"))
|
|
27
|
+
: {};
|
|
28
|
+
const directory = fileAssetDirectory(manifest);
|
|
29
|
+
if (!directory || !entries.length) return null;
|
|
30
|
+
const bytes = Buffer.concat(entries.map((entry) => Buffer.from(entry.data)));
|
|
31
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
32
|
+
const id = String(manifest.gea?.id || path.basename(appDir)).replace(
|
|
33
|
+
/[^a-zA-Z0-9_-]/g,
|
|
34
|
+
"_",
|
|
35
|
+
);
|
|
36
|
+
const filename = `${id}-${kind}-${hash}.bin`;
|
|
37
|
+
const local = path.join(path.dirname(outCpp), filename);
|
|
38
|
+
const remote = path.posix.join(directory, filename);
|
|
39
|
+
fs.mkdirSync(path.dirname(local), { recursive: true });
|
|
40
|
+
if (!fs.existsSync(local) || !fs.readFileSync(local).equals(bytes))
|
|
41
|
+
fs.writeFileSync(local, bytes);
|
|
42
|
+
const deployment = {
|
|
43
|
+
version: 1,
|
|
44
|
+
files: [{ local, remote, bytes: bytes.length, sha256: hash }],
|
|
45
|
+
};
|
|
46
|
+
const metadata = `${outCpp}.assets.json`;
|
|
47
|
+
const contents = JSON.stringify(deployment, null, 2) + "\n";
|
|
48
|
+
if (
|
|
49
|
+
!fs.existsSync(metadata) ||
|
|
50
|
+
fs.readFileSync(metadata, "utf8") !== contents
|
|
51
|
+
)
|
|
52
|
+
fs.writeFileSync(metadata, contents);
|
|
53
|
+
let crc = 0xffffffff;
|
|
54
|
+
for (const value of bytes) {
|
|
55
|
+
crc ^= value;
|
|
56
|
+
for (let bit = 0; bit < 8; bit++)
|
|
57
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
entries,
|
|
61
|
+
remote,
|
|
62
|
+
bytes: bytes.length,
|
|
63
|
+
crc: (crc ^ 0xffffffff) >>> 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fileAssetLoader(bundle, name) {
|
|
68
|
+
return [
|
|
69
|
+
`static bool ${name}() {`,
|
|
70
|
+
" static const bool ready = [] {",
|
|
71
|
+
" const gea::platform::storage::AssetSegment segments[] = {",
|
|
72
|
+
...bundle.entries.map(
|
|
73
|
+
(entry) => ` {${entry.symbol}, ${entry.data.length}},`,
|
|
74
|
+
),
|
|
75
|
+
" };",
|
|
76
|
+
` return gea::platform::storage::loadAssetBundle(${JSON.stringify(bundle.remote)}, segments, ${bundle.entries.length}, ${bundle.bytes}, ${bundle.crc}U);`,
|
|
77
|
+
" }();",
|
|
78
|
+
" return ready;",
|
|
79
|
+
"}",
|
|
80
|
+
"",
|
|
81
|
+
];
|
|
82
|
+
}
|
|
@@ -35,7 +35,9 @@ export function geaNativeStylePlugin({ runtimeEntry }) {
|
|
|
35
35
|
return [path.join(sourceRoot, `${name}.ts`), path.join(sourceRoot, name, 'index.ts')].find(entry => fs.existsSync(entry)) || null
|
|
36
36
|
},
|
|
37
37
|
load(id) {
|
|
38
|
-
|
|
38
|
+
// Vite spells module ids with forward slashes on Windows too; normalize
|
|
39
|
+
// before comparing with the native path (a no-op on POSIX).
|
|
40
|
+
if (path.normalize(id.split('?')[0]) !== serializer) return null
|
|
39
41
|
// This module is the browser serialization boundary, not the JSX IR.
|
|
40
42
|
// Replacing it here preserves numeric values in the native IR and prevents
|
|
41
43
|
// browser px defaults from reaching native runtime-backed components/h().
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/** Honor an app's declared compiler plugin; only legacy undeclared apps inherit Core's. */
|
|
7
|
+
function resolveImport(directory) {
|
|
8
|
+
// Let Node apply real ESM export conditions (the plugin is import-only).
|
|
9
|
+
const url = execFileSync(process.execPath,
|
|
10
|
+
['--input-type=module', '-e', "process.stdout.write(import.meta.resolve('@geajs/vite-plugin'))"],
|
|
11
|
+
{ cwd: directory, encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
12
|
+
return fs.realpathSync(fileURLToPath(url))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function workspaceOwner(appDir) {
|
|
16
|
+
const app = path.resolve(appDir)
|
|
17
|
+
if (fs.existsSync(path.join(app, 'node_modules', '@geajs', 'vite-plugin'))) return null
|
|
18
|
+
for (let dir = path.dirname(app); dir !== path.dirname(dir); dir = path.dirname(dir)) {
|
|
19
|
+
let manifest
|
|
20
|
+
try { manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) } catch { continue }
|
|
21
|
+
const patterns = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? []
|
|
22
|
+
const relative = path.relative(dir, app).split(path.sep).join('/')
|
|
23
|
+
if (patterns.some(pattern => path.posix.matchesGlob(relative, pattern.replace(/\/+$/, '')))) return dir
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveAppGeaVitePlugin(appDir, corePackageJson) {
|
|
29
|
+
const manifest = path.join(appDir, 'package.json')
|
|
30
|
+
// A bare fixture directory has no manifest and so declares no plugin of its own.
|
|
31
|
+
const app = fs.existsSync(manifest) ? JSON.parse(fs.readFileSync(manifest, 'utf8')) : {}
|
|
32
|
+
const declared = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
|
|
33
|
+
.some(field => Object.hasOwn(app[field] || {}, '@geajs/vite-plugin'))
|
|
34
|
+
if (!declared) return resolveImport(path.dirname(corePackageJson instanceof URL ? fileURLToPath(corePackageJson) : corePackageJson))
|
|
35
|
+
// Resolving from the app may otherwise walk up to an unrelated ancestor install.
|
|
36
|
+
// Require an actual app-owned installation entry, or the hoisted one of an npm
|
|
37
|
+
// workspace root that lists this app among its workspaces.
|
|
38
|
+
const installed = path.join(workspaceOwner(appDir) ?? appDir, 'node_modules', '@geajs', 'vite-plugin')
|
|
39
|
+
try {
|
|
40
|
+
const packageRoot = fs.realpathSync(installed)
|
|
41
|
+
const actual = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
|
|
42
|
+
if (actual.name !== '@geajs/vite-plugin') throw new Error('installed package name mismatch')
|
|
43
|
+
const resolved = resolveImport(appDir)
|
|
44
|
+
if (!resolved.startsWith(packageRoot + path.sep)) throw new Error('resolved outside declared installation')
|
|
45
|
+
return resolved
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`App declares @geajs/vite-plugin but its installation is unavailable: ${error.message}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { describeStaticAsset } from './gea-static-asset.mjs'
|
|
26
26
|
import { compileTimeOnlyAssetPaths } from './gea-embedded-asset-policy.mjs'
|
|
27
|
+
import { fileAssetBundle, fileAssetLoader } from './gea-file-asset-bundle.mjs'
|
|
27
28
|
import fs from 'node:fs'
|
|
28
29
|
import path from 'node:path'
|
|
29
30
|
import process from 'node:process'
|
|
@@ -230,12 +231,19 @@ function writeFileIfChanged(filePath, contents) {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
const assets = collectAssets(appDir)
|
|
234
|
+
const bundleEntries = new Map()
|
|
235
|
+
for (const file of assets) {
|
|
236
|
+
const symbol = assetSymbol(relPath(file))
|
|
237
|
+
if (!bundleEntries.has(symbol)) bundleEntries.set(symbol, { symbol, data: fs.readFileSync(file) })
|
|
238
|
+
}
|
|
239
|
+
const bundle = fileAssetBundle(appDir, outCpp, 'images', [...bundleEntries.values()])
|
|
233
240
|
|
|
234
241
|
const parts = []
|
|
235
242
|
parts.push('// Generated by scripts/generate-gea-embedded-assets.mjs. Do not edit by hand.')
|
|
236
243
|
parts.push(`// app: ${relPath(appDir) || path.basename(appDir)}`)
|
|
237
244
|
parts.push('')
|
|
238
245
|
parts.push('#include <cstring>')
|
|
246
|
+
if (bundle) parts.push('#include "platform/asset_bundle.h"')
|
|
239
247
|
parts.push('')
|
|
240
248
|
|
|
241
249
|
if (assets.length === 0) {
|
|
@@ -280,14 +288,23 @@ if (assets.length === 0) {
|
|
|
280
288
|
// rejects `weak` on an internal const. `extern "C"` + initializer = a weak
|
|
281
289
|
// definition with external, unmangled (namespace-independent) linkage,
|
|
282
290
|
// which both GCC and Clang accept.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
if (bundle) {
|
|
292
|
+
parts.push(`extern "C" { __attribute__((weak)) GEA_FILE_ASSET_BUFFER unsigned char ${sym}[${Math.max(1, buffer.length)}]; }`)
|
|
293
|
+
} else {
|
|
294
|
+
parts.push(`extern "C" __attribute__((weak)) const unsigned char ${sym}[] = {`)
|
|
295
|
+
parts.push(byteArrayBody(buffer))
|
|
296
|
+
parts.push('};')
|
|
297
|
+
}
|
|
286
298
|
parts.push(`extern "C" __attribute__((weak)) const unsigned long ${sym}_len = ${buffer.length}UL;`)
|
|
287
299
|
parts.push('')
|
|
288
300
|
}
|
|
301
|
+
if (bundle) {
|
|
302
|
+
parts.push(...fileAssetLoader(bundle, 'prepareImageAssets'))
|
|
303
|
+
parts.push('extern "C" bool gea_embedded_assets_prepare() { return prepareImageAssets(); }')
|
|
304
|
+
}
|
|
289
305
|
parts.push('extern "C" __attribute__((weak)) bool gea_embedded_asset_lookup(const char *path, const unsigned char **data, unsigned long *length) {')
|
|
290
306
|
parts.push(' if (!path || !data || !length) return false;')
|
|
307
|
+
if (bundle) parts.push(' if (!prepareImageAssets()) return false;')
|
|
291
308
|
for (const file of assets) {
|
|
292
309
|
const rel = relPath(file)
|
|
293
310
|
const sym = assetSymbol(rel)
|