@auto-engineer/design-system-importer 0.4.8 → 0.5.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.
@@ -1,11 +1,31 @@
1
1
  import * as dotenv from 'dotenv';
2
2
  import * as Figma from 'figma-api';
3
+ import createDebug from 'debug';
4
+
5
+ const debug = createDebug('design-system-importer:figma-builder');
6
+ const debugComponents = createDebug('design-system-importer:figma-builder:components');
7
+ const debugFilters = createDebug('design-system-importer:figma-builder:filters');
8
+ const debugAPI = createDebug('design-system-importer:figma-builder:api');
9
+ const debugTree = createDebug('design-system-importer:figma-builder:tree');
3
10
 
4
11
  dotenv.config();
5
12
 
13
+ debug('Initializing FigmaComponentsBuilder module');
14
+
15
+ // Add timeout wrapper for Figma API calls
16
+ const withTimeout = <T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> => {
17
+ return Promise.race([
18
+ promise,
19
+ new Promise<T>((_, reject) =>
20
+ setTimeout(() => reject(new Error(`Figma API request timed out after ${timeoutMs}ms`)), timeoutMs),
21
+ ),
22
+ ]);
23
+ };
24
+
6
25
  const figmaApi = new Figma.Api({
7
26
  personalAccessToken: process.env.FIGMA_PERSONAL_TOKEN as string,
8
27
  });
28
+ debug('Figma API initialized with personal access token');
9
29
 
10
30
  export interface FigmaComponent {
11
31
  name: string;
@@ -26,47 +46,103 @@ interface FigmaNode {
26
46
  export class FigmaComponentsBuilder {
27
47
  components: FigmaComponent[] = [];
28
48
 
49
+ constructor() {
50
+ debug('FigmaComponentsBuilder instance created');
51
+ debugComponents('Initial components array: empty');
52
+ }
53
+
29
54
  async withFigmaComponents() {
55
+ debugAPI('Fetching Figma components from file: %s', process.env.FIGMA_FILE_ID);
56
+
57
+ if (
58
+ process.env.FIGMA_PERSONAL_TOKEN?.trim() === '' ||
59
+ process.env.FIGMA_FILE_ID?.trim() === '' ||
60
+ process.env.FIGMA_PERSONAL_TOKEN === undefined ||
61
+ process.env.FIGMA_FILE_ID === undefined
62
+ ) {
63
+ debugAPI('Missing Figma credentials. FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID not set');
64
+ console.warn('Skipping Figma import: Missing FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID in environment');
65
+ return this;
66
+ }
67
+
30
68
  try {
69
+ debugAPI('Making API call to getFileComponents...');
31
70
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32
- const response = await figmaApi.getFileComponents({ file_key: process.env.FIGMA_FILE_ID });
71
+ const response = await withTimeout(figmaApi.getFileComponents({ file_key: process.env.FIGMA_FILE_ID }), 10000);
72
+ debugAPI('API response received');
33
73
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
34
74
  if (response.meta.components.length > 0) {
35
75
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
36
- console.log(JSON.stringify(response.meta.components.length, null, 2));
76
+ debugComponents('Found %d components in Figma file', response.meta.components.length);
37
77
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
38
78
  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
- }),
79
+ (component: { name: string; description: string; thumbnail_url: string }) => {
80
+ debugComponents('Processing component: %s', component.name);
81
+ return {
82
+ name: component.name,
83
+ description: component.description,
84
+ thumbnail: component.thumbnail_url,
85
+ };
86
+ },
44
87
  );
88
+ debugComponents('Successfully mapped %d components', this.components.length);
89
+ } else {
90
+ debugComponents('No components found in Figma file');
45
91
  }
46
92
  } catch (e) {
47
- console.error(e);
93
+ debugAPI('ERROR: Failed to fetch Figma components: %O', e);
94
+ console.error('Failed to fetch Figma components:', e);
48
95
  }
96
+ debugComponents('withFigmaComponents complete, total components: %d', this.components.length);
49
97
  return this;
50
98
  }
51
99
 
