Compare commits

...

18 Commits

Author SHA1 Message Date
Andrey Antukh
fa987c53d2 Include plugins on the frontend bundle 2026-02-24 00:14:01 +01:00
Andrey Antukh
8547fc72ee Make the rename-layers-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
765a57e0a0 Make the create-palette-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
0ccdc97c83 Make the constrast-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
5b122c01e8 Make the lorem-ipsum-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
a02dd1c6fb Make the poc-tokens-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
5bb2302c79 Make the poc-state-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
f65034e20c Make the colors-to-tokens-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
976708a733 Make the table-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
8a9349ef76 Make the icons-plugin work correctly on subpath 2026-02-24 00:14:01 +01:00
Andrey Antukh
a4cbd31904 Add minor changes on plugins examples-style 2026-02-24 00:14:01 +01:00
Andrey Antukh
1103bba5b0 Add the ability to accept plugin permission pressing enter 2026-02-24 00:14:01 +01:00
Andrey Antukh
5a4e62070c 💄 Add cosmetic changes to workspace plugins dialog components 2026-02-24 00:14:01 +01:00
Andrey Antukh
679d1dc432 Install plugin by pressing enter on plugins dialog 2026-02-24 00:14:01 +01:00
Andrey Antukh
4d45dfc38e Append manifest.json to plugin uri if it comes without it 2026-02-24 00:14:01 +01:00
Andrey Antukh
791f3f3b28 Make penpot depend on local plugins runtime
This removes the need to publish versions to pnpn
2026-02-24 00:13:59 +01:00
Andrey Antukh
3c82e6b239 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.
2026-02-24 00:11:08 +01:00
Andrey Antukh
56358ab631 Update devenv nginx to serve locally builded plugins 2026-02-24 00:11:08 +01:00
50 changed files with 331 additions and 134 deletions

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"type": "module",
"repository": {
"type": "git",

View File

@@ -126,6 +126,12 @@ http {
proxy_http_version 1.1;
}
location /plugins {
autoindex on;
alias /home/penpot/penpot/plugins/dist/apps;
proxy_http_version 1.1;
}
location /mcp/ws {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"browserslist": [
"defaults"
],
@@ -48,7 +48,7 @@
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/plugins-runtime": "link:../plugins/dist/plugins-runtime",
"@penpot/plugins-runtime": "link:../plugins/libs/plugins-runtime",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",
"@penpot/tokenscript": "workspace:./packages/tokenscript",

View File

@@ -20,8 +20,8 @@ importers:
specifier: workspace:./packages/mousetrap
version: link:packages/mousetrap
'@penpot/plugins-runtime':
specifier: link:../plugins/dist/plugins-runtime
version: link:../plugins/dist/plugins-runtime
specifier: link:../plugins/libs/plugins-runtime
version: link:../plugins/libs/plugins-runtime
'@penpot/svgo':
specifier: penpot/svgo#v3.2
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b

View File

@@ -39,6 +39,14 @@ rm -rf node_modules;
WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build:multi-user
popd;
pushd ../plugins
rm -rf node_modules;
rm -rf dist/apps/;
corepack install;
pnpm -r install;
pnpm run build:plugins;
popd
pnpm run build:app:main $EXTRA_PARAMS;
pnpm run build:app:libs;
pnpm run build:app:assets;
@@ -47,6 +55,11 @@ sed -i "s/\.\/render.js/.\/render.js?version=$VERSION_TAG/g" resources/public/js
rsync -avr resources/public/ target/dist/
# Include all plugins on the bundle
rsync -avr ../plugins/dist/apps/ target/dist/plugins/;
# Include the MCP plugin on the bundle
mkdir -p target/dist/plugins/mcp/;
rsync -avr ../mcp/packages/plugin/dist/ target/dist/plugins/mcp/
mkdir -p target/dist/plugins/mcp;
rsync -avr ../mcp/packages/plugin/dist/ target/dist/plugins/mcp/;

View File

@@ -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))

View File

