diff --git a/package-lock.json b/package-lock.json index b7d4c6a0..37693781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@create-figma-plugin/ui": "^3.2", "base64-js": "^1.5", "classnames": "^2.5", + "fastq": "^1.17.1", "lru-cache": "^10.2", "preact": "^10.22", "react-hook-form": "^7.51", @@ -4821,7 +4822,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -7129,7 +7129,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" diff --git a/package.json b/package.json index d63859d6..0e0ee53b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@create-figma-plugin/ui": "^3.2", "base64-js": "^1.5", "classnames": "^2.5", + "fastq": "^1.17.1", "lru-cache": "^10.2", "preact": "^10.22", "react-hook-form": "^7.51", diff --git a/plugin-src/Queue.ts b/plugin-src/Queue.ts new file mode 100644 index 00000000..9d67f941 --- /dev/null +++ b/plugin-src/Queue.ts @@ -0,0 +1,30 @@ +import * as fastq from 'fastq'; +import type { queueAsPromised } from 'fastq'; + +import { transformSceneNode } from '@plugin/transformers'; + +import { PenpotNode } from '@ui/types'; + +class Queue { + private queue: queueAsPromised; + + constructor(worker: fastq.asyncWorker, concurrency: number) { + this.queue = fastq.promise(worker, concurrency); + } + + public enqueue(task: T): Promise { + return this.queue.push(task); + } + + public async waitIdle() { + await this.queue.drain(); + } +} + +export const nodeQueue = new Queue( + async ([sceneNode, position]: [SceneNode, number]): Promise<[PenpotNode | undefined, number]> => [ + await transformSceneNode(sceneNode), + position + ], + 1 +); diff --git a/plugin-src/RemoteComponentLibrary.ts b/plugin-src/RemoteComponentLibrary.ts index fd537703..f7116fda 100644 --- a/plugin-src/RemoteComponentLibrary.ts +++ b/plugin-src/RemoteComponentLibrary.ts @@ -1,33 +1,27 @@ -class RemoteComponentsLibrary { - private components: Record = {}; - private queue: string[] = []; +import { PenpotNode } from '@ui/types'; - public register(id: string, component: ComponentNode | ComponentSetNode) { - if (!Object.prototype.hasOwnProperty.call(this.components, id)) { - this.queue.push(id); - } +class RemoteComponentsLibrary { + private components: PenpotNode[] = []; + private registry: Record = {}; - this.components[id] = component; + public add(component: PenpotNode) { + this.components.push(component); } - public get(id: string): ComponentNode | ComponentSetNode | undefined { - return this.components[id]; + public register(id: string) { + this.registry[id] = null; } - public next(): ComponentNode | ComponentSetNode { - const lastKey = this.queue.pop(); - - if (!lastKey) throw new Error('No components to pop'); - - return this.components[lastKey]; + public has(id: string): boolean { + return this.registry[id] === null; } - public remaining(): number { - return this.queue.length; + public total(): number { + return this.components.length; } - public total(): number { - return Object.keys(this.components).length; + public all(): PenpotNode[] { + return this.components; } } diff --git a/plugin-src/transformers/partials/transformChildren.ts b/plugin-src/transformers/partials/transformChildren.ts index 20bb5e5f..6013fea1 100644 --- a/plugin-src/transformers/partials/transformChildren.ts +++ b/plugin-src/transformers/partials/transformChildren.ts @@ -6,13 +6,13 @@ const nodeActsAsMask = (node: SceneNode): boolean => { return 'isMask' in node && node.isMask; }; -export const transformChildren = async (node: ChildrenMixin): Promise => { +export const transformChildren = (node: ChildrenMixin): Children => { const maskIndex = node.children.findIndex(nodeActsAsMask); const containsMask = maskIndex !== -1; return { children: containsMask - ? await translateMaskChildren(node.children, maskIndex) - : await translateChildren(node.children) + ? translateMaskChildren(node.children, maskIndex) + : translateChildren(node.children) }; }; diff --git a/plugin-src/transformers/transformBooleanNode.ts b/plugin-src/transformers/transformBooleanNode.ts index c31481f6..78401ae1 100644 --- a/plugin-src/transformers/transformBooleanNode.ts +++ b/plugin-src/transformers/transformBooleanNode.ts @@ -22,7 +22,7 @@ export const transformBooleanNode = async (node: BooleanOperationNode): Promise< name: node.name, boolType: translateBoolType(node.booleanOperation), ...transformFigmaIds(node), - ...(await transformChildren(node)), + ...transformChildren(node), ...transformFills(node), ...transformEffects(node), ...transformStrokes(node), diff --git a/plugin-src/transformers/transformComponentNode.ts b/plugin-src/transformers/transformComponentNode.ts index 311d872e..671dcfaa 100644 --- a/plugin-src/transformers/transformComponentNode.ts +++ b/plugin-src/transformers/transformComponentNode.ts @@ -18,7 +18,7 @@ import { import { ComponentRoot } from '@ui/types'; -export const transformComponentNode = async (node: ComponentNode): Promise => { +export const transformComponentNode = (node: ComponentNode): ComponentRoot => { componentsLibrary.register(node.id, { type: 'component', name: node.name, @@ -33,7 +33,7 @@ export const transformComponentNode = async (node: ComponentNode): Promise => { export const transformDocumentNode = async (node: DocumentNode): Promise => { const children = await processPages(node); - if (remoteComponentLibrary.remaining() > 0) { + await nodeQueue.waitIdle(); + + if (remoteComponentLibrary.total() > 0) { children.push({ name: 'External Components', - children: await translateRemoteChildren() + children: remoteComponentLibrary.all() }); } diff --git a/plugin-src/transformers/transformFrameNode.ts b/plugin-src/transformers/transformFrameNode.ts index 870a7d35..d6d9a489 100644 --- a/plugin-src/transformers/transformFrameNode.ts +++ b/plugin-src/transformers/transformFrameNode.ts @@ -23,9 +23,9 @@ const isSectionNode = (node: FrameNode | SectionNode | ComponentSetNode): node i return node.type === 'SECTION'; }; -export const transformFrameNode = async ( +export const transformFrameNode = ( node: FrameNode | SectionNode | ComponentSetNode -): Promise => { +): FrameShape => { let frameSpecificAttributes: Partial = {}; let referencePoint: Point = { x: node.absoluteTransform[0][2], y: node.absoluteTransform[1][2] }; @@ -60,7 +60,7 @@ export const transformFrameNode = async ( ...referencePoint, ...frameSpecificAttributes, ...transformDimension(node), - ...(await transformChildren(node)), + ...transformChildren(node), ...transformSceneNode(node), ...transformOverrides(node) }; diff --git a/plugin-src/transformers/transformGroupNode.ts b/plugin-src/transformers/transformGroupNode.ts index 471c7184..10be8a86 100644 --- a/plugin-src/transformers/transformGroupNode.ts +++ b/plugin-src/transformers/transformGroupNode.ts @@ -11,20 +11,20 @@ import { transformChildren } from '@plugin/transformers/partials'; import { GroupShape } from '@ui/lib/types/shapes/groupShape'; -export const transformGroupNode = async (node: GroupNode): Promise => { +export const transformGroupNode = (node: GroupNode): GroupShape => { return { ...transformFigmaIds(node), ...transformGroupNodeLike(node), ...transformEffects(node), ...transformBlend(node), - ...(await transformChildren(node)), + ...transformChildren(node), ...transformOverrides(node) }; }; export const transformGroupNodeLike = ( node: BaseNodeMixin & LayoutMixin & SceneNodeMixin -): GroupShape => { +): Omit => { return { type: 'group', name: node.name, diff --git a/plugin-src/transformers/transformInstanceNode.ts b/plugin-src/transformers/transformInstanceNode.ts index 2843d5c2..7ffa16fc 100644 --- a/plugin-src/transformers/transformInstanceNode.ts +++ b/plugin-src/transformers/transformInstanceNode.ts @@ -1,4 +1,5 @@ import { overridesLibrary } from '@plugin/OverridesLibrary'; +import { nodeQueue } from '@plugin/Queue'; import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary'; import { transformAutoLayout, @@ -64,7 +65,7 @@ export const transformInstanceNode = async ( ...transformRotationAndPosition(node), ...transformConstraints(node), ...transformAutoLayout(node), - ...(await transformChildren(node)), + ...transformChildren(node), ...transformOverrides(node) }; }; @@ -78,11 +79,15 @@ const getPrimaryComponent = (mainComponent: ComponentNode): ComponentNode | Comp }; const registerExternalComponents = (primaryComponent: ComponentNode | ComponentSetNode): void => { - if (remoteComponentLibrary.get(primaryComponent.id) !== undefined) { + if (remoteComponentLibrary.has(primaryComponent.id)) { return; } - remoteComponentLibrary.register(primaryComponent.id, primaryComponent); + remoteComponentLibrary.register(primaryComponent.id); + + nodeQueue.enqueue([primaryComponent, 0]).then(([penpotNode, _]) => { + if (penpotNode) remoteComponentLibrary.add(penpotNode); + }); }; const getComponentTextPropertyOverrides = ( diff --git a/plugin-src/transformers/transformPageNode.ts b/plugin-src/transformers/transformPageNode.ts index 3f658bcb..af6c1949 100644 --- a/plugin-src/transformers/transformPageNode.ts +++ b/plugin-src/transformers/transformPageNode.ts @@ -9,6 +9,6 @@ export const transformPageNode = async (node: PageNode): Promise => options: { background: node.backgrounds.length ? translatePageFill(node.backgrounds[0]) : undefined }, - children: await translateChildren(node.children) + children: translateChildren(node.children) }; }; diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index a4a7b90e..9a712ca5 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -54,7 +54,7 @@ export const transformSceneNode = async (node: SceneNode): Promise => { +): PenpotNode[] => { const maskChild = children[maskIndex]; - const unmaskedChildren = await translateChildren(children.slice(0, maskIndex)); - const maskedChildren = await translateChildren(children.slice(maskIndex)); + const unmaskedChildren = translateChildren(children.slice(0, maskIndex)); + const maskedChildren = translateChildren(children.slice(maskIndex)); if ( maskChild.type === 'STICKY' || @@ -48,47 +47,14 @@ export const translateMaskChildren = async ( return [...unmaskedChildren, maskGroup]; }; -export const translateChildren = async (children: readonly SceneNode[]): Promise => { +export const translateChildren = (children: readonly SceneNode[]): PenpotNode[] => { const transformedChildren: PenpotNode[] = []; + let count = 0; for (const child of children) { - const penpotNode = await transformSceneNode(child); - - if (penpotNode) transformedChildren.push(penpotNode); - - await sleep(0); - } - - return transformedChildren; -}; - -export const translateRemoteChildren = async (): Promise => { - const transformedChildren: PenpotNode[] = []; - let currentRemote = 1; - - figma.ui.postMessage({ - type: 'PROGRESS_STEP', - data: 'remote' - }); - - while (remoteComponentLibrary.remaining() > 0) { - figma.ui.postMessage({ - type: 'PROGRESS_TOTAL_ITEMS', - data: remoteComponentLibrary.total() - }); - - const child = remoteComponentLibrary.next(); - - const penpotNode = await transformSceneNode(child); - - if (penpotNode) transformedChildren.push(penpotNode); - - figma.ui.postMessage({ - type: 'PROGRESS_PROCESSED_ITEMS', - data: currentRemote++ + nodeQueue.enqueue([child, count++]).then(([penpotNode, position]) => { + if (penpotNode) transformedChildren[position] = penpotNode; }); - - await sleep(0); } return transformedChildren;