Skip to content

Commit

Permalink
draft of figma converter
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasfrancisco committed Aug 16, 2024
1 parent bc266d5 commit 96f2f6c
Show file tree
Hide file tree
Showing 34 changed files with 21,438 additions and 78 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"db:stop": "pnpx supabase stop"
},
"dependencies": {
"@ds-project/types": "workspace:*",
"@ds-project/components": "workspace:*",
"@hookform/resolvers": "^3.9.0",
"@octokit/app": "^15.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async function searchFileSha({
path: string[];
index?: number;
}) {
// TODO: Explore recursive approach instead https://arc.net/l/quote/wmwrxkfr
const { data: treeData } = await installation.request(
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}',
{
Expand Down
26 changes: 26 additions & 0 deletions apps/dashboard/src/app/(dashboard)/tokens/converter/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Text } from '@ds-project/components';
import { JsonBlock, MainContent } from '@/components';
import { fetchTokens } from '../_actions';

export default async function Tokens() {
const figmaVariables = await fetchTokens();

if (!figmaVariables) {
return (
<Text>
<p>Error fetching variables</p>
</Text>
);
}

return (
<MainContent
description="Compare the extraction from Figma Variables to the conversion to Design Tokens"
title="Design Tokens"
>
<div className="flex flex-col gap-4">
<JsonBlock src={figmaVariables} />
</div>
</MainContent>
);
}
29 changes: 29 additions & 0 deletions apps/dashboard/src/app/api/figma/variables/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { NextRequest } from 'next/server';
import { eq } from 'drizzle-orm';
import { database } from '@/lib/drizzle';
import { insertResourcesSchema, resourcesTable } from '@/lib/drizzle/schema';
import { isAuthenticated } from '@/lib/supabase/server/utils/is-authenticated';

export async function POST(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return new Response('Not authenticated', { status: 401 });
}

try {
const validatedData = insertResourcesSchema
.pick({ designTokens: true, projectId: true })
.parse(await request.json());

// Update database
await database
.update(resourcesTable)
.set({
designTokens: validatedData.designTokens,
})
.where(eq(resourcesTable.projectId, validatedData.projectId));
} catch (error) {
return new Response(JSON.stringify(error), { status: 400 });
}

return new Response('OK', { status: 200 });
}
3 changes: 2 additions & 1 deletion packages/figma-plugin/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
extends: [
'@repo/eslint-config/react.js',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@figma/figma-plugins/recommended',
],
globals: {
Expand Down
2 changes: 2 additions & 0 deletions packages/figma-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"dev": "concurrently -n main,ui \"npm run main -- --watch\" \"npm run ui -- --watch\""
},
"dependencies": {
"@ds-project/types": "workspace:*",
"@apollo/client": "^3.7.11",
"@ds-project/components": "workspace:*",
"@octokit/core": "^6.1.2",
Expand All @@ -33,6 +34,7 @@
"@types/object-hash": "^3.0.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@vitejs/plugin-react": "^3.1.0",
"concurrently": "^8.0.1",
"esbuild": "^0.17.15",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { extractLocalVariableCollections } from './extractors';

export async function extractVariables(figma: PluginAPI) {
const variableCollections = await extractLocalVariableCollections(figma);

return variableCollections;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './local-variable-collections';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { extractVariableCollection } from './variable-collection';

export async function extractLocalVariableCollections(figma: PluginAPI) {
const collections = await figma.variables.getLocalVariableCollectionsAsync();

return await Promise.all(collections.map(extractVariableCollection(figma)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FigmaExtractedVariableCollection } from '@ds-project/types/src/types/figma';
import { nonNullable } from '../utils/non-nullable';
import { extractVariable } from './variable';

export const extractVariableCollection =
(figma: PluginAPI) =>
async (
variableCollection: VariableCollection
): Promise<FigmaExtractedVariableCollection> => {
const variables = (
await Promise.all(
variableCollection.variableIds.map(extractVariable(figma))
)
).filter(nonNullable);

return {
defaultModeId: variableCollection.defaultModeId,
hiddenFromPublishing: variableCollection.hiddenFromPublishing,
id: variableCollection.id,
key: variableCollection.key,
modes: variableCollection.modes,
name: variableCollection.name,
remote: variableCollection.remote,
variableIds: variableCollection.variableIds,
variables,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FigmaExtractedVariable } from '@ds-project/types/src/types/figma';

export const extractVariable =
(figma: PluginAPI) =>
async (variableId: string): Promise<FigmaExtractedVariable | undefined> => {
const variable = await figma.variables.getVariableByIdAsync(variableId);

if (!variable) {
return undefined;
}

const publishStatus = await variable?.getPublishStatusAsync();

return {
publishStatus: publishStatus,
description: variable.description,
hiddenFromPublishing: variable.hiddenFromPublishing,
name: variable.name,
resolvedType: variable.resolvedType,
valuesByMode: variable.valuesByMode,
id: variable.id,
key: variable.key,
remote: variable.remote,
scopes: variable.scopes,
variableCollectionId: variable.variableCollectionId,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function nonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
7 changes: 4 additions & 3 deletions packages/figma-plugin/src/plugin/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { convertFigmaVariablesToDesignTokens } from '@ds-project/types';
import { AsyncMessage } from '../message';
import { AsyncMessageTypes } from '../message.types';
import type { Credentials } from '../types/credentials';
import { config } from '../ui/config';
import { extractVariables } from './extract-variables/extract-variables';
import { storage } from './storage';
import { getFigmaVariables } from './variables/utils/get-figma-variables';

figma.showUI(__html__, {
themeColors: true,
Expand Down Expand Up @@ -50,10 +51,10 @@ AsyncMessage.plugin.handle(AsyncMessageTypes.DeleteCredentials, async () => {
});

AsyncMessage.plugin.handle(AsyncMessageTypes.GetDesignTokens, async () => {
const styleDictionary = await getFigmaVariables();
const variables = await extractVariables(figma);

return {
designTokens: styleDictionary,
designTokens: convertFigmaVariablesToDesignTokens(variables),
};
});

Expand Down
5 changes: 4 additions & 1 deletion packages/figma-plugin/src/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AsyncMessage } from '../message';
import { useDSApi } from './modules/providers/ds-api-provider';
import { LinkDesignSystem } from './modules/link-design-system';
import { useAuth } from './modules/providers/auth-provider';
import { config } from './config';

function App() {
const { login, logout, state } = useAuth();
Expand All @@ -21,7 +22,9 @@ function App() {
type: AsyncMessageTypes.GetDesignTokens,
})
.then(({ designTokens }) => {
void updateDesignTokens(designTokens);
if (config.features.shouldUpdateTokens) {
void updateDesignTokens(designTokens);
}
})
.catch((error) => {
// eslint-disable-next-line no-console -- TODO: replace with monitoring
Expand Down
5 changes: 5 additions & 0 deletions packages/figma-plugin/src/ui/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const featureFlags = {
shouldUpdateTokens: true,
} as Record<string, boolean>;

export const config = {
AUTH_API_HOST: 'https://localhost:3000',
READ_INTERVAL: 5 * 1000, // 5 seconds
CREDENTIALS_KEY: 'ds-project-credentials',
PROJECT_ID_KEY: 'ds-project-id',
features: featureFlags,
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function DSApiProvider({ children }: { children: React.ReactNode }) {

const updateDesignTokens = useCallback(
async (designTokens: DesignTokens) => {
const response = await apiFetch('/api/figma/design-tokens', {
const response = await apiFetch('/api/figma/variables', {
method: 'POST',
body: JSON.stringify({
projectId: linkedProjectId,
Expand Down
15 changes: 0 additions & 15 deletions packages/figma-plugin/style-dictionary/config.json

This file was deleted.

20 changes: 0 additions & 20 deletions packages/figma-plugin/style-dictionary/tokens/color.json

This file was deleted.

23 changes: 23 additions & 0 deletions packages/figma-to-design-tokens/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@ds-project/types",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"private": true,
"scripts": {
"build": "tsup src/index.ts",
"test": "tsx src/test.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@figma/plugin-typings": "^1.98.0",
"style-dictionary": "^4.0.1",
"tsup": "^8.2.4",
"tsx": "^4.17.0"
},
"dependencies": {
"color2k": "^2.0.3"
}
}
40 changes: 40 additions & 0 deletions packages/figma-to-design-tokens/src/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DesignToken, DesignTokens } from 'style-dictionary/types';
import { FigmaExtractedVariableCollection } from './types/figma';
import { extractVariable } from './extractors/extract-variable';
import { notImplemented } from './filters/not-implemented';

export function convertFigmaVariablesToDesignTokens(
variableCollections: Array<FigmaExtractedVariableCollection>
): DesignTokens {
return {
$type: 'figma-design-tokens',
collections: variableCollections.flatMap<DesignTokens>((collection) => ({
$type: 'collection',
$description: 'Figma Variable Collections',
attributes: {
figma: {
id: collection.id,
key: collection.key,
name: collection.name,
hiddenFromPublishing: collection.hiddenFromPublishing,
modes: collection.modes,
},
},
...collection.modes.reduce(
(collectionModeVariables, mode) => ({
...collectionModeVariables,
[mode.name]: {
...collection.variables.filter(notImplemented).reduce<DesignToken>(
(variables, variable) => ({
...variables,
[variable.name]: extractVariable(variable, mode.modeId),
}),
{}
),
},
}),
{}
),
})),
};
}
19 changes: 19 additions & 0 deletions packages/figma-to-design-tokens/src/extractors/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DesignToken } from 'style-dictionary/types';
import { toHex } from 'color2k';
import { FigmaExtractedVariable } from '../types';

export function extractBoolean(
value: FigmaExtractedVariable['valuesByMode'][string]
): DesignToken['$value'] {
if (
typeof value === 'string' ||
typeof value === 'number' ||
(typeof value === 'object' && ('b' in value || 'id' in value))
) {
// string | number | RGB | RGBA | VariableAlias
// We should not receive these types under boolean. If that happens, something went wrong during the extraction from Figma.
throw new Error('Unexpected boolean type');
}

throw new Error('Boolean extraction is not yet implemented');
}
Loading

0 comments on commit 96f2f6c

Please sign in to comment.