11import fs from 'node:fs/promises'
22import path from 'node:path'
33import process from 'node:process'
4- import { cancel , intro , isCancel , outro , select , spinner , text } from '@clack/prompts'
4+ import { cancel , intro , isCancel , multiselect , outro , select , spinner , text } from '@clack/prompts'
55import ansis from 'ansis'
66import { zip as fflateZip } from 'fflate'
77import revealFile from 'reveal-file'
88import { printTitle } from './utils'
99
10+ const DEFAULT_GLOB_PATTERN = '**/*'
11+
1012export interface ZipOptions {
1113 directory : string
1214 open ?: boolean
@@ -28,25 +30,32 @@ function zipAsync(files: FflateFiles): Promise<Uint8Array> {
2830
2931async function collectAllFiles (
3032 dir : string ,
31- root : string ,
32- collected : Array < { relative : string , absolute : string } > ,
33- ) : Promise < void > {
34- const entries = await fs . readdir ( dir , { withFileTypes : true } )
35- for ( const entry of entries ) {
36- const absolutePath = path . join ( dir , entry . name )
37- if ( entry . isDirectory ( ) ) {
38- await collectAllFiles ( absolutePath , root , collected )
39- }
40- else if ( entry . isFile ( ) ) {
41- collected . push ( { relative : path . relative ( root , absolutePath ) , absolute : absolutePath } )
33+ patterns : string | string [ ] = '**/*' ,
34+ ) : Promise < Array < { relative : string , absolute : string } > > {
35+ const patternList = Array . isArray ( patterns ) ? patterns : [ patterns ]
36+ const seen = new Set < string > ( )
37+ const collected : Array < { relative : string , absolute : string } > = [ ]
38+
39+ for ( const pattern of patternList ) {
40+ const glob = new Bun . Glob ( pattern )
41+ for await ( const file of glob . scan ( { cwd : dir , onlyFiles : true } ) ) {
42+ const absolute = path . join ( dir , file )
43+ if ( ! seen . has ( absolute ) ) {
44+ seen . add ( absolute )
45+ collected . push ( { relative : file , absolute } )
46+ }
4247 }
4348 }
49+
50+ return collected
4451}
4552
46- async function resolveZipDestination ( dir : string ) : Promise < { input : string , file : string } > {
53+ async function resolveZipOptions ( dir : string ) : Promise < { input : string , file : string , glob : string [ ] } > {
4754 const originalDir = path . resolve ( dir )
4855 let absDir = originalDir
4956
57+ let glob : string [ ] = [ DEFAULT_GLOB_PATTERN ]
58+
5059 const selectInputDirs : string [ ] = [ absDir ]
5160
5261 let isNodeProject = false
@@ -73,15 +82,37 @@ async function resolveZipDestination(dir: string): Promise<{ input: string, file
7382 if ( selectInputDirs . length > 1 ) {
7483 const selectedDir = await select ( {
7584 message : 'Multiple directories found. Select the one to zip:' ,
76- options : selectInputDirs . map ( dir => ( { value : dir , label : path . relative ( process . cwd ( ) , dir ) } ) ) ,
85+ options : selectInputDirs . map ( dir => ( { value : dir , label : path . relative ( process . cwd ( ) , dir ) || '.' } ) ) ,
7786 } )
7887
7988 if ( isCancel ( selectedDir ) ) {
8089 cancel ( 'Operation cancelled.' )
8190 process . exit ( 0 )
8291 }
92+
93+ absDir = path . resolve ( selectedDir )
8394 }
8495
96+ // multiselect for glob patterns, default is **/*
97+ const selectedPatterns = await multiselect ( {
98+ message : 'Select file patterns to include in the zip:' ,
99+ options : [
100+ { value : DEFAULT_GLOB_PATTERN , label : 'All files (default)' } ,
101+ { value : '*.html' , label : 'HTML files' } ,
102+ ] ,
103+ initialValues : [ DEFAULT_GLOB_PATTERN ] ,
104+ } )
105+
106+ if ( isCancel ( selectedPatterns ) ) {
107+ cancel ( 'Operation cancelled.' )
108+ process . exit ( 0 )
109+ }
110+
111+ if ( ! selectedPatterns . includes ( DEFAULT_GLOB_PATTERN ) ) {
112+ glob = selectedPatterns . length > 0 ? selectedPatterns : [ DEFAULT_GLOB_PATTERN ]
113+ }
114+
115+ // default zip file name is the directory name, if withDir option is not set
85116 let file = path . basename ( originalDir )
86117 if ( ! isNodeProject ) {
87118 file = path . basename ( absDir )
@@ -103,15 +134,15 @@ async function resolveZipDestination(dir: string): Promise<{ input: string, file
103134
104135 file = fileOutput . trim ( )
105136
106- return { input : absDir , file }
137+ return { input : absDir , file, glob }
107138}
108139
109140export async function zip ( options : ZipOptions ) : Promise < void > {
110141 printTitle ( )
111142 intro ( ansis . bold ( 'Zip Directory' ) )
112143
113144 // Resolve and validate input directory
114- const { input : absDir , file } = await resolveZipDestination ( options . directory )
145+ const { input : absDir , file, glob } = await resolveZipOptions ( options . directory )
115146 // Validate directory exists and is a directory
116147 try {
117148 const stat = await fs . stat ( absDir )
@@ -131,15 +162,22 @@ export async function zip(options: ZipOptions): Promise<void> {
131162 // Phase 1: Collect files
132163 const collectSpin = spinner ( )
133164 collectSpin . start ( 'Collecting files...' )
134- const fileEntries : Array < { relative : string , absolute : string } > = [ ]
165+ let fileEntries : Array < { relative : string , absolute : string } > = [ ]
135166 try {
136- await collectAllFiles ( absDir , absDir , fileEntries )
167+ fileEntries = await collectAllFiles ( absDir , glob )
137168 }
138169 catch ( err ) {
139170 collectSpin . stop ( 'File collection failed.' )
140171 cancel ( `Error reading directory: ${ err instanceof Error ? err . message : String ( err ) } ` )
141172 return
142173 }
174+
175+ if ( fileEntries . length === 0 ) {
176+ collectSpin . stop ( 'No files found to zip.' )
177+ cancel ( 'No files matched the selected patterns.' )
178+ return
179+ }
180+
143181 collectSpin . stop ( `Collected ${ fileEntries . length } file${ fileEntries . length !== 1 ? 's' : '' } ` )
144182
145183 // Phase 2: Read files and compress
0 commit comments