diff --git a/packages/graph-editor/src/components/flow/nodes/typeConverterNode.tsx b/packages/graph-editor/src/components/flow/nodes/typeConverterNode.tsx
new file mode 100644
index 00000000..52e8399e
--- /dev/null
+++ b/packages/graph-editor/src/components/flow/nodes/typeConverterNode.tsx
@@ -0,0 +1,89 @@
+import { Handle } from '../handles.js';
+import { HandleContainer } from '../handles.js';
+import { Stack, Text } from '@tokens-studio/ui';
+import { extractType, extractTypeIcon } from '../wrapper/nodeV2.js';
+import { icons } from '@/registry/icon.js';
+import { useLocalGraph } from '@/context/graph.js';
+import { useSelector } from 'react-redux';
+import React from 'react';
+
+export const TypeConverterNode = (args) => {
+ const { id } = args;
+ const graph = useLocalGraph();
+ const node = graph.getNode(id);
+ const iconTypeRegistry = useSelector(icons);
+
+ if (!node) return null;
+
+ const inputPort = node.inputs.value;
+ const outputPort = node.outputs.value;
+ const inputTypeCol = extractTypeIcon(inputPort, iconTypeRegistry);
+ const outputTypeCol = extractTypeIcon(outputPort, iconTypeRegistry);
+ const inputType = extractType(inputPort.type);
+ const outputType = extractType(outputPort.type);
+
+ // Get conversion description from node annotations
+ const conversionDescription =
+ (node.annotations['conversion.description'] as string) || 'TYPE';
+
+ return (
+
+
+
+
+
+
+
+
+ {conversionDescription}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/graph-editor/src/editor/actions/connect.ts b/packages/graph-editor/src/editor/actions/connect.ts
index 73c37415..86dcd1fd 100644
--- a/packages/graph-editor/src/editor/actions/connect.ts
+++ b/packages/graph-editor/src/editor/actions/connect.ts
@@ -1,6 +1,7 @@
import { Connection, Edge } from 'reactflow';
import { Dispatch } from '@/redux/store.js';
-import { Graph } from '@tokens-studio/graph-engine';
+import { Graph, canConvertSchemaTypesExtended } from '@tokens-studio/graph-engine';
+import { TYPE_CONVERTER } from '@/ids.js';
import { deletable } from '@/annotations/index.js';
import { getVariadicIndex, stripVariadic } from '@/utils/stripVariadic.js';
@@ -9,10 +10,14 @@ export const connectNodes =
graph,
setEdges,
dispatch,
+ nodeLookup,
+ reactFlowInstance,
}: {
graph: Graph;
setEdges: React.Dispatch>;
dispatch: Dispatch;
+ nodeLookup: Record;
+ reactFlowInstance: any;
}) =>
(params: Connection) => {
//Create the connection in the underlying graph
@@ -70,6 +75,107 @@ export const connectNodes =
graph.removeEdge(targetPort._edges[0].id);
}
+ // Check if types are compatible and need conversion
+ const sourceType = sourcePort.type;
+ const targetType = targetPort.type;
+ const needsConversion = sourceType.$id !== targetType.$id &&
+ canConvertSchemaTypesExtended(sourceType, targetType) &&
+ sourceType.$id !== 'https://schemas.tokens.studio/any.json' &&
+ targetType.$id !== 'https://schemas.tokens.studio/any.json';
+
+ if (needsConversion) {
+ // Create a type converter node
+ const TypeConverterClass = nodeLookup[TYPE_CONVERTER];
+ if (TypeConverterClass) {
+ const converterNode = new TypeConverterClass({ graph });
+
+ // Set the conversion types
+ converterNode.setConversionTypes(sourceType, targetType);
+
+ // Calculate position between source and target
+ const sourcePosition = reactFlowInstance?.getNode(params.source!)?.position || { x: 0, y: 0 };
+ const targetPosition = reactFlowInstance?.getNode(params.target!)?.position || { x: 100, y: 0 };
+ const converterPosition = {
+ x: (sourcePosition.x + targetPosition.x) / 2,
+ y: (sourcePosition.y + targetPosition.y) / 2
+ };
+
+ // Add the converter node to the graph
+ graph.addNode(converterNode);
+
+ // Add the converter node to React Flow
+ const converterFlowNode = {
+ id: converterNode.id,
+ type: TYPE_CONVERTER,
+ position: converterPosition,
+ data: {},
+ };
+
+ reactFlowInstance?.addNodes(converterFlowNode);
+
+ // Connect source to converter
+ const sourceToConverterEdge = graph.connect(
+ sourceNode,
+ sourcePort,
+ converterNode,
+ converterNode.inputs.value
+ );
+
+ // Connect converter to target
+ const converterToTargetEdge = graph.connect(
+ converterNode,
+ converterNode.outputs.value,
+ targetNode,
+ targetPort
+ );
+
+ // Create React Flow edges
+ const sourceToConverterFlowEdge = {
+ id: sourceToConverterEdge.id,
+ source: params.source!,
+ sourceHandle: params.sourceHandle!,
+ target: converterNode.id,
+ targetHandle: 'value',
+ type: 'custom',
+ };
+
+ const converterToTargetFlowEdge = {
+ id: converterToTargetEdge.id,
+ source: converterNode.id,
+ sourceHandle: 'value',
+ target: params.target!,
+ targetHandle: params.targetHandle!,
+ type: 'custom',
+ };
+
+ dispatch.graph.appendLog({
+ time: new Date(),
+ type: 'info',
+ data: {
+ msg: `Type converter inserted between ${sourceType.$id} and ${targetType.$id}`,
+ },
+ });
+
+ return setEdges((eds) => {
+ const newEdgs = eds.reduce(
+ (acc, edge) => {
+ //All our inputs take a single input only, disconnect if we have a connection already
+ if (
+ edge.targetHandle == params.targetHandle &&
+ edge.target === params.target
+ ) {
+ return acc;
+ }
+ acc.push(edge);
+ return acc;
+ },
+ [sourceToConverterFlowEdge, converterToTargetFlowEdge] as Edge[],
+ );
+ return newEdgs;
+ });
+ }
+ }
+
const newGraphEdge = graph.connect(
sourceNode,
sourcePort,
diff --git a/packages/graph-editor/src/editor/graph.tsx b/packages/graph-editor/src/editor/graph.tsx
index 0f7b738e..f58bafc0 100644
--- a/packages/graph-editor/src/editor/graph.tsx
+++ b/packages/graph-editor/src/editor/graph.tsx
@@ -49,12 +49,13 @@ import { GraphContextProvider } from '@/context/graph.js';
import { GraphEditorProps, ImperativeEditorRef } from './editorTypes.js';
import { GraphToolbar } from '@/components/toolbar/index.js';
import { HotKeys } from '@/components/hotKeys/index.js';
-import { NOTE, PASSTHROUGH } from '@/ids.js';
+import { NOTE, PASSTHROUGH, TYPE_CONVERTER } from '@/ids.js';
import { NodeContextMenu } from '../components/contextMenus/nodeContextMenu.js';
import { NodeV2 } from '@/components/index.js';
import { PaneContextMenu } from '../components/contextMenus/paneContextMenu.js';
import { PassthroughNode } from '@/components/flow/nodes/passthroughNode.js';
import { SelectionContextMenu } from '@/components/contextMenus/selectionContextMenu.js';
+import { TypeConverterNode } from '@/components/flow/nodes/typeConverterNode.js';
import {
capabilitiesSelector,
nodeTypesSelector,
@@ -373,6 +374,7 @@ export const EditorApp = React.forwardRef<
...customNodeUI,
GenericNode: NodeV2,
[PASSTHROUGH]: PassthroughNode,
+ [TYPE_CONVERTER]: TypeConverterNode,
[EditorNodeTypes.GROUP]: groupNode,
[NOTE]: noteNode,
});
@@ -530,8 +532,15 @@ export const EditorApp = React.forwardRef<
}, []);
const onConnect = useMemo(
- () => connectNodes({ graph, setEdges, dispatch }),
- [dispatch, graph, setEdges],
+ () =>
+ connectNodes({
+ graph,
+ setEdges,
+ dispatch,
+ nodeLookup: fullNodeLookup,
+ reactFlowInstance,
+ }),
+ [dispatch, graph, setEdges, fullNodeLookup, reactFlowInstance],
);
const onNodeDragStop = useCallback(
diff --git a/packages/graph-editor/src/ids.ts b/packages/graph-editor/src/ids.ts
index 3ace9833..0643e70b 100644
--- a/packages/graph-editor/src/ids.ts
+++ b/packages/graph-editor/src/ids.ts
@@ -1,4 +1,5 @@
export const INPUT = 'studio.tokens.generic.input';
export const OUTPUT = 'studio.tokens.generic.output';
export const PASSTHROUGH = 'studio.tokens.generic.passthrough';
+export const TYPE_CONVERTER = 'studio.tokens.generic.typeConverter';
export const NOTE = 'studio.tokens.generic.note';
diff --git a/packages/graph-engine/src/nodes/generic/index.ts b/packages/graph-engine/src/nodes/generic/index.ts
index 978149db..1bf88069 100644
--- a/packages/graph-engine/src/nodes/generic/index.ts
+++ b/packages/graph-engine/src/nodes/generic/index.ts
@@ -10,6 +10,7 @@ import panic from './panic.js';
import passthrough from './passthrough.js';
import subgraph from './subgraph.js';
import time from './time.js';
+import typeConverter from './typeConverter.js';
export const nodes = [
constant,
@@ -23,5 +24,6 @@ export const nodes = [
objectPath,
objectMerge,
time,
- delay
+ delay,
+ typeConverter
];
diff --git a/packages/graph-engine/src/nodes/generic/typeConverter.ts b/packages/graph-engine/src/nodes/generic/typeConverter.ts
new file mode 100644
index 00000000..ab08b741
--- /dev/null
+++ b/packages/graph-engine/src/nodes/generic/typeConverter.ts
@@ -0,0 +1,72 @@
+import {
+ AnySchema,
+ convertSchemaTypeExtended,
+ getConversionDescriptionExtended
+} from '../../schemas/index.js';
+import { INodeDefinition, ToInput, ToOutput } from '../../index.js';
+import { Node } from '../../programmatic/node.js';
+
+/**
+ * A node that converts between different types automatically.
+ * This node is typically inserted automatically when connecting incompatible but convertible types.
+ */
+export default class NodeDefinition extends Node {
+ static title = 'Type Converter';
+ static type = 'studio.tokens.generic.typeConverter';
+ static description = 'Automatically converts between compatible types';
+
+ declare inputs: ToInput<{
+ value: TInput;
+ }>;
+ declare outputs: ToOutput<{
+ value: TOutput;
+ }>;
+
+ private sourceType: any = AnySchema;
+ private targetType: any = AnySchema;
+
+ constructor(props: INodeDefinition) {
+ super(props);
+ this.addInput('value', {
+ type: AnySchema
+ });
+ this.addOutput('value', {
+ type: AnySchema
+ });
+ }
+
+ /**
+ * Sets the source and target types for this converter
+ */
+ setConversionTypes(sourceType: any, targetType: any) {
+ this.sourceType = sourceType;
+ this.targetType = targetType;
+
+ // Update the input and output types using the proper API
+ this.inputs.value.setType(sourceType);
+ this.outputs.value.setType(targetType);
+
+ // Store conversion info in annotations for UI display
+ this.annotations['conversion.source'] = sourceType.$id;
+ this.annotations['conversion.target'] = targetType.$id;
+ this.annotations['conversion.description'] =
+ getConversionDescriptionExtended(sourceType, targetType);
+ }
+
+ /**
+ * Gets the conversion description for display
+ */
+ getConversionDescription(): string {
+ return (this.annotations['conversion.description'] as string) || 'ANY→ANY';
+ }
+
+ execute(): void | Promise {
+ const input = this.inputs.value;
+ const convertedValue = convertSchemaTypeExtended(
+ this.sourceType,
+ this.targetType,
+ input.value
+ );
+ this.outputs.value.set(convertedValue, this.targetType);
+ }
+}
diff --git a/packages/graph-engine/src/schemas/index.ts b/packages/graph-engine/src/schemas/index.ts
index 59c45825..2ad14026 100644
--- a/packages/graph-engine/src/schemas/index.ts
+++ b/packages/graph-engine/src/schemas/index.ts
@@ -343,6 +343,7 @@ export const canConvertSchemaTypes = (
if (src.$id === target.$id) return true;
//Any can always accept anything
if (target.$id === ANY) return true;
+ if (src.$id === ANY) return true;
if (src.type == 'array' && target.$id == ANY_ARRAY) {
return true;
@@ -355,29 +356,149 @@ export const canConvertSchemaTypes = (
case NUMBER:
switch (target.$id) {
case STRING:
+ case BOOLEAN:
+ case COLOR:
+ case VEC2:
+ case VEC3:
return true;
+ }
+ break;
+ case STRING:
+ switch (target.$id) {
+ case NUMBER:
case BOOLEAN:
+ case COLOR:
+ case OBJECT:
+ case ANY_ARRAY:
return true;
}
break;
- case STRING: {
+ case BOOLEAN:
switch (target.$id) {
+ case STRING:
case NUMBER:
return true;
- case BOOLEAN:
+ }
+ break;
+ case COLOR:
+ switch (target.$id) {
+ case STRING:
+ case VEC3:
+ case NUMBER:
return true;
}
- }
+ break;
+ case VEC2:
+ switch (target.$id) {
+ case STRING:
+ case NUMBER:
+ case VEC3:
+ return true;
+ }
+ break;
+ case VEC3:
+ switch (target.$id) {
+ case STRING:
+ case NUMBER:
+ case VEC2:
+ case COLOR:
+ return true;
+ }
+ break;
+ case OBJECT:
+ switch (target.$id) {
+ case STRING:
+ return true;
+ }
+ break;
+ case ANY_ARRAY:
+ switch (target.$id) {
+ case STRING:
+ return true;
+ }
+ break;
+ case CURVE:
+ switch (target.$id) {
+ case STRING:
+ return true;
+ }
+ break;
+ case GRADIENT:
+ switch (target.$id) {
+ case STRING:
+ return true;
+ }
+ break;
+ case FLOATCURVE:
+ switch (target.$id) {
+ case STRING:
+ return true;
+ }
+ break;
}
return false;
};
+/**
+ * Helper function to parse color from string
+ */
+const parseColorFromString = (str: string) => {
+ try {
+ // Handle hex colors
+ if (str.startsWith('#')) {
+ const hex = str.slice(1);
+ if (hex.length === 3) {
+ const r = parseInt(hex[0] + hex[0], 16) / 255;
+ const g = parseInt(hex[1] + hex[1], 16) / 255;
+ const b = parseInt(hex[2] + hex[2], 16) / 255;
+ return { channels: [r, g, b], space: 'srgb' };
+ } else if (hex.length === 6) {
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
+ return { channels: [r, g, b], space: 'srgb' };
+ }
+ }
+ // Handle rgb() format
+ if (str.startsWith('rgb(')) {
+ const values = str
+ .slice(4, -1)
+ .split(',')
+ .map(v => parseFloat(v.trim()));
+ if (values.length >= 3) {
+ return {
+ channels: [values[0] / 255, values[1] / 255, values[2] / 255],
+ space: 'srgb'
+ };
+ }
+ }
+ // Fallback to black
+ return { channels: [0, 0, 0], space: 'srgb' };
+ } catch {
+ return { channels: [0, 0, 0], space: 'srgb' };
+ }
+};
+
+/**
+ * Helper function to convert color to hex string
+ */
+const colorToHex = (color: any) => {
+ if (!color || !color.channels || !Array.isArray(color.channels)) {
+ return '#000000';
+ }
+ const [r, g, b] = color.channels;
+ const toHex = (n: number) => {
+ const hex = Math.round(Math.max(0, Math.min(255, n * 255))).toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ };
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+};
+
/**
* Handles the actual conversion of a value from one schema to another
* @param srcSchema
* @param targetSchema
* @param src
- * @param target
* @returns
*/
export const convertSchemaType = (
@@ -392,18 +513,257 @@ export const convertSchemaType = (
return String(src);
case BOOLEAN:
return Boolean(src);
+ case COLOR:
+ return { channels: [src, src, src], space: 'srgb' };
+ case VEC2:
+ return [src, src];
+ case VEC3:
+ return [src, src, src];
}
break;
case STRING:
switch (targetSchema.$id) {
+ case NUMBER:
+ return parseFloat(src) || 0;
case BOOLEAN:
return Boolean(src);
+ case COLOR:
+ return parseColorFromString(src);
+ case OBJECT:
+ try {
+ return JSON.parse(src);
+ } catch {
+ return {};
+ }
+ case ANY_ARRAY:
+ try {
+ return JSON.parse(src);
+ } catch {
+ return [];
+ }
+ }
+ break;
+ case BOOLEAN:
+ switch (targetSchema.$id) {
+ case STRING:
+ return String(src);
+ case NUMBER:
+ return Number(src);
+ }
+ break;
+ case COLOR:
+ switch (targetSchema.$id) {
+ case STRING:
+ return colorToHex(src);
+ case VEC3:
+ return src?.channels || [0, 0, 0];
+ case NUMBER:
+ return src?.channels?.[0] || 0;
+ }
+ break;
+ case VEC2:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ case NUMBER:
+ return Array.isArray(src) ? src[0] || 0 : 0;
+ case VEC3:
+ return Array.isArray(src) ? [...src, 0] : [0, 0, 0];
+ }
+ break;
+ case VEC3:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ case NUMBER:
+ return Array.isArray(src) ? src[0] || 0 : 0;
+ case VEC2:
+ return Array.isArray(src) ? [src[0] || 0, src[1] || 0] : [0, 0];
+ case COLOR:
+ return {
+ channels: Array.isArray(src) ? src : [0, 0, 0],
+ space: 'srgb'
+ };
+ }
+ break;
+ case OBJECT:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ }
+ break;
+ case ANY_ARRAY:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ }
+ break;
+ case CURVE:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ }
+ break;
+ case GRADIENT:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
+ }
+ break;
+ case FLOATCURVE:
+ switch (targetSchema.$id) {
+ case STRING:
+ return JSON.stringify(src);
}
break;
}
return src;
};
+/**
+ * Gets a human-readable description of the conversion between two schema types
+ * @param srcSchema
+ * @param targetSchema
+ * @returns
+ */
+export const getConversionDescription = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject
+): string => {
+ const getShortName = (schema: SchemaObject): string => {
+ switch (schema.$id) {
+ case NUMBER:
+ return 'NUM';
+ case STRING:
+ return 'STR';
+ case BOOLEAN:
+ return 'BOOL';
+ case COLOR:
+ return 'COL';
+ case VEC2:
+ return 'V2';
+ case VEC3:
+ return 'V3';
+ case OBJECT:
+ return 'OBJ';
+ case ANY_ARRAY:
+ return 'ARR';
+ case CURVE:
+ return 'CRV';
+ case GRADIENT:
+ return 'GRAD';
+ case FLOATCURVE:
+ return 'FCRV';
+ case ANY:
+ return 'ANY';
+ default:
+ return 'UNK';
+ }
+ };
+
+ return `${getShortName(srcSchema)}→${getShortName(targetSchema)}`;
+};
+
+/**
+ * Type for conversion extension functions
+ */
+export type ConversionExtension = {
+ canConvertSchemaTypes?: (src: SchemaObject, target: SchemaObject) => boolean;
+ convertSchemaType?: (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject,
+ src: any
+ ) => any;
+ getConversionDescription?: (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject
+ ) => string;
+};
+
+/**
+ * Registry for conversion extensions
+ */
+const conversionExtensions: ConversionExtension[] = [];
+
+/**
+ * Register a conversion extension
+ */
+export const registerConversionExtension = (extension: ConversionExtension) => {
+ conversionExtensions.push(extension);
+};
+
+/**
+ * Extended canConvertSchemaTypes that checks extensions
+ */
+export const canConvertSchemaTypesExtended = (
+ src: SchemaObject,
+ target: SchemaObject
+): boolean => {
+ // Check base conversions first
+ if (canConvertSchemaTypes(src, target)) {
+ return true;
+ }
+
+ // Check extensions
+ for (const extension of conversionExtensions) {
+ if (extension.canConvertSchemaTypes?.(src, target)) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Extended convertSchemaType that uses extensions
+ */
+export const convertSchemaTypeExtended = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject,
+ src: any
+): any => {
+ // Try extensions first
+ for (const extension of conversionExtensions) {
+ if (extension.canConvertSchemaTypes?.(srcSchema, targetSchema)) {
+ const result = extension.convertSchemaType?.(
+ srcSchema,
+ targetSchema,
+ src
+ );
+ if (result !== undefined) {
+ return result;
+ }
+ }
+ }
+
+ // Fall back to base conversion
+ return convertSchemaType(srcSchema, targetSchema, src);
+};
+
+/**
+ * Extended getConversionDescription that uses extensions
+ */
+export const getConversionDescriptionExtended = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject
+): string => {
+ // Try extensions first
+ for (const extension of conversionExtensions) {
+ if (extension.canConvertSchemaTypes?.(srcSchema, targetSchema)) {
+ const result = extension.getConversionDescription?.(
+ srcSchema,
+ targetSchema
+ );
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ // Fall back to base description
+ return getConversionDescription(srcSchema, targetSchema);
+};
+
export type GraphSchema = SchemaObject;
export const AllSchemas = [
diff --git a/packages/graph-engine/tests/suites/connectionWithConversion.test.ts b/packages/graph-engine/tests/suites/connectionWithConversion.test.ts
new file mode 100644
index 00000000..28259f6f
--- /dev/null
+++ b/packages/graph-engine/tests/suites/connectionWithConversion.test.ts
@@ -0,0 +1,125 @@
+import { Graph } from '../../src/graph/graph.js';
+import {
+ NumberSchema,
+ StringSchema,
+ canConvertSchemaTypes
+} from '../../src/schemas/index.js';
+import { describe, expect, test } from 'vitest';
+import ConstantNode from '../../src/nodes/generic/constant.js';
+import TypeConverter from '../../src/nodes/generic/typeConverter.js';
+
+describe('Connection with Type Conversion', () => {
+ test('simulates the connection logic that would trigger automatic conversion', () => {
+ const graph = new Graph();
+
+ // Create source node (number constant)
+ const sourceNode = new ConstantNode({ graph });
+ sourceNode.inputs.value.setValue(42);
+ sourceNode.inputs.value.setType(NumberSchema);
+ sourceNode.outputs.value.setType(NumberSchema);
+
+ // Create target node (string constant)
+ const targetNode = new ConstantNode({ graph });
+ targetNode.inputs.value.setType(StringSchema);
+ targetNode.outputs.value.setType(StringSchema);
+
+ // Simulate the connection logic check
+ const sourceType = sourceNode.outputs.value.type;
+ const targetType = targetNode.inputs.value.type;
+ const needsConversion =
+ sourceType.$id !== targetType.$id &&
+ canConvertSchemaTypes(sourceType, targetType) &&
+ sourceType.$id !== 'https://schemas.tokens.studio/any.json' &&
+ targetType.$id !== 'https://schemas.tokens.studio/any.json';
+
+ // Verify that conversion would be needed
+ expect(needsConversion).toBe(true);
+ expect(sourceType.$id).toBe('https://schemas.tokens.studio/number.json');
+ expect(targetType.$id).toBe('https://schemas.tokens.studio/string.json');
+ });
+
+ test('verifies that same types do not trigger conversion', () => {
+ const graph = new Graph();
+
+ // Create two nodes with the same type
+ const sourceNode = new ConstantNode({ graph });
+ sourceNode.outputs.value.setType(NumberSchema);
+
+ const targetNode = new ConstantNode({ graph });
+ targetNode.inputs.value.setType(NumberSchema);
+
+ // Simulate the connection logic check
+ const sourceType = sourceNode.outputs.value.type;
+ const targetType = targetNode.inputs.value.type;
+ const needsConversion =
+ sourceType.$id !== targetType.$id &&
+ canConvertSchemaTypes(sourceType, targetType) &&
+ sourceType.$id !== 'https://schemas.tokens.studio/any.json' &&
+ targetType.$id !== 'https://schemas.tokens.studio/any.json';
+
+ // Verify that conversion would NOT be needed
+ expect(needsConversion).toBe(false);
+ expect(sourceType.$id).toBe(targetType.$id);
+ });
+
+ test('verifies converter node positioning logic', () => {
+ // Simulate React Flow node positions
+ const sourcePosition = { x: 100, y: 100 };
+ const targetPosition = { x: 300, y: 200 };
+
+ // Calculate converter position (should be in the middle)
+ const converterPosition = {
+ x: (sourcePosition.x + targetPosition.x) / 2,
+ y: (sourcePosition.y + targetPosition.y) / 2
+ };
+
+ expect(converterPosition.x).toBe(200);
+ expect(converterPosition.y).toBe(150);
+ });
+
+ test('verifies full conversion chain execution', async () => {
+ const graph = new Graph();
+
+ // Create source node
+ const sourceNode = new ConstantNode({ graph });
+ sourceNode.inputs.value.setValue(123);
+ sourceNode.inputs.value.setType(NumberSchema);
+ sourceNode.outputs.value.setType(NumberSchema);
+
+ // Create converter
+ const converter = new TypeConverter({ graph });
+ converter.setConversionTypes(NumberSchema, StringSchema);
+
+ // Create target node
+ const targetNode = new ConstantNode({ graph });
+ targetNode.inputs.value.setType(StringSchema);
+ targetNode.outputs.value.setType(StringSchema);
+
+ // Connect the chain: source -> converter -> target
+ graph.connect(
+ sourceNode,
+ sourceNode.outputs.value,
+ converter,
+ converter.inputs.value
+ );
+ graph.connect(
+ converter,
+ converter.outputs.value,
+ targetNode,
+ targetNode.inputs.value
+ );
+
+ // Execute the chain
+ await sourceNode.execute();
+ await converter.execute();
+ await targetNode.execute();
+
+ // Verify the conversion worked
+ expect(sourceNode.outputs.value.value).toBe(123);
+ expect(converter.outputs.value.value).toBe('123');
+ expect(targetNode.outputs.value.value).toBe('123');
+
+ // Verify the converter shows the right description
+ expect(converter.getConversionDescription()).toBe('NUM→STR');
+ });
+});
diff --git a/packages/graph-engine/tests/suites/typeConverter.test.ts b/packages/graph-engine/tests/suites/typeConverter.test.ts
new file mode 100644
index 00000000..c9786d6a
--- /dev/null
+++ b/packages/graph-engine/tests/suites/typeConverter.test.ts
@@ -0,0 +1,39 @@
+import { Graph } from '../../src/graph/graph.js';
+import {
+ NumberSchema,
+ StringSchema,
+ canConvertSchemaTypes,
+ convertSchemaType
+} from '../../src/schemas/index.js';
+import { describe, expect, test } from 'vitest';
+import TypeConverter from '../../src/nodes/generic/typeConverter.js';
+
+describe('Type Converter', () => {
+ test('can convert number to string', () => {
+ const result = convertSchemaType(NumberSchema, StringSchema, 42);
+ expect(result).toBe('42');
+ });
+
+ test('can check type compatibility', () => {
+ expect(canConvertSchemaTypes(NumberSchema, StringSchema)).toBe(true);
+ expect(canConvertSchemaTypes(StringSchema, NumberSchema)).toBe(true);
+ });
+
+ test('type converter node works correctly', async () => {
+ const graph = new Graph();
+ const converter = new TypeConverter({ graph });
+
+ // Set up conversion from number to string
+ converter.setConversionTypes(NumberSchema, StringSchema);
+
+ // Set input value
+ converter.inputs.value.setValue(123);
+
+ // Execute the node
+ await converter.execute();
+
+ // Check output
+ expect(converter.outputs.value.value).toBe('123');
+ expect(converter.getConversionDescription()).toBe('NUM→STR');
+ });
+});
diff --git a/packages/nodes-design-tokens/src/extensions/typeConversions.ts b/packages/nodes-design-tokens/src/extensions/typeConversions.ts
new file mode 100644
index 00000000..86fefff8
--- /dev/null
+++ b/packages/nodes-design-tokens/src/extensions/typeConversions.ts
@@ -0,0 +1,122 @@
+import {
+ SchemaObject,
+ canConvertSchemaTypes as baseCanConvertSchemaTypes,
+ convertSchemaType as baseConvertSchemaType,
+ getConversionDescription as baseGetConversionDescription,
+ registerConversionExtension
+} from '@tokens-studio/graph-engine';
+import { TokenSchema, TokenSetSchema } from '../schemas/index.js';
+import { arrayOf } from '../schemas/utils.js';
+import { flatTokensRestoreToMap, flatten } from '../utils/index.js';
+import type { DeepKeyTokenMap, SingleToken } from '@tokens-studio/types';
+
+/**
+ * Extended conversion description generator for design tokens
+ */
+export const getConversionDescription = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject
+): string => {
+ const srcIsTokenArray = srcSchema.type === 'array' && srcSchema.items?.$id === TokenSchema.$id;
+ const targetIsTokenArray = targetSchema.type === 'array' && targetSchema.items?.$id === TokenSchema.$id;
+
+ // Design token specific descriptions
+ if (srcIsTokenArray && targetSchema.$id === TokenSetSchema.$id) {
+ return 'TOK[]→SET';
+ }
+ if (srcSchema.$id === TokenSetSchema.$id && targetIsTokenArray) {
+ return 'SET→TOK[]';
+ }
+ if (srcSchema.$id === TokenSchema.$id && targetIsTokenArray) {
+ return 'TOK→TOK[]';
+ }
+
+ // Fall back to base descriptions
+ return baseGetConversionDescription(srcSchema, targetSchema);
+};
+
+/**
+ * Extended type conversion checker that includes design token conversions
+ */
+export const canConvertSchemaTypes = (
+ src: SchemaObject,
+ target: SchemaObject
+): boolean => {
+ // Add design token specific conversions
+ const srcIsTokenArray = src.type === 'array' && src.items?.$id === TokenSchema.$id;
+ const targetIsTokenArray = target.type === 'array' && target.items?.$id === TokenSchema.$id;
+
+ // Array of tokens to token set
+ if (srcIsTokenArray && target.$id === TokenSetSchema.$id) {
+ return true;
+ }
+
+ // Token set to array of tokens
+ if (src.$id === TokenSetSchema.$id && targetIsTokenArray) {
+ return true;
+ }
+
+ // Single token to array of tokens
+ if (src.$id === TokenSchema.$id && targetIsTokenArray) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Extended type converter that includes design token conversions
+ */
+export const convertSchemaType = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject,
+ src: any
+): any => {
+ // Handle design token specific conversions
+ const srcIsTokenArray = srcSchema.type === 'array' && srcSchema.items?.$id === TokenSchema.$id;
+ const targetIsTokenArray = targetSchema.type === 'array' && targetSchema.items?.$id === TokenSchema.$id;
+
+ // Array of tokens to token set
+ if (srcIsTokenArray && targetSchema.$id === TokenSetSchema.$id) {
+ try {
+ // Cast SingleToken[] to IResolvedToken[] as done in existing nodes
+ return flatTokensRestoreToMap(src as any);
+ } catch (error) {
+ console.warn('Failed to convert token array to token set:', error);
+ return {};
+ }
+ }
+
+ // Token set to array of tokens
+ if (srcSchema.$id === TokenSetSchema.$id && targetIsTokenArray) {
+ try {
+ // Use flatten to convert token set to array, then cast to SingleToken[]
+ return flatten(src as DeepKeyTokenMap) as any;
+ } catch (error) {
+ console.warn('Failed to convert token set to token array:', error);
+ return [];
+ }
+ }
+
+ // Single token to array of tokens
+ if (srcSchema.$id === TokenSchema.$id && targetIsTokenArray) {
+ return [src];
+ }
+
+ // Return undefined to indicate no conversion was performed
+ return undefined;
+};
+
+/**
+ * Register the design token conversion extensions
+ */
+export const registerDesignTokenConversions = () => {
+ registerConversionExtension({
+ canConvertSchemaTypes,
+ convertSchemaType,
+ getConversionDescription
+ });
+};
+
+// Auto-register when this module is imported
+registerDesignTokenConversions();
diff --git a/packages/nodes-design-tokens/src/index.ts b/packages/nodes-design-tokens/src/index.ts
index a77b9975..677efb82 100644
--- a/packages/nodes-design-tokens/src/index.ts
+++ b/packages/nodes-design-tokens/src/index.ts
@@ -2,3 +2,6 @@ export * from './nodes/index.js';
export * from './schemas/index.js';
export * from './ui/index.js';
export * from './utils/index.js';
+
+// Import extensions to register them
+import './extensions/typeConversions.js';
diff --git a/packages/nodes-design-tokens/src/schemas/conversions.ts b/packages/nodes-design-tokens/src/schemas/conversions.ts
new file mode 100644
index 00000000..1ec4c0e3
--- /dev/null
+++ b/packages/nodes-design-tokens/src/schemas/conversions.ts
@@ -0,0 +1,101 @@
+import {
+ SchemaObject,
+ canConvertSchemaTypes as baseCanConvertSchemaTypes,
+ convertSchemaType as baseConvertSchemaType
+} from '@tokens-studio/graph-engine';
+import { TokenSchema, TokenSetSchema } from './index.js';
+import { arrayOf } from './utils.js';
+import { flatTokensRestoreToMap } from '../utils/index.js';
+import type { SingleToken } from '@tokens-studio/types';
+
+// Define the token array schema
+export const TOKEN_ARRAY = 'https://schemas.tokens.studio/tokenArray.json';
+export const TokenArraySchema: SchemaObject = arrayOf(TokenSchema);
+
+/**
+ * Extended type conversion checker that includes design token conversions
+ */
+export const canConvertSchemaTypes = (
+ src: SchemaObject,
+ target: SchemaObject
+): boolean => {
+ // First check the base conversions
+ if (baseCanConvertSchemaTypes(src, target)) {
+ return true;
+ }
+
+ // Add design token specific conversions
+ switch (src.$id || (src.type === 'array' && src.items?.$id)) {
+ case TokenSchema.$id:
+ // Single token to array of tokens
+ if (target.type === 'array' && target.items?.$id === TokenSchema.$id) {
+ return true;
+ }
+ break;
+ case TokenSchema.$id: // When src is array of tokens
+ if (src.type === 'array' && src.items?.$id === TokenSchema.$id) {
+ switch (target.$id) {
+ case TokenSetSchema.$id:
+ return true; // Array of tokens to token set
+ }
+ }
+ break;
+ case TokenSetSchema.$id:
+ // Token set to array of tokens
+ if (target.type === 'array' && target.items?.$id === TokenSchema.$id) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+};
+
+/**
+ * Extended type converter that includes design token conversions
+ */
+export const convertSchemaType = (
+ srcSchema: SchemaObject,
+ targetSchema: SchemaObject,
+ src: any
+): any => {
+ // First try the base conversions
+ const baseResult = baseConvertSchemaType(srcSchema, targetSchema, src);
+ if (baseResult !== src) {
+ return baseResult; // Base conversion handled it
+ }
+
+ // Handle design token specific conversions
+ const srcIsTokenArray = srcSchema.type === 'array' && srcSchema.items?.$id === TokenSchema.$id;
+ const targetIsTokenArray = targetSchema.type === 'array' && targetSchema.items?.$id === TokenSchema.$id;
+
+ // Array of tokens to token set
+ if (srcIsTokenArray && targetSchema.$id === TokenSetSchema.$id) {
+ try {
+ // Cast SingleToken[] to IResolvedToken[] as done in existing nodes
+ return flatTokensRestoreToMap(src as any);
+ } catch (error) {
+ console.warn('Failed to convert token array to token set:', error);
+ return {};
+ }
+ }
+
+ // Token set to array of tokens
+ if (srcSchema.$id === TokenSetSchema.$id && targetIsTokenArray) {
+ try {
+ // This is just a placeholder - the real implementation is in extensions/typeConversions.ts
+ return [];
+ } catch (error) {
+ console.warn('Failed to convert token set to token array:', error);
+ return [];
+ }
+ }
+
+ // Single token to array of tokens
+ if (srcSchema.$id === TokenSchema.$id && targetIsTokenArray) {
+ return [src];
+ }
+
+ // No conversion available, return original
+ return src;
+};
diff --git a/packages/nodes-design-tokens/tests/extensions/typeConversions.test.ts b/packages/nodes-design-tokens/tests/extensions/typeConversions.test.ts
new file mode 100644
index 00000000..56417582
--- /dev/null
+++ b/packages/nodes-design-tokens/tests/extensions/typeConversions.test.ts
@@ -0,0 +1,79 @@
+import '../../src/extensions/typeConversions.js';
+import { Graph, canConvertSchemaTypesExtended, convertSchemaTypeExtended, getConversionDescriptionExtended } from '@tokens-studio/graph-engine';
+import { TokenSchema, TokenSetSchema } from '../../src/schemas/index.js';
+import { arrayOf } from '../../src/schemas/utils.js';
+import { beforeAll, describe, expect, test } from 'vitest'; // Import to register extensions
+
+describe('Design Token Type Conversions', () => {
+ const TokenArraySchema = arrayOf(TokenSchema);
+
+ test('can convert token array to token set', () => {
+ expect(canConvertSchemaTypesExtended(TokenArraySchema, TokenSetSchema)).toBe(true);
+ });
+
+ test('can convert token set to token array', () => {
+ expect(canConvertSchemaTypesExtended(TokenSetSchema, TokenArraySchema)).toBe(true);
+ });
+
+ test('can convert single token to token array', () => {
+ expect(canConvertSchemaTypesExtended(TokenSchema, TokenArraySchema)).toBe(true);
+ });
+
+ test('converts token array to token set correctly', () => {
+ const tokenArray = [
+ {
+ name: 'colors.primary',
+ value: '#ff0000',
+ type: 'color'
+ },
+ {
+ name: 'colors.secondary',
+ value: '#00ff00',
+ type: 'color'
+ }
+ ];
+
+ const result = convertSchemaTypeExtended(TokenArraySchema, TokenSetSchema, tokenArray);
+
+ expect(result).toEqual({
+ colors: {
+ primary: {
+ value: '#ff0000',
+ type: 'color'
+ },
+ secondary: {
+ value: '#00ff00',
+ type: 'color'
+ }
+ }
+ });
+ });
+
+ test('converts single token to token array correctly', () => {
+ const token = {
+ name: 'colors.primary',
+ value: '#ff0000',
+ type: 'color'
+ };
+
+ const result = convertSchemaTypeExtended(TokenSchema, TokenArraySchema, token);
+
+ expect(result).toEqual([token]);
+ });
+
+ test('provides correct conversion descriptions', () => {
+ expect(getConversionDescriptionExtended(TokenArraySchema, TokenSetSchema)).toBe('TOK[]→SET');
+ expect(getConversionDescriptionExtended(TokenSetSchema, TokenArraySchema)).toBe('SET→TOK[]');
+ expect(getConversionDescriptionExtended(TokenSchema, TokenArraySchema)).toBe('TOK→TOK[]');
+ });
+
+ test('handles conversion errors gracefully', () => {
+ // Test with invalid token array
+ const invalidTokenArray = [{ invalid: 'data' }];
+
+ const result = convertSchemaTypeExtended(TokenArraySchema, TokenSetSchema, invalidTokenArray);
+
+ // Should return empty object on error
+ expect(result).toEqual({});
+ });
+});
diff --git a/packages/nodes-design-tokens/tests/integration/automaticConversion.test.ts b/packages/nodes-design-tokens/tests/integration/automaticConversion.test.ts
new file mode 100644
index 00000000..5cb4c500
--- /dev/null
+++ b/packages/nodes-design-tokens/tests/integration/automaticConversion.test.ts
@@ -0,0 +1,78 @@
+import '../../src/extensions/typeConversions.js';
+import { Graph, nodeLookup } from '@tokens-studio/graph-engine';
+import { TokenSchema, TokenSetSchema } from '../../src/schemas/index.js';
+import { arrayOf } from '../../src/schemas/utils.js';
+import { describe, expect, test } from 'vitest'; // Register extensions
+
+const TypeConverter = nodeLookup['studio.tokens.generic.typeConverter'];
+
+describe('Automatic Type Conversion Integration', () => {
+ test('type converter can handle token array to token set conversion', async () => {
+ const graph = new Graph();
+
+ // Create a type converter for token array to token set
+ const converter = new TypeConverter({ graph });
+ const TokenArraySchema = arrayOf(TokenSchema);
+ converter.setConversionTypes(TokenArraySchema, TokenSetSchema);
+
+ // Set input token array
+ const tokenArray = [
+ {
+ name: 'colors.primary',
+ value: '#ff0000',
+ type: 'color'
+ },
+ {
+ name: 'spacing.small',
+ value: '8px',
+ type: 'dimension'
+ }
+ ];
+
+ converter.inputs.value.setValue(tokenArray);
+
+ // Execute conversion
+ await converter.execute();
+
+ // Verify output
+ const result = converter.outputs.value.value;
+ expect(result).toEqual({
+ colors: {
+ primary: {
+ value: '#ff0000',
+ type: 'color'
+ }
+ },
+ spacing: {
+ small: {
+ value: '8px',
+ type: 'dimension'
+ }
+ }
+ });
+
+ // Verify conversion description
+ expect(converter.getConversionDescription()).toBe('TOK[]→SET');
+ });
+
+ test('single token to array conversion works', async () => {
+ const graph = new Graph();
+
+ const converter = new TypeConverter({ graph });
+ const TokenArraySchema = arrayOf(TokenSchema);
+ converter.setConversionTypes(TokenSchema, TokenArraySchema);
+
+ const singleToken = {
+ name: 'colors.primary',
+ value: '#ff0000',
+ type: 'color'
+ };
+
+ converter.inputs.value.setValue(singleToken);
+ await converter.execute();
+
+ const result = converter.outputs.value.value;
+ expect(result).toEqual([singleToken]);
+ expect(converter.getConversionDescription()).toBe('TOK→TOK[]');
+ });
+});
diff --git a/packages/nodes-design-tokens/tests/integration/endToEndConversion.test.ts b/packages/nodes-design-tokens/tests/integration/endToEndConversion.test.ts
new file mode 100644
index 00000000..c7b0b3ab
--- /dev/null
+++ b/packages/nodes-design-tokens/tests/integration/endToEndConversion.test.ts
@@ -0,0 +1,180 @@
+import '../../src/extensions/typeConversions.js';
+import { Graph, canConvertSchemaTypesExtended, convertSchemaTypeExtended, nodeLookup } from '@tokens-studio/graph-engine';
+import { TokenSchema, TokenSetSchema } from '../../src/schemas/index.js';
+import { arrayOf } from '../../src/schemas/utils.js';
+import { describe, expect, test } from 'vitest'; // Register extensions
+
+const TypeConverter = nodeLookup['studio.tokens.generic.typeConverter'];
+
+describe('End-to-End Type Conversion', () => {
+ test('complete workflow: token set → array → token set with type converters', async () => {
+ const graph = new Graph();
+
+ // Original token set
+ const originalTokenSet = {
+ colors: {
+ primary: {
+ value: '#ff0000',
+ type: 'color'
+ },
+ secondary: {
+ value: '#00ff00',
+ type: 'color'
+ }
+ },
+ spacing: {
+ small: {
+ value: '8px',
+ type: 'dimension'
+ },
+ medium: {
+ value: '16px',
+ type: 'dimension'
+ }
+ }
+ };
+
+ // Step 1: Convert token set to array using type converter
+ const setToArrayConverter = new TypeConverter({ graph });
+ const TokenArraySchema = arrayOf(TokenSchema);
+ setToArrayConverter.setConversionTypes(TokenSetSchema, TokenArraySchema);
+
+ setToArrayConverter.inputs.value.setValue(originalTokenSet);
+ await setToArrayConverter.execute();
+
+ const tokenArray = setToArrayConverter.outputs.value.value;
+
+ // Verify array conversion
+ expect(Array.isArray(tokenArray)).toBe(true);
+ expect(tokenArray.length).toBe(4); // 2 colors + 2 spacing tokens
+ expect(setToArrayConverter.getConversionDescription()).toBe('SET→TOK[]');
+
+ // Verify token names are correct
+ const tokenNames = tokenArray.map(token => token.name);
+ expect(tokenNames).toContain('colors.primary');
+ expect(tokenNames).toContain('colors.secondary');
+ expect(tokenNames).toContain('spacing.small');
+ expect(tokenNames).toContain('spacing.medium');
+
+ // Step 2: Convert array back to token set using type converter
+ const arrayToSetConverter = new TypeConverter({ graph });
+ arrayToSetConverter.setConversionTypes(TokenArraySchema, TokenSetSchema);
+
+ arrayToSetConverter.inputs.value.setValue(tokenArray);
+ await arrayToSetConverter.execute();
+
+ const finalTokenSet = arrayToSetConverter.outputs.value.value;
+
+ // Verify round-trip conversion
+ expect(finalTokenSet).toEqual(originalTokenSet);
+ expect(arrayToSetConverter.getConversionDescription()).toBe('TOK[]→SET');
+ });
+
+ test('type compatibility checking works correctly', () => {
+ const TokenArraySchema = arrayOf(TokenSchema);
+
+ // These should be compatible
+ expect(canConvertSchemaTypesExtended(TokenArraySchema, TokenSetSchema)).toBe(true);
+ expect(canConvertSchemaTypesExtended(TokenSetSchema, TokenArraySchema)).toBe(true);
+ expect(canConvertSchemaTypesExtended(TokenSchema, TokenArraySchema)).toBe(true);
+
+ // Same types should be compatible (but won't need conversion)
+ expect(canConvertSchemaTypesExtended(TokenSchema, TokenSchema)).toBe(true);
+ expect(canConvertSchemaTypesExtended(TokenSetSchema, TokenSetSchema)).toBe(true);
+ expect(canConvertSchemaTypesExtended(TokenArraySchema, TokenArraySchema)).toBe(true);
+ });
+
+ test('direct conversion functions work correctly', () => {
+ const TokenArraySchema = arrayOf(TokenSchema);
+
+ // Test token array to token set
+ const tokenArray = [
+ { name: 'colors.primary', value: '#ff0000', type: 'color' },
+ { name: 'spacing.small', value: '8px', type: 'dimension' }
+ ];
+
+ const tokenSet = convertSchemaTypeExtended(TokenArraySchema, TokenSetSchema, tokenArray);
+
+ expect(tokenSet).toEqual({
+ colors: {
+ primary: {
+ value: '#ff0000',
+ type: 'color'
+ }
+ },
+ spacing: {
+ small: {
+ value: '8px',
+ type: 'dimension'
+ }
+ }
+ });
+
+ // Test token set to token array
+ const backToArray = convertSchemaTypeExtended(TokenSetSchema, TokenArraySchema, tokenSet);
+
+ expect(Array.isArray(backToArray)).toBe(true);
+ expect(backToArray.length).toBe(2);
+
+ const names = backToArray.map(token => token.name);
+ expect(names).toContain('colors.primary');
+ expect(names).toContain('spacing.small');
+ });
+
+ test('single token to array conversion', () => {
+ const TokenArraySchema = arrayOf(TokenSchema);
+ const singleToken = { name: 'colors.primary', value: '#ff0000', type: 'color' };
+
+ const result = convertSchemaTypeExtended(TokenSchema, TokenArraySchema, singleToken);
+
+ expect(result).toEqual([singleToken]);
+ });
+
+ test('handles complex nested token structures', async () => {
+ const graph = new Graph();
+ const converter = new TypeConverter({ graph });
+ const TokenArraySchema = arrayOf(TokenSchema);
+
+ // Complex nested structure
+ const complexTokenSet = {
+ design: {
+ system: {
+ colors: {
+ brand: {
+ primary: {
+ value: '#007bff',
+ type: 'color'
+ },
+ secondary: {
+ value: '#6c757d',
+ type: 'color'
+ }
+ }
+ },
+ typography: {
+ heading: {
+ large: {
+ value: '32px',
+ type: 'dimension'
+ }
+ }
+ }
+ }
+ }
+ };
+
+ converter.setConversionTypes(TokenSetSchema, TokenArraySchema);
+ converter.inputs.value.setValue(complexTokenSet);
+ await converter.execute();
+
+ const result = converter.outputs.value.value;
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(3);
+
+ const tokenNames = result.map(token => token.name);
+ expect(tokenNames).toContain('design.system.colors.brand.primary');
+ expect(tokenNames).toContain('design.system.colors.brand.secondary');
+ expect(tokenNames).toContain('design.system.typography.heading.large');
+ });
+});