@volter/tabnode 0.5.5 → 0.5.7

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.
@@ -7817,7 +7817,7 @@ class Pipe extends LibuvStreamWrap {
7817
7817
  if (native) return native.write(req, [bytes2], handle);
7818
7818
  if (handle === void 0 || handle === null) return super.dispatchWrite(req, bytes2);
7819
7819
  const sent = handle;
7820
- const given = sent.peer !== null ? sent.duplicate() : sent;
7820
+ const given = sent.listening === true ? sent : sent.duplicate();
7821
7821
  __adoptHandle(given, ownerOf(this.peer ?? this));
7822
7822
  const status = super.dispatchWrite(req, bytes2, given);
7823
7823
  if (status !== 0 && given !== sent) given.close();
@@ -70769,12 +70769,27 @@ function programExists(file, cwd, env) {
70769
70769
  const name = __substrateProgramName(file);
70770
70770
  const bare = name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
70771
70771
  if (shellCommandNames().has(bare)) return true;
70772
- if (name.includes("/")) return existsInTree(resolve(cwd ?? "/", name));
70772
+ if (name.includes("/")) {
70773
+ const path2 = resolve(cwd ?? "/", name);
70774
+ return existsInTree(path2) && !isRegisteredProgramStub(path2);
70775
+ }
70773
70776
  for (const dir of (env.PATH ?? "/usr/local/bin:/usr/bin:/bin:/node_modules/.bin").split(":")) {
70774
- if (dir.length > 0 && existsInTree(`${dir}/${name}`.replace(/\/+/gu, "/"))) return true;
70777
+ const path2 = `${dir}/${name}`.replace(/\/+/gu, "/");
70778
+ if (dir.length > 0 && existsInTree(path2) && !isRegisteredProgramStub(path2)) return true;
70775
70779
  }
70776
70780
  return false;
70777
70781
  }
70782
+ const REGISTERED_PROGRAM_STUB = /^#!\/bin\/sh\n# [\w.+-]+: runtime-registered program\n/u;
70783
+ function isRegisteredProgramStub(path2) {
70784
+ if (!currentVfs$1) return false;
70785
+ try {
70786
+ const stat = currentVfs$1.statSync(path2);
70787
+ if (!stat.isFile() || stat.size > 256) return false;
70788
+ return REGISTERED_PROGRAM_STUB.test(String(currentVfs$1.readFileSync(path2, "utf8")));
70789
+ } catch {
70790
+ return false;
70791
+ }
70792
+ }
70778
70793
  let __nextChildRun = 1;
70779
70794
  function startChildRun(request) {
70780
70795
  var _a3;
@@ -74067,7 +74082,7 @@ function transformDynamicImportsRegex(code2) {
74067
74082
  return code2.replace(new RegExp("(?<![.$\\w#])import\\s*\\(", "g"), "__dynamicImport(");
74068
74083
  }
74069
74084
  function __substrateRewriteDynamicImportsInScript(code2) {
74070
- if (!new RegExp("(?<![.$\\w#])import\\s*\\(").test(code2)) return code2;
74085
+ if (!new RegExp("(?<![.$\\w#])(?:import|eval)\\s*\\(").test(code2)) return code2;
74071
74086
  let ast;
74072
74087
  try {
74073
74088
  ast = parse3(code2, { ecmaVersion: "latest", sourceType: "script", allowReturnOutsideFunction: true, allowHashBang: true, allowAwaitOutsideFunction: true });
@@ -74076,14 +74091,30 @@ function __substrateRewriteDynamicImportsInScript(code2) {
74076
74091
  }
74077
74092
  const replacements = [];
74078
74093
  walkAst(ast, (node) => {
74079
- if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6]);
74094
+ if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6, "__dynamicImport"]);
74095
+ collectDirectEval(node, replacements);
74080
74096
  });
74081
74097
  if (replacements.length === 0) return code2;
