Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions packages/pi-fff/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ export type FileFinderStatic = {

let sdkPromise: Promise<{ FileFinder: FileFinderStatic }> | null = null;

const SDK_ORDER: Record<"bun" | "node", readonly [string, string]> = {
// fff-bun is TS-source only and cannot be imported by Bun-compiled hosts
// (e.g. omp) whose module resolver rejects .ts under node_modules, so the
// JS-compiled fff-node is kept as a fallback for every runtime.
bun: ["@ff-labs/fff-bun", "@ff-labs/fff-node"],
node: ["@ff-labs/fff-node", "@ff-labs/fff-bun"],
};

// Literal dynamic imports so hosts that statically scan extension graphs
// (omp's legacy-pi-compat loader) discover and hook both SDK packages;
// a variable `import(pkg)` would bypass that scan and fail at runtime.
const SDK_IMPORTS = {
"@ff-labs/fff-bun": () => import("@ff-labs/fff-bun"),
"@ff-labs/fff-node": () => import("@ff-labs/fff-node"),
} as const;

function detectRuntime(): "bun" | "node" {
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") return "bun";
if (
Expand All @@ -19,6 +35,26 @@ function detectRuntime(): "bun" | "node" {
return "node";
}

/** Preferred SDK order for the detected runtime. */
export function sdkCandidates(): readonly [string, string] {
return SDK_ORDER[detectRuntime()];
}

export async function loadFirst(
candidates: readonly [string, string],
loaders: Record<string, () => Promise<unknown>> = SDK_IMPORTS,
): Promise<{ FileFinder: FileFinderStatic }> {
let lastError: unknown;
for (const pkg of candidates) {
try {
return (await loaders[pkg]()) as { FileFinder: FileFinderStatic };
} catch (error) {
lastError = error;
}
}
throw lastError;
}

export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
if (sdkPromise) return sdkPromise;

Expand All @@ -34,8 +70,7 @@ export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
}

// default to node as it seems like default option
const pkg = detectRuntime() === "bun" ? "@ff-labs/fff-bun" : "@ff-labs/fff-node";
const p = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>;
const p = loadFirst(sdkCandidates());
sdkPromise = p;
(globalThis as Record<string, unknown>).__fffSdkPromiseGlobal = p;
return p;
Expand Down
45 changes: 45 additions & 0 deletions packages/pi-fff/test/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { loadFirst, sdkCandidates } from "../src/sdk";

describe("sdkCandidates", () => {
test("defaults to bun candidates under a Bun runtime", () => {
expect(sdkCandidates()).toEqual(["@ff-labs/fff-bun", "@ff-labs/fff-node"]);
});
});

describe("literal SDK imports", () => {
test("both SDK specifiers appear as literal dynamic imports for static graph scans", () => {
const source = readFileSync(new URL("../src/sdk.ts", import.meta.url), "utf8");
expect(source).toContain('import("@ff-labs/fff-bun")');
expect(source).toContain('import("@ff-labs/fff-node")');
});
});

describe("loadFirst", () => {
test("prefers the first candidate when both load", async () => {
const loaders = {
a: () => Promise.resolve({ FileFinder: { create: () => "a" } }),
b: () => Promise.resolve({ FileFinder: { create: () => "b" } }),
};
const mod = await loadFirst(["a", "b"], loaders);
expect(mod.FileFinder.create()).toBe("a");
});

test("falls back to the second candidate when the first import fails", async () => {
const loaders = {
a: () => Promise.reject(new Error("cannot find a")),
b: () => Promise.resolve({ FileFinder: { create: () => "b" } }),
};
const mod = await loadFirst(["a", "b"], loaders);
expect(mod.FileFinder.create()).toBe("b");
});

test("throws the last error when every candidate fails", async () => {
const loaders = {
a: () => Promise.reject(new Error("first failure")),
b: () => Promise.reject(new Error("second failure")),
};
await expect(loadFirst(["a", "b"], loaders)).rejects.toThrow("second failure");
});
});
Loading