52
100
  async withFigmaComponentSets() {
101
+ debugAPI('Fetching Figma component sets from file: %s', process.env.FIGMA_FILE_ID);
102
+
103
+ if (
104
+ process.env.FIGMA_PERSONAL_TOKEN?.trim() === '' ||
105
+ process.env.FIGMA_FILE_ID?.trim() === '' ||
106
+ process.env.FIGMA_PERSONAL_TOKEN === undefined ||
107
+ process.env.FIGMA_FILE_ID === undefined
108
+ ) {
109
+ debugAPI('Missing Figma credentials. FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID not set');
110
+ console.warn('Skipping Figma import: Missing FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID in environment');
111
+ return this;
112
+ }
113
+
53
114
  try {
115
+ debugAPI('Making API call to getFileComponentSets...');
54
116
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
55
- const response = await figmaApi.getFileComponentSets({ file_key: process.env.FIGMA_FILE_ID });
117
+ const response = await withTimeout(figmaApi.getFileComponentSets({ file_key: process.env.FIGMA_FILE_ID }), 10000);
118
+ debugAPI('API response received');
56
119
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
57
120
  if (response.meta.component_sets.length > 0) {
121
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
122
+ debugComponents('Found %d component sets in Figma file', response.meta.component_sets.length);
58
123
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
59
124
  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
- }),
125
+ (component: { name: string; description: string; thumbnail_url: string }) => {
126
+ debugComponents('Processing component set: %s', component.name);
127
+ const mapped = {
128
+ name: component.name,
129
+ description: component.description ?? 'N/A',
130
+ thumbnail: component.thumbnail_url ?? 'N/A',
131
+ };
132
+ debugComponents(' - Description: %s', mapped.description.substring(0, 50));
133
+ debugComponents(' - Has thumbnail: %s', mapped.thumbnail !== 'N/A');
134
+ return mapped;
135
+ },
65
136
  );
137
+ debugComponents('Successfully mapped %d component sets', this.components.length);
138
+ } else {
139
+ debugComponents('No component sets found in Figma file');
66
140
  }
67
141
  } catch (e) {
68
- console.error(e);
142
+ debugAPI('ERROR: Failed to fetch Figma component sets: %O', e);
143
+ console.error('Failed to fetch Figma component sets:', e);
69
144
  }
145
+ debugComponents('withFigmaComponentSets complete, total components: %d', this.components.length);
70
146
  return this;
71
147
  }
72
148
 
