@auto-engineer/design-system-importer 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auto-engineer/design-system-importer",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -18,7 +18,8 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@auto-engineer/message-bus": "^0.0.3"
21
+ "figma-api": "2.0.2-beta",
22
+ "@auto-engineer/message-bus": "^0.1.0"
22
23
  },
23
24
  "scripts": {
24
25
  "start": "tsx src/index.ts",
@@ -0,0 +1,207 @@
1
+ import * as dotenv from 'dotenv';
2
+ import * as Figma from 'figma-api';
3
+
4
+ dotenv.config();
5
+
6
+ const figmaApi = new Figma.Api({
7
+ personalAccessToken: process.env.FIGMA_PERSONAL_TOKEN as string,
8
+ });
9
+
10
+ export interface FigmaComponent {
11
+ name: string;
12
+ description: string;
13
+ thumbnail: string;
14
+ }
15
+
16
+ export type FilterFunctionType = (components: FigmaComponent[]) => FigmaComponent[];
17
+
18
+ interface FigmaNode {
19
+ type: string;
20
+ name: string;
21
+ children: FigmaNode[];
22
+ description: string;
23
+ thumbnail_url: string;
24
+ }
25
+
26
+ export class FigmaComponentsBuilder {
27
+ components: FigmaComponent[] = [];
28
+
29
+ async withFigmaComponents() {
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32
+ const response = await figmaApi.getFileComponents({ file_key: process.env.FIGMA_FILE_ID });
33
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
34
+ if (response.meta.components.length > 0) {
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
36
+ console.log(JSON.stringify(response.meta.components.length, null, 2));
37
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
38
+ this.components = response.meta.components.map(
39
+ (component: { name: string; description: string; thumbnail_url: string }) => ({
40
+ name: component.name,
41
+ description: component.description,
42
+ thumbnail: component.thumbnail_url,
43
+ }),
44
+ );
45
+ }
46
+ } catch (e) {
47
+ console.error(e);
48
+ }
49
+ return this;
50
+ }
51
+
52
+ async withFigmaComponentSets() {
53
+ try {
54
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
55
+ const response = await figmaApi.getFileComponentSets({ file_key: process.env.FIGMA_FILE_ID });
56
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
57
+ if (response.meta.component_sets.length > 0) {
58
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
59
+ this.components = response.meta.component_sets.map(
60
+ (component: { name: string; description: string; thumbnail_url: string }) => ({
61
+ name: component.name,
62
+ description: component.description ?? 'N/A',
63
+ thumbnail: component.thumbnail_url ?? 'N/A',
64
+ }),
65
+ );
66
+ }
67
+ } catch (e) {
68
+ console.error(e);
69
+ }
70
+ return this;
71
+ }
72
+
73
+ // extractBracketedComponents(item: FigmaComponent): FigmaComponent | null {
74
+ // const match = item.name.match(/<([^>]+)>/);
75
+ // return match ? { ...item, name: match[1].trim() } : null;
76
+ // }
77
+ //
78
+ // withFilteredNamesForMui() {
79
+ // // eslint-disable-next-line @typescript-eslint/unbound-method
80
+ // this.components = this.components.map(this.extractBracketedComponents).filter(Boolean) as FigmaComponent[];
81
+ // }
82
+
83
+ withFilteredNamesForShadcn(): void {
84
+ this.components = this.components
85
+ .map((comp: FigmaComponent): FigmaComponent | null => {
86
+ if (!comp?.name) return null;
87
+
88
+ let str = comp.name.trim();
89
+
90
+ if (str.includes('/')) {
91
+ str = str.split('/')[0].trim();
92
+ } else {
93
+ str = str.split(' ')[0].trim();
94
+ }
95
+
96
+ if (str.length > 0) {
97
+ str = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
98
+ } else {
99
+ return null;
100
+ }
101
+
102
+ return {
103
+ ...comp,
104
+ name: str.toLowerCase(),
105
+ };
106
+ })
107
+ .filter((c: FigmaComponent | null): c is FigmaComponent => Boolean(c));
108
+ }
109
+
110
+ withFilter(filter: FilterFunctionType): void {
111
+ try {
112
+ this.components = filter(this.components);
113
+ } catch (e) {
114
+ console.error('Error applying custom filter:', e);
115
+ }
116
+ }
117
+
118
+ async withAllFigmaInstanceNames() {
119
+ try {
120
+ /// ----
121
+ const usedComponentMap = new Map<string, { name: string; description: string; thumbnail: string }>();
122
+
123
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
124
+ const { document } = await figmaApi.getFile({
125
+ file_key: process.env.FIGMA_FILE_ID,
126
+ depth: 1,
127
+ });
128
+
129
+ function walkTree(node: FigmaNode) {
130
+ if (node.type === 'INSTANCE' && Boolean(node.name)) {
131
+ if (!usedComponentMap.has(node.name)) {
132
+ usedComponentMap.set(node.name, {
133
+ name: node.name,
134
+ description: node.description ?? '',
135
+ thumbnail: node.thumbnail_url ?? '',
136
+ });
137
+ }
138
+ }
139
+
140
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
141
+ if (node.children) {
142
+ for (const child of node.children) {
143
+ walkTree(child);
144
+ }
145
+ }
146
+ }
147
+
148
+ walkTree(document);
149
+
150
+ this.components = Array.from(usedComponentMap.values());
151
+
152
+ /// ----
153
+
154
+ // const components = []
155
+ // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
156
+ // const { meta } = await figmaApi.getFileComponentSets({ file_key: process.env.FIGMA_FILE_ID });
157
+ // // console.log(response);
158
+ //
159
+ // console.log('Component sets:', meta.component_sets); // ⬅️ Check this!
160
+ //
161
+ // const componentSetIds = meta.component_sets.map(componentSet => componentSet.node_id); // This must return valid Figma node IDs
162
+ //
163
+ // console.log('ComponentSet IDs:', componentSetIds);
164
+ //
165
+ // const fileNodes = await figmaApi.getFileNodes({
166
+ // file_key: process.env.FIGMA_FILE_ID,
167
+ // }, {
168
+ // ids: componentSetIds.join(','),
169
+ // });
170
+ //
171
+ // for (const node of Object.values(fileNodes.nodes)) {
172
+ // const componentSet = node.document;
173
+ //
174
+ // if (componentSet.type === 'COMPONENT_SET' && componentSet.children?.length) {
175
+ // const variants = componentSet.children;
176
+ // const firstVariant = variants[0]; // or apply filtering logic
177
+ //
178
+ // components.push({
179
+ // name: componentSet.name, // e.g. "Button"
180
+ // id: firstVariant.id, // this is the actual variant node
181
+ // variantName: firstVariant.name // e.g. "primary=true, size=md"
182
+ // });
183
+ // }
184
+ // }
185
+ //
186
+ // fs.writeFileSync('output.json', JSON.stringify(components, null, 2));
187
+
188
+ // if (Boolean(response)) {
189
+ // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
190
+ // this.components = response.meta.component_sets.map(
191
+ // (component: { name: string; description: string; thumbnail_url: string }) => ({
192
+ // name: component.name,
193
+ // description: component.description,
194
+ // thumbnail_url: component.thumbnail_url,
195
+ // }),
196
+ // );
197
+ // }
198
+ } catch (e) {
199
+ console.error(e);
200
+ }
201
+ return this;
202
+ }
203
+
204
+ build() {
205
+ return this.components;
206
+ }
207
+ }
@@ -1,12 +1,15 @@
1
1
  import { type CommandHandler, type Command, type Event } from '@auto-engineer/message-bus';
