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
|
@@ -109,7 +109,7 @@ export function stripTypescriptSyntax(content: string): string {
|
|
|
109
109
|
|
|
110
110
|
// Remove type annotations on variables
|
|
111
111
|
// e.g., const config: Config = { ... }
|
|
112
|
-
result = result.replace(/:\s*
|
|
112
|
+
result = result.replace(/:\s*[A-Z]\w*\s*=/g, ' =');
|
|
113
113
|
|
|
114
114
|
// Remove 'as const' assertions
|
|
115
115
|
result = result.replace(/\s+as\s+const\s*/g, ' ');
|
|
@@ -7,6 +7,7 @@ import { DevServer, DevServerOptions, ResponseData, HMRUpdate } from '../dev-ser
|
|
|
7
7
|
import { VirtualFS } from '../virtual-fs';
|
|
8
8
|
import { Buffer } from '../shims/stream';
|
|
9
9
|
import { simpleHash } from '../utils/hash';
|
|
10
|
+
import { addReactRefresh as _addReactRefresh } from './code-transforms';
|
|
10
11
|
|
|
11
12
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
12
13
|
// jsdom has window but doesn't have ServiceWorker or SharedArrayBuffer
|
|
@@ -574,68 +575,8 @@ export class ViteDevServer extends DevServer {
|
|
|
574
575
|
return result.code;
|
|
575
576
|
}
|
|
576
577
|
|
|
577
|
-
/**
|
|
578
|
-
* Add React Refresh registration to transformed code
|
|
579
|
-
* This enables true HMR (state-preserving) for React components
|
|
580
|
-
*/
|
|
581
578
|
private addReactRefresh(code: string, filename: string): string {
|
|
582
|
-
|
|
583
|
-
const components: string[] = [];
|
|
584
|
-
|
|
585
|
-
// Match function declarations: function App() { ... }
|
|
586
|
-
// Also handles: export function App() { ... }
|
|
587
|
-
const funcDeclRegex = /(?:^|\n)(?:export\s+)?function\s+([A-Z][a-zA-Z0-9]*)\s*\(/g;
|
|
588
|
-
let match;
|
|
589
|
-
while ((match = funcDeclRegex.exec(code)) !== null) {
|
|
590
|
-
if (!components.includes(match[1])) {
|
|
591
|
-
components.push(match[1]);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Match arrow function components: const App = () => { ... }
|
|
596
|
-
// Also handles: export const App = () => { ... }
|
|
597
|
-
// And: const App = function() { ... }
|
|
598
|
-
const arrowRegex = /(?:^|\n)(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9]*)\s*=/g;
|
|
599
|
-
while ((match = arrowRegex.exec(code)) !== null) {
|
|
600
|
-
if (!components.includes(match[1])) {
|
|
601
|
-
components.push(match[1]);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// If no components found, just add hot module setup without refresh
|
|
606
|
-
if (components.length === 0) {
|
|
607
|
-
return `// HMR Setup
|
|
608
|
-
import.meta.hot = window.__vite_hot_context__("${filename}");
|
|
609
|
-
|
|
610
|
-
${code}
|
|
611
|
-
|
|
612
|
-
// HMR Accept
|
|
613
|
-
if (import.meta.hot) {
|
|
614
|
-
import.meta.hot.accept();
|
|
615
|
-
}
|
|
616
|
-
`;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Build React Refresh registration calls
|
|
620
|
-
const registrations = components
|
|
621
|
-
.map(name => ` $RefreshReg$(${name}, "${filename} ${name}");`)
|
|
622
|
-
.join('\n');
|
|
623
|
-
|
|
624
|
-
return `// HMR Setup
|
|
625
|
-
import.meta.hot = window.__vite_hot_context__("${filename}");
|
|
626
|
-
|
|
627
|
-
${code}
|
|
628
|
-
|
|
629
|
-
// React Refresh Registration
|
|
630
|
-
if (import.meta.hot) {
|
|
631
|
-
${registrations}
|
|
632
|
-
import.meta.hot.accept(() => {
|
|
633
|
-
if (window.$RefreshRuntime$) {
|
|
634
|
-
window.$RefreshRuntime$.performReactRefresh();
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
`;
|
|
579
|
+
return _addReactRefresh(code, filename);
|
|
639
580
|
}
|
|
640
581
|
|
|
641
582
|
/**
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
76
76
|
serverBridge: ServerBridge;
|
|
77
77
|
execute: (code: string, filename?: string) => { exports: unknown };
|
|
78
78
|
runFile: (filename: string) => { exports: unknown };
|
|
79
|
+
createREPL: () => { eval: (code: string) => unknown };
|
|
79
80
|
on: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
80
81
|
} {
|
|
81
82
|
const vfs = new VirtualFS();
|
|
@@ -93,6 +94,7 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
93
94
|
serverBridge,
|
|
94
95
|
execute: (code: string, filename?: string) => runtime.execute(code, filename),
|
|
95
96
|
runFile: (filename: string) => runtime.runFile(filename),
|
|
97
|
+
createREPL: () => runtime.createREPL(),
|
|
96
98
|
on: (event: string, listener: (...args: unknown[]) => void) => {
|
|
97
99
|
serverBridge.on(event, listener);
|
|
98
100
|
},
|
package/src/runtime.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { VirtualFS } from './virtual-fs';
|
|
|
9
9
|
import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
|
|
10
10
|
import type { PackageJson } from './types/package-json';
|
|
11
11
|
import { simpleHash } from './utils/hash';
|
|
12
|
+
import { uint8ToBase64, uint8ToHex } from './utils/binary-encoding';
|
|
12
13
|
import { createFsShim, FsShim } from './shims/fs';
|
|
13
14
|
import * as pathShim from './shims/path';
|
|
14
15
|
import { createProcess, Process } from './shims/process';
|
|
@@ -548,6 +549,12 @@ function createRequire(
|
|
|
548
549
|
// Cache before loading to handle circular dependencies
|
|
549
550
|
moduleCache[resolvedPath] = module;
|
|
550
551
|
|
|
552
|
+
// Evict oldest entry if cache exceeds bounds
|
|
553
|
+
const cacheKeys = Object.keys(moduleCache);
|
|
554
|
+
if (cacheKeys.length > 2000) {
|
|
555
|
+
delete moduleCache[cacheKeys[0]];
|
|
556
|
+
}
|
|
557
|
+
|
|
551
558
|
// Handle JSON files
|
|
552
559
|
if (resolvedPath.endsWith('.json')) {
|
|
553
560
|
const content = vfs.readFileSync(resolvedPath, 'utf8');
|
|
@@ -907,27 +914,15 @@ export class Runtime {
|
|
|
907
914
|
: new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
908
915
|
|
|
909
916
|
if (this.encoding === 'base64') {
|
|
910
|
-
|
|
911
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
912
|
-
binary += String.fromCharCode(bytes[i]);
|
|
913
|
-
}
|
|
914
|
-
return btoa(binary);
|
|
917
|
+
return uint8ToBase64(bytes);
|
|
915
918
|
}
|
|
916
919
|
|
|
917
920
|
if (this.encoding === 'base64url') {
|
|
918
|
-
|
|
919
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
920
|
-
binary += String.fromCharCode(bytes[i]);
|
|
921
|
-
}
|
|
922
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
921
|
+
return uint8ToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
923
922
|
}
|
|
924
923
|
|
|
925
924
|
if (this.encoding === 'hex') {
|
|
926
|
-
|
|
927
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
928
|
-
hex += bytes[i].toString(16).padStart(2, '0');
|
|
929
|
-
}
|
|
930
|
-
return hex;
|
|
925
|
+
return uint8ToHex(bytes);
|
|
931
926
|
}
|
|
932
927
|
|
|
933
928
|
// Fallback: decode as utf-8
|
|
@@ -1085,6 +1080,90 @@ ${code}
|
|
|
1085
1080
|
getProcess(): Process {
|
|
1086
1081
|
return this.process;
|
|
1087
1082
|
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Create a REPL context that evaluates expressions and persists state.
|
|
1086
|
+
*
|
|
1087
|
+
* Returns an object with an `eval` method that:
|
|
1088
|
+
* - Returns the value of the last expression (unlike `execute` which returns module.exports)
|
|
1089
|
+
* - Persists variables between calls (`var x = 1` then `x` works)
|
|
1090
|
+
* - Has access to `require`, `console`, `process`, `Buffer` (same as execute)
|
|
1091
|
+
*
|
|
1092
|
+
* Security: The eval runs inside a Generator's local scope via direct eval,
|
|
1093
|
+
* NOT in the global scope. Only the runtime's own require/console/process are
|
|
1094
|
+
* exposed — the same sandbox boundary as execute(). Variables created in the
|
|
1095
|
+
* REPL are confined to the generator's closure and cannot leak to the page.
|
|
1096
|
+
*
|
|
1097
|
+
* Note: `const`/`let` are transformed to `var` so they persist across calls
|
|
1098
|
+
* (var hoists to the generator's function scope, const/let are block-scoped
|
|
1099
|
+
* to each eval call and would be lost).
|
|
1100
|
+
*/
|
|
1101
|
+
createREPL(): { eval: (code: string) => unknown } {
|
|
1102
|
+
const require = createRequire(
|
|
1103
|
+
this.vfs,
|
|
1104
|
+
this.fsShim,
|
|
1105
|
+
this.process,
|
|
1106
|
+
'/',
|
|
1107
|
+
this.moduleCache,
|
|
1108
|
+
this.options,
|
|
1109
|
+
this.processedCodeCache
|
|
1110
|
+
);
|
|
1111
|
+
const consoleWrapper = createConsoleWrapper(this.options.onConsole);
|
|
1112
|
+
const process = this.process;
|
|
1113
|
+
const buffer = bufferShim.Buffer;
|
|
1114
|
+
|
|
1115
|
+
// Use a Generator to maintain a persistent eval scope.
|
|
1116
|
+
// Generator functions preserve their local scope across yields, so
|
|
1117
|
+
// var declarations from eval() persist between calls. Direct eval
|
|
1118
|
+
// runs in the generator's scope (not global), providing isolation.
|
|
1119
|
+
const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor;
|
|
1120
|
+
const replGen = new GeneratorFunction(
|
|
1121
|
+
'require',
|
|
1122
|
+
'console',
|
|
1123
|
+
'process',
|
|
1124
|
+
'Buffer',
|
|
1125
|
+
`var __code, __result;
|
|
1126
|
+
while (true) {
|
|
1127
|
+
__code = yield;
|
|
1128
|
+
try {
|
|
1129
|
+
__result = eval(__code);
|
|
1130
|
+
yield { value: __result, error: null };
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
yield { value: undefined, error: e };
|
|
1133
|
+
}
|
|
1134
|
+
}`
|
|
1135
|
+
)(require, consoleWrapper, process, buffer);
|
|
1136
|
+
replGen.next(); // prime the generator
|
|
1137
|
+
|
|
1138
|
+
return {
|
|
1139
|
+
eval(code: string): unknown {
|
|
1140
|
+
// Transform const/let to var for persistence across REPL calls.
|
|
1141
|
+
// var declarations in direct eval are added to the enclosing function
|
|
1142
|
+
// scope (the generator), so they survive across yields.
|
|
1143
|
+
const transformed = code.replace(/^\s*(const|let)\s+/gm, 'var ');
|
|
1144
|
+
|
|
1145
|
+
// Try as expression first (wrapping in parens), fall back to statement.
|
|
1146
|
+
// replGen.next(code) sends code to the generator, which evals it and
|
|
1147
|
+
// yields the result — so the result is in the return value of .next().
|
|
1148
|
+
const exprResult = replGen.next('(' + transformed + ')').value as { value: unknown; error: unknown };
|
|
1149
|
+
if (!exprResult.error) {
|
|
1150
|
+
// Advance past the wait-for-code yield so it's ready for next call
|
|
1151
|
+
replGen.next();
|
|
1152
|
+
return exprResult.value;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Expression parse failed — advance past wait-for-code, then try as statement
|
|
1156
|
+
replGen.next();
|
|
1157
|
+
const stmtResult = replGen.next(transformed).value as { value: unknown; error: unknown };
|
|
1158
|
+
if (stmtResult.error) {
|
|
1159
|
+
replGen.next(); // advance past wait-for-code yield
|
|
1160
|
+
throw stmtResult.error;
|
|
1161
|
+
}
|
|
1162
|
+
replGen.next(); // advance past wait-for-code yield
|
|
1163
|
+
return stmtResult.value;
|
|
1164
|
+
},
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1088
1167
|
}
|
|
1089
1168
|
|
|
1090
1169
|
/**
|
package/src/server-bridge.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
} from './shims/http';
|
|
13
13
|
import { EventEmitter } from './shims/events';
|
|
14
14
|
import { Buffer } from './shims/stream';
|
|
15
|
+
import { uint8ToBase64 } from './utils/binary-encoding';
|
|
16
|
+
|
|
17
|
+
const _encoder = new TextEncoder();
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Interface for virtual servers that can be registered with the bridge
|
|
@@ -50,11 +53,13 @@ export interface InitServiceWorkerOptions {
|
|
|
50
53
|
* Server Bridge manages virtual HTTP servers and routes requests
|
|
51
54
|
*/
|
|
52
55
|
export class ServerBridge extends EventEmitter {
|
|
56
|
+
static DEBUG = false;
|
|
53
57
|
private servers: Map<number, VirtualServer> = new Map();
|
|
54
58
|
private baseUrl: string;
|
|
55
59
|
private options: BridgeOptions;
|
|
56
60
|
private messageChannel: MessageChannel | null = null;
|
|
57
61
|
private serviceWorkerReady: boolean = false;
|
|
62
|
+
private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
58
63
|
|
|
59
64
|
constructor(options: BridgeOptions = {}) {
|
|
60
65
|
super();
|
|
@@ -164,6 +169,15 @@ export class ServerBridge extends EventEmitter {
|
|
|
164
169
|
|
|
165
170
|
const swUrl = options?.swUrl ?? '/__sw__.js';
|
|
166
171
|
|
|
172
|
+
// Set up controllerchange listener BEFORE registration so we don't miss the event.
|
|
173
|
+
// clients.claim() in the SW's activate handler fires controllerchange, and it can
|
|
174
|
+
// happen before our activation wait completes.
|
|
175
|
+
const controllerReady = navigator.serviceWorker.controller
|
|
176
|
+
? Promise.resolve()
|
|
177
|
+
: new Promise<void>((resolve) => {
|
|
178
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
167
181
|
// Register service worker
|
|
168
182
|
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
169
183
|
scope: '/',
|
|
@@ -180,11 +194,13 @@ export class ServerBridge extends EventEmitter {
|
|
|
180
194
|
if (sw.state === 'activated') {
|
|
181
195
|
resolve();
|
|
182
196
|
} else {
|
|
183
|
-
|
|
197
|
+
const handler = () => {
|
|
184
198
|
if (sw.state === 'activated') {
|
|
199
|
+
sw.removeEventListener('statechange', handler);
|
|
185
200
|
resolve();
|
|
186
201
|
}
|
|
187
|
-
}
|
|
202
|
+
};
|
|
203
|
+
sw.addEventListener('statechange', handler);
|
|
188
204
|
}
|
|
189
205
|
});
|
|
190
206
|
|
|
@@ -197,6 +213,36 @@ export class ServerBridge extends EventEmitter {
|
|
|
197
213
|
this.messageChannel.port2,
|
|
198
214
|
]);
|
|
199
215
|
|
|
216
|
+
// Wait for SW to actually control this page (clients.claim() in SW activate handler)
|
|
217
|
+
// Without this, fetch requests bypass the SW and go directly to the server
|
|
218
|
+
await controllerReady;
|
|
219
|
+
|
|
220
|
+
// Re-establish communication when the SW loses its port (idle termination)
|
|
221
|
+
// or when the SW is replaced (new deployment). The SW sends 'sw-needs-init'
|
|
222
|
+
// to all clients when a request arrives but mainPort is null.
|
|
223
|
+
const reinit = () => {
|
|
224
|
+
if (navigator.serviceWorker.controller) {
|
|
225
|
+
this.messageChannel = new MessageChannel();
|
|
226
|
+
this.messageChannel.port1.onmessage = this.handleServiceWorkerMessage.bind(this);
|
|
227
|
+
navigator.serviceWorker.controller.postMessage(
|
|
228
|
+
{ type: 'init', port: this.messageChannel.port2 },
|
|
229
|
+
[this.messageChannel.port2]
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
navigator.serviceWorker.addEventListener('controllerchange', reinit);
|
|
234
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
235
|
+
if (event.data?.type === 'sw-needs-init') {
|
|
236
|
+
reinit();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Keep the SW alive with periodic pings. Browsers terminate idle SWs
|
|
241
|
+
// after ~30s, losing the MessageChannel port and all in-memory state.
|
|
242
|
+
this.keepaliveInterval = setInterval(() => {
|
|
243
|
+
this.messageChannel?.port1.postMessage({ type: 'keepalive' });
|
|
244
|
+
}, 20_000);
|
|
245
|
+
|
|
200
246
|
this.serviceWorkerReady = true;
|
|
201
247
|
this.emit('sw-ready');
|
|
202
248
|
}
|
|
@@ -207,14 +253,14 @@ export class ServerBridge extends EventEmitter {
|
|
|
207
253
|
private async handleServiceWorkerMessage(event: MessageEvent): Promise<void> {
|
|
208
254
|
const { type, id, data } = event.data;
|
|
209
255
|
|
|
210
|
-
console.log('[ServerBridge] SW message:', type, id, data?.url);
|
|
256
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] SW message:', type, id, data?.url);
|
|
211
257
|
|
|
212
258
|
if (type === 'request') {
|
|
213
259
|
const { port, method, url, headers, body, streaming } = data;
|
|
214
260
|
|
|
215
|
-
console.log('[ServerBridge] Handling request:', port, method, url, 'streaming:', streaming);
|
|
261
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] Handling request:', port, method, url, 'streaming:', streaming);
|
|
216
262
|
if (streaming) {
|
|
217
|
-
console.log('[ServerBridge] 🔴 Will use streaming handler');
|
|
263
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] 🔴 Will use streaming handler');
|
|
218
264
|
}
|
|
219
265
|
|
|
220
266
|
try {
|
|
@@ -224,21 +270,16 @@ export class ServerBridge extends EventEmitter {
|
|
|
224
270
|
} else {
|
|
225
271
|
// Handle regular request
|
|
226
272
|
const response = await this.handleRequest(port, method, url, headers, body);
|
|
227
|
-
console.log('[ServerBridge] Response:', response.statusCode, 'body length:', response.body?.length);
|
|
273
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] Response:', response.statusCode, 'body length:', response.body?.length);
|
|
228
274
|
|
|
229
275
|
// Convert body to base64 string to avoid structured cloning issues with Uint8Array
|
|
230
276
|
let bodyBase64 = '';
|
|
231
277
|
if (response.body && response.body.length > 0) {
|
|
232
|
-
// Convert Uint8Array to base64 string
|
|
233
278
|
const bytes = response.body instanceof Uint8Array ? response.body : new Uint8Array(0);
|
|
234
|
-
|
|
235
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
236
|
-
binary += String.fromCharCode(bytes[i]);
|
|
237
|
-
}
|
|
238
|
-
bodyBase64 = btoa(binary);
|
|
279
|
+
bodyBase64 = uint8ToBase64(bytes);
|
|
239
280
|
}
|
|
240
281
|
|
|
241
|
-
console.log('[ServerBridge] Sending response to SW, body base64 length:', bodyBase64.length);
|
|
282
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] Sending response to SW, body base64 length:', bodyBase64.length);
|
|
242
283
|
|
|
243
284
|
this.messageChannel?.port1.postMessage({
|
|
244
285
|
type: 'response',
|
|
@@ -287,7 +328,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
287
328
|
// Check if the server supports streaming (has handleStreamingRequest method)
|
|
288
329
|
const server = virtualServer.server as any;
|
|
289
330
|
if (typeof server.handleStreamingRequest === 'function') {
|
|
290
|
-
console.log('[ServerBridge] 🟢 Server has streaming support, calling handleStreamingRequest');
|
|
331
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] 🟢 Server has streaming support, calling handleStreamingRequest');
|
|
291
332
|
// Use streaming handler
|
|
292
333
|
const bodyBuffer = body ? Buffer.from(new Uint8Array(body)) : undefined;
|
|
293
334
|
|
|
@@ -298,7 +339,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
298
339
|
bodyBuffer,
|
|
299
340
|
// onStart - called with headers
|
|
300
341
|
(statusCode: number, statusMessage: string, respHeaders: Record<string, string>) => {
|
|
301
|
-
console.log('[ServerBridge] 🟢 onStart called, sending stream-start');
|
|
342
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] 🟢 onStart called, sending stream-start');
|
|
302
343
|
this.messageChannel?.port1.postMessage({
|
|
303
344
|
type: 'stream-start',
|
|
304
345
|
id,
|
|
@@ -307,13 +348,9 @@ export class ServerBridge extends EventEmitter {
|
|
|
307
348
|
},
|
|
308
349
|
// onChunk - called for each chunk
|
|
309
350
|
(chunk: string | Uint8Array) => {
|
|
310
|
-
const bytes = typeof chunk === 'string' ?
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
binary += String.fromCharCode(bytes[i]);
|
|
314
|
-
}
|
|
315
|
-
const chunkBase64 = btoa(binary);
|
|
316
|
-
console.log('[ServerBridge] 🟡 onChunk called, sending stream-chunk, size:', chunkBase64.length);
|
|
351
|
+
const bytes = typeof chunk === 'string' ? _encoder.encode(chunk) : chunk;
|
|
352
|
+
const chunkBase64 = uint8ToBase64(bytes);
|
|
353
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] 🟡 onChunk called, sending stream-chunk, size:', chunkBase64.length);
|
|
317
354
|
this.messageChannel?.port1.postMessage({
|
|
318
355
|
type: 'stream-chunk',
|
|
319
356
|
id,
|
|
@@ -322,7 +359,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
322
359
|
},
|
|
323
360
|
// onEnd - called when response is complete
|
|
324
361
|
() => {
|
|
325
|
-
console.log('[ServerBridge] 🟢 onEnd called, sending stream-end');
|
|
362
|
+
ServerBridge.DEBUG && console.log('[ServerBridge] 🟢 onEnd called, sending stream-end');
|
|
326
363
|
this.messageChannel?.port1.postMessage({ type: 'stream-end', id });
|
|
327
364
|
}
|
|
328
365
|
);
|
|
@@ -344,14 +381,10 @@ export class ServerBridge extends EventEmitter {
|
|
|
344
381
|
|
|
345
382
|
if (response.body && response.body.length > 0) {
|
|
346
383
|
const bytes = response.body instanceof Uint8Array ? response.body : new Uint8Array(0);
|
|
347
|
-
let binary = '';
|
|
348
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
349
|
-
binary += String.fromCharCode(bytes[i]);
|
|
350
|
-
}
|
|
351
384
|
this.messageChannel?.port1.postMessage({
|
|
352
385
|
type: 'stream-chunk',
|
|
353
386
|
id,
|
|
354
|
-
data: { chunkBase64:
|
|
387
|
+
data: { chunkBase64: uint8ToBase64(bytes) },
|
|
355
388
|
});
|
|
356
389
|
}
|
|
357
390
|
|
package/src/shims/crypto.ts
CHANGED
|
@@ -16,6 +16,18 @@ export function randomBytes(size: number): Buffer {
|
|
|
16
16
|
return Buffer.from(array);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function randomFillSync(
|
|
20
|
+
buffer: Uint8Array | Buffer,
|
|
21
|
+
offset?: number,
|
|
22
|
+
size?: number
|
|
23
|
+
): Uint8Array | Buffer {
|
|
24
|
+
const start = offset || 0;
|
|
25
|
+
const len = size !== undefined ? size : (buffer.length - start);
|
|
26
|
+
const view = new Uint8Array(buffer.buffer, buffer.byteOffset + start, len);
|
|
27
|
+
crypto.getRandomValues(view);
|
|
28
|
+
return buffer;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
export function randomUUID(): string {
|
|
20
32
|
return crypto.randomUUID();
|
|
21
33
|
}
|
|
@@ -808,6 +820,7 @@ async function importKey(
|
|
|
808
820
|
|
|
809
821
|
export default {
|
|
810
822
|
randomBytes,
|
|
823
|
+
randomFillSync,
|
|
811
824
|
randomUUID,
|
|
812
825
|
randomInt,
|
|
813
826
|
getRandomValues,
|
package/src/shims/esbuild.ts
CHANGED
|
@@ -717,7 +717,7 @@ export async function build(options: BuildOptions): Promise<BuildResult> {
|
|
|
717
717
|
// This preserves the original paths for esbuild's output file naming.
|
|
718
718
|
let entryPoints = options.entryPoints;
|
|
719
719
|
if (entryPoints && globalVFS) {
|
|
720
|
-
const absWorkingDir = options.absWorkingDir || '/';
|
|
720
|
+
const absWorkingDir = options.absWorkingDir || (typeof globalThis !== 'undefined' && globalThis.process && typeof globalThis.process.cwd === 'function' ? globalThis.process.cwd() : '/');
|
|
721
721
|
entryPoints = entryPoints.map(ep => {
|
|
722
722
|
// Handle paths that came from previous builds with vfs: namespace prefix
|
|
723
723
|
if (ep.includes('vfs:')) {
|
|
@@ -750,11 +750,14 @@ export async function build(options: BuildOptions): Promise<BuildResult> {
|
|
|
750
750
|
}
|
|
751
751
|
|
|
752
752
|
// In browser, we need write: false to get outputFiles
|
|
753
|
+
// Pass absWorkingDir so metafile paths are relative to the correct directory
|
|
754
|
+
const resolvedAbsWorkingDir = options.absWorkingDir || (typeof globalThis !== 'undefined' && globalThis.process && typeof globalThis.process.cwd === 'function' ? globalThis.process.cwd() : '/');
|
|
753
755
|
const result = await esbuildInstance.build({
|
|
754
756
|
...options,
|
|
755
757
|
entryPoints,
|
|
756
758
|
plugins,
|
|
757
759
|
write: false,
|
|
760
|
+
absWorkingDir: resolvedAbsWorkingDir,
|
|
758
761
|
}) as BuildResult;
|
|
759
762
|
|
|
760
763
|
// Strip 'vfs:' namespace prefix from output file paths
|
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 {
|