FN-7956: bundle three plugins as install-safe JS entrypoints
Ship reports, cli-printing-press, and whatsapp-chat through tsup so global/npm installs no longer load raw .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). - Route WhatsApp Chat, Reports, and CLI Printing Press through bundlePluginEntry instead of copying src/ - Re-export postgresSchema from the core runtime shim for published plugin bundles - Keep Baileys optional deps external for WhatsApp Chat bundling - Extend bundle-output helpers/tests to assert self-contained plugin bundles - Add patch changeset for @runfusion/fusion packaging fix Files changed: .changeset/fn-7956-bundle-three-plugins.md | 7 ++ .../cli/src/__tests__/bundle-output-helpers.ts | 30 +++++++- packages/cli/src/__tests__/bundle-output.test.ts | 83 ++++++++++++++-------- packages/cli/src/plugin-sdk-core-runtime-shim.ts | 6 ++ packages/cli/tsup.config.ts | 71 +++++++----------- 5 files changed, 123 insertions(+), 74 deletions(-) Fusion-Task-Id: FN-7956 Fusion-Task-Lineage: e32736d9-e637-450c-966b-6b2d6bc69b88 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7956-bundle-three-plugins.md
Normal file
7
.changeset/fn-7956-bundle-three-plugins.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Reports, CLI Printing Press, and WhatsApp Chat plugins now load from global installs.
|
||||||
|
category: fix
|
||||||
|
dev: Stages the three plugins through bundlePluginEntry with postgresSchema shim coverage and bundle-output regression assertions.
|
||||||
@@ -9,6 +9,24 @@ export const bundlePath = join(cliRoot, "dist", "bin.js");
|
|||||||
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
|
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
|
||||||
const cursorPluginManifestPath = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime", "manifest.json");
|
const cursorPluginManifestPath = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime", "manifest.json");
|
||||||
const roadmapPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap", "bundled.js");
|
const roadmapPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap", "bundled.js");
|
||||||
|
const reportsPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-reports", "bundled.js");
|
||||||
|
const cliPrintingPressPluginBundledPath = join(
|
||||||
|
cliRoot,
|
||||||
|
"dist",
|
||||||
|
"plugins",
|
||||||
|
"fusion-plugin-cli-printing-press",
|
||||||
|
"bundled.js",
|
||||||
|
);
|
||||||
|
const whatsappChatPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-whatsapp-chat", "bundled.js");
|
||||||
|
const compoundEngineeringSkillPath = join(
|
||||||
|
cliRoot,
|
||||||
|
"dist",
|
||||||
|
"plugins",
|
||||||
|
"fusion-plugin-compound-engineering",
|
||||||
|
"skills",
|
||||||
|
"ce-brainstorm",
|
||||||
|
"SKILL.md",
|
||||||
|
);
|
||||||
export const openclawMcpSchemaServerPath = join(
|
export const openclawMcpSchemaServerPath = join(
|
||||||
cliRoot,
|
cliRoot,
|
||||||
"dist",
|
"dist",
|
||||||
@@ -50,6 +68,10 @@ export function hasBuiltDashboardAssets(): boolean {
|
|||||||
!existsSync(clientIndexPath) ||
|
!existsSync(clientIndexPath) ||
|
||||||
!existsSync(cursorPluginManifestPath) ||
|
!existsSync(cursorPluginManifestPath) ||
|
||||||
!existsSync(roadmapPluginBundledPath) ||
|
!existsSync(roadmapPluginBundledPath) ||
|
||||||
|
!existsSync(reportsPluginBundledPath) ||
|
||||||
|
!existsSync(cliPrintingPressPluginBundledPath) ||
|
||||||
|
!existsSync(whatsappChatPluginBundledPath) ||
|
||||||
|
!existsSync(compoundEngineeringSkillPath) ||
|
||||||
!existsSync(openclawMcpSchemaServerPath) ||
|
!existsSync(openclawMcpSchemaServerPath) ||
|
||||||
!existsSync(droidPluginMcpServerPath)
|
!existsSync(droidPluginMcpServerPath)
|
||||||
) {
|
) {
|
||||||
@@ -115,7 +137,11 @@ export function buildCliWithRealDashboardAssets() {
|
|||||||
try {
|
try {
|
||||||
runBuildCommand(`node ${join(workspaceRoot, "scripts", "ensure-test-artifacts.mjs")}`, workspaceRoot);
|
runBuildCommand(`node ${join(workspaceRoot, "scripts", "ensure-test-artifacts.mjs")}`, workspaceRoot);
|
||||||
runBuildCommand("pnpm --filter @fusion/dashboard build:client", workspaceRoot);
|
runBuildCommand("pnpm --filter @fusion/dashboard build:client", workspaceRoot);
|
||||||
runBuildCommand("pnpm build", cliRoot);
|
/*
|
||||||
|
* FNXC:BundledPlugins 2026-07-15-09:08:
|
||||||
|
* bundle-output tests assert the published CLI packaging surface, including staged bundled plugins and skill assets. Local `pnpm build` may use fast package mode, so bootstrap with `build:package` to force FUSION_CLI_FULL_PACKAGE and avoid reading stale raw-src plugin output from dist/.
|
||||||
|
*/
|
||||||
|
runBuildCommand("pnpm build:package", cliRoot);
|
||||||
|
|
||||||
if (hasBuiltDashboardAssets()) {
|
if (hasBuiltDashboardAssets()) {
|
||||||
return;
|
return;
|
||||||
@@ -124,7 +150,7 @@ export function buildCliWithRealDashboardAssets() {
|
|||||||
// Fallback for environments where build:client alone does not refresh the
|
// Fallback for environments where build:client alone does not refresh the
|
||||||
// dashboard dist/client bundle consumed by the CLI copy step.
|
// dashboard dist/client bundle consumed by the CLI copy step.
|
||||||
runBuildCommand("pnpm --filter @fusion/dashboard build", workspaceRoot);
|
runBuildCommand("pnpm --filter @fusion/dashboard build", workspaceRoot);
|
||||||
runBuildCommand("pnpm build", cliRoot);
|
runBuildCommand("pnpm build:package", cliRoot);
|
||||||
} finally {
|
} finally {
|
||||||
if (acquiredLock) {
|
if (acquiredLock) {
|
||||||
rmSync(buildLockDir, { recursive: true, force: true });
|
rmSync(buildLockDir, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -22,8 +22,16 @@ const bundlePluginEntryPluginIds = [
|
|||||||
"fusion-plugin-dependency-graph",
|
"fusion-plugin-dependency-graph",
|
||||||
"fusion-plugin-roadmap",
|
"fusion-plugin-roadmap",
|
||||||
"fusion-plugin-compound-engineering",
|
"fusion-plugin-compound-engineering",
|
||||||
|
"fusion-plugin-whatsapp-chat",
|
||||||
|
"fusion-plugin-reports",
|
||||||
|
"fusion-plugin-cli-printing-press",
|
||||||
"fusion-plugin-linear-import",
|
"fusion-plugin-linear-import",
|
||||||
] as const;
|
] as const;
|
||||||
|
const selfContainedBundlePluginIds = [
|
||||||
|
"fusion-plugin-reports",
|
||||||
|
"fusion-plugin-cli-printing-press",
|
||||||
|
"fusion-plugin-whatsapp-chat",
|
||||||
|
] as const;
|
||||||
const knownCompoundEngineeringSkillIds = [
|
const knownCompoundEngineeringSkillIds = [
|
||||||
"ce-brainstorm",
|
"ce-brainstorm",
|
||||||
"ce-code-review",
|
"ce-code-review",
|
||||||
@@ -39,6 +47,37 @@ const knownCompoundEngineeringSkillIds = [
|
|||||||
"ce-work",
|
"ce-work",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
function expectSelfContainedBundle(pluginId: typeof selfContainedBundlePluginIds[number]) {
|
||||||
|
const stagedRoot = join(cliRoot, "dist", "plugins", pluginId);
|
||||||
|
const manifestPath = join(stagedRoot, "manifest.json");
|
||||||
|
const packageJsonPath = join(stagedRoot, "package.json");
|
||||||
|
const bundledPath = join(stagedRoot, "bundled.js");
|
||||||
|
|
||||||
|
expect(existsSync(bundledPath), `${pluginId} should ship bundled.js`).toBe(true);
|
||||||
|
expect(existsSync(join(stagedRoot, "src")), `${pluginId} should not ship raw src/`).toBe(false);
|
||||||
|
expect(
|
||||||
|
readdirSync(stagedRoot).some((entry) => /\.index\.reload-\d+\.ts$/.test(entry)),
|
||||||
|
`${pluginId} should not ship hot-reload TypeScript artifacts`,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(existsSync(manifestPath), `${pluginId} manifest should exist`).toBe(true);
|
||||||
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
|
||||||
|
expect(manifest.id).toBe(pluginId);
|
||||||
|
expect(typeof manifest.name).toBe("string");
|
||||||
|
expect(manifest.name?.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
||||||
|
exports?: { "."?: { import?: string } };
|
||||||
|
};
|
||||||
|
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
|
||||||
|
|
||||||
|
const bundled = readFileSync(bundledPath, "utf-8");
|
||||||
|
expect(bundled, `${pluginId} should not keep a bare @fusion/core import`).not.toMatch(
|
||||||
|
/from\s+["']@fusion\/core["']/,
|
||||||
|
);
|
||||||
|
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain("@fusion/core");
|
||||||
|
}
|
||||||
|
|
||||||
describe("CLI bundle output", () => {
|
describe("CLI bundle output", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Intentional: bundle-output tests validate compiled artifacts, so they
|
// Intentional: bundle-output tests validate compiled artifacts, so they
|
||||||
@@ -296,40 +335,28 @@ describe("CLI bundle output", () => {
|
|||||||
/from\s+["']@fusion\/core["']/,
|
/from\s+["']@fusion\/core["']/,
|
||||||
);
|
);
|
||||||
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain(
|
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain(
|
||||||
'"@fusion/core"',
|
"@fusion/core",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(inspectedPluginIds.length).toBeGreaterThan(0);
|
expect(inspectedPluginIds.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dist/plugins/fusion-plugin-whatsapp-chat/ is staged with a valid manifest", () => {
|
it("reports, cli-printing-press, and whatsapp-chat ship self-contained bundled.js outputs", () => {
|
||||||
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-whatsapp-chat");
|
/*
|
||||||
const manifestPath = join(stagedRoot, "manifest.json");
|
* FNXC:BundledPlugins 2026-07-15-00:00:
|
||||||
|
* Surface Enumeration for FN-7956:
|
||||||
expect(existsSync(manifestPath)).toBe(true);
|
* - [x] Providers / bridges / execution paths: fusion-plugin-reports, fusion-plugin-cli-printing-press, and fusion-plugin-whatsapp-chat are all asserted through this bundlePluginEntry output invariant.
|
||||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
|
* - [x] Desktop + mobile breakpoints / platforms: N/A build/packaging-only change; desktop missing-bundle behavior for non-staged dependencies is unchanged.
|
||||||
expect(manifest.id).toBe("fusion-plugin-whatsapp-chat");
|
* - [x] Empty / undefined / duplicate / populated data states: each staged root must contain bundled.js, omit src/, and omit .index.reload-N.ts artifacts.
|
||||||
expect(typeof manifest.name).toBe("string");
|
* - [x] Shared hooks / components / modules / helpers: these ids are included in bundlePluginEntryPluginIds so the shared @fusion/core runtime-shim alias invariant covers them.
|
||||||
expect(manifest.name?.length).toBeGreaterThan(0);
|
* - [x] Every component that renders the affordance: N/A no UI affordance add/remove.
|
||||||
});
|
* - [x] Leftover shells after removal: package output assertions fail if the old raw-src branches leave src/ or reload TypeScript files behind.
|
||||||
|
* - [x] Runtime-value assertion: each emitted bundled.js has no from "@fusion/core" import and no literal @fusion/core occurrence.
|
||||||
it("dist/plugins/fusion-plugin-cli-printing-press/ is staged with source entry for bundled install", () => {
|
*/
|
||||||
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cli-printing-press");
|
for (const pluginId of selfContainedBundlePluginIds) {
|
||||||
const manifestPath = join(stagedRoot, "manifest.json");
|
expectSelfContainedBundle(pluginId);
|
||||||
const packageJsonPath = join(stagedRoot, "package.json");
|
}
|
||||||
|
|
||||||
expect(existsSync(manifestPath)).toBe(true);
|
|
||||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
|
|
||||||
expect(manifest.id).toBe("fusion-plugin-cli-printing-press");
|
|
||||||
expect(typeof manifest.name).toBe("string");
|
|
||||||
expect(manifest.name?.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
expect(existsSync(join(stagedRoot, "src", "index.ts"))).toBe(true);
|
|
||||||
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
||||||
exports?: { "."?: { import?: string } };
|
|
||||||
};
|
|
||||||
expect(stagedPkg.exports?.["."]?.import).toBe("./src/index.ts");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dist/plugins/fusion-plugin-openclaw-runtime/ is staged with required bridge assets", () => {
|
it("dist/plugins/fusion-plugin-openclaw-runtime/ is staged with required bridge assets", () => {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { BoardActionTaskStore, ColumnId, Task } from "@fusion/core";
|
import type { BoardActionTaskStore, ColumnId, Task } from "@fusion/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:BundledPlugins 2026-07-15-00:00:
|
||||||
|
* Reports and CLI Printing Press import the `postgresSchema` runtime namespace while the CLI bundler aliases `@fusion/core` to this shim for published plugin bundles. Re-export the concrete core schema build artifact here so npm-installed `bundled.js` files resolve `postgresSchema.plugin` without keeping a bare private `@fusion/core` runtime specifier or crashing with `Cannot find package '@fusion/core'`; using dist keeps @runfusion/fusion typecheck inside its package root while esbuild still bundles the schema runtime values.
|
||||||
|
*/
|
||||||
|
export * as postgresSchema from "../../core/dist/postgres/schema/index.js";
|
||||||
|
|
||||||
export const WORKFLOW_EXTENSION_SCHEMA_VERSION = 1 as const;
|
export const WORKFLOW_EXTENSION_SCHEMA_VERSION = 1 as const;
|
||||||
|
|
||||||
export function workflowExtensionRegistryId(pluginId: string, extensionId: string): string {
|
export function workflowExtensionRegistryId(pluginId: string, extensionId: string): string {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ type BundlePluginEntryOptions = {
|
|||||||
srcDir: string;
|
srcDir: string;
|
||||||
destDir: string;
|
destDir: string;
|
||||||
withMcpAsset?: boolean;
|
withMcpAsset?: boolean;
|
||||||
|
external?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type PackageManifest = {
|
type PackageManifest = {
|
||||||
@@ -161,7 +162,7 @@ function writeSanitizedCopiedManifest(srcPkgPath: string, destPkgPath: string) {
|
|||||||
writeFileSync(destPkgPath, JSON.stringify(destPkg, null, 2));
|
writeFileSync(destPkgPath, JSON.stringify(destPkg, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false }: BundlePluginEntryOptions) {
|
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false, external = [] }: BundlePluginEntryOptions) {
|
||||||
if (existsSync(destDir)) {
|
if (existsSync(destDir)) {
|
||||||
rmSync(destDir, { recursive: true, force: true });
|
rmSync(destDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -199,7 +200,7 @@ async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = fal
|
|||||||
platform: "node",
|
platform: "node",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
outfile: join(destDir, "bundled.js"),
|
outfile: join(destDir, "bundled.js"),
|
||||||
external: ["@fusion/engine"],
|
external: ["@fusion/engine", ...external],
|
||||||
alias: {
|
alias: {
|
||||||
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
|
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
|
||||||
/*
|
/*
|
||||||
@@ -475,20 +476,20 @@ const cliBuildConfig = {
|
|||||||
destDir: dependencyGraphPluginDest,
|
destDir: dependencyGraphPluginDest,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existsSync(whatsappChatPluginDest)) {
|
/*
|
||||||
rmSync(whatsappChatPluginDest, { recursive: true, force: true });
|
* FNXC:BundledPlugins 2026-07-15-00:00:
|
||||||
}
|
* FN-7956 moves WhatsApp Chat, Reports, and CLI Printing Press off raw `src/` staging because npm/global installs place them under `node_modules`, where Node refuses TypeScript type stripping. Route them through bundlePluginEntry so each published plugin root contains install-safe `bundled.js` output and no `.ts` entry tree.
|
||||||
if (existsSync(whatsappChatPluginSrc)) {
|
*/
|
||||||
mkdirSync(whatsappChatPluginDest, { recursive: true });
|
await bundlePluginEntry({
|
||||||
cpSync(join(whatsappChatPluginSrc, "manifest.json"), join(whatsappChatPluginDest, "manifest.json"));
|
pluginId: "fusion-plugin-whatsapp-chat",
|
||||||
writeSanitizedCopiedManifest(join(whatsappChatPluginSrc, "package.json"), join(whatsappChatPluginDest, "package.json"));
|
srcDir: whatsappChatPluginSrc,
|
||||||
cpSync(join(whatsappChatPluginSrc, "src"), join(whatsappChatPluginDest, "src"), { recursive: true });
|
destDir: whatsappChatPluginDest,
|
||||||
console.log("Copied WhatsApp chat plugin to dist/plugins/fusion-plugin-whatsapp-chat/");
|
/*
|
||||||
} else {
|
* FNXC:BundledPlugins 2026-07-15-09:12:
|
||||||
console.warn(
|
* Baileys contains optional dynamic require() paths for QR/link-preview/media helpers that are not needed for plugin module load. Keep those optional packages external so the published WhatsApp Chat bundled.js can be produced and loaded without reintroducing a raw TypeScript src/ entry under node_modules.
|
||||||
`WARNING: WhatsApp chat plugin source not found at ${whatsappChatPluginSrc}; bundled auto-install will be unavailable.`,
|
*/
|
||||||
);
|
external: ["jimp", "link-preview-js", "qrcode-terminal"],
|
||||||
}
|
});
|
||||||
|
|
||||||
await bundlePluginEntry({
|
await bundlePluginEntry({
|
||||||
pluginId: "fusion-plugin-roadmap",
|
pluginId: "fusion-plugin-roadmap",
|
||||||
@@ -508,35 +509,17 @@ const cliBuildConfig = {
|
|||||||
destDir: linearImportPluginDest,
|
destDir: linearImportPluginDest,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existsSync(reportsPluginDest)) {
|
await bundlePluginEntry({
|
||||||
rmSync(reportsPluginDest, { recursive: true, force: true });
|
pluginId: "fusion-plugin-reports",
|
||||||
}
|
srcDir: reportsPluginSrc,
|
||||||
if (existsSync(reportsPluginSrc)) {
|
destDir: reportsPluginDest,
|
||||||
mkdirSync(reportsPluginDest, { recursive: true });
|
});
|
||||||
cpSync(join(reportsPluginSrc, "manifest.json"), join(reportsPluginDest, "manifest.json"));
|
|
||||||
writeSanitizedCopiedManifest(join(reportsPluginSrc, "package.json"), join(reportsPluginDest, "package.json"));
|
|
||||||
cpSync(join(reportsPluginSrc, "src"), join(reportsPluginDest, "src"), { recursive: true });
|
|
||||||
console.log("Copied reports plugin to dist/plugins/fusion-plugin-reports/");
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`WARNING: Reports plugin source not found at ${reportsPluginSrc}; bundled auto-install will be unavailable.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(cliPrintingPressPluginDest)) {
|
await bundlePluginEntry({
|
||||||
rmSync(cliPrintingPressPluginDest, { recursive: true, force: true });
|
pluginId: "fusion-plugin-cli-printing-press",
|
||||||
}
|
srcDir: cliPrintingPressPluginSrc,
|
||||||
if (existsSync(cliPrintingPressPluginSrc)) {
|
destDir: cliPrintingPressPluginDest,
|
||||||
mkdirSync(cliPrintingPressPluginDest, { recursive: true });
|
});
|
||||||
cpSync(join(cliPrintingPressPluginSrc, "manifest.json"), join(cliPrintingPressPluginDest, "manifest.json"));
|
|
||||||
writeSanitizedCopiedManifest(join(cliPrintingPressPluginSrc, "package.json"), join(cliPrintingPressPluginDest, "package.json"));
|
|
||||||
cpSync(join(cliPrintingPressPluginSrc, "src"), join(cliPrintingPressPluginDest, "src"), { recursive: true });
|
|
||||||
console.log("Copied cli-printing-press plugin to dist/plugins/fusion-plugin-cli-printing-press/");
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`WARNING: cli-printing-press plugin source not found at ${cliPrintingPressPluginSrc}; bundled auto-install will be unavailable.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bundle each runtime plugin into a self-contained ESM file so npm/npx
|
// Bundle each runtime plugin into a self-contained ESM file so npm/npx
|
||||||
// installs can load them without the workspace `@fusion/plugin-sdk`.
|
// installs can load them without the workspace `@fusion/plugin-sdk`.
|
||||||
|
|||||||
Reference in New Issue
Block a user