Skip to content

Commit a3c5849

Browse files
committed
feat: add functions to exclude files from bundling in extensions
1 parent 4960398 commit a3c5849

2 files changed

Lines changed: 345 additions & 1 deletion

File tree

cmd/extensions.go

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,31 +294,94 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err
294294
return fmt.Errorf("directory %s does not exist", absDir)
295295
}
296296

297+
// Pre-flight size check
298+
if in.Output != "json" {
299+
pterm.Info.Println("Analyzing extension directory...")
300+
}
301+
302+
estimatedSize, err := estimateExtensionSize(absDir)
303+
if err != nil {
304+
pterm.Warning.Printf("Could not estimate size: %v\n", err)
305+
} else {
306+
const maxSize = 50 * 1024 * 1024
307+
if estimatedSize > maxSize {
308+
pterm.Error.Printf("Estimated bundle size (%s) exceeds limit (50MB)\n",
309+
formatBytes(estimatedSize))
310+
pterm.Info.Println("\nTo reduce size:")
311+
pterm.Info.Println(" 1. Ensure node_modules/ is not needed (already excluded)")
312+
pterm.Info.Println(" 2. Remove large assets or documentation files")
313+
pterm.Info.Println(" 3. Build extension for production (minified)")
314+
return fmt.Errorf("bundle would exceed maximum size")
315+
}
316+
317+
if in.Output != "json" {
318+
pterm.Info.Printf("Estimated bundle size: %s\n", formatBytes(estimatedSize))
319+
}
320+
}
321+
297322
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_ext_%d.zip", time.Now().UnixNano()))
323+
298324
if in.Output != "json" {
299325
pterm.Info.Println("Zipping extension directory...")
300326
}
301-
if err := util.ZipDirectory(absDir, tmpFile); err != nil {
327+
328+
// Use new extension-specific zip function
329+
stats, err := util.ZipExtensionDirectory(absDir, tmpFile, &util.ExtensionZipOptions{
330+
ExcludeDefaults: false, // Apply defaults
331+
Verbose: false,
332+
})
333+
if err != nil {
302334
pterm.Error.Println("Failed to zip directory")
303335
return err
304336
}
305337
defer os.Remove(tmpFile)
306338

339+
// Show helpful stats
340+
if in.Output != "json" {
341+
pterm.Success.Printf("Created bundle: %s (%d files)\n",
342+
formatBytes(stats.BytesIncluded), stats.FilesIncluded)
343+
}
344+
345+
// Final size validation
346+
fileInfo, err := os.Stat(tmpFile)
347+
if err != nil {
348+
return fmt.Errorf("failed to stat zip: %w", err)
349+
}
350+
351+
const maxSize = 50 * 1024 * 1024
352+
if fileInfo.Size() > maxSize {
353+
pterm.Error.Printf("Extension bundle is too large: %s (max: 50MB)\n",
354+
formatBytes(fileInfo.Size()))
355+
pterm.Info.Println("\nSuggestions to reduce size:")
356+
pterm.Info.Println(" 1. Ensure you're building the extension for production")
357+
pterm.Info.Println(" 2. Remove unnecessary assets (large images, videos)")
358+
pterm.Info.Println(" 3. Check manifest.json references only needed files")
359+
return fmt.Errorf("bundle exceeds maximum size")
360+
}
361+
362+
// Open file for upload
307363
f, err := os.Open(tmpFile)
308364
if err != nil {
309365
return fmt.Errorf("failed to open temp zip: %w", err)
310366
}
311367
defer f.Close()
312368

369+
// Upload
370+
if in.Output != "json" {
371+
pterm.Info.Println("Uploading extension...")
372+
}
373+
313374
params := kernel.ExtensionUploadParams{File: f}
314375
if in.Name != "" {
315376
params.Name = kernel.Opt(in.Name)
316377
}
378+
317379
item, err := e.extensions.Upload(ctx, params)
318380
if err != nil {
319381
return util.CleanedUpSdkError{Err: err}
320382
}
321383

384+
// Display results
322385
if in.Output == "json" {
323386
return util.PrintPrettyJSON(item)
324387
}
@@ -336,6 +399,62 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err
336399
return nil
337400
}
338401

