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
|
@@ -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, ' ');
|
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';
|
|
@@ -456,16 +457,26 @@ function createRequire(
|
|
|
456
457
|
return null;
|
|
457
458
|
};
|
|
458
459
|
|
|
460
|
+
// Apply browser field object remapping for a resolved file within a package
|
|
461
|
+
const applyBrowserFieldRemap = (resolvedPath: string, pkg: PackageJson, pkgRoot: string): string | null => {
|
|
462
|
+
if (!pkg.browser || typeof pkg.browser !== 'object') return resolvedPath;
|
|
463
|
+
const browserMap = pkg.browser as Record<string, string | false>;
|
|
464
|
+
// Build relative path from package root (e.g., "./lib/node.js")
|
|
465
|
+
const relPath = './' + pathShim.relative(pkgRoot, resolvedPath);
|
|
466
|
+
// Also check without extension for common patterns
|
|
467
|
+
const relPathNoExt = relPath.replace(/\.(js|json|cjs|mjs)$/, '');
|
|
468
|
+
for (const key of [relPath, relPathNoExt]) {
|
|
469
|
+
if (key in browserMap) {
|
|
470
|
+
if (browserMap[key] === false) return null; // Module excluded in browser
|
|
471
|
+
return tryResolveFile(pathShim.join(pkgRoot, browserMap[key] as string));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return resolvedPath;
|
|
475
|
+
};
|
|
476
|
+
|
|
459
477
|
// Helper to resolve from a node_modules directory
|
|
460
478
|
const tryResolveFromNodeModules = (nodeModulesDir: string, moduleId: string): string | null => {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// Check if path exists (as file or directory)
|
|
464
|
-
const resolved = tryResolveFile(fullPath);
|
|
465
|
-
if (resolved) return resolved;
|
|
466
|
-
|
|
467
|
-
// Check if this is a package (has package.json)
|
|
468
|
-
// For sub-paths like "pkg/sub", we need to find the package root first
|
|
479
|
+
// Determine the package name and root
|
|
469
480
|
const parts = moduleId.split('/');
|
|
470
481
|
const pkgName = parts[0].startsWith('@') && parts.length > 1
|
|
471
482
|
? `${parts[0]}/${parts[1]}` // Scoped package
|
|
@@ -474,6 +485,7 @@ function createRequire(
|
|
|
474
485
|
const pkgRoot = pathShim.join(nodeModulesDir, pkgName);
|
|
475
486
|
const pkgPath = pathShim.join(pkgRoot, 'package.json');
|
|
476
487
|
|
|
488
|
+
// Check package.json first — it controls entry points (browser, main, exports)
|
|
477
489
|
const pkg = getParsedPackageJson(pkgPath);
|
|
478
490
|
if (pkg) {
|
|
479
491
|
// Use resolve.exports to handle the exports field
|
|
@@ -493,15 +505,27 @@ function createRequire(
|
|
|
493
505
|
}
|
|
494
506
|
}
|
|
495
507
|
|
|
496
|
-
// If this is the package root (no sub-path), use main entry
|
|
508
|
+
// If this is the package root (no sub-path), use browser/main entry
|
|
497
509
|
if (pkgName === moduleId) {
|
|
498
|
-
|
|
510
|
+
// Prefer browser field (string form) since we're running in a browser
|
|
511
|
+
let main: string | undefined;
|
|
512
|
+
if (typeof pkg.browser === 'string') {
|
|
513
|
+
main = pkg.browser;
|
|
514
|
+
}
|
|
515
|
+
if (!main) {
|
|
516
|
+
main = pkg.main || 'index.js';
|
|
517
|
+
}
|
|
499
518
|
const mainPath = pathShim.join(pkgRoot, main);
|
|
500
519
|
const resolvedMain = tryResolveFile(mainPath);
|
|
501
520
|
if (resolvedMain) return resolvedMain;
|
|
502
521
|
}
|
|
503
522
|
}
|
|
504
523
|
|
|
524
|
+
// Fall back to direct file/directory resolution (for sub-paths or packages without package.json)
|
|
525
|
+
const fullPath = pathShim.join(nodeModulesDir, moduleId);
|
|
526
|
+
const resolved = tryResolveFile(fullPath);
|
|
527
|
+
if (resolved) return resolved;
|
|
528
|
+
|
|
505
529
|
return null;
|
|
506
530
|
};
|
|
507
531
|
|
|
@@ -548,6 +572,12 @@ function createRequire(
|
|
|
548
572
|
// Cache before loading to handle circular dependencies
|
|
549
573
|
moduleCache[resolvedPath] = module;
|
|
550
574
|
|
|
575
|
+
// Evict oldest entry if cache exceeds bounds
|
|
576
|
+
const cacheKeys = Object.keys(moduleCache);
|
|
577
|
+
if (cacheKeys.length > 2000) {
|
|
578
|
+
delete moduleCache[cacheKeys[0]];
|
|
579
|
+
}
|
|
580
|
+
|
|
551
581
|
// Handle JSON files
|
|
552
582
|
if (resolvedPath.endsWith('.json')) {
|
|
553
583
|
const content = vfs.readFileSync(resolvedPath, 'utf8');
|
|
@@ -859,6 +889,10 @@ export class Runtime {
|
|
|
859
889
|
// Initialize esbuild shim with VFS for file access
|
|
860
890
|
esbuildShim.setVFS(vfs);
|
|
861
891
|
|
|
892
|
+
// Polyfill Error.captureStackTrace/prepareStackTrace for Safari/WebKit
|
|
893
|
+
// (V8-specific API used by Express's depd and other npm packages)
|
|
894
|
+
this.setupStackTracePolyfill();
|
|
895
|
+
|
|
862
896
|
// Polyfill TextDecoder to handle base64/base64url/hex gracefully
|
|
863
897
|
// (Some CLI tools incorrectly try to use TextDecoder for these)
|
|
864
898
|
this.setupTextDecoderPolyfill();
|
|
@@ -907,27 +941,15 @@ export class Runtime {
|
|
|
907
941
|
: new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
908
942
|
|
|
909
943
|
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);
|
|
944
|
+
return uint8ToBase64(bytes);
|
|
915
945
|
}
|
|
916
946
|
|
|
917
947
|
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, '');
|
|
948
|
+
return uint8ToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
923
949
|
}
|
|
924
950
|
|
|
925
951
|
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;
|
|
952
|
+
return uint8ToHex(bytes);
|
|
931
953
|
}
|
|
932
954
|
|
|
933
955
|
// Fallback: decode as utf-8
|
|
@@ -946,6 +968,146 @@ export class Runtime {
|
|
|
946
968
|
globalThis.TextDecoder = PolyfillTextDecoder as unknown as typeof TextDecoder;
|
|
947
969
|
}
|
|
948
970
|
|
|
971
|
+
/**
|
|
972
|
+
* Polyfill V8's Error.captureStackTrace and Error.prepareStackTrace for Safari/WebKit.
|
|
973
|
+
* Express's `depd` and other npm packages use these V8-specific APIs which don't
|
|
974
|
+
* exist in Safari, causing "callSite.getFileName is not a function" errors.
|
|
975
|
+
*/
|
|
976
|
+
private setupStackTracePolyfill(): void {
|
|
977
|
+
// Only polyfill if not already available (i.e., not V8/Chrome)
|
|
978
|
+
if (typeof (Error as any).captureStackTrace === 'function') return;
|
|
979
|
+
|
|
980
|
+
// Set a default stackTraceLimit so Math.max(10, undefined) doesn't produce NaN
|
|
981
|
+
// (depd and other packages read this value)
|
|
982
|
+
if ((Error as any).stackTraceLimit === undefined) {
|
|
983
|
+
(Error as any).stackTraceLimit = 10;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Parse a stack trace string into structured frames
|
|
987
|
+
function parseStack(stack: string): Array<{fn: string, file: string, line: number, col: number}> {
|
|
988
|
+
if (!stack) return [];
|
|
989
|
+
const frames: Array<{fn: string, file: string, line: number, col: number}> = [];
|
|
990
|
+
const lines = stack.split('\n');
|
|
991
|
+
|
|
992
|
+
for (const raw of lines) {
|
|
993
|
+
const line = raw.trim();
|
|
994
|
+
if (!line || line.startsWith('Error') || line.startsWith('TypeError')) continue;
|
|
995
|
+
|
|
996
|
+
let fn = '', file = '', lineNo = 0, colNo = 0;
|
|
997
|
+
|
|
998
|
+
// Safari format: "functionName@file:line:col" or "@file:line:col"
|
|
999
|
+
const safariMatch = line.match(/^(.*)@(.*?):(\d+):(\d+)$/);
|
|
1000
|
+
if (safariMatch) {
|
|
1001
|
+
fn = safariMatch[1] || '';
|
|
1002
|
+
file = safariMatch[2];
|
|
1003
|
+
lineNo = parseInt(safariMatch[3], 10);
|
|
1004
|
+
colNo = parseInt(safariMatch[4], 10);
|
|
1005
|
+
frames.push({ fn, file, line: lineNo, col: colNo });
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Chrome format: "at functionName (file:line:col)" or "at file:line:col"
|
|
1010
|
+
const chromeMatch = line.match(/^at\s+(?:(.+?)\s+\()?(.*?):(\d+):(\d+)\)?$/);
|
|
1011
|
+
if (chromeMatch) {
|
|
1012
|
+
fn = chromeMatch[1] || '';
|
|
1013
|
+
file = chromeMatch[2];
|
|
1014
|
+
lineNo = parseInt(chromeMatch[3], 10);
|
|
1015
|
+
colNo = parseInt(chromeMatch[4], 10);
|
|
1016
|
+
frames.push({ fn, file, line: lineNo, col: colNo });
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return frames;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Create a mock CallSite object from a parsed frame
|
|
1024
|
+
function createCallSite(frame: {fn: string, file: string, line: number, col: number}) {
|
|
1025
|
+
return {
|
|
1026
|
+
getFileName: () => frame.file || null,
|
|
1027
|
+
getLineNumber: () => frame.line || null,
|
|
1028
|
+
getColumnNumber: () => frame.col || null,
|
|
1029
|
+
getFunctionName: () => frame.fn || null,
|
|
1030
|
+
getMethodName: () => frame.fn || null,
|
|
1031
|
+
getTypeName: () => null,
|
|
1032
|
+
getThis: () => undefined,
|
|
1033
|
+
getFunction: () => undefined,
|
|
1034
|
+
getEvalOrigin: () => undefined,
|
|
1035
|
+
isNative: () => false,
|
|
1036
|
+
isConstructor: () => false,
|
|
1037
|
+
isToplevel: () => !frame.fn,
|
|
1038
|
+
isEval: () => false,
|
|
1039
|
+
toString: () => frame.fn
|
|
1040
|
+
? `${frame.fn} (${frame.file}:${frame.line}:${frame.col})`
|
|
1041
|
+
: `${frame.file}:${frame.line}:${frame.col}`,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Helper: parse stack and create CallSite objects, used by both captureStackTrace and .stack getter
|
|
1046
|
+
function buildCallSites(stack: string, constructorOpt?: Function) {
|
|
1047
|
+
const frames = parseStack(stack);
|
|
1048
|
+
let startIdx = 0;
|
|
1049
|
+
if (constructorOpt && constructorOpt.name) {
|
|
1050
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1051
|
+
if (frames[i].fn === constructorOpt.name) {
|
|
1052
|
+
startIdx = i + 1;
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return frames.slice(startIdx).map(createCallSite);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Symbol to store raw stack string, used by the .stack getter
|
|
1061
|
+
const stackSymbol = Symbol('rawStack');
|
|
1062
|
+
|
|
1063
|
+
// Intercept .stack on Error.prototype so that packages using the V8 pattern
|
|
1064
|
+
// "Error.prepareStackTrace = fn; new Error().stack" also get CallSite objects.
|
|
1065
|
+
// In V8, reading .stack lazily triggers prepareStackTrace; Safari doesn't do this.
|
|
1066
|
+
Object.defineProperty(Error.prototype, 'stack', {
|
|
1067
|
+
get() {
|
|
1068
|
+
const rawStack = (this as any)[stackSymbol];
|
|
1069
|
+
if (rawStack !== undefined && typeof (Error as any).prepareStackTrace === 'function') {
|
|
1070
|
+
const callSites = buildCallSites(rawStack);
|
|
1071
|
+
try {
|
|
1072
|
+
return (Error as any).prepareStackTrace(this, callSites);
|
|
1073
|
+
} catch {
|
|
1074
|
+
return rawStack;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return rawStack;
|
|
1078
|
+
},
|
|
1079
|
+
set(value: string) {
|
|
1080
|
+
(this as any)[stackSymbol] = value;
|
|
1081
|
+
},
|
|
1082
|
+
configurable: true,
|
|
1083
|
+
enumerable: false,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Polyfill Error.captureStackTrace
|
|
1087
|
+
(Error as any).captureStackTrace = function(target: any, constructorOpt?: Function) {
|
|
1088
|
+
// Temporarily clear prepareStackTrace to get the raw stack string
|
|
1089
|
+
// (otherwise our .stack getter would call prepareStackTrace recursively)
|
|
1090
|
+
const savedPrepare = (Error as any).prepareStackTrace;
|
|
1091
|
+
(Error as any).prepareStackTrace = undefined;
|
|
1092
|
+
const err = new Error();
|
|
1093
|
+
const rawStack = err.stack || '';
|
|
1094
|
+
(Error as any).prepareStackTrace = savedPrepare;
|
|
1095
|
+
|
|
1096
|
+
// If prepareStackTrace is set, provide structured call sites
|
|
1097
|
+
if (typeof savedPrepare === 'function') {
|
|
1098
|
+
const callSites = buildCallSites(rawStack, constructorOpt);
|
|
1099
|
+
try {
|
|
1100
|
+
target.stack = savedPrepare(target, callSites);
|
|
1101
|
+
} catch (e) {
|
|
1102
|
+
console.warn('[almostnode] Error.prepareStackTrace threw:', e);
|
|
1103
|
+
target.stack = rawStack;
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
target.stack = rawStack;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
949
1111
|
/**
|
|
950
1112
|
* Execute code as a module (synchronous - backward compatible)
|
|
951
1113
|
*/
|
|
@@ -1085,6 +1247,90 @@ ${code}
|
|
|
1085
1247
|
getProcess(): Process {
|
|
1086
1248
|
return this.process;
|
|
1087
1249
|
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Create a REPL context that evaluates expressions and persists state.
|
|
1253
|
+
*
|
|
1254
|
+
* Returns an object with an `eval` method that:
|
|
1255
|
+
* - Returns the value of the last expression (unlike `execute` which returns module.exports)
|
|
1256
|
+
* - Persists variables between calls (`var x = 1` then `x` works)
|
|
1257
|
+
* - Has access to `require`, `console`, `process`, `Buffer` (same as execute)
|
|
1258
|
+
*
|
|
1259
|
+
* Security: The eval runs inside a Generator's local scope via direct eval,
|
|
1260
|
+
* NOT in the global scope. Only the runtime's own require/console/process are
|
|
1261
|
+
* exposed — the same sandbox boundary as execute(). Variables created in the
|
|
1262
|
+
* REPL are confined to the generator's closure and cannot leak to the page.
|
|
1263
|
+
*
|
|
1264
|
+
* Note: `const`/`let` are transformed to `var` so they persist across calls
|
|
1265
|
+
* (var hoists to the generator's function scope, const/let are block-scoped
|
|
1266
|
+
* to each eval call and would be lost).
|
|
1267
|
+
*/
|
|
1268
|
+
createREPL(): { eval: (code: string) => unknown } {
|
|
1269
|
+
const require = createRequire(
|
|
1270
|
+
this.vfs,
|
|
1271
|
+
this.fsShim,
|
|
1272
|
+
this.process,
|
|
1273
|
+
'/',
|
|
1274
|
+
this.moduleCache,
|
|
1275
|
+
this.options,
|
|
1276
|
+
this.processedCodeCache
|
|
1277
|
+
);
|
|
1278
|
+
const consoleWrapper = createConsoleWrapper(this.options.onConsole);
|
|
1279
|
+
const process = this.process;
|
|
1280
|
+
const buffer = bufferShim.Buffer;
|
|
1281
|
+
|
|
1282
|
+
// Use a Generator to maintain a persistent eval scope.
|
|
1283
|
+
// Generator functions preserve their local scope across yields, so
|
|
1284
|
+
// var declarations from eval() persist between calls. Direct eval
|
|
1285
|
+
// runs in the generator's scope (not global), providing isolation.
|
|
1286
|
+
const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor;
|
|
1287
|
+
const replGen = new GeneratorFunction(
|
|
1288
|
+
'require',
|
|
1289
|
+
'console',
|
|
1290
|
+
'process',
|
|
1291
|
+
'Buffer',
|
|
1292
|
+
`var __code, __result;
|
|
1293
|
+
while (true) {
|
|
1294
|
+
__code = yield;
|
|
1295
|
+
try {
|
|
1296
|
+
__result = eval(__code);
|
|
1297
|
+
yield { value: __result, error: null };
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
yield { value: undefined, error: e };
|
|
1300
|
+
}
|
|
1301
|
+
}`
|
|
1302
|
+
)(require, consoleWrapper, process, buffer);
|
|
1303
|
+
replGen.next(); // prime the generator
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
eval(code: string): unknown {
|
|
1307
|
+
// Transform const/let to var for persistence across REPL calls.
|
|
1308
|
+
// var declarations in direct eval are added to the enclosing function
|
|
1309
|
+
// scope (the generator), so they survive across yields.
|
|
1310
|
+
const transformed = code.replace(/^\s*(const|let)\s+/gm, 'var ');
|
|
1311
|
+
|
|
1312
|
+
// Try as expression first (wrapping in parens), fall back to statement.
|
|
1313
|
+
// replGen.next(code) sends code to the generator, which evals it and
|
|
1314
|
+
// yields the result — so the result is in the return value of .next().
|
|
1315
|
+
const exprResult = replGen.next('(' + transformed + ')').value as { value: unknown; error: unknown };
|
|
1316
|
+
if (!exprResult.error) {
|
|
1317
|
+
// Advance past the wait-for-code yield so it's ready for next call
|
|
1318
|
+
replGen.next();
|
|
1319
|
+
return exprResult.value;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Expression parse failed — advance past wait-for-code, then try as statement
|
|
1323
|
+
replGen.next();
|
|
1324
|
+
const stmtResult = replGen.next(transformed).value as { value: unknown; error: unknown };
|
|
1325
|
+
if (stmtResult.error) {
|
|
1326
|
+
replGen.next(); // advance past wait-for-code yield
|
|
1327
|
+
throw stmtResult.error;
|
|
1328
|
+
}
|
|
1329
|
+
replGen.next(); // advance past wait-for-code yield
|
|
1330
|
+
return stmtResult.value;
|
|
1331
|
+
},
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1088
1334
|
}
|
|
1089
1335
|
|
|
1090
1336
|
/**
|
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
|