almostnode 0.2.7 → 0.2.9
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 +4 -2
- package/dist/CNAME +1 -0
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-ujGAG2t7.js} +1278 -828
- package/dist/assets/runtime-worker-ujGAG2t7.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -1
- 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 +6 -6
- 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/index.cjs +3024 -2465
- 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 +3336 -2787
- package/dist/index.mjs.map +1 -1
- package/dist/og-image.png +0 -0
- package/dist/runtime.d.ts +26 -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/types/package-json.d.ts +1 -0
- package/dist/types/package-json.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 +4 -4
- package/src/convex-app-demo-entry.ts +229 -35
- package/src/frameworks/code-transforms.ts +5 -1
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +76 -1675
- 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/index.ts +2 -0
- package/src/runtime.ts +271 -25
- 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 +312 -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 +95 -2
- package/src/types/package-json.ts +1 -0
- 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/fs.ts
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { VirtualFS, createNodeError } from '../virtual-fs';
|
|
7
7
|
import type { Stats, FSWatcher, WatchListener, WatchEventType } from '../virtual-fs';
|
|
8
|
+
import { uint8ToBase64, uint8ToHex } from '../utils/binary-encoding';
|
|
8
9
|
|
|
9
10
|
export type { Stats, FSWatcher, WatchListener, WatchEventType };
|
|
10
11
|
|
|
12
|
+
const _decoder = new TextDecoder();
|
|
13
|
+
const _encoder = new TextEncoder();
|
|
14
|
+
|
|
11
15
|
export interface FsShim {
|
|
12
16
|
readFileSync(path: string): Buffer;
|
|
13
17
|
readFileSync(path: string, encoding: 'utf8' | 'utf-8'): string;
|
|
@@ -131,19 +135,13 @@ function createBuffer(data: Uint8Array): Buffer {
|
|
|
131
135
|
Object.defineProperty(buffer, 'toString', {
|
|
132
136
|
value: function (encoding?: string) {
|
|
133
137
|
if (encoding === 'utf8' || encoding === 'utf-8' || !encoding) {
|
|
134
|
-
return
|
|
138
|
+
return _decoder.decode(this);
|
|
135
139
|
}
|
|
136
140
|
if (encoding === 'base64') {
|
|
137
|
-
|
|
138
|
-
for (let i = 0; i < this.length; i++) {
|
|
139
|
-
binary += String.fromCharCode(this[i]);
|
|
140
|
-
}
|
|
141
|
-
return btoa(binary);
|
|
141
|
+
return uint8ToBase64(this);
|
|
142
142
|
}
|
|
143
143
|
if (encoding === 'hex') {
|
|
144
|
-
return
|
|
145
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
146
|
-
.join('');
|
|
144
|
+
return uint8ToHex(this);
|
|
147
145
|
}
|
|
148
146
|
throw new Error(`Unsupported encoding: ${encoding}`);
|
|
149
147
|
},
|
|
@@ -437,7 +435,7 @@ export function createFsShim(vfs: VirtualFS, getCwd?: () => string): FsShim {
|
|
|
437
435
|
throw err;
|
|
438
436
|
}
|
|
439
437
|
// Convert string to Uint8Array if needed
|
|
440
|
-
const bytes = typeof data === 'string' ?
|
|
438
|
+
const bytes = typeof data === 'string' ? _encoder.encode(data) : data;
|
|
441
439
|
// Replace entire content
|
|
442
440
|
entry.content = new Uint8Array(bytes);
|
|
443
441
|
entry.position = bytes.length;
|
|
@@ -620,7 +618,7 @@ export function createFsShim(vfs: VirtualFS, getCwd?: () => string): FsShim {
|
|
|
620
618
|
// Handle string input
|
|
621
619
|
let data: Uint8Array;
|
|
622
620
|
if (typeof buffer === 'string') {
|
|
623
|
-
data =
|
|
621
|
+
data = _encoder.encode(buffer);
|
|
624
622
|
offset = 0;
|
|
625
623
|
length = data.length;
|
|
626
624
|
} else {
|
package/src/shims/http.ts
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
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
|
+
// Only capture in a real browser — Node.js 21+ has native WebSocket but it connects
|
|
15
|
+
// to real servers, which isn't what the shim needs.
|
|
16
|
+
const _isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
17
|
+
const _BrowserWebSocket: typeof globalThis.WebSocket | null =
|
|
18
|
+
_isBrowser && typeof globalThis.WebSocket === 'function' ? globalThis.WebSocket : null;
|
|
9
19
|
|
|
10
20
|
export type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;
|
|
11
21
|
|
|
@@ -14,6 +24,7 @@ export interface RequestOptions {
|
|
|
14
24
|
path?: string;
|
|
15
25
|
headers?: Record<string, string | string[]>;
|
|
16
26
|
hostname?: string;
|
|
27
|
+
host?: string;
|
|
17
28
|
port?: number;
|
|
18
29
|
}
|
|
19
30
|
|
|
@@ -604,13 +615,29 @@ export class ClientRequest extends Writable {
|
|
|
604
615
|
if (this._aborted) return;
|
|
605
616
|
|
|
606
617
|
try {
|
|
607
|
-
// Build URL
|
|
618
|
+
// Build URL — Node.js supports both `hostname` and `host` (host may include port)
|
|
608
619
|
const protocol = this._protocol === 'https' ? 'https:' : 'http:';
|
|
609
|
-
|
|
610
|
-
|
|
620
|
+
let hostname = this._options.hostname || '';
|
|
621
|
+
let port = this._options.port ? `:${this._options.port}` : '';
|
|
622
|
+
if (!hostname && this._options.host) {
|
|
623
|
+
// host can be "domain.com" or "domain.com:8080"
|
|
624
|
+
const hostParts = this._options.host.split(':');
|
|
625
|
+
hostname = hostParts[0];
|
|
626
|
+
if (!port && hostParts[1]) {
|
|
627
|
+
port = `:${hostParts[1]}`;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (!hostname) hostname = 'localhost';
|
|
611
631
|
const path = this._options.path || '/';
|
|
612
632
|
const url = `${protocol}//${hostname}${port}${path}`;
|
|
613
633
|
|
|
634
|
+
// WebSocket upgrade requests can't use fetch() — browsers strip
|
|
635
|
+
// Connection/Upgrade headers. Bridge to the browser's native WebSocket.
|
|
636
|
+
if (this.headers['upgrade']?.toLowerCase() === 'websocket') {
|
|
637
|
+
this._handleWebSocketUpgrade(url);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
614
641
|
// Use CORS proxy if configured
|
|
615
642
|
const corsProxy = getCorsProxy();
|
|
616
643
|
const fetchUrl = corsProxy
|
|
@@ -691,6 +718,181 @@ export class ClientRequest extends Writable {
|
|
|
691
718
|
|
|
692
719
|
return msg;
|
|
693
720
|
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Bridge a WebSocket upgrade request to the browser's native WebSocket.
|
|
724
|
+
*
|
|
725
|
+
* The bundled ws library (inside the Convex CLI) creates WebSocket connections
|
|
726
|
+
* via http.request() with Upgrade headers. It expects frame-level I/O on the
|
|
727
|
+
* socket from the 'upgrade' event. This method bridges between:
|
|
728
|
+
* - ws library ↔ frame-level I/O on a mock Socket
|
|
729
|
+
* - browser native WebSocket ↔ message-level I/O
|
|
730
|
+
*/
|
|
731
|
+
private _handleWebSocketUpgrade(url: string): void {
|
|
732
|
+
// Convert http(s):// to ws(s)://
|
|
733
|
+
const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
|
|
734
|
+
|
|
735
|
+
// Get Sec-WebSocket-Key from request headers (sent by ws library)
|
|
736
|
+
const wsKey = this.headers['sec-websocket-key'] || '';
|
|
737
|
+
|
|
738
|
+
// Use the saved browser WebSocket (captured at module load time before CLI overrides)
|
|
739
|
+
const NativeWS = _BrowserWebSocket;
|
|
740
|
+
|
|
741
|
+
if (!NativeWS) {
|
|
742
|
+
// No native WebSocket (test env / Node.js) — emit TypeError like fetch would
|
|
743
|
+
setTimeout(() => {
|
|
744
|
+
this.emit('error', new TypeError('Failed to fetch'));
|
|
745
|
+
}, 0);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Compute Sec-WebSocket-Accept using the same hash as the ws library.
|
|
750
|
+
// The ws library (bundled in the Convex CLI) uses require("crypto") which
|
|
751
|
+
// resolves to our crypto shim's createHash (syncHash). We must use the same
|
|
752
|
+
// GUID as the bundled ws@8.18.0 (which differs from the standard RFC 6455 GUID).
|
|
753
|
+
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
754
|
+
const acceptValue = createHash('sha1')
|
|
755
|
+
.update(wsKey + GUID)
|
|
756
|
+
.digest('base64') as string;
|
|
757
|
+
|
|
758
|
+
let nativeWs: globalThis.WebSocket;
|
|
759
|
+
try {
|
|
760
|
+
nativeWs = new NativeWS(wsUrl);
|
|
761
|
+
nativeWs.binaryType = 'arraybuffer';
|
|
762
|
+
} catch (e) {
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
this.emit('error', e instanceof Error ? e : new Error(String(e)));
|
|
765
|
+
}, 0);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Create mock socket for the ws library's frame-level I/O
|
|
770
|
+
const socket = new Socket();
|
|
771
|
+
// ws library calls cork/uncork for batching — add as no-ops if missing
|
|
772
|
+
if (typeof (socket as any).cork !== 'function') (socket as any).cork = () => {};
|
|
773
|
+
if (typeof (socket as any).uncork !== 'function') (socket as any).uncork = () => {};
|
|
774
|
+
// ws library checks _readableState and _writableState on socket close
|
|
775
|
+
(socket as any)._readableState = { endEmitted: false };
|
|
776
|
+
(socket as any)._writableState = { finished: false, errorEmitted: false };
|
|
777
|
+
|
|
778
|
+
// Buffer for partial frame writes from ws library
|
|
779
|
+
let writeBuffer = new Uint8Array(0);
|
|
780
|
+
|
|
781
|
+
// Override socket.write to intercept outgoing frames from ws library
|
|
782
|
+
socket.write = ((
|
|
783
|
+
chunk: Uint8Array | string,
|
|
784
|
+
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
|
|
785
|
+
callback?: (error?: Error | null) => void
|
|
786
|
+
): boolean => {
|
|
787
|
+
const data = typeof chunk === 'string' ? Buffer.from(chunk) : new Uint8Array(chunk);
|
|
788
|
+
const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
789
|
+
|
|
790
|
+
// Append to write buffer
|
|
791
|
+
const newBuf = new Uint8Array(writeBuffer.length + data.length);
|
|
792
|
+
newBuf.set(writeBuffer, 0);
|
|
793
|
+
newBuf.set(data, writeBuffer.length);
|
|
794
|
+
writeBuffer = newBuf;
|
|
795
|
+
|
|
796
|
+
// Parse and forward complete frames
|
|
797
|
+
while (writeBuffer.length >= 2) {
|
|
798
|
+
const parsed = _parseWsFrame(writeBuffer);
|
|
799
|
+
if (!parsed) break; // incomplete frame — wait for more data
|
|
800
|
+
|
|
801
|
+
const { opcode, payload, totalLength } = parsed;
|
|
802
|
+
writeBuffer = writeBuffer.slice(totalLength);
|
|
803
|
+
|
|
804
|
+
if (nativeWs.readyState !== NativeWS.OPEN) continue;
|
|
805
|
+
|
|
806
|
+
if (opcode === 0x08) {
|
|
807
|
+
// Close frame
|
|
808
|
+
nativeWs.close();
|
|
809
|
+
} else if (opcode === 0x09) {
|
|
810
|
+
// Ping — respond with pong (shouldn't happen from client but handle it)
|
|
811
|
+
nativeWs.send(payload);
|
|
812
|
+
} else if (opcode === 0x0A) {
|
|
813
|
+
// Pong — ignore
|
|
814
|
+
} else if (opcode === 0x01) {
|
|
815
|
+
// Text frame
|
|
816
|
+
const text = new TextDecoder().decode(payload);
|
|
817
|
+
nativeWs.send(text);
|
|
818
|
+
} else if (opcode === 0x02) {
|
|
819
|
+
// Binary frame
|
|
820
|
+
nativeWs.send(payload);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (cb) queueMicrotask(() => cb(null));
|
|
825
|
+
return true;
|
|
826
|
+
}) as any;
|
|
827
|
+
|
|
828
|
+
nativeWs.onopen = () => {
|
|
829
|
+
// Create HTTP 101 response
|
|
830
|
+
const response = new IncomingMessage(socket);
|
|
831
|
+
response.statusCode = 101;
|
|
832
|
+
response.statusMessage = 'Switching Protocols';
|
|
833
|
+
response.headers = {
|
|
834
|
+
'upgrade': 'websocket',
|
|
835
|
+
'connection': 'Upgrade',
|
|
836
|
+
'sec-websocket-accept': acceptValue,
|
|
837
|
+
};
|
|
838
|
+
// Mark as complete so ws library doesn't wait for body
|
|
839
|
+
response.complete = true;
|
|
840
|
+
response.push(null);
|
|
841
|
+
|
|
842
|
+
// Emit upgrade event — ws library listens for this
|
|
843
|
+
this.emit('upgrade', response, socket, Buffer.alloc(0));
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
nativeWs.onmessage = (event: MessageEvent) => {
|
|
847
|
+
// Create unmasked WebSocket frame and push to mock socket
|
|
848
|
+
let payload: Uint8Array;
|
|
849
|
+
let opcode: number;
|
|
850
|
+
|
|
851
|
+
if (typeof event.data === 'string') {
|
|
852
|
+
payload = new TextEncoder().encode(event.data);
|
|
853
|
+
opcode = 0x01; // text
|
|
854
|
+
} else if (event.data instanceof ArrayBuffer) {
|
|
855
|
+
payload = new Uint8Array(event.data);
|
|
856
|
+
opcode = 0x02; // binary
|
|
857
|
+
} else {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const frame = _createWsFrame(opcode, payload, false); // unmasked (server → client)
|
|
862
|
+
socket._receiveData(Buffer.from(frame));
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
nativeWs.onclose = (event: CloseEvent) => {
|
|
866
|
+
// Send close frame to ws library
|
|
867
|
+
const code = event.code || 1000;
|
|
868
|
+
const closePayload = new Uint8Array(2);
|
|
869
|
+
closePayload[0] = (code >> 8) & 0xFF;
|
|
870
|
+
closePayload[1] = code & 0xFF;
|
|
871
|
+
const frame = _createWsFrame(0x08, closePayload, false);
|
|
872
|
+
socket._receiveData(Buffer.from(frame));
|
|
873
|
+
|
|
874
|
+
setTimeout(() => {
|
|
875
|
+
(socket as any)._readableState.endEmitted = true;
|
|
876
|
+
socket._receiveEnd();
|
|
877
|
+
socket.emit('close', false);
|
|
878
|
+
}, 10);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
nativeWs.onerror = () => {
|
|
882
|
+
socket.emit('error', new Error('WebSocket connection error'));
|
|
883
|
+
socket.destroy();
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// Clean up native WS when socket is destroyed
|
|
887
|
+
const origDestroy = socket.destroy.bind(socket);
|
|
888
|
+
socket.destroy = ((error?: Error): Socket => {
|
|
889
|
+
if (nativeWs.readyState === NativeWS.OPEN || nativeWs.readyState === NativeWS.CONNECTING) {
|
|
890
|
+
nativeWs.close();
|
|
891
|
+
}
|
|
892
|
+
return origDestroy(error);
|
|
893
|
+
}) as any;
|
|
894
|
+
}
|
|
895
|
+
|
|
694
896
|
}
|
|
695
897
|
|
|
696
898
|
/**
|
|
@@ -884,6 +1086,111 @@ export class Agent extends EventEmitter {
|
|
|
884
1086
|
// Global agent instance
|
|
885
1087
|
export const globalAgent = new Agent();
|
|
886
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* Parse a WebSocket frame from raw bytes.
|
|
1091
|
+
* Returns null if the buffer doesn't contain a complete frame.
|
|
1092
|
+
*/
|
|
1093
|
+
export function _parseWsFrame(data: Uint8Array): {
|
|
1094
|
+
opcode: number;
|
|
1095
|
+
payload: Uint8Array;
|
|
1096
|
+
totalLength: number;
|
|
1097
|
+
} | null {
|
|
1098
|
+
if (data.length < 2) return null;
|
|
1099
|
+
|
|
1100
|
+
const opcode = data[0] & 0x0F;
|
|
1101
|
+
const masked = (data[1] & 0x80) !== 0;
|
|
1102
|
+
let payloadLength = data[1] & 0x7F;
|
|
1103
|
+
let offset = 2;
|
|
1104
|
+
|
|
1105
|
+
if (payloadLength === 126) {
|
|
1106
|
+
if (data.length < 4) return null;
|
|
1107
|
+
payloadLength = (data[2] << 8) | data[3];
|
|
1108
|
+
offset = 4;
|
|
1109
|
+
} else if (payloadLength === 127) {
|
|
1110
|
+
if (data.length < 10) return null;
|
|
1111
|
+
// Use lower 32 bits (sufficient for WebSocket messages)
|
|
1112
|
+
payloadLength = (data[6] << 24) | (data[7] << 16) | (data[8] << 8) | data[9];
|
|
1113
|
+
offset = 10;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (masked) {
|
|
1117
|
+
if (data.length < offset + 4 + payloadLength) return null;
|
|
1118
|
+
const maskKey = data.slice(offset, offset + 4);
|
|
1119
|
+
offset += 4;
|
|
1120
|
+
|
|
1121
|
+
const payload = new Uint8Array(payloadLength);
|
|
1122
|
+
for (let i = 0; i < payloadLength; i++) {
|
|
1123
|
+
payload[i] = data[offset + i] ^ maskKey[i % 4];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return { opcode, payload, totalLength: offset + payloadLength };
|
|
1127
|
+
} else {
|
|
1128
|
+
if (data.length < offset + payloadLength) return null;
|
|
1129
|
+
const payload = data.slice(offset, offset + payloadLength);
|
|
1130
|
+
return { opcode, payload, totalLength: offset + payloadLength };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Create a WebSocket frame.
|
|
1136
|
+
* @param opcode - Frame opcode (0x01=text, 0x02=binary, 0x08=close, 0x09=ping, 0x0A=pong)
|
|
1137
|
+
* @param payload - Frame payload
|
|
1138
|
+
* @param masked - Whether to mask the payload (client→server frames are masked)
|
|
1139
|
+
*/
|
|
1140
|
+
export function _createWsFrame(opcode: number, payload: Uint8Array, masked: boolean): Uint8Array {
|
|
1141
|
+
const length = payload.length;
|
|
1142
|
+
let headerSize = 2;
|
|
1143
|
+
|
|
1144
|
+
if (length > 125 && length <= 65535) {
|
|
1145
|
+
headerSize += 2;
|
|
1146
|
+
} else if (length > 65535) {
|
|
1147
|
+
headerSize += 8;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (masked) {
|
|
1151
|
+
headerSize += 4;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const frame = new Uint8Array(headerSize + length);
|
|
1155
|
+
frame[0] = 0x80 | opcode; // FIN + opcode
|
|
1156
|
+
|
|
1157
|
+
let offset = 2;
|
|
1158
|
+
if (length <= 125) {
|
|
1159
|
+
frame[1] = (masked ? 0x80 : 0) | length;
|
|
1160
|
+
} else if (length <= 65535) {
|
|
1161
|
+
frame[1] = (masked ? 0x80 : 0) | 126;
|
|
1162
|
+
frame[2] = (length >> 8) & 0xFF;
|
|
1163
|
+
frame[3] = length & 0xFF;
|
|
1164
|
+
offset = 4;
|
|
1165
|
+
} else {
|
|
1166
|
+
frame[1] = (masked ? 0x80 : 0) | 127;
|
|
1167
|
+
frame[2] = 0; frame[3] = 0; frame[4] = 0; frame[5] = 0;
|
|
1168
|
+
frame[6] = (length >> 24) & 0xFF;
|
|
1169
|
+
frame[7] = (length >> 16) & 0xFF;
|
|
1170
|
+
frame[8] = (length >> 8) & 0xFF;
|
|
1171
|
+
frame[9] = length & 0xFF;
|
|
1172
|
+
offset = 10;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (masked) {
|
|
1176
|
+
const maskKey = new Uint8Array(4);
|
|
1177
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
1178
|
+
crypto.getRandomValues(maskKey);
|
|
1179
|
+
} else {
|
|
1180
|
+
for (let i = 0; i < 4; i++) maskKey[i] = Math.floor(Math.random() * 256);
|
|
1181
|
+
}
|
|
1182
|
+
frame.set(maskKey, offset);
|
|
1183
|
+
offset += 4;
|
|
1184
|
+
for (let i = 0; i < length; i++) {
|
|
1185
|
+
frame[offset + i] = payload[i] ^ maskKey[i % 4];
|
|
1186
|
+
}
|
|
1187
|
+
} else {
|
|
1188
|
+
frame.set(payload, offset);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return frame;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
887
1194
|
export default {
|
|
888
1195
|
Server,
|
|
889
1196
|
IncomingMessage,
|
|
@@ -901,4 +1208,6 @@ export default {
|
|
|
901
1208
|
_createClientRequest,
|
|
902
1209
|
Agent,
|
|
903
1210
|
globalAgent,
|
|
1211
|
+
_parseWsFrame,
|
|
1212
|
+
_createWsFrame,
|
|
904
1213
|
};
|
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,78 @@ 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
|
+
// Only use native WebSocket in a real browser — Node.js 21+ has native WebSocket
|
|
173
|
+
// but it connects to real servers, which breaks tests and isn't what the shim needs.
|
|
174
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
175
|
+
const NativeWS = isBrowser && typeof globalThis.WebSocket === 'function' && globalThis.WebSocket !== (WebSocket as any)
|
|
176
|
+
? globalThis.WebSocket
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
if (!NativeWS) {
|
|
180
|
+
// No native WebSocket (test env, Node.js, etc.) — act as if connected
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
this.readyState = WebSocket.OPEN;
|
|
183
|
+
this.emit('open');
|
|
184
|
+
if (this.onopen) this.onopen(new Event('open'));
|
|
185
|
+
}, 0);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
this._nativeWs = new NativeWS(this.url);
|
|
191
|
+
this._nativeWs.binaryType = this.binaryType === 'arraybuffer' ? 'arraybuffer' : 'blob';
|
|
192
|
+
} catch {
|
|
193
|
+
this.readyState = WebSocket.CLOSED;
|
|
194
|
+
const errorEvent = new Event('error');
|
|
195
|
+
this.emit('error', errorEvent);
|
|
196
|
+
if (this.onerror) this.onerror(errorEvent);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this._nativeWs.onopen = () => {
|
|
201
|
+
this.readyState = WebSocket.OPEN;
|
|
202
|
+
this.emit('open');
|
|
203
|
+
if (this.onopen) this.onopen(new Event('open'));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this._nativeWs.onmessage = (event: globalThis.MessageEvent) => {
|
|
207
|
+
const msgEvent = new MessageEventPolyfill('message', { data: event.data });
|
|
208
|
+
this.emit('message', msgEvent);
|
|
209
|
+
if (this.onmessage) this.onmessage(msgEvent as unknown as MessageEvent);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this._nativeWs.onclose = (event: globalThis.CloseEvent) => {
|
|
213
|
+
this.readyState = WebSocket.CLOSED;
|
|
214
|
+
this._nativeWs = null;
|
|
215
|
+
const closeEvent = new CloseEventPolyfill('close', {
|
|
216
|
+
code: event.code,
|
|
217
|
+
reason: event.reason,
|
|
218
|
+
wasClean: event.wasClean,
|
|
219
|
+
});
|
|
220
|
+
this.emit('close', closeEvent);
|
|
221
|
+
if (this.onclose) this.onclose(closeEvent as unknown as CloseEvent);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this._nativeWs.onerror = () => {
|
|
225
|
+
const errorEvent = new Event('error');
|
|
226
|
+
this.emit('error', errorEvent);
|
|
227
|
+
if (this.onerror) this.onerror(errorEvent);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
161
231
|
send(data: string | ArrayBuffer | Uint8Array): void {
|
|
162
232
|
if (this.readyState !== WebSocket.OPEN) {
|
|
163
233
|
throw new Error('WebSocket is not open');
|
|
164
234
|
}
|
|
165
235
|
|
|
236
|
+
// If connected to native WebSocket (external server)
|
|
237
|
+
if (this._nativeWs) {
|
|
238
|
+
this._nativeWs.send(data);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
166
242
|
// If connected to internal server
|
|
167
243
|
if (this._server) {
|
|
168
244
|
this._server._handleClientMessage(this, data);
|
|
@@ -187,6 +263,12 @@ export class WebSocket extends EventEmitter {
|
|
|
187
263
|
|
|
188
264
|
this.readyState = WebSocket.CLOSING;
|
|
189
265
|
|
|
266
|
+
// If connected to native WebSocket, close it (onclose handler emits events)
|
|
267
|
+
if (this._nativeWs) {
|
|
268
|
+
this._nativeWs.close(code, reason);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
190
272
|
if (messageChannel) {
|
|
191
273
|
messageChannel.postMessage({
|
|
192
274
|
type: 'disconnect',
|
|
@@ -218,7 +300,18 @@ export class WebSocket extends EventEmitter {
|
|
|
218
300
|
}
|
|
219
301
|
|
|
220
302
|
terminate(): void {
|
|
221
|
-
this.
|
|
303
|
+
if (this._nativeWs) {
|
|
304
|
+
this._nativeWs.close();
|
|
305
|
+
this._nativeWs = null;
|
|
306
|
+
}
|
|
307
|
+
this.readyState = WebSocket.CLOSED;
|
|
308
|
+
const closeEvent = new CloseEventPolyfill('close', {
|
|
309
|
+
code: 1006,
|
|
310
|
+
reason: 'Connection terminated',
|
|
311
|
+
wasClean: false,
|
|
312
|
+
});
|
|
313
|
+
this.emit('close', closeEvent);
|
|
314
|
+
if (this.onclose) this.onclose(closeEvent as unknown as CloseEvent);
|
|
222
315
|
}
|
|
223
316
|
|
|
224
317
|
// For internal server use
|