Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions app/figma-plugin/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@ interface AnalysisNode {
style?: Record<string, unknown>;
devStatus?: { type: string; description?: string };
isAsset?: boolean;

// Responsive / size constraints
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
layoutGrow?: 0 | 1;
constraints?: { horizontal: string; vertical: string };

// Wrap
layoutWrap?: string;
counterAxisSpacing?: number;
counterAxisAlignContent?: string;

// Grid layout (container)
gridRowCount?: number;
gridColumnCount?: number;
gridRowGap?: number;
gridColumnGap?: number;
gridColumnsSizing?: string;
gridRowsSizing?: string;

// Grid layout (child)
gridChildHorizontalAlign?: string;
gridChildVerticalAlign?: string;
gridRowSpan?: number;
gridColumnSpan?: number;
gridRowAnchorIndex?: number;
gridColumnAnchorIndex?: number;

// Overflow / clip
clipsContent?: boolean;
overflowDirection?: string;
Comment on lines +79 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file structure
git ls-files | grep -E "(figma-plugin|contracts)" | head -20

Repository: let-sunny/canicode

Length of output: 773


🏁 Script executed:

# Look for the main.ts file and read the problematic lines
if [ -f "app/figma-plugin/src/main.ts" ]; then
  wc -l app/figma-plugin/src/main.ts
fi

Repository: let-sunny/canicode

Length of output: 94


🏁 Script executed:

# Read the section around lines 79-110 of main.ts
if [ -f "app/figma-plugin/src/main.ts" ]; then
  sed -n '60,130p' app/figma-plugin/src/main.ts
fi

Repository: let-sunny/canicode

Length of output: 1749


🏁 Script executed:

# Look for the figma-node.ts contract file
find . -name "figma-node.ts" -o -name "*figma*" -type f | grep -E "\.(ts|tsx)$" | head -20

Repository: let-sunny/canicode

Length of output: 387


🏁 Script executed:

# Read the complete figma-node.ts contract file
cat -n src/core/contracts/figma-node.ts

Repository: let-sunny/canicode

Length of output: 7612


🏁 Script executed:

# Check for Zod usage and schema definitions in the codebase
rg "import.*zod" --type ts --type tsx | head -20

Repository: let-sunny/canicode

Length of output: 89


🏁 Script executed:

# Look for schema definitions related to figma-node
rg "const.*Schema|export.*z\." src/core/contracts/ -A 3 | head -50

Repository: let-sunny/canicode

Length of output: 2508


🏁 Script executed:

# Check the imports at the top of main.ts and understand the interface definition context
head -85 app/figma-plugin/src/main.ts | tail -30

Repository: let-sunny/canicode

Length of output: 956


🏁 Script executed:

# Get the full AnalysisNode interface definition from main.ts
sed -n '40,115p' app/figma-plugin/src/main.ts

Repository: let-sunny/canicode

Length of output: 1942


🏁 Script executed:

# Check imports in main.ts
head -30 app/figma-plugin/src/main.ts

Repository: let-sunny/canicode

Length of output: 830


🏁 Script executed:

# Search for any schema usage or validation in main.ts
rg "AnalysisNodeSchema|z\.|parse|validate" app/figma-plugin/src/main.ts

Repository: let-sunny/canicode

Length of output: 206


🏁 Script executed:

# Check if figma-node contract types are imported anywhere in main.ts
rg "from.*figma-node|import.*AnalysisNode" app/figma-plugin/src/main.ts

Repository: let-sunny/canicode

Length of output: 44


Import and reuse AnalysisNode type from the shared contract instead of redefining it locally.

The local interface in lines 40–115 claims to "match src/core/contracts/figma-node.ts" but actually widens multiple fields to bare string types: layoutWrap, counterAxisAlignContent, gridChildHorizontalAlign, gridChildVerticalAlign, overflowDirection, and the constraints object. The shared schema constrains these to specific enums and unions (LayoutWrap, GridChildAlign, OverflowDirection, LayoutConstraint, etc.).

This divergence means the plugin—which is the data producer—can emit values that violate the shared contract, defeating compile-time safety and creating maintenance burden. Remove this local interface and import the actual AnalysisNode type from src/core/contracts/figma-node.ts to ensure type alignment with the Zod-validated schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/figma-plugin/src/main.ts` around lines 79 - 110, Replace the locally
redefined AnalysisNode interface with the canonical type from the shared
contract: remove the local interface declaration and import AnalysisNode from
src/core/contracts/figma-node.ts so the plugin uses the same enums/unions
(LayoutWrap, GridChildAlign, OverflowDirection, LayoutConstraint, etc.) and
matches the Zod-validated schema; update any usages in main.ts to reference the
imported AnalysisNode type (and adjust imports) so fields like layoutWrap,
counterAxisAlignContent, gridChildHorizontalAlign, gridChildVerticalAlign,
overflowDirection and constraints use the constrained types instead of bare
string/object definitions.


children?: AnalysisNode[];
}

Expand All @@ -94,6 +128,24 @@ interface AnalysisFile {
>;
}

// ---- Plugin → REST API constraint enum conversion ----

const HORIZONTAL_CONSTRAINT_MAP: Record<string, string> = {
MIN: "LEFT",
CENTER: "CENTER",
MAX: "RIGHT",
STRETCH: "LEFT_RIGHT",
SCALE: "SCALE",
};

const VERTICAL_CONSTRAINT_MAP: Record<string, string> = {
MIN: "TOP",
CENTER: "CENTER",
MAX: "BOTTOM",
STRETCH: "TOP_BOTTOM",
SCALE: "SCALE",
};

// ---- Transform Figma Plugin nodes to AnalysisNode ----

function transformPluginNode(node: SceneNode): AnalysisNode {
Expand Down Expand Up @@ -137,6 +189,83 @@ function transformPluginNode(node: SceneNode): AnalysisNode {
}
}

// Responsive / size constraints
if (hasAutoLayout(node)) {
if ("minWidth" in node && typeof node.minWidth === "number") {
result.minWidth = node.minWidth;
}
if ("maxWidth" in node && typeof node.maxWidth === "number") {
result.maxWidth = node.maxWidth;
}
if ("minHeight" in node && typeof node.minHeight === "number") {
result.minHeight = node.minHeight;
}
if ("maxHeight" in node && typeof node.maxHeight === "number") {
result.maxHeight = node.maxHeight;
}
if ("layoutGrow" in node) {
result.layoutGrow = (node as FrameNode).layoutGrow as 0 | 1;
}

// Wrap
if ("layoutWrap" in node && node.layoutWrap) {
result.layoutWrap = node.layoutWrap;
}
if ("counterAxisSpacing" in node && typeof node.counterAxisSpacing === "number") {
result.counterAxisSpacing = node.counterAxisSpacing;
}
if ("counterAxisAlignContent" in node) {
result.counterAxisAlignContent = (node as FrameNode).counterAxisAlignContent;
}

// Grid layout (container)
if (node.layoutMode === "GRID") {
if ("gridRowCount" in node) result.gridRowCount = node.gridRowCount as number;
if ("gridColumnCount" in node) result.gridColumnCount = node.gridColumnCount as number;
if ("gridRowGap" in node) result.gridRowGap = node.gridRowGap as number;
if ("gridColumnGap" in node) result.gridColumnGap = node.gridColumnGap as number;
if ("gridColumnsSizing" in node) result.gridColumnsSizing = node.gridColumnsSizing as string;
if ("gridRowsSizing" in node) result.gridRowsSizing = node.gridRowsSizing as string;
}
}

// Overflow / clip (applies to all container types, not just auto-layout)
if ("clipsContent" in node && typeof (node as FrameNode).clipsContent === "boolean") {
result.clipsContent = (node as FrameNode).clipsContent;
}
if ("overflowDirection" in node && (node as FrameNode).overflowDirection) {
result.overflowDirection = (node as FrameNode).overflowDirection;
}

// Constraints (Plugin API MIN/MAX/STRETCH → REST API LEFT/RIGHT/LEFT_RIGHT)
if ("constraints" in node && (node as FrameNode).constraints) {
const c = (node as FrameNode).constraints;
result.constraints = {
horizontal: HORIZONTAL_CONSTRAINT_MAP[c.horizontal] ?? c.horizontal,
vertical: VERTICAL_CONSTRAINT_MAP[c.vertical] ?? c.vertical,
};
}

// Grid child properties (on any child of a grid parent)
if ("gridChildHorizontalAlign" in node) {
result.gridChildHorizontalAlign = (node as unknown as { gridChildHorizontalAlign: string }).gridChildHorizontalAlign;
}
if ("gridChildVerticalAlign" in node) {
result.gridChildVerticalAlign = (node as unknown as { gridChildVerticalAlign: string }).gridChildVerticalAlign;
}
if ("gridRowSpan" in node) {
result.gridRowSpan = (node as unknown as { gridRowSpan: number }).gridRowSpan;
}
if ("gridColumnSpan" in node) {
result.gridColumnSpan = (node as unknown as { gridColumnSpan: number }).gridColumnSpan;
}
if ("gridRowAnchorIndex" in node) {
result.gridRowAnchorIndex = (node as unknown as { gridRowAnchorIndex: number }).gridRowAnchorIndex;
}
if ("gridColumnAnchorIndex" in node) {
result.gridColumnAnchorIndex = (node as unknown as { gridColumnAnchorIndex: number }).gridColumnAnchorIndex;
}

// layoutPositioning (for children in auto-layout)
if ("layoutPositioning" in node) {
const lp = (node as FrameNode).layoutPositioning;
Expand Down
150 changes: 150 additions & 0 deletions src/core/adapters/figma-transformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { transformFigmaResponse } from "./figma-transformer.js";
import type { GetFileResponse } from "@figma/rest-api-spec";

function makeFigmaNode(overrides: Record<string, unknown>) {
return {
id: "0:1",
name: "Test",
type: "FRAME",
...overrides,
};
}

function makeFigmaResponse(document: Record<string, unknown>): GetFileResponse {
return {
name: "TestFile",
lastModified: "2024-01-01",
version: "1",
document: document as GetFileResponse["document"],
components: {},
styles: {},
schemaVersion: 0,
role: "owner",
thumbnailUrl: "",
editorType: "figma",
} as GetFileResponse;
}

describe("figma-transformer responsive fields", () => {
it("maps minWidth/maxWidth/minHeight/maxHeight", () => {
const response = makeFigmaResponse(
makeFigmaNode({
minWidth: 100,
maxWidth: 800,
minHeight: 50,
maxHeight: 600,
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.minWidth).toBe(100);
expect(result.document.maxWidth).toBe(800);
expect(result.document.minHeight).toBe(50);
expect(result.document.maxHeight).toBe(600);
});

it("maps layoutGrow", () => {
const response = makeFigmaResponse(
makeFigmaNode({ layoutGrow: 1 }),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.layoutGrow).toBe(1);
});

it("maps constraints", () => {
const response = makeFigmaResponse(
makeFigmaNode({
constraints: { horizontal: "LEFT_RIGHT", vertical: "TOP_BOTTOM" },
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.constraints).toEqual({
horizontal: "LEFT_RIGHT",
vertical: "TOP_BOTTOM",
});
});

it("maps layoutWrap and counterAxisSpacing", () => {
const response = makeFigmaResponse(
makeFigmaNode({
layoutWrap: "WRAP",
counterAxisSpacing: 16,
counterAxisAlignContent: "SPACE_BETWEEN",
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.layoutWrap).toBe("WRAP");
expect(result.document.counterAxisSpacing).toBe(16);
expect(result.document.counterAxisAlignContent).toBe("SPACE_BETWEEN");
});

it("maps clipsContent and overflowDirection", () => {
const response = makeFigmaResponse(
makeFigmaNode({
clipsContent: true,
overflowDirection: "VERTICAL_SCROLLING",
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.clipsContent).toBe(true);
expect(result.document.overflowDirection).toBe("VERTICAL_SCROLLING");
});

it("maps layoutMode GRID", () => {
const response = makeFigmaResponse(
makeFigmaNode({ layoutMode: "GRID" }),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.layoutMode).toBe("GRID");
});

it("maps grid container fields", () => {
const response = makeFigmaResponse(
makeFigmaNode({
layoutMode: "GRID",
gridRowCount: 3,
gridColumnCount: 4,
gridRowGap: 8,
gridColumnGap: 16,
gridColumnsSizing: "1fr 1fr 1fr 1fr",
gridRowsSizing: "auto auto auto",
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.gridRowCount).toBe(3);
expect(result.document.gridColumnCount).toBe(4);
expect(result.document.gridRowGap).toBe(8);
expect(result.document.gridColumnGap).toBe(16);
expect(result.document.gridColumnsSizing).toBe("1fr 1fr 1fr 1fr");
expect(result.document.gridRowsSizing).toBe("auto auto auto");
});

it("maps grid child fields", () => {
const response = makeFigmaResponse(
makeFigmaNode({
gridChildHorizontalAlign: "CENTER",
gridChildVerticalAlign: "MAX",
gridRowSpan: 2,
gridColumnSpan: 3,
gridRowAnchorIndex: 0,
gridColumnAnchorIndex: 1,
}),
);
const result = transformFigmaResponse("test-key", response);
expect(result.document.gridChildHorizontalAlign).toBe("CENTER");
expect(result.document.gridChildVerticalAlign).toBe("MAX");
expect(result.document.gridRowSpan).toBe(2);
expect(result.document.gridColumnSpan).toBe(3);
expect(result.document.gridRowAnchorIndex).toBe(0);
expect(result.document.gridColumnAnchorIndex).toBe(1);
});

it("does not set fields when absent", () => {
const response = makeFigmaResponse(makeFigmaNode({}));
const result = transformFigmaResponse("test-key", response);
expect(result.document.minWidth).toBeUndefined();
expect(result.document.maxWidth).toBeUndefined();
expect(result.document.layoutWrap).toBeUndefined();
expect(result.document.clipsContent).toBeUndefined();
expect(result.document.constraints).toBeUndefined();
});
});
83 changes: 83 additions & 0 deletions src/core/adapters/figma-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,89 @@ function transformNode(node: Node): AnalysisNode {
base.paddingBottom = node.paddingBottom as number;
}

// Size constraints (responsive)
if ("minWidth" in node && typeof node.minWidth === "number") {
base.minWidth = node.minWidth;
}
if ("maxWidth" in node && typeof node.maxWidth === "number") {
base.maxWidth = node.maxWidth;
}
if ("minHeight" in node && typeof node.minHeight === "number") {
base.minHeight = node.minHeight;
}
if ("maxHeight" in node && typeof node.maxHeight === "number") {
base.maxHeight = node.maxHeight;
}
if ("layoutGrow" in node && node.layoutGrow !== undefined) {
base.layoutGrow = node.layoutGrow as 0 | 1;
}
if ("constraints" in node && node.constraints) {
base.constraints = node.constraints as AnalysisNode["constraints"];
}

// Wrap (flex-wrap)
if ("layoutWrap" in node && node.layoutWrap) {
base.layoutWrap = node.layoutWrap as AnalysisNode["layoutWrap"];
}
if ("counterAxisSpacing" in node && typeof node.counterAxisSpacing === "number") {
base.counterAxisSpacing = node.counterAxisSpacing;
}
if ("counterAxisAlignContent" in node && node.counterAxisAlignContent) {
base.counterAxisAlignContent =
node.counterAxisAlignContent as AnalysisNode["counterAxisAlignContent"];
}

// Grid layout (container)
if ("gridRowCount" in node && typeof node.gridRowCount === "number") {
base.gridRowCount = node.gridRowCount;
}
if ("gridColumnCount" in node && typeof node.gridColumnCount === "number") {
base.gridColumnCount = node.gridColumnCount;
}
if ("gridRowGap" in node && typeof node.gridRowGap === "number") {
base.gridRowGap = node.gridRowGap;
}
if ("gridColumnGap" in node && typeof node.gridColumnGap === "number") {
base.gridColumnGap = node.gridColumnGap;
}
if ("gridColumnsSizing" in node && typeof node.gridColumnsSizing === "string") {
base.gridColumnsSizing = node.gridColumnsSizing;
}
if ("gridRowsSizing" in node && typeof node.gridRowsSizing === "string") {
base.gridRowsSizing = node.gridRowsSizing;
}

// Grid layout (child)
if ("gridChildHorizontalAlign" in node && node.gridChildHorizontalAlign) {
base.gridChildHorizontalAlign =
node.gridChildHorizontalAlign as AnalysisNode["gridChildHorizontalAlign"];
}
if ("gridChildVerticalAlign" in node && node.gridChildVerticalAlign) {
base.gridChildVerticalAlign =
node.gridChildVerticalAlign as AnalysisNode["gridChildVerticalAlign"];
}
if ("gridRowSpan" in node && typeof node.gridRowSpan === "number") {
base.gridRowSpan = node.gridRowSpan;
}
if ("gridColumnSpan" in node && typeof node.gridColumnSpan === "number") {
base.gridColumnSpan = node.gridColumnSpan;
}
if ("gridRowAnchorIndex" in node && typeof node.gridRowAnchorIndex === "number") {
base.gridRowAnchorIndex = node.gridRowAnchorIndex;
}
if ("gridColumnAnchorIndex" in node && typeof node.gridColumnAnchorIndex === "number") {
base.gridColumnAnchorIndex = node.gridColumnAnchorIndex;
}

// Overflow / clip
if ("clipsContent" in node && typeof node.clipsContent === "boolean") {
base.clipsContent = node.clipsContent;
}
if ("overflowDirection" in node && node.overflowDirection) {
base.overflowDirection =
node.overflowDirection as AnalysisNode["overflowDirection"];
}

// Size/position
if ("absoluteBoundingBox" in node && node.absoluteBoundingBox) {
base.absoluteBoundingBox = node.absoluteBoundingBox;
Expand Down
Loading
Loading