Skip to content

Commit

Permalink
feat: component instance handling (#36)
Browse files Browse the repository at this point in the history
Added first attempt to handle component instances
  • Loading branch information
MiguelFranken authored Apr 23, 2024
2 parents 1c461e8 + 2153ea6 commit 4de630a
Show file tree
Hide file tree
Showing 19 changed files with 550 additions and 198 deletions.
3 changes: 3 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 82 additions & 5 deletions packages/code/src/generate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ComponentTreeNode } from '@onyx-gen/types'
import FigmaNodeParser from './parsers/figma-node.parser'
import HTMLGenerator from './generators/html.generator'
import { getSelectedNodes } from './utils'
import { getSelectedNodes, getUniqueInstanceNodes } from './utils'
import ComponentSetProcessor from './set/component-set-processor'
import {
sendExecutionTimeMessage,
sendHtmlMessage,
sendGeneratedComponentsMessage,
sendIsLoadingMessage,
sendSelectedMessage,
sendUnselectedMessage,
Expand Down Expand Up @@ -86,11 +87,42 @@ export default async function generate(config: Configuration): Promise<string |
sendSelectedMessage([])
}

const instanceNodes = (await Promise.all(nodes.map(node => getUniqueInstanceNodes(node)))).flat()

const mainComponentsOfInstanceNodes = await Promise.all(
instanceNodes
.map(instanceNode => instanceNode.getMainComponentAsync())
.filter((mainComponent): mainComponent is Promise<ComponentNode> => mainComponent !== null),
)

const instances = await Promise.all(mainComponentsOfInstanceNodes.map(instanceNode => generateComponentTree(instanceNode, config)))

console.log('SELECTED NODE', nodes[0])

const mainNode = nodes[0]
let mainNodeName = mainNode.name

if (mainNode.type === 'COMPONENT') {
const hasComponentSetParent = mainNode.parent?.type === 'COMPONENT_SET'

if (hasComponentSetParent)
mainNodeName = mainNode.parent.name
}

const componentTree: ComponentTreeNode = {
name: mainNodeName,
figmaNode: nodes[0] as ComponentNode, // TODO MF: Should also work for other node types
code: html,
instances,
}

console.log('### COMPONENT TREE ###', componentTree)

// only send message if html is not empty
if (html)
sendHtmlMessage(html)
else
sendUnselectedMessage()
sendGeneratedComponentsMessage({ componentTree })

else sendUnselectedMessage()
}
catch (e) {
figma.notify('Onyx: Unexpected Error')
Expand All @@ -103,3 +135,48 @@ export default async function generate(config: Configuration): Promise<string |
sendExecutionTimeMessage(executionTime)
}
}

/**
* Recursively generates a component tree for given Figma nodes.
* Each node's HTML code is generated using FigmaNodeParser and HTMLGenerator.
*
* @param node - The initial node to generate the tree from.
* @param config - Configuration object containing settings and options for node processing.
* @returns A promise that resolves to a ComponentTreeNode representing the node hierarchy with generated HTML.
*/
async function generateComponentTree(node: ComponentNode, config: Configuration): Promise<ComponentTreeNode> {
const parser = new FigmaNodeParser({ default: 'default' }, config)
const generator = new HTMLGenerator([], {}, config)

const tree = await parser.parse(node)
const html = tree ? await generator.generate(tree) : ''

const instances: ComponentTreeNode[] = []

const instanceNodes = await getUniqueInstanceNodes(node)
for (const instanceNode of instanceNodes) {
if (!config.ignoredComponentInstances.includes(instanceNode.name)) {
const mainComponent = await instanceNode.getMainComponentAsync()
if (mainComponent) {
const instanceTree = await generateComponentTree(mainComponent, config)
instances.push(instanceTree)
}
}
}

let nodeName = node.name

if (node.type === 'COMPONENT') {
const hasComponentSetParent = node.parent?.type === 'COMPONENT_SET'

if (hasComponentSetParent)
nodeName = node.parent.name
}

return {
name: nodeName,
code: html,
figmaNode: node,
instances,
}
}
11 changes: 7 additions & 4 deletions packages/code/src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type {
ConfigurationPluginMessage,
ExecutionTimePluginMessage,
HtmlPluginMessage,
GeneratedComponentsPluginMessage,
GeneratedComponentsPluginMessageData,
IConfiguration,
IsLoadingPluginMessage,
PluginMessage,
SelectedNode,
SelectedPluginMessage,
UnselectedPluginMessage,
} from '@onyx-gen/types'

import EventBus from './event-bus'