@@ -13,7 +13,7 @@
[rumext.v2 :as mf]))
(mf/defc search-bar*
[{:keys [id class value placeholder icon-id auto-focus on-change on-clear children]}]
[{:keys [id class value placeholder icon-id auto-focus on-change on-clear on-submit children]}]
(let [handle-change
(mf/use-fn
(mf/deps on-change)
@@ -31,12 +31,19 @@
handle-key-down
(mf/use-fn
(mf/deps on-submit)
(fn [event]
(let [enter? (kbd/enter? event)
esc? (kbd/esc? event)
node (dom/get-target event)]
(when ^boolean enter? (dom/blur! node))
(when ^boolean enter?
(dom/blur! node)
(when (fn? on-submit)
(let [value (dom/get-target-val event)]
(on-submit value event))))
(when ^boolean esc? (dom/blur! node)))))]
[:span {:class (stl/css-case :search-box true
:has-children (some? children))}
children

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uri :as u]
[app.config :as cfg]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
@@ -24,6 +25,7 @@
[app.plugins.register :as preg]
[app.util.avatars :as avatars]
[app.util.dom :as dom]
[app.util.globals :as global]
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@@ -33,7 +35,19 @@
(def ^:private close-icon
(deprecated-icon/icon-xref :close (stl/css :close-icon)))
(defn icon-url
(defn- normalize-plugin-url
"Automatically appens manifest.json if the plugin-uri comes without it."
[plugin-url]
(if (str/ends-with? plugin-url "manifest.json")
plugin-url
(-> (u/uri plugin-url)
(update :path (fn [path]
(if (str/ends-with? path "/")
(str path "manifest.json")
(str path "/manifest.json"))))
(str))))
(defn- icon-url
"Creates an sanitizes de icon URL to display"
[host icon]
(dm/str host
@@ -94,48 +108,49 @@
::mf/register-as :plugin-management}
[]
(let [plugins-state* (mf/use-state #(preg/plugins-list))
plugins-state (deref plugins-state*)
(let [plugins-state* (mf/use-state #(preg/plugins-list))
plugins-state (deref plugins-state*)
plugin-url* (mf/use-state "")
plugin-url (deref plugin-url*)
plugin-url* (mf/use-state "")
plugin-url (deref plugin-url*)
fetching-manifest? (mf/use-state false)
input-status* (mf/use-state nil) ;; :error-url :error-manifest :success
input-status (deref input-status*)
input-status* (mf/use-state nil) ;; :error-url :error-manifest :success
input-status @input-status*
error-url? (= :error-url input-status)
error-url? (= :error-url input-status)
error-manifest? (= :error-manifest input-status)
error? (or error-url? error-manifest?)
error? (or error-url? error-manifest?)
user-can-edit? (:can-edit (deref refs/permissions))
permissions (mf/deref refs/permissions)
user-can-edit? (get permissions :can-edit)
handle-url-input
fetching-manifest?
(mf/use-state false)
on-url-change
(mf/use-fn
(fn [value]
(reset! input-status* nil)
(reset! plugin-url* value)))
handle-install-click
on-install
(mf/use-fn
(mf/deps plugins-state plugin-url)
(fn []
(reset! fetching-manifest? true)
(->> (dp/fetch-manifest plugin-url)
(->> (dp/fetch-manifest (normalize-plugin-url plugin-url))
(rx/subs!
(fn [plugin]
(reset! fetching-manifest? false)
(if plugin
(do
(st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
(modal/show!
:plugin-permissions
{:plugin plugin
:on-accept
#(do
(preg/install-plugin! plugin)
(modal/show! :plugin-management {}))})
(modal/show! :plugin-permissions
{:plugin plugin
:on-accept
#(do
(preg/install-plugin! plugin)
(modal/show! :plugin-management {}))})
(reset! input-status* :success)
(reset! plugin-url* ""))
;; Cannot get the manifest
@@ -145,7 +160,7 @@
(reset! fetching-manifest? false)
(reset! input-status* :error-url))))))
handle-open-plugin
on-open-plugin
(mf/use-fn
(fn [manifest]
(st/emit! (ptk/event ::ev/event {::ev/name "start-plugin"
@@ -155,7 +170,7 @@
(dp/open-plugin! manifest user-can-edit?)
(modal/hide!)))
handle-remove-plugin
on-remove-plugin
(mf/use-fn
(mf/deps plugins-state)
(fn [plugin-index]
@@ -175,14 +190,16 @@
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :top-bar)}
[:> search-bar* {:on-change handle-url-input
[:> search-bar* {:on-change on-url-change
:on-submit on-install
:value plugin-url
:placeholder (tr "workspace.plugins.search-placeholder")
:class (stl/css-case :input-error error?)}]
[:button {:class (stl/css :primary-button)
:disabled @fetching-manifest?
:on-click handle-install-click} (tr "workspace.plugins.install")]]
:on-click on-install}
(tr "workspace.plugins.install")]]
(when error-url?
[:div {:class (stl/css-case :info true :error error?)}
@@ -220,10 +237,11 @@
:index idx
:manifest manifest
:user-can-edit user-can-edit?
:on-open-plugin handle-open-plugin
:on-remove-plugin handle-remove-plugin}])]])]]]))
:on-open-plugin on-open-plugin
:on-remove-plugin on-remove-plugin}])]])]]]))
(mf/defc plugins-permission-list
(mf/defc plugins-permission-list*
{::mf/private true}
[{:keys [permissions]}]
[:div {:class (stl/css :permissions-list)}
(cond
@@ -291,36 +309,45 @@
::mf/register-as :plugin-permissions}
[{:keys [plugin on-accept on-close]}]
(let [{:keys [host permissions]} plugin
permissions (set permissions)
(let [host
(:host plugin)
handle-accept-dialog
permissions
(-> plugin :permissions set)
on-accept-dialog
(mf/use-fn
(mf/deps on-accept)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "allow-plugin-permissions"
:host host
:permissions (->> permissions (str/join ", "))})
(st/emit! (ev/event {::ev/name "allow-plugin-permissions"
:host host
:permissions (str/join ", " permissions)})
(modal/hide))
(when on-accept (on-accept))))
handle-close-dialog
on-close-dialog
(mf/use-fn
(mf/deps on-close)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "reject-plugin-permissions"
:host host
:permissions (->> permissions (str/join ", "))})
(st/emit! (ev/event {::ev/name "reject-plugin-permissions"
:host host
:permissions (str/join ", " permissions)})
(modal/hide))
(when on-close (on-close))))]
(mf/with-effect [on-accept-dialog]
(.addEventListener ^js global/document "keydown" on-accept-dialog)
#(.removeEventListener ^js global/document "keydown" on-accept-dialog))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :plugin-permissions)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon]
[:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions.title" (str/upper (:name plugin)))]
[:div {:class (stl/css :modal-content)}
[:& plugins-permission-list {:permissions permissions}]
[:> plugins-permission-list* {:permissions permissions}]
(when-not (contains? cfg/plugins-whitelist host)
[:div {:class (stl/css :permissions-disclaimer)}
@@ -332,13 +359,13 @@
{:class (stl/css :cancel-button :button-expand)
:type "button"
:value (tr "ds.confirm-cancel")
:on-click handle-close-dialog}]
:on-click on-close-dialog}]
[:input
{:class (stl/css :primary-button :button-expand)
:type "button"
:value (tr "ds.confirm-allow")
:on-click handle-accept-dialog}]]]]]))
:on-click on-accept-dialog}]]]]]))
(mf/defc plugins-permissions-updated-dialog
@@ -346,11 +373,15 @@
::mf/register-as :plugin-permissions-update}
[{:keys [plugin on-accept on-close]}]
(let [{:keys [host permissions]} plugin
permissions (set permissions)
(let [host
(:host plugin)
handle-accept-dialog
permissions
(-> plugin :permissions set)
on-accept-dialog
(mf/use-fn
(mf/deps on-accept)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "allow-plugin-permissions"
@@ -359,8 +390,9 @@
(modal/hide))
(when on-accept (on-accept))))
handle-close-dialog
on-close-dialog
(mf/use-fn
(mf/deps on-close)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "reject-plugin-permissions"
@@ -369,16 +401,20 @@
(modal/hide))
(when on-close (on-close))))]
(mf/with-effect [on-accept-dialog]
(.addEventListener ^js global/document "keydown" on-accept-dialog)
#(.removeEventListener ^js global/document "keydown" on-accept-dialog))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :plugin-permissions)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon]
[:div {:class (stl/css :modal-title)}
(tr "workspace.plugins.permissions-update.title" (str/upper (:name plugin)))]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-paragraph)}
(tr "workspace.plugins.permissions-update.warning")]
[:& plugins-permission-list {:permissions permissions}]]
[:> plugins-permission-list* {:permissions permissions}]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
@@ -386,13 +422,13 @@
{:class (stl/css :cancel-button :button-expand)
:type "button"
:value (tr "ds.confirm-cancel")
:on-click handle-close-dialog}]
:on-click on-close-dialog}]
[:input
{:class (stl/css :primary-button :button-expand)
:type "button"
:value (tr "ds.confirm-allow")
:on-click handle-accept-dialog}]]]]]))
:on-click on-accept-dialog}]]]]]))
(mf/defc plugins-try-out-dialog
@@ -402,25 +438,31 @@
(let [{:keys [icon host name]} plugin
handle-accept-dialog
on-accept-dialog
(mf/use-fn
(mf/deps on-accept)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "try-out-accept"})
(modal/hide))
(when on-accept (on-accept))))
handle-close-dialog
on-close-dialog
(mf/use-fn
(mf/deps on-close)
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "try-out-cancel"})
(modal/hide))
(when on-close (on-close))))]
(mf/with-effect [on-accept-dialog]
(.addEventListener ^js global/document "keydown" on-accept-dialog)
#(.removeEventListener ^js global/document "keydown" on-accept-dialog))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :plugin-try-out)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon]
[:div {:class (stl/css :modal-title)}
[:div {:class (stl/css :plugin-icon)}
[:img {:src (if (some? icon)
@@ -438,10 +480,10 @@
{:class (stl/css :cancel-button :button-expand)
:type "button"
:value (tr "workspace.plugins.try-out.cancel")
:on-click handle-close-dialog}]
:on-click on-close-dialog}]
[:input
{:class (stl/css :primary-button :button-expand)
:type "button"
:value (tr "workspace.plugins.try-out.try")
:on-click handle-accept-dialog}]]]]]))
:on-click on-accept-dialog}]]]]]))

