fix(plugins): make manifests deployment relative

This commit is contained in:
Federico Jaramillo Martinez
2026-07-12 23:28:03 +02:00
parent 4b2882bb5c
commit ceeb0d413d
7 changed files with 70 additions and 34 deletions
+32 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadExternalPlugins } from "./external";
import { loadExternalPlugins, resolvePluginModuleUrl } from "./external";
beforeEach(() => {
vi.stubGlobal("document", { baseURI: "https://pi.example.test/" });
@@ -18,4 +18,35 @@ describe("external plugin manifests", () => {
expect(fetchMock).toHaveBeenCalledWith("https://pi.example.test/pi-web-plugins/manifest.json", { cache: "no-store" });
});
it("loads manifest-relative modules from a nested deployment", async () => {
const manifestUrl = "https://pi.example.test/test/ai/pi-web-plugins/manifest.json";
const fetchMock = vi.fn(() => Promise.resolve(new Response(JSON.stringify({
plugins: [{ id: "info", module: "./info/pi-web-plugin.js?v=1", machineSpecific: false }],
}))));
const moduleLoader = vi.fn(() => Promise.resolve({
default: { apiVersion: 1, name: "Info", activate: () => ({ contributions: {} }) },
}));
vi.stubGlobal("fetch", fetchMock);
const registrations = await loadExternalPlugins(manifestUrl, { moduleLoader });
expect(fetchMock).toHaveBeenCalledWith(manifestUrl, { cache: "no-store" });
expect(moduleLoader).toHaveBeenCalledWith("https://pi.example.test/test/ai/pi-web-plugins/info/pi-web-plugin.js?v=1");
expect(registrations).toMatchObject([{ id: "info", machineSpecific: false, plugin: { apiVersion: 1, name: "Info" } }]);
});
it("treats root-style modules from existing manifests as application-root paths", () => {
const rootManifestUrl = "https://pi.example.test/pi-web-plugins/manifest.json";
const nestedManifestUrl = "https://pi.example.test/test/ai/pi-web-plugins/manifest.json";
expect(resolvePluginModuleUrl("/pi-web-plugins/info/pi-web-plugin.js?v=1", rootManifestUrl, {
viteBaseUrl: "/",
documentBaseUrl: "https://pi.example.test/",
})).toBe("https://pi.example.test/pi-web-plugins/info/pi-web-plugin.js?v=1");
expect(resolvePluginModuleUrl("/pi-web-plugins/info/pi-web-plugin.js?v=1", nestedManifestUrl, {
viteBaseUrl: "./",
documentBaseUrl: "https://pi.example.test/test/ai/",
})).toBe("https://pi.example.test/test/ai/pi-web-plugins/info/pi-web-plugin.js?v=1");
});
});
+13 -3
View File
@@ -1,5 +1,5 @@
import { machineScopedPluginId } from "../../../shared/machinePluginIds";
import { resolveAppUrl } from "../appUrl";
import { resolveAppUrl, type AppUrlContext } from "../appUrl";
import type { PiWebPlugin, PiWebPluginRegistration } from "./types";
export interface PluginManifestEntry {
@@ -15,6 +15,7 @@ interface PluginManifest {
export interface LoadExternalPluginsOptions {
machineId?: string;
shouldLoadPlugin?: (entry: PluginManifestEntry) => boolean;
moduleLoader?: (moduleUrl: string) => Promise<unknown>;
}
export async function loadExternalPlugins(manifestUrl = "pi-web-plugins/manifest.json", options: LoadExternalPluginsOptions = {}): Promise<PiWebPluginRegistration[]> {
@@ -26,8 +27,8 @@ export async function loadExternalPlugins(manifestUrl = "pi-web-plugins/manifest
for (const entry of manifest.plugins) {
if (options.shouldLoadPlugin?.(entry) === false) continue;
try {
const moduleUrl = new URL(entry.module, resolvedManifestUrl).toString();
const module: unknown = await import(/* @vite-ignore */ moduleUrl);
const moduleUrl = resolvePluginModuleUrl(entry.module, resolvedManifestUrl);
const module = await (options.moduleLoader ?? importPluginModule)(moduleUrl);
const plugin = parsePluginModule(module, moduleUrl);
registrations.push({
id: options.machineId === undefined ? entry.id : machineScopedPluginId(options.machineId, entry.id),
@@ -42,6 +43,15 @@ export async function loadExternalPlugins(manifestUrl = "pi-web-plugins/manifest
return registrations;
}
export function resolvePluginModuleUrl(moduleReference: string, manifestUrl: string, appUrlContext?: AppUrlContext): string {
if (!moduleReference.startsWith("/")) return new URL(moduleReference, manifestUrl).toString();
return appUrlContext === undefined ? resolveAppUrl(moduleReference) : resolveAppUrl(moduleReference, appUrlContext);
}
async function importPluginModule(moduleUrl: string): Promise<unknown> {
return import(/* @vite-ignore */ moduleUrl);
}
async function fetchPluginManifest(manifestUrl: string): Promise<PluginManifest | undefined> {
const response = await fetch(manifestUrl, { cache: "no-store" });
if (response.status === 404) return undefined;