402+
// formatBytes formats bytes in a human-readable format
403+
func formatBytes(bytes int64) string {
404+
const unit = 1024
405+
if bytes < unit {
406+
return fmt.Sprintf("%d B", bytes)
407+
}
408+
div, exp := int64(unit), 0
409+
for n := bytes / unit; n >= unit; n /= unit {
410+
div *= unit
411+
exp++
412+
}
413+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
414+
}
415+
416+
// estimateExtensionSize walks the directory and estimates the final zip size
417+
// This provides fast feedback if the bundle will be too large
418+
func estimateExtensionSize(srcDir string) (int64, error) {
419+
var totalSize int64
420+
excludeMap := make(map[string]bool)
421+
422+
// Build exclusion map for fast lookup
423+
for _, pattern := range util.DefaultExtensionExclusions.ExcludeDirectory {
424+
excludeMap[pattern] = true
425+
}
426+
427+
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
428+
if err != nil {
429+
return err
430+
}
431+
432+
// Check if path should be excluded
433+
if info.IsDir() {
434+
if excludeMap[filepath.Base(path)] {
435+
return filepath.SkipDir
436+
}
437+
return nil
438+
}
439+
440+
// Check against exact file names
441+
base := filepath.Base(path)
442+
// Check against file patterns
443+
for _, pattern := range util.DefaultExtensionExclusions.ExcludeFilenamePatterns {
444+
if matched, _ := filepath.Match(pattern, base); matched {
445+
return nil
446+
}
447+
}
448+
449+
totalSize += info.Size()
450+
return nil
451+
})
452+
453+
// Zip typically compresses 20-40% for code/text files
454+
// Use conservative 30% compression estimate
455+
return int64(float64(totalSize) * 0.7), err
456+
}
457+
339458
// --- Cobra wiring ---
340459

