mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-04 13:50:13 -05:00
feat: plugin libraries
This commit is contained in:
parent
03c055eac6
commit
070bb62328
41 changed files with 2508 additions and 298 deletions
35
.eslintrc.base.json
Normal file
35
.eslintrc.base.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"ignorePatterns": ["**/*"],
|
||||||
|
"plugins": ["@nx"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {
|
||||||
|
"@nx/enforce-module-boundaries": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"enforceBuildableLibDependency": true,
|
||||||
|
"allow": [],
|
||||||
|
"depConstraints": [
|
||||||
|
{
|
||||||
|
"sourceTag": "*",
|
||||||
|
"onlyDependOnLibsWithTags": ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"extends": ["plugin:@nx/typescript"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"extends": ["plugin:@nx/javascript"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
|
||||||
"ignorePatterns": ["**/*"],
|
"ignorePatterns": ["**/*"],
|
||||||
"plugins": ["@nx"],
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
@ -14,15 +12,27 @@
|
||||||
"depConstraints": [
|
"depConstraints": [
|
||||||
{
|
{
|
||||||
"sourceTag": "type:plugin",
|
"sourceTag": "type:plugin",
|
||||||
"onlyDependOnLibsWithTags": ["type:util", "type:ui", "type:feature"]
|
"onlyDependOnLibsWithTags": [
|
||||||
|
"type:util",
|
||||||
|
"type:ui",
|
||||||
|
"type:feature"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sourceTag": "type:app",
|
"sourceTag": "type:app",
|
||||||
"onlyDependOnLibsWithTags": [ "type:util", "type:ui", "type:feature"]
|
"onlyDependOnLibsWithTags": [
|
||||||
|
"type:util",
|
||||||
|
"type:ui",
|
||||||
|
"type:feature"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sourceTag": "type:feature",
|
"sourceTag": "type:feature",
|
||||||
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
|
"onlyDependOnLibsWithTags": [
|
||||||
|
"type:feature",
|
||||||
|
"type:ui",
|
||||||
|
"type:util"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sourceTag": "type:ui",
|
"sourceTag": "type:ui",
|
||||||
|
@ -42,7 +52,6 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
"plugin:@nx/typescript",
|
|
||||||
"plugin:deprecation/recommended",
|
"plugin:deprecation/recommended",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
|
@ -68,12 +77,10 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
"plugin:@nx/typescript",
|
|
||||||
"plugin:deprecation/recommended",
|
"plugin:deprecation/recommended",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error"],
|
"@typescript-eslint/no-unused-vars": ["error"],
|
||||||
"no-multiple-empty-lines": [
|
"no-multiple-empty-lines": [
|
||||||
2,
|
2,
|
||||||
|
@ -99,8 +106,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.js", "*.jsx"],
|
"files": ["*.js", "*.jsx"],
|
||||||
"extends": ["plugin:@nx/javascript"],
|
|
||||||
"rules": {}
|
"rules": {}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"extends": ["./.eslintrc.base.json"]
|
||||||
}
|
}
|
||||||
|
|
25
.verdaccio/config.yml
Normal file
25
.verdaccio/config.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# a list of other known repositories we can talk to
|
||||||
|
uplinks:
|
||||||
|
npmjs:
|
||||||
|
url: https://registry.npmjs.org/
|
||||||
|
maxage: 60m
|
||||||
|
|
||||||
|
packages:
|
||||||
|
'**':
|
||||||
|
# give all users (including non-authenticated users) full access
|
||||||
|
# because it is a local registry
|
||||||
|
access: $all
|
||||||
|
publish: $all
|
||||||
|
unpublish: $all
|
||||||
|
|
||||||
|
# if package is not available locally, proxy requests to npm registry
|
||||||
|
proxy: npmjs
|
||||||
|
|
||||||
|
# log settings
|
||||||
|
logs:
|
||||||
|
type: stdout
|
||||||
|
format: pretty
|
||||||
|
level: warn
|
||||||
|
|
||||||
|
publish:
|
||||||
|
allow_offline: true # set offline to true to allow publish offline
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||||
"include": ["src/**/*.ts", "../../libs/plugins-runtime/src/lib/index.d.ts"]
|
"include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||||
"include": ["src/**/*.ts", "../../libs/plugins-runtime/src/lib/index.d.ts"]
|
"include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*"],
|
"ignorePatterns": ["!**/*"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -1,24 +1,32 @@
|
||||||
# Create plugin
|
# Creating a Plugin
|
||||||
|
|
||||||
To create the basic scaffolding run the following command. Remember to replace `example-plugin` with your own.
|
This guide walks you through the steps to create a plugin for our platform. You'll start by setting up the basic structure, configuring necessary files, and then running a local server to preview your plugin. Let's dive in.
|
||||||
|
|
||||||
```
|
### Step 1: Initialize the Plugin
|
||||||
|
|
||||||
|
First, you need to create the scaffolding for your plugin. Use the following command, replacing `example-plugin` with the name of your plugin:
|
||||||
|
|
||||||
|
```sh
|
||||||
npx nx g @nx/web:application example-plugin --directory=apps/example-plugin
|
npx nx g @nx/web:application example-plugin --directory=apps/example-plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a `manifest.json` in `/public`.
|
### Step 2: Configure the Manifest
|
||||||
|
|
||||||
|
Next, create a `manifest.json` file inside the `/public` directory. This file is crucial as it defines key properties of your plugin, including permissions and the entry point script.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Example plugin",
|
"name": "Example Plugin",
|
||||||
"code": "http://localhost:4201/plugin.js",
|
"code": "http://localhost:4201/plugin.js",
|
||||||
"permissions": ["page:read", "file:read", "selection:read"]
|
"permissions": ["page:read", "file:read", "selection:read"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add to the example `vite.config.ts`
|
### Step 3: Update Vite Configuration
|
||||||
|
|
||||||
```json
|
Now, add the following configuration to your `vite.config.ts` to specify the entry points for the build process:
|
||||||
|
|
||||||
|
```typescript
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
@ -32,24 +40,39 @@ build: {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add to `tsconfig.app.json`
|
### Step 4: Modify TypeScript Configuration
|
||||||
|
|
||||||
|
Update your `tsconfig.app.json` to include the necessary TypeScript files for your plugin:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"include": ["src/**/*.ts", "../../libs/plugins-runtime/src/lib/index.d.ts"]
|
{
|
||||||
|
"include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run the static server
|
### Step 5: Run a Static Server
|
||||||
|
|
||||||
```
|
To preview your plugin, start a static server by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
npx nx run example-plugin:build --watch & npx nx run example-plugin:preview
|
npx nx run example-plugin:build --watch & npx nx run example-plugin:preview
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, go to penpot and load the plugin. Run the command in the console devtools from your browser.
|
### Step 6: Load the Plugin in Penpot
|
||||||
|
|
||||||
```ts
|
Finally, to load your plugin into Penpot, execute the following command in the browser's console devtools:
|
||||||
|
|
||||||
|
```typescript
|
||||||
ɵloadPlugin({ manifest: 'http://localhost:4201/manifest.json' });
|
ɵloadPlugin({ manifest: 'http://localhost:4201/manifest.json' });
|
||||||
```
|
```
|
||||||
|
|
||||||
### More about plugin development
|
### Learn More About Plugin Development
|
||||||
|
|
||||||
Check the [plugin usage](docs/plugin-usage.md) and the [create API](docs/create-api.md) documentation.
|
For more detailed information on plugin development, check out our guides:
|
||||||
|
|
||||||
|
- [Plugin Usage Documentation](docs/plugin-usage.md)
|
||||||
|
- [Create API Documentation](docs/create-api.md)
|
||||||
|
|
||||||
|
### Using a Starter Template
|
||||||
|
|
||||||
|
If you prefer to kickstart your plugin development, consider using the [Penpot Plugin Starter Template](https://github.com/penpot/penpot-plugin-starter-template). It's a template designed to streamline the creation process for Penpot plugins.
|
||||||
|
|
85
docs/publish-package.md
Normal file
85
docs/publish-package.md
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# Publishing Packages
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This guide details the process of publishing `plugin-types` and `plugins-styles` packages, which are essential for plugin development. To facilitate testing and distribution, we leverage npm for publishing and Verdaccio for setting up a local registry. Below is a walkthrough for publishing these packages, setting up a local registry, and managing releases.
|
||||||
|
|
||||||
|
## Setting Up a Local Registry with Verdaccio
|
||||||
|
|
||||||
|
Setting up a local registry is for testing plugins in isolation from the monorepo. We utilize Verdaccio, a npm proxy registry, for this purpose.
|
||||||
|
|
||||||
|
**Launch the Registry**: Initiate the Verdaccio registry by executing the command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run registry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing Libraries
|
||||||
|
|
||||||
|
Publishing packages allows you to distribute your libraries to other developers and environments. Follow the steps below for both automated and manual publishing processes.
|
||||||
|
|
||||||
|
### Automated Publishing:
|
||||||
|
|
||||||
|
To publish the libraries automatically, use the command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run publish -- --version 0.1.0 --tag 0.1.0 --registry http://localhost:4873
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Publishing:
|
||||||
|
|
||||||
|
For manual publication, navigate to the library directory and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm publish --registry http://localhost:4873
|
||||||
|
```
|
||||||
|
|
||||||
|
### Independent Publishing:
|
||||||
|
|
||||||
|
To publish libraries independently, specify the package name along with version and tag:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx publish plugin-types -- --version 0.1.0 --tag 0.1.0
|
||||||
|
npx nx publish plugins-styles -- --version 0.1.0 --tag 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installing Libraries:
|
||||||
|
|
||||||
|
When installing the library, ensure to specify the registry:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i --registry http://localhost:4873
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: For direct npm publication, omit the `--registry` flag.
|
||||||
|
|
||||||
|
## Managing Releases
|
||||||
|
|
||||||
|
### Generating a Release:
|
||||||
|
|
||||||
|
For regular releases, execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx release
|
||||||
|
```
|
||||||
|
|
||||||
|
For the initial release, use the `--first-release` flag:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx release --first-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to the [Nx Release Documentation](https://nx.dev/recipes/nx-release/publish-in-ci-cd) for detailed information.
|
||||||
|
|
||||||
|
## Important Reminders
|
||||||
|
|
||||||
|
Ensure to update the [penpot-plugin-starter-template](https://github.com/penpot/penpot-plugin-starter-template) with every release to provide developers with the latest configuration and features.
|
||||||
|
|
||||||
|
## Relevant Files and Scripts
|
||||||
|
|
||||||
|
- **Verdaccio Configuration**: `./project.json`
|
||||||
|
- **CSS Build Script**: `./tools/scripts/build-css.mjs`
|
||||||
|
- **Types Build Script**: `./tools/scripts/build-types.mjs`
|
||||||
|
- **Publish Script**: `./tools/scripts/publish.mjs`
|
||||||
|
|
||||||
|
This improved documentation aims to streamline the package publishing process, making it more accessible and understandable for developers.
|
25
libs/plugin-types/.eslintrc.json
Normal file
25
libs/plugin-types/.eslintrc.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.base.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.json"],
|
||||||
|
"parser": "jsonc-eslint-parser",
|
||||||
|
"rules": {
|
||||||
|
"@nx/dependency-checks": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
21
libs/plugin-types/README.md
Normal file
21
libs/plugin-types/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Penpot plugin-types
|
||||||
|
|
||||||
|
This repository contains the typings for the Penpot Plugin API.
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
Install the package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @penpot/plugin-types
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure `tsconfig.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@penpot"
|
||||||
|
],
|
||||||
|
"types": ["plugin-types"],
|
||||||
|
```
|
|
@ -1,26 +1,24 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
export interface PageState {
|
||||||
|
|
||||||
interface Page {
|
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface File {
|
export interface FileState {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
revn: number;
|
revn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventsMap {
|
export interface EventsMap {
|
||||||
pagechange: Page;
|
pagechange: PageState;
|
||||||
filechange: File;
|
filechange: FileState;
|
||||||
selectionchange: string[];
|
selectionchange: string[];
|
||||||
themechange: Theme;
|
themechange: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Theme = 'light' | 'dark';
|
export type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
interface Penpot {
|
export interface Penpot {
|
||||||
ui: {
|
ui: {
|
||||||
open: (
|
open: (
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -30,7 +28,7 @@ interface Penpot {
|
||||||
sendMessage: (message: unknown) => void;
|
sendMessage: (message: unknown) => void;
|
||||||
onMessage: <T>(callback: (message: T) => void) => void;
|
onMessage: <T>(callback: (message: T) => void) => void;
|
||||||
};
|
};
|
||||||
log: (...data: any[]) => void;
|
log: (...data: unknown[]) => void;
|
||||||
setTimeout: (callback: () => void, time: number) => void;
|
setTimeout: (callback: () => void, time: number) => void;
|
||||||
closePlugin: () => void;
|
closePlugin: () => void;
|
||||||
on: <T extends keyof EventsMap>(
|
on: <T extends keyof EventsMap>(
|
||||||
|
@ -41,13 +39,13 @@ interface Penpot {
|
||||||
type: T,
|
type: T,
|
||||||
callback: (event: EventsMap[T]) => void
|
callback: (event: EventsMap[T]) => void
|
||||||
) => void;
|
) => void;
|
||||||
getFileState: () => File | null;
|
getFileState: () => FileState | null;
|
||||||
getPageState: () => Page | null;
|
getPageState: () => PageState | null;
|
||||||
getSelection: () => string[];
|
getSelection: () => string[];
|
||||||
getTheme: () => Theme;
|
getTheme: () => Theme;
|
||||||
fetch: typeof fetch;
|
fetch: typeof fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace globalThis {
|
declare global {
|
||||||
const penpot: Penpot;
|
const penpot: Penpot;
|
||||||
}
|
}
|
5
libs/plugin-types/package.json
Normal file
5
libs/plugin-types/package.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "@penpot/plugin-types",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"typings": "./index.d.ts"
|
||||||
|
}
|
19
libs/plugin-types/project.json
Normal file
19
libs/plugin-types/project.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "plugin-types",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/plugin-types",
|
||||||
|
"projectType": "library",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"command": "node tools/scripts/build-types.mjs",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/plugin-types"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"command": "node tools/scripts/publish.mjs plugin-types {args.ver} {args.tag}",
|
||||||
|
"dependsOn": ["build"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["type:ui"]
|
||||||
|
}
|
19
libs/plugin-types/tsconfig.json
Normal file
19
libs/plugin-types/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "node16",
|
||||||
|
"lib": [
|
||||||
|
"es6",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"types": [],
|
||||||
|
"noEmit": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "plugins-data-parser",
|
"name": "plugins-data-parser",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*"],
|
"ignorePatterns": ["!**/*"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "plugins-runtime",
|
"name": "plugins-runtime",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"happy-dom": "^13.6.2"
|
"happy-dom": "^13.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"plugins-data-parser": "^0.0.1",
|
"@penpot/plugin-types": "^0.1.0",
|
||||||
|
"plugins-data-parser": "^0.1.0",
|
||||||
"vitest": "1.2.2",
|
"vitest": "1.2.2",
|
||||||
"ses": "^1.1.0",
|
"ses": "^1.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { setModalTheme } from '../create-modal';
|
import { setModalTheme } from '../create-modal';
|
||||||
import { Manifest, Permissions } from '../models/manifest.model';
|
import { Manifest, Permissions } from '../models/manifest.model';
|
||||||
import { OpenUIOptions } from '../models/open-ui-options.model';
|
import { OpenUIOptions } from '../models/open-ui-options.model';
|
||||||
|
import type {
|
||||||
|
Penpot,
|
||||||
|
EventsMap,
|
||||||
|
FileState,
|
||||||
|
PageState,
|
||||||
|
Theme,
|
||||||
|
} from '@penpot/plugin-types';
|
||||||
import openUIApi from './openUI.api';
|
import openUIApi from './openUI.api';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
@ -17,8 +24,8 @@ export let uiMessagesCallbacks: Callback<unknown>[] = [];
|
||||||
|
|
||||||
let modal: HTMLElement | null = null;
|
let modal: HTMLElement | null = null;
|
||||||
|
|
||||||
let pageState: Page | null = null;
|
let pageState: PageState | null = null;
|
||||||
let fileState: File | null = null;
|
let fileState: FileState | null = null;
|
||||||
let selection: string[] = [];
|
let selection: string[] = [];
|
||||||
let themeState: Theme = 'dark';
|
let themeState: Theme = 'dark';
|
||||||
|
|
||||||
|
@ -38,13 +45,13 @@ export function triggerEvent(
|
||||||
listeners.forEach((listener) => listener(message));
|
listeners.forEach((listener) => listener(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPageState(page: Page) {
|
export function setPageState(page: PageState) {
|
||||||
pageState = page;
|
pageState = page;
|
||||||
|
|
||||||
triggerEvent('pagechange', page);
|
triggerEvent('pagechange', page);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFileState(file: File) {
|
export function setFileState(file: FileState) {
|
||||||
fileState = file;
|
fileState = file;
|
||||||
|
|
||||||
triggerEvent('filechange', file);
|
triggerEvent('filechange', file);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
uiMessagesCallbacks,
|
uiMessagesCallbacks,
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import openUIApi from './openUI.api.js';
|
import openUIApi from './openUI.api.js';
|
||||||
|
import { FileState } from '@penpot/plugin-types';
|
||||||
|
|
||||||
vi.mock('./openUI.api', () => {
|
vi.mock('./openUI.api', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -220,7 +221,7 @@ describe('Plugin api', () => {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
id: '123',
|
id: '123',
|
||||||
revn: 0,
|
revn: 0,
|
||||||
} as File;
|
} as FileState;
|
||||||
|
|
||||||
setFileState(exampleFile);
|
setFileState(exampleFile);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { OpenUIOptions } from './models/open-ui-options.model';
|
import { OpenUIOptions } from './models/open-ui-options.model';
|
||||||
|
|
||||||
|
import type { Theme } from '@penpot/plugin-types';
|
||||||
|
|
||||||
export function setModalTheme(modal: HTMLElement, theme: Theme) {
|
export function setModalTheme(modal: HTMLElement, theme: Theme) {
|
||||||
modal.setAttribute('data-theme', theme);
|
modal.setAttribute('data-theme', theme);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": ["../../.eslintrc.json"],
|
"extends": ["../../.eslintrc.base.json", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
"ignorePatterns": ["!**/*", "vite.config.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
# plugins-styles
|
# Penpot plugin-styles
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
This plugin contains a CSS file to help build the UI for Penpot plugins.
|
||||||
|
|
||||||
## Building
|
### Getting started
|
||||||
|
|
||||||
Run `nx build plugins-styles` to build the library.
|
Install the package:
|
||||||
|
|
||||||
## Running unit tests
|
```bash
|
||||||
|
npm install @penpot/plugin-styles
|
||||||
|
```
|
||||||
|
|
||||||
Run `nx test plugins-styles` to execute the unit tests via [Jest](https://jestjs.io).
|
```css
|
||||||
|
@import '@penpot/plugin-styles/styles.css';
|
||||||
|
```
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "plugins-styles",
|
"name": "@penpot/plugin-styles",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"dependencies": {},
|
"dependencies": {}
|
||||||
"main": "./index.js",
|
|
||||||
"module": "./index.mjs",
|
|
||||||
"typings": "./index.d.ts"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
"executor": "@nx/vite:build",
|
"command": "node tools/scripts/build-css.mjs",
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/plugins-styles"
|
"outputPath": "dist/plugins-styles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"publish": {
|
||||||
|
"command": "node tools/scripts/publish.mjs plugins-styles {args.ver} {args.tag}",
|
||||||
|
"dependsOn": ["build"]
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nx/eslint:lint",
|
"executor": "@nx/eslint:lint",
|
||||||
"outputs": ["{options.outputFile}"]
|
"outputs": ["{options.outputFile}"]
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function pluginsCssLib(): string {
|
|
||||||
return 'plugins-styles';
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
|
||||||
},
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.lib.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"declaration": true,
|
|
||||||
"types": ["node", "vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
|
||||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../dist/out-tsc",
|
|
||||||
"module": "commonjs",
|
|
||||||
"types": ["node"]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.test.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/// <reference types='vitest' />
|
|
||||||
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';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
root: __dirname,
|
|
||||||
cacheDir: '../node_modules/.vite/plugins-styles',
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
nxViteTsPaths(),
|
|
||||||
dts({
|
|
||||||
entryRoot: 'src',
|
|
||||||
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
|
|
||||||
skipDiagnostics: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Uncomment this if you are using workers.
|
|
||||||
// worker: {
|
|
||||||
// plugins: [ nxViteTsPaths() ],
|
|
||||||
// },
|
|
||||||
|
|
||||||
// Configuration for building your library.
|
|
||||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
|
||||||
build: {
|
|
||||||
outDir: '../dist/plugins-styles',
|
|
||||||
reportCompressedSize: true,
|
|
||||||
commonjsOptions: {
|
|
||||||
transformMixedEsModules: true,
|
|
||||||
},
|
|
||||||
lib: {
|
|
||||||
// Could also be a dictionary or array of multiple entry points.
|
|
||||||
entry: 'src/index.ts',
|
|
||||||
name: 'plugins-styles',
|
|
||||||
fileName: 'index',
|
|
||||||
// Change this to the formats you want to support.
|
|
||||||
// Don't forget to update your package.json as well.
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
// External packages that should not be bundled into your library.
|
|
||||||
external: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
2170
package-lock.json
generated
2170
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "penpot-plugins",
|
"name": "penpot-plugins",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx nx run plugins-runtime:build --watch & npx nx run plugins-runtime:preview",
|
"start": "npx nx run plugins-runtime:build --watch & npx nx run plugins-runtime:preview",
|
||||||
|
@ -11,6 +11,8 @@
|
||||||
"lint": "nx run-many --all --target=lint --parallel",
|
"lint": "nx run-many --all --target=lint --parallel",
|
||||||
"lint:affected": "npx nx affected --target=lint",
|
"lint:affected": "npx nx affected --target=lint",
|
||||||
"test": "npx nx test plugins-runtime",
|
"test": "npx nx test plugins-runtime",
|
||||||
|
"publish": "nx run-many -t publish -p plugins-styles plugin-types --parallel=false --",
|
||||||
|
"registry": "nx local-registry",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
"eslint": "~8.48.0",
|
"eslint": "~8.48.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-deprecation": "^2.0.0",
|
"eslint-plugin-deprecation": "^2.0.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
"happy-dom": "^13.6.2",
|
"happy-dom": "^13.6.2",
|
||||||
"husky": "^9.0.10",
|
"husky": "^9.0.10",
|
||||||
"jsdom": "~22.1.0",
|
"jsdom": "~22.1.0",
|
||||||
|
@ -45,6 +48,7 @@
|
||||||
"swc-loader": "0.1.15",
|
"swc-loader": "0.1.15",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
|
"verdaccio": "^5.0.4",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-dts": "~2.3.0",
|
"vite-plugin-dts": "~2.3.0",
|
||||||
"vitest": "1.2.2"
|
"vitest": "1.2.2"
|
||||||
|
@ -63,5 +67,8 @@
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"includedScripts": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
project.json
Normal file
15
project.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "penpot-plugins",
|
||||||
|
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||||
|
"targets": {
|
||||||
|
"local-registry": {
|
||||||
|
"executor": "@nx/js:verdaccio",
|
||||||
|
"options": {
|
||||||
|
"port": 4873,
|
||||||
|
"config": ".verdaccio/config.yml",
|
||||||
|
"storage": "tmp/local-registry/storage",
|
||||||
|
"clear": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
tools/scripts/build-css.mjs
Normal file
23
tools/scripts/build-css.mjs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import esbuild from 'esbuild';
|
||||||
|
import { copy } from 'fs-extra';
|
||||||
|
|
||||||
|
const source = 'libs/plugins-styles';
|
||||||
|
const dist = 'dist/plugins-styles';
|
||||||
|
|
||||||
|
const handleErr = (err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: [`${source}/src/lib/styles.css`],
|
||||||
|
bundle: true,
|
||||||
|
outfile: `${dist}/styles.css`,
|
||||||
|
minify: true,
|
||||||
|
loader: {
|
||||||
|
'.svg': 'text'
|
||||||
|
}
|
||||||
|
}).catch(handleErr);
|
||||||
|
|
||||||
|
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
||||||
|
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
13
tools/scripts/build-types.mjs
Normal file
13
tools/scripts/build-types.mjs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { copy } from 'fs-extra';
|
||||||
|
|
||||||
|
const source = 'libs/plugin-types';
|
||||||
|
const dist = 'dist/plugin-types';
|
||||||
|
|
||||||
|
const handleErr = (err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
||||||
|
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
||||||
|
copy(`${source}/index.d.ts`, `${dist}/index.d.ts`).catch(handleErr);
|
65
tools/scripts/publish.mjs
Normal file
65
tools/scripts/publish.mjs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* This is a minimal script to publish your package to "npm".
|
||||||
|
* This is meant to be used as-is or customize as you see fit.
|
||||||
|
*
|
||||||
|
* This script is executed on "dist/path/to/library" as "cwd" by default.
|
||||||
|
*
|
||||||
|
* You might need to authenticate with NPM before running this script.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
import devkit from '@nx/devkit';
|
||||||
|
const { readCachedProjectGraph } = devkit;
|
||||||
|
|
||||||
|
function invariant(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag}
|
||||||
|
// Default "tag" to "next" so we won't publish the "latest" tag by accident.
|
||||||
|
const [, , name, version, tag = 'next', registry] = process.argv;
|
||||||
|
|
||||||
|
// A simple SemVer validation to validate the version
|
||||||
|
const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/;
|
||||||
|
invariant(
|
||||||
|
version && validVersion.test(version),
|
||||||
|
`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const graph = readCachedProjectGraph();
|
||||||
|
const project = graph.nodes[name];
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
project,
|
||||||
|
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputPath = project.data?.targets?.build?.options?.outputPath;
|
||||||
|
invariant(
|
||||||
|
outputPath,
|
||||||
|
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.chdir(outputPath);
|
||||||
|
|
||||||
|
// Updating the version in "package.json" before publishing
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(readFileSync(`package.json`).toString());
|
||||||
|
json.version = version;
|
||||||
|
writeFileSync(`package.json`, JSON.stringify(json, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error reading package.json file from library build output.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute "npm publish" to publish
|
||||||
|
let command = `npm publish --access public --tag ${tag}`;
|
||||||
|
if (registry) {
|
||||||
|
command += ` --registry ${registry}`;
|
||||||
|
}
|
||||||
|
execSync(command);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@penpot/plugin-types": ["libs/plugin-types/index.d.ts"],
|
||||||
"plugins-parser": ["libs/plugins-data-parser/src/index.ts"],
|
"plugins-parser": ["libs/plugins-data-parser/src/index.ts"],
|
||||||
"plugins-runtime": ["libs/plugins-runtime/src/index.ts"],
|
"plugins-runtime": ["libs/plugins-runtime/src/index.ts"],
|
||||||
"plugins-styles/*": ["libs/plugins-styles/src/*"]
|
"plugins-styles/*": ["libs/plugins-styles/src/*"]
|
||||||
|
|
Loading…
Reference in a new issue