74082
- replacements.sort((a, b) => b[0] - a[0]);
74098
+ return applyReplacements(code2, replacements);
74099
+ }
74100
+ function collectDirectEval(node, replacements) {
74101
+ var _a3, _b3;
74102
+ if (node.type !== "CallExpression" || node.optional || ((_a3 = node.callee) == null ? void 0 : _a3.type) !== "Identifier" || node.callee.name !== "eval") return;
74103
+ const source = (_b3 = node.arguments) == null ? void 0 : _b3[0];
74104
+ if (!source || source.type === "SpreadElement") return;
74105
+ replacements.push([source.start, source.start, "__substrateEvalSource("]);
74106
+ replacements.push([source.end, source.end, ")"]);
74107
+ }
74108
+ function applyReplacements(code2, replacements) {
74109
+ replacements.sort((a, b) => b[0] - a[0] || b[1] - a[1]);
74083
74110
  let out = code2;
74084
- for (const [start, end] of replacements) out = out.slice(0, start) + "__dynamicImport" + out.slice(end);
74111
+ for (const [start, end, text] of replacements) out = out.slice(0, start) + text + out.slice(end);
74085
74112
  return out;
74086
74113
  }
74114
+ Object.defineProperty(globalThis, "__substrateEvalSource", {
74115
+ configurable: true,
74116
+ value: (source) => typeof source === "string" ? __substrateRewriteDynamicImportsInScript(source) : source
74117
+ });
74087
74118
  function transformEsmToCjs(code2, filename) {
74088
74119
  const maybeEsm = __substrateHasEsmSyntax(code2);
74089
74120
  if (!maybeEsm) return __substrateRewriteDynamicImportsInScript(code2);
@@ -74107,17 +74138,14 @@ function transformEsmToCjsAst(code2, filename) {
74107
74138
  if (node.type === "ImportExpression") {
74108
74139
  deepReplacements.push([node.start, node.start + 6, "__dynamicImport"]);
74109
74140
  }
74141
+ collectDirectEval(node, deepReplacements);
74110
74142
  });
74111
74143
  const hasImportDecl = ast.body.some((n8) => n8.type === "ImportDeclaration");
74112
74144
  const hasExportDecl = ast.body.some((n8) => {
74113
74145
  var _a3;
74114
74146
  return (_a3 = n8.type) == null ? void 0 : _a3.startsWith("Export");
74115
74147
  });
