almostnode 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/__sw__.js +80 -84
  3. package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
  4. package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
  5. package/dist/frameworks/code-transforms.d.ts +53 -0
  6. package/dist/frameworks/code-transforms.d.ts.map +1 -0
  7. package/dist/frameworks/next-config-parser.d.ts +16 -0
  8. package/dist/frameworks/next-config-parser.d.ts.map +1 -0
  9. package/dist/frameworks/next-dev-server.d.ts +29 -18
  10. package/dist/frameworks/next-dev-server.d.ts.map +1 -1
  11. package/dist/frameworks/next-html-generator.d.ts +35 -0
  12. package/dist/frameworks/next-html-generator.d.ts.map +1 -0
  13. package/dist/frameworks/next-shims.d.ts +79 -0
  14. package/dist/frameworks/next-shims.d.ts.map +1 -0
  15. package/dist/frameworks/vite-dev-server.d.ts +0 -4
  16. package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
  17. package/dist/index.cjs +30392 -9523
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.mjs +27296 -8797
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/runtime.d.ts +20 -0
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/server-bridge.d.ts +2 -0
  26. package/dist/server-bridge.d.ts.map +1 -1
  27. package/dist/shims/crypto.d.ts +2 -0
  28. package/dist/shims/crypto.d.ts.map +1 -1
  29. package/dist/shims/esbuild.d.ts.map +1 -1
  30. package/dist/shims/fs.d.ts.map +1 -1
  31. package/dist/shims/http.d.ts +29 -0
  32. package/dist/shims/http.d.ts.map +1 -1
  33. package/dist/shims/path.d.ts.map +1 -1
  34. package/dist/shims/stream.d.ts.map +1 -1
  35. package/dist/shims/vfs-adapter.d.ts.map +1 -1
  36. package/dist/shims/ws.d.ts +2 -0
  37. package/dist/shims/ws.d.ts.map +1 -1
  38. package/dist/utils/binary-encoding.d.ts +13 -0
  39. package/dist/utils/binary-encoding.d.ts.map +1 -0
  40. package/dist/virtual-fs.d.ts.map +1 -1
  41. package/package.json +8 -4
  42. package/src/convex-app-demo-entry.ts +231 -35
  43. package/src/frameworks/code-transforms.ts +581 -0
  44. package/src/frameworks/next-config-parser.ts +140 -0
  45. package/src/frameworks/next-dev-server.ts +561 -1641
  46. package/src/frameworks/next-html-generator.ts +597 -0
  47. package/src/frameworks/next-shims.ts +1050 -0
  48. package/src/frameworks/tailwind-config-loader.ts +1 -1
  49. package/src/frameworks/vite-dev-server.ts +2 -61
  50. package/src/index.ts +2 -0
  51. package/src/runtime.ts +94 -15
  52. package/src/server-bridge.ts +61 -28
  53. package/src/shims/crypto.ts +13 -0
  54. package/src/shims/esbuild.ts +4 -1
  55. package/src/shims/fs.ts +9 -11
  56. package/src/shims/http.ts +309 -3
  57. package/src/shims/path.ts +6 -13
  58. package/src/shims/stream.ts +12 -26
  59. package/src/shims/vfs-adapter.ts +5 -2
  60. package/src/shims/ws.ts +92 -2
  61. package/src/utils/binary-encoding.ts +43 -0
  62. package/src/virtual-fs.ts +7 -15
  63. package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
@@ -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*Config\s*=/g, ' =');
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
- // Find React components (functions starting with uppercase letter)
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
- let binary = '';
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
- let binary = '';
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
- let hex = '';
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
  /**
@@ -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
- sw.addEventListener('statechange', () => {
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
- let binary = '';
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' ? new TextEncoder().encode(chunk) : chunk;
311
- let binary = '';
312
- for (let i = 0; i < bytes.length; i++) {
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: btoa(binary) },
387
+ data: { chunkBase64: uint8ToBase64(bytes) },
355
388
  });
356
389
  }
357
390
 
@@ -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,
@@ -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 new TextDecoder().decode(this);
138
+ return _decoder.decode(this);
135
139
  }
136
140
  if (encoding === 'base64') {
137
- let binary = '';
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 Array.from(this as Uint8Array)
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' ? new TextEncoder().encode(data) : data;
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 = new TextEncoder().encode(buffer);
621
+ data = _encoder.encode(buffer);
624
622
  offset = 0;
625
623
  length = data.length;
626
624
  } else {