/**
Expand Down Expand Up @@ -56,12 +58,13 @@ export function sendSelectedMessage(nodes: SelectedNode[] | null): void {
/**
* Sends an HTML message to the UI plugin.
*
* @param {string} html - The HTML content to send.
* @param {GeneratedComponentsPluginMessageData} data - The generated components data.
*
* @return {void}
*/
export function sendHtmlMessage(html: string): void {
const pluginMessage: HtmlPluginMessage = { event: 'html', data: { html } }
export function sendGeneratedComponentsMessage(data: GeneratedComponentsPluginMessageData): void {
const pluginMessage: GeneratedComponentsPluginMessage = { event: 'generated-components', data }
console.log('sent', pluginMessage)
figma.ui.postMessage(pluginMessage)
}

Expand Down
46 changes: 46 additions & 0 deletions packages/code/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,52 @@ export function getSelectedNode(): SceneNode | null {
return null
}

/**
* Checks if the node is an instance node.
* @param node - The node to check.
*/
function isInstanceNode(node: SceneNode): node is InstanceNode {
return node.type === 'INSTANCE'
}

/**
* Retrieves all instance nodes in the node tree.
* @param node
*/
export function getInstanceNodes(node: SceneNode): InstanceNode[] {
if (isInstanceNode(node))
return [node]

else if ('children' in node)
return node.children.flatMap(getInstanceNodes)

else return []
}

/**
* Retrieves all unique instance nodes in the node tree.
* @param node
*/
export async function getUniqueInstanceNodes(node: SceneNode): Promise<InstanceNode[]> {
const instanceNodes = getInstanceNodes(node)

const mainComponentsInstanceNodePairs = (
await Promise.all(
instanceNodes
.map(async (node) => {
const mainComponent = await node.getMainComponentAsync()
return [node, mainComponent] as [InstanceNode, ComponentNode | null]
}),
)
).filter((d): d is [InstanceNode, ComponentNode] => d[1] !== null)

const uniqueMainComponentIds = Array.from(new Set(mainComponentsInstanceNodePairs.map(([_, mainComponent]) => mainComponent.id)))
return uniqueMainComponentIds.map((id) => {
const foundPair = mainComponentsInstanceNodePairs.find(([_, mainComponent]) => mainComponent.id === id)
return foundPair![0]
})
}

/**
* Zips two arrays together, creating pairs of corresponding elements.
*
Expand Down
12 changes: 12 additions & 0 deletions packages/icons/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @onyx-gen/icons

## 0.0.3

### Patch Changes

- Add logo

## 0.0.2

### Patch Changes

- Add eye icons

## 0.0.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/icons/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@onyx-gen/icons",
"type": "module",
"version": "0.0.1",
"version": "0.0.3",
"private": false,
"license": "MIT",
"publishConfig": {
Expand Down
44 changes: 38 additions & 6 deletions packages/renderer/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { type App, createApp } from 'vue'
import type { App, ComponentOptions } from 'vue'
import { createApp, defineComponent } from 'vue'
import type {
ComponentTreeNode,
GeneratedComponentsPluginMessageData,

RendererPluginMessage,
} from '@onyx-gen/types'

Expand All @@ -9,21 +13,49 @@ import './unocss'
let app: App | null = null

// Listen for messages from the parent frame
window.addEventListener('message', async (event) => {
window.addEventListener('message', async (event: MessageEvent & { data: GeneratedComponentsPluginMessageData }) => {
console.log('[iFrame] Received message', event.data)

// Unmount the previous app
if (app !== null)
app.unmount()

const data: GeneratedComponentsPluginMessageData = event.data

// Construct Vue components recursively from the component tree
const rootComponentOptions = createVueComponent(data.componentTree)

// Create the Vue app and mount it to the root element
try {
app = createApp({ template: event.data.vue })
app = createApp(rootComponentOptions)
app.mount('#app')
}
catch (e) {
console.error('[iFrame] Error creating Vue app', e)
return
}

app.mount('#app')
})

/**
* Recursively creates a Vue component from a component tree node.
*
* @param node - The current node of the component tree.
* @returns A Vue component options object representing the node and its children.
*/
function createVueComponent(node: ComponentTreeNode): ComponentOptions {
const childComponents: Record<string, ComponentOptions> = {}
node.instances.forEach((instance) => {
childComponents[instance.name] = createVueComponent(instance)
})

const options: ComponentOptions = {
name: node.name,
template: node.code,
components: childComponents,
}

return defineComponent(options as any) as any
}

function observeHeight() {
const targetElement = document.getElementById('app')!

Expand Down
10 changes: 5 additions & 5 deletions packages/types/src/events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentProps, IConfiguration, Mode, Unit, VariableNameTransformations } from './types'
import type { ComponentProps, ComponentTreeNode, IConfiguration, Mode, Unit, VariableNameTransformations } from './types'

export type PluginMessageEvent = MessageEvent<{ pluginMessage: PluginMessage }>

Expand All @@ -23,10 +23,10 @@ interface RendererPluginMessageData {
height: number
}

export type HtmlPluginMessage = BasePluginMessage<'html', HtmlPluginMessageData>
export type GeneratedComponentsPluginMessage = BasePluginMessage<'generated-components', GeneratedComponentsPluginMessageData>

interface HtmlPluginMessageData {
html: string
export interface GeneratedComponentsPluginMessageData {
componentTree: ComponentTreeNode
}

export type ConfigurationPluginMessage = BasePluginMessage<'configuration', ConfigurationPluginMessageData>
Expand Down Expand Up @@ -110,7 +110,7 @@ interface SelectedPluginMessageData {

export type PluginMessage =
| RendererPluginMessage
| HtmlPluginMessage
| GeneratedComponentsPluginMessage
| UnselectedPluginMessage
| ExecutionTimePluginMessage
| IsLoadingPluginMessage
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ export interface IConfiguration {
variableNameTransformations: VariableNameTransformations
ignoredComponentInstances: string[]
}

export interface ComponentTreeNode {
name: string
code: string
figmaNode: ComponentNode
instances: ComponentTreeNode[]
}
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@figma/plugin-typings": "^1.91.0",
"@onyx-gen/icons": "0.0.1",
"@onyx-gen/icons": "0.0.3",
"@onyx-gen/tsconfig": "workspace:^",
"@onyx-gen/types": "workspace:^",
"@unocss/transformer-variant-group": "^0.59.4",
Expand Down
Loading

0 comments on commit 4de630a

Please sign in to comment.