@efffrida/vitest-pool 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,141 +1,95 @@
1
- import type * as PlatformError from "@effect/platform/Error";
2
- import type * as FridaDeviceAcquisitionError from "@efffrida/frida-tools/FridaDeviceAcquisitionError";
3
- import type * as FridaSessionError from "@efffrida/frida-tools/FridaSessionError";
4
- import type * as Option from "effect/Option";
5
- import type * as ParseResult from "effect/ParseResult";
6
- import type * as Runtime from "effect/Runtime";
7
- import type * as VitestNode from "vitest/node";
1
+ import type * as Context from "effect/Context";
8
2
 
9
- import * as NodeContext from "@effect/platform-node/NodeContext";
10
- import * as Command from "@effect/platform/Command";
11
- import * as CommandExecutor from "@effect/platform/CommandExecutor";
12
- import * as FileSystem from "@effect/platform/FileSystem";
13
- import * as Path from "@effect/platform/Path";
14
- import * as FridaDevice from "@efffrida/frida-tools/FridaDevice";
15
- import * as FridaScript from "@efffrida/frida-tools/FridaScript";
16
- import * as FridaSession from "@efffrida/frida-tools/FridaSession";
3
+ import * as Array from "effect/Array";
17
4
  import * as Cause from "effect/Cause";
18
5
  import * as Deferred from "effect/Deferred";
19
6
  import * as Duration from "effect/Duration";
20
7
  import * as Effect from "effect/Effect";
21
8
  import * as Exit from "effect/Exit";
9
+ import * as FileSystem from "effect/FileSystem";
10
+ import * as Function from "effect/Function";
22
11
  import * as Layer from "effect/Layer";
23
- import * as ManagedRuntime from "effect/ManagedRuntime";
24
12
  import * as Match from "effect/Match";
13
+ import * as Path from "effect/Path";
25
14
  import * as Schema from "effect/Schema";
26
15
  import * as Scope from "effect/Scope";
27
- import * as Sink from "effect/Sink";
28
16
  import * as Stream from "effect/Stream";
29
- import * as Esbuild from "esbuild";
17
+ import * as ChildProcess from "effect/unstable/process/ChildProcess";
18
+
19
+ import type * as VitestNode from "vitest/node";
20
+
21
+ import * as NodeServices from "@effect/platform-node/NodeServices";
22
+ import * as FridaDevice from "@efffrida/frida-tools/FridaDevice";
23
+ import * as FridaScript from "@efffrida/frida-tools/FridaScript";
24
+ import * as FridaSession from "@efffrida/frida-tools/FridaSession";
30
25
  import * as Flatted from "flatted";
31
26
  import * as Frida from "frida";
32
27
 
33
28
  // First, pick your device
34
- class DeviceSchema extends Schema.Union(
29
+ const DeviceSchema = Schema.Union([
35
30
  Schema.Struct({
36
- device: Schema.Literal("local"),
31
+ connection: Schema.Literal("local"),
37
32
  }),
38
33
  Schema.Struct({
39
- device: Schema.Literal("usb"),
40
- timeout: Schema.optionalWith(Schema.DurationFromMillis, { nullable: true }),
34
+ connection: Schema.Literal("usb"),
35
+ timeout: Schema.optional(Schema.DurationFromMillis),
41
36
  }),
42
37
  Schema.Struct({
43
38
  address: Schema.String,
44
- device: Schema.Literal("remote"),
45
- token: Schema.optionalWith(Schema.String, { nullable: true }),
46
- origin: Schema.optionalWith(Schema.String, { nullable: true }),
47
- keepaliveInterval: Schema.optionalWith(Schema.DurationFromMillis, { nullable: true }),
39
+ connection: Schema.Literal("remote"),
40
+ token: Schema.optional(Schema.String),
41
+ origin: Schema.optional(Schema.String),
42
+ keepaliveInterval: Schema.optional(Schema.DurationFromMillis),
48
43
  }),
49
44
  Schema.Struct({
50
45
  emulatorName: Schema.String,
51
- device: Schema.Literal("android-emulator"),
52
- hidden: Schema.optionalWith(Schema.Boolean, { nullable: true }),
53
- adbExecutable: Schema.optionalWith(Schema.String, { nullable: true }),
54
- fridaExecutable: Schema.optionalWith(Schema.String, { nullable: true }),
55
- emulatorExecutable: Schema.optionalWith(Schema.String, { nullable: true }),
56
- })
57
- ) {}
46
+ hidden: Schema.optional(Schema.Boolean),
47
+ adbExecutable: Schema.optional(Schema.String),
48
+ connection: Schema.Literal("android-emulator"),
49
+ fridaExecutable: Schema.optional(Schema.String),
50
+ emulatorExecutable: Schema.optional(Schema.String),
51
+ }),
52
+ ]);
58
53
 