341460
var extensionsCmd = &cobra.Command{

pkg/util/zip_extensions.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package util
2+
3+
import (
4+
"archive/zip"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"sync"
11+
12+
"github.com/boyter/gocodewalker"
13+
)
14+
15+
// DefaultExtensionExclusions contains patterns for files that are not needed
16+
// Focus on large directories and files that provide meaningful size reduction.
17+
// These will be passed to gocodewalker's ExcludeDirectory and ExcludeFilename fields.
18+
var DefaultExtensionExclusions = struct {
19+
// ExcludeDirectory: exact directory names (case-sensitive)
20+
ExcludeDirectory []string
21+
// ExcludeFilenamePatterns: patterns for filename matching (handled manually)
22+
ExcludeFilenamePatterns []string
23+
}{
24+
ExcludeDirectory: []string{
25+
// Dependencies
26+
"node_modules",
27+
28+
// Version control
29+
".git",
30+
31+
// Test/Coverage
32+
"__tests__",
33+
"coverage",
34+
},
35+
36+
ExcludeFilenamePatterns: []string{
37+
// Test files
38+
"*.test.js",
39+
"*.test.ts",
40+
"*.spec.js",
41+
"*.spec.ts",
42+
43+
// Log files
44+
"*.log",
45+
46+
// Temp files
47+
"*.swp",
48+
},
49+
}
50+
51+
// ExtensionZipOptions configures extension-specific zipping behavior
52+
type ExtensionZipOptions struct {
53+
ExcludeDefaults bool // If true, don't apply default exclusions
54+
Verbose bool // Track individual excluded files
55+
}
56+
57+
// ZipStats tracks statistics about the zipping operation
58+
type ZipStats struct {
59+
mu sync.Mutex
60+
FilesIncluded int
61+
FilesExcluded int
62+
BytesIncluded int64
63+
BytesExcluded int64
64+
ExcludedPaths []string
65+
}
66+
67+
func (s *ZipStats) AddIncluded(bytes int64) {
68+
s.mu.Lock()
69+
defer s.mu.Unlock()
70+
s.FilesIncluded++
71+
s.BytesIncluded += bytes
72+
}
73+
74+
func (s *ZipStats) AddExcluded(path string, bytes int64, verbose bool) {
75+
s.mu.Lock()
76+
defer s.mu.Unlock()
77+
s.FilesExcluded++
78+
s.BytesExcluded += bytes
79+
if verbose {
80+
s.ExcludedPaths = append(s.ExcludedPaths, path)
81+
}
82+
}
83+
84+
// ZipExtensionDirectory zips a Chrome extension directory with smart defaults
85+
// that automatically exclude development files (node_modules, .git, etc.)
86+
func ZipExtensionDirectory(srcDir, destZip string, opts *ExtensionZipOptions) (*ZipStats, error) {
87+
if opts == nil {
88+
opts = &ExtensionZipOptions{}
89+
}
90+
91+
stats := &ZipStats{}
92+
93+
zipFile, err := os.Create(destZip)
94+
if err != nil {
95+
return nil, err
96+
}
97+
defer zipFile.Close()
98+
99+
zipWriter := zip.NewWriter(zipFile)
100+
defer zipWriter.Close()
101+
102+
// Configure file walker with exclusions
103+
fileQueue := make(chan *gocodewalker.File, 256)
104+
walker := gocodewalker.NewFileWalker(srcDir, fileQueue)
105+
walker.IncludeHidden = true
106+
107+
// Apply default exclusions unless disabled
108+
if !opts.ExcludeDefaults {
109+
walker.ExcludeDirectory = append(walker.ExcludeDirectory, DefaultExtensionExclusions.ExcludeDirectory...)
110+
walker.ExcludeFilename = append(walker.ExcludeFilename, DefaultExtensionExclusions.ExcludeFilenamePatterns...)
111+
}
112+
113+
// Track walker errors
114+
errChan := make(chan error, 1)
115+
go func() {
116+
errChan <- walker.Start()
117+
}()
118+
119+
// Track directories we've added to avoid duplicates
120+
dirsAdded := make(map[string]struct{})
121+
122+
// Process files from walker
123+
for f := range fileQueue {
124+
relPath, err := filepath.Rel(srcDir, f.Location)
125+
if err != nil {
126+
return stats, err
127+
}
128+
relPath = filepath.ToSlash(relPath)
129+
130+
// Check against pattern-based exclusions (if defaults are enabled)
131+
shouldExclude := false
132+
if !opts.ExcludeDefaults {
133+
// Check filename against patterns only if defaults are enabled
134+
filename := filepath.Base(f.Location)
135+
for _, pattern := range DefaultExtensionExclusions.ExcludeFilenamePatterns {
136+
matched, err := filepath.Match(pattern, filename)
137+
if err == nil && matched {
138+
shouldExclude = true
139+
break
140+
}
141+
}
142+
}
143+
144+
if shouldExclude {
145+
continue
146+
}
147+
148+
// Ensure parent directories exist in archive
149+
if dir := filepath.Dir(relPath); dir != "." && dir != "" {
150+
segments := strings.Split(dir, "/")
151+
var current string
152+
for _, segment := range segments {
153+
if current == "" {
154+
current = segment
155+
} else {
156+
current = current + "/" + segment
157+
}
158+
if _, exists := dirsAdded[current+"/"]; !exists {
159+
if _, err := zipWriter.Create(current + "/"); err != nil {
160+
return stats, err
161+
}
162+
dirsAdded[current+"/"] = struct{}{}
163+
}
164+
}
165+
}
166+
167+
// Get file info
168+
fileInfo, err := os.Lstat(f.Location)
169+
if err != nil {
170+
return stats, err
171+
}
172+
173+
// Handle symlinks
174+
if fileInfo.Mode()&os.ModeSymlink != 0 {
175+
linkTarget, err := os.Readlink(f.Location)
176+
if err != nil {
177+
return stats, err
178+
}
179+
180+
hdr := &zip.FileHeader{
181+
Name: relPath,
182+
Method: zip.Store,
183+
}
184+
hdr.SetMode(os.ModeSymlink | 0777)
185+
186+
zipFileWriter, err := zipWriter.CreateHeader(hdr)
187+
if err != nil {
188+
return stats, err
189+
}
190+
if _, err := zipFileWriter.Write([]byte(linkTarget)); err != nil {
191+
return stats, err
192+
}
193+
stats.AddIncluded(int64(len(linkTarget)))
194+
} else {
195+
// Regular file
196+
zipFileWriter, err := zipWriter.Create(relPath)
197+
if err != nil {
198+
return stats, err
199+
}
200+
201+
file, err := os.Open(f.Location)
202+
if err != nil {
203+
return stats, err
204+
}
205+
206+
written, err := io.Copy(zipFileWriter, file)
207+
closeErr := file.Close()
208+
if closeErr != nil {
209+
return stats, closeErr
210+
}
211+
if err != nil {
212+
return stats, err
213+
}
214+
215+
stats.AddIncluded(written)
216+
}
217+
}
218+
219+
// Check if walker had an error
220+
if err := <-errChan; err != nil {
221+
return stats, fmt.Errorf("directory walk failed: %w", err)
222+
}
223+
224+
return stats, nil
225+
}

0 commit comments

Comments
 (0)