74116
- let transformed = code2;
74117
- deepReplacements.sort((a, b) => b[0] - a[0]);
74118
- for (const [start, end, replacement] of deepReplacements) {
74119
- transformed = transformed.slice(0, start) + replacement + transformed.slice(end);
74120
- }
74148
+ let transformed = applyReplacements(code2, deepReplacements);
74121
74149
  if (hasImportDecl || hasExportDecl) {
74122
74150
  transformed = transformEsmToCjsSimple(transformed);
74123
74151
  if (hasExportDecl) {
package/dist/index.mjs CHANGED
@@ -7980,7 +7980,7 @@ class Pipe extends LibuvStreamWrap {
7980
7980
  if (native) return native.write(req, [bytes2], handle);
7981
7981
  if (handle === void 0 || handle === null) return super.dispatchWrite(req, bytes2);
7982
7982
  const sent = handle;
7983
- const given = sent.peer !== null ? sent.duplicate() : sent;
7983
+ const given = sent.listening === true ? sent : sent.duplicate();
7984
7984
  __adoptHandle(given, ownerOf(this.peer ?? this));
7985
7985
  const status = super.dispatchWrite(req, bytes2, given);
7986
7986
  if (status !== 0 && given !== sent) given.close();
@@ -30461,12 +30461,27 @@ function programExists(file, cwd, env) {
30461
30461
  const name = __substrateProgramName(file);
30462
30462
  const bare = name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
30463
30463
  if (shellCommandNames().has(bare)) return true;
30464
- if (name.includes("/")) return existsInTree(resolve(cwd ?? "/", name));
30464
+ if (name.includes("/")) {
30465
+ const path2 = resolve(cwd ?? "/", name);
30466
+ return existsInTree(path2) && !isRegisteredProgramStub(path2);
30467
+ }
30465
30468
  for (const dir of (env.PATH ?? "/usr/local/bin:/usr/bin:/bin:/node_modules/.bin").split(":")) {
30466
- if (dir.length > 0 && existsInTree(`${dir}/${name}`.replace(/\/+/gu, "/"))) return true;
30469
+ const path2 = `${dir}/${name}`.replace(/\/+/gu, "/");
30470
+ if (dir.length > 0 && existsInTree(path2) && !isRegisteredProgramStub(path2)) return true;
30467
30471
  }
30468
30472
  return false;
30469
30473
  }
30474
+ const REGISTERED_PROGRAM_STUB = /^#!\/bin\/sh\n# [\w.+-]+: runtime-registered program\n/u;
30475
+ function isRegisteredProgramStub(path2) {
30476
+ if (!currentVfs$1) return false;
30477
+ try {
30478
+ const stat = currentVfs$1.statSync(path2);
30479
+ if (!stat.isFile() || stat.size > 256) return false;
30480
+ return REGISTERED_PROGRAM_STUB.test(String(currentVfs$1.readFileSync(path2, "utf8")));
30481
+ } catch {
30482
+ return false;
30483
+ }
30484
+ }
30470
30485
  let __nextChildRun = 1;
30471
30486
  function startChildRun(request) {
30472
30487
  var _a2;
@@ -33760,7 +33775,7 @@ function transformDynamicImportsRegex(code2) {
33760
33775
  return code2.replace(new RegExp("(?<![.$\\w#])import\\s*\\(", "g"), "__dynamicImport(");
33761
33776
  }
33762
33777
  function __substrateRewriteDynamicImportsInScript(code2) {
33763
- if (!new RegExp("(?<![.$\\w#])import\\s*\\(").test(code2)) return code2;
33778
+ if (!new RegExp("(?<![.$\\w#])(?:import|eval)\\s*\\(").test(code2)) return code2;
33764
33779
  let ast;
33765
33780
  try {
33766
33781
  ast = parse3(code2, { ecmaVersion: "latest", sourceType: "script", allowReturnOutsideFunction: true, allowHashBang: true, allowAwaitOutsideFunction: true });
@@ -33769,14 +33784,30 @@ function __substrateRewriteDynamicImportsInScript(code2) {
33769
33784
  }
33770
33785
  const replacements = [];
33771
33786
  walkAst(ast, (node) => {
33772
- if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6]);
33787
+ if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6, "__dynamicImport"]);
33788
+ collectDirectEval(node, replacements);
33773
33789
  });
33774
33790
  if (replacements.length === 0) return code2;
33775
- replacements.sort((a, b) => b[0] - a[0]);
33791
+ return applyReplacements(code2, replacements);
33792
+ }
33793
+ function collectDirectEval(node, replacements) {
33794
+ var _a2, _b2;
33795
+ if (node.type !== "CallExpression" || node.optional || ((_a2 = node.callee) == null ? void 0 : _a2.type) !== "Identifier" || node.callee.name !== "eval") return;
33796
+ const source = (_b2 = node.arguments) == null ? void 0 : _b2[0];
33797
+ if (!source || source.type === "SpreadElement") return;
33798
+ replacements.push([source.start, source.start, "__substrateEvalSource("]);
33799
+ replacements.push([source.end, source.end, ")"]);
33800
+ }
33801
+ function applyReplacements(code2, replacements) {
33802
+ replacements.sort((a, b) => b[0] - a[0] || b[1] - a[1]);
33776
33803
  let out = code2;
33777
- for (const [start, end] of replacements) out = out.slice(0, start) + "__dynamicImport" + out.slice(end);
33804
+ for (const [start, end, text] of replacements) out = out.slice(0, start) + text + out.slice(end);
33778
33805
  return out;
33779
33806
  }
33807
+ Object.defineProperty(globalThis, "__substrateEvalSource", {
33808
+ configurable: true,
33809
+ value: (source) => typeof source === "string" ? __substrateRewriteDynamicImportsInScript(source) : source
33810
+ });
33780
33811
  function transformEsmToCjs(code2, filename) {
33781
33812
  const maybeEsm = __substrateHasEsmSyntax(code2);
33782
33813
  if (!maybeEsm) return __substrateRewriteDynamicImportsInScript(code2);
@@ -33800,17 +33831,14 @@ function transformEsmToCjsAst(code2, filename) {
33800
33831
  if (node.type === "ImportExpression") {
33801
33832
  deepReplacements.push([node.start, node.start + 6, "__dynamicImport"]);
33802
33833
  }
33834
+ collectDirectEval(node, deepReplacements);
33803
33835
  });
33804
33836
  const hasImportDecl = ast.body.some((n) => n.type === "ImportDeclaration");
33805
33837
  const hasExportDecl = ast.body.some((n) => {
33806
33838
  var _a2;
33807
33839
  return (_a2 = n.type) == null ? void 0 : _a2.startsWith("Export");
33808
33840
  });
33809
- let transformed = code2;
33810
- deepReplacements.sort((a, b) => b[0] - a[0]);
33811
- for (const [start, end, replacement] of deepReplacements) {
33812
- transformed = transformed.slice(0, start) + replacement + transformed.slice(end);
33813
- }
33841
+ let transformed = applyReplacements(code2, deepReplacements);
33814
33842
  if (hasImportDecl || hasExportDecl) {
33815
33843
  transformed = transformEsmToCjsSimple(transformed);
33816
33844
  if (hasExportDecl) {
@@ -35655,7 +35683,7 @@ class WorkerRuntime {
35655
35683
  this.worker = new Worker(
35656
35684
  new URL(
35657
35685
  /* @vite-ignore */
35658
- "/assets/runtime-worker-iInsmIAS.js",
35686
+ "/assets/runtime-worker-CZQ5Vxoj.js",
35659
35687
  import.meta.url
35660
35688
  ),
35661
35689
  { type: "module" }
@@ -38023,7 +38051,11 @@ function resetServerBridge() {
38023
38051
  }
38024
38052
  const scopes = /* @__PURE__ */ new WeakMap();
38025
38053
  class NativeStreamScope {
38026
- constructor(limits, emit2) {
38054
+ /**
38055
+ * `ownerPid` names the process whose worker this scope serves: a listener
38056
+ * it opens is that process's socket, as the container's /proc shows it.
38057
+ */
38058
+ constructor(limits, emit2, ownerPid) {
38027
38059
  __publicField(this, "entries", /* @__PURE__ */ new Map());
38028
38060
  __publicField(this, "descriptors", /* @__PURE__ */ new Map());
38029
38061
  __publicField(this, "writes", /* @__PURE__ */ new Map());
@@ -38033,6 +38065,7 @@ class NativeStreamScope {
38033
38065
  __publicField(this, "disposed", false);
38034
38066
  __publicField(this, "limits");
38035
38067
  this.emit = emit2;
38068
+ this.ownerPid = ownerPid;
38036
38069
  if ([limits.maxHandles, limits.maxReadChunkBytes, limits.maxQueuedBytes, limits.maxQueuedHandles, limits.maxPendingWriteBytes].some((value) => !Number.isSafeInteger(value) || value < 1) || limits.maxReadChunkBytes > 2147483647 || limits.maxReadChunkBytes > limits.maxQueuedBytes || limits.maxReadChunkBytes > limits.maxPendingWriteBytes) throw new Error("Invalid native stream resource limits.");
38037
38070
  this.limits = Object.freeze({ ...limits });
38038
38071
  }
@@ -38222,7 +38255,7 @@ class NativeStreamScope {
38222
38255
  if (bytes2.byteLength > this.limits.maxReadChunkBytes || this.pendingWriteBytes + bytes2.byteLength > this.limits.maxPendingWriteBytes) return Promise.resolve(UV_ENOBUFS);
38223
38256
  const sent = sentId === void 0 ? void 0 : (_b2 = this.entry(sentId)) == null ? void 0 : _b2.handle;
38224
38257
  if (sentId !== void 0 && !sent) return Promise.resolve(UV_EBADF);
38225
- if (sent && (!(handle instanceof Pipe) || !sent.peer || sent.listening || bytes2.byteLength === 0)) return Promise.resolve(UV_ENOTSUP);
38258
+ if (sent && (!(handle instanceof Pipe) || sent.listening || bytes2.byteLength === 0)) return Promise.resolve(UV_ENOTSUP);
38226
38259
  const owned = new Uint8Array(bytes2.byteLength);
38227
38260
  owned.set(bytes2);
38228
38261
  const retained = sent == null ? void 0 : sent.duplicate();
@@ -38317,6 +38350,10 @@ class NativeStreamScope {
38317
38350
  if (failures.length) throw new AggregateError(failures, "Native stream scope cleanup failed.");
38318
38351
  }
38319
38352
  }
38353
+ function nativeStreamOwnerPid(handle) {
38354
+ var _a2, _b2;
38355
+ return (_b2 = (_a2 = scopes.get(handle)) == null ? void 0 : _a2.ownerPid) == null ? void 0 : _b2.call(_a2);
38356
+ }
38320
38357
  let __nextRunName = 1;
38321
38358
  function createContainer(options) {
38322
38359
  const vfs2 = options && options.vfs || new VirtualFS();
@@ -38374,7 +38411,8 @@ function createContainer(options) {
38374
38411
  portPid: (port) => {
38375
38412
  var _a2;
38376
38413
  const listener = listenerOnPort(port);
38377
- return listener ? (_a2 = runPid(ownerOf(listener))) == null ? void 0 : _a2.pid : void 0;
38414
+ if (!listener) return void 0;
38415
+ return ((_a2 = runPid(ownerOf(listener))) == null ? void 0 : _a2.pid) ?? nativeStreamOwnerPid(listener);
38378
38416
  },
38379
38417
  /**
38380
38418
  * Deliver a signal to the named run as another process's `kill(pid)` does:
@@ -94,6 +94,7 @@ export interface NativeStreamTransport {
94
94
  */
95
95
  export declare class NativeStreamScope {
96
96
  private readonly emit;
97
+ readonly ownerPid?: (() => number | undefined) | undefined;
97
98
  private readonly entries;
98
99
  private readonly descriptors;
99
100
  private readonly writes;
@@ -102,7 +103,11 @@ export declare class NativeStreamScope {
102
103
  private pendingWriteBytes;
103
104
  private disposed;
104
105
  private readonly limits;
105
- constructor(limits: NativeStreamLimits, emit: (event: NativeStreamEvent) => void);
106
+ /**
107
+ * `ownerPid` names the process whose worker this scope serves: a listener
108
+ * it opens is that process's socket, as the container's /proc shows it.
109
+ */
110
+ constructor(limits: NativeStreamLimits, emit: (event: NativeStreamEvent) => void, ownerPid?: (() => number | undefined) | undefined);
106
111
  private entry;
107
112
  private publish;
108
113
  private claim;
@@ -119,3 +124,5 @@ export declare class NativeStreamScope {
119
124
  private close;
120
125
  dispose(): void;
121
126
  }
127
+ /** The process a handle opened for another worker's process belongs to, where a scope holds it. */
128
+ export declare function nativeStreamOwnerPid(handle: object): number | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ import { runCommand, registerRunStreams, releaseRunStreams, sendStdin } from './
56
56
  import { Server as NetServer, __releaseOwnedHandles, type Socket as NetSocket } from './node-lib/net-module';
57
57
  import { __adoptHandle, ownerOf, type OwnedHandle } from './node-lib/binding/handles';
58
58
  import { listenerOnPort } from './node-lib/binding/tcp_wrap';
59
+ import { nativeStreamOwnerPid } from './native-stream-owner';
59
60
 
60
61
  import { __currentProcessToken, __runFor, __signalOwnedProcess, __stopOwnedProcess, runPid, processByPid } from './process-tokens';
61
62
  export { runPid, processByPid };
@@ -215,7 +216,9 @@ export function createContainer(options?: ContainerOptions): {
215
216
  processPorts: (token: string): number[] => __ownedServerPorts(token),
216
217
  portPid: (port: number): number | undefined => {
217
218
  const listener = listenerOnPort(port);
218
- return listener ? runPid(ownerOf(listener as unknown as OwnedHandle))?.pid : undefined;
219
+ if (!listener) return undefined;
220
+ // a guest's own listener, or one this engine holds for another worker's process
221
+ return runPid(ownerOf(listener as unknown as OwnedHandle))?.pid ?? nativeStreamOwnerPid(listener);
219
222
  },
220
223
  /**
221
224
  * Deliver a signal to the named run as another process's `kill(pid)` does:
@@ -56,7 +56,11 @@ export class NativeStreamScope {
56
56
  private disposed = false;
57
57
  private readonly limits: Readonly<NativeStreamLimits>;
58
58
 
59
- constructor(limits: NativeStreamLimits, private readonly emit: (event: NativeStreamEvent) => void) {
59
+ /**
60
+ * `ownerPid` names the process whose worker this scope serves: a listener
61
+ * it opens is that process's socket, as the container's /proc shows it.
62
+ */
63
+ constructor(limits: NativeStreamLimits, private readonly emit: (event: NativeStreamEvent) => void, readonly ownerPid?: () => number | undefined) {
60
64
  if ([limits.maxHandles, limits.maxReadChunkBytes, limits.maxQueuedBytes, limits.maxQueuedHandles, limits.maxPendingWriteBytes].some(value => !Number.isSafeInteger(value) || value < 1)
61
65
  || limits.maxReadChunkBytes > 0x7fffffff
62
66
  || limits.maxReadChunkBytes > limits.maxQueuedBytes
@@ -236,7 +240,11 @@ export class NativeStreamScope {
236
240
  || this.pendingWriteBytes + bytes.byteLength > this.limits.maxPendingWriteBytes) return Promise.resolve(UV_ENOBUFS);
237
241
  const sent = sentId === undefined ? undefined : this.entry(sentId)?.handle;
238
242
  if (sentId !== undefined && !sent) return Promise.resolve(UV_EBADF);
239
- if (sent && (!(handle instanceof Pipe) || !sent.peer || sent.listening || bytes.byteLength === 0)) return Promise.resolve(UV_ENOTSUP);
243
+ // A connection crosses whether or not its other end is still there: Node
244
+ // passes a socket whose peer has closed, and the receiver reads its EOF.
245
+ // Refusing it failed the whole channel write (`write ENOTSUP`), which ended
246
+ // VS Code's extension host when a reconnecting client had already gone.
247
+ if (sent && (!(handle instanceof Pipe) || sent.listening || bytes.byteLength === 0)) return Promise.resolve(UV_ENOTSUP);
240
248
  // Retain the descriptor while capacity is unavailable; the sender can close
241
249
  // its original after submitting the write without invalidating this copy.
242
250
  const owned = new Uint8Array(bytes.byteLength);
@@ -315,3 +323,8 @@ export class NativeStreamScope {
315
323
  if (failures.length) throw new AggregateError(failures, 'Native stream scope cleanup failed.');
316
324
  }
317
325
  }
326
+
327
+ /** The process a handle opened for another worker's process belongs to, where a scope holds it. */
328
+ export function nativeStreamOwnerPid(handle: object): number | undefined {
329
+ return scopes.get(handle as NativeHandle)?.ownerPid?.();
330
+ }
@@ -165,10 +165,11 @@ export class Pipe extends LibuvStreamWrap {
165
165
  // A descriptor crosses as a duplicate, as `SCM_RIGHTS` hands one over: the
166
166
  // receiver gets its own handle on the same connection, so the sender's
167
167
  // close on `NODE_HANDLE_ACK` -- which Node's `child_process.js` does the
168
- // moment the receiver confirms -- ends nothing. A handle with no pairing,
169
- // a listening server's, has nothing to duplicate and crosses as it is.
170
- const sent = handle as LibuvStreamWrap;
171
- const given = sent.peer !== null ? sent.duplicate() : sent;
168
+ // moment the receiver confirms -- ends nothing. A connection is duplicated
169
+ // whether or not its other end is still there; a listening server has no
170
+ // connection to duplicate and crosses as it is.
171
+ const sent = handle as LibuvStreamWrap & { listening?: boolean };
172
+ const given = sent.listening === true ? sent : sent.duplicate();
172
173
  // The receiving run owns this reference; the sender retains its own until
173
174
  // Node closes it on acknowledgement, or continues using it with keepOpen.
174
175
  __adoptHandle(given, ownerOf(this.peer ?? this));
package/src/runtime.ts CHANGED
@@ -343,23 +343,49 @@ function transformDynamicImportsRegex(code: string): string {
343
343
  * saw no import at all.
344
344
  */
345
345
  function __substrateRewriteDynamicImportsInScript(code: string): string {
346
- if (!/(?<![.$\w#])import\s*\(/.test(code)) return code;
346
+ if (!/(?<![.$\w#])(?:import|eval)\s*\(/.test(code)) return code;
347
347
  let ast;
348
348
  try {
349
349
  ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "script", allowReturnOutsideFunction: true, allowHashBang: true, allowAwaitOutsideFunction: true });
350
350
  } catch {
351
351
  return code;
352
352
  }
353
- const replacements: Array<[number, number]> = [];
353
+ const replacements: Array<[number, number, string]> = [];
354
354
  walkAst(ast, (node: any) => {
355
- if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6]);
355
+ if (node.type === "ImportExpression") replacements.push([node.start, node.start + 6, "__dynamicImport"]);
356
+ collectDirectEval(node, replacements);
356
357
  });
357
358
  if (replacements.length === 0) return code;
358
- replacements.sort((a, b) => b[0] - a[0]);
359
+ return applyReplacements(code, replacements);
360
+ }
361
+
362
+ /**
363
+ * A direct `eval` runs source the module builds at run time, and Node runs an
364
+ * `import()` in it through the module's loader: Playwright loads an ES config
365
+ * with ``eval(`import(${url})`)``. The call stays a direct eval, so the
366
+ * source keeps the module's scope, `__dynamicImport` included; only the
367
+ * source it is given is rewritten the way the module's own was.
368
+ */
369
+ function collectDirectEval(node: any, replacements: Array<[number, number, string]>): void {
370
+ if (node.type !== "CallExpression" || node.optional || node.callee?.type !== "Identifier" || node.callee.name !== "eval") return;
371
+ const source = node.arguments?.[0];
372
+ if (!source || source.type === "SpreadElement") return;
373
+ replacements.push([source.start, source.start, "__substrateEvalSource("]);
374
+ replacements.push([source.end, source.end, ")"]);
375
+ }
376
+
377
+ /** Applies insertions and replacements from the end, so earlier offsets hold. */
378
+ function applyReplacements(code: string, replacements: Array<[number, number, string]>): string {
379
+ replacements.sort((a, b) => b[0] - a[0] || b[1] - a[1]);
359
380
  let out = code;
360
- for (const [start, end] of replacements) out = out.slice(0, start) + "__dynamicImport" + out.slice(end);
381
+ for (const [start, end, text] of replacements) out = out.slice(0, start) + text + out.slice(end);
361
382
  return out;
362
383
  }
384
+
385
+ Object.defineProperty(globalThis, "__substrateEvalSource", {
386
+ configurable: true,
387
+ value: (source: unknown): unknown => typeof source === "string" ? __substrateRewriteDynamicImportsInScript(source) : source,
388
+ });
363
389
  function transformEsmToCjs(code: string, filename: string): string {
364
390
  // Quick check: does the code have any ESM-like patterns?
365
391
  const maybeEsm = __substrateHasEsmSyntax(code);
@@ -401,6 +427,7 @@ function transformEsmToCjsAst(code: string, filename: string): string {
401
427
  // Replace just the 'import' keyword, preserving the (...) part
402
428
  deepReplacements.push([node.start, node.start + 6, '__dynamicImport']);
403
429
  }
430
+ collectDirectEval(node, deepReplacements);
404
431
  });
405
432
 
406
433
  // Check for actual import/export declarations
@@ -408,11 +435,7 @@ function transformEsmToCjsAst(code: string, filename: string): string {
408
435
  const hasExportDecl = ast.body.some((n: any) => n.type?.startsWith('Export'));
409
436
 
410
437
  // Apply deep replacements from end to start (preserves earlier positions)
411
- let transformed = code;
412
- deepReplacements.sort((a, b) => b[0] - a[0]);
413
- for (const [start, end, replacement] of deepReplacements) {
414
- transformed = transformed.slice(0, start) + replacement + transformed.slice(end);
415
- }
438
+ let transformed = applyReplacements(code, deepReplacements);
416
439
 
417
440
  // Transform import/export declarations (re-parses the modified code)
418
441
  if (hasImportDecl || hasExportDecl) {
@@ -1650,13 +1650,34 @@ function programExists(file: string, cwd: string | undefined, env: Record<string
1650
1650
  // what `spawn('/usr/bin/env', …)` means and what a Dockerfile's shebang
1651
1651
  // reaches. A file in the tree is a program too, and so is one on PATH.
1652
1652
  if (shellCommandNames().has(bare)) return true;
1653
- if (name.includes('/')) return existsInTree(__resolvePath(cwd ?? '/', name));
1653
+ if (name.includes('/')) {
1654
+ const path = __resolvePath(cwd ?? '/', name);
1655
+ return existsInTree(path) && !isRegisteredProgramStub(path);
1656
+ }
1654
1657
  for (const dir of (env.PATH ?? '/usr/local/bin:/usr/bin:/bin:/node_modules/.bin').split(':')) {
1655
- if (dir.length > 0 && existsInTree(`${dir}/${name}`.replace(/\/+/gu, '/'))) return true;
1658
+ const path = `${dir}/${name}`.replace(/\/+/gu, '/');
1659
+ if (dir.length > 0 && existsInTree(path) && !isRegisteredProgramStub(path)) return true;
1656
1660
  }
1657
1661
  return false;
1658
1662
  }
1659
1663
 
1664
+ /**
1665
+ * A program the page registered is not a file, so a host places a stub where
1666
+ * a loader looks for it (`#!/bin/sh` then `# <name>: runtime-registered
1667
+ * program`), for a caller that checks the path exists or walks PATH. The stub
1668
+ * is the page's program, not a script of this engine's: it is relayed to the
1669
+ * host, whose process host maps the pathname to the program by its basename.
1670
+ */
1671
+ const REGISTERED_PROGRAM_STUB = /^#!\/bin\/sh\n# [\w.+-]+: runtime-registered program\n/u;
1672
+ function isRegisteredProgramStub(path: string): boolean {
1673
+ if (!currentVfs) return false;
1674
+ try {
1675
+ const stat = currentVfs.statSync(path);
1676
+ if (!stat.isFile() || stat.size > 256) return false;
1677
+ return REGISTERED_PROGRAM_STUB.test(String(currentVfs.readFileSync(path, 'utf8')));
1678
+ } catch { return false; }
1679
+ }
1680
+
1660
1681
  /** The runs a `Process` handle started, so a later name never lands on a live one. */
1661
1682
  let __nextChildRun = 1;
1662
1683