From 7c2dee65fcd40f8fcdf2638112ed5e50e7e37370 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Fri, 17 May 2024 12:52:42 +0200 Subject: [PATCH] feat: add type checking to plugin api (t#7606) --- libs/plugin-types/index.d.ts | 37 ++-- libs/plugin-types/project.json | 3 + libs/plugins-runtime/src/index.ts | 9 +- libs/plugins-runtime/src/lib/api/index.ts | 12 +- .../plugins-runtime/src/lib/api/openUI.api.ts | 2 +- .../src/lib/api/plugin-api.spec.ts | 26 +-- .../src/lib/models/manifest.model.ts | 4 +- .../src/lib/models/open-ui-options.model.ts | 4 +- .../plugins-runtime/src/lib/parse-manifest.ts | 6 +- libs/plugins-runtime/vite.config.ts | 6 + package-lock.json | 203 +++++++++++++++++- package.json | 1 + 12 files changed, 262 insertions(+), 51 deletions(-) diff --git a/libs/plugin-types/index.d.ts b/libs/plugin-types/index.d.ts index 3388fe3..a9a0611 100644 --- a/libs/plugin-types/index.d.ts +++ b/libs/plugin-types/index.d.ts @@ -172,17 +172,17 @@ export interface PenpotGridLayout { bottomPadding: number; leftPadding: number; - addRow(type: PenpotTrackType, value?: number); - addRowAtIndex(index: number, type: PenpotTrackType, value?: number); - addColumn(type: PenpotTrackType, value?: number); - addColumnAtIndex(index: number, type: PenpotTrackType, value: number); - removeRow(index: number); - removeColumn(index: number); - setColumn(index: number, type: PenpotTrackType, value?: number); - setRow(index: number, type: PenpotTrackType, value?: number); + addRow(type: PenpotTrackType, value?: number): void; + addRowAtIndex(index: number, type: PenpotTrackType, value?: number): void; + addColumn(type: PenpotTrackType, value?: number): void; + addColumnAtIndex(index: number, type: PenpotTrackType, value: number): void; + removeRow(index: number): void; + removeColumn(index: number): void; + setColumn(index: number, type: PenpotTrackType, value?: number): void; + setRow(index: number, type: PenpotTrackType, value?: number): void; - appendChild(child: PenpotShape, row: number, column: number); - remove(); + appendChild(child: PenpotShape, row: number, column: number): void; + remove(): void; } export interface PenpotShapeBase { @@ -239,9 +239,9 @@ export interface PenpotShapeBase { fills: PenpotFill[]; strokes: PenpotStroke[]; - resize(width: number, height: number); + resize(width: number, height: number): void; clone(): PenpotShape; - remove(); + remove(): void; } export interface PenpotFrame extends PenpotShapeBase { @@ -342,16 +342,21 @@ export interface PenpotContext { uploadMediaUrl(name: string, url: string): Promise; - group(first: PenpotShape, ...rest: PenpotShape): PenpotGroup; - ungroup(first: PenpotShape, ...rest: PenpotShape); + group(first: PenpotShape, ...rest: PenpotShape[]): PenpotGroup; + ungroup(first: PenpotShape, ...rest: PenpotShape[]): void; createRectangle(): PenpotRectangle; createFrame(): PenpotFrame; - createShapeFromSvg(svgString): PenpotGroup; + createShapeFromSvg(svgString: string): PenpotGroup; createText(text: string): PenpotText; + addListener( + type: T, + callback: (event: EventsMap[T]) => void + ): void; } -export interface Penpot extends PenpotContext { +export interface Penpot + extends Omit { ui: { open: ( name: string, diff --git a/libs/plugin-types/project.json b/libs/plugin-types/project.json index 2dda7f0..a136f63 100644 --- a/libs/plugin-types/project.json +++ b/libs/plugin-types/project.json @@ -4,6 +4,9 @@ "sourceRoot": "libs/plugin-types", "projectType": "library", "targets": { + "lint": { + "command": "tsc --noEmit" + }, "build": { "command": "node tools/scripts/build-types.mjs", "options": { diff --git a/libs/plugins-runtime/src/index.ts b/libs/plugins-runtime/src/index.ts index 45bbe96..4b6c473 100644 --- a/libs/plugins-runtime/src/index.ts +++ b/libs/plugins-runtime/src/index.ts @@ -14,12 +14,13 @@ repairIntrinsics({ consoleTaming: 'unsafe', }); -globalThis.initPluginsRuntime = (context: PenpotContext) => { +const globalThisAny$ = globalThis as any; + +globalThisAny$.initPluginsRuntime = (context: PenpotContext) => { if (context) { console.log('%c[PLUGINS] Initialize context', 'color: #008d7c'); - /* eslint-disable */ - globalThis.ɵcontext = context; + globalThisAny$.ɵcontext = context; globalThis.ɵloadPlugin = ɵloadPlugin; setContext(context); @@ -27,7 +28,5 @@ globalThis.initPluginsRuntime = (context: PenpotContext) => { for (const event of api.validEvents) { context.addListener(event, api.triggerEvent.bind(null, event)); } - - /* eslint-enable */ } }; diff --git a/libs/plugins-runtime/src/lib/api/index.ts b/libs/plugins-runtime/src/lib/api/index.ts index 0b55b62..1181bf5 100644 --- a/libs/plugins-runtime/src/lib/api/index.ts +++ b/libs/plugins-runtime/src/lib/api/index.ts @@ -16,7 +16,7 @@ import type { import { Manifest, Permissions } from '../models/manifest.model.js'; import { OpenUIOptions } from '../models/open-ui-options.model.js'; import openUIApi from './openUI.api.js'; -import z from 'zod'; +import { z } from 'zod'; import type { PluginModalElement } from '../plugin-modal.js'; type Callback = (message: T) => void; @@ -45,7 +45,7 @@ export function triggerEvent( message: EventsMap[keyof EventsMap] ) { if (type === 'themechange' && modal) { - modal.setTheme(message); + modal.setTheme(message as PenpotTheme); } const listeners = eventListeners.get(type) || []; listeners.forEach((listener) => listener(message)); @@ -110,7 +110,7 @@ export function createApi(context: PenpotContext, manifest: Manifest): Penpot { setTimeout: z .function() .args(z.function(), z.number()) - .implement((callback, time) => { + .implement((callback: Callback, time: number) => { setTimeout(callback, time); }), @@ -169,17 +169,17 @@ export function createApi(context: PenpotContext, manifest: Manifest): Penpot { return context.selection; }, - get viewport(): PenpotViewport[] { + get viewport(): PenpotViewport { checkPermission('selection:read'); return context.viewport; }, - getFile(): PenpotFile { + getFile(): PenpotFile | null { checkPermission('file:read'); return context.getFile(); }, - getPage(): PenpotPage { + getPage(): PenpotPage | null { checkPermission('page:read'); return context.getPage(); }, diff --git a/libs/plugins-runtime/src/lib/api/openUI.api.ts b/libs/plugins-runtime/src/lib/api/openUI.api.ts index 35a28f1..675add3 100644 --- a/libs/plugins-runtime/src/lib/api/openUI.api.ts +++ b/libs/plugins-runtime/src/lib/api/openUI.api.ts @@ -1,4 +1,4 @@ -import z from 'zod'; +import { z } from 'zod'; import { openUISchema } from '../models/open-ui-options.schema.js'; import { createModal } from '../create-modal.js'; diff --git a/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts b/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts index 39fa139..34c3fec 100644 --- a/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts +++ b/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts @@ -1,7 +1,7 @@ import { expect, describe, vi } from 'vitest'; import { createApi, triggerEvent, uiMessagesCallbacks } from './index.js'; import openUIApi from './openUI.api.js'; -import { FileState } from '@penpot/plugin-types'; +import type { PenpotFile } from '@penpot/plugin-types'; vi.mock('./openUI.api', () => { return { @@ -23,7 +23,6 @@ vi.hoisted(() => { describe('Plugin api', () => { const mockContext = { - addListener: vi.fn(), getFile: vi.fn(), getPage: vi.fn(), getSelected: vi.fn(), @@ -31,7 +30,7 @@ describe('Plugin api', () => { getTheme: vi.fn(() => 'dark'), }; - const api = createApi(mockContext, { + const api = createApi(mockContext as any, { name: 'test', code: '', permissions: ['page:read', 'file:read', 'selection:read'], @@ -168,11 +167,14 @@ describe('Plugin api', () => { }); describe.concurrent('permissions', () => { - const api = createApi({ - name: 'test', - code: '', - permissions: [], - }); + const api = createApi( + {} as any, + { + name: 'test', + code: '', + permissions: [], + } as any + ); it('on', () => { const callback = vi.fn(); @@ -192,15 +194,15 @@ describe('Plugin api', () => { it('get states', () => { expect(() => { - api.getFileState(); + api.getFile(); }).toThrow(); expect(() => { - api.getPageState(); + api.getPage(); }).toThrow(); expect(() => { - api.getSelection(); + api.getSelected(); }).toThrow(); }); }); @@ -223,7 +225,7 @@ describe('Plugin api', () => { name: 'test', id: '123', revn: 0, - } as FileState; + } as PenpotFile; mockContext.getFile.mockImplementation(() => exampleFile); diff --git a/libs/plugins-runtime/src/lib/models/manifest.model.ts b/libs/plugins-runtime/src/lib/models/manifest.model.ts index c9ad172..db676e1 100644 --- a/libs/plugins-runtime/src/lib/models/manifest.model.ts +++ b/libs/plugins-runtime/src/lib/models/manifest.model.ts @@ -1,5 +1,5 @@ -import z from 'zod'; -import { manifestSchema } from './manifest.schema'; +import { z } from 'zod'; +import { manifestSchema } from './manifest.schema.js'; export type Manifest = z.infer; export type Permissions = Manifest['permissions'][number]; diff --git a/libs/plugins-runtime/src/lib/models/open-ui-options.model.ts b/libs/plugins-runtime/src/lib/models/open-ui-options.model.ts index 72e1248..6c5fdf4 100644 --- a/libs/plugins-runtime/src/lib/models/open-ui-options.model.ts +++ b/libs/plugins-runtime/src/lib/models/open-ui-options.model.ts @@ -1,4 +1,4 @@ -import z from 'zod'; -import { openUISchema } from './open-ui-options.schema'; +import { z } from 'zod'; +import { openUISchema } from './open-ui-options.schema.js'; export type OpenUIOptions = z.infer; diff --git a/libs/plugins-runtime/src/lib/parse-manifest.ts b/libs/plugins-runtime/src/lib/parse-manifest.ts index db34dc9..6f1cc1f 100644 --- a/libs/plugins-runtime/src/lib/parse-manifest.ts +++ b/libs/plugins-runtime/src/lib/parse-manifest.ts @@ -1,6 +1,6 @@ -import { Manifest } from './models/manifest.model'; -import { manifestSchema } from './models/manifest.schema'; -import { PluginConfig } from './models/plugin-config.model'; +import { Manifest } from './models/manifest.model.js'; +import { manifestSchema } from './models/manifest.schema.js'; +import { PluginConfig } from './models/plugin-config.model.js'; export function loadManifest(url: string): Promise { return fetch(url) diff --git a/libs/plugins-runtime/vite.config.ts b/libs/plugins-runtime/vite.config.ts index 49e7cc3..dc7d423 100644 --- a/libs/plugins-runtime/vite.config.ts +++ b/libs/plugins-runtime/vite.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; import * as path from 'path'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import checker from 'vite-plugin-checker'; export default defineConfig({ root: __dirname, @@ -15,6 +16,11 @@ export default defineConfig({ tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), skipDiagnostics: true, }), + checker({ + typescript: { + buildMode: true, + }, + }), ], // Uncomment this if you are using workers. diff --git a/package-lock.json b/package-lock.json index 5d29208..fdc9869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ "typescript": "~5.2.2", "verdaccio": "^5.0.4", "vite": "^5.0.0", + "vite-plugin-checker": "^0.6.4", "vite-plugin-dts": "~2.3.0", "vitest": "1.2.2" } @@ -10773,6 +10774,18 @@ "node": ">=16" } }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -16608,17 +16621,58 @@ } }, "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, "engines": { - "node": ">=16.10" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -21248,6 +21302,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, "node_modules/tiny-lru": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.4.1.tgz", @@ -22168,6 +22228,78 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-checker": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz", + "integrity": "sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "ansi-escapes": "^4.3.0", + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "fast-glob": "^3.2.7", + "fs-extra": "^11.1.0", + "npm-run-path": "^4.0.1", + "semver": "^7.5.0", + "strip-ansi": "^6.0.0", + "tiny-invariant": "^1.1.0", + "vscode-languageclient": "^7.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.2" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "eslint": ">=7", + "meow": "^9.0.0", + "optionator": "^0.9.1", + "stylelint": ">=13", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": ">=1.3.9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/vite-plugin-dts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.3.0.tgz", @@ -22434,6 +22566,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true, + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", + "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4", + "semver": "^7.3.4", + "vscode-languageserver-protocol": "3.16.0" + }, + "engines": { + "vscode": "^1.52.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 7e98cf0..2f575e2 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "typescript": "~5.2.2", "verdaccio": "^5.0.4", "vite": "^5.0.0", + "vite-plugin-checker": "^0.6.4", "vite-plugin-dts": "~2.3.0", "vitest": "1.2.2" },