almostnode 0.2.6 → 0.2.8

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/__sw__.js +80 -84
  3. package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
  4. package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
  5. package/dist/frameworks/code-transforms.d.ts +53 -0
  6. package/dist/frameworks/code-transforms.d.ts.map +1 -0
  7. package/dist/frameworks/next-config-parser.d.ts +16 -0
  8. package/dist/frameworks/next-config-parser.d.ts.map +1 -0
  9. package/dist/frameworks/next-dev-server.d.ts +29 -18
  10. package/dist/frameworks/next-dev-server.d.ts.map +1 -1
  11. package/dist/frameworks/next-html-generator.d.ts +35 -0
  12. package/dist/frameworks/next-html-generator.d.ts.map +1 -0
  13. package/dist/frameworks/next-shims.d.ts +79 -0
  14. package/dist/frameworks/next-shims.d.ts.map +1 -0
  15. package/dist/frameworks/vite-dev-server.d.ts +0 -4
  16. package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
  17. package/dist/index.cjs +30392 -9523
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.mjs +27296 -8797
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/runtime.d.ts +20 -0
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/server-bridge.d.ts +2 -0
  26. package/dist/server-bridge.d.ts.map +1 -1
  27. package/dist/shims/crypto.d.ts +2 -0
  28. package/dist/shims/crypto.d.ts.map +1 -1
  29. package/dist/shims/esbuild.d.ts.map +1 -1
  30. package/dist/shims/fs.d.ts.map +1 -1
  31. package/dist/shims/http.d.ts +29 -0
  32. package/dist/shims/http.d.ts.map +1 -1
  33. package/dist/shims/path.d.ts.map +1 -1
  34. package/dist/shims/stream.d.ts.map +1 -1
  35. package/dist/shims/vfs-adapter.d.ts.map +1 -1
  36. package/dist/shims/ws.d.ts +2 -0
  37. package/dist/shims/ws.d.ts.map +1 -1
  38. package/dist/utils/binary-encoding.d.ts +13 -0
  39. package/dist/utils/binary-encoding.d.ts.map +1 -0
  40. package/dist/virtual-fs.d.ts.map +1 -1
  41. package/package.json +8 -4
  42. package/src/convex-app-demo-entry.ts +231 -35
  43. package/src/frameworks/code-transforms.ts +581 -0
  44. package/src/frameworks/next-config-parser.ts +140 -0
  45. package/src/frameworks/next-dev-server.ts +561 -1641
  46. package/src/frameworks/next-html-generator.ts +597 -0
  47. package/src/frameworks/next-shims.ts +1050 -0
  48. package/src/frameworks/tailwind-config-loader.ts +1 -1
  49. package/src/frameworks/vite-dev-server.ts +2 -61
  50. package/src/index.ts +2 -0
  51. package/src/runtime.ts +94 -15
  52. package/src/server-bridge.ts +61 -28
  53. package/src/shims/crypto.ts +13 -0
  54. package/src/shims/esbuild.ts +4 -1
  55. package/src/shims/fs.ts +9 -11
  56. package/src/shims/http.ts +309 -3
  57. package/src/shims/path.ts +6 -13
  58. package/src/shims/stream.ts +12 -26
  59. package/src/shims/vfs-adapter.ts +5 -2
  60. package/src/shims/ws.ts +92 -2
  61. package/src/utils/binary-encoding.ts +43 -0
  62. package/src/virtual-fs.ts +7 -15
  63. package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
package/src/shims/http.ts CHANGED
@@ -6,6 +6,13 @@
6
6
  import { EventEmitter, type EventListener } from './events';
7
7
  import { Readable, Writable, Buffer } from './stream';
8
8
  import { Socket, Server as NetServer, AddressInfo } from './net';
9
+ import { createHash } from './crypto';
10
+
11
+ // Save the browser's native WebSocket at module load time, BEFORE any CLI bundle
12
+ // can overwrite it (e.g. Convex CLI does `globalThis.WebSocket = bundledWs`).
13
+ // This ensures our WebSocket bridge always uses the real browser implementation.
14
+ const _BrowserWebSocket: typeof globalThis.WebSocket | null =
15
+ typeof globalThis.WebSocket === 'function' ? globalThis.WebSocket : null;
9
16
 