@@ -81,73 +157,122 @@ export class FigmaComponentsBuilder {
81
157
  // }
82
158
 
83
159
  withFilteredNamesForShadcn(): void {
160
+ debugFilters('Applying Shadcn name filter to %d components', this.components.length);
161
+ const originalCount = this.components.length;
84
162
  this.components = this.components
85
163
  .map((comp: FigmaComponent): FigmaComponent | null => {
86
- if (!comp?.name) return null;
164
+ if (!comp?.name) {
165
+ debugFilters('Skipping component with no name');
166
+ return null;
167
+ }
87
168
 
88
169
  let str = comp.name.trim();
170
+ debugFilters('Processing component name: %s', str);
89
171
 
90
172
  if (str.includes('/')) {
173
+ const original = str;
91
174
  str = str.split('/')[0].trim();
175
+ debugFilters(' Split by /: %s -> %s', original, str);
92
176
  } else {
177
+ const original = str;
93
178
  str = str.split(' ')[0].trim();
179
+ debugFilters(' Split by space: %s -> %s', original, str);
94
180
  }
95
181
 
96
182
  if (str.length > 0) {
97
- str = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
183
+ const capitalized = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
184
+ const final = capitalized.toLowerCase();
185
+ debugFilters(' Normalized: %s -> %s', str, final);
186
+ return {
187
+ ...comp,
188
+ name: final,
189
+ };
98
190
  } else {
191
+ debugFilters(' Empty string after processing, skipping');
99
192
  return null;
100
193
  }
101
-
102
- return {
103
- ...comp,
104
- name: str.toLowerCase(),
105
- };
106
194
  })
107
195
  .filter((c: FigmaComponent | null): c is FigmaComponent => Boolean(c));
196
+ debugFilters('Shadcn filter complete: %d -> %d components', originalCount, this.components.length);
108
197
  }
109
198
 
110
199
  withFilter(filter: FilterFunctionType): void {
200
+ debugFilters('Applying custom filter function to %d components', this.components.length);
201
+ const originalCount = this.components.length;
111
202
  try {
112
203
  this.components = filter(this.components);
204
+ debugFilters('Custom filter applied successfully: %d -> %d components', originalCount, this.components.length);
113
205
  } catch (e) {
206
+ debugFilters('ERROR: Failed to apply custom filter: %O', e);
114
207
  console.error('Error applying custom filter:', e);
115
208
  }
116
209
  }
117
210
 
118
211
  async withAllFigmaInstanceNames() {
212
+ debugAPI('Fetching all Figma instance names from file: %s', process.env.FIGMA_FILE_ID);
213
+
214
+ if (
215
+ process.env.FIGMA_PERSONAL_TOKEN?.trim() === '' ||
216
+ process.env.FIGMA_FILE_ID?.trim() === '' ||
217
+ process.env.FIGMA_PERSONAL_TOKEN === undefined ||
218
+ process.env.FIGMA_FILE_ID === undefined
219
+ ) {
220
+ debugAPI('Missing Figma credentials. FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID not set');
221
+ console.warn('Skipping Figma import: Missing FIGMA_PERSONAL_TOKEN or FIGMA_FILE_ID in environment');
222
+ return this;
223
+ }
224
+
119
225
  try {
120
226
  /// ----
121
227
  const usedComponentMap = new Map<string, { name: string; description: string; thumbnail: string }>();
228
+ debugTree('Created component map for tracking unique instances');
122
229
 
230
+ debugAPI('Making API call to getFile with depth: 1...');
123
231
  // 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
- });
232
+ const { document } = await withTimeout(
233
+ figmaApi.getFile({
234
+ file_key: process.env.FIGMA_FILE_ID,
235
+ depth: 1,
236
+ }),
237
+ 10000,
238
+ );
239
+ debugAPI('File document received, starting tree walk');
240
+
241
+ // eslint-disable-next-line complexity
242
+ function walkTree(node: FigmaNode, depth: number = 0) {
243
+ const indent = ' '.repeat(depth);
244
+ debugTree('%sVisiting node: %s (type: %s)', indent, node.name, node.type);
128
245
 
129
- function walkTree(node: FigmaNode) {
130
246
  if (node.type === 'INSTANCE' && Boolean(node.name)) {
131
247
  if (!usedComponentMap.has(node.name)) {
248
+ debugTree('%s -> Found new instance: %s', indent, node.name);
132
249
  usedComponentMap.set(node.name, {
133
250
  name: node.name,
134
251
  description: node.description ?? '',
135
252
  thumbnail: node.thumbnail_url ?? '',
136
253
  });
254
+ debugTree('%s Description: %s', indent, node.description ? 'present' : 'empty');
255
+ debugTree('%s Thumbnail: %s', indent, node.thumbnail_url ? 'present' : 'missing');
256
+ } else {
257
+ debugTree('%s -> Instance already tracked: %s', indent, node.name);
137
258
  }
138
259
  }
139
260
 
140
261
  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
141
262
  if (node.children) {
263
+ debugTree('%s Has %d children', indent, node.children.length);
142
264
  for (const child of node.children) {
143
- walkTree(child);
265
+ walkTree(child, depth + 1);
144
266
  }
145
267
  }
146
268
  }
147
269
 
270
+ debugTree('Starting tree walk from document root');
148
271
  walkTree(document);
272
+ debugTree('Tree walk complete');
149
273
 
150
274
  this.components = Array.from(usedComponentMap.values());
275
+ debugComponents('Extracted %d unique component instances', this.components.length);
151
276
 
152
277
  /// ----
153
278
 
@@ -196,12 +321,20 @@ export class FigmaComponentsBuilder {
196
321
  // );
197
322
  // }
198
323
  } catch (e) {
199
- console.error(e);
324
+ debugAPI('ERROR: Failed to fetch Figma instance names: %O', e);
325
+ console.error('Failed to fetch Figma instance names:', e);
200
326
  }
327
+ debugComponents('withAllFigmaInstanceNames complete, total components: %d', this.components.length);
201
328
  return this;
202
329
  }
203
330
 
204
331
  build() {
332
+ debug('Building final component list');
333
+ debugComponents('Returning %d components', this.components.length);
334
+ if (this.components.length > 0) {
335
+ debugComponents('First component: %s', this.components[0].name);
336
+ debugComponents('Last component: %s', this.components[this.components.length - 1].name);
337
+ }
205
338
  return this.components;
206
339
  }
207
340
  }
