0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-21 21:23:06 -05:00

Update packages and add linters (#1)

* Configure linters

* Move to older eslint config, add stylelint, correct configuration for editorconfig

* simplify packages

* improve eslint config

* Fix typescript

* remove line

* github workflow

* package-lock.json

* lint fix

* make plugin compatible with Figma

* Add stylelint to ci

---------

Co-authored-by: Alex Sánchez <alejandro@runroom.com>
This commit is contained in:
Jordi Sala Morales 2024-04-08 11:43:30 +02:00 committed by GitHub
parent 7dc730994f
commit 308fa93625
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 9757 additions and 1850 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
indent_style = space
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
src/penpot.js

22
.eslintrc.cjs Normal file
View file

@ -0,0 +1,22 @@
module.exports = {
root: true,
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 'latest',
sourceType: 'module'
},
env: { browser: true, node: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:@figma/figma-plugins/recommended'
],
settings: {
react: {
version: 'detect'
}
}
};

34
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,34 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
name: Build ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
matrix:
node: ['20.x']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- run: npm run build
lint:
name: Lint ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
matrix:
node: ['20.x']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- run: npm run lint

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
node_modules
package-lock.json
dist

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
src/penpot.js
LICENSE

View file

@ -1,39 +1,40 @@
# Developer guide
The plugin relies in [penpot.js](https://github.com/penpot/penpot-exporter-figma-plugin/blob/main/src/penpot.js)
library. It contains a subset of Penpot frontend app, transpiled into
javascript to be used from js code (this was easy since the frontend is written
in ClojureScript, that has direct translation to javascript).
The plugin relies in
[penpot.js](https://github.com/penpot/penpot-exporter-figma-plugin/blob/main/src/penpot.js) library.
It contains a subset of Penpot frontend app, transpiled into javascript to be used from js code
(this was easy since the frontend is written in ClojureScript, that has direct translation to
javascript).
Basically, it exports the `createFile` function and the `File` data type, that
represents a Penpot file as it resides in memory inside the frontend app. It
has function to create pages and their content, and also an `export` function
that generates and downloads a .zip archive with the Penpot file as svg
documents in Penpot annotated format, that you can import directly into Penpot.
Basically, it exports the `createFile` function and the `File` data type, that represents a Penpot
file as it resides in memory inside the frontend app. It has function to create pages and their
content, and also an `export` function that generates and downloads a .zip archive with the Penpot
file as svg documents in Penpot annotated format, that you can import directly into Penpot.
You can see the [source of the library at Penpot repo](https://github.com/penpot/penpot/tree/develop/frontend/src/app/libs).
You can see the
[source of the library at Penpot repo](https://github.com/penpot/penpot/tree/develop/frontend/src/app/libs).
To see a general description of the data types used in the functions you can
see [the data model](https://help.penpot.app/technical-guide/data-model/).
Their full specifications are in the [common types module](https://github.com/penpot/penpot/tree/develop/common/src/app/common/types).
To see a general description of the data types used in the functions you can see
[the data model](https://help.penpot.app/technical-guide/data-model/). Their full specifications are
in the
[common types module](https://github.com/penpot/penpot/tree/develop/common/src/app/common/types).
Those types are defined in [Clojure spec](https://clojure.org/guides/spec) format.
For those unfamiliar with the syntax, here is a small basic guide:
Those types are defined in [Clojure spec](https://clojure.org/guides/spec) format. For those
unfamiliar with the syntax, here is a small basic guide:
```clojure
(s/def ::name string?)
(s/def ::id uuid?)
```
A parameter or attribute called `name` is of type string, and `id` is an *uuid*
string (e.g. "000498f3-27fc-8000-8988-ca7d52f46843").
A parameter or attribute called `name` is of type string, and `id` is an _uuid_ string (e.g.
"000498f3-27fc-8000-8988-ca7d52f46843").
```clojure
(s/def ::stroke-alignment #{:center :inner :outer})
```
`stroke-alignment` is of an enumerated type, and the valid values are "center",
"inner" and "outer".
`stroke-alignment` is of an enumerated type, and the valid values are "center", "inner" and "outer".
```clojure
(ns app.common.types.shape
@ -44,17 +45,17 @@ string (e.g. "000498f3-27fc-8000-8988-ca7d52f46843").
(s/def ::line-height ::us/safe-number)
```
`line-height` is of the type `safe-number`, defined in `app.common.spec`
namespace, that here is imported with the name `us` (a *safe number* is a
integer or floating point number, with a value not too big or small).
`line-height` is of the type `safe-number`, defined in `app.common.spec` namespace, that here is
imported with the name `us` (a _safe number_ is a integer or floating point number, with a value not
too big or small).
```clojure
(s/def ::page
(s/keys :req-un [::id ::name ::objects ::options]))
```
`page` is an object with four required arguments: `id`, `name`, `objects` and
`options`, whose types have to be defined above.
`page` is an object with four required arguments: `id`, `name`, `objects` and `options`, whose types
have to be defined above.
```clojure
(s/def ::column
@ -66,8 +67,8 @@ integer or floating point number, with a value not too big or small).
::gutter]))
```
`column` has one required attribute `color` and five optional ones `size`,
`type`, `item-length`, `margin` and `gutter`.
`column` has one required attribute `color` and five optional ones `size`, `type`, `item-length`,
`margin` and `gutter`.
```clojure
(s/def ::children
@ -76,8 +77,8 @@ integer or floating point number, with a value not too big or small).
:min-count 1))
```
`children` is a collection (implemented as a vector) of objects of type
`content`, and must have a minimun lenght of 1.
`children` is a collection (implemented as a vector) of objects of type `content`, and must have a
minimun lenght of 1.
```clojure
(defmulti animation-spec :animation-type)
@ -102,11 +103,9 @@ integer or floating point number, with a value not too big or small).
(s/multi-spec animation-spec ::animation-type))
```
This is probably the most complex construct. `animation` is a multi schema
object. It has an attribute called `animation-type` and the rest of the fields
depend on its value. For example, if `animation-type` is "dissolve", the object
must also have a `duration` and an `easing` attribute.
Other constructs should be more or less auto explicative with this guide
and the clojure.spec manual linked above.
This is probably the most complex construct. `animation` is a multi schema object. It has an
attribute called `animation-type` and the rest of the fields depend on its value. For example, if
`animation-type` is "dissolve", the object must also have a `duration` and an `easing` attribute.
Other constructs should be more or less auto explicative with this guide and the clojure.spec manual
linked above.

121
README.md
View file

@ -1,4 +1,3 @@
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
@ -25,9 +24,10 @@
![](penpotexporter.gif)
This is a **very early-stage** Figma plugin to export Figma files to Penpot format. For now is little more than a proof of concept, or a first scaffolding, not a fully functional exporter.
This is a **very early-stage** Figma plugin to export Figma files to Penpot format. For now is
little more than a proof of concept, or a first scaffolding, not a fully functional exporter.
## Table of contents ##
## Table of contents
- [Table of contents](#table-of-contents)
- [Why a Penpot exporter](#why-a-penpot-exporter)
@ -42,70 +42,97 @@ This is a **very early-stage** Figma plugin to export Figma files to Penpot form
- [Contributing](#contributing)
- [License](#license)
## Why a Penpot exporter ##
## Why a Penpot exporter
The aim of this plugin is to help people migrate their files from Figma to [Penpot](https://penpot.app/). Migrating work from one design tool to another was never an easy task due to the abundance of closed and non-standard formats, and this is not a different case. Our approach to better solve this situation is to release a code skeleton for the minimum version of a Figma plugin that can convert a Figma file into a Penpot annotated SVG file.
The aim of this plugin is to help people migrate their files from Figma to
[Penpot](https://penpot.app/). Migrating work from one design tool to another was never an easy task
due to the abundance of closed and non-standard formats, and this is not a different case. Our
approach to better solve this situation is to release a code skeleton for the minimum version of a
Figma plugin that can convert a Figma file into a Penpot annotated SVG file.
There is a sense of urgency for this capability because there is a feeling that Adobe might force Figma to limit exports and interoperability via plugins very soon.
There is a sense of urgency for this capability because there is a feeling that Adobe might force
Figma to limit exports and interoperability via plugins very soon.
## Getting started
## Getting started ##
This plugin makes use of npm, webpack and react, and is written on TypeScript. It also includes a Penpot file builder library.
This plugin makes use of npm, webpack and react, and is written on TypeScript. It also includes a
Penpot file builder library.
### Pre-requisites
To use this plugin, you will need to have `node` and `npm` installed on your computer. If you don't already have these, you can download and install them from the official website ([https://nodejs.org/en/](https://nodejs.org/en/)).
To use this plugin, you will need to have `node` and `npm` installed on your computer. If you don't
already have these, you can download and install them from the official website
([https://nodejs.org/en/](https://nodejs.org/en/)).
Once you have `node` and `npm` installed, you will need to download the source code for this plugin.
You can do this by clicking the "Clone or download" button on the GitHub page for this project and
then selecting "Download ZIP". Extract the ZIP file to a location on your computer.
Once you have `node` and `npm` installed, you will need to download the source code for this plugin. You can do this by clicking the "Clone or download" button on the GitHub page for this project and then selecting "Download ZIP". Extract the ZIP file to a location on your computer.
### Building
#### For Windows users:
1. Open the terminal by searching for "Command Prompt" in the start menu.
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
example, if the repository is located in the `Downloads` folder, you can use the following
command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the
dependencies, and then the `npm run build` command to build the plugin.
#### For Mac users:
1. Open the terminal by searching for "Terminal" in the Launchpad or by using the `Command + Space` keyboard shortcut and searching for "Terminal".
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
1. Open the terminal by searching for "Terminal" in the Launchpad or by using the `Command + Space`
keyboard shortcut and searching for "Terminal".
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
example, if the repository is located in the `Downloads` folder, you can use the following
command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the
dependencies, and then the `npm run build` command to build the plugin.
#### For Linux users:
1. Open the terminal by using the `Ctrl + Alt + T` keyboard shortcut.
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
example, if the repository is located in the `Downloads` folder, you can use the following
command: `cd Downloads/penpot-exporter-figma-plugin`.
3. Once you are in the correct folder, you can run the `npm install` command to install the
dependencies, and then the `npm run build` command to build the plugin.
### Add to Figma
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…`
To add the plugin to Figma, open Figma and go to the `Plugins` menu. Select `Development` and then choose `Import plugin from manifest…`.
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…` To add the plugin to
Figma, open Figma and go to the `Plugins` menu. Select `Development` and then choose
`Import plugin from manifest…`.
<img src="resources/Import plugin from manifest.png" alt='Screenshot of the Plugins > Development menus open showing the, "Import plugin from manifest" option.'>
Select the `manifest.json` file that is located in the folder where you extracted the source code for the plugin.
Select the `manifest.json` file that is located in the folder where you extracted the source code
for the plugin.
### To use the plugin
1. Select what you want to export
2. `Figma menu` > `Plugins` > `Development` > `Penpot Exporter`
go to the `Plugins` menu in Figma and select `Development` followed by `Penpot Exporter`.
2. `Figma menu` > `Plugins` > `Development` > `Penpot Exporter` go to the `Plugins` menu in Figma
and select `Development` followed by `Penpot Exporter`.
3. This will generate a .zip file that you can import into Penpot.
## Call to the community
## Call to the community ##
Answering to the interest expressed by community members to build the plugin by themselves, at the Penpot team we decided to help solve the need without having to depend on our current product priorities. That is why we have published this bare minimum version of the plugin, unsatisfactory in itself, but it unlocks the possibility for others to continue the task.
Answering to the interest expressed by community members to build the plugin by themselves, at the
Penpot team we decided to help solve the need without having to depend on our current product
priorities. That is why we have published this bare minimum version of the plugin, unsatisfactory in
itself, but it unlocks the possibility for others to continue the task.
Yes, we are asking for help. 🤗
We have explained this approach in a [community post](https://community.penpot.app/t/figma-file-importer/1684). Feel free to join the conversation there.
We have explained this approach in a
[community post](https://community.penpot.app/t/figma-file-importer/1684). Feel free to join the
conversation there.
## What can this plugin currently import? ##
## What can this plugin currently import?
As mentioned above, this plugin gets you to a starting point. Things that are currently included in the import are:
As mentioned above, this plugin gets you to a starting point. Things that are currently included in
the import are:
- **Basic shapes** (rectangles, ellipses).
- **Frames** (Boards in Penpot).
@ -114,20 +141,27 @@ As mentioned above, this plugin gets you to a starting point. Things that are cu
- **Texts** (basic support. Only fonts available on Google fonts).
- **Images** (basic support)
## Limitations ##
The obvious limitations are the features that are in Figma but not in Penpot or work differently in both tools so they can not be easily converted. We leave some comments below about the ones that are commonly considered more important:
## Limitations
- **Autolayout**: Not in Penpot yet but in a very advanced state of development. There will be news soon.
- **Components**: Currently very different from their counterparts at Figma. However, Penpot components are under a rework that we expect will make the conversion easier.
- **Variants**: Not expected in the short term. Also, we are thinking of different solutions to solve component states, things that eventually could make it difficult to import.
The obvious limitations are the features that are in Figma but not in Penpot or work differently in
both tools so they can not be easily converted. We leave some comments below about the ones that are
commonly considered more important:
- **Autolayout**: Not in Penpot yet but in a very advanced state of development. There will be news
soon.
- **Components**: Currently very different from their counterparts at Figma. However, Penpot
components are under a rework that we expect will make the conversion easier.
- **Variants**: Not expected in the short term. Also, we are thinking of different solutions to
solve component states, things that eventually could make it difficult to import.
## Contributing
## Contributing ##
If you want to make many people very happy and help us build this code skeleton for the minimum version of the Figma plugin, a further effort will be needed to have a satisfactory import experience.
If you want to make many people very happy and help us build this code skeleton for the minimum
version of the Figma plugin, a further effort will be needed to have a satisfactory import
experience.
For instance, it will be interesting to add:
- Strokes
- Fills with radial gradients
- Paths
@ -137,10 +171,11 @@ For instance, it will be interesting to add:
- Constraints
- ...
Motivated to contribute? Take a look at our [Contributing Guide](https://help.penpot.app/contributing-guide/) that explains our guidelines (they're for the Penpot Core, but are mostly of application here too).
Motivated to contribute? Take a look at our
[Contributing Guide](https://help.penpot.app/contributing-guide/) that explains our guidelines
(they're for the Penpot Core, but are mostly of application here too).
## License ##
## License
```
This Source Code Form is subject to the terms of the Mozilla Public
@ -149,4 +184,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC
```
Penpot and the Penpot exporter plugin are Kaleidos [open source projects](https://kaleidos.net/products)
Penpot and the Penpot exporter plugin are Kaleidos
[open source projects](https://kaleidos.net/products)

7654
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,52 @@
{
"name": "penpot-exporter",
"id": "1161652283781700708",
"version": "0.0.1",
"description": "Penpot exporter",
"type": "module",
"main": "code.js",
"scripts": {
"build": "webpack",
"watch": "webpack watch"
"watch": "webpack watch",
"lint": "run-p lint:*",
"lint:eslint": "eslint .",
"lint:stylelint": "stylelint src/**.css",
"lint:prettier": "prettier --check .",
"lint:tsc": "tsc --noEmit --pretty false",
"fix-lint": "run-p fix-lint:*",
"fix-lint:eslint": "eslint . --fix",
"fix-lint:stylelint": "stylelint src/**.css --fix",
"fix-lint:prettier": "prettier --write ."
},
"author": "Kaleidos",
"license": "MPL2.0",
"devDependencies": {
"@figma/plugin-typings": "*",
"@types/node": "^16.11.64",
"css-loader": "^6.2.0",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^5.3.2",
"style-loader": "^3.2.1",
"ts-loader": "^9.2.5",
"typescript": "^4.3.5",
"url-loader": "^4.1.1",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0"
},
"dependencies": {
"@figma-plugin/helpers": "^0.15.2",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"crypto": "^1.0.1",
"crypto-browserify": "^3.12.0",
"react": "^17.0.2",
"react-dev-utils": "^12.0.1",
"react-dom": "^17.0.2",
"slugify": "^1.6.5"
"react": "^18.2",
"react-dom": "^18.2",
"slugify": "^1.6"
},
"devDependencies": {
"@figma/eslint-plugin-figma-plugins": "^0.15",
"@figma/plugin-typings": "^1.88",
"@trivago/prettier-plugin-sort-imports": "^4.3",
"@types/react": "^18.2",
"@types/react-dom": "^18.2",
"css-loader": "^6.10",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1",
"eslint-plugin-prettier": "^5.1",
"eslint-plugin-react": "^7.34",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^5.6",
"npm-run-all2": "^6.1",
"prettier": "^3.2",
"react-dev-utils": "^12.0",
"style-loader": "^3.3",
"stylelint": "^16.3",
"stylelint-config-standard": "^36.0",
"ts-loader": "^9.5",
"typescript": "^5.4",
"typescript-eslint": "^7.4",
"webpack": "^5.91",
"webpack-cli": "^5.1"
}
}

19
prettier.config.mjs Normal file
View file

@ -0,0 +1,19 @@
/** @type {import("prettier").Config} */
const config = {
arrowParens: 'avoid',
bracketSpacing: true,
printWidth: 100,
proseWrap: 'always',
quoteProps: 'consistent',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'none',
useTabs: false,
plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrder: ['^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true
};
export default config;

View file

@ -1,80 +1,83 @@
import { NodeData, TextData } from './interfaces';
let fileData = [];
type NodeData = {
id: string
name: string,
type: string,
children: Node[],
x: number,
y: number,
width: number,
height: number,
fills: any
interface Signatures {
[key: string]: string;
}
const signatures = {
R0lGODdh: "image/gif",
R0lGODlh: "image/gif",
iVBORw0KGgo: "image/png",
"/9j/": "image/jpg"
const signatures: Signatures = {
'R0lGODdh': 'image/gif',
'R0lGODlh': 'image/gif',
'iVBORw0KGgo': 'image/png',
'/9j/': 'image/jpg'
};
function detectMimeType(b64) {
for (var s in signatures) {
function detectMimeType(b64: string) {
for (const s in signatures) {
if (b64.indexOf(s) === 0) {
return signatures[s];
}
}
}
function traverse(node): NodeData {
let children:any[] = [];
function traverse(node: BaseNode): NodeData | TextData {
const children: (NodeData | TextData)[] = [];
if ("children" in node) {
if (node.type !== "INSTANCE") {
if ('children' in node) {
if (node.type !== 'INSTANCE') {
for (const child of node.children) {
children.push (traverse(child))
children.push(traverse(child));
}
}
}
let result = {
const result = {
id: node.id,
type: node.type,
name: node.name,
children: children,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
fills: node.fills === figma.mixed ? [] : node.fills //TODO: Support mixed fills
}
if (node.fills && Array.isArray(node.fills)){
x: 'x' in node ? node.x : 0,
y: 'y' in node ? node.y : 0,
width: 'width' in node ? node.width : 0,
height: 'height' in node ? node.height : 0,
fills: 'fills' in node ? (node.fills === figma.mixed ? [] : node.fills) : [] // TODO: Support mixed fills
};
if (result.fills) {
// Find any fill of type image
const imageFill = node.fills.find(fill => fill.type === "IMAGE");
const imageFill = result.fills.find(fill => fill.type === 'IMAGE');
if (imageFill) {
// An "image" in Figma is a shape with one or more image fills, potentially blended with other fill
// types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class
// objects, we're going to simplify this by exporting this shape as a PNG image.
node.exportAsync({format: "PNG"}).then((value) => {
'exportAsync' in node &&
node.exportAsync({ format: 'PNG' }).then(value => {
const b64 = figma.base64Encode(value);
figma.ui.postMessage({type: "IMAGE", data: {
figma.ui.postMessage({
type: 'IMAGE',
data: {
id: node.id,
value: "data:" + detectMimeType(b64) + ";base64," + b64
}});
value: 'data:' + detectMimeType(b64) + ';base64,' + b64
}
});
});
}
}
if (node.type == "TEXT") {
const styledTextSegments = node.getStyledTextSegments(["fontName", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textCase", "textDecoration", "fills"]);
if (node.type == 'TEXT') {
const styledTextSegments = node.getStyledTextSegments([
'fontName',
'fontSize',
'fontWeight',
'lineHeight',
'letterSpacing',
'textCase',
'textDecoration',
'fills'
]);
if (styledTextSegments[0]) {
let font = {
const font = {
...result,
fontName: styledTextSegments[0].fontName,
fontSize: styledTextSegments[0].fontSize.toString(),
fontWeight: styledTextSegments[0].fontWeight.toString(),
@ -88,25 +91,25 @@ function traverse(node): NodeData {
textAlignVertical: node.textAlignVertical,
children: styledTextSegments
};
result = {...result, ...font};
return font as TextData;
}
}
return result;
return result as NodeData;
}
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
let root: NodeData = traverse(figma.root) // start the traversal at the root
figma.ui.postMessage({type: "FIGMAFILE", data: root});
const root: NodeData | TextData = traverse(figma.root); // start the traversal at the root
figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
figma.ui.onmessage = (msg) => {
if (msg.type === "cancel") {
figma.ui.onmessage = msg => {
if (msg.type === 'cancel') {
figma.closePlugin();
}
if (msg.type === "resize") {
if (msg.type === 'resize') {
figma.ui.resize(msg.width, msg.height);
}
};

43
src/interfaces.ts Normal file
View file

@ -0,0 +1,43 @@
export type NodeData = {
id: string;
name: string;
type: string;
children: NodeData[];
x: number;
y: number;
width: number;
height: number;
fills: readonly Paint[];
};
export type TextDataChildren = Pick<
StyledTextSegment,
| 'fills'
| 'characters'
| 'start'
| 'end'
| 'fontSize'
| 'fontName'
| 'fontWeight'
| 'textDecoration'
| 'textCase'
| 'lineHeight'
| 'letterSpacing'
>;
export type TextData = Pick<
NodeData,
'id' | 'name' | 'type' | 'x' | 'y' | 'width' | 'height' | 'fills'
> & {
fontName: FontName;
fontSize: string;
fontWeight: string;
characters: string;
lineHeight: LineHeight;
letterSpacing: LetterSpacing;
textCase: TextCase;
textDecoration: TextDecoration;
textAlignHorizontal: 'CENTER' | 'LEFT' | 'RIGHT' | 'JUSTIFIED';
textAlignVertical: 'CENTER' | 'TOP' | 'BOTTOM';
children: TextDataChildren[];
};

18
src/penpot.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface PenpotFile {
asMap(): any;
export(): void;
addPage(name: string): void;
closePage(): void;
addArtboard(artboard: any): void;
closeArtboard(): void;
addGroup(group: any): void;
closeGroup(): void;
createRect(rect: any): void;
createCircle(circle: any): void;
createText(text: any): void;
createImage(image: any): void;
}
export function createFile(name: string): PenpotFile;

View file

@ -23,8 +23,8 @@ main {
body,
input,
button {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 1rem;
text-align: center;
}
@ -43,16 +43,20 @@ button {
border: 1px solid var(--color-border);
padding: 0.5rem 1rem;
}
button:hover {
background-color: var(--color-bg-hover);
}
button:active {
background-color: var(--color-bg-active);
}
button:focus-visible {
border: none;
outline-color: var(--color-border-focus);
}
button.brand {
--color-bg: var(--color-bg-brand);
--color-text: var(--color-text-brand);
@ -75,7 +79,7 @@ input:focus-visible {
}
svg {
stroke: var(--color-icon, rgba(0, 0, 0, 0.9));
stroke: var(--color-icon, rgb(0 0 0 / 90%));
}
main {
@ -96,6 +100,7 @@ section {
section > * + * {
margin-top: 0.5rem;
}
footer > * + * {
margin-left: 0.5rem;
}

View file

@ -1,249 +1,256 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import "./ui.css";
import * as penpot from "./penpot.js";
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import slugify from 'slugify';
import { extractLinearGradientParamsFromTransform } from "@figma-plugin/helpers";
import slugify from "slugify";
import { NodeData, TextData, TextDataChildren } from './interfaces';
import { PenpotFile, createFile } from './penpot';
import './ui.css';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare function require(path: string): any;
// Open resources/gfonts.json and create a set of matched font names
const gfonts = new Set();
require("./gfonts.json").forEach((font) => gfonts.add(font));
type PenpotExporterProps = {
}
require('./gfonts.json').forEach((font: string) => gfonts.add(font));
type FigmaImageData = {
value: string,
width: number,
height: number
}
value: string;
width: number;
height: number;
};
type PenpotExporterState = {
isDebug: boolean,
penpotFileData: string
missingFonts: Set<string>
figmaFileData: string
figmaRootNode: NodeData
images: { [id: string] : FigmaImageData; };
}
isDebug: boolean;
penpotFileData: string;
missingFonts: Set<string>;
figmaFileData: string;
figmaRootNode: NodeData | null;
images: { [id: string]: FigmaImageData };
};
export default class PenpotExporter extends React.Component<PenpotExporterProps, PenpotExporterState> {
export default class PenpotExporter extends React.Component<unknown, PenpotExporterState> {
state: PenpotExporterState = {
isDebug: false,
penpotFileData: "",
figmaFileData: "",
penpotFileData: '',
figmaFileData: '',
missingFonts: new Set(),
figmaRootNode: null,
images: {}
};
componentDidMount = () => {
window.addEventListener("message", this.onMessage);
}
window.addEventListener('message', this.onMessage);
};
componentDidUpdate = () => {
this.setDimensions();
}
};
componentWillUnmount = () =>{
componentWillUnmount = () => {
window.removeEventListener('message', this.onMessage);
}
};
rgbToHex = (color) => {
rgbToHex = (color: RGB) => {
const r = Math.round(255 * color.r);
const g = Math.round(255 * color.g);
const b = Math.round(255 * color.b);
const rgb = (r << 16) | (g << 8) | (b << 0);
return '#' + (0x1000000 + rgb).toString(16).slice(1);
}
};
translateSolidFill(fill){
translateSolidFill(fill: SolidPaint) {
return {
fillColor: this.rgbToHex(fill.color),
fillOpacity: fill.opacity
}
fillOpacity: fill.visible === false ? 0 : fill.opacity
};
}
translateGradientLinearFill(fill, width, height){
const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
translateGradientLinearFill(fill: GradientPaint /*, width: number, height: number*/) {
// const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
return {
fillColorGradient: {
type: Symbol.for("linear"),
type: Symbol.for('linear'),
width: 1,
startX: points.start[0] / width,
startY: points.start[1] / height,
endX: points.end[0] / width,
endY: points.end[1] / height,
stops: [{
// startX: points.start[0] / width,
// startY: points.start[1] / height,
// endX: points.end[0] / width,
// endY: points.end[1] / height,
stops: [
{
color: this.rgbToHex(fill.gradientStops[0].color),
offset: fill.gradientStops[0].position,
opacity: fill.gradientStops[0].color.a * fill.opacity
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1)
},
{
color: this.rgbToHex(fill.gradientStops[1].color),
offset: fill.gradientStops[1].position,
opacity: fill.gradientStops[1].color.a * fill.opacity
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
}
]
}
}
},
fillOpacity: fill.visible === false ? 0 : undefined
};
}
translateFill(fill, width, height){
if (fill.type === "SOLID"){
translateFill(fill: Paint /*, width: number, height: number*/) {
if (fill.type === 'SOLID') {
return this.translateSolidFill(fill);
} else if (fill.type === "GRADIENT_LINEAR"){
return this.translateGradientLinearFill(fill, width, height);
} else if (fill.type === 'GRADIENT_LINEAR') {
return this.translateGradientLinearFill(fill /*, width, height*/);
} else {
console.error('Color type '+fill.type+' not supported yet');
console.error('Color type ' + fill.type + ' not supported yet');
return null;
}
}
translateFills(fills, width, height){
let penpotFills = [];
translateFills(fills: readonly Paint[] /*, width: number, height: number*/) {
const penpotFills = [];
let penpotFill = null;
for (var fill of fills){
penpotFill = this.translateFill(fill, width, height);
for (const fill of fills) {
penpotFill = this.translateFill(fill /*, width, height*/);
// Penpot does not track fill visibility, so if the Figma fill is invisible we
// force opacity to 0
if (fill.visible === false){
penpotFill.fillOpacity = 0;
}
if (penpotFill !== null){
if (penpotFill !== null) {
penpotFills.unshift(penpotFill);
}
}
return penpotFills;
}
addFontWarning(font){
addFontWarning(font: string) {
const newMissingFonts = this.state.missingFonts;
newMissingFonts.add(font);
this.setState(_ => ({missingFonts: newMissingFonts }));
this.setState(() => ({ missingFonts: newMissingFonts }));
}
createPenpotPage(file, node){
createPenpotPage(file: PenpotFile, node: NodeData) {
file.addPage(node.name);
for (var child of node.children){
for (const child of node.children) {
this.createPenpotItem(file, child, 0, 0);
}
file.closePage();
}
createPenpotBoard(file, node, baseX, baseY){
file.addArtboard({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
fills: this.translateFills(node.fills, node.width, node.height)
createPenpotBoard(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file.addArtboard({
name: node.name,
x: node.x + baseX,
y: node.y + baseY,
width: node.width,
height: node.height,
fills: this.translateFills(node.fills /*, node.width, node.height*/)
});
for (var child of node.children){
for (const child of node.children) {
this.createPenpotItem(file, child, node.x + baseX, node.y + baseY);
}
file.closeArtboard();
}
createPenpotGroup(file, node, baseX, baseY){
file.addGroup({name: node.name});
for (var child of node.children){
createPenpotGroup(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file.addGroup({ name: node.name });
for (const child of node.children) {
this.createPenpotItem(file, child, baseX, baseY);
}
file.closeGroup();
}
createPenpotRectangle(file, node, baseX, baseY){
file.createRect({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
fills: this.translateFills(node.fills, node.width, node.height),
createPenpotRectangle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file.createRect({
name: node.name,
x: node.x + baseX,
y: node.y + baseY,
width: node.width,
height: node.height,
fills: this.translateFills(node.fills /*, node.width, node.height*/)
});
}
createPenpotCircle(file, node, baseX, baseY){
file.createCircle({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
fills: this.translateFills(node.fills, node.width, node.height),
createPenpotCircle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file.createCircle({
name: node.name,
x: node.x + baseX,
y: node.y + baseY,
width: node.width,
height: node.height,
fills: this.translateFills(node.fills /*, node.width, node.height*/)
});
}
translateHorizontalAlign(align: string){
if (align === "RIGHT"){
return Symbol.for("right");
translateHorizontalAlign(align: string) {
if (align === 'RIGHT') {
return Symbol.for('right');
}
if (align === "CENTER"){
return Symbol.for("center");
if (align === 'CENTER') {
return Symbol.for('center');
}
return Symbol.for("left")
return Symbol.for('left');
}
translateVerticalAlign(align: string){
if (align === "BOTTOM"){
return Symbol.for("bottom");
translateVerticalAlign(align: string) {
if (align === 'BOTTOM') {
return Symbol.for('bottom');
}
if (align === "CENTER"){
return Symbol.for("center");
if (align === 'CENTER') {
return Symbol.for('center');
}
return Symbol.for("top")
return Symbol.for('top');
}
translateFontStyle(style:string){
return style.toLowerCase().replace(/\s/g, "")
translateFontStyle(style: string) {
return style.toLowerCase().replace(/\s/g, '');
}
getTextDecoration(node){
getTextDecoration(node: TextData | TextDataChildren) {
const textDecoration = node.textDecoration;
if (textDecoration === "STRIKETHROUGH"){
return "line-through";
if (textDecoration === 'STRIKETHROUGH') {
return 'line-through';
}
if (textDecoration === "UNDERLINE"){
return "underline";
if (textDecoration === 'UNDERLINE') {
return 'underline';
}
return "none";
return 'none';
}
getTextTransform(node){
getTextTransform(node: TextData | TextDataChildren) {
const textCase = node.textCase;
if (textCase === "UPPER"){
return "uppercase";
if (textCase === 'UPPER') {
return 'uppercase';
}
if (textCase === "LOWER"){
return "lowercase";
if (textCase === 'LOWER') {
return 'lowercase';
}
if (textCase === "TITLE"){
return "capitalize";
if (textCase === 'TITLE') {
return 'capitalize';
}
return "none";
return 'none';
}
validateFont(fontName) {
validateFont(fontName: FontName) {
const name = slugify(fontName.family.toLowerCase());
if (!gfonts.has(name)) {
this.addFontWarning(name);
}
}
createPenpotText(file, node, baseX, baseY){
const children = node.children.map((val) => {
createPenpotText(file: PenpotFile, node: TextData, baseX: number, baseY: number) {
const children = node.children.map(val => {
this.validateFont(val.fontName);
return {
lineHeight: val.lineHeight,
fontStyle: "normal",
fontStyle: 'normal',
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
fontId: "gfont-" + slugify(val.fontName.family.toLowerCase()),
fontId: 'gfont-' + slugify(val.fontName.family.toLowerCase()),
fontSize: val.fontSize.toString(),
fontWeight: val.fontWeight.toString(),
fontVariantId: this.translateFontStyle(val.fontName.style),
textDecoration: this.getTextDecoration(val),
textTransform: this.getTextTransform(val),
letterSpacing: val.letterSpacing,
fills: this.translateFills(val.fills, node.width, node.height),
fills: this.translateFills(val.fills /*, node.width, node.height*/),
fontFamily: val.fontName.family,
text: val.characters }
text: val.characters
};
});
this.validateFont(node.fontName);
@ -255,34 +262,49 @@ export default class PenpotExporter extends React.Component<PenpotExporterProps,
width: node.width,
height: node.height,
rotation: 0,
type: Symbol.for("text"),
type: Symbol.for('text'),
content: {
type: "root",
type: 'root',
verticalAlign: this.translateVerticalAlign(node.textAlignVertical),
children: [{
type: "paragraph-set",
children: [{
children: [
{
type: 'paragraph-set',
children: [
{
lineHeight: node.lineHeight,
fontStyle: "normal",
fontStyle: 'normal',
children: children,
textTransform: this.getTextTransform(node),
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
fontId: "gfont-" + slugify(node.fontName.family.toLowerCase()),
fontId: 'gfont-' + slugify(node.fontName.family.toLowerCase()),
fontSize: node.fontSize.toString(),
fontWeight: node.fontWeight.toString(),
type: "paragraph",
type: 'paragraph',
textDecoration: this.getTextDecoration(node),
letterSpacing: node.letterSpacing,
fills: this.translateFills(node.fills, node.width, node.height),
fills: this.translateFills(node.fills /*, node.width, node.height*/),
fontFamily: node.fontName.family
}]
}]
}
]
}
]
}
});
}
createPenpotImage(file, node, baseX, baseY, image){
file.createImage({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: image.width, height: image.height,
createPenpotImage(
file: PenpotFile,
node: NodeData,
baseX: number,
baseY: number,
image: FigmaImageData
) {
file.createImage({
name: node.name,
x: node.x + baseX,
y: node.y + baseY,
width: image.width,
height: image.height,
metadata: {
width: image.width,
height: image.height
@ -291,65 +313,66 @@ export default class PenpotExporter extends React.Component<PenpotExporterProps,
});
}
calculateAdjustment(node){
calculateAdjustment(node: NodeData) {
// For each child, check whether the X or Y position is less than 0 and less than the
// current adjustment.
let adjustedX = 0;
let adjustedY = 0;
for (var child of node.children){
if (child.x < adjustedX){
for (const child of node.children) {
if (child.x < adjustedX) {
adjustedX = child.x;
}
if (child.y < adjustedY){
if (child.y < adjustedY) {
adjustedY = child.y;
}
}
return [adjustedX, adjustedY];
}
createPenpotItem(file, node, baseX, baseY){
createPenpotItem(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
// We special-case images because an image in figma is a shape with one or many
// image fills. Given that handling images in Penpot is a bit different, we
// rasterize a figma shape with any image fills to a PNG and then add it as a single
// Penpot image. Implication is that any node that has an image fill will only be
// treated as an image, so we skip node type checks.
const hasImageFill = node.fills?.some(fill => fill.type === "IMAGE");
if (hasImageFill){
const hasImageFill = node.fills?.some((fill: Paint) => fill.type === 'IMAGE');
if (hasImageFill) {
// If the nested frames extended the bounds of the rasterized image, we need to
// account for this both in position on the canvas and the calculated width and
// height of the image.
const [adjustedX, adjustedY] = this.calculateAdjustment(node);
const width = node.width + Math.abs(adjustedX);
const height = node.height + Math.abs(adjustedY);
this.createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY, this.state.images[node.id]);
}
else if (node.type == "PAGE"){
this.createPenpotImage(
file,
node,
baseX + adjustedX,
baseY + adjustedY,
this.state.images[node.id]
);
} else if (node.type == 'PAGE') {
this.createPenpotPage(file, node);
}
else if (node.type == "FRAME"){
} else if (node.type == 'FRAME') {
this.createPenpotBoard(file, node, baseX, baseY);
}
else if (node.type == "GROUP"){
this.createPenpotGroup(file, node,baseX, baseY);
}
else if (node.type == "RECTANGLE"){
} else if (node.type == 'GROUP') {
this.createPenpotGroup(file, node, baseX, baseY);
} else if (node.type == 'RECTANGLE') {
this.createPenpotRectangle(file, node, baseX, baseY);
}
else if (node.type == "ELLIPSE"){
} else if (node.type == 'ELLIPSE') {
this.createPenpotCircle(file, node, baseX, baseY);
}
else if (node.type == "TEXT"){
this.createPenpotText(file, node, baseX, baseY);
} else if (node.type == 'TEXT') {
this.createPenpotText(file, node as unknown as TextData, baseX, baseY);
}
}
createPenpotFile(){
let node = this.state.figmaRootNode;
const file = penpot.createFile(node.name);
for (var page of node.children){
createPenpotFile() {
const node = this.state.figmaRootNode;
if (node === null) {
throw new Error('No Figma file data found');
}
const file = createFile(node.name);
for (const page of node.children) {
this.createPenpotItem(file, page, 0, 0);
}
return file;
@ -358,108 +381,123 @@ export default class PenpotExporter extends React.Component<PenpotExporterProps,
onCreatePenpot = () => {
const file = this.createPenpotFile();
const penpotFileMap = file.asMap();
this.setState(state => ({penpotFileData: JSON.stringify(penpotFileMap, (key, value) => (value instanceof Map ? [...value] : value), 4)}));
file.export()
this.setState(() => ({
penpotFileData: JSON.stringify(
penpotFileMap,
(key, value) => (value instanceof Map ? [...value] : value),
4
)
}));
file.export();
};
onCancel = () => {
parent.postMessage({ pluginMessage: { type: "cancel" } }, "*");
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*');
};
onMessage = (event) => {
if (event.data.pluginMessage.type == "FIGMAFILE") {
this.setState(state => ({
figmaFileData: JSON.stringify(event.data.pluginMessage.data, (key, value) => (value instanceof Map ? [...value] : value), 4),
figmaRootNode: event.data.pluginMessage.data}));
}
else if (event.data.pluginMessage.type == "IMAGE") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onMessage = (event: any) => {
if (event.data.pluginMessage.type == 'FIGMAFILE') {
this.setState(() => ({
figmaFileData: JSON.stringify(
event.data.pluginMessage.data,
(key, value) => (value instanceof Map ? [...value] : value),
4
),
figmaRootNode: event.data.pluginMessage.data
}));
} else if (event.data.pluginMessage.type == 'IMAGE') {
const data = event.data.pluginMessage.data;
const image = document.createElement('img');
const thisObj = this;
image.addEventListener('load', function() {
image.addEventListener('load', () => {
// Get byte array from response
thisObj.setState(state =>
{
this.setState(state => {
state.images[data.id] = {
value: data.value,
width: image.naturalWidth,
height: image.naturalHeight
};
return state;
}
);
});
});
image.src = data.value;
}
}
};
setDimensions = () => {
const isMissingFonts = this.state.missingFonts.size > 0;
let width = 300;
let height = 280;
if (isMissingFonts) {
height += (this.state.missingFonts.size * 20);
height += this.state.missingFonts.size * 20;
width = 400;
}
if (this.state.isDebug){
if (this.state.isDebug) {
height += 600;
width = 800;
}
parent.postMessage({ pluginMessage: { type: "resize", width: width, height: height } }, "*");
}
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
};
toggleDebug = (event) => {
toggleDebug = (event: React.ChangeEvent<HTMLInputElement>) => {
const isDebug = event.currentTarget.checked;
this.setState (state => ({isDebug: isDebug}));
}
this.setState(() => ({ isDebug: isDebug }));
};
renderFontWarnings = () => {
return (
<ul >
{Array.from(this.state.missingFonts).map((font) => (
<ul>
{Array.from(this.state.missingFonts).map(font => (
<li key={font}>{font}</li>
))}
</ul>
);
}
};
render() {
// Update the dimensions of the plugin window based on available data and selections
return (
<main>
<header>
<img src={require("./logo.svg")} />
<img src={require('./logo.svg')} />
<h2>Penpot Exporter</h2>
</header>
<section>
<div style={{display:this.state.missingFonts.size > 0 ? "inline" : "none"}}>
<div id="missing-fonts">{this.state.missingFonts.size} non-default font{this.state.missingFonts.size > 1 ? 's' : ''}: </div>
<div style={{ display: this.state.missingFonts.size > 0 ? 'inline' : 'none' }}>
<div id="missing-fonts">
{this.state.missingFonts.size} non-default font
{this.state.missingFonts.size > 1 ? 's' : ''}:{' '}
</div>
<small>Ensure fonts are installed in Penpot before importing.</small>
<div id="missing-fonts-list">
{this.renderFontWarnings()}
<div id="missing-fonts-list">{this.renderFontWarnings()}</div>
</div>
</div>
<div >
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug}/>
<div>
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug} />
<label htmlFor="chkDebug">Show debug data</label>
</div>
</section>
<div style={{display: this.state.isDebug? '': 'none'}}>
<div style={{ display: this.state.isDebug ? '' : 'none' }}>
<section>
<textarea style={{width:'790px', height:'270px'}} id="figma-file-data" value={this.state.figmaFileData} readOnly />
<textarea
style={{ width: '790px', height: '270px' }}
id="figma-file-data"
value={this.state.figmaFileData}
readOnly
/>
<label htmlFor="figma-file-data">Figma file data</label>
</section>
<section>
<textarea style={{width:'790px', height:'270px'}} id="penpot-file-data" value={this.state.penpotFileData} readOnly />
<textarea
style={{ width: '790px', height: '270px' }}
id="penpot-file-data"
value={this.state.penpotFileData}
readOnly
/>
<label htmlFor="penpot-file-data">Penpot file data</label>
</section>
</div>
@ -474,9 +512,8 @@ export default class PenpotExporter extends React.Component<PenpotExporterProps,
}
}
ReactDOM.render(
createRoot(document.getElementById('penpot-export-page') as HTMLElement).render(
<React.StrictMode>
<PenpotExporter />
</React.StrictMode>,
document.getElementById('penpot-export-page')
</React.StrictMode>
);

4
stylelint.config.mjs Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('stylelint').Config} */
export default {
extends: ['stylelint-config-recommended', 'stylelint-config-standard']
};

View file

@ -1,13 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"jsx": "react",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@figma"
],
"moduleResolution":"node"
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
"lib": ["DOM", "ES6"],
"target": "ES6",
"module": "ESNext",
"moduleResolution": "Node10",
"strict": true,
"typeRoots": ["src/penpot.d.ts", "node_modules/@figma", "node_modules/@types"]
}
}

View file

@ -1,8 +1,9 @@
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const webpack = require('webpack');
module.exports = (env, argv) => ({
mode: argv.mode === 'production' ? 'production' : 'development',
@ -12,7 +13,7 @@ module.exports = (env, argv) => ({
entry: {
ui: './src/ui.tsx', // The entry point for your UI code
code: './src/code.ts', // The entry point for your plugin code
code: './src/code.ts' // The entry point for your plugin code
},
module: {
@ -27,7 +28,7 @@ module.exports = (env, argv) => ({
// Enables including CSS by doing "import './file.css'" in your TypeScript code
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
use: ['style-loader', 'css-loader']
},
// Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
// { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] }
@ -39,25 +40,24 @@ module.exports = (env, argv) => ({
},
// Webpack tries these extensions for you if you omit the extension like "import './file'"
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'] ,
fallback: { "crypto": false }},
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'], fallback: { crypto: false } },
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist"
path: path.resolve(__dirname, 'dist') // Compile into a folder called "dist"
},
// Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
plugins: [
new webpack.DefinePlugin({
'global': {} // Fix missing symbol error when running in developer VM
global: {} // Fix missing symbol error when running in developer VM
}),
new HtmlWebpackPlugin({
inject: "body",
inject: 'body',
template: './src/ui.html',
filename: 'ui.html',
chunks: ['ui']
}),
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]),
],
})
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/])
]
});