59
54
  // Second, pick your session
60
- class AttachSchema extends Schema.Union(
55
+ const AttachSchema = Schema.Union([
61
56
  Schema.Struct({
62
- attach: Schema.Number.pipe(Schema.brand("pid")),
57
+ pid: Schema.Number,
63
58
  }),
64
59
  Schema.Struct({
65
- spawn: Schema.NonEmptyArrayEnsure(Schema.String),
66
- preSpawn: Schema.optionalWith(Schema.Boolean, { nullable: true }),
60
+ spawn: Schema.NonEmptyArray(Schema.String),
61
+ preSpawn: Schema.optional(Schema.Boolean),
67
62
  }),
68
63
  Schema.Struct({
69
64
  attachFrontmost: Schema.Literal(true),
70
- frontmostScope: Schema.optionalWith(Schema.Literal("minimal", "metadata", "full"), { nullable: true }),
71
- })
72
- ) {}
65
+ frontmostScope: Schema.optional(Schema.Literals(["minimal", "metadata", "full"])),
66
+ }),
67
+ ]);
73
68
 
74
69
  // Third, pick your runtime and platform
75
- class ConfigSchema extends Schema.Struct({
76
- runtime: Schema.optionalWith(Schema.Literal("default", "qjs", "v8"), { nullable: true }),
77
- platform: Schema.optionalWith(Schema.Literal("gum", "browser", "neutral"), { nullable: true }),
78
- })
79
- .pipe(Schema.extend(DeviceSchema), Schema.extend(AttachSchema)) {}
70
+ const ConfigSchema = Schema.Struct({
71
+ device: DeviceSchema,
72
+ attach: AttachSchema,
73
+ runtime: Schema.optional(Schema.Literals(["default", "qjs", "v8"])),
74
+ platform: Schema.optional(Schema.Literals(["gum", "browser", "neutral"])),
75
+ });
80
76
 
81
77
  /**
82
78
  * @since 1.0.0
83
79
  * @category Tests
84
80
  */