View File

@@ -78,6 +78,7 @@
(d/without-nils
{:plugin-id plugin-id
:url (str plugin-url)
:version vers
:name name
:description desc
:host origin

View File

@@ -12,7 +12,10 @@
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/apps/contrast-plugin",
"outputPath": {
"base": "dist/apps/contrast-plugin",
"browser": ""
},
"index": "apps/contrast-plugin/src/index.html",
"browser": "apps/contrast-plugin/src/main.ts",
"polyfills": ["zone.js"],
@@ -20,6 +23,7 @@
"assets": [
"apps/contrast-plugin/src/_headers",
"apps/contrast-plugin/src/favicon.ico",
"apps/contrast-plugin/src/manifest.json",
"apps/contrast-plugin/src/assets"
],
"styles": [
@@ -88,6 +92,7 @@
"assets": [
"apps/icons-plugin/src/_headers",
"apps/icons-plugin/src/favicon.ico",
"apps/icons-plugin/src/manifest.json",
"apps/icons-plugin/src/assets"
],
"styles": [
@@ -156,6 +161,7 @@
"assets": [
"apps/lorem-ipsum-plugin/src/_headers",
"apps/lorem-ipsum-plugin/src/favicon.ico",
"apps/lorem-ipsum-plugin/src/manifest.json",
"apps/lorem-ipsum-plugin/src/assets"
],
"styles": [
@@ -218,7 +224,10 @@
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/apps/table-plugin",
"outputPath": {
"base": "dist/apps/table-plugin",
"browser": ""
},
"index": "apps/table-plugin/src/index.html",
"browser": "apps/table-plugin/src/main.ts",
"polyfills": ["zone.js"],
@@ -226,6 +235,7 @@
"assets": [
"apps/table-plugin/src/_headers",
"apps/table-plugin/src/favicon.ico",
"apps/table-plugin/src/manifest.json",
"apps/table-plugin/src/assets"
],
"styles": [
@@ -294,6 +304,7 @@
"assets": [
"apps/rename-layers-plugin/src/_headers",
"apps/rename-layers-plugin/src/favicon.ico",
"apps/rename-layers-plugin/src/manifest.json",
"apps/rename-layers-plugin/src/assets"
],
"styles": [
@@ -356,7 +367,10 @@
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/apps/colors-to-tokens-plugin",
"outputPath": {
"base": "dist/apps/colors-to-tokens-plugin",
"browser": ""
},
"index": "apps/colors-to-tokens-plugin/src/index.html",
"browser": "apps/colors-to-tokens-plugin/src/main.ts",
"polyfills": ["zone.js"],
@@ -364,6 +378,7 @@
"assets": [
"apps/colors-to-tokens-plugin/src/_headers",
"apps/colors-to-tokens-plugin/src/favicon.ico",
"apps/colors-to-tokens-plugin/src/manifest.json",
"apps/colors-to-tokens-plugin/src/assets"
],
"styles": [
@@ -433,6 +448,7 @@
"tsConfig": "apps/poc-state-plugin/tsconfig.app.json",
"assets": [
"apps/poc-state-plugin/src/favicon.ico",
"apps/poc-state-plugin/src/manifest.json",
"apps/poc-state-plugin/src/assets"
],
"styles": [
@@ -503,6 +519,7 @@
"assets": [
"apps/poc-tokens-plugin/src/_headers",
"apps/poc-tokens-plugin/src/favicon.ico",
"apps/poc-tokens-plugin/src/manifest.json",
"apps/poc-tokens-plugin/src/assets"
],
"styles": [

View File

@@ -7,9 +7,9 @@
"build": "ng build colors-to-tokens-plugin && pnpm run build:plugin",
"build:dev": "ng build colors-to-tokens-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=colors-to-tokens-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=colors-to-tokens-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=colors-to-tokens-plugin --watch",
"serve": "ng serve colors-to-tokens-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -1,6 +1,6 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withHashLocation } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter([])],
providers: [provideRouter([], withHashLocation())],
};

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>colors-to-tokens-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>

View File

@@ -0,0 +1,8 @@
{
"name": "Colors to Tokens",
"description": "Generate a design tokens file from a list of colors",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"permissions": ["content:read", "library:read", "allow:downloads"]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build contrast-plugin && pnpm run build:plugin",
"build:dev": "ng build contrast-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=contrast-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=contrast-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=contrast-plugin --watch",
"serve": "ng serve contrast-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -1,6 +1,6 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withHashLocation } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter([])],
providers: [provideRouter([], withHashLocation())],
};

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>contrast-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>

View File

@@ -0,0 +1,8 @@
{
"name": "Contrast",
"description": "Measure contrast plugin",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"permissions": ["content:read"]
}

View File

@@ -5,10 +5,10 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:watch": "vite build --watch --mode development",
"preview": "vite preview",
"init": "concurrently --kill-others --names build,serve \"pnpm run build:watch\" \"pnpm run preview\"",
"build": "vite build --emptyOutDir",
"watch": "vite build --watch --mode development",
"serve": "vite preview",
"init": "concurrently --kill-others --names build,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -0,0 +1,8 @@
{
"name": "Create Palette from library",
"description": "Create a board with all the colors in the local library",
"code": "plugin.js",
"version": 2,
"icon": "assets/icon.png",
"permissions": ["content:read", "content:write", "library:read"]
}

View File

@@ -5,12 +5,11 @@ import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
server: {
port: 4305,
port: 4202,
host: '0.0.0.0',
},
preview: {
port: 4305,
port: 4202,
host: '0.0.0.0',
},
plugins: [tsconfigPaths()],

View File

@@ -3,8 +3,6 @@
<head>
<meta charset="utf-8" />
<title>Penpot plugin styles</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />

View File

@@ -6,8 +6,9 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"build:watch": "vite build --watch --mode development",
"preview": "vite preview",
"watch": "vite build --watch --mode development",
"init": "concurrently --kill-others --names build,serve \"pnpm run watch\" \"pnpm run serve\"",
"serve": "vite preview",
"lint": "eslint ."
}
}

View File

@@ -5,12 +5,12 @@ import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
server: {
port: 4201,
port: 4202,
host: '0.0.0.0',
},
preview: {
port: 4201,
port: 4202,
host: '0.0.0.0',
},

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>icons-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>

View File

@@ -0,0 +1,8 @@
{
"name": "Icons plugin",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"description": "Create icons from the Feather Icons set",
"permissions": ["content:read", "content:write"]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build lorem-ipsum-plugin && pnpm run build:plugin",
"build:dev": "ng build lorem-ipsum-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=lorem-ipsum-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=lorem-ipsum-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=lorem-ipsum-plugin --watch",
"serve": "ng serve lorem-ipsum-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>lorem-ipsum-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>

View File

@@ -0,0 +1,8 @@
{
"name": "Lorem ipsum",
"description": "Lorem ipsum text generator plugin",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"permissions": ["content:read", "content:write"]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build poc-state-plugin && pnpm run build:plugin",
"build:dev": "ng build poc-state-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-state-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-state-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-state-plugin --watch",
"serve": "ng serve poc-state-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>poc-state-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>

View File

@@ -0,0 +1,14 @@
{
"name": "POC State Read",
"description": "Sandbox plugin for plugins development",
"version": 2,
"code": "assets/plugin.js",
"permissions": [
"content:write",
"library:write",
"comment:write",
"user:read",
"allow:downloads",
"allow:localstorage"
]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build poc-tokens-plugin && pnpm run build:plugin",
"build:dev": "ng build poc-tokens-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-tokens-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-tokens-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=poc-tokens-plugin --watch",
"serve": "ng serve poc-tokens-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "exit 0"
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>Angular example plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>

View File

@@ -0,0 +1,15 @@
{
"name": "Design tokens plugin POC",
"description": "This is a plugin to try Design Tokens in Penpot API",
"version": 2,
"code": "assets/plugin.js",
"permissions": [
"page:read",
"content:read",
"file:read",
"selection:read",
"content:write",
"library:read",
"library:write"
]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build rename-layers-plugin && pnpm run build:plugin",
"build:dev": "ng build rename-layers-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=rename-layers-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=rename-layers-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=rename-layers-plugin --watch",
"serve": "ng serve rename-layers-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>rename-layers-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>

View File

@@ -0,0 +1,8 @@
{
"name": "Rename layers plugin",
"description": "Change the name of one or several layers",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"permissions": ["content:read", "content:write"]
}

View File

@@ -7,9 +7,9 @@
"build": "ng build table-plugin && pnpm run build:plugin",
"build:dev": "ng build table-plugin --configuration development",
"build:plugin": "node ../../tools/scripts/build-plugin.mjs --plugin=table-plugin",
"build:plugin:watch": "node ../../tools/scripts/build-plugin.mjs --plugin=table-plugin --watch",
"watch": "node ../../tools/scripts/build-plugin.mjs --plugin=table-plugin --watch",
"serve": "ng serve table-plugin",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run build:plugin:watch\" \"pnpm run serve\"",
"init": "concurrently --kill-others --names plugin,serve \"pnpm run watch\" \"pnpm run serve\"",
"lint": "eslint .",
"test": "vitest"
}

View File

@@ -1,7 +1,7 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withHashLocation } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes)],
providers: [provideRouter(appRoutes, withHashLocation())],
};

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<title>table-plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>

View File

@@ -0,0 +1,8 @@
{
"name": "Table plugin",
"description": "Table plugin to import or create tables",
"version": 2,
"code": "assets/plugin.js",
"icon": "assets/icon.png",
"permissions": ["content:read", "content:write"]
}

View File

@@ -0,0 +1,6 @@
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
export default defineConfig({
root: './',
});

View File

@@ -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([

View File

@@ -1,8 +1,32 @@
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<Manifest> {

View File

@@ -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,10 @@ 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 +114,9 @@ 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',

View File

@@ -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;

View File

@@ -35,7 +35,7 @@ export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../../dist/plugins-runtime',
outDir: './dist/',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,

View File

@@ -3,7 +3,7 @@
"version": "0.6.0",
"type": "module",
"license": "MIT",
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"scripts": {
"start": "pnpm run start:app:runtime",
"start:app:runtime": "concurrently --kill-others --names build,server \"pnpm --filter @penpot/plugins-runtime run build:watch\" \"pnpm --filter @penpot/plugins-runtime run preview\"",
@@ -18,7 +18,7 @@
"start:plugin:colors-to-tokens": "pnpm --filter colors-to-tokens-plugin run init",
"start:plugin:poc-tokens": "pnpm --filter poc-tokens-plugin run init",
"build:runtime": "pnpm --filter @penpot/plugins-runtime build",
"build:plugins": "pnpm --filter './apps/*-plugin' --filter '!poc-state-plugin' build",
"build:plugins": "pnpm --parallel --filter './apps/*-plugin' --filter '!poc-state-plugin' build",
"build:styles-example": "pnpm --filter example-styles build",
"lint": "pnpm -r --parallel lint",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,html,css}\"",