Skip to content

Commit 6bf0331

Browse files
authored
[kernel-869] exclude files from extensions (#98)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches shared zipping logic used by deploy and browser/extension workflows; incorrect exclusion or walker error handling could omit files or break packaging, though most callers pass `nil` to preserve behavior. > > **Overview** > Adds configurable zip exclusions to `util.ZipDirectory` via new `ZipOptions` (directory names + filename glob patterns) and propagates the new signature to existing callers. > > Extension uploads now build smaller bundles by default (excluding e.g. `node_modules`, `.git`, tests/logs), print the bundle size, and fail fast if the resulting zip exceeds **50MB**; deploy/browser extension zipping keeps current behavior by passing `nil`. Adds `util.FormatBytes` and new tests covering zip exclusions and unzip behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e0e904b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6defae2 commit 6bf0331

File tree

6 files changed

+289
-9
lines changed

6 files changed

+289
-9
lines changed

cmd/browsers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1940,7 +1940,7 @@ func (b BrowsersCmd) ExtensionsUpload(ctx context.Context, in BrowsersExtensions
19401940
tempZipPath := filepath.Join(os.TempDir(), fmt.Sprintf("kernel-ext-%s.zip", extName))
19411941

19421942
pterm.Info.Printf("Zipping %s as %s...\n", extPath, extName)
1943-
if err := util.ZipDirectory(extPath, tempZipPath); err != nil {
1943+
if err := util.ZipDirectory(extPath, tempZipPath, nil); err != nil {
19441944
pterm.Error.Printf("Failed to zip %s: %v\n", extPath, err)
19451945
return nil
19461946
}

cmd/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) {
236236
}
237237
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_%d.zip", time.Now().UnixNano()))
238238
logger.Debug("compressing files", logger.Args("sourceDir", sourceDir, "tmpFile", tmpFile))
239-
if err := util.ZipDirectory(sourceDir, tmpFile); err != nil {
239+
if err := util.ZipDirectory(sourceDir, tmpFile, nil); err != nil {
240240
if spinner != nil {
241241
spinner.Fail("Failed to compress files")
242242
}

cmd/extensions.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ import (
1818
"github.com/spf13/cobra"
1919
)
2020

21+
const (
22+
MaxExtensionSizeBytes = 50 * 1024 * 1024 // 50MB
23+
)
24+
25+
// defaultExtensionExclusions contains patterns for files that are not needed
26+
// when zipping Chrome extensions
27+
var defaultExtensionExclusions = util.ZipOptions{
28+
ExcludeDirectories: []string{
29+
"node_modules",
30+
".git",
31+
"__tests__",
32+
"coverage",
33+
},
34+
ExcludeFilenamePatterns: []string{
35+
"*.test.js",
36+
"*.test.ts",
37+
"*.spec.js",
38+
"*.spec.ts",
39+
"*.log",
40+
"*.swp",
41+
},
42+
}
43+
2144
// ExtensionsService defines the subset of the Kernel SDK extension client that we use.
2245
type ExtensionsService interface {
2346
List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.ExtensionListResponse, err error)
@@ -294,26 +317,53 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err
294317
return fmt.Errorf("directory %s does not exist", absDir)
295318
}
296319

297-
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_ext_%d.zip", time.Now().UnixNano()))
320+
// Pre-flight size check
298321
if in.Output != "json" {
299-
pterm.Info.Println("Zipping extension directory...")
322+
pterm.Info.Println("Compressing extension directory...")
300323
}
301-
if err := util.ZipDirectory(absDir, tmpFile); err != nil {
324+
325+
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_ext_%d.zip", time.Now().UnixNano()))
326+
327+
if err := util.ZipDirectory(absDir, tmpFile, &defaultExtensionExclusions); err != nil {
302328
pterm.Error.Println("Failed to zip directory")
303329
return err
304330
}
305331
defer os.Remove(tmpFile)
306332

333+
fileInfo, err := os.Stat(tmpFile)
334+
if err != nil {
335+
return fmt.Errorf("failed to stat zip: %w", err)
336+
}
337+
338+
if in.Output != "json" {
339+
pterm.Success.Printf("Created bundle: %s\n", util.FormatBytes(fileInfo.Size()))
340+
}
341+
342+
if fileInfo.Size() > MaxExtensionSizeBytes {
343+
pterm.Error.Printf("Extension bundle is too large: %s (max: %s)\n",
344+
util.FormatBytes(fileInfo.Size()), util.FormatBytes(MaxExtensionSizeBytes))
345+
pterm.Info.Println("\nSuggestions to reduce size:")
346+
pterm.Info.Println(" 1. Ensure you're building the extension for production")
347+
pterm.Info.Println(" 2. Remove unnecessary assets (large images, videos)")
348+
pterm.Info.Println(" 3. Check manifest.json references only needed files")
349+
return fmt.Errorf("bundle exceeds maximum size")
350+
}
351+
307352
f, err := os.Open(tmpFile)
308353
if err != nil {
309354
return fmt.Errorf("failed to open temp zip: %w", err)
310355
}
311356
defer f.Close()
312357

358+
if in.Output != "json" {
359+
pterm.Info.Println("Uploading extension...")
360+
}
361+
313362
params := kernel.ExtensionUploadParams{File: f}
314363
if in.Name != "" {
315364
params.Name = kernel.Opt(in.Name)
316365
}
366+
317367
item, err := e.extensions.Upload(ctx, params)
318368
if err != nil {
319369
return util.CleanedUpSdkError{Err: err}

pkg/util/format.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package util
22

3-
import "strings"
3+
import (
4+
"fmt"
5+
"strings"
6+
)
47

58
// OrDash returns the string if non-empty, otherwise returns "-".
69
func OrDash(s string) string {
@@ -29,3 +32,17 @@ func JoinOrDash(items ...string) string {
2932
}
3033
return strings.Join(items, ", ")
3134
}
35+
36+
// FormatBytes formats bytes in a human-readable format
37+
func FormatBytes(bytes int64) string {
38+
const unit = 1024
39+
if bytes < unit {
40+
return fmt.Sprintf("%d B", bytes)
41+
}
42+
div, exp := int64(unit), 0
43+
for n := bytes / unit; n >= unit; n /= unit {
44+
div *= unit
45+
exp++
46+
}
47+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
48+
}

pkg/util/zip.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ import (
1111
"github.com/boyter/gocodewalker"
1212
)
1313

14+
// ZipOptions which directories and files to exclude from the zip
15+
type ZipOptions struct {
16+
// ExcludeDirectories: exact directory names to exclude (case-sensitive)
17+
ExcludeDirectories []string
18+
// ExcludeFilenamePatterns: glob patterns for filename exclusion (e.g., "*.test.js")
19+
ExcludeFilenamePatterns []string
20+
}
21+
1422
// ZipDirectory compresses the given source directory into the destination file path.
15-
func ZipDirectory(srcDir, destZip string) error {
23+
func ZipDirectory(srcDir, destZip string, opts *ZipOptions) error {
1624
zipFile, err := os.Create(destZip)
1725
if err != nil {
1826
return err
@@ -28,9 +36,16 @@ func ZipDirectory(srcDir, destZip string) error {
2836
// Include hidden files (to match previous behaviour) but still respect .gitignore rules
2937
walker.IncludeHidden = true
3038

31-
// Start walking in a separate goroutine so we can process files as they arrive
39+
// Apply directory exclusions to walker
40+
if opts != nil {
41+
walker.ExcludeDirectory = append(walker.ExcludeDirectory, opts.ExcludeDirectories...)
42+
}
43+
44+
defer walker.Terminate()
45+
46+
errChan := make(chan error, 1)
3247
go func() {
33-
_ = walker.Start()
48+
errChan <- walker.Start()
3449
}()
3550

3651
// Track directories we've already added to the zip archive so we don't duplicate entries
@@ -44,6 +59,22 @@ func ZipDirectory(srcDir, destZip string) error {
4459
}
4560
relPath = filepath.ToSlash(relPath)
4661

62+
// Check against pattern-based exclusions if provided
63+
if opts != nil && len(opts.ExcludeFilenamePatterns) > 0 {
64+
filename := filepath.Base(f.Location)
65+
shouldExclude := false
66+
for _, pattern := range opts.ExcludeFilenamePatterns {
67+
matched, err := filepath.Match(pattern, filename)
68+
if err == nil && matched {
69+
shouldExclude = true
70+
break
71+
}
72+
}
73+
if shouldExclude {
74+
continue
75+
}
76+
}
77+
4778
// Ensure parent directories exist in the archive
4879
if dir := filepath.Dir(relPath); dir != "." && dir != "" {
4980
// Walk up the directory tree ensuring each level exists
@@ -115,6 +146,10 @@ func ZipDirectory(srcDir, destZip string) error {
115146
}
116147
}
117148

149+
if err := <-errChan; err != nil {
150+
return fmt.Errorf("directory walk failed: %w", err)
151+
}
152+
118153
return nil
119154
}
120155

pkg/util/zip_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package util
2+
3+
import (
4+
"archive/zip"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestZipDirectory(t *testing.T) {
11+
tmpDir, err := os.MkdirTemp("", "zip-test-*")
12+
if err != nil {
13+
t.Fatalf("Failed to create temp dir: %v", err)
14+
}
15+
defer os.RemoveAll(tmpDir)
16+
17+
files := map[string]string{
18+
"manifest.json": `{"name": "test", "version": "1.0"}`,
19+
"background.js": "console.log('background');",
20+
"content.js": "console.log('content');",
21+
"icons/icon.png": "fake-png-data",
22+
"node_modules/dep/foo.js": "should be excluded",
23+
"test.test.js": "should be excluded",
24+
}
25+
26+
for path, content := range files {
27+
fullPath := filepath.Join(tmpDir, path)
28+
dir := filepath.Dir(fullPath)
29+
if err := os.MkdirAll(dir, 0755); err != nil {
30+
t.Fatalf("Failed to create directory %s: %v", dir, err)
31+
}
32+
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
33+
t.Fatalf("Failed to write file %s: %v", fullPath, err)
34+
}
35+
}
36+
37+
tmpZip, err := os.CreateTemp("", "test-zip-*.zip")
38+
if err != nil {
39+
t.Fatalf("Failed to create temp zip: %v", err)
40+
}
41+
tmpZip.Close()
42+
defer os.Remove(tmpZip.Name())
43+
44+
// Test with exclusions
45+
t.Run("with exclusions", func(t *testing.T) {
46+
opts := &ZipOptions{
47+
ExcludeDirectories: []string{"node_modules"},
48+
ExcludeFilenamePatterns: []string{"*.test.js"},
49+
}
50+
if err := ZipDirectory(tmpDir, tmpZip.Name(), opts); err != nil {
51+
t.Fatalf("ZipDirectory failed: %v", err)
52+
}
53+
54+
// Verify the zip contents
55+
r, err := zip.OpenReader(tmpZip.Name())
56+
if err != nil {
57+
t.Fatalf("Failed to open zip: %v", err)
58+
}
59+
defer r.Close()
60+
61+
expectedFiles := map[string]bool{
62+
"manifest.json": false,
63+
"background.js": false,
64+
"content.js": false,
65+
"icons/": false,
66+
"icons/icon.png": false,
67+
}
68+
69+
for _, f := range r.File {
70+
if f.FileInfo().IsDir() {
71+
expectedFiles[f.Name] = true
72+
} else {
73+
if _, ok := expectedFiles[f.Name]; ok {
74+
expectedFiles[f.Name] = true
75+
} else {
76+
t.Errorf("Unexpected file found in zip: %s", f.Name)
77+
}
78+
}
79+
}
80+
81+
for name, found := range expectedFiles {
82+
if !found && name != "icons/" {
83+
t.Errorf("Expected file not found in zip: %s", name)
84+
}
85+
}
86+
})
87+
88+
// Test without exclusions (nil opts)
89+
t.Run("without exclusions", func(t *testing.T) {
90+
tmpZip2, err := os.CreateTemp("", "test-zip-no-exclude-*.zip")
91+
if err != nil {
92+
t.Fatalf("Failed to create temp zip: %v", err)
93+
}
94+
tmpZip2.Close()
95+
defer os.Remove(tmpZip2.Name())
96+
97+
if err := ZipDirectory(tmpDir, tmpZip2.Name(), nil); err != nil {
98+
t.Fatalf("ZipDirectory failed: %v", err)
99+
}
100+
101+
// Verify all files are included (no exclusions)
102+
r, err := zip.OpenReader(tmpZip2.Name())
103+
if err != nil {
104+
t.Fatalf("Failed to open zip: %v", err)
105+
}
106+
defer r.Close()
107+
108+
fileCount := 0
109+
for _, f := range r.File {
110+
if !f.FileInfo().IsDir() {
111+
fileCount++
112+
}
113+
}
114+
if fileCount <= 4 {
115+
t.Errorf("Expected more than 4 files when exclusions are disabled, got %d", fileCount)
116+
}
117+
})
118+
}
119+
120+
func TestUnzip(t *testing.T) {
121+
tmpZip, err := os.CreateTemp("", "test-unzip-*.zip")
122+
if err != nil {
123+
t.Fatalf("Failed to create temp zip: %v", err)
124+
}
125+
tmpZip.Close()
126+
defer os.Remove(tmpZip.Name())
127+
128+
zw, err := os.Create(tmpZip.Name())
129+
if err != nil {
130+
t.Fatalf("Failed to open zip for writing: %v", err)
131+
}
132+
zipWriter := zip.NewWriter(zw)
133+
134+
testFiles := map[string]string{
135+
"file1.txt": "content of file 1",
136+
"subdir/file2.txt": "content of file 2",
137+
}
138+
139+
if _, err := zipWriter.Create("subdir/"); err != nil {
140+
t.Fatalf("Failed to create dir entry: %v", err)
141+
}
142+
143+
for name, content := range testFiles {
144+
w, err := zipWriter.Create(name)
145+
if err != nil {
146+
t.Fatalf("Failed to create zip entry %s: %v", name, err)
147+
}
148+
if _, err := w.Write([]byte(content)); err != nil {
149+
t.Fatalf("Failed to write zip entry %s: %v", name, err)
150+
}
151+
}
152+
zipWriter.Close()
153+
zw.Close()
154+
155+
// Unzip to temp directory
156+
destDir, err := os.MkdirTemp("", "test-unzip-dest-*")
157+
if err != nil {
158+
t.Fatalf("Failed to create dest dir: %v", err)
159+
}
160+
defer os.RemoveAll(destDir)
161+
162+
if err := Unzip(tmpZip.Name(), destDir); err != nil {
163+
t.Fatalf("Unzip failed: %v", err)
164+
}
165+
166+
// Verify extracted files
167+
for name, expectedContent := range testFiles {
168+
path := filepath.Join(destDir, name)
169+
content, err := os.ReadFile(path)
170+
if err != nil {
171+
t.Errorf("Failed to read extracted file %s: %v", name, err)
172+
continue
173+
}
174+
if string(content) != expectedContent {
175+
t.Errorf("File %s: expected %q, got %q", name, expectedContent, string(content))
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)