10
17
  export type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;
11
18
 
@@ -14,6 +21,7 @@ export interface RequestOptions {
14
21
  path?: string;
15
22
  headers?: Record<string, string | string[]>;
16
23
  hostname?: string;
24
+ host?: string;
17
25
  port?: number;
18
26
  }
19
27
 
@@ -604,13 +612,29 @@ export class ClientRequest extends Writable {
604
612
  if (this._aborted) return;
605
613
 
606
614
  try {
607
- // Build URL
615
+ // Build URL — Node.js supports both `hostname` and `host` (host may include port)
608
616
  const protocol = this._protocol === 'https' ? 'https:' : 'http:';
609
- const hostname = this._options.hostname || 'localhost';
610
- const port = this._options.port ? `:${this._options.port}` : '';
617
+ let hostname = this._options.hostname || '';
618
+ let port = this._options.port ? `:${this._options.port}` : '';
619
+ if (!hostname && this._options.host) {
620
+ // host can be "domain.com" or "domain.com:8080"
621
+ const hostParts = this._options.host.split(':');
622
+ hostname = hostParts[0];
623
+ if (!port && hostParts[1]) {
624
+ port = `:${hostParts[1]}`;
625
+ }
626
+ }
627
+ if (!hostname) hostname = 'localhost';
611
628
  const path = this._options.path || '/';
612
629
  const url = `${protocol}//${hostname}${port}${path}`;
613
630
 
631
+ // WebSocket upgrade requests can't use fetch() — browsers strip
632
+ // Connection/Upgrade headers. Bridge to the browser's native WebSocket.
633
+ if (this.headers['upgrade']?.toLowerCase() === 'websocket') {
634
+ this._handleWebSocketUpgrade(url);
635
+ return;
636
+ }
637
+
614
638
  // Use CORS proxy if configured
615
639
  const corsProxy = getCorsProxy();
616
640
  const fetchUrl = corsProxy
@@ -691,6 +715,181 @@ export class ClientRequest extends Writable {
691
715
 
692
716
  return msg;
693
717
  }
718
+
719
+ /**
720
+ * Bridge a WebSocket upgrade request to the browser's native WebSocket.
721
+ *
722
+ * The bundled ws library (inside the Convex CLI) creates WebSocket connections
723
+ * via http.request() with Upgrade headers. It expects frame-level I/O on the
724
+ * socket from the 'upgrade' event. This method bridges between:
725
+ * - ws library ↔ frame-level I/O on a mock Socket
726
+ * - browser native WebSocket ↔ message-level I/O
727
+ */
728
+ private _handleWebSocketUpgrade(url: string): void {
729
+ // Convert http(s):// to ws(s)://
730
+ const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
731
+
732
+ // Get Sec-WebSocket-Key from request headers (sent by ws library)
733
+ const wsKey = this.headers['sec-websocket-key'] || '';
734
+
735
+ // Use the saved browser WebSocket (captured at module load time before CLI overrides)
736
+ const NativeWS = _BrowserWebSocket;
737
+
738
+ if (!NativeWS) {
739
+ // No native WebSocket (test env / Node.js) — emit TypeError like fetch would
740
+ setTimeout(() => {
741
+ this.emit('error', new TypeError('Failed to fetch'));
742
+ }, 0);
743
+ return;
744
+ }
745
+
746
+ // Compute Sec-WebSocket-Accept using the same hash as the ws library.
747
+ // The ws library (bundled in the Convex CLI) uses require("crypto") which
748
+ // resolves to our crypto shim's createHash (syncHash). We must use the same
749
+ // GUID as the bundled ws@8.18.0 (which differs from the standard RFC 6455 GUID).
750
+ const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
751
+ const acceptValue = createHash('sha1')
752
+ .update(wsKey + GUID)
753
+ .digest('base64') as string;
754
+
755
+ let nativeWs: globalThis.WebSocket;
756
+ try {
757
+ nativeWs = new NativeWS(wsUrl);
758
+ nativeWs.binaryType = 'arraybuffer';
759
+ } catch (e) {
760
+ setTimeout(() => {
761
+ this.emit('error', e instanceof Error ? e : new Error(String(e)));
762
+ }, 0);
763
+ return;
764
+ }
765
+
766
+ // Create mock socket for the ws library's frame-level I/O
767
+ const socket = new Socket();
768
+ // ws library calls cork/uncork for batching — add as no-ops if missing
769
+ if (typeof (socket as any).cork !== 'function') (socket as any).cork = () => {};
770
+ if (typeof (socket as any).uncork !== 'function') (socket as any).uncork = () => {};
771
+ // ws library checks _readableState and _writableState on socket close
772
+ (socket as any)._readableState = { endEmitted: false };
773
+ (socket as any)._writableState = { finished: false, errorEmitted: false };
774
+
775
+ // Buffer for partial frame writes from ws library
776
+ let writeBuffer = new Uint8Array(0);
777
+
778
+ // Override socket.write to intercept outgoing frames from ws library
779
+ socket.write = ((
780
+ chunk: Uint8Array | string,
781
+ encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
782
+ callback?: (error?: Error | null) => void
783
+ ): boolean => {
784
+ const data = typeof chunk === 'string' ? Buffer.from(chunk) : new Uint8Array(chunk);
785
+ const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
786
+
787
+ // Append to write buffer
788
+ const newBuf = new Uint8Array(writeBuffer.length + data.length);
789
+ newBuf.set(writeBuffer, 0);
790
+ newBuf.set(data, writeBuffer.length);
791
+ writeBuffer = newBuf;
792
+
793
+ // Parse and forward complete frames
794
+ while (writeBuffer.length >= 2) {
795
+ const parsed = _parseWsFrame(writeBuffer);
796
+ if (!parsed) break; // incomplete frame — wait for more data
797
+
798
+ const { opcode, payload, totalLength } = parsed;
799
+ writeBuffer = writeBuffer.slice(totalLength);
800
+
801
+ if (nativeWs.readyState !== NativeWS.OPEN) continue;
802
+
803
+ if (opcode === 0x08) {
804
+ // Close frame
805
+ nativeWs.close();
806
+ } else if (opcode === 0x09) {
807
+ // Ping — respond with pong (shouldn't happen from client but handle it)
808
+ nativeWs.send(payload);
809
+ } else if (opcode === 0x0A) {
810
+ // Pong — ignore
811
+ } else if (opcode === 0x01) {
812
+ // Text frame
813
+ const text = new TextDecoder().decode(payload);
814
+ nativeWs.send(text);
815
+ } else if (opcode === 0x02) {
816
+ // Binary frame
817
+ nativeWs.send(payload);
818
+ }
819
+ }
820
+
821
+ if (cb) queueMicrotask(() => cb(null));
822
+ return true;
823
+ }) as any;
824
+
825
+ nativeWs.onopen = () => {
826
+ // Create HTTP 101 response
827
+ const response = new IncomingMessage(socket);
828
+ response.statusCode = 101;
829
+ response.statusMessage = 'Switching Protocols';
830
+ response.headers = {
831
+ 'upgrade': 'websocket',
832
+ 'connection': 'Upgrade',
833
+ 'sec-websocket-accept': acceptValue,
834
+ };
835
+ // Mark as complete so ws library doesn't wait for body
836
+ response.complete = true;
837
+ response.push(null);
838
+
839
+ // Emit upgrade event — ws library listens for this
840
+ this.emit('upgrade', response, socket, Buffer.alloc(0));
841
+ };
842
+
843
+ nativeWs.onmessage = (event: MessageEvent) => {
844
+ // Create unmasked WebSocket frame and push to mock socket
845
+ let payload: Uint8Array;
846
+ let opcode: number;
847
+
848
+ if (typeof event.data === 'string') {
849
+ payload = new TextEncoder().encode(event.data);
850
+ opcode = 0x01; // text
851
+ } else if (event.data instanceof ArrayBuffer) {
852
+ payload = new Uint8Array(event.data);
853
+ opcode = 0x02; // binary
854
+ } else {
855
+ return;
856
+ }
857
+
858
+ const frame = _createWsFrame(opcode, payload, false); // unmasked (server → client)
859
+ socket._receiveData(Buffer.from(frame));
860
+ };
861
+
862
+ nativeWs.onclose = (event: CloseEvent) => {
863
+ // Send close frame to ws library
864
+ const code = event.code || 1000;
865
+ const closePayload = new Uint8Array(2);
866
+ closePayload[0] = (code >> 8) & 0xFF;
867
+ closePayload[1] = code & 0xFF;
868
+ const frame = _createWsFrame(0x08, closePayload, false);
869
+ socket._receiveData(Buffer.from(frame));
870
+
871
+ setTimeout(() => {
872
+ (socket as any)._readableState.endEmitted = true;
873
+ socket._receiveEnd();
874
+ socket.emit('close', false);
875
+ }, 10);
876
+ };
877
+
878
+ nativeWs.onerror = () => {
879
+ socket.emit('error', new Error('WebSocket connection error'));
880
+ socket.destroy();
881
+ };
882
+
883
+ // Clean up native WS when socket is destroyed
884
+ const origDestroy = socket.destroy.bind(socket);
885
+ socket.destroy = ((error?: Error): Socket => {
886
+ if (nativeWs.readyState === NativeWS.OPEN || nativeWs.readyState === NativeWS.CONNECTING) {
887
+ nativeWs.close();
888
+ }
889
+ return origDestroy(error);
890
+ }) as any;
891
+ }
892
+
694
893
  }
695
894
 
696
895
  /**
@@ -884,6 +1083,111 @@ export class Agent extends EventEmitter {
884
1083
  // Global agent instance
885
1084
  export const globalAgent = new Agent();
886
1085
 
1086
+ /**
1087
+ * Parse a WebSocket frame from raw bytes.
1088
+ * Returns null if the buffer doesn't contain a complete frame.
1089
+ */
1090
+ export function _parseWsFrame(data: Uint8Array): {
1091
+ opcode: number;
1092
+ payload: Uint8Array;
1093
+ totalLength: number;
1094
+ } | null {
1095
+ if (data.length < 2) return null;
1096
+
1097
+ const opcode = data[0] & 0x0F;
1098
+ const masked = (data[1] & 0x80) !== 0;
1099
+ let payloadLength = data[1] & 0x7F;
1100
+ let offset = 2;
1101
+
1102
+ if (payloadLength === 126) {
1103
+ if (data.length < 4) return null;
1104
+ payloadLength = (data[2] << 8) | data[3];
1105
+ offset = 4;
1106
+ } else if (payloadLength === 127) {
1107
+ if (data.length < 10) return null;
1108
+ // Use lower 32 bits (sufficient for WebSocket messages)
1109
+ payloadLength = (data[6] << 24) | (data[7] << 16) | (data[8] << 8) | data[9];
1110
+ offset = 10;
1111
+ }
1112
+
1113
+ if (masked) {
1114
+ if (data.length < offset + 4 + payloadLength) return null;
1115
+ const maskKey = data.slice(offset, offset + 4);
1116
+ offset += 4;
1117
+
1118
+ const payload = new Uint8Array(payloadLength);
1119
+ for (let i = 0; i < payloadLength; i++) {
1120
+ payload[i] = data[offset + i] ^ maskKey[i % 4];
1121
+ }
1122
+
1123
+ return { opcode, payload, totalLength: offset + payloadLength };
1124
+ } else {
1125
+ if (data.length < offset + payloadLength) return null;
1126
+ const payload = data.slice(offset, offset + payloadLength);
1127
+ return { opcode, payload, totalLength: offset + payloadLength };
1128
+ }
1129
+ }
1130
+
1131
+ /**
1132
+ * Create a WebSocket frame.
1133
+ * @param opcode - Frame opcode (0x01=text, 0x02=binary, 0x08=close, 0x09=ping, 0x0A=pong)
1134
+ * @param payload - Frame payload
1135
+ * @param masked - Whether to mask the payload (client→server frames are masked)
1136
+ */
1137
+ export function _createWsFrame(opcode: number, payload: Uint8Array, masked: boolean): Uint8Array {
1138
+ const length = payload.length;
1139
+ let headerSize = 2;
1140
+
1141
+ if (length > 125 && length <= 65535) {
1142
+ headerSize += 2;
1143
+ } else if (length > 65535) {
1144
+ headerSize += 8;
1145
+ }
1146
+
1147
+ if (masked) {
1148
+ headerSize += 4;
1149
+ }
1150
+
1151
+ const frame = new Uint8Array(headerSize + length);
1152
+ frame[0] = 0x80 | opcode; // FIN + opcode
1153
+
1154
+ let offset = 2;
1155
+ if (length <= 125) {
1156
+ frame[1] = (masked ? 0x80 : 0) | length;
1157
+ } else if (length <= 65535) {
1158
+ frame[1] = (masked ? 0x80 : 0) | 126;
1159
+ frame[2] = (length >> 8) & 0xFF;
1160
+ frame[3] = length & 0xFF;
1161
+ offset = 4;
1162
+ } else {
1163
+ frame[1] = (masked ? 0x80 : 0) | 127;
1164
+ frame[2] = 0; frame[3] = 0; frame[4] = 0; frame[5] = 0;
1165
+ frame[6] = (length >> 24) & 0xFF;
1166
+ frame[7] = (length >> 16) & 0xFF;
1167
+ frame[8] = (length >> 8) & 0xFF;
1168
+ frame[9] = length & 0xFF;
1169
+ offset = 10;
1170
+ }
1171
+
1172
+ if (masked) {
1173
+ const maskKey = new Uint8Array(4);
1174
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
1175
+ crypto.getRandomValues(maskKey);
1176
+ } else {
1177
+ for (let i = 0; i < 4; i++) maskKey[i] = Math.floor(Math.random() * 256);
1178
+ }
1179
+ frame.set(maskKey, offset);
1180
+ offset += 4;
1181
+ for (let i = 0; i < length; i++) {
1182
+ frame[offset + i] = payload[i] ^ maskKey[i % 4];
1183
+ }
1184
+ } else {
1185
+ frame.set(payload, offset);
1186
+ }
1187
+
1188
+ return frame;
1189
+ }
1190
+
887
1191
  export default {
888
1192
  Server,
889
1193
  IncomingMessage,
@@ -901,4 +1205,6 @@ export default {
901
1205
  _createClientRequest,
902
1206
  Agent,
903
1207
  globalAgent,
1208
+ _parseWsFrame,
1209
+ _createWsFrame,
904
1210
  };
package/src/shims/path.ts CHANGED
@@ -33,20 +33,9 @@ export function normalize(path: string): string {
33
33
  return result || '.';
34
34
  }
35
35
 
36
- // Track join calls to debug infinite recursion
37
- let joinCallCount = 0;
38
-
39
36
  export function join(...paths: string[]): string {
40
37
  if (paths.length === 0) return '.';
41
- const result = normalize(paths.filter(Boolean).join('/'));
42
- // Debug: Log _generated path joins
43
- if (paths.some(p => p && p.includes('_generated'))) {
44
- joinCallCount++;
45
- if (joinCallCount <= 20) {
46
- console.log(`[path.join] (${paths.join(', ')}) -> ${result}`);
47
- }
48
- }
49
- return result;
38
+ return normalize(paths.filter(Boolean).join('/'));
50
39
  }
51
40
 
52
41
  export function resolve(...paths: string[]): string {
@@ -59,7 +48,11 @@ export function resolve(...paths: string[]): string {
59
48
  }
60
49
 
61
50
  if (!resolvedPath.startsWith('/')) {
62
- resolvedPath = '/' + resolvedPath;
51
+ // Use process.cwd() if available, matching Node.js behavior
52
+ const cwd = typeof globalThis !== 'undefined' && globalThis.process && typeof globalThis.process.cwd === 'function'
53
+ ? globalThis.process.cwd()
54
+ : '/';
55
+ resolvedPath = cwd + (resolvedPath ? '/' + resolvedPath : '');
63
56
  }
64
57
 
65
58
  return normalize(resolvedPath);
@@ -4,6 +4,10 @@
4
4
  */
5
5
 
6
6
  import { EventEmitter } from './events';
7
+ import { uint8ToBase64, uint8ToHex, uint8ToBinaryString } from '../utils/binary-encoding';
8
+
9
+ const _encoder = new TextEncoder();
10
+ const _decoder = new TextDecoder('utf-8');
7
11
 
8
12
  export class Readable extends EventEmitter {
9
13
  private _buffer: Uint8Array[] = [];
@@ -547,8 +551,7 @@ class BufferPolyfill extends Uint8Array {
547
551
  }
548
552
 
549
553
  // Default: utf8
550
- const encoder = new TextEncoder();
551
- const bytes = encoder.encode(data);
554
+ const bytes = _encoder.encode(data);
552
555
  return new BufferPolyfill(bytes);
553
556
  }
554
557
  if (data instanceof ArrayBuffer) {
@@ -602,47 +605,30 @@ class BufferPolyfill extends Uint8Array {
602
605
  if (enc === 'hex') {
603
606
  return string.length / 2;
604
607
  }
605
- return new TextEncoder().encode(string).length;
608
+ return _encoder.encode(string).length;
606
609
  }
607
610
 
608
611
  toString(encoding: BufferEncoding = 'utf8'): string {
609
612
  const enc = (encoding || 'utf8').toLowerCase();
610
613
 
611
614
  if (enc === 'base64') {
612
- let binary = '';
613
- for (let i = 0; i < this.length; i++) {
614
- binary += String.fromCharCode(this[i]);
615
- }
616
- return btoa(binary);
615
+ return uint8ToBase64(this);
617
616
  }
618
617
 
619
618
  if (enc === 'base64url') {
620
- let binary = '';
621
- for (let i = 0; i < this.length; i++) {
622
- binary += String.fromCharCode(this[i]);
623
- }
624
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
619
+ return uint8ToBase64(this).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
625
620
  }
626
621
 
627
622
  if (enc === 'hex') {
628
- let hex = '';
629
- for (let i = 0; i < this.length; i++) {
630
- hex += this[i].toString(16).padStart(2, '0');
631
- }
632
- return hex;
623
+ return uint8ToHex(this);
633
624
  }
634
625
 
635
626
  if (enc === 'latin1' || enc === 'binary') {
636
- let str = '';
637
- for (let i = 0; i < this.length; i++) {
638
- str += String.fromCharCode(this[i]);
639
- }
640
- return str;
627
+ return uint8ToBinaryString(this);
641
628
  }
642
629
 
643
630
  // Default: utf8
644
- const decoder = new TextDecoder('utf-8');
645
- return decoder.decode(this);
631
+ return _decoder.decode(this);
646
632
  }
647
633
 
648
634
  slice(start?: number, end?: number): BufferPolyfill {
@@ -654,7 +640,7 @@ class BufferPolyfill extends Uint8Array {
654
640
  }
655
641
 
656
642
  write(string: string, offset?: number): number {
657
- const bytes = new TextEncoder().encode(string);
643
+ const bytes = _encoder.encode(string);
658
644
  this.set(bytes, offset || 0);
659
645
  return bytes.length;
660
646
  }
@@ -14,6 +14,9 @@ import type {
14
14
  } from 'just-bash';
15
15
  import type { VirtualFS } from '../virtual-fs';
16
16
  import { createNodeError } from '../virtual-fs';
17
+ import { uint8ToBinaryString } from '../utils/binary-encoding';
18
+
19
+ const _decoder = new TextDecoder();
17
20
 
18
21
  // Local types for just-bash interface compatibility
19
22
  // These are not exported from just-bash main entry point
@@ -53,7 +56,7 @@ export class VirtualFSAdapter implements IFileSystem {
53
56
  // For binary/latin1 encoding, convert each byte to a character
54
57
  if (encoding === 'binary' || encoding === 'latin1') {
55
58
  const buffer = this.vfs.readFileSync(path);
56
- return String.fromCharCode(...buffer);
59
+ return uint8ToBinaryString(buffer);
57
60
  }
58
61
 
59
62
  // For other encodings, fall back to utf8
@@ -93,7 +96,7 @@ export class VirtualFSAdapter implements IFileSystem {
93
96
  // File doesn't exist, start with empty content
94
97
  }
95
98
  const newContent =
96
- typeof content === 'string' ? content : new TextDecoder().decode(content);
99
+ typeof content === 'string' ? content : _decoder.decode(content);
97
100
  this.vfs.writeFileSync(path, existing + newContent);
98
101
  }
99
102
 
package/src/shims/ws.ts CHANGED
@@ -59,6 +59,7 @@ export class WebSocket extends EventEmitter {
59
59
 
60
60
  private _id: string;
61
61
  private _server: WebSocketServer | null = null;
62
+ private _nativeWs: globalThis.WebSocket | null = null;
62
63
 
63
64
  // Event handler properties
64
65
  onopen: ((event: Event) => void) | null = null;
@@ -88,7 +89,15 @@ export class WebSocket extends EventEmitter {
88
89
  return;
89
90
  }
90
91
 
91
- // If no BroadcastChannel, act as if connected
92
+ // For external WebSocket connections, use the browser's native WebSocket.
93
+ // This allows libraries like the Convex CLI (which require('ws')) to
94
+ // communicate with real remote servers.
95
+ if (this.url.startsWith('ws://') || this.url.startsWith('wss://')) {
96
+ this._connectNative();
97
+ return;
98
+ }
99
+
100
+ // For all other URLs, use BroadcastChannel (internal Vite HMR)
92
101
  if (!messageChannel) {
93
102
  setTimeout(() => {
94
103
  this.readyState = WebSocket.OPEN;
@@ -158,11 +167,75 @@ export class WebSocket extends EventEmitter {
158
167
  }, 100);
159
168
  }
160
169
 
170
+ private _connectNative(): void {
171
+ // Check that the browser's native WebSocket is available and is not our own shim
172
+ const NativeWS = typeof globalThis.WebSocket === 'function' && globalThis.WebSocket !== (WebSocket as any)
173
+ ? globalThis.WebSocket
174
+ : null;
175
+
176
+ if (!NativeWS) {
177
+ // No native WebSocket (test env, Node.js, etc.) — act as if connected
178
+ setTimeout(() => {
179
+ this.readyState = WebSocket.OPEN;
180
+ this.emit('open');
181
+ if (this.onopen) this.onopen(new Event('open'));
182
+ }, 0);
183
+ return;
184
+ }
185
+
186
+ try {
187
+ this._nativeWs = new NativeWS(this.url);
188
+ this._nativeWs.binaryType = this.binaryType === 'arraybuffer' ? 'arraybuffer' : 'blob';
189
+ } catch {
190
+ this.readyState = WebSocket.CLOSED;
191
+ const errorEvent = new Event('error');
192
+ this.emit('error', errorEvent);
193
+ if (this.onerror) this.onerror(errorEvent);
194
+ return;
195
+ }
196
+
197
+ this._nativeWs.onopen = () => {
198
+ this.readyState = WebSocket.OPEN;
199
+ this.emit('open');
200
+ if (this.onopen) this.onopen(new Event('open'));
201
+ };
202
+
203
+ this._nativeWs.onmessage = (event: globalThis.MessageEvent) => {
204
+ const msgEvent = new MessageEventPolyfill('message', { data: event.data });
205
+ this.emit('message', msgEvent);
206
+ if (this.onmessage) this.onmessage(msgEvent as unknown as MessageEvent);
207
+ };
208
+
209
+ this._nativeWs.onclose = (event: globalThis.CloseEvent) => {
210
+ this.readyState = WebSocket.CLOSED;
211
+ this._nativeWs = null;
212
+ const closeEvent = new CloseEventPolyfill('close', {
213
+ code: event.code,
214
+ reason: event.reason,
215
+ wasClean: event.wasClean,
216
+ });
217
+ this.emit('close', closeEvent);
218
+ if (this.onclose) this.onclose(closeEvent as unknown as CloseEvent);
219
+ };
220
+
221
+ this._nativeWs.onerror = () => {
222
+ const errorEvent = new Event('error');
223
+ this.emit('error', errorEvent);
224
+ if (this.onerror) this.onerror(errorEvent);
225
+ };
226
+ }
227
+
161
228
  send(data: string | ArrayBuffer | Uint8Array): void {
162
229
  if (this.readyState !== WebSocket.OPEN) {
163
230
  throw new Error('WebSocket is not open');
164
231
  }
165
232
 
233
+ // If connected to native WebSocket (external server)
234
+ if (this._nativeWs) {
235
+ this._nativeWs.send(data);
236
+ return;
237
+ }
238
+
166
239
  // If connected to internal server
167
240
  if (this._server) {
168
241
  this._server._handleClientMessage(this, data);
@@ -187,6 +260,12 @@ export class WebSocket extends EventEmitter {
187
260
 
188
261
  this.readyState = WebSocket.CLOSING;
189
262
 
263
+ // If connected to native WebSocket, close it (onclose handler emits events)
264
+ if (this._nativeWs) {
265
+ this._nativeWs.close(code, reason);
266
+ return;
267
+ }
268
+
190
269
  if (messageChannel) {
191
270
  messageChannel.postMessage({
192
271
  type: 'disconnect',
@@ -218,7 +297,18 @@ export class WebSocket extends EventEmitter {
218
297
  }
219
298
 
220
299
  terminate(): void {
221
- this.close(1006, 'Connection terminated');
300
+ if (this._nativeWs) {
301
+ this._nativeWs.close();
302
+ this._nativeWs = null;
303
+ }
304
+ this.readyState = WebSocket.CLOSED;
305
+ const closeEvent = new CloseEventPolyfill('close', {
306
+ code: 1006,
307
+ reason: 'Connection terminated',
308
+ wasClean: false,
309
+ });
310
+ this.emit('close', closeEvent);
311
+ if (this.onclose) this.onclose(closeEvent as unknown as CloseEvent);
222
312
  }
223
313
 
224
314
  // For internal server use
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared binary encoding utilities.
3
+ * Replaces O(n²) string concatenation patterns used throughout the codebase.
4
+ */
5
+
6
+ const CHUNK = 8192;
7
+
8
+ /** Convert Uint8Array to base64 string */
9
+ export function uint8ToBase64(bytes: Uint8Array): string {
10
+ const parts: string[] = [];
11
+ for (let i = 0; i < bytes.length; i += CHUNK) {
12
+ parts.push(String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK))));
13
+ }
14
+ return btoa(parts.join(''));
15
+ }
16
+
17
+ /** Convert base64 string to Uint8Array */
18
+ export function base64ToUint8(base64: string): Uint8Array {
19
+ const binary = atob(base64);
20
+ const bytes = new Uint8Array(binary.length);
21
+ for (let i = 0; i < binary.length; i++) {
22
+ bytes[i] = binary.charCodeAt(i);
23
+ }
24
+ return bytes;
25
+ }
26
+
27
+ /** Convert Uint8Array to hex string */
28
+ export function uint8ToHex(bytes: Uint8Array): string {
29
+ const hex = new Array(bytes.length);
30
+ for (let i = 0; i < bytes.length; i++) {
31
+ hex[i] = bytes[i].toString(16).padStart(2, '0');
32
+ }
33
+ return hex.join('');
34
+ }
35
+
36
+ /** Convert Uint8Array to binary (latin1) string */
37
+ export function uint8ToBinaryString(bytes: Uint8Array): string {
38
+ const parts: string[] = [];
39
+ for (let i = 0; i < bytes.length; i += CHUNK) {
40
+ parts.push(String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK))));
41
+ }
42
+ return parts.join('');
43
+ }