85
81
  export class FridaPoolWorker implements VitestNode.PoolWorker {
86
- readonly name = "frida-pool";
87
- readonly agentTemplatePath = new URL("../frida/agent.ts", import.meta.url);
88
-
89
- private readonly poolOptions: VitestNode.PoolOptions;
90
- private readonly customOptions: Schema.Schema.Type<ConfigSchema>;
91
-
92
- private readonly cancelables: Map<
93
- (arg: any) => void,
94
- Runtime.Cancel<
95
- unknown,
96
- | FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
97
- | PlatformError.PlatformError
98
- | FridaSessionError.FridaSessionError
99
- >
100
- >;
101
-
102
- private modifiedAgentScope: Scope.CloseableScope;
103
- private managedRuntime:
104
- | ManagedRuntime.ManagedRuntime<
105
- FridaScript.FridaScript,
106
- | FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
107
- | PlatformError.PlatformError
108
- | FridaSessionError.FridaSessionError
109
- >
110
- | undefined;
111
- private sends: Array<
112
- Promise<
113
- Exit.Exit<
114
- void,
115
- | FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
116
- | PlatformError.PlatformError
117
- | FridaSessionError.FridaSessionError
118
- | ParseResult.ParseError
119
- >
120
- >
121
- > = [];
122
-
123
- constructor(poolOptions: VitestNode.PoolOptions, customOptions: Schema.Schema.Type<ConfigSchema>) {
124
- this.poolOptions = poolOptions;
125
- this.customOptions = customOptions;
126
- this.cancelables = new Map();
127
- this.managedRuntime = undefined;
128
- this.modifiedAgentScope = Effect.runSync(Scope.make());
129
- }
82
+ public readonly name = "frida-pool";
83
+ private static initQueue: Promise<void> = Promise.resolve();
130
84
 
131
- async start(): Promise<void> {
132
- const tempAgentUrl = await compileTestFiles(this.agentTemplatePath, this.poolOptions).pipe(
133
- Scope.extend(this.modifiedAgentScope),
134
- Effect.provide(NodeContext.layer),
135
- Effect.runPromise
136
- );
85
+ private readonly scope: Scope.Closeable;
86
+ private readonly scriptContext: Promise<Context.Context<FridaScript.FridaScript>>;
87
+ private readonly cancelables: Map<(arg: any) => void, (interrupter?: number) => void> = new Map();
137
88
 
138
- const FridaRuntime = Match.value(this.customOptions.runtime).pipe(
89
+ private sends: Array<Promise<unknown>> = [];
90
+
91
+ constructor(poolOptions: VitestNode.PoolOptions, customOptions: Schema.Schema.Type<typeof ConfigSchema>) {
92
+ const FridaRuntime = Match.value(customOptions.runtime).pipe(
139
93
  Match.when(undefined, () => undefined),
140
94
  Match.when("v8", () => Frida.ScriptRuntime.V8),
141
95
  Match.when("qjs", () => Frida.ScriptRuntime.QJS),
@@ -143,7 +97,7 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
143
97
  Match.exhaustive
144
98
  );
145
99
 
146
- const FridaPlatform = Match.value(this.customOptions.platform).pipe(
100
+ const FridaPlatform = Match.value(customOptions.platform).pipe(
147
101
  Match.when(undefined, () => undefined),
148
102
  Match.when("gum", () => Frida.JsPlatform.Gum),
149
103
  Match.when("browser", () => Frida.JsPlatform.Browser),
@@ -151,137 +105,211 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
151
105
  Match.exhaustive
152
106
  );
153
107
 
154
- const DeviceLive = Match.value(this.customOptions).pipe(
155
- Match.when({ device: "local" }, () => FridaDevice.layerLocalDevice),
156
- Match.when({ device: "usb" }, ({ timeout }) =>
157
- FridaDevice.layerUsbDevice({
158
- timeout: timeout ? Duration.toMillis(timeout) : undefined,
159
- } as Frida.GetDeviceOptions)
108
+ const DeviceLive = Match.value(customOptions.device).pipe(
109
+ Match.when({ connection: "local" }, () => FridaDevice.layerLocalDevice),
110
+ Match.when({ connection: "usb" }, ({ timeout }) =>
111
+ FridaDevice.layerUsbDevice(timeout !== undefined ? { timeout: Duration.toMillis(timeout) } : {})
160
112
  ),
161
- Match.when({ device: "remote" }, ({ address, keepaliveInterval, origin, token }) =>
113
+ Match.when({ connection: "remote" }, ({ address, keepaliveInterval, origin, token }) =>
162
114
  FridaDevice.layerRemoteDevice(address, {
163
- token,
164
- origin,
165
- keepaliveInterval: keepaliveInterval ? Duration.toSeconds(keepaliveInterval) : undefined,
166
- } as Frida.RemoteDeviceOptions)
115
+ ...(token !== undefined ? { token } : {}),
116
+ ...(origin !== undefined ? { origin } : {}),
117
+ ...(keepaliveInterval !== undefined
118
+ ? { keepaliveInterval: Duration.toMillis(keepaliveInterval) }
119
+ : {}),
120
+ })
167
121
  ),
168
122
  Match.when(
169
- { device: "android-emulator" },
123
+ { connection: "android-emulator" },
170
124
  ({ adbExecutable, emulatorExecutable, emulatorName, fridaExecutable, hidden }) =>
171
125
  FridaDevice.layerAndroidEmulatorDevice(emulatorName, {
172
- hidden,
173
- adbExecutable,
174
- fridaExecutable,
175
- emulatorExecutable,
126
+ hidden: hidden ?? undefined,
127
+ adbExecutable: adbExecutable ?? undefined,
128
+ fridaExecutable: fridaExecutable ?? undefined,
129
+ emulatorExecutable: emulatorExecutable ?? undefined,
176
130
  })
177
131
  ),
178
132
  Match.exhaustive
179
133
  );
180
134
 
181
- const SessionLive = Match.value(this.customOptions).pipe(
182
- Match.when({ attach: Match.number }, ({ attach }) => FridaSession.layer(attach)),
183
- Match.when({ attachFrontmost: true }, ({ frontmostScope }) =>
184
- FridaSession.layerFrontmost({ scope: frontmostScope } as Frida.FrontmostQueryOptions)
185
- ),
135
+ const SessionLive = Match.value(customOptions.attach).pipe(
136
+ Match.when({ pid: Match.number }, ({ pid }) => FridaSession.layer(pid)),
137
+ Match.when({ attachFrontmost: true }, ({ frontmostScope }) => {
138
+ return FridaSession.layerFrontmost(
139
+ frontmostScope !== undefined
140
+ ? {
141
+ scope: Match.value(frontmostScope).pipe(
142
+ Match.when("minimal", () => Frida.Scope.Minimal),
143
+ Match.when("metadata", () => Frida.Scope.Metadata),
144
+ Match.when("full", () => Frida.Scope.Full),
145
+ Match.exhaustive
146
+ ),
147
+ }
148
+ : {}
149
+ );
150
+ }),
186
151
  Match.when({ preSpawn: true }, ({ spawn }) =>
187
- Layer.unwrapScoped(
152
+ Layer.unwrap(
188
153
  Effect.gen(function* () {
189
- const executor = yield* CommandExecutor.CommandExecutor;
190
- const command = Command.make(...spawn);
191
- const process = yield* executor.start(command);
192
- return FridaSession.layer(process.pid);
154
+ const [command, ...args] = spawn;
155
+ const handle = yield* ChildProcess.make(command, args);
156
+ return FridaSession.layer(handle.pid);
193
157
  })
194
158
  )
195
159
  ),
196
160
  Match.orElse(({ spawn }) => FridaSession.layer(spawn))
197
161
  );
198
162
 
199
- const FridaLive = Layer.provide(SessionLive, DeviceLive).pipe(Layer.provide(NodeContext.layer));
200
- const ScriptLive = FridaScript.layer(tempAgentUrl, {
201
- externals: ["jsdom", "happy-dom", "@edge-runtime/vm"],
202
- ...(FridaRuntime !== undefined ? { runtime: FridaRuntime } : {}),
203
- ...(FridaPlatform !== undefined ? { platform: FridaPlatform } : {}),
204
- }).pipe(Layer.provide(FridaLive));
163
+ const FridaLive = Layer.provide(SessionLive, DeviceLive);
164
+ const ScriptLive = Effect.gen(function* () {
165
+ const path = yield* Path.Path;
166
+ const fs = yield* FileSystem.FileSystem;
167
+
168
+ const tempDir = path.join(poolOptions.project.config.root, "temp");
169
+ yield* fs.makeDirectory(tempDir, { recursive: true });
170
+
171
+ const agentUrl = yield* path.fromFileUrl(new URL("../frida/agent.ts", import.meta.url));
172
+ const baseAgent = yield* fs.readFileString(agentUrl);
173
+ const tempFile = yield* fs.makeTempFileScoped({
174
+ directory: tempDir,
175
+ prefix: ".vitest-frida-pool-agent-",
176
+ suffix: ".ts",
177
+ });
178
+
179
+ const setupFiles = poolOptions.project.config.setupFiles;
180
+ const testFiles = yield* Effect.map(
181
+ Effect.promise(() => poolOptions.project.globTestFiles()),
182
+ ({ testFiles }) => testFiles
183
+ );
184
+ const globalSetupFiles = Array.isArray(poolOptions.project.config.globalSetup)
185
+ ? poolOptions.project.config.globalSetup
186
+ : Array.make(poolOptions.project.config.globalSetup);
187
+
188
+ const marker = "// @efffrida/vitest-pool/agent/file-map";
189
+ const allFiles = Array.flatten([setupFiles, testFiles, globalSetupFiles]);
190
+ const newContent = Function.pipe(
191
+ allFiles,
192
+ Array.map(
193
+ (file) => `
194
+ if (_file === "${file}") {
195
+ // @ts-ignore
196
+ return await import("${file}")
197
+ }`
198
+ ),
199
+ Array.join("\n")
200
+ );
201
+
202
+ yield* fs.writeFileString(tempFile, baseAgent.replace(marker, newContent));
203
+ return yield* path.toFileUrl(tempFile);
204
+ }).pipe(
205
+ Effect.map(
206
+ FridaScript.layer({
207
+ ...(FridaRuntime !== undefined ? { runtime: FridaRuntime } : {}),
208
+ ...(FridaPlatform !== undefined ? { platform: FridaPlatform } : {}),
209
+ })
210
+ ),
211
+ Layer.unwrap,
212
+ Layer.provide(FridaLive),
213
+ Layer.provide(NodeServices.layer),
214
+ Layer.satisfiesSuccessType<FridaScript.FridaScript>()
215
+ );
205
216
 
206
- this.managedRuntime = ManagedRuntime.make(ScriptLive);
217
+ this.scope = Scope.makeUnsafe();
218
+ const prev = FridaPoolWorker.initQueue;
219
+ const runInit = () => ScriptLive.pipe(Layer.buildWithScope(this.scope), Effect.runPromise);
220
+ this.scriptContext = prev.then(runInit, runInit);
221
+ FridaPoolWorker.initQueue = this.scriptContext.then(
222
+ () => undefined,
223
+ () => undefined
224
+ );
225
+ }
207
226
 
208
- const exit = await this.managedRuntime.runPromiseExit(Effect.void);
209
- if (Exit.isSuccess(exit)) return;
210
- const prettyError = Cause.prettyErrors(exit.cause);
211
- throw prettyError[0];
227
+ async start(): Promise<void> {
228
+ await this.scriptContext;
212
229
  }
213
230
 
214
231
  async stop(): Promise<void> {
232
+ await Promise.allSettled(this.sends);
215
233
  for (const cancelable of this.cancelables.values()) cancelable();
234
+ await Scope.close(this.scope, Exit.void).pipe(Effect.runPromise);
216
235
  this.cancelables.clear();
217
- // await Promise.allSettled(this.sends);
218
- await this.managedRuntime!.dispose();
219
- await Effect.runPromise(Scope.close(this.modifiedAgentScope, Exit.void));
220
236
  }
221
237
 
222
238
  async send(message: VitestNode.WorkerRequest): Promise<void> {
223
- const sendPromise = this.managedRuntime!.runPromiseExit(
224
- Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
225
- fridaScript.callExport("onMessage", Schema.Void)(message)
226
- )
227
- );
228
-
229
- this.sends.push(sendPromise);
230
- const exit = await sendPromise;
231
- this.sends = this.sends.filter((p) => p !== sendPromise);
232
- if (Exit.isSuccess(exit)) return;
233
- const prettyError = Cause.prettyErrors(exit.cause);
234
- throw prettyError[0];
239
+ const context = await this.scriptContext;
240
+ let sendPromise: Promise<unknown> = undefined!;
241
+
242
+ try {
243
+ sendPromise = Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
244
+ fridaScript.callExport("onMessage")(message)
245
+ ).pipe(Effect.runPromiseWith(context));
246
+ this.sends.push(sendPromise);
247
+ await sendPromise;
248
+ } finally {
249
+ this.sends = this.sends.filter((p) => p !== sendPromise);
250
+ }
235
251
  }
236
252
 
237
253
  on(event: string, callback: (arg: any) => void): void {
238
- let cancelable!: Runtime.Cancel<
239
- unknown,
240
- | FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
241
- | PlatformError.PlatformError
242
- | FridaSessionError.FridaSessionError
243
- >;
244
-
245
- const sink = Sink.forEach<
246
- {
247
- message: unknown;
248
- data: Option.Option<Buffer<ArrayBufferLike>>;
249
- },
250
- void,
251
- never,
252
- never
253
- >((input) =>
254
- Effect.sync(() => {
255
- callback(input.message);
256
- })
257
- );
258
-
259
254
  switch (event) {
260
- case "message":
261
- cancelable = this.managedRuntime!.runCallback(
262
- Effect.flatMap(FridaScript.FridaScript, (fridaScript) => Stream.run(fridaScript.stream, sink))
263
- );
255
+ case "message": {
256
+ this.scriptContext.then((ctx) => {
257
+ this.cancelables.set(
258
+ callback,
259
+ Effect.runCallbackWith(ctx)(
260
+ Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
261
+ Stream.runForEach(fridaScript.stream, (input) =>
262
+ Effect.sync(() => callback(input.message))
263
+ )
264
+ )
265
+ )
266
+ );
267
+ });
268
+
264
269
  break;
270
+ }
265
271
 
266
272
  case "error":
267
- cancelable = this.managedRuntime!.runCallback(
268
- Effect.flatMap(FridaScript.FridaScript, (fridaScript) => Deferred.await(fridaScript.scriptError)),
269
- { onExit: (exit) => (Exit.isSuccess(exit) ? callback(exit.value) : !Cause.isInterruptedOnly(exit.cause) ? callback(exit.cause) : {}) }
270
- );
273
+ this.scriptContext.then((ctx) => {
274
+ this.cancelables.set(
275
+ callback,
276
+ Effect.runCallbackWith(ctx)(
277
+ Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
278
+ Deferred.await(fridaScript.scriptError)
279
+ ),
280
+ {
281
+ onExit: (exit) => {
282
+ if (Exit.isSuccess(exit)) {
283
+ callback(exit.value);
284
+ } else if (!Cause.hasInterruptsOnly(exit.cause)) {
285
+ callback(exit.cause);
286
+ }
287
+ },
288
+ }
289
+ )
290
+ );
291
+ });
292
+
271
293
  break;
272
294
 
273
295
  case "exit":
274
- cancelable = this.managedRuntime!.runCallback(
275
- Effect.flatMap(FridaScript.FridaScript, (fridaScript) => Deferred.await(fridaScript.destroyed)),
276
- { onExit: () => callback(void 0) }
277
- );
296
+ this.scriptContext.then((ctx) => {
297
+ this.cancelables.set(
298
+ callback,
299
+ Effect.runCallbackWith(ctx)(
300
+ Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
301
+ Deferred.await(fridaScript.destroyed)
302
+ ),
303
+ { onExit: () => callback(void 0) }
304
+ )
305
+ );
306
+ });
307
+
278
308
  break;
279
309
 
280
310
  default:
281
311
  throw new Error(`Event ${event} not supported in FridaPoolWorker`);
282
312
  }
283
-
284
- this.cancelables.set(callback, cancelable);
285
313
  }
286
314
 
287
315
  off(_event: string, callback: (arg: any) => void): void {
@@ -307,206 +335,11 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
307
335
  * @category Tests
308
336
  */
309
337
  export const createFridaPool = (
310
- customOptions: Schema.Schema.Encoded<ConfigSchema>
338
+ customOptions: Schema.Codec.Encoded<typeof ConfigSchema>
311
339
  ): VitestNode.PoolRunnerInitializer => {
312
- const decoded = Schema.decodeUnknownSync(ConfigSchema)(customOptions);
313
340
  return {
314
341
  name: "frida-pool",
315
- createPoolWorker: (options: VitestNode.PoolOptions) => new FridaPoolWorker(options, decoded),
342
+ createPoolWorker: (options: VitestNode.PoolOptions) =>
343
+ new FridaPoolWorker(options, Schema.decodeUnknownSync(ConfigSchema)(customOptions)),
316
344
  };
317
345
  };
318
-
319
- /** @internal */
320
- const vitestGlobalsPlugin: Esbuild.Plugin = {
321
- name: "vitest-globals",
322
- setup(build) {
323
- // Handle vitest and @vitest/* imports
324
- build.onResolve({ filter: /^(vitest|@vitest\/.*)$/ }, (args) => ({
325
- path: args.path,
326
- namespace: "vitest-globals",
327
- }));
328
-
329
- build.onLoad({ filter: /.*/, namespace: "vitest-globals" }, (args) => {
330
- if (args.path === "vitest") {
331
- return {
332
- loader: "js",
333
- contents: "export * from 'VITEST_GLOBAL';\nexport { default } from 'VITEST_GLOBAL';",
334
- };
335
- }
336
- if (args.path === "@vitest/runner") {
337
- return {
338
- loader: "js",
339
- contents: "export * from 'VITEST_RUNNER_GLOBAL';\nexport { default } from 'VITEST_RUNNER_GLOBAL';",
340
- };
341
- }
342
- return {
343
- loader: "js",
344
- contents: "export * from 'VITEST_GLOBAL';\nexport { default } from 'VITEST_GLOBAL';",
345
- };
346
- });
347
-
348
- build.onResolve({ filter: /^VITEST_(GLOBAL|RUNNER_GLOBAL)$/ }, (args) => ({
349
- path: args.path,
350
- namespace: "vitest-synthetic",
351
- }));
352
-
353
- build.onLoad({ filter: /.*/, namespace: "vitest-synthetic" }, (args) => {
354
- const globalName = args.path === "VITEST_GLOBAL" ? "__vitest" : "__vitest_runner";
355
- return {
356
- loader: "js",
357
- contents: `
358
- const g = globalThis.${globalName};
359
- export default g;
360
- export const {
361
- describe, it, test, expect, vi, beforeAll, afterAll,
362
- beforeEach, afterEach, suite, bench, assert
363
- } = g;
364
- `,
365
- };
366
- });
367
-
368
- // Handle Node.js built-in modules - redirect to globals set up by the agent
369
- // The agent imports these from frida-compile's shims and exposes them as globals
370
- const nodeModuleMap: Record<string, string> = {
371
- "node:assert": "__node_assert",
372
- "node:buffer": "__node_buffer",
373
- "node:crypto": "__node_crypto",
374
- "node:diagnostics_channel": "__node_diagnosticsChannel",
375
- "node:events": "__node_events",
376
- "node:fs": "__node_fs",
377
- "node:net": "__node_net",
378
- "node:os": "__node_os",
379
- "node:path": "__node_path",
380
- "node:process": "__node_process",
381
- "node:stream": "__node_stream",
382
- "node:timers": "__node_timers",
383
- "node:tty": "__node_tty",
384
- "node:url": "__node_url",
385
- "node:util": "__node_util",
386
- "node:vm": "__node_vm",
387
- assert: "__node_assert",
388
- buffer: "__node_buffer",
389
- crypto: "__node_crypto",
390
- diagnostics_channel: "__node_diagnosticsChannel",
391
- events: "__node_events",
392
- fs: "__node_fs",
393
- net: "__node_net",
394
- os: "__node_os",
395
- path: "__node_path",
396
- process: "__node_process",
397
- stream: "__node_stream",
398
- timers: "__node_timers",
399
- tty: "__node_tty",
400
- url: "__node_url",
401
- util: "__node_util",
402
- vm: "__node_vm",
403
- };
404
-
405
- build.onResolve(
406
- {
407
- filter: /^(node:)?(assert|buffer|crypto|diagnostics_channel|events|fs|net|os|path|process|stream|timers|tty|url|util|vm)$/,
408
- },
409
- (args) => ({
410
- path: args.path,
411
- namespace: "node-globals",
412
- })
413
- );
414
-
415
- build.onLoad({ filter: /.*/, namespace: "node-globals" }, (args) => {
416
- const globalName = nodeModuleMap[args.path];
417
- if (!globalName) {
418
- return { loader: "js", contents: "export default {};" };
419
- }
420
- return {
421
- loader: "js",
422
- contents: `
423
- const m = globalThis.${globalName};
424
- export default m;
425
- export const { Buffer } = m.Buffer ? m : { Buffer: m.default?.Buffer };
426
- export * from 'NODE_MODULE_REEXPORT_${globalName}';
427
- `,
428
- };
429
- });
430
-
431
- // Handle re-exports from node modules
432
- build.onResolve({ filter: /^NODE_MODULE_REEXPORT_/ }, (args) => ({
433
- path: args.path,
434
- namespace: "node-reexport",
435
- }));
436
-
437
- build.onLoad({ filter: /.*/, namespace: "node-reexport" }, (args) => {
438
- const globalName = args.path.replace("NODE_MODULE_REEXPORT_", "");
439
- return {
440
- loader: "js",
441
- contents: `
442
- const m = globalThis.${globalName};
443
- const mod = m.default || m;
444
- for (const key in mod) {
445
- if (key !== 'default') {
446
- Object.defineProperty(exports, key, {
447
- enumerable: true,
448
- get: () => mod[key]
449
- });
450
- }
451
- }
452
- `,
453
- };
454
- });
455
- },
456
- };
457
-
458
- /** @internal */
459
- const compileTestFiles = Effect.fnUntraced(function* (
460
- agentTemplatePath: URL,
461
- poolOptions: VitestNode.PoolOptions,
462
- customOptions?: Schema.Schema.Type<ConfigSchema> | undefined
463
- ): Effect.fn.Return<URL, PlatformError.PlatformError, Path.Path | FileSystem.FileSystem | Scope.Scope> {
464
- const path = yield* Path.Path;
465
- const fs = yield* FileSystem.FileSystem;
466
- const url = yield* path.fromFileUrl(agentTemplatePath);
467
-
468
- const testFiles: Array<string> = (poolOptions.project as any).testFilesList ?? [];
469
- const testFilesMap: Record<string, string> = {};
470
-
471
- const esbuildPlatform = Match.value(customOptions?.platform).pipe(
472
- Match.when("browser", () => "browser" as const),
473
- Match.when("neutral", () => "neutral" as const),
474
- Match.whenOr("gum", undefined, () => "node" as const),
475
- Match.orElseAbsurd
476
- );
477
-
478
- for (const testFile of testFiles) {
479
- const result = yield* Effect.promise(() =>
480
- Esbuild.build({
481
- bundle: true,
482
- write: false,
483
- format: "esm",
484
- platform: esbuildPlatform,
485
- target: "es2020",
486
- entryPoints: [testFile],
487
- plugins: [vitestGlobalsPlugin],
488
- })
489
- );
490
-
491
- if (!result.outputFiles || result.outputFiles.length === 0) {
492
- return yield* Effect.dieMessage(`esbuild produced no output for ${testFile}`);
493
- } else {
494
- testFilesMap[testFile] = Buffer.from(result.outputFiles[0].text).toString("base64");
495
- }
496
- }
497
-
498
- const agentTemplateContents = yield* fs.readFileString(url);
499
- const modifiedAgentContents = agentTemplateContents.replace(
500
- /^const testFiles: Record<string, string> = \{\};$/m,
501
- `const testFiles: Record<string, string> = ${JSON.stringify(testFilesMap, null, 4)};`
502
- );
503
-
504
- const tempAgentPath = yield* fs.makeTempFileScoped({
505
- suffix: ".ts",
506
- prefix: ".agent-",
507
- directory: path.dirname(url),
508
- });
509
-
510
- yield* fs.writeFileString(tempAgentPath, modifiedAgentContents);
511
- return yield* path.toFileUrl(tempAgentPath);
512
- });