env-runner 0.3.0 → 0.3.1

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,414 @@
1
1
  import { EnvRunner, ResolvedVirtualModule, RunnerMessageListener, VirtualModuleUpdates, VirtualModules, WorkerAddress, WorkerHooks } from "./types.mjs";
2
- import { IncomingMessage } from "node:http";
3
2
  import { Socket } from "node:net";
3
+ import { MessagePort } from "node:worker_threads";
4
+ import { IncomingMessage } from "node:http";
5
+ /**
6
+ * Module type of code: `js`, `jsx`, `ts`, `tsx` and `json` from the
7
+ * extension, other extensions as themselves (`vue` for `.vue`), `js` without
8
+ * an extension.
9
+ */
10
+ export type PluginModuleType = "js" | "jsx" | "ts" | "tsx" | "json" | (string & {});
11
+ /** A RegExp as sent to workers (`glob`: compiled from an `id` glob). */
12
+ interface SerializedPattern {
13
+ source: string;
14
+ flags: string;
15
+ glob?: true;
16
+ }
17
+ /** A filter expression as sent to workers. */
18
+ type SerializedFilterNode = {
19
+ kind: "and" | "or";
20
+ args: SerializedFilterNode[];
21
+ } | {
22
+ kind: "not";
23
+ expr: SerializedFilterNode;
24
+ } | {
25
+ kind: "id";
26
+ pattern: SerializedPattern;
27
+ } | {
28
+ kind: "code";
29
+ pattern: string | SerializedPattern;
30
+ } | {
31
+ kind: "moduleType";
32
+ pattern: string;
33
+ } | {
34
+ kind: "query";
35
+ key: string;
36
+ pattern: string | boolean | SerializedPattern;
37
+ };
38
+ interface SerializedFilterExpression {
39
+ kind: "include" | "exclude";
40
+ expr: SerializedFilterNode;
41
+ }
42
+ /**
43
+ * A plugin's filter as workers check it: its `id`/`moduleType` filters, or
44
+ * its filter expressions (`code` is unknown there).
45
+ */
46
+ interface SerializedPrefilter {
47
+ id?: {
48
+ include: SerializedPattern[];
49
+ exclude: SerializedPattern[];
50
+ };
51
+ moduleTypes?: PluginModuleType[];
52
+ expr?: SerializedFilterExpression[];
53
+ /**
54
+ * A `load` filter: it can't name module types, so a module its `id`
55
+ * include (or include expression) names counts as `"typed"`
56
+ * ({@link PrefilterMatch}).
57
+ */
58
+ load?: true;
59
+ /** A `resolveId` hook for imports the runtime fails to resolve. */
60
+ fallback?: true;
61
+ }
62
+ type MaybeArray<T> = T | T[];
63
+ /** Include values, or `{ include, exclude }` (exclude wins). */
64
+ export type PluginStringFilter = MaybeArray<string | RegExp> | {
65
+ include?: MaybeArray<string | RegExp>;
66
+ exclude?: MaybeArray<string | RegExp>;
67
+ };
68
+ /**
69
+ * `transform` hook filter (all given properties must match; empty ones are
70
+ * ignored). Ids are matched `/`-separated and with their query string: the
71
+ * glob `**\/*.svg` and the RegExp `/\.svg$/` don't match `/app/a.svg?raw`, but
72
+ * `/\.svg(?:\?.*)?$/` does (or add a `query` filter expression).
73
+ * - `id`: RegExps are tested, strings are globs: `*` (within a path segment),
74
+ * `?`, `**` (any number of segments), `[abc]`/`[!abc]`, `{a,b}` and `\`
75
+ * escapes, case-sensitive; `*` and `**` also match dot files and
76
+ * directories. Globs not starting with `**` and not absolute resolve from
77
+ * the working directory when the runner is created.
78
+ * - `code`: strings are substrings, RegExps are tested.
79
+ * - `moduleType`: see {@link PluginModuleType}.
80
+ *
81
+ * `id` and `moduleType` are also checked in the worker, before a module is
82
+ * sent to the runner: give plugins both where possible, so other modules
83
+ * load without a round trip.
84
+ */
85
+ export interface PluginTransformFilter {
86
+ id?: PluginStringFilter;
87
+ code?: PluginStringFilter;
88
+ moduleType?: PluginModuleType[] | {
89
+ include?: PluginModuleType[];
90
+ };
91
+ }
92
+ /**
93
+ * `resolveId` and `load` hook filter: `id` only, matched like
94
+ * {@link PluginTransformFilter} `id`. For `resolveId` it is the import
95
+ * specifier (with its query) and globs match it as written (not resolved
96
+ * from the working directory).
97
+ */
98
+ export interface PluginHookFilter {
99
+ id?: PluginStringFilter;
100
+ }
101
+ /**
102
+ * A filter expression, matched like the {@link PluginTransformFilter}
103
+ * properties. `query` parses the id's query with `URLSearchParams`: `true`
104
+ * matches when `key` is present (`?raw`), `false` when it's absent, a string
105
+ * equals its value, a RegExp tests it (`""` when absent). `importerId` is
106
+ * rejected. `resolveId` and `load` filters take no `code` or `moduleType`
107
+ * expressions.
108
+ */
109
+ export type PluginFilterExpression = {
110
+ kind: "and" | "or";
111
+ args: PluginFilterExpression[];
112
+ } | {
113
+ kind: "not";
114
+ expr: PluginFilterExpression;
115
+ } | {
116
+ kind: "id" | "importerId";
117
+ pattern: string | RegExp;
118
+ } | {
119
+ kind: "code";
120
+ pattern: string | RegExp;
121
+ } | {
122
+ kind: "moduleType";
123
+ pattern: PluginModuleType;
124
+ } | {
125
+ kind: "query";
126
+ key: string;
127
+ pattern: string | RegExp | boolean;
128
+ };
129
+ /**
130
+ * A filter as a list of include and exclude expressions: the first one that
131
+ * matches decides (an include matches, an exclude doesn't); if none does, the
132
+ * module matches only when there are no includes. The worker's prefilter
133
+ * sends a module unless the expressions can't match whatever its code.
134
+ */
135
+ export interface PluginTopLevelFilterExpression {
136
+ kind: "include" | "exclude";
137
+ expr: PluginFilterExpression;
138
+ }
139
+ /** Passed to every handler. */
140
+ export interface PluginTransformMeta {
141
+ /** Language of `code` (updated by results returning a `moduleType`). */
142
+ moduleType: PluginModuleType;
143
+ }
144
+ /**
145
+ * A message for {@link PluginContext}: a string, or a log object. A log's
146
+ * `loc` (1-based line, 0-based column) or `pos` (an offset) is used when no
147
+ * position is given, and its `frame` is shown below the message.
148
+ */
149
+ export type PluginLog = string | {
150
+ message: string;
151
+ loc?: {
152
+ line: number;
153
+ column: number;
154
+ file?: string;
155
+ };
156
+ pos?: number;
157
+ frame?: string;
158
+ };
159
+ /** An offset in the code, or a 1-based line and 0-based column. */
160
+ export type PluginLogPosition = number | {
161
+ line: number;
162
+ column: number;
163
+ };
164
+ /**
165
+ * `this` in handlers. Messages are prefixed with the plugin name and module
166
+ * id, and a position given is appended to the id (`id:line:column`).
167
+ */
168
+ export interface PluginContext {
169
+ /** Log a warning (on the host). */
170
+ warn(log: PluginLog, pos?: PluginLogPosition): void;
171
+ /** Log a message (on the host). */
172
+ info(log: PluginLog, pos?: PluginLogPosition): void;
173
+ /** Ignored (debug messages aren't shown). */
174
+ debug(log: PluginLog, pos?: PluginLogPosition): void;
175
+ /**
176
+ * Throw an error (an `Error` or log object given is kept as its `cause`).
177
+ * Errors a handler throws are reported the same way, with their `loc`,
178
+ * `pos` and `frame`.
179
+ */
180
+ error(log: PluginLog, pos?: PluginLogPosition): never;
181
+ /**
182
+ * Resolve an import like the runner would, on the host: the `resolveId`
183
+ * hooks (with `skipSelf`, the default, without the calling plugin's), then
184
+ * Node.js ESM resolution from `importer` (or the working directory) with
185
+ * the runner's export conditions: a file path, a builtin (`external`), or
186
+ * `null` when it doesn't resolve. No extensions or directory indexes are
187
+ * tried, as in Node.js.
188
+ */
189
+ resolve(source: string, importer?: string, options?: PluginContextResolveOptions): Promise<PluginResolvedId | null>;
190
+ /** Ignored: the runner doesn't watch files (nothing is ever returned). */
191
+ addWatchFile(id: string): void;
192
+ /** Always empty (see `addWatchFile`). */
193
+ getWatchFiles(): string[];
194
+ /** `watchMode` is `false`: plugins aren't told about file changes. */
195
+ meta: {
196
+ watchMode: boolean;
197
+ };
198
+ }
199
+ /**
200
+ * Runs on the host and may be async. Return nullish to keep the code. `id` is
201
+ * the module's path with the import's query (`/app/a.ts?raw`), or an id a
202
+ * `resolveId` hook returned.
203
+ */
204
+ export type PluginTransformHandler = (this: PluginContext, code: string, id: string, meta: PluginTransformMeta) => PluginTransformResult | Promise<PluginTransformResult>;
205
+ /**
206
+ * New code, or `{ code, map, moduleType }`: a `moduleType` tells later
207
+ * handlers the new language (e.g. `js` after compiling TypeScript).
208
+ * `moduleSideEffects` and `meta` are accepted and ignored.
209
+ */
210
+ export type PluginTransformResult = string | {
211
+ code?: string;
212
+ map?: SourceMapLike | string | null;
213
+ moduleType?: PluginModuleType;
214
+ moduleSideEffects?: unknown;
215
+ meta?: unknown;
216
+ } | null | undefined;
217
+ /** Options of {@link PluginContext.resolve}. */
218
+ export interface PluginContextResolveOptions {
219
+ /**
220
+ * Skip the calling plugin's `resolveId` hook, also when other plugins call
221
+ * `this.resolve()` with the same source and importer meanwhile (default
222
+ * `true`; only for calls from `resolveId`).
223
+ */
224
+ skipSelf?: boolean;
225
+ isEntry?: boolean;
226
+ attributes?: Record<string, string>;
227
+ }
228
+ /** Passed to `resolveId` handlers. */
229
+ export interface PluginResolveIdOptions {
230
+ /** Whether this is the runner's entry. */
231
+ isEntry: boolean;
232
+ /** Import attributes (`with { type: "json" }`), when the runtime reports them. */
233
+ attributes: Record<string, string>;
234
+ }
235
+ /**
236
+ * Runs on the host for the imports its filter matches (the import specifier
237
+ * as written, with its query, `file:` URLs as paths) and may be async.
238
+ * `importer` is the importing module's id (a path with its query, or an id a
239
+ * `resolveId` returned), `undefined` for the entry.
240
+ */
241
+ export type PluginResolveIdHandler = (this: PluginContext, source: string, importer: string | undefined, options: PluginResolveIdOptions) => PluginResolveIdResult | Promise<PluginResolveIdResult>;
242
+ /**
243
+ * The module's id, or nullish to try the next plugin (then the runtime
244
+ * resolves it). An absolute path loads that file (through `load` hooks,
245
+ * then from disk); any other id (like `\0virtual:foo`) must be loaded by a
246
+ * `load` hook. `false` or `external` leaves the import (or the returned id)
247
+ * to the runtime. `moduleSideEffects`, `meta` and other properties are
248
+ * ignored.
249
+ */
250
+ export type PluginResolveIdResult = string | false | {
251
+ id: string;
252
+ external?: boolean | "absolute" | "relative";
253
+ [key: string]: unknown;
254
+ } | null | undefined;
255
+ /**
256
+ * Runs on the host for the modules its filter matches and may be async:
257
+ * return the module's code, or nullish to try the next plugin (then the file
258
+ * is read from disk, without the id's query). `transform` hooks run on the
259
+ * result. `id` is like the {@link PluginTransformHandler} one.
260
+ */
261
+ export type PluginLoadHandler = (this: PluginContext, id: string) => PluginLoadResult | Promise<PluginLoadResult>;
262
+ /**
263
+ * Code, or `{ code, map, moduleType }`: without a `moduleType`, it is the
264
+ * id's ({@link PluginModuleType}), `js` for other extensions.
265
+ * `moduleSideEffects` and `meta` are accepted and ignored.
266
+ */
267
+ export type PluginLoadResult = string | {
268
+ code: string;
269
+ map?: SourceMapLike | string | null;
270
+ moduleType?: PluginModuleType;
271
+ moduleSideEffects?: unknown;
272
+ meta?: unknown;
273
+ } | null | undefined;
274
+ interface SourceMapLike {
275
+ version?: number;
276
+ mappings: string;
277
+ names?: string[];
278
+ sources?: string[];
279
+ sourcesContent?: (string | null)[];
280
+ }
281
+ /** A hook: its handler, or `{ order, filter, handler }`. */
282
+ export type PluginHook<Handler, Filter> = Handler | {
283
+ order?: "pre" | "post" | null;
284
+ filter?: Filter | PluginTopLevelFilterExpression[];
285
+ handler: Handler;
286
+ };
287
+ /**
288
+ * A runner plugin. Plugins live on the host: workers send the imports and
289
+ * modules their filters match to the runner, which runs the hooks and sends
290
+ * the result back. Each hook runs in `plugins` order within its `order`
291
+ * group: `"pre"`, then unordered, then `"post"` (plugins are sorted by
292
+ * `enforce` first). Other properties (and hooks) are ignored.
293
+ *
294
+ * - `resolveId`: resolve an import; the first result wins.
295
+ * - `load`: provide a module's code; the first result wins.
296
+ * - `transform`: change a module's code; every matching handler runs.
297
+ */
298
+ export interface EnvRunnerPlugin {
299
+ name?: string;
300
+ /**
301
+ * Orders the whole plugin: `"pre"` plugins come first and `"post"` last,
302
+ * keeping `plugins` order within each group. Each hook's own `order`
303
+ * applies on top of that.
304
+ */
305
+ enforce?: "pre" | "post";
306
+ resolveId?: PluginResolveIdHook;
307
+ load?: PluginHook<PluginLoadHandler, PluginHookFilter>;
308
+ transform?: PluginHook<PluginTransformHandler, PluginTransformFilter>;
309
+ }
310
+ /**
311
+ * A `resolveId` hook. With `fallback: true`, it only runs for imports the
312
+ * runtime fails to resolve (the worker tries first, so imports it resolves
313
+ * take no round trip), after the other `resolveId` hooks, which run before
314
+ * the runtime.
315
+ */
316
+ export type PluginResolveIdHook = PluginResolveIdHandler | {
317
+ order?: "pre" | "post" | null;
318
+ filter?: PluginHookFilter | PluginTopLevelFilterExpression[];
319
+ fallback?: boolean;
320
+ handler: PluginResolveIdHandler;
321
+ };
322
+ /** `plugins` option entries: nested arrays are flattened, falsy ones skipped. */
323
+ export type EnvRunnerPluginOption = EnvRunnerPlugin | EnvRunnerPluginOption[] | false | null | undefined;
324
+ /** Result of {@link PluginPipeline.transform} and {@link PluginPipeline.load}. */
325
+ interface PluginTransformOutput {
326
+ /** The new code, with an inline source map when a plugin returned one. */
327
+ code: string;
328
+ /**
329
+ * `js`; `ts` when no plugin compiled it (the worker strips the types where
330
+ * the runtime does it natively, like for untransformed files); `json` for
331
+ * JSON (served as a JSON module).
332
+ */
333
+ moduleType: "js" | "ts" | "json";
334
+ }
335
+ /** Options of {@link PluginPipeline.resolveId}. */
336
+ interface PluginResolveIdCallOptions extends Partial<PluginResolveIdOptions> {
337
+ /** Run the `fallback` hooks: the runtime failed to resolve the import. */
338
+ fallback?: boolean;
339
+ }
340
+ /** Result of {@link PluginPipeline.resolveId}. */
341
+ export type PluginResolvedId = {
342
+ id: string;
343
+ /** Leave the import (as `id`) to the runtime. */
344
+ external: boolean;
345
+ };
346
+ /** The `plugins` option of a runner, ready to run on the host. */
347
+ interface PluginPipeline {
348
+ /** Plugin names, in `plugins` order. */
349
+ names: string[];
350
+ /** The `load` and `transform` filters as the worker's prefilter checks them. */
351
+ prefilters: SerializedPrefilter[];
352
+ /** The `resolveId` filters as the worker's prefilter checks them. */
353
+ resolvePrefilters: SerializedPrefilter[];
354
+ /**
355
+ * Whether a module goes through the plugins: some `load` or `transform`
356
+ * filter may match (its `code` parts aren't checked). Modules of other than
357
+ * script types only match filters naming them (see {@link requiredMatch}),
358
+ * and disk modules under `/node_modules/` only ones naming that, unless
359
+ * `resolved` (a `resolveId` hook returned the path) or an id with a
360
+ * `moduleType` (virtual modules) is given.
361
+ */
362
+ filter(id: string, moduleType?: PluginModuleType, resolved?: boolean): boolean;
363
+ /**
364
+ * Whether some `resolveId` filter matches an import specifier (`fallback`:
365
+ * of the hooks for imports the runtime can't resolve).
366
+ */
367
+ resolveFilter(source: string, fallback?: boolean): boolean;
368
+ /**
369
+ * Run the `resolveId` hooks (`fallback`: those for imports the runtime
370
+ * failed to resolve): the first result, or `undefined` when none resolved
371
+ * it (the runtime resolves it, or fails). Rejects with their errors.
372
+ */
373
+ resolveId(source: string, importer?: string, options?: PluginResolveIdCallOptions): Promise<PluginResolvedId | undefined>;
374
+ /**
375
+ * Run the `load` hooks, then the `transform` hooks on the loaded code. When
376
+ * no `load` hook returned code, `read()` reads the module (none: rejects,
377
+ * the id isn't a file), and the result is `undefined` when no plugin changed
378
+ * it (load it as if unmatched). Rejects like {@link PluginPipeline.transform}.
379
+ */
380
+ load(id: string, read?: () => string, options?: {
381
+ resolved?: boolean;
382
+ }): Promise<PluginTransformOutput | undefined>;
383
+ /**
384
+ * Run the `transform` hooks: `undefined` when none changed the code (load it
385
+ * as if unmatched). Rejects with their errors (naming the plugin and id), and
386
+ * when the code changed but is still neither JavaScript, TypeScript nor JSON
387
+ * (e.g. JSX no plugin compiled).
388
+ */
389
+ transform(id: string, code: string, moduleType?: PluginModuleType): Promise<PluginTransformOutput | undefined>;
390
+ }
391
+ /** How a worker reaches the runner's plugins (part of the runner data). */
392
+ interface TransformChannel {
393
+ port?: MessagePort;
394
+ state?: Int32Array;
395
+ socket?: string;
396
+ }
397
+ /** Runner side of an open channel. */
398
+ interface TransformChannelHost {
399
+ /** Sent to the worker (`port` must be in the transfer list). */
400
+ channel: TransformChannel;
401
+ close(): void;
402
+ }
403
+ /** Runner data key of {@link PluginWorkerData} (only set with the `plugins` option). */
404
+ declare const PLUGINS_DATA_KEY = "__envRunnerPlugins";
405
+ /** What a worker gets for the runner's `plugins`. */
406
+ interface PluginWorkerData extends TransformChannel {
407
+ /** `load` and `transform` filters. */
408
+ prefilters: SerializedPrefilter[];
409
+ /** `resolveId` filters (none: imports are never sent). */
410
+ resolvePrefilters?: SerializedPrefilter[];
411
+ }
4
412
  export interface EnvRunnerData {
5
413
  name?: string;
6
414
  /**
@@ -29,11 +437,22 @@ export declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposabl
29
437
  protected _virtualResolved?: Promise<void>;
30
438
  protected _virtualUpdates: Promise<void>;
31
439
  protected _processData?: string;
440
+ protected _plugins?: PluginPipeline;
441
+ protected _transformChannel?: TransformChannelHost;
32
442
  constructor(opts: {
33
443
  name: string;
34
444
  workerEntry: string;
35
445
  hooks?: WorkerHooks;
36
446
  data?: EnvRunnerData;
447
+ /**
448
+ * Plugins whose `resolveId`, `load` and `transform` hooks run on the host
449
+ * for the imports, disk modules and virtual modules their filters match
450
+ * (e.g. compiling TypeScript enums and JSX, or serving generated modules).
451
+ * The worker sends each matching import or module to the runner and uses
452
+ * the result. Nested arrays are flattened and falsy entries skipped. Not
453
+ * supported by the `self` runner.
454
+ */
455
+ plugins?: EnvRunnerPluginOption[];
37
456
  });
38
457
  get ready(): boolean;
39
458
  get address(): WorkerAddress | undefined;
@@ -83,6 +502,20 @@ export declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposabl
83
502
  * base64; throws if not JSON-serializable.
84
503
  */
85
504
  protected _processEnv(): NodeJS.ProcessEnv;
505
+ /**
506
+ * Export conditions the runtime resolves packages with, for the plugins'
507
+ * `this.resolve()` (`undefined`: Node.js's). Called lazily, after the
508
+ * subclass constructor.
509
+ */
510
+ protected _resolveConditions(): string[] | undefined;
511
+ /**
512
+ * Runner data entries for the `plugins` option (none without plugins): the
513
+ * plugins' prefilters and a transform channel, opened on the first call.
514
+ * A `port` channel's `MessagePort` must be transferred to the worker.
515
+ */
516
+ protected _pluginWorkerData(kind: "port" | "socket"): {
517
+ [PLUGINS_DATA_KEY]?: PluginWorkerData;
518
+ };
86
519
  /**
87
520
  * Process worker messages: answer the `request-init-data` handshake with the
88
521
  * runner data (internal, not forwarded to listeners), handle everything else.