env-runner 0.1.9 → 0.1.11

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 (45) hide show
  1. package/README.md +130 -16
  2. package/dist/THIRD-PARTY-LICENSES.md +19 -0
  3. package/dist/_chunks/base-runner.d.mts +80 -3
  4. package/dist/_chunks/base-runner.mjs +108 -44
  5. package/dist/_chunks/libs/cjs-module-lexer.mjs +1 -1
  6. package/dist/_chunks/libs/es-module-lexer.mjs +83 -0
  7. package/dist/_chunks/runner.d.mts +3 -3
  8. package/dist/_chunks/runner.mjs +6 -3
  9. package/dist/_chunks/runner2.d.mts +15 -3
  10. package/dist/_chunks/runner2.mjs +145 -56
  11. package/dist/_chunks/runner3.d.mts +3 -3
  12. package/dist/_chunks/runner3.mjs +3 -3
  13. package/dist/_chunks/runner4.d.mts +4 -4
  14. package/dist/_chunks/runner4.mjs +3 -2
  15. package/dist/_chunks/runner5.d.mts +4 -4
  16. package/dist/_chunks/runner5.mjs +3 -2
  17. package/dist/_chunks/server.mjs +60 -17
  18. package/dist/_chunks/types.d.mts +15 -3
  19. package/dist/_chunks/virtual-loader.mjs +63 -0
  20. package/dist/_chunks/worker-utils.mjs +123 -9
  21. package/dist/cli.mjs +1 -1
  22. package/dist/index.d.mts +52 -14
  23. package/dist/index.mjs +6 -6
  24. package/dist/runners/bun-process/runner.d.mts +2 -2
  25. package/dist/runners/bun-process/runner.mjs +12 -2
  26. package/dist/runners/bun-process/worker.mjs +23 -3
  27. package/dist/runners/deno-process/runner.d.mts +2 -2
  28. package/dist/runners/deno-process/runner.mjs +1 -1
  29. package/dist/runners/deno-process/worker.mjs +22 -3
  30. package/dist/runners/miniflare/runner.d.mts +2 -2
  31. package/dist/runners/miniflare/runner.mjs +1 -1
  32. package/dist/runners/netlify/runner.d.mts +2 -2
  33. package/dist/runners/netlify/runner.mjs +1 -1
  34. package/dist/runners/node-process/runner.d.mts +2 -2
  35. package/dist/runners/node-process/runner.mjs +4 -2
  36. package/dist/runners/node-process/worker.mjs +23 -3
  37. package/dist/runners/node-worker/runner.d.mts +2 -2
  38. package/dist/runners/node-worker/runner.mjs +1 -1
  39. package/dist/runners/node-worker/worker.mjs +22 -3
  40. package/dist/runners/self/runner.d.mts +3 -2
  41. package/dist/runners/self/runner.mjs +6 -3
  42. package/dist/runners/vercel/runner.d.mts +2 -2
  43. package/dist/runners/vercel/runner.mjs +1 -1
  44. package/dist/vite.d.mts +1 -1
  45. package/package.json +12 -11
