@@ -17,13 +17,60 @@ program
1717 . name ( programName )
1818 . description ( 'A CLI tool for generating CSS stylesheets from Figma Library Components' )
1919 . requiredOption ( '-l, --library <path>' , 'Path to the library configuration file.' )
20+ . option ( '-c, --components <names>' , 'Comma-delimited list or wildcard pattern of components to generate (e.g., "anchor,button" or "content navigation *")' )
2021 . action ( main ) ;
2122
2223interface ProgramOptions {
2324 library : string ;
25+ components ?: string ;
2426}
2527
26- async function main ( { library } : ProgramOptions ) {
28+ /**
29+ * Matches a component name against a pattern that may include wildcards.
30+ * @param componentName - The component name to test
31+ * @param pattern - The pattern to match against (supports * wildcard)
32+ * @returns true if the component name matches the pattern
33+ */
34+ function matchesPattern ( componentName : string , pattern : string ) : boolean {
35+ const normalizedComponent = componentName . toLowerCase ( ) . trim ( ) ;
36+ const normalizedPattern = pattern . toLowerCase ( ) . trim ( ) ;
37+
38+ // Convert wildcard pattern to regex
39+ const regexPattern = normalizedPattern
40+ . replace ( / [ . + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) // Escape special regex chars except *
41+ . replace ( / \* / g, '.*' ) ; // Convert * to .*
42+
43+ const regex = new RegExp ( `^${ regexPattern } $` ) ;
44+ return regex . test ( normalizedComponent ) ;
45+ }
46+
47+ /**
48+ * Filters component names based on comma-delimited patterns or wildcards.
49+ * @param componentNames - Array of all available component names
50+ * @param filterString - Comma-delimited patterns (e.g., "button,card" or "content navigation *")
51+ * @returns Array of matching component names
52+ */
53+ function filterComponentNames ( componentNames : string [ ] , filterString : string ) : string [ ] {
54+ const patterns = filterString . split ( ',' ) . map ( p => p . trim ( ) ) . filter ( p => p . length > 0 ) ;
55+
56+ if ( patterns . length === 0 ) {
57+ return componentNames ;
58+ }
59+
60+ const matchedNames = new Set < string > ( ) ;
61+
62+ for ( const pattern of patterns ) {
63+ for ( const componentName of componentNames ) {
64+ if ( matchesPattern ( componentName , pattern ) ) {
65+ matchedNames . add ( componentName ) ;
66+ }
67+ }
68+ }
69+
70+ return Array . from ( matchedNames ) . sort ( alphabetize ) ;
71+ }
72+
73+ async function main ( { library, components } : ProgramOptions ) {
2774 const configPath = path . resolve ( process . cwd ( ) , library ) ;
2875 logger . neutral ( 'Validating library config file: ' + configPath ) ;
2976 const libraryConfig = await LibraryConfig . create ( configPath ) ;
@@ -60,14 +107,14 @@ async function main({ library }: ProgramOptions) {
60107 const libraryComponentSetsResponse = await client . getFileComponentSets ( libraryConfig . file ) ;
61108
62109 if ( libraryComponentSetsResponse . error || libraryComponentSetsResponse . status !== 200 ) {
63- logger . fail ( `Accessing Figma library component sets failed with status code ${ libraryComponentSetsResponse . status } ` ) ;
110+ logger . fail ( `Accessing Figma library component sets failed with status code ${ libraryComponentSetsResponse . status } : ${ ( libraryComponentSetsResponse as any ) . err } ` ) ;
64111 process . exit ( 1 ) ;
65112 }
66113
67114 const libraryComponentsResponse = await client . getFileComponents ( libraryConfig . file ) ;
68115
69116 if ( libraryComponentsResponse . error || libraryComponentsResponse . status !== 200 ) {
70- logger . fail ( `Accessing Figma library components failed with status code ${ libraryComponentsResponse . status } ` ) ;
117+ logger . fail ( `Accessing Figma library components failed with status code ${ libraryComponentsResponse . status } : ${ ( libraryComponentsResponse as any ) . err } ` ) ;
71118 process . exit ( 1 ) ;
72119 }
73120
@@ -84,32 +131,48 @@ async function main({ library }: ProgramOptions) {
84131 // Also filter out components which aren't in a container frame (assume they are helper/utility for now)
85132 const hasContainingFrame = component . containing_frame !== undefined ;
86133
87- return ! hasComponentSet && ! hasContainingFrame ;
134+ return ! hasComponentSet && hasContainingFrame ;
88135 } ) ;
89136 const allComponents = libraryComponentSets . concat ( uniqueComponents ) ;
90137
91138 const componentNames = allComponents . map ( ( value ) => value . name ) . sort ( alphabetize ) ;
92- const pickComponentsRequest = {
93- type : 'list' ,
94- name : 'all' ,
95- message : 'Which component stylesheets would you like to generate?' ,
96- choices : [ 'All' , 'Choose which' ] ,
97- } ;
98-
99- const pickComponentsResponse = await inquirer . prompt ( [ pickComponentsRequest ] ) ;
100- const componentNamesToRender : string [ ] = [ ] ;
101-
102- if ( pickComponentsResponse . all !== 'All' ) {
103- const chooseComponentsRequest = {
104- type : 'checkbox' ,
105- name : 'which' ,
106- message : 'Choose components:' ,
107- choices : componentNames ,
108- } ;
109- const components = await inquirer . prompt ( [ chooseComponentsRequest ] ) ;
110- componentNamesToRender . push ( ...components . which ) ;
139+ let componentNamesToRender : string [ ] = [ ] ;
140+
141+ // If components filter is provided via CLI, use it directly
142+ if ( components ) {
143+ componentNamesToRender = filterComponentNames ( componentNames , components ) ;
144+
145+ if ( componentNamesToRender . length === 0 ) {
146+ logger . fail ( `No components matched the pattern: "${ components } "` ) ;
147+ logger . neutral ( `Available components:\n${ componentNames . join ( ', ' ) } ` ) ;
148+ process . exit ( 1 ) ;
149+ }
150+
151+ logger . success ( `Found ${ componentNamesToRender . length } component${ componentNamesToRender . length === 1 ? '' : 's' } matching "${ components } ":` ) ;
152+ logger . neutral ( componentNamesToRender . join ( ', ' ) ) ;
111153 } else {
112- componentNamesToRender . push ( ...componentNames ) ;
154+ // Interactive mode when no -c parameter is provided
155+ const pickComponentsRequest = {
156+ type : 'list' ,
157+ name : 'all' ,
158+ message : 'Which component stylesheets would you like to generate?' ,
159+ choices : [ 'All' , 'Choose which' ] ,
160+ } ;
161+
162+ const pickComponentsResponse = await inquirer . prompt ( [ pickComponentsRequest ] ) ;
163+
164+ if ( pickComponentsResponse . all !== 'All' ) {
165+ const chooseComponentsRequest = {
166+ type : 'checkbox' ,
167+ name : 'which' ,
168+ message : 'Choose components:' ,
169+ choices : componentNames ,
170+ } ;
171+ const chosenComponents = await inquirer . prompt ( [ chooseComponentsRequest ] ) ;
172+ componentNamesToRender . push ( ...chosenComponents . which ) ;
173+ } else {
174+ componentNamesToRender . push ( ...componentNames ) ;
175+ }
113176 }
114177
115178 if ( componentNamesToRender . length === 0 ) {
@@ -123,10 +186,17 @@ async function main({ library }: ProgramOptions) {
123186 } \n${ componentNamesToRender . join ( ', ' ) } `
124187 ) ;
125188
126- const confirm = await inquirer . prompt ( { type : 'confirm' , message : 'Would you like to continue?' , name : 'confirm' } ) ;
189+ // Only prompt for confirmation in interactive mode
190+ if ( ! components ) {
191+ const confirm = await inquirer . prompt ( { type : 'confirm' , message : 'Would you like to continue?' , name : 'confirm' } ) ;
192+
193+ if ( ! confirm . confirm ) {
194+ logger . neutral ( `Exiting ${ programName } ` ) ;
195+ process . exit ( 0 ) ;
196+ }
197+ }
127198
128- if ( confirm . confirm ) {
129- logger . success ( 'Generating component stylesheets. This may take a moment.' ) ;
199+ logger . success ( 'Generating component stylesheets. This may take a moment.' ) ;
130200
131201 const nameLookup = new Set ( componentNamesToRender ) ;
132202 const componentsToRender = allComponents . filter ( ( value ) => {
@@ -159,11 +229,6 @@ async function main({ library }: ProgramOptions) {
159229 } )
160230 ) ;
161231
162- // process components
163- } else {
164- logger . neutral ( `Exiting ${ programName } ` ) ;
165- }
166-
167232 process . exit ( 0 ) ;
168233}
169234
0 commit comments