2
2
  import { promises as fs } from 'fs';
3
- import { copyDesignSystemDocsAndUserPreferences } from '../index';
3
+ import { importDesignSystemComponentsFromFigma, ImportStrategy, type FilterFunctionType } from '../index';
4
+ import { FilterLoader } from '../utils/FilterLoader';
4
5
 
5
6
  export type ImportDesignSystemCommand = Command<
6
7
  'ImportDesignSystem',
7
8
  {
8
9
  inputDir: string;
9
10
  outputDir: string;
11
+ strategy?: keyof typeof ImportStrategy;
12
+ filterPath?: string;
10
13
  }
11
14
  >;
12
15
 
@@ -31,7 +34,7 @@ export type DesignSystemImportFailedEvent = Event<
31
34
  export async function handleImportDesignSystemCommand(
32
35
  command: ImportDesignSystemCommand,
33
36
  ): Promise<DesignSystemImportedEvent | DesignSystemImportFailedEvent> {
34
- const { inputDir, outputDir } = command.data;
37
+ const { inputDir, outputDir, strategy, filterPath } = command.data;
35
38
 
36
39
  try {
37
40
  // Check if input directory exists
@@ -55,7 +58,22 @@ export async function handleImportDesignSystemCommand(
55
58
  };
56
59
  }
57
60
 
58
- await copyDesignSystemDocsAndUserPreferences(inputDir, outputDir);
61
+ const resolvedStrategy = strategy ? ImportStrategy[strategy] : ImportStrategy.WITH_COMPONENT_SETS;
62
+
63
+ let filterFn: FilterFunctionType | undefined;
64
+ let loader: FilterLoader | undefined;
65
+ if (typeof filterPath === 'string' && filterPath.trim().length > 0) {
66
+ try {
67
+ loader = new FilterLoader();
68
+ filterFn = await loader.loadFilter(filterPath);
69
+ } catch (e) {
70
+ console.warn(`Could not import filter from ${filterPath}. Skipping custom filter.`, e);
71
+ } finally {
72
+ if (loader) loader.cleanup();
73
+ }
74
+ }
75
+
76
+ await importDesignSystemComponentsFromFigma(outputDir, resolvedStrategy, filterFn);
59
77
  console.log(`Design system files processed from ${inputDir} to ${outputDir}`);
60
78
 
61
79
  return {
package/src/index.ts CHANGED
@@ -2,11 +2,18 @@ import * as path from 'path';
2
2
  import * as fs from 'fs/promises';
3
3
  import * as dotenv from 'dotenv';
4
4
  import { fileURLToPath } from 'url';
5
+ import * as Figma from 'figma-api';
6
+ import { FigmaComponentsBuilder, type FilterFunctionType } from './FigmaComponentsBuilder';
7
+ // import { AIProvider, generateTextWithAI } from '@auto-engineer/ai-gateway';
5
8
 
6
9
  dotenv.config();
7
10
 
8
11
  const __filename = fileURLToPath(import.meta.url);
9
12
 
13
+ const api = new Figma.Api({
14
+ personalAccessToken: process.env.FIGMA_PERSONAL_TOKEN as string,
15
+ });
16
+
10
17
  async function getAllTsxFiles(dir: string): Promise<string[]> {
11
18
  let results: string[] = [];
12
19
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -47,6 +54,7 @@ export async function generateDesignSystemMarkdown(inputDir: string, outputDir:
47
54
  }
48
55
 
49
56
  export * from './commands/import-design-system';
57
+ export type { FilterFunctionType } from './FigmaComponentsBuilder';
50
58
 
51
59
  async function copyFile(inputDir: string, outputDir: string, file: string): Promise<void> {
52
60
  const srcPath = path.join(inputDir, file);
@@ -92,6 +100,97 @@ export async function copyDesignSystemDocsAndUserPreferences(inputDir: string, o
92
100
  }
93
101
  }
94
102
 
103
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
104
+ async function getFigmaComponents(): Promise<{ name: string; description: string; thumbnail: string }[]> {
105
+ let components: { name: string; description: string; thumbnail: string }[] = [];
106
+ try {
107
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
108
+ const response = await api.getFileComponentSets({ file_key: process.env.FIGMA_FILE_ID });
109
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
110
+ components = response.meta.component_sets.map(
111
+ (component: { name: string; description: string; thumbnail_url: string }) => ({
112
+ name: component.name,
113
+ description: component.description,
114
+ thumbnail: component.thumbnail_url,
115
+ }),
116
+ );
117
+ console.log('figma response: ', response);
118
+ } catch (e) {
119
+ console.error(e);
120
+ }
121
+ console.log(components.length);
122
+ return components;
123
+ }
124
+
125
+ export function generateMarkdownFromComponents(
126
+ components: { name: string; description: string; thumbnail: string }[],
127
+ ): string {
128
+ let md = '# Design System\n\n## Components\n\n';
129
+
130
+ if (components.length === 0) {
131
+ console.warn('No components found');
132
+ }
133
+
134
+ for (const component of components) {
135
+ md += `### ${component.name}\nDescription: ${component.description}\nImage: ![${component.name} image](${component.thumbnail})\n\n`;
136
+ }
137
+
138
+ return md;
139
+ }
140
+
141
+ export enum ImportStrategy {
142
+ WITH_COMPONENTS = 'WITH_COMPONENTS',
143
+ WITH_COMPONENT_SETS = 'WITH_COMPONENT_SETS',
144
+ WITH_ALL_FIGMA_INSTANCES = 'WITH_ALL_FIGMA_INSTANCES',
145
+ }
146
+
147
+ export async function importDesignSystemComponentsFromFigma(
148
+ outputDir: string,
149
+ strategy: ImportStrategy = ImportStrategy.WITH_COMPONENT_SETS,
150
+ filterFn?: FilterFunctionType,
151
+ ): Promise<void> {
152
+ const figmaComponentsBuilder = new FigmaComponentsBuilder();
153
+
154
+ if (strategy === ImportStrategy.WITH_COMPONENTS) {
155
+ await figmaComponentsBuilder.withFigmaComponents();
156
+ } else if (strategy === ImportStrategy.WITH_COMPONENT_SETS) {
157
+ await figmaComponentsBuilder.withFigmaComponentSets();
158
+ } else if (strategy === ImportStrategy.WITH_ALL_FIGMA_INSTANCES) {
159
+ await figmaComponentsBuilder.withAllFigmaInstanceNames();
160
+ }
161
+
162
+ // figmaComponentsBuilder.withFilteredNamesForMui();
163
+ // figmaComponentsBuilder.withFilteredNamesForShadcn();
164
+
165
+ if (filterFn) {
166
+ figmaComponentsBuilder.withFilter(filterFn);
167
+ }
168
+
169
+ const figmaComponents = figmaComponentsBuilder.build();
170
+
171
+ console.log(figmaComponents.length);
172
+
173
+ const generatedComponentsMDFile = generateMarkdownFromComponents(figmaComponents);
174
+ // const mdWithImageAnalysis = await generateTextWithAI(
175
+ // `
176
+ // Given this markdown file content:
177
+ // ${generatedComponentsMDFile}
178
+ //
179
+ // ------ INSTRUCTIONS -------
180
+ // !IMPORTANT: Only return with Markdown content, nothing else, I will be putting this straight in a .md file. Don't even start the file with \`\`\`markdown
181
+ // For every component Image: Analyze the given image and add to the given component.
182
+ // - add more content to the "Description:" part of the component.
183
+ // - add "Hierarchy:" part under the component, returning the parts a component is build of. like [Button, Input]
184
+ // `,
185
+ // AIProvider.OpenAI,
186
+ // { temperature: 0.2, maxTokens: 8000 },
187
+ // );
188
+ // await fs.mkdir(outputDir, { recursive: true });
189
+ const outPath = path.join(outputDir, 'design-system.md');
190
+ await fs.writeFile(outPath, generatedComponentsMDFile);
191
+ // await copyFile("../../../examples/design-system/design-system-principles.md", outputDir, 'design-system-principles.md');
192
+ }
193
+
95
194
  // Check if this file is being run directly
96
195
  if (process.argv[1] === __filename) {
97
196
  const [, , inputDir, outputDir] = process.argv;
@@ -107,12 +206,20 @@ if (process.argv[1] === __filename) {
107
206
  // console.error('Error generating design-system.md:', err);
108
207
  // process.exit(1);
109
208
  // });
110
- copyDesignSystemDocsAndUserPreferences(inputDir, outputDir)
209
+ // copyDesignSystemDocsAndUserPreferences(inputDir, outputDir)
210
+ // .then(() => {
211
+ // console.log(`design-system.md copied to ${outputDir}`);
212
+ // })
213
+ // .catch((err) => {
214
+ // console.error('Error copying design-system.md:', err);
215
+ // process.exit(1);
216
+ // });
217
+ importDesignSystemComponentsFromFigma(outputDir)
111
218
  .then(() => {
112
- console.log(`design-system.md copied to ${outputDir}`);
219
+ console.log(`design-system.md generated to ${outputDir}`);
113
220
  })
114
221
  .catch((err) => {
115
- console.error('Error copying design-system.md:', err);
222
+ console.error('Error generating design-system.md:', err);
116
223
  process.exit(1);
117
224
  });
118
225
  }
@@ -0,0 +1,76 @@
1
+ import { extname, resolve as resolvePath } from 'path';
2
+ import { pathToFileURL } from 'url';
3
+ import { existsSync } from 'fs';
4
+ import type { FilterFunctionType } from '../FigmaComponentsBuilder';
5
+
6
+ export class FilterLoader {
7
+ private tsxRegistered = false;
8
+ private tsxUnregister: (() => void) | null = null;
9
+
10
+ async loadFilter(filePath: string): Promise<FilterFunctionType> {
11
+ if (typeof filePath !== 'string' || filePath.trim().length === 0) {
12
+ throw new Error('Filter file path is required');
13
+ }
14
+
15
+ const absolutePath = resolvePath(process.cwd(), filePath);
16
+
17
+ if (!existsSync(absolutePath)) {
18
+ throw new Error(`Filter file not found: ${absolutePath}`);
19
+ }
20
+
21
+ const ext = extname(absolutePath).toLowerCase();
22
+
23
+ // Enable tsx loader for TS files at runtime
24
+ if (ext === '.ts' || ext === '.tsx') {
25
+ await this.ensureTsxSupport();
26
+ }
27
+
28
+ const fileUrl = pathToFileURL(absolutePath).href;
29
+
30
+ const loadedUnknown: unknown = await import(fileUrl);
31
+ const loadedModule = loadedUnknown as { filter?: unknown; default?: unknown };
32
+
33
+ if (typeof loadedModule.filter === 'function') {
34
+ return loadedModule.filter as FilterFunctionType;
35
+ }
36
+
37
+ if (typeof loadedModule.default === 'function') {
38
+ console.warn('Using default export from filter module. Prefer a named export "filter".');
39
+ return loadedModule.default as FilterFunctionType;
40
+ }
41
+
42
+ throw new Error('No filter function found. Export a function named "filter" or as a default export from the file.');
43
+ }
44
+
45
+ private async ensureTsxSupport(): Promise<void> {
46
+ if (this.tsxRegistered) return;
47
+
48
+ try {
49
+ const tsxUnknown: unknown = await import('tsx/esm/api');
50
+ const tsxApi = tsxUnknown as { register: () => () => void };
51
+ if (typeof tsxApi.register === 'function') {
52
+ this.tsxUnregister = tsxApi.register();
53
+ this.tsxRegistered = true;
54
+ } else {
55
+ throw new Error('tsx/esm/api.register is not a function');
56
+ }
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ } catch (error) {
59
+ throw new Error(
60
+ 'TypeScript filter detected but tsx is not available.\n' +
61
+ 'Install tsx (npm i -D tsx) or provide a JavaScript file (.js/.mjs).',
62
+ );
63
+ }
64
+ }
65
+
66
+ cleanup(): void {
67
+ if (this.tsxUnregister) {
68
+ try {
69
+ this.tsxUnregister();
70
+ } finally {
71
+ this.tsxUnregister = null;
72
+ this.tsxRegistered = false;
73
+ }
74
+ }
75
+ }
76
+ }