@@ -0,0 +1,18 @@
1
+ import type { CliManifest } from '@auto-engineer/cli/manifest-types';
2
+
3
+ export const CLI_MANIFEST: CliManifest = {
4
+ category: '@auto-engineer/design-system-importer',
5
+ commands: {
6
+ 'import:design-system': {
7
+ handler: () => import('./commands/import-design-system'),
8
+ description: 'Import Figma design system',
9
+ usage: 'import:design-system <src> <mode> [filter]',
10
+ examples: ['$ auto import:design-system ./.context WITH_COMPONENT_SETS ./shadcn-filter.ts'],
11
+ args: [
12
+ { name: 'src', description: 'Source directory for design system', required: true },
13
+ { name: 'mode', description: 'Import mode (e.g., WITH_COMPONENT_SETS)', required: true },
14
+ { name: 'filter', description: 'Optional filter file', required: false },
15
+ ],
16
+ },
17
+ },
18
+ };
@@ -1,6 +1,12 @@
1
1
  import { type CommandHandler, type Command, type Event } from '@auto-engineer/message-bus';
2
2
  import { importDesignSystemComponentsFromFigma, ImportStrategy, type FilterFunctionType } from '../index';
3
3
  import { FilterLoader } from '../utils/FilterLoader';
4
+ import createDebug from 'debug';
5
+
6
+ const debug = createDebug('design-system-importer:command');
7
+ const debugFilter = createDebug('design-system-importer:command:filter');
8
+ const debugHandler = createDebug('design-system-importer:command:handler');
9
+ const debugResult = createDebug('design-system-importer:command:result');
4
10
 
5
11
  export type ImportDesignSystemCommand = Command<
6
12
  'ImportDesignSystem',
@@ -27,31 +33,51 @@ export type DesignSystemImportFailedEvent = Event<
27
33
  >;
28
34
 
29
35
  // Handler
