Skip to content

Commit 62539d3

Browse files
committed
feat: improve zip command
1 parent 7b65c28 commit 62539d3

1 file changed

Lines changed: 56 additions & 18 deletions

File tree

src/zip.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import fs from 'node:fs/promises'
22
import path from 'node:path'
33
import 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'
55
import ansis from 'ansis'
66
import { zip as fflateZip } from 'fflate'
77
import revealFile from 'reveal-file'
88
import { printTitle } from './utils'
99

10+
const DEFAULT_GLOB_PATTERN = '**/*'
11+
1012
export interface ZipOptions {
1113
directory: string
1214
open?: boolean
@@ -28,25 +30,32 @@ function zipAsync(files: FflateFiles): Promise<Uint8Array> {
2830

2931
async 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

109140
export 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

Comments
 (0)