From 3c82e6b239a269779b60bcd67f048d18485bf6c9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Feb 2026 18:35:29 +0100 Subject: [PATCH] :sparkles: Add better approach for handling plugin iframe url Ensure params are passed correctly to plugins declared to be version 2 and are prepared to run in a subpath. --- frontend/src/app/main/data/plugins.cljs | 24 +++++++++---------- frontend/src/app/plugins/register.cljs | 1 + .../src/lib/models/manifest.schema.ts | 1 + .../plugins-runtime/src/lib/parse-manifest.ts | 24 +++++++++++++++++-- .../src/lib/plugin-manager.spec.ts | 8 ++++--- .../plugins-runtime/src/lib/plugin-manager.ts | 7 +++--- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs index 5bdcdb776e..990cb2b981 100644 --- a/frontend/src/app/main/data/plugins.cljs +++ b/frontend/src/app/main/data/plugins.cljs @@ -66,22 +66,22 @@ (update-in state [:workspace-local :open-plugins] (fnil disj #{}) id)))) (defn- load-plugin! - [{:keys [plugin-id name description host code icon permissions]}] + [{:keys [plugin-id name version description host code icon permissions]}] (try (st/emit! (save-current-plugin plugin-id) (reset-plugin-flags plugin-id)) - (.ɵloadPlugin - ^js ug/global - #js {:pluginId plugin-id - :name name - :description description - :host host - :code code - :icon icon - :permissions (apply array permissions)} - (fn [] - (st/emit! (remove-current-plugin plugin-id)))) + (.ɵloadPlugin ^js ug/global + #js {:pluginId plugin-id + :name name + :description description + :version version + :host host + :code code + :icon icon + :permissions (apply array permissions)} + (fn [] + (st/emit! (remove-current-plugin plugin-id)))) (catch :default e (st/emit! (remove-current-plugin plugin-id)) diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index ebdee92254..81e9cd26d4 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -78,6 +78,7 @@ (d/without-nils {:plugin-id plugin-id :url (str plugin-url) + :version vers :name name :description desc :host origin diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts index 54f12e7044..bd5895c02b 100644 --- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts +++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts @@ -6,6 +6,7 @@ export const manifestSchema = z.object({ host: z.string().url(), code: z.string(), icon: z.string().optional(), + version: z.number().optional(), description: z.string().max(200).optional(), permissions: z.array( z.enum([ diff --git a/plugins/libs/plugins-runtime/src/lib/parse-manifest.ts b/plugins/libs/plugins-runtime/src/lib/parse-manifest.ts index c5323dd30a..95cf043d6f 100644 --- a/plugins/libs/plugins-runtime/src/lib/parse-manifest.ts +++ b/plugins/libs/plugins-runtime/src/lib/parse-manifest.ts @@ -1,8 +1,28 @@ import { Manifest } from './models/manifest.model.js'; import { manifestSchema } from './models/manifest.schema.js'; -export function getValidUrl(host: string, path: string): string { - return new URL(path, host).toString(); +export function getValidUrl(host: string, path: string): URL { + return new URL(path, host); +} + +export function prepareUrl(manifest: Manifest, url: string, params: object): string { + const result = getValidUrl(manifest.host, url); + for (const [k, v] of Object.entries(params)) { + if (!result.searchParams.has(k)) { + result.searchParams.set(k, v); + } + } + + if (manifest.version === undefined || manifest.version === 1) { + return result.toString(); + } else if (manifest.version === 2) { + const queryString = result.searchParams.toString(); + result.search = ''; + result.hash = `/?${queryString}`; + return result.toString(); + } else { + throw new Error('invalid manifest version'); + } } export function loadManifest(url: string): Promise { diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts index d8661a71d7..db33f3aa9b 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts @@ -1,6 +1,6 @@ import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'; import { createPluginManager } from './plugin-manager'; -import { loadManifestCode, getValidUrl } from './parse-manifest.js'; +import { loadManifestCode, getValidUrl, prepareUrl } from './parse-manifest.js'; import { PluginModalElement } from './modal/plugin-modal.js'; import { openUIApi } from './api/openUI.api.js'; import type { Context, Theme } from '@penpot/plugin-types'; @@ -9,6 +9,7 @@ import type { Manifest } from './models/manifest.model.js'; vi.mock('./parse-manifest.js', () => ({ loadManifestCode: vi.fn(), getValidUrl: vi.fn(), + prepareUrl: vi.fn(), })); vi.mock('./api/openUI.api.js', () => ({ @@ -71,7 +72,8 @@ describe('createPluginManager', () => { vi.mocked(loadManifestCode).mockResolvedValue( 'console.log("Plugin loaded");', ); - vi.mocked(getValidUrl).mockReturnValue('https://example.com/plugin'); + vi.mocked(getValidUrl).mockReturnValue(new URL('https://example.com/plugin')); + vi.mocked(prepareUrl).mockReturnValue('https://example.com/plugin'); }); afterEach(() => { @@ -110,7 +112,7 @@ describe('createPluginManager', () => { height: 300, }); - expect(getValidUrl).toHaveBeenCalledWith(manifest.host, '/test-url'); + expect(prepareUrl).toHaveBeenCalledWith(manifest, '/test-url', {theme: 'light'}); expect(openUIApi).toHaveBeenCalledWith( 'Test Modal', 'https://example.com/plugin', diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts index d6af48fc07..9022d242d9 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts @@ -1,6 +1,6 @@ import type { Context, Theme } from '@penpot/plugin-types'; -import { getValidUrl, loadManifestCode } from './parse-manifest.js'; +import { prepareUrl, loadManifestCode } from './parse-manifest.js'; import { Manifest } from './models/manifest.model.js'; import { PluginModalElement } from './modal/plugin-modal.js'; import { openUIApi } from './api/openUI.api.js'; @@ -80,9 +80,8 @@ export async function createPluginManager( }; const openModal = (name: string, url: string, options?: OpenUIOptions) => { - const theme = context.theme as 'light' | 'dark'; - - const modalUrl = getValidUrl(manifest.host, url); + const theme = context.theme as Theme; + const modalUrl = prepareUrl(manifest, url, {theme}); if (modal?.getAttribute('iframe-src') === modalUrl) { return;