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.
- package/README.md +1 -1
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
- package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts +53 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -0
- package/dist/frameworks/next-config-parser.d.ts +16 -0
- package/dist/frameworks/next-config-parser.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +29 -18
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/next-html-generator.d.ts +35 -0
- package/dist/frameworks/next-html-generator.d.ts.map +1 -0
- package/dist/frameworks/next-shims.d.ts +79 -0
- package/dist/frameworks/next-shims.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +0 -4
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
- package/dist/index.cjs +30392 -9523
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27296 -8797
- package/dist/index.mjs.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +2 -0
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/shims/crypto.d.ts +2 -0
- package/dist/shims/crypto.d.ts.map +1 -1
- package/dist/shims/esbuild.d.ts.map +1 -1
- package/dist/shims/fs.d.ts.map +1 -1
- package/dist/shims/http.d.ts +29 -0
- package/dist/shims/http.d.ts.map +1 -1
- package/dist/shims/path.d.ts.map +1 -1
- package/dist/shims/stream.d.ts.map +1 -1
- package/dist/shims/vfs-adapter.d.ts.map +1 -1
- package/dist/shims/ws.d.ts +2 -0
- package/dist/shims/ws.d.ts.map +1 -1
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/convex-app-demo-entry.ts +231 -35
- package/src/frameworks/code-transforms.ts +581 -0
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +561 -1641
- package/src/frameworks/next-html-generator.ts +597 -0
- package/src/frameworks/next-shims.ts +1050 -0
- package/src/frameworks/tailwind-config-loader.ts +1 -1
- package/src/frameworks/vite-dev-server.ts +2 -61
- package/src/index.ts +2 -0
- package/src/runtime.ts +94 -15
- package/src/server-bridge.ts +61 -28
- package/src/shims/crypto.ts +13 -0
- package/src/shims/esbuild.ts +4 -1
- package/src/shims/fs.ts +9 -11
- package/src/shims/http.ts +309 -3
- package/src/shims/path.ts +6 -13
- package/src/shims/stream.ts +12 -26
- package/src/shims/vfs-adapter.ts +5 -2
- package/src/shims/ws.ts +92 -2
- package/src/utils/binary-encoding.ts +43 -0
- package/src/virtual-fs.ts +7 -15
- 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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/shims/stream.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
643
|
+
const bytes = _encoder.encode(string);
|
|
658
644
|
this.set(bytes, offset || 0);
|
|
659
645
|
return bytes.length;
|
|
660
646
|
}
|
package/src/shims/vfs-adapter.ts
CHANGED
|
@@ -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
|
|
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 :
|
|
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
|
-
//
|
|
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.
|
|
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
|
+
}
|