@geastack/core 0.1.33 → 0.1.35

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/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  /// <reference path="./css.d.ts" />
2
2
 
3
+ export type * from './workers'
4
+ export type * from './audio-worklet'
5
+ /** Gea-only hardware output cancellation; call from an AudioWorklet message handler. */
6
+ export declare function flushAudioWorkletOutput(): void
7
+ export type * from './typed-arrays'
8
+ import type { AudioContextOptions, AudioWorklet, MediaStreamAudioSourceNode } from './audio-worklet'
9
+
3
10
  // Opt-in native 64-bit integer. Structurally a `number` (assign/read without
4
11
  // casts), but where a binding, parameter, field, or return is annotated `int`,
5
12
  // geatsc lowers it to a C++ `long long` and emits exact integer arithmetic
@@ -70,6 +77,7 @@ export interface GeaIntrinsicElements {
70
77
  h5: NativeViewProps
71
78
  h6: NativeViewProps
72
79
  audio: AudioProps
80
+ video: VideoProps
73
81
  button: NativeButtonProps
74
82
  img: ImageProps
75
83
  input: NativeInputElementProps
@@ -117,9 +125,13 @@ declare global {
117
125
  // (`double setTimeout(TimerCallback, double)`), so both the id and the delay
118
126
  // are `number`, and the callback takes no arguments (unlike the browser's,
119
127
  // which forwards extra `setTimeout` arguments to it).
128
+ /** @gea-host-no-property-writes */
120
129
  function setTimeout(callback: () => void, delayMs: number): number
130
+ /** @gea-host-no-property-writes */
121
131
  function clearTimeout(id: number): void
132
+ /** @gea-host-no-property-writes */
122
133
  function setInterval(callback: () => void, delayMs: number): number
134
+ /** @gea-host-no-property-writes */
123
135
  function clearInterval(id: number): void
124
136
 
125
137
  /**
@@ -364,6 +376,7 @@ export interface Document {
364
376
  querySelector(selector: string): Element | null
365
377
  querySelectorAll(selector: string): readonly Element[]
366
378
  createElement(tag: 'audio'): HTMLAudioElement
379
+ createElement(tag: 'video'): HTMLVideoElement
367
380
  createElement(tag: 'virtual-list'): VirtualListElement
368
381
  createElement(tag: string): Element
369
382
  createTextNode(text: string): Node
@@ -596,13 +609,28 @@ export interface AudioProps extends DataAttributes {
596
609
  autoplay?: boolean | string | number
597
610
  controls?: boolean | string | number
598
611
  loop?: boolean | string | number
599
- ref?: HTMLAudioElement | null
612
+ ref?: HTMLAudioElement | (typeof globalThis extends { HTMLAudioElement: { prototype: infer E } } ? E : never) | null
613
+ autoPlay?: boolean
600
614
  children?: any
601
615
  }
602
616
 
617
+ export interface VideoProps extends DataAttributes {
618
+ id?: string
619
+ class?: ClassValue
620
+ style?: Style
621
+ autoplay?: boolean
622
+ muted?: boolean
623
+ playsinline?: boolean
624
+ autoPlay?: boolean
625
+ playsInline?: boolean
626
+ ref?: HTMLVideoElement | (typeof globalThis extends { HTMLVideoElement: { prototype: infer E } } ? E : never) | null
627
+ }
628
+
603
629
  export type ImageSource = string | ArrayBuffer | Uint8Array | GeaEmbeddedImage
604
630
 
605
631
  export interface ImageProps extends DataAttributes {
632
+ alt?: string
633
+ key?: string | number
606
634
  id?: string
607
635
  class?: ClassValue
608
636
  // Optional: a static template can leave this unset and apply the source
@@ -1300,15 +1328,26 @@ export interface OscillatorNode {
1300
1328
  }
1301
1329
 
1302
1330
  export interface AudioContext {
1331
+ /** Conservative maximum software buffering delay, in seconds. */
1332
+ readonly baseLatency: number
1333
+ /** Device buffering estimate in seconds; zero when unavailable. */
1334
+ readonly outputLatency: number
1335
+ readonly sampleRate: number
1336
+ readonly state: 'running' | 'suspended' | 'closed'
1337
+ readonly audioWorklet: AudioWorklet
1303
1338
  readonly currentTime: number
1304
1339
  readonly destination: AudioDestinationNode
1305
1340
  createOscillator(): OscillatorNode
1306
1341
  createBufferSource(): AudioBufferSourceNode
1307
1342
  decodeAudioData(audioData: ArrayBuffer | Uint8Array | GeaAudioBlob): Promise<AudioBuffer>
1343
+ createMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode
1344
+ resume(): Promise<void>
1345
+ suspend(): Promise<void>
1346
+ close(): Promise<void>
1308
1347
  }
1309
1348
 
1310
1349
  export interface AudioContextConstructor {
1311
- new (): AudioContext
1350
+ new (options?: AudioContextOptions): AudioContext
1312
1351
  }
1313
1352
 
1314
1353
  export declare const audioContext: AudioContext
@@ -1531,6 +1570,7 @@ export interface ButtonProps {
1531
1570
  }
1532
1571
 
1533
1572
  export interface NativeButtonProps extends NativeEventAttributes {
1573
+ disabled?: boolean
1534
1574
  key?: string | number
1535
1575
  class?: ClassValue
1536
1576
  style?: Style
@@ -1653,9 +1693,13 @@ export interface FetchResponse {
1653
1693
  bytes(): Uint8Array
1654
1694
  }
1655
1695
 
1696
+ /** @gea-host-no-property-writes */
1656
1697
  export declare function fetchAsync(url: string, init?: FetchRequestInit): number
1698
+ /** @gea-host-no-property-writes */
1657
1699
  export declare function fetchReady(id: number): boolean
1700
+ /** @gea-host-no-property-writes */
1658
1701
  export declare function fetchResult(id: number): FetchResponse
1702
+ /** @gea-host-no-property-writes */
1659
1703
  export declare function fetchRelease(id: number): void
1660
1704
 
1661
1705
  /**
@@ -1692,6 +1736,7 @@ export declare function fetchUploadTotal(id: number): number
1692
1736
 
1693
1737
  declare global {
1694
1738
  function fetch(url: string, init?: FetchRequestInit): FetchResponse
1739
+ /** @gea-host-no-property-writes */
1695
1740
  function fetchAsync(url: string, init?: FetchRequestInit): number
1696
1741
  function fetchUploadFileAsync(
1697
1742
  url: string,
@@ -1705,8 +1750,11 @@ declare global {
1705
1750
  function fetchUploadProgress(id: number): number
1706
1751
  function fetchUploadSent(id: number): number
1707
1752
  function fetchUploadTotal(id: number): number
1753
+ /** @gea-host-no-property-writes */
1708
1754
  function fetchReady(id: number): boolean
1755
+ /** @gea-host-no-property-writes */
1709
1756
  function fetchResult(id: number): FetchResponse
1757
+ /** @gea-host-no-property-writes */
1710
1758
  function fetchRelease(id: number): void
1711
1759
  function atob(data: string): string
1712
1760
  function btoa(data: string | ArrayBuffer | Uint8Array): string
@@ -1727,18 +1775,20 @@ export interface WebSocketErrorEvent {
1727
1775
  }
1728
1776
 
1729
1777
  export interface WebSocketInstance {
1778
+ readonly bufferedAmount: number
1730
1779
  readonly url: string
1731
1780
  readonly readyState: number
1732
1781
  onopen: (() => void) | null
1733
1782
  onmessage: ((event: WebSocketMessageEvent) => void) | null
1734
1783
  onclose: ((event: WebSocketCloseEvent) => void) | null
1735
1784
  onerror: ((event: WebSocketErrorEvent) => void) | null
1785
+ /** @gea-host-no-property-writes */
1736
1786
  send(data: string): void
1737
1787
  close(code?: number, reason?: string): void
1738
1788
  }
1739
1789
 
1740
1790
  export interface WebSocketConstructor {
1741
- new (url: string): WebSocketInstance
1791
+ new (url: string, protocols?: string | string[]): WebSocketInstance
1742
1792
  readonly CONNECTING: 0
1743
1793
  readonly OPEN: 1
1744
1794
  readonly CLOSING: 2
@@ -1805,7 +1855,7 @@ export declare const http: HttpModule
1805
1855
 
1806
1856
  export interface MediaStreamTrack {
1807
1857
  readonly id: string
1808
- readonly kind: 'audio'
1858
+ readonly kind: string
1809
1859
  enabled: boolean
1810
1860
  readonly readyState: 'live' | 'ended'
1811
1861
  stop(): void
@@ -1813,12 +1863,19 @@ export interface MediaStreamTrack {
1813
1863
 
1814
1864
  export interface MediaStream {
1815
1865
  readonly id: string
1866
+ readonly active: boolean
1816
1867
  getAudioTracks(): MediaStreamTrack[]
1868
+ getVideoTracks(): MediaStreamTrack[]
1817
1869
  getTracks(): MediaStreamTrack[]
1870
+ getTrackById(id: string): MediaStreamTrack | null
1871
+ addTrack(track: MediaStreamTrack): void
1872
+ removeTrack(track: MediaStreamTrack): void
1818
1873
  }
1819
1874
 
1820
1875
  export interface MediaStreamConstructor {
1821
1876
  new (): MediaStream
1877
+ new (stream: MediaStream): MediaStream
1878
+ new (tracks: MediaStreamTrack[]): MediaStream
1822
1879
  }
1823
1880
 
1824
1881
  export interface MediaStreamConstraints {
@@ -1863,10 +1920,34 @@ export interface MediaRecorderConstructor {
1863
1920
 
1864
1921
  export interface HTMLAudioElement extends Element {
1865
1922
  src: string
1923
+ srcObject: MediaStream | null
1924
+ autoplay: boolean
1925
+ readonly paused: boolean
1866
1926
  currentTime: number
1867
1927
  volume: number
1868
1928
  loop: boolean
1869
- play(): void
1929
+ play(): Promise<void>
1930
+ pause(): void
1931
+ /** Freeze the current PCM queue endpoint; later RTP silence is excluded. */
1932
+ beginDrain(): void
1933
+ /** Discard queued speech while keeping the live output clock running. */
1934
+ clearBufferedAudio(): void
1935
+ /** Queued PCM has reached the output driver. Hardware DMA drains on capture handoff. */
1936
+ readonly drained: boolean
1937
+ /** RMS of PCM being pulled for playback, normalized to 0–1. */
1938
+ readonly audioLevel: number
1939
+ }
1940
+
1941
+ export interface HTMLVideoElement extends Element {
1942
+ /** Native HTTP multipart JPEG streaming; decoded on native network/video workers. */
1943
+ src: string
1944
+ srcObject: MediaStream | null
1945
+ autoplay: boolean
1946
+ readonly paused: boolean
1947
+ readonly videoWidth: number
1948
+ readonly videoHeight: number
1949
+ onplaying: (() => void) | null
1950
+ play(): Promise<void>
1870
1951
  pause(): void
1871
1952
  }
1872
1953
 
@@ -1875,6 +1956,13 @@ export interface AudioConstructor {
1875
1956
  }
1876
1957
 
1877
1958
  declare global {
1959
+ interface HTMLAudioElement {
1960
+ beginDrain(): void
1961
+ clearBufferedAudio(): void
1962
+ readonly drained: boolean
1963
+ /** RMS of PCM being pulled for playback, normalized to 0–1. */
1964
+ readonly audioLevel: number
1965
+ }
1878
1966
  interface Blob {
1879
1967
  readonly path: string
1880
1968
  }
@@ -1907,10 +1995,54 @@ export interface RTCSessionDescriptionInit {
1907
1995
  sdp?: string
1908
1996
  }
1909
1997
 
1998
+ export interface RTCSessionDescription {
1999
+ readonly type: RTCSessionDescriptionInit['type']
2000
+ readonly sdp: string
2001
+ toJSON(): RTCSessionDescriptionInit
2002
+ }
2003
+ export interface RTCSessionDescriptionConstructor {
2004
+ new (init: RTCSessionDescriptionInit): RTCSessionDescription
2005
+ }
2006
+ declare global {
2007
+ const RTCSessionDescription: RTCSessionDescriptionConstructor
2008
+ }
2009
+
1910
2010
  export interface RTCIceCandidateInit {
1911
- candidate: string
2011
+ candidate?: string
1912
2012
  sdpMid?: string | null
1913
2013
  sdpMLineIndex?: number | null
2014
+ usernameFragment?: string | null
2015
+ }
2016
+
2017
+ export interface RTCLocalIceCandidateInit extends RTCIceCandidateInit {
2018
+ relayProtocol?: 'udp' | 'tcp' | 'tls' | null
2019
+ url?: string | null
2020
+ }
2021
+
2022
+ export interface RTCIceCandidate {
2023
+ readonly candidate: string
2024
+ readonly sdpMid: string | null
2025
+ readonly sdpMLineIndex: number | null
2026
+ readonly foundation: string | null
2027
+ readonly component: 'rtp' | 'rtcp' | null
2028
+ readonly priority: number | null
2029
+ readonly address: string | null
2030
+ readonly protocol: 'udp' | 'tcp' | null
2031
+ readonly port: number | null
2032
+ readonly type: 'host' | 'srflx' | 'prflx' | 'relay' | null
2033
+ readonly tcpType: 'active' | 'passive' | 'so' | null
2034
+ readonly relatedAddress: string | null
2035
+ readonly relatedPort: number | null
2036
+ readonly usernameFragment: string | null
2037
+ readonly relayProtocol: 'udp' | 'tcp' | 'tls' | null
2038
+ readonly url: string | null
2039
+ toJSON(): RTCIceCandidateInit
2040
+ }
2041
+ export interface RTCIceCandidateConstructor {
2042
+ new (init?: RTCLocalIceCandidateInit): RTCIceCandidate
2043
+ }
2044
+ declare global {
2045
+ const RTCIceCandidate: RTCIceCandidateConstructor
1914
2046
  }
1915
2047
 
1916
2048
  export interface RTCIceServer {
@@ -1921,15 +2053,61 @@ export interface RTCIceServer {
1921
2053
 
1922
2054
  export interface RTCConfiguration {
1923
2055
  iceServers?: RTCIceServer[]
2056
+ iceTransportPolicy?: 'all' | 'relay'
1924
2057
  }
1925
2058
 
1926
2059
  export interface RTCPeerConnectionIceEvent {
1927
- readonly candidate: RTCIceCandidateInit | null
2060
+ readonly candidate: RTCIceCandidate | null
1928
2061
  }
1929
2062
 
1930
2063
  export interface RTCTrackEvent {
1931
2064
  readonly track: MediaStreamTrack
1932
2065
  readonly streams: readonly MediaStream[]
2066
+ readonly receiver: RTCRtpReceiver
2067
+ readonly transceiver: RTCRtpTransceiver
2068
+ }
2069
+
2070
+ export interface RTCDataChannelInit {
2071
+ ordered?: boolean
2072
+ maxPacketLifeTime?: number
2073
+ maxRetransmits?: number
2074
+ protocol?: string
2075
+ negotiated?: boolean
2076
+ id?: number
2077
+ }
2078
+ export interface RTCDataChannelMessageEvent { readonly data: string | ArrayBuffer }
2079
+ export interface RTCDataChannel {
2080
+ readonly label: string
2081
+ readonly protocol: string
2082
+ readonly ordered: boolean
2083
+ readonly id: number | null
2084
+ readonly maxPacketLifeTime: number | null
2085
+ readonly maxRetransmits: number | null
2086
+ readonly readyState: 'connecting' | 'open' | 'closing' | 'closed'
2087
+ binaryType: 'arraybuffer'
2088
+ readonly bufferedAmount: number
2089
+ bufferedAmountLowThreshold: number
2090
+ onopen: (() => void) | null
2091
+ onclose: (() => void) | null
2092
+ onmessage: ((event: RTCDataChannelMessageEvent) => void) | null
2093
+ onbufferedamountlow: (() => void) | null
2094
+ send(data: string | ArrayBuffer | Uint8Array): void
2095
+ close(): void
2096
+ }
2097
+ export interface RTCRtpSender {
2098
+ readonly track: MediaStreamTrack | null
2099
+ replaceTrack(track: MediaStreamTrack | null): Promise<void>
2100
+ }
2101
+ export interface RTCRtpReceiver { readonly track: MediaStreamTrack | null }
2102
+ export interface RTCRtpTransceiverInit { direction?: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive' }
2103
+ export interface RTCRtpTransceiver {
2104
+ readonly sender: RTCRtpSender
2105
+ readonly receiver: RTCRtpReceiver
2106
+ readonly mid: string | null
2107
+ direction: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive'
2108
+ readonly currentDirection: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive' | null
2109
+ readonly stopped: boolean
2110
+ stop(): void
1933
2111
  }
1934
2112
 
1935
2113
  export interface RTCPeerConnectionInstance {
@@ -1941,7 +2119,19 @@ export interface RTCPeerConnectionInstance {
1941
2119
  onconnectionstatechange: (() => void) | null
1942
2120
  oniceconnectionstatechange: (() => void) | null
1943
2121
 
1944
- addTrack(track: MediaStreamTrack, stream: MediaStream): void
2122
+ readonly signalingState: 'stable' | 'have-local-offer' | 'have-remote-offer' | 'closed'
2123
+ readonly localDescription: RTCSessionDescription | null
2124
+ readonly remoteDescription: RTCSessionDescription | null
2125
+ onsignalingstatechange: (() => void) | null
2126
+ onnegotiationneeded: (() => void) | null
2127
+ ondatachannel: ((event: { channel: RTCDataChannel }) => void) | null
2128
+ addTrack(track: MediaStreamTrack, stream?: MediaStream): RTCRtpSender
2129
+ removeTrack(sender: RTCRtpSender): void
2130
+ addTransceiver(trackOrKind: MediaStreamTrack | 'audio', init?: RTCRtpTransceiverInit): RTCRtpTransceiver
2131
+ getTransceivers(): RTCRtpTransceiver[]
2132
+ getSenders(): RTCRtpSender[]
2133
+ getReceivers(): RTCRtpReceiver[]
2134
+ createDataChannel(label: string, init?: RTCDataChannelInit): RTCDataChannel
1945
2135
  createOffer(): Promise<RTCSessionDescriptionInit>
1946
2136
  createAnswer(): Promise<RTCSessionDescriptionInit>
1947
2137
  setLocalDescription(desc: RTCSessionDescriptionInit): Promise<void>
@@ -1958,3 +2148,33 @@ declare global {
1958
2148
  const RTCPeerConnection: RTCPeerConnectionConstructor
1959
2149
  function Image(props: ImageProps): GeaJsxElement
1960
2150
  }
2151
+
2152
+
2153
+ /** Streaming mono signed PCM16, little endian, carried as base64 text. */
2154
+ export interface PcmAudioStream {
2155
+ setInput(stream: MediaStream): void
2156
+ /** Capture and upload on the native socket worker, independently of UI callbacks. */
2157
+ pipeTo(socket: WebSocketInstance, prefix: string, suffix: string): void
2158
+ /** Decode matching PCM packets on the native receive worker; control messages still reach JS. */
2159
+ receiveFrom(socket: WebSocketInstance, prefix: string, suffix: string): void
2160
+ /** Read the next 40ms capture block, or an empty string if not ready. */
2161
+ readBase64(): string
2162
+ writeBase64(data: string): void
2163
+ resetPlayback(interrupted?: boolean): void
2164
+ close(): void
2165
+ readonly queuedMs: number
2166
+ /** Conservative audible duration, excluding mixer and device DMA latency. */
2167
+ readonly playedMs: number
2168
+ readonly drained: boolean
2169
+ readonly audioLevel: number
2170
+ readonly capturePendingMs: number
2171
+ readonly captureDroppedSamples: number
2172
+ readonly capturePackets: number
2173
+ }
2174
+ export interface PcmAudioStreamConstructor {
2175
+ new (sampleRate: number): PcmAudioStream
2176
+ }
2177
+
2178
+ declare global {
2179
+ const PcmAudioStream: PcmAudioStreamConstructor
2180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/core",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "GeaStack app API, reactivity headers, runtime sources, and type declarations.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -34,6 +34,9 @@
34
34
  "files": [
35
35
  "app_frame_perf.cpp",
36
36
  "css.d.ts",
37
+ "workers.d.ts",
38
+ "audio-worklet.d.ts",
39
+ "typed-arrays.d.ts",
37
40
  "events.cpp",
38
41
  "gea_sources.sh",
39
42
  "gea_sources.mjs",
@@ -63,7 +66,7 @@
63
66
  "@geajs/core": "1.5.1",
64
67
  "@geajs/vite-plugin": "1.5.0",
65
68
  "@geastack/compiler": "^1.0.20",
66
- "@geastack/geatsc-plugin-gea": "^0.1.14",
69
+ "@geastack/geatsc-plugin-gea": "^0.1.15",
67
70
  "opentype.js": "^2.0.0",
68
71
  "vite": "^8.0.0"
69
72
  }
@@ -0,0 +1,51 @@
1
+ import type { MessagePort } from '../workers.js'
2
+
3
+ /** Native implementations of the AudioWorkletGlobalScope's two class APIs.
4
+ * The module loader supplies these lexical bindings to compiled worklet modules;
5
+ * applications use the browser globals, without imports or platform branches.
6
+ */
7
+
8
+ declare function __geaAudioWorkletProcessorPort(): MessagePort
9
+ declare function __geaFlushAudioWorkletOutput(): void
10
+
11
+ /** Gea hardware cancellation extension; capture remains active. */
12
+ export function flushAudioWorkletOutput(): void {
13
+ __geaFlushAudioWorkletOutput()
14
+ }
15
+
16
+ declare function __geaRegisterAudioWorkletProcessor(
17
+ name: string,
18
+ create: () => AudioWorkletProcessor,
19
+ process: (
20
+ processor: AudioWorkletProcessor,
21
+ inputs: Float32Array[][],
22
+ outputs: Float32Array[][]
23
+ ) => boolean
24
+ ): void
25
+
26
+ export abstract class AudioWorkletProcessor {
27
+ readonly port: MessagePort
28
+
29
+ constructor() {
30
+ this.port = __geaAudioWorkletProcessorPort()
31
+ }
32
+
33
+ abstract process(
34
+ inputs: Float32Array[][],
35
+ outputs: Float32Array[][],
36
+ parameters: Record<string, Float32Array>
37
+ ): boolean
38
+ }
39
+
40
+ // The native module builder validates the concrete registration constructor.
41
+ // AudioParam descriptors are explicitly unsupported, before code generation.
42
+ export function registerProcessor(
43
+ name: string,
44
+ constructor: new () => AudioWorkletProcessor
45
+ ): void {
46
+ __geaRegisterAudioWorkletProcessor(
47
+ name,
48
+ () => new constructor(),
49
+ (processor, inputs, outputs) => processor.process(inputs, outputs, {})
50
+ )
51
+ }
@@ -1,4 +1,7 @@
1
1
  export * from './runtime/compiler'
2
+ export type * from './workers'
3
+ export type * from './audio-worklet'
4
+ export { flushAudioWorkletOutput } from './runtime/audio-worklet'
2
5
  export * from './runtime/host'
3
6
  export * from './runtime/ble'
4
7
  export * from './runtime/images'
@@ -109,6 +112,10 @@ export type {
109
112
  GeolocationPosition,
110
113
  GeolocationPositionError,
111
114
  HTMLAudioElement,
115
+ PcmAudioStream,
116
+ PcmAudioStreamConstructor,
117
+ HTMLVideoElement,
118
+ VideoProps,
112
119
  HttpModule,
113
120
  HttpRequestHandler,
114
121
  HttpServer,
@@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
8
8
  import { inlineJsonImports, transformGeaEmbeddedCompatSource } from './gea-embedded-compat-transform.mjs'
9
9
  import { normalizeEmbeddedJsxOptions, shouldIgnoreCompatStagingDirectory } from './gea-embedded-compat-staging.mjs'
10
10
  import { dotEnvDefines, inlineProcessEnv } from './dotenv-defines.mjs'
11
+ import { collectWorkerModuleSources, writeWorkerRegistry, validateWorkletRegistrations } from './gea-worker-modules.mjs'
11
12
  import { resolveCompilerRuntimeEntry } from './gea-native-style-plugin.mjs'
12
13
  import {
13
14
  MODULE_HINT_SCOPE,
@@ -20,6 +21,8 @@ import {
20
21
  restoreModuleGraphElAnnotations,
21
22
  } from './gea-bundle-type-hints.mjs'
22
23
 
24
+ const nativeModules = new Map()
25
+ let nativeModuleSources = new Map()
23
26
  const args = process.argv.slice(2)
24
27
  const requireFromLib = createRequire(new URL('../package.json', import.meta.url))
25
28
  const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
@@ -246,7 +249,11 @@ function copyCompatSourceTree(srcDir, dstDir, envDefines = {}) {
246
249
  // (the fully native store/record lowering path). Then bake in .env vars
247
250
  // (process.env.<KEY> -> literal) so the module-graph snapshots geatsc
248
251
  // compiles carry the value — Vite's `define` only rewrites the bundle.
249
- fs.writeFileSync(dst, inlineProcessEnv(transformGeaEmbeddedCompatSource(inlineJsonImports(code, src), src), envDefines))
252
+ const native = nativeModuleSources.get(src) ?? { code, modules: [] }
253
+ for (const module of native.modules) {
254
+ nativeModules.set(`${module.kind}:${module.url}`, { ...module, stagedEntry: path.resolve(path.dirname(dst), path.relative(path.dirname(src), module.entry)) })
255
+ }
256
+ fs.writeFileSync(dst, inlineProcessEnv(transformGeaEmbeddedCompatSource(inlineJsonImports(native.code, src), src), envDefines))
250
257
  } else {
251
258
  fs.copyFileSync(src, dst)
252
259
  if (path.extname(entry.name) === '.css') copyCssUrlAssets(src, dst)
@@ -304,7 +311,7 @@ function linkCompatNodeModules(appDir, stagedAppDir, compatSrcDir) {
304
311
  // staging its containing directory wholesale (which also brings the css/asset
305
312
  // files discoverLocalImport skips).
306
313
  function escapedCompatImportDirs(appDir, entry) {
307
- const escaped = collectReachableSourceFiles(appDir, entry).filter((file) => path.relative(appDir, file).startsWith('..'))
314
+ const escaped = [...new Set([...collectReachableSourceFiles(appDir, entry), ...nativeModuleSources.keys()])].filter((file) => path.relative(appDir, file).startsWith('..'))
308
315
  const dirs = [...new Set(escaped.map((file) => path.dirname(file)))].sort()
309
316
  return dirs.filter((dir, index) => !dirs.slice(0, index).some((outer) => outer !== dir && !path.relative(outer, dir).startsWith('..')))
310
317
  }
@@ -4393,6 +4400,8 @@ let stagedEntryFile = null
4393
4400
  let fontSourceDir = appDir
4394
4401
 
4395
4402
  if (geaEmbeddedCompat) {
4403
+ nativeModuleSources = collectWorkerModuleSources(path.resolve(appDir, entry),
4404
+ (file) => collectReachableSourceFiles(path.dirname(file), path.basename(file)))
4396
4405
  const compatSrcDir = path.join(outDir, 'gea-embedded-compat-src')
4397
4406
  fs.rmSync(compatSrcDir, { recursive: true, force: true })
4398
4407
  // Relative imports may escape the app dir (examples/shared components).
@@ -4587,7 +4596,46 @@ if (hasFlag('--gea-ir-backend') || hasFlag('--gea-replace-renderers')) {
4587
4596
  // measurement scripts are always run with this
4588
4597
  // limit; the build has to state the same one, not depend on the caller's
4589
4598
  // NODE_OPTIONS.
4590
- run('node', ['--max-old-space-size=32768', ...geatscArgs])
4599
+ // Native interface call graphs can exceed V8's default ~1 MiB stack during
4600
+ // closure analysis; keep the compiler stack below the host's 8 MiB limit.
4601
+ run('node', ['--stack_size=8192', '--max-old-space-size=32768', ...geatscArgs])
4602
+
4603
+ if (nativeModules.size) {
4604
+ const nativeSources = []
4605
+ for (const module of nativeModules.values()) {
4606
+ if (!fs.existsSync(module.stagedEntry)) fail(`missing ${module.kind} module: ${module.entry}`)
4607
+ if (module.kind === 'worklet') {
4608
+ const implementation = path.join(coreRoot, 'runtime/audio-worklet.ts')
4609
+ const bindings = `import { AudioWorkletProcessor, registerProcessor } from ${JSON.stringify(implementation)}\n`
4610
+ const source = fs.readFileSync(module.stagedEntry, 'utf8')
4611
+ fs.writeFileSync(module.stagedEntry, bindings + source)
4612
+ const compilerRequire = createRequire(pathToFileURL(geatscBin))
4613
+ validateWorkletRegistrations(compilerRequire('typescript'), module.stagedEntry, implementation)
4614
+ }
4615
+ const moduleOut = path.join(outDir, `${module.kind}-${module.id}`)
4616
+ const moduleArgs = [
4617
+ geatscBin, 'compile', module.stagedEntry, '--out-dir', moduleOut,
4618
+ '--target', 'cpp', '--plugin', geaPluginSpecifier,
4619
+ '--entry-symbol', module.symbol, '--isolate-symbols', '--realm-storage',
4620
+ '--plugin-option', `gea.microtasks-namespace=gea::framework::app::generated::gea_${module.kind}_${module.id}`,
4621
+ ]
4622
+ if (cppBoard) moduleArgs.push('--cpp-board', cppBoard)
4623
+ if (cxxStandard) moduleArgs.push('--cxx-standard', cxxStandard)
4624
+ run('node', ['--stack_size=8192', '--max-old-space-size=32768', ...moduleArgs])
4625
+ const list = fs.readFileSync(path.join(moduleOut, 'geatsc-sources.txt'), 'utf8').split(/\r?\n/).filter(Boolean)
4626
+ nativeSources.push(...list.filter((source) => path.basename(source) !== 'gea_runtime_builtins.cpp'))
4627
+ }
4628
+ const listPath = path.join(outDir, 'geatsc-sources.txt')
4629
+ const sources = fs.readFileSync(listPath, 'utf8').split(/\r?\n/).filter(Boolean)
4630
+ const registry = path.join(outDir, 'gea_worker_modules.cpp')
4631
+ writeWorkerRegistry([...nativeModules.values()], registry)
4632
+ // The app entry and its native registrations must be in one archive member:
4633
+ // constructor-only members are not extracted by ordinary static linking.
4634
+ const nativeEntry = path.join(outDir, 'gea_native_entry.cpp')
4635
+ fs.writeFileSync(nativeEntry, `#include ${JSON.stringify(sources[0])}\n#include ${JSON.stringify(registry)}\n`)
4636
+ fs.writeFileSync(listPath, [nativeEntry, ...sources.slice(1), ...nativeSources].join('\n') + '\n')
4637
+ }
4638
+
4591
4639
 
4592
4640
  process.stdout.write(`gea-vite-bundle=${bundlePath}\n`)
4593
4641
  if (moduleGraphOutDir) process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
@@ -156,8 +156,16 @@ export function discoverLocalImport(source, srcId, localImports) {
156
156
  if (source.endsWith('.css')) return
157
157
  const base = resolveLocalImportBase(source, srcId)
158
158
  if (!base) return
159
+ // TypeScript's ESM imports name the emitted .js/.mjs/.cjs file while the
160
+ // source tree contains .ts/.mts/.cts. Follow those edges before staging.
161
+ const extension = path.extname(base)
162
+ const sourceExtensions = extension === '.js' ? ['.ts', '.tsx', '.d.ts']
163
+ : extension === '.mjs' ? ['.mts', '.d.mts']
164
+ : extension === '.cjs' ? ['.cts', '.d.cts'] : []
165
+ const stem = base.slice(0, base.length - extension.length)
159
166
  for (const candidate of [
160
167
  base,
168
+ ...sourceExtensions.map((extension) => stem + extension),
161
169
  base + '.tsx',
162
170
  base + '.ts',
163
171
  base + '.jsx',
@@ -177,7 +185,7 @@ export function discoverLocalImport(source, srcId, localImports) {
177
185
  // non-ASCII byte (`wOFF` header → "Unexpected character" at column 4).
178
186
  // Assets aren't parseable modules; skip them (the added-extension
179
187
  // candidates below are always JS/TS, so a genuine module still resolves).
180
- if (!/\.(?:mjs|cjs|jsx?|tsx?)$/i.test(candidate)) break
188
+ if (!/\.(?:[cm]?[jt]s|jsx|tsx)$/i.test(candidate)) break
181
189
  localImports.push(candidate)
182
190
  break
183
191
  }
@@ -5,6 +5,8 @@ export const COMPAT_STAGING_IGNORED_DIRECTORIES = Object.freeze([
5
5
  '.build',
6
6
  '.build-test',
7
7
  '.vite',
8
+ '.gea',
9
+ '.git',
8
10
  '.scratch',
9
11
  '.test-tmp',
10
12
  'generated-output',
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createHash } from 'node:crypto'
4
+
5
+ // One identity for Vite's native module graph and the firmware lookup table.
6
+ export function describeStaticAsset(source) {
7
+ const contents = fs.readFileSync(source)
8
+ const sha256 = createHash('sha256').update(contents).digest('hex')
9
+ return {
10
+ source,
11
+ bytes: contents.length,
12
+ sha256,
13
+ url: `gea-asset://sha256/${sha256}/${encodeURIComponent(path.basename(source))}`,
14
+ }
15
+ }
@@ -1,3 +1,4 @@
1
+ import { describeStaticAsset } from './gea-static-asset.mjs'
1
2
  import crypto from 'node:crypto'
2
3
  import fs from 'node:fs'
3
4
  import path from 'node:path'
@@ -424,20 +425,6 @@ function entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds) {
424
425
  return allIds.filter((id) => reachable.has(id))
425
426
  }
426
427
 
427
- function describeStaticAsset(source) {
428
- const contents = fs.readFileSync(source)
429
- const sha256 = crypto.createHash('sha256').update(contents).digest('hex')
430
- // The hash makes the logical URL collision-safe and stable for unchanged
431
- // bytes. The basename remains available to native bundle adapters, while the
432
- // manifest is the authoritative URL -> source mapping.
433
- const basename = encodeURIComponent(path.basename(source))
434
- return {
435
- source,
436
- bytes: contents.length,
437
- sha256,
438
- url: `gea-asset://sha256/${sha256}/${basename}`,
439
- }
440
- }
441
428
 
442
429
  async function captureResolvedImports(pluginContext, state, id, code) {
443
430
  const normalized = normalizeModuleId(id)