env-runner 0.2.2 → 0.3.0

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.
@@ -1,6 +1,16 @@
1
- import { resolveVirtualModules } from "./virtual-loader.mjs";
1
+ import { encodeVirtualModules, normalizeVirtualModules, resolveVirtualModules, warnVirtualPathCollisions } from "./virtual-loader.mjs";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { proxyFetch, proxyUpgrade } from "httpxy";
4
+ function hostEnv(extra) {
5
+ const env = {
6
+ ...process.env,
7
+ ...extra
8
+ };
9
+ const stdout = process.stdout;
10
+ if (!("FORCE_COLOR" in env) && !env.NO_COLOR && stdout?.isTTY) env.FORCE_COLOR = "1";
11
+ if (!("COLUMNS" in env) && stdout?.columns) env.COLUMNS = String(stdout.columns);
12
+ return env;
13
+ }
4
14
  var BaseEnvRunner = class {
5
15
  closed = false;
6
16
  _name;
@@ -11,7 +21,11 @@ var BaseEnvRunner = class {
11
21
  _address;
12
22
  _messageListeners;
13
23
  _pendingRequests;
24
+ _closeCause;
14
25
  _virtualResolved;
26
+ _virtualUpdates = Promise.resolve();
27
+ #virtualUpdateId = 0;
28
+ _processData;
15
29
  constructor(opts) {
16
30
  this._name = opts.name;
17
31
  this._workerEntry = opts.workerEntry;
@@ -27,7 +41,7 @@ var BaseEnvRunner = class {
27
41
  return this._address;
28
42
  }
29
43
  async fetch(input, init) {
30
- for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
44
+ await this._waitForAddress();
31
45
  if (!this._address) return new Response(`${this._runtimeType()} env runner is unavailable`, { status: 503 });
32
46
  return proxyFetch(this._address, this._resolveFetchInput(input), init);
33
47
  }
@@ -49,24 +63,29 @@ var BaseEnvRunner = class {
49
63
  }
50
64
  waitForReady(timeout = 15e3) {
51
65
  if (this.ready) return Promise.resolve();
52
- if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
66
+ if (this.closed) return Promise.reject(closedBeforeReadyError(this._closeCause));
53
67
  return new Promise((resolve, reject) => {
54
68
  const timer = setTimeout(() => {
55
- this._messageListeners.delete(listener);
69
+ cleanup();
56
70
  reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
57
71
  }, timeout);
58
72
  const listener = () => {
59
73
  if (this.ready) {
60
- clearTimeout(timer);
61
- this._messageListeners.delete(listener);
74
+ cleanup();
62
75
  resolve();
63
- } else if (this.closed) {
64
- clearTimeout(timer);
65
- this._messageListeners.delete(listener);
66
- reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
67
76
  }
68
77
  };
78
+ const onClose = (cause) => {
79
+ cleanup();
80
+ reject(closedBeforeReadyError(cause));
81
+ };
82
+ const cleanup = () => {
83
+ clearTimeout(timer);
84
+ this._messageListeners.delete(listener);
85
+ this._pendingRequests.delete(onClose);
86
+ };
69
87
  this._messageListeners.add(listener);
88
+ this._pendingRequests.add(onClose);
70
89
  });
71
90
  }
72
91
  rpc(name, data, opts) {
@@ -82,27 +101,27 @@ var BaseEnvRunner = class {
82
101
  }).then((msg) => msg.data);
83
102
  }
84
103
  async reloadModule(timeout = 5e3) {
104
+ await this._virtualUpdates;
85
105
  await this._request({ event: "reload-module" }, {
86
106
  match: (msg) => msg?.event === "module-reloaded",
87
107
  timeout,
88
108
  timeoutError: "Module reload timed out"
89
109
  });
90
110
  }
91
- async invalidateModule(specifier, timeout = 5e3) {
92
- const source = await this._refreshVirtualSource(specifier);
93
- await this._request({
94
- event: "invalidate-module",
95
- specifier,
96
- source
97
- }, {
98
- match: (msg) => msg?.event === "module-invalidated" && msg.specifier === specifier,
99
- timeout,
100
- timeoutError: `Module invalidation timed out for "${specifier}"`
101
- });
111
+ updateVirtualModules(changes, timeout = 5e3) {
112
+ return this._enqueueVirtualUpdate(() => changes, timeout);
113
+ }
114
+ invalidateModule(specifier, timeout = 5e3) {
115
+ return this._enqueueVirtualUpdate(() => {
116
+ const source = this._virtualSources?.[specifier];
117
+ if (source === void 0) throw new Error(`Cannot invalidate "${specifier}" (not a registered virtual module)`);
118
+ return { [specifier]: source };
119
+ }, timeout);
102
120
  }
103
121
  async close(cause) {
104
122
  if (this.closed) return;
105
123
  this.closed = true;
124
+ this._closeCause = cause;
106
125
  for (const rejectPending of this._pendingRequests) rejectPending(cause);
107
126
  this._pendingRequests.clear();
108
127
  this._hooks.onClose?.(this, cause);
@@ -118,6 +137,9 @@ var BaseEnvRunner = class {
118
137
  const status = this.closed ? "closed" : this.ready ? "ready" : "pending";
119
138
  return `${this.constructor.name}#${this._name}(${status})`;
120
139
  }
140
+ async _waitForAddress() {
141
+ for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
142
+ }
121
143
  _resolveFetchInput(input) {
122
144
  if (typeof input === "string" && !URL.canParse(input)) return new URL(input, "http://localhost");
123
145
  return input;
@@ -130,6 +152,36 @@ var BaseEnvRunner = class {
130
152
  if (message?.event === "init-error" && !this.ready && !this.closed) this.close(new Error(String(message.error || "Worker initialization failed")));
131
153
  for (const listener of this._messageListeners) listener(message);
132
154
  }
155
+ _processEnv() {
156
+ const data = this._data || {};
157
+ const virtual = data.virtual;
158
+ try {
159
+ this._processData = JSON.stringify(virtual ? {
160
+ ...data,
161
+ virtual: encodeVirtualModules(virtual)
162
+ } : data);
163
+ } catch (error) {
164
+ throw new TypeError(`Runner data must be JSON-serializable: ${error?.message || error}`, { cause: error });
165
+ }
166
+ return hostEnv({ ENV_RUNNER_NAME: this._name });
167
+ }
168
+ _handleProcessMessage(message) {
169
+ if (message?.event !== "request-init-data") {
170
+ this._handleMessage(message);
171
+ return;
172
+ }
173
+ if (this.closed) return;
174
+ try {
175
+ this.sendMessage({
176
+ event: "init-data",
177
+ data: this._processData ?? "{}"
178
+ });
179
+ } catch (error) {
180
+ const cause = new Error(`Failed to send runner data to the worker over IPC: ${error?.message || error}`, { cause: error });
181
+ console.error(`[env-runner] ${cause.message}`);
182
+ this.close(cause);
183
+ }
184
+ }
133
185
  _request(message, opts) {
134
186
  if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner is closed"));
135
187
  return new Promise((resolve, reject) => {
@@ -165,8 +217,17 @@ var BaseEnvRunner = class {
165
217
  }
166
218
  _resolveVirtualData() {
167
219
  const virtual = this._data?.virtual;
168
- this._virtualSources = virtual;
169
- if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return;
220
+ this._virtualSources = { ...virtual };
221
+ this._data = { ...this._data };
222
+ warnVirtualPathCollisions(Object.keys(virtual ?? {}));
223
+ if (!virtual) return;
224
+ if (!Object.values(virtual).some((v) => typeof v === "function")) try {
225
+ this._data.virtual = normalizeVirtualModules(virtual);
226
+ return;
227
+ } catch (error) {
228
+ this._virtualResolved = Promise.reject(error);
229
+ return this._virtualResolved;
230
+ }
170
231
  this._virtualResolved = resolveVirtualModules(virtual).then((resolved) => {
171
232
  this._data = {
172
233
  ...this._data,
@@ -175,20 +236,50 @@ var BaseEnvRunner = class {
175
236
  });
176
237
  return this._virtualResolved;
177
238
  }
178
- async _refreshVirtualSource(specifier) {
179
- await this._virtualResolved?.catch(() => {});
180
- const original = this._virtualSources?.[specifier];
181
- if (typeof original !== "function") return;
182
- const source = await original();
183
- const resolved = this._data?.virtual;
184
- if (resolved) resolved[specifier] = source;
185
- return source;
239
+ _enqueueVirtualUpdate(changes, timeout) {
240
+ const update = this._virtualUpdates.then(async () => {
241
+ await this._virtualResolved;
242
+ if (this.closed) throw new Error("Runner is closed");
243
+ const entries = Object.entries(changes()).filter(([, source]) => source !== void 0);
244
+ if (entries.length === 0) return;
245
+ const sets = Object.fromEntries(entries.filter((entry) => entry[1] !== null));
246
+ const resolved = await resolveVirtualModules(sets);
247
+ const sources = this._virtualSources ??= {};
248
+ const virtual = (this._data ??= {}).virtual ??= {};
249
+ let added = false;
250
+ for (const [key, source] of entries) if (source === null) {
251
+ resolved[key] = null;
252
+ delete sources[key];
253
+ delete virtual[key];
254
+ } else {
255
+ added ||= !Object.hasOwn(sources, key);
256
+ sources[key] = source;
257
+ virtual[key] = resolved[key];
258
+ }
259
+ if (added) warnVirtualPathCollisions(Object.keys(virtual));
260
+ if (!this.ready) await this.waitForReady();
261
+ await this._applyVirtualUpdates(resolved, timeout);
262
+ });
263
+ this._virtualUpdates = update.catch(() => {});
264
+ return update;
265
+ }
266
+ async _applyVirtualUpdates(changes, timeout) {
267
+ const id = ++this.#virtualUpdateId;
268
+ await this._request({
269
+ event: "update-virtual-modules",
270
+ id,
271
+ changes: encodeVirtualModules(changes)
272
+ }, {
273
+ match: (msg) => msg?.event === "virtual-modules-updated" && msg.id === id,
274
+ timeout,
275
+ timeoutError: "Virtual module update timed out"
276
+ });
186
277
  }
187
278
  _initWithVirtualData(init) {
188
279
  const pending = this._resolveVirtualData();
189
280
  if (pending) pending.then(() => {
190
281
  if (!this.closed) init();
191
- }, (error) => this.close(error));
282
+ }).catch((error) => this.close(error));
192
283
  else init();
193
284
  }
194
285
  async _closeSocket() {
@@ -197,4 +288,7 @@ var BaseEnvRunner = class {
197
288
  this._address = void 0;
198
289
  }
199
290
  };
200
- export { BaseEnvRunner };
291
+ function closedBeforeReadyError(cause) {
292
+ return new Error("Runner closed before becoming ready", cause ? { cause } : void 0);
293
+ }
294
+ export { BaseEnvRunner, hostEnv };
@@ -0,0 +1,13 @@
1
+ function receiveProcessData() {
2
+ return new Promise((resolve) => {
3
+ const onMessage = (message) => {
4
+ if (message?.event === "init-data") {
5
+ process.off("message", onMessage);
6
+ resolve(message.data);
7
+ }
8
+ };
9
+ process.on("message", onMessage);
10
+ process.send({ event: "request-init-data" });
11
+ }).then((json) => JSON.parse(json));
12
+ }
13
+ export { receiveProcessData };
@@ -1,18 +1,7 @@
1
1
  /**
2
- * A runtime dependency the application owns, not `env-runner`.
3
- *
4
- * Every runner option that names an external package accepts the same three
5
- * shapes, so the choice is about ergonomics rather than per-runner API:
6
- *
7
- * - the **imported module** (`import * as miniflare from "miniflare"`) — the
8
- * version installed by the app is the version that runs
9
- * - a **module specifier** (`"miniflare"`, `import.meta.resolve("miniflare")`,
10
- * or a `URL`) — resolved from {@link ResolveRuntimeDepOptions.from} (cwd by
11
- * default), so a bare specifier resolves against the app rather than against
12
- * `env-runner`'s own `node_modules`
13
- * - `false` — opt out of the package entirely
14
- *
15
- * Omitting the option falls back to an optional `import()` of the package.
2
+ * An app-owned package: the imported module, a specifier (resolved from the
3
+ * app, not `env-runner`), or `false` to opt out. Omitted falls back to an
4
+ * optional `import()`.
16
5
  */
17
6
  export type RuntimeDep<T> = T | string | URL | false;
18
7
  interface ResolveRuntimeDepOptions<T> {
@@ -22,29 +11,17 @@ interface ResolveRuntimeDepOptions<T> {
22
11
  option: string;
23
12
  /** Value as passed by the caller. */
24
13
  value?: RuntimeDep<T>;
25
- /**
26
- * Named export the resolved module must expose. A module that lacks it is
27
- * treated as the wrong package (throws for an explicit value).
28
- */
14
+ /** Named export identifying the package (a mismatch throws for an explicit value). */
29
15
  expect?: string;
30
16
  /** Directory bare specifiers resolve from. @default process.cwd() */
31
17
  from?: string;
32
- /**
33
- * Throw when the package cannot be resolved at all. Otherwise an
34
- * unavailable optional package resolves to `undefined` and the caller
35
- * degrades (minimal reader, shim, no-op).
36
- */
18
+ /** Throw when unresolvable instead of resolving `undefined` (callers degrade). */
37
19
  required?: boolean;
38
20
  /** Extra sentence appended to the "not installed" error when `required`. */
39
21
  hint?: string;
40
22
  }
41
23
  /**
42
- * Resolve a {@link RuntimeDep} to an imported module.
43
- *
44
- * Resolution order: `false` → `undefined`; an imported module → validated and
45
- * returned as-is; a specifier → imported (errors propagate, since an explicit
46
- * specifier that cannot load is a mistake worth surfacing); omitted → optional
47
- * `import(name)`, which yields `undefined` when the package isn't installed
48
- * unless {@link ResolveRuntimeDepOptions.required} is set.
24
+ * Resolve a {@link RuntimeDep} to a module. An explicit specifier that fails to
25
+ * import throws; an omitted one resolves `undefined` unless `required`.
49
26
  */
50
27
  export declare function resolveRuntimeDep<T>(opts: ResolveRuntimeDepOptions<T>): Promise<T | undefined>;