30
- export async function handleImportDesignSystemCommand(
36
+ // eslint-disable-next-line complexity
37
+ async function handleImportDesignSystemCommandInternal(
31
38
  command: ImportDesignSystemCommand,
32
39
  ): Promise<DesignSystemImportedEvent | DesignSystemImportFailedEvent> {
33
40
  const { outputDir, strategy, filterPath } = command.data;
34
41
 
42
+ debug('Handling ImportDesignSystemCommand');
43
+ debug(' Output directory: %s', outputDir);
44
+ debug(' Strategy: %s', strategy ?? 'default');
45
+ debug(' Filter path: %s', filterPath ?? 'none');
46
+ debug(' Request ID: %s', command.requestId);
47
+ debug(' Correlation ID: %s', command.correlationId ?? 'none');
48
+
35
49
  try {
36
50
  const resolvedStrategy = strategy ? ImportStrategy[strategy] : ImportStrategy.WITH_COMPONENT_SETS;
51
+ debugHandler('Resolved strategy: %s', resolvedStrategy);
37
52
 
38
53
  let filterFn: FilterFunctionType | undefined;
39
54
  let loader: FilterLoader | undefined;
40
55
  if (typeof filterPath === 'string' && filterPath.trim().length > 0) {
56
+ debugFilter('Loading custom filter from: %s', filterPath);
41
57
  try {
42
58
  loader = new FilterLoader();
59
+ debugFilter('FilterLoader instance created');
43
60
  filterFn = await loader.loadFilter(filterPath);
61
+ debugFilter('Filter function loaded successfully');
44
62
  } catch (e) {
63
+ debugFilter('ERROR: Failed to load filter from %s: %O', filterPath, e);
45
64
  console.warn(`Could not import filter from ${filterPath}. Skipping custom filter.`, e);
46
65
  } finally {
47
- if (loader) loader.cleanup();
66
+ if (loader) {
67
+ debugFilter('Cleaning up FilterLoader');
68
+ loader.cleanup();
69
+ }
48
70
  }
71
+ } else {
72
+ debugFilter('No filter path provided, proceeding without custom filter');
49
73
  }
50
74
 
75
+ debugHandler('Calling importDesignSystemComponentsFromFigma...');
51
76
  await importDesignSystemComponentsFromFigma(outputDir, resolvedStrategy, filterFn);
77
+ debugHandler('Import completed successfully');
52
78
  console.log(`Design system files processed to ${outputDir}`);
53
79
 
54
- return {
80
+ const successEvent: DesignSystemImportedEvent = {
55
81
  type: 'DesignSystemImported',
56
82
  data: {
57
83
  outputDir,
@@ -60,11 +86,16 @@ export async function handleImportDesignSystemCommand(
60
86
  requestId: command.requestId,
61
87
  correlationId: command.correlationId,
62
88
  };
89
+ debugResult('Returning success event: DesignSystemImported');
90
+ debugResult(' Output directory: %s', outputDir);
91
+ return successEvent;
63
92
  } catch (error) {
64
93
  const errorMessage = error instanceof Error ? error.message : String(error);
94
+ debugHandler('ERROR: Design system import failed: %O', error);
95
+ debugResult('Error message: %s', errorMessage);
65
96
  console.error('Error importing design system:', error);
66
97
 
67
- return {
98
+ const failureEvent: DesignSystemImportFailedEvent = {
68
99
  type: 'DesignSystemImportFailed',
69
100
  data: {
70
101
  error: errorMessage,
@@ -74,17 +105,82 @@ export async function handleImportDesignSystemCommand(
74
105
  requestId: command.requestId,
75
106
  correlationId: command.correlationId,
76
107
  };
108
+ debugResult('Returning failure event: DesignSystemImportFailed');
109
+ debugResult(' Error: %s', errorMessage);
110
+ debugResult(' Output directory: %s', outputDir);
111
+ return failureEvent;
77
112
  }
78
113
  }
79
114
 
80
115
  export const importDesignSystemCommandHandler: CommandHandler<ImportDesignSystemCommand> = {
81
116
  name: 'ImportDesignSystem',
82
117
  handle: async (command: ImportDesignSystemCommand): Promise<void> => {
83
- const result = await handleImportDesignSystemCommand(command);
118
+ debug('CommandHandler executing for ImportDesignSystem');
119
+ const result = await handleImportDesignSystemCommandInternal(command);
84
120
  if (result.type === 'DesignSystemImported') {
121
+ debug('Command handler completed: success');
85
122
  console.log('Design system imported successfully');
86
123
  } else {
124
+ debug('Command handler completed: failure - %s', result.data.error);
87
125
  console.error(`Failed: ${result.data.error}`);
88
126
  }
89
127
  },
90
128
  };
129
+
130
+ // CLI arguments interface
131
+ interface CliArgs {
132
+ _: string[];
133
+ strategy?: 'WITH_COMPONENTS' | 'WITH_COMPONENT_SETS' | 'WITH_ALL_FIGMA_INSTANCES';
134
+ filter?: string;
135
+ [key: string]: unknown;
136
+ }
137
+
138
+ // Type guard to check if it's an ImportDesignSystemCommand
139
+ function isImportDesignSystemCommand(obj: unknown): obj is ImportDesignSystemCommand {
140
+ return (
141
+ typeof obj === 'object' &&
142
+ obj !== null &&
143
+ 'type' in obj &&
144
+ 'data' in obj &&
145
+ (obj as { type: unknown }).type === 'ImportDesignSystem'
146
+ );
147
+ }
148
+
149
+ // Default export for CLI usage
150
+ export default async (commandOrArgs: ImportDesignSystemCommand | CliArgs) => {
151
+ let command: ImportDesignSystemCommand;
152
+
153
+ if (isImportDesignSystemCommand(commandOrArgs)) {
154
+ command = commandOrArgs;
155
+ } else if ('outputDir' in commandOrArgs) {
156
+ // Handle message bus format
157
+ const args = commandOrArgs as { outputDir?: string; strategy?: string; filterPath?: string };
158
+ command = {
159
+ type: 'ImportDesignSystem' as const,
160
+ data: {
161
+ outputDir: args.outputDir ?? '.context',
162
+ strategy: args.strategy as keyof typeof ImportStrategy | undefined,
163
+ filterPath: args.filterPath,
164
+ },
165
+ timestamp: new Date(),
166
+ };
167
+ } else {
168
+ // Handle traditional CLI args format
169
+ command = {
170
+ type: 'ImportDesignSystem' as const,
171
+ data: {
172
+ outputDir: commandOrArgs._?.[0] ?? '.context',
173
+ strategy: commandOrArgs._?.[1] as keyof typeof ImportStrategy | undefined,
174
+ filterPath: commandOrArgs._?.[2],
175
+ },
176
+ timestamp: new Date(),
177
+ };
178
+ }
179
+
180
+ const result = await handleImportDesignSystemCommandInternal(command);
181
+ if (result.type === 'DesignSystemImported') {
182
+ console.log('Design system imported successfully');
183
+ } else {
184
+ console.error(`Failed: ${result.data.error}`);
185
+ }
186
+ };