-
-
Notifications
You must be signed in to change notification settings - Fork 197
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [WIP] webpack config setup for fast refresh + websocket server * add prod/dev hmr webpack config option * render immediately and handle AsyncMessage in startup app hook * forward ui AsyncMessages to browser via WebSockets * null check sentry transaction to fix browser error * refactor AsyncMessageChannel code for browser implementation * webpack use swc-loader for browser version + speed/bundle size plugin options * prettify browser dev preview UI * enable loading screen if startup params missing (for web serve + disconnected browser dev preview) * attempt to fix webpack build for tests * add SpeedMeasurePlugin package * create AsyncMessageChannel dev docs * replace web-preview.md ASCII data flow diagram with mermaid * use radii/spacing tokens instead of px for web preview.tsx styles Co-authored-by: Jan Six <[email protected]> * remove commented out startup handler (handled in startup.tsx useEffect now) Co-authored-by: Jan Six <[email protected]> * remove commented out code Co-authored-by: Jan Six <[email protected]> * replace px values with tokens Co-authored-by: Jan Six <[email protected]> * conditional export for AsyncMessageChannel preview env * add browser preview WEBSOCKETS_PORT env * fix typescript issue with PreviewAsyncMessageChannel.isWsConnected * add test coverage for AsyncMessageChannelPreview * Browser preview debug UI (#2803) * fix AsyncMessageChannelPreview undefined error + export WS URI * browser preview CSS file for UI fixes * create previewUtils for browser color scheme + setFigmaBrowserTheme * browser preview URL params + fullscreen/theme/action modes * two bug fixes for browser/plugin websocket preview bridge * add preview dist folder for web preview builds * [WIP] browser preview dev knowledge docs * feat(dev): request startup on browser preview page open * refactor(dev): use env vars for browser preview ws src * fix(debug): remove console.log from asyncmessagechannelpreview * fix(css): figmaloading full height css for browser preview * refactor(dev): use enums for websockets src in browser preview tsx * fix(dev): remove comments * refactor: reuse htmlClassList variable * remove unused package --------- Co-authored-by: macintoshhelper <[email protected]> --------- Co-authored-by: macintoshhelper <[email protected]> Co-authored-by: Jan Six <[email protected]>
- Loading branch information
Showing
20 changed files
with
1,378 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
# AsyncMessageChannel | ||
|
||
## `AsyncMessageChannel` Data Flow | ||
|
||
```mermaid | ||
graph TD | ||
A["Figma Plugin Controller\n(Sandbox Env)\ncontroller.ts"] | ||
B[AsyncMessageChannel\nPluginInstance\nEnvironment.CONTROLLER] | ||
C[Figma Plugin UI\nUI entrypoint\napp/index.tsx] | ||
D[AsyncMessageChannel\nReactInstance\nEnvironment.UI] | ||
E[Web Browser Preview\nhttp://localhost:9000] | ||
F[AsyncMessageChannel\nReactInstance\nEnvironment.BROWSER] | ||
A -->|"PluginInstance.connect()"| B | ||
B -->|"PluginInstance.handle(...)"| A | ||
B -->|"ReactInstance.connect()"| C | ||
C -->|"ReactInstance.handle(...)"| B | ||
C -->|"sendMessageToUi\n(figma.ui.postMessage(...))"| D | ||
D -->|"sendMessageToController\n(parent.postMessage({ pluginMessage: {...} }))"| C | ||
D -->|"ReactInstance.connect()"| E | ||
E -->|"ReactInstance.handle(...)"| D | ||
E -->|"sendMessageToBrowser\n(ws.send(...))"| F | ||
F -->|"sendMessageFromBrowser\n(ws.send(...))"| E | ||
``` | ||
|
||
## Instances | ||
|
||
Static instances of `AsyncMessageChannel` are initialised when the class is loaded: | ||
|
||
- `PluginInstance` - used from inside of `controller` entrypoint | ||
- `ReactInstance` - used from inside of `ui` entrypoint | ||
|
||
```ts | ||
class AsyncMessageChannel { | ||
public static PluginInstance: AsyncMessageChannel = new AsyncMessageChannel(true); | ||
public static ReactInstance: AsyncMessageChannel = new AsyncMessageChannel(false); | ||
|
||
protected inFigmaSandbox = false; | ||
|
||
constructor(inFigmaSandbox: boolean) { | ||
this.inFigmaSandbox = inFigmaSandbox | ||
} | ||
} | ||
|
||
``` | ||
|
||
- | ||
|
||
## Environments | ||
|
||
There are currently three environments: | ||
|
||
```ts | ||
enum Environment { | ||
PLUGIN = 'PLUGIN', | ||
UI = 'UI', | ||
BROWSER = 'BROWSER', | ||
``` | ||
- `Environment.PLUGIN` – `controller` entrypoint | ||
- `Environment.UI` – `ui` entrypoint | ||
- Has access to `parent.postMessage` | ||
- `Environment.BROWSER` – `ui` entrypoint | ||
- Need to use WebSockets to send messages to the plugin `ui` | ||
## Lifecycle | ||
**`.connect()`** | ||
Example: `AsyncMessageChannel.PluginInstance.connect();` or `AsyncMessageChannel.ReactInstance.connect();` | ||
If in a web preview environment (`Environment.BROWSER` or `Environment.UI`), a WebSocket client listener is registered here (`this.startWebSocketConnection();`) | ||
Registers message listeners with `this.attachMessageListener(callback)`, where `callback` in this case is [`this.onMessageEvent`](#onmessageeventmsg) | ||
**`.attachMessageListener(callback)`** | ||
Conditionally registers message event handlers depending on the environment: | ||
- `Environment.CONTROLLER` | ||
- `figma.ui.on('message', listener)` | ||
- `Environment.UI` | ||
- `window.addEventListener('message', listener)` – listens to messages controller | ||
- *IF process.env.PREVIEW_MODE IS SET* | ||
- `this.ws?.addEventListener('message', listener)` | ||
- Where if this condition is true, `UI` has two message listeners, one to listen | ||
- `Environment.CONTROLLER` | ||
- `this.ws?.addEventListener('message', listener)` | ||
|
||
Where `listener` is a function that is wrapping `callback`: | ||
|
||
### `.onMessageEvent(msg)` | ||
|
||
If the environment is preview, and message is not async, the UI environment will forward the message to the browser. Else, non async messages are discarded with `return;` | ||
|
||
Next, if the environment is `UI` and `PREVIEW_MODE` is truthy, the message is forwarded via WebSockets to the browser, or to the controller. | ||
|
||
Then the handler is called; the function is retrieved from `$handlers[msg.message.type]`. | ||
|
||
The result of the handler function is `await`ed and a message is sent back to the source with the same message type, and the payload from the handler function result. | ||
|
||
## Message Handling | ||
|
||
`AsyncMessageChannel` handles messages with `.message` to send messages and receives messages (say in a different instance) by registering a handler with `.handle()`. Each handler is stored in the class/instance in `$handlers`, keyed by the message type. | ||
|
||
Example: `AsyncMessageChannel.ReactInstance.handle(AsyncMessageTypes.STARTUP, asyncHandlers.startup)`. | ||
|
||
### Startup Process | ||
|
||
**`controller.ts`** | ||
|
||
```ts | ||
AsyncMessageChannel.PluginInstance.connect(); | ||
``` | ||
|
||
|
||
**`init.ts`** | ||
|
||
```ts | ||
// Creating the plugin UI instance (AsyncMessageChannel.ReactInstance) | ||
figma.showUI(__html__, { | ||
themeColors: true, | ||
width: params.settings.width ?? DefaultWindowSize.width, | ||
height: params.settings.height ?? DefaultWindowSize.height, | ||
}); | ||
// | ||
await AsyncMessageChannel.PluginInstance.message({ | ||
type: AsyncMessageTypes.STARTUP, | ||
...params, | ||
}); | ||
``` | ||
|
||
**`asyncMessageHandlers/startup.tsx` / `StartupApp`** | ||
|
||
```tsx | ||
useEffect(() => { | ||
AsyncMessageChannel.ReactInstance.handle(AsyncMessageTypes.STARTUP, async (startupParams) => { | ||
setParams(startupParams); | ||
}); | ||
return () => { | ||
AsyncMessageChannel.ReactInstance.handle(AsyncMessageTypes.STARTUP, (() => {}) as any); | ||
}; | ||
}, []); | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Web Preview | ||
|
||
## Getting Started | ||
|
||
1. Open two Terminal windows/tabs | ||
|
||
> Terminal 1 (Plugin) | ||
```sh | ||
npm run preview:plugin | ||
``` | ||
|
||
> Terminal 2 (Browser) | ||
|
||
```sh | ||
npm run preview:browser | ||
``` | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
const express = require("express"); | ||
const http = require("http"); | ||
const WebSocket = require("ws"); | ||
const path = require("path"); | ||
|
||
const PORT = process.env.WEBSOCKETS_PORT || 9001; | ||
|
||
const app = express(); | ||
|
||
app.use(express.static(__dirname + '/dist')) | ||
|
||
app.get("/", (req, res) => { | ||
// res.sendFile(path.join(__dirname, 'dist', 'index.html')); | ||
res.status(200).send("working"); | ||
}); | ||
|
||
const server = http.createServer(app); | ||
|
||
// initialize the WebSocket server instance | ||
const wss = new WebSocket.Server({ server }); | ||
wss.on("connection", (ws) => { | ||
ws.isAlive = true; | ||
ws.on("pong", () => { | ||
ws.isAlive = true; | ||
}); | ||
|
||
// connection is up, let's add a simple simple event | ||
ws.on("message", (data, isBinary) => { | ||
const message = isBinary ? data : data.toString(); | ||
// send back the message to the other clients | ||
wss.clients.forEach((client) => { | ||
if (client != ws) { | ||
client.send(JSON.stringify({ message, src: 'server' })); | ||
} | ||
}); | ||
}); | ||
|
||
// send immediatly a feedback to the incoming connection | ||
// ws.send('Hi there, I am a WebSocket server'); | ||
}); | ||
|
||
setInterval(() => { | ||
wss.clients.forEach((ws) => { | ||
if (!ws.isAlive) return ws.terminate(); | ||
ws.isAlive = false; | ||
ws.ping(); | ||
}); | ||
}, 10000); | ||
|
||
server.listen(PORT, () => { | ||
console.log(`Preview server started on port: ${PORT}`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.