import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { loadExternalPlugin } from "./loader";
/**
* The plugin the smoke writes: one import from each kind of module a real
* plugin uses. `react` and the JSX runtime have to be the host's own
* instance, `gloomberb/ui` is shared state, `gloomberb/types/config` is a
* value from a module that used to be bundled from the package directory,
* and `smoke-dep` stands in for a dependency `bun install` put in the
* plugin's own `node_modules`.
*/
const SMOKE_PLUGIN_SOURCE = `
import React from "react";
import { Box, Text } from "gloomberb/ui";
import { formatNumber } from "gloomberb/utils";
import { TICKER_RESEARCH_PANE_ID } from "gloomberb/types/config";
import { platform } from "smoke-dep";
function Probe() {
return {formatNumber(1)};
}
export default {
id: "smoke-plugin-host",
name: "Plugin host smoke",
version: "0.0.0",
probe() {
const element = ;
const tree = Probe();
return {
react: typeof React.createElement === "function" && typeof React.useState === "function",
jsx: typeof element.type === "function" && tree != null && typeof tree === "object" && "type" in tree,
paneId: TICKER_RESEARCH_PANE_ID,
platform,
};
},
};
`;
/**
* A dependency whose `exports` map picks the entry by condition, as
* `youtubei.js`, `meriyah`, and `@earendil-works/pi-ai` do. A standalone
* executable that does not read package.json at runtime cannot resolve it
* at all, and one that resolves it with the wrong conditions lands on the
* browser build.
*/
const SMOKE_DEPENDENCY_PACKAGE = {
name: "smoke-dep",
version: "1.0.0",
type: "module",
exports: {
".": {
node: { import: "./node.js", default: "./node.js" },
browser: "./browser.js",
default: "./browser.js",
},
},
};
interface SmokeProbe {
react: boolean;
jsx: boolean;
paneId: string;
platform: string;
}
/**
* Writes the plugin to a temporary directory, loads it through the real
* loader, and throws with the loader's own error when it does not come up.
*/
export async function smokePluginHostLoad(): Promise {
const pluginDir = mkdtempSync(join(tmpdir(), "gloomberb-plugin-host-smoke-"));
try {
writeFileSync(join(pluginDir, "package.json"), JSON.stringify({ name: "smoke-plugin-host", main: "index.tsx" }));
writeFileSync(join(pluginDir, "index.tsx"), SMOKE_PLUGIN_SOURCE);
const dependencyDir = join(pluginDir, "node_modules", "smoke-dep");
mkdirSync(dependencyDir, { recursive: true });
writeFileSync(join(dependencyDir, "package.json"), JSON.stringify(SMOKE_DEPENDENCY_PACKAGE));
writeFileSync(join(dependencyDir, "node.js"), "export const platform = \"node\";\n");
writeFileSync(join(dependencyDir, "browser.js"), "export const platform = \"browser\";\n");
const loaded = await loadExternalPlugin(pluginDir, "tui");
if (!loaded) throw new Error("The smoke plugin directory was not recognised as a plugin.");
if (loaded.error) throw new Error(`The smoke plugin failed to load: ${loaded.error}`);
const probe = (loaded.plugin as unknown as { probe?: () => SmokeProbe }).probe?.();
if (!probe) throw new Error("The smoke plugin loaded without its probe export.");
if (!probe.react) throw new Error("The smoke plugin did not receive a working React.");
if (!probe.jsx) throw new Error("The smoke plugin's JSX did not compile against the host runtime.");
if (probe.paneId !== "ticker-research") {
throw new Error(`The smoke plugin read "${probe.paneId}" from gloomberb/types/config.`);
}
if (probe.platform !== "node") {
throw new Error(`The smoke plugin's dependency resolved to its "${probe.platform}" entry instead of "node".`);
}
} finally {
rmSync(pluginDir, { recursive: true, force: true });
}
}