@@ -5,6 +5,8 @@ var RunnerManager = class {
5
5
  _messageListeners = /* @__PURE__ */ new Set();
6
6
  _closed = false;
7
7
  _reloading = false;
8
+ _moduleInvalidated = false;
9
+ _pendingModuleReload;
8
10
  _closeListeners = /* @__PURE__ */ new Set();
9
11
  _readyListeners = /* @__PURE__ */ new Set();
10
12
  constructor(runner) {
@@ -18,17 +20,35 @@ var RunnerManager = class {
18
20
  }
19
21
  async reload(runner) {
20
22
  this._reloading = true;
21
- const prev = this._runner;
22
- this._detach();
23
- this._attach(runner);
24
- this._reloading = false;
25
- if (prev) await prev.close();
23
+ try {
24
+ runner ??= await this._createRunner();
25
+ const prev = this._runner;
26
+ this._detach();
27
+ this._attach(runner);
28
+ this._moduleInvalidated = false;
29
+ if (prev) await prev.close();
30
+ } finally {
31
+ this._reloading = false;
32
+ }
33
+ }
34
+ _createRunner() {
35
+ throw new Error("reload() requires a runner argument (this manager has no runner factory)");
26
36
  }
27
- fetch = async (input, init) => {
37
+ fetch = (input, init) => this._fetch(input, init);
38
+ async _fetch(input, init) {
28
39
  const runner = await this._waitForRunner();
29
40
  if (!runner) return new Response("Runner is unavailable", { status: 503 });
41
+ if (this._moduleInvalidated) await this._flushInvalidation(runner);
30
42
  return runner.fetch(input, init);
31
- };
43
+ }
44
+ _flushInvalidation(runner) {
45
+ this._pendingModuleReload ??= Promise.resolve(runner.reloadModule?.()).then(() => {
46
+ this._moduleInvalidated = false;
47
+ }).finally(() => {
48
+ this._pendingModuleReload = void 0;
49
+ });
50
+ return this._pendingModuleReload;
51
+ }
32
52
  upgrade = (context) => {
33
53
  this._runner?.upgrade?.(context);
34
54
  };
@@ -93,7 +113,13 @@ var RunnerManager = class {
93
113
  }
94
114
  async reloadModule(timeout) {
95
115
  if (!this._runner?.reloadModule) throw new Error("Active runner does not support reloadModule()");
96
- return this._runner.reloadModule(timeout);
116
+ await this._runner.reloadModule(timeout);
117
+ this._moduleInvalidated = false;
118
+ }
119
+ async invalidateModule(specifier, timeout) {
120
+ if (!this._runner?.invalidateModule) throw new Error("Active runner does not support invalidateModule()");
121
+ await this._runner.invalidateModule(specifier, timeout);
122
+ this._moduleInvalidated = true;
97
123
  }
98
124
  async close() {
99
125
  this._closed = true;
@@ -102,6 +128,9 @@ var RunnerManager = class {
102
128
  this._detach();
103
129
  if (runner) await runner.close();
104
130
  }
131
+ async [Symbol.asyncDispose]() {
132
+ await this.close();
133
+ }
105
134
  onClose(listener) {
106
135
  this._closeListeners.add(listener);
107
136
  }
@@ -179,6 +208,7 @@ var EnvServer = class extends RunnerManager {
179
208
  _watchers = [];
180
209
  _reloadTimeout;
181
210
  _reloadListeners = /* @__PURE__ */ new Set();
211
+ _startPromise;
182
212
  runner = null;
183
213
  onReload(listener) {
184
214
  this._reloadListeners.add(listener);
@@ -190,18 +220,32 @@ var EnvServer = class extends RunnerManager {
190
220
  super();
191
221
  this._opts = opts;
192
222
  }
193
- async start() {
194
- this.runner = await this._createRunner();
195
- await this.reload(this.runner);
196
- if (this._opts.watch) this._startWatching();
197
- return this;
223
+ start() {
224
+ this._startPromise ??= this._start().catch((error) => {
225
+ this._startPromise = void 0;
226
+ throw error;
227
+ });
228
+ return this._startPromise;
229
+ }
230
+ async reload(runner) {
231
+ this.runner = runner ?? await this._createRunner();
232
+ await super.reload(this.runner);
198
233
  }
199
234
  async close() {
200
235
  this._stopWatching();
201
236
  await super.close();
202
237
  }
238
+ async _fetch(input, init) {
239
+ if (!this.closed) await this.start();
240
+ return super._fetch(input, init);
241
+ }
242
+ async _start() {
243
+ await this.reload();
244
+ if (this._opts.watch) this._startWatching();
245
+ return this;
246
+ }
203
247
  async _createRunner() {
204
- return loadRunner(this._opts.runner, {
248
+ return loadRunner(this._opts.runner || "node-worker", {
205
249
  name: this._opts.name || this._opts.entry,
206
250
  hooks: this._opts.hooks,
207
251
  data: {
@@ -229,8 +273,7 @@ var EnvServer = class extends RunnerManager {
229
273
  clearTimeout(this._reloadTimeout);
230
274
  this._reloadTimeout = setTimeout(async () => {
231
275
  try {
232
- this.runner = await this._createRunner();
233
- await this.reload(this.runner);
276
+ await this.reload();
234
277
  for (const fn of this._reloadListeners) fn();
235
278
  } catch (error) {
236
279
  console.error("Failed to reload runner:", error);
@@ -238,4 +281,4 @@ var EnvServer = class extends RunnerManager {
238
281
  }, 100);
239
282
  }
240
283
  };
241
- export { loadRunner as n, RunnerManager as r, EnvServer as t };
284
+ export { EnvServer, RunnerManager, loadRunner };
@@ -1,6 +1,11 @@
1
1
  import { IncomingMessage } from "node:http";
2
2
  import { Socket } from "node:net";
3
- /** Handler for proxying HTTP requests to the worker. */
3
+ /**
4
+ * Handler for proxying HTTP requests to the worker.
5
+ *
6
+ * A relative string input (e.g. `"/path"`) is resolved against a placeholder
7
+ * `http://localhost` origin before dispatching.
8
+ */
4
9
  type FetchHandler = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
10
  /** Callback for receiving messages from the worker. */
6
11
  type RunnerMessageListener = (data: unknown) => void;
@@ -52,7 +57,7 @@ interface RPCOptions {
52
57
  timeout?: number;
53
58
  }
54
59
  /** Core runner interface combining lifecycle hooks, RPC, and request proxying. */
55
- interface EnvRunner extends RunnerRPCHooks {
60
+ interface EnvRunner extends RunnerRPCHooks, AsyncDisposable {
56
61
  /** Whether the worker is ready to accept requests. */
57
62
  readonly ready: boolean;
58
63
  /** Whether the runner has been closed. */
@@ -67,7 +72,14 @@ interface EnvRunner extends RunnerRPCHooks {
67
72
  rpc<T = unknown>(name: string, data?: unknown, opts?: RPCOptions): Promise<T>;
68
73
  /** Re-import the entry module without restarting the worker/process. */
69
74
  reloadModule?(timeout?: number): Promise<void>;
75
+ /**
76
+ * Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
77
+ * A factory-valued source is re-run for fresh contents.
78
+ */
79
+ invalidateModule?(specifier: string, timeout?: number): Promise<void>;
70
80
  /** Gracefully shut down the worker. */
71
81
  close(): Promise<void>;
82
+ /** Alias for `close()`, enabling `await using` (explicit resource management). */
83
+ [Symbol.asyncDispose](): Promise<void>;
72
84
  }
73
- export { RunnerMessageListener as a, UpgradeHandler as c, RPCOptions as i, WorkerAddress as l, FetchHandler as n, RunnerRPCHooks as o, NodeUpgradeContext as r, UpgradeContext as s, EnvRunner as t, WorkerHooks as u };
85
+ export { EnvRunner, FetchHandler, NodeUpgradeContext, RPCOptions, RunnerMessageListener, RunnerRPCHooks, UpgradeContext, UpgradeHandler, WorkerAddress, WorkerHooks };
@@ -0,0 +1,63 @@
1
+ async function resolveVirtualModules(virtual) {
2
+ const entries = await Promise.all(Object.entries(virtual).map(async ([key, value]) => [key, typeof value === "function" ? await value() : value]));
3
+ return Object.fromEntries(entries);
4
+ }
5
+ const VIRTUAL_SCHEME = "virtual:";
6
+ function createVirtualHooks(virtual, versions) {
7
+ const resolve = (specifier, context, nextResolve) => {
8
+ const key = _stripQuery(specifier);
9
+ if (Object.hasOwn(virtual, key)) {
10
+ const version = versions?.get(key);
11
+ return {
12
+ url: VIRTUAL_SCHEME + encodeURIComponent(specifier) + (version ? `?v=${version}` : ""),
13
+ shortCircuit: true
14
+ };
15
+ }
16
+ return nextResolve(specifier, context);
17
+ };
18
+ const load = (url, context, nextLoad) => {
19
+ if (url.startsWith(VIRTUAL_SCHEME)) {
20
+ const key = _stripQuery(decodeURIComponent(url.slice(8)));
21
+ if (Object.hasOwn(virtual, key)) return {
22
+ format: virtualModuleFormat(key),
23
+ source: virtual[key],
24
+ shortCircuit: true
25
+ };
26
+ }
27
+ return nextLoad(url, context);
28
+ };
29
+ return {
30
+ resolve,
31
+ load
32
+ };
33
+ }
34
+ function virtualModuleFormat(specifier) {
35
+ if (specifier.endsWith(".json")) return "json";
36
+ if (specifier.endsWith(".ts") || specifier.endsWith(".mts")) return "module-typescript";
37
+ return "module";
38
+ }
39
+ function stripVirtualTypeScript(specifier, source, stripTypeScriptTypes, hints) {
40
+ if (typeof stripTypeScriptTypes !== "function") throw new TypeError(`[env-runner] virtual TypeScript module "${specifier}" requires \`module.stripTypeScriptTypes\` ${hints.requirement}; ${hints.remedy} or provide a pre-transpiled JavaScript source instead.`);
41
+ return stripTypeScriptTypes(source);
42
+ }
43
+ function expandVirtualInvalidation(virtual, specifier) {
44
+ const invalidated = [specifier];
45
+ const seen = new Set(invalidated);
46
+ for (const target of invalidated) {
47
+ const refs = [
48
+ `"${target}"`,
49
+ `'${target}'`,
50
+ "`" + target + "`"
51
+ ];
52
+ for (const [key, source] of Object.entries(virtual)) if (!seen.has(key) && refs.some((ref) => source.includes(ref))) {
53
+ seen.add(key);
54
+ invalidated.push(key);
55
+ }
56
+ }
57
+ return invalidated;
58
+ }
59
+ function _stripQuery(specifier) {
60
+ const qIndex = specifier.indexOf("?");
61
+ return qIndex === -1 ? specifier : specifier.slice(0, qIndex);
62
+ }
63
+ export { createVirtualHooks, expandVirtualInvalidation, resolveVirtualModules, stripVirtualTypeScript, virtualModuleFormat };
@@ -1,8 +1,115 @@
1
- import { readFileSync } from "node:fs";
1
+ import { createVirtualHooks, expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
2
+ import { existsSync, readFileSync } from "node:fs";
2
3
  import { pathToFileURL } from "node:url";
3
4
  import { isAbsolute } from "node:path";
4
- async function resolveEntry(entryPath) {
5
- const mod = await import(_toImportPath(entryPath));
5
+ async function registerVirtualModules(virtual) {
6
+ if (!virtual || Object.keys(virtual).length === 0) return _noop;
7
+ const { registerHooks, stripTypeScriptTypes } = await import("node:module");
8
+ if (typeof registerHooks === "function") {
9
+ let transformSource;
10
+ if ("Deno" in globalThis) {
11
+ transformSource = (specifier, source) => _transformSourceForDeno(specifier, source, stripTypeScriptTypes);
12
+ const transformed = {};
13
+ for (const [specifier, source] of Object.entries(virtual)) transformed[specifier] = transformSource(specifier, source);
14
+ virtual = transformed;
15
+ }
16
+ const registration = {
17
+ virtual,
18
+ versions: /* @__PURE__ */ new Map(),
19
+ transformSource
20
+ };
21
+ const hooks = registerHooks(createVirtualHooks(virtual, registration.versions));
22
+ _hooksRegistrations.unshift(registration);
23
+ return _once(() => {
24
+ const index = _hooksRegistrations.indexOf(registration);
25
+ if (index !== -1) _hooksRegistrations.splice(index, 1);
26
+ hooks.deregister();
27
+ });
28
+ }
29
+ if (typeof globalThis.Bun?.plugin === "function") {
30
+ _bunVirtual = virtual;
31
+ _registerBunModules(Object.keys(virtual));
32
+ return _once(() => {
33
+ if (_bunVirtual === virtual) _bunVirtual = void 0;
34
+ });
35
+ }
36
+ console.warn("[env-runner] virtual modules require `module.registerHooks` (Node.js >= 22.15 / Deno >= 2.x) or `Bun.plugin`; skipping registration.");
37
+ return _noop;
38
+ }
39
+ function refreshVirtualModule(specifier) {
40
+ if (_bunVirtual?.[specifier] === void 0) return false;
41
+ _registerBunModules([specifier]);
42
+ return true;
43
+ }
44
+ function invalidateVirtualModule(specifier, source) {
45
+ for (const registration of _hooksRegistrations) {
46
+ if (!Object.hasOwn(registration.virtual, specifier)) continue;
47
+ const { virtual, versions, transformSource } = registration;
48
+ if (source !== void 0) virtual[specifier] = transformSource ? transformSource(specifier, source) : source;
49
+ for (const key of expandVirtualInvalidation(virtual, specifier)) versions.set(key, (versions.get(key) ?? 0) + 1);
50
+ return true;
51
+ }
52
+ if (_bunVirtual && Object.hasOwn(_bunVirtual, specifier)) {
53
+ if (source !== void 0) _bunVirtual[specifier] = source;
54
+ _registerBunModules(expandVirtualInvalidation(_bunVirtual, specifier));
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ function handleInvalidateModule(message, sendMessage) {
60
+ const ok = invalidateVirtualModule(message.specifier, message.source);
61
+ sendMessage({
62
+ event: "module-invalidated",
63
+ specifier: message.specifier,
64
+ error: ok ? void 0 : `Cannot invalidate "${message.specifier}" (not a registered virtual module)`
65
+ });
66
+ }
67
+ const _hooksRegistrations = [];
68
+ let _bunVirtual;
69
+ function _registerBunModules(specifiers) {
70
+ globalThis.Bun.plugin({
71
+ name: "env-runner-virtual",
72
+ setup(build) {
73
+ for (const specifier of specifiers) build.module(specifier, () => {
74
+ const source = _bunVirtual?.[specifier];
75
+ if (source === void 0) throw new Error(`Cannot find virtual module "${specifier}" (unregistered)`);
76
+ const format = virtualModuleFormat(specifier);
77
+ if (format === "json") return {
78
+ exports: { default: JSON.parse(source) },
79
+ loader: "object"
80
+ };
81
+ return {
82
+ contents: source,
83
+ loader: format === "module-typescript" ? "ts" : "js"
84
+ };
85
+ });
86
+ }
87
+ });
88
+ }
89
+ function _transformSourceForDeno(specifier, source, stripTypeScriptTypes) {
90
+ const format = virtualModuleFormat(specifier);
91
+ if (format === "module-typescript") return stripVirtualTypeScript(specifier, source, stripTypeScriptTypes, {
92
+ requirement: "(custom load hooks bypass Deno's native type stripping)",
93
+ remedy: "upgrade Deno"
94
+ });
95
+ if (format === "json") return `export default JSON.parse(${JSON.stringify(source)});`;
96
+ return source;
97
+ }
98
+ const _noop = () => {};
99
+ function _once(fn) {
100
+ let done = false;
101
+ return () => {
102
+ if (!done) {
103
+ done = true;
104
+ fn();
105
+ }
106
+ };
107
+ }
108
+ function isVirtualSpecifier(specifier, virtual) {
109
+ return Boolean(specifier && virtual && Object.hasOwn(virtual, specifier));
110
+ }
111
+ async function resolveEntry(entryPath, virtual) {
112
+ const mod = await (virtual ? import(entryPath) : import(_toImportPath(entryPath)));
6
113
  const entry = mod.default || mod;
7
114
  if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
8
115
  return entry;
@@ -14,9 +121,9 @@ function parseServerAddress(server) {
14
121
  port: Number(url.port)
15
122
  };
16
123
  }
17
- async function reloadEntryModule(entryPath, currentEntry, sendMessage) {
124
+ async function reloadEntryModule(entryPath, currentEntry, sendMessage, virtual) {
18
125
  await currentEntry.ipc?.onClose?.();
19
- const newEntry = await _importFresh(entryPath);
126
+ const newEntry = await _importFresh(entryPath, virtual);
20
127
  await newEntry.ipc?.onOpen?.({ sendMessage });
21
128
  return newEntry;
22
129
  }
@@ -27,11 +134,18 @@ function _toImportPath(entryPath) {
27
134
  if (isAbsolute(filePath)) return pathToFileURL(filePath).href + query;
28
135
  return entryPath;
29
136
  }
30
- async function _importFresh(entryPath) {
31
- const code = readFileSync(entryPath, "utf8");
32
- const mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
137
+ let _reloadCounter = 0;
138
+ async function _importFresh(entryPath, virtual) {
139
+ const qIndex = entryPath.indexOf("?");
140
+ const filePath = qIndex === -1 ? entryPath : entryPath.slice(0, qIndex);
141
+ let mod;
142
+ if (!virtual && existsSync(filePath)) {
143
+ const code = readFileSync(filePath, "utf8");
144
+ mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
145
+ } else if (virtual && refreshVirtualModule(filePath)) mod = await import(filePath);
146
+ else mod = await import(entryPath + (qIndex === -1 ? "?" : "&") + "__envRunnerReload=" + _reloadCounter++);
33
147
  const entry = mod.default || mod;
34
148
  if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
35
149
  return entry;
36
150
  }
37
- export { reloadEntryModule as n, resolveEntry as r, parseServerAddress as t };
151
+ export { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry };
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as EnvServer } from "./_chunks/server.mjs";
1
+ import { EnvServer } from "./_chunks/server.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { serve } from "srvx";
4
4
  import { parseArgs } from "node:util";
package/dist/index.d.mts CHANGED
@@ -1,29 +1,45 @@
1
- import { a as RunnerMessageListener, c as UpgradeHandler, i as RPCOptions, l as WorkerAddress, n as FetchHandler, o as RunnerRPCHooks, r as NodeUpgradeContext, s as UpgradeContext, t as EnvRunner, u as WorkerHooks } from "./_chunks/types.mjs";
2
- import { n as EnvRunnerData, t as BaseEnvRunner } from "./_chunks/base-runner.mjs";
3
- import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
- import { i as TransformResult, n as MiniflareEnvRunnerOptions, r as MiniflareExportInfo, t as MiniflareEnvRunner } from "./_chunks/runner2.mjs";
5
- import { t as VercelEnvRunner } from "./_chunks/runner4.mjs";
6
- import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs";
1
+ import { EnvRunner, FetchHandler, NodeUpgradeContext, RPCOptions, RunnerMessageListener, RunnerRPCHooks, UpgradeContext, UpgradeHandler, WorkerAddress, WorkerHooks } from "./_chunks/types.mjs";
2
+ import { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules } from "./_chunks/base-runner.mjs";
3
+ import { DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
+ import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "./_chunks/runner2.mjs";
5
+ import { VercelEnvRunner } from "./_chunks/runner4.mjs";
6
+ import { NetlifyEnvRunner } from "./_chunks/runner5.mjs";
7
7
  import { ServerOptions } from "srvx";
8
8
  import { Hooks } from "crossws";
9
9
  /**
10
10
  * Manages an active `EnvRunner` instance, proxying all calls to it.
11
11
  * Supports hot-reload, auto-restart on unexpected exit, and message queueing.
12
12
  */
13
- declare class RunnerManager implements EnvRunner {
13
+ declare class RunnerManager implements EnvRunner, AsyncDisposable {
14
14
  private _runner;
15
15
  private _messageQueue;
16
16
  private _messageListeners;
17
17
  private _closed;
18
18
  private _reloading;
19
+ private _moduleInvalidated;
20
+ private _pendingModuleReload;
19
21
  private _closeListeners;
20
22
  private _readyListeners;
21
23
  constructor(runner?: EnvRunner);
22
24
  get ready(): boolean;
23
25
  get closed(): boolean;
24
- /** Replace the active runner with a new one. Closes the previous runner. */
25
- reload(runner: EnvRunner): Promise<void>;
26
+ /**
27
+ * Replace the active runner with a new one. Closes the previous runner.
28
+ *
29
+ * When called without a runner, a fresh one is created via `_createRunner()`
30
+ * (only available on subclasses with a runner factory, e.g. `EnvServer`).
31
+ */
32
+ reload(runner?: EnvRunner): Promise<void>;
33
+ /** Create a fresh runner for argument-less `reload()`. Overridden by subclasses with a runner factory. */
34
+ protected _createRunner(): EnvRunner | Promise<EnvRunner>;
26
35
  fetch: FetchHandler;
36
+ protected _fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
37
+ /**
38
+ * Lazily satisfy a pending `invalidateModule()` with a single entry reload
39
+ * before serving — concurrent fetches share the same reload. A failed reload
40
+ * keeps the invalidation pending, so the next fetch retries.
41
+ */
42
+ private _flushInvalidation;
27
43
  upgrade: UpgradeHandler;
28
44
  sendMessage(message: unknown): void;
29
45
  onMessage(listener: RunnerMessageListener): void;
@@ -33,7 +49,14 @@ declare class RunnerManager implements EnvRunner {
33
49
  timeout?: number;
34
50
  }): Promise<T>;
35
51
  reloadModule(timeout?: number): Promise<void>;
52
+ /**
53
+ * Invalidate a virtual module on the active runner and mark the manager
54
+ * dirty: the next `fetch()` reloads the entry automatically, so callers
55
+ * don't need to pair the call with an explicit `reloadModule()`.
56
+ */
57
+ invalidateModule(specifier: string, timeout?: number): Promise<void>;
36
58
  close(): Promise<void>;
59
+ [Symbol.asyncDispose](): Promise<void>;
37
60
  onClose(listener: (runner: EnvRunner, cause?: unknown) => void): void;
38
61
  offClose(listener: (runner: EnvRunner, cause?: unknown) => void): void;
39
62
  onReady(listener: (runner: EnvRunner, address?: WorkerAddress) => void): void;
@@ -56,8 +79,8 @@ interface LoadRunnerOptions {
56
79
  }
57
80
  declare function loadRunner(runner: RunnerName, opts: LoadRunnerOptions): Promise<EnvRunner>;
58
81
  interface EnvServerOptions {
59
- /** Runner implementation to use. */
60
- runner: RunnerName;
82
+ /** Runner implementation to use (defaults to `"node-worker"`). */
83
+ runner?: RunnerName;
61
84
  /** Path to the user entry module (passed as `data.entry`). */
62
85
  entry: string;
63
86
  /** Runner instance name. */
@@ -78,16 +101,31 @@ declare class EnvServer extends RunnerManager {
78
101
  private _watchers;
79
102
  private _reloadTimeout;
80
103
  private _reloadListeners;
104
+ private _startPromise;
81
105
  runner: Awaited<ReturnType<typeof loadRunner>> | null;
82
106
  /** Register a listener called when the runner is reloaded due to a file change. */
83
107
  onReload(listener: () => void): void;
84
108
  /** Remove a previously registered reload listener. */
85
109
  offReload(listener: () => void): void;
86
110
  constructor(opts: EnvServerOptions);
87
- /** Start the server by loading and attaching the runner. */
111
+ /**
112
+ * Start the server by loading and attaching the runner.
113
+ *
114
+ * Idempotent — concurrent and repeated calls share one startup. Calling
115
+ * `start()` explicitly is optional: the first `fetch()` auto-starts the
116
+ * server. A failed start resets so a later call can retry.
117
+ */
88
118
  start(): Promise<this>;
119
+ /**
120
+ * Replace the active runner. When called without an argument, a fresh
121
+ * runner is created from the server options.
122
+ */
123
+ reload(runner?: EnvRunner): Promise<void>;
89
124
  close(): Promise<void>;
90
- private _createRunner;
125
+ /** Auto-start on first fetch so an explicit `start()` call is optional. */
126
+ protected _fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
127
+ private _start;
128
+ protected _createRunner(): Promise<EnvRunner>;
91
129
  private _startWatching;
92
130
  private _stopWatching;
93
131
  private _scheduleReload;
@@ -108,4 +146,4 @@ interface AppEntry {
108
146
  plugins?: ServerOptions["plugins"];
109
147
  ipc?: AppEntryIPC;
110
148
  }
111
- export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunnerData, type EnvRunner, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, NetlifyEnvRunner, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, VercelEnvRunner, type WorkerAddress, type WorkerHooks, loadRunner };
149
+ export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunner, type EnvRunnerData, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, NetlifyEnvRunner, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, VercelEnvRunner, type VirtualModuleSource, type VirtualModules, type WorkerAddress, type WorkerHooks, loadRunner };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { t as BaseEnvRunner } from "./_chunks/base-runner.mjs";
2
- import { n as loadRunner, r as RunnerManager, t as EnvServer } from "./_chunks/server.mjs";
3
- import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
- import { t as MiniflareEnvRunner } from "./_chunks/runner2.mjs";
5
- import { t as VercelEnvRunner } from "./_chunks/runner4.mjs";
6
- import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs";
1
+ import { BaseEnvRunner } from "./_chunks/base-runner.mjs";
2
+ import { EnvServer, RunnerManager, loadRunner } from "./_chunks/server.mjs";
3
+ import { DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
+ import { MiniflareEnvRunner } from "./_chunks/runner2.mjs";
5
+ import { VercelEnvRunner } from "./_chunks/runner4.mjs";
6
+ import { NetlifyEnvRunner } from "./_chunks/runner5.mjs";
7
7
  export { BaseEnvRunner, DenoProcessEnvRunner, EnvServer, MiniflareEnvRunner, NetlifyEnvRunner, RunnerManager, VercelEnvRunner, loadRunner };
@@ -1,5 +1,5 @@
1
- import { u as WorkerHooks } from "../../_chunks/types.mjs";
2
- import { n as EnvRunnerData, t as BaseEnvRunner } from "../../_chunks/base-runner.mjs";
1
+ import { WorkerHooks } from "../../_chunks/types.mjs";
2
+ import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs";
3
3
  declare class BunProcessEnvRunner extends BaseEnvRunner {
4
4
  #private;
5
5
  constructor(opts: {
@@ -1,4 +1,4 @@
1
- import { t as BaseEnvRunner } from "../../_chunks/base-runner.mjs";
1
+ import { BaseEnvRunner } from "../../_chunks/base-runner.mjs";
2
2
  import { existsSync } from "node:fs";
3
3
  import { execSync, spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
@@ -7,6 +7,12 @@ import { homedir } from "node:os";
7
7
  let _defaultEntry;
8
8
  let _bunPath;
9
9
  const _isBun = typeof Bun !== "undefined";
10
+ async function forwardStream(stream, dest) {
11
+ if (!stream) return;
12
+ try {
13
+ for await (const chunk of stream) dest.write(chunk);
14
+ } catch {}
15
+ }
10
16
  function resolveBunPath() {
11
17
  if (_bunPath) return _bunPath;
12
18
  const candidates = [join(homedir(), ".bun", "bin", "bun")];
@@ -25,7 +31,7 @@ var BunProcessEnvRunner = class extends BaseEnvRunner {
25
31
  ...opts,
26
32
  workerEntry: opts.workerEntry || _defaultEntry
27
33
  });
28
- this.#initProcess(opts.execArgv);
34
+ this._initWithVirtualData(() => this.#initProcess(opts.execArgv));
29
35
  }
30
36
  sendMessage(message) {
31
37
  if (!this.#process) throw new Error("Bun env process should be initialized before sending messages.");
@@ -86,6 +92,8 @@ var BunProcessEnvRunner = class extends BaseEnvRunner {
86
92
  child._exitCode = code;
87
93
  this.close(`process exited with code ${code}`);
88
94
  });
95
+ forwardStream(proc.stdout, process.stdout);
96
+ forwardStream(proc.stderr, process.stderr);
89
97
  this.#process = child;
90
98
  }
91
99
  #initNodeProcess(execArgv, env) {
@@ -122,6 +130,8 @@ var BunProcessEnvRunner = class extends BaseEnvRunner {
122
130
  child.on("message", (message) => {
123
131
  this._handleMessage(message);
124
132
  });
133
+ child.stdout?.pipe(process.stdout);
134
+ child.stderr?.pipe(process.stderr);
125
135
  this.#process = handle;
126
136
  }
127
137
  };
@@ -1,9 +1,24 @@
1
- import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs";
1
+ import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
2
2
  import { serve } from "srvx";
3
3
  import { plugin } from "crossws/server";
4
+ process.on("disconnect", () => process.exit(0));
4
5
  const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}");
5
- let entry = await resolveEntry(data.entry);
6
6
  const sendMessage = (message) => process.send(message);
7
+ const virtualEntry = isVirtualSpecifier(data.entry, data.virtual);
8
+ let unregisterVirtualModules;
9
+ let entry;
10
+ try {
11
+ unregisterVirtualModules = await registerVirtualModules(data.virtual);
12
+ entry = await resolveEntry(data.entry, virtualEntry);
13
+ } catch (error) {
14
+ const message = error?.message || String(error);
15
+ sendMessage({
16
+ event: "init-error",
17
+ error: message
18
+ });
19
+ console.error(`[env-runner] worker init failed: ${message}`);
20
+ process.exit(1);
21
+ }
7
22
  const server = serve({
8
23
  port: 0,
9
24
  hostname: "127.0.0.1",
@@ -26,13 +41,14 @@ process.send({ address: parseServerAddress(server) });
26
41
  process.on("message", async (message) => {
27
42
  if (message?.event === "shutdown") {
28
43
  Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => {
44
+ unregisterVirtualModules();
29
45
  process.send({ event: "exit" });
30
46
  });
31
47
  return;
32
48
  }
33
49
  if (message?.event === "reload-module") {
34
50
  try {
35
- entry = await reloadEntryModule(data.entry, entry, sendMessage);
51
+ entry = await reloadEntryModule(data.entry, entry, sendMessage, virtualEntry);
36
52
  process.send({ event: "module-reloaded" });
37
53
  } catch (error) {
38
54
  process.send({
@@ -42,6 +58,10 @@ process.on("message", async (message) => {
42
58
  }
43
59
  return;
44
60
  }
61
+ if (message?.event === "invalidate-module") {
62
+ handleInvalidateModule(message, sendMessage);
63
+ return;
64
+ }
45
65
  if (message?.type === "ping") {
46
66
  process.send({
47
67
  type: "pong",
@@ -1,3 +1,3 @@
1
- import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs";
2
- import { t as DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
1
+ import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
2
+ import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
3
3
  export { DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData };
@@ -1,2 +1,2 @@
1
- import { t as DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
1
+ import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
2
2
  export { DenoProcessEnvRunner };