@browserwright/pi 0.10.0 → 0.10.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.
@@ -22,20 +22,73 @@ import { pathToFileURL } from "node:url";
22
22
  import { isAbsolute, resolve } from "node:path";
23
23
  import type { ModuleContext, ModuleProvider, ModuleRunner, ProviderOutcome, Role } from "./types.ts";
24
24
 
25
- const cache = new Map<string, ModuleRunner<unknown>>();
25
+ /**
26
+ * Completed and in-flight loads, keyed by spec.
27
+ *
28
+ * This stores the PROMISE, not the resolved runner, on purpose. Two web_search
29
+ * calls fired in the same turn both miss a value cache and both import; under
30
+ * pi's jiti runtime that race is not merely wasteful but wrong — the second
31
+ * caller can observe a half-initialized module record and fail the load (see
32
+ * the mechanism note above loadRunner). A promise cache collapses the second
33
+ * caller onto the first's in-flight import, so everyone awaits the same
34
+ * import and sees the same resolved runner.
35
+ */
36
+ const cache = new Map<string, Promise<ModuleRunner<unknown>>>();
26
37
 
27
- async function loadRunner(spec: string, dir: string): Promise<ModuleRunner<unknown>> {
38
+ /**
39
+ * How a module runner is loaded. Injectable for tests; production always uses
40
+ * the native dynamic import, which pi's jiti runtime routes through its own
41
+ * transpile-and-cache pipeline.
42
+ *
43
+ * Why the unwrap (measured, not hypothetical): jiti transpiles .ts modules to
44
+ * CommonJS and rewrites every static import into a top-level
45
+ * `await jitiImport(...)` inside an async wrapper. While the module body is
46
+ * suspended on those awaits, the module record is already in jiti's shared
47
+ * per-chain cache with `exports.default` still unset. A concurrent import of
48
+ * the same module then hits that half-initialized record, and jiti's
49
+ * interopDefault proxy surfaces the RAW exports object (`{ default: runner,
50
+ * explain }`) as the module's `.default` — which is not a function. Observable
51
+ * as an intermittent "does not default-export a runner function" on the first
52
+ * concurrent call, never again once cached. The promise cache above removes
53
+ * the race; this unwrap is belt-and-braces for any residual interop quirk
54
+ * (e.g. an environment forcing JITI_INTEROP_DEFAULT=false).
55
+ */
56
+ export type ModuleImporter = (href: string) => Promise<{ default?: unknown }>;
57
+
58
+ const defaultImporter: ModuleImporter = (href) => import(href) as Promise<{ default?: unknown }>;
59
+
60
+ export async function loadRunner(
61
+ spec: string,
62
+ dir: string,
63
+ importer: ModuleImporter = defaultImporter,
64
+ ): Promise<ModuleRunner<unknown>> {
28
65
  const cached = cache.get(spec);
29
66
  if (cached) return cached;
30
67
 
31
68
  const path = isAbsolute(spec) ? spec : resolve(dir, spec);
32
- const imported = (await import(pathToFileURL(path).href)) as { default?: unknown };
33
- const runner = imported.default;
34
- if (typeof runner !== "function") {
35
- throw new Error(`${spec} does not default-export a runner function`);
69
+ const load = (async () => {
70
+ const imported = (await importer(pathToFileURL(path).href)) as { default?: unknown };
71
+ const direct = imported.default as unknown;
72
+ const runner =
73
+ typeof direct === "function"
74
+ ? (direct as ModuleRunner<unknown>)
75
+ : ((direct as { default?: unknown } | null)?.default as unknown);
76
+ if (typeof runner !== "function") {
77
+ throw new Error(`${spec} does not default-export a runner function`);
78
+ }
79
+ return runner as ModuleRunner<unknown>;
80
+ })();
81
+
82
+ cache.set(spec, load);
83
+ try {
84
+ return await load;
85
+ } catch (error) {
86
+ // A failed load must not poison the cache forever: drop it so the next
87
+ // call re-imports. Both callers of a shared rejected promise reach here
88
+ // (delete is idempotent).
89
+ cache.delete(spec);
90
+ throw error;
36
91
  }
37
- cache.set(spec, runner as ModuleRunner<unknown>);
38
- return runner as ModuleRunner<unknown>;
39
92
  }
40
93
 
41
94
  export async function execModule<T>(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserwright/pi",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "web_fetch and web_search for the pi coding agent, driving the user's real browser through browserwright",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",