diff --git a/bundle/generate/downloader.go b/bundle/generate/downloader.go index 1d6ed73ce9..c6583db2ef 100644 --- a/bundle/generate/downloader.go +++ b/bundle/generate/downloader.go @@ -88,7 +88,7 @@ func (n *Downloader) MarkDirectoryForDownload(ctx context.Context, dirPath *stri n.basePath = *dirPath } - objects, err := n.w.Workspace.RecursiveList(ctx, *dirPath) + objects, err := n.recursiveListWithExclusions(ctx, *dirPath) if err != nil { return err } @@ -113,6 +113,37 @@ func (n *Downloader) MarkDirectoryForDownload(ctx context.Context, dirPath *stri return nil } +// recursiveListWithExclusions recursively lists all files in a directory, +// but skips recursing into directories that should be excluded (like node_modules). +func (n *Downloader) recursiveListWithExclusions(ctx context.Context, dirPath string) ([]workspace.ObjectInfo, error) { + var result []workspace.ObjectInfo + + objects, err := n.w.Workspace.ListAll(ctx, workspace.ListWorkspaceRequest{ + Path: dirPath, + }) + if err != nil { + return nil, err + } + + for _, obj := range objects { + if obj.ObjectType == workspace.ObjectTypeDirectory { + if path.Base(obj.Path) == "node_modules" { + continue + } + + subObjects, err := n.recursiveListWithExclusions(ctx, obj.Path) + if err != nil { + return nil, err + } + result = append(result, subObjects...) + } else { + result = append(result, obj) + } + } + + return result, nil +} + type workspaceStatus struct { Language workspace.Language `json:"language,omitempty"` ObjectType workspace.ObjectType `json:"object_type,omitempty"` diff --git a/bundle/generate/downloader_test.go b/bundle/generate/downloader_test.go index d87b373262..3f4dba6aad 100644 --- a/bundle/generate/downloader_test.go +++ b/bundle/generate/downloader_test.go @@ -40,3 +40,57 @@ func TestDownloader_MarkFileReturnsRelativePath(t *testing.T) { require.NoError(t, err) assert.Equal(t, filepath.FromSlash("../source/d"), f2) } + +func TestDownloader_DoesNotRecurseIntoNodeModules(t *testing.T) { + ctx := context.Background() + m := mocks.NewMockWorkspaceClient(t) + + dir := "base/dir" + sourceDir := filepath.Join(dir, "source") + configDir := filepath.Join(dir, "config") + downloader := NewDownloader(m.WorkspaceClient, sourceDir, configDir) + + rootPath := "/workspace/app" + + // Mock the root directory listing + m.GetMockWorkspaceAPI().EXPECT(). + GetStatusByPath(ctx, rootPath). + Return(&workspace.ObjectInfo{Path: rootPath}, nil) + + // Root directory contains: app.py, src/, node_modules/ + m.GetMockWorkspaceAPI().EXPECT(). + ListAll(ctx, workspace.ListWorkspaceRequest{Path: rootPath}). + Return([]workspace.ObjectInfo{ + {Path: "/workspace/app/app.py", ObjectType: workspace.ObjectTypeFile}, + {Path: "/workspace/app/src", ObjectType: workspace.ObjectTypeDirectory}, + {Path: "/workspace/app/node_modules", ObjectType: workspace.ObjectTypeDirectory}, + }, nil) + + // src/ directory contains: index.js + m.GetMockWorkspaceAPI().EXPECT(). + ListAll(ctx, workspace.ListWorkspaceRequest{Path: "/workspace/app/src"}). + Return([]workspace.ObjectInfo{ + {Path: "/workspace/app/src/index.js", ObjectType: workspace.ObjectTypeFile}, + }, nil) + + // We should NOT list node_modules directory - this is the key assertion + // If this expectation is not met, the test will fail + + // Mock file downloads to make markFileForDownload work + m.GetMockWorkspaceAPI().EXPECT(). + GetStatusByPath(ctx, "/workspace/app/app.py"). + Return(&workspace.ObjectInfo{Path: "/workspace/app/app.py"}, nil) + + m.GetMockWorkspaceAPI().EXPECT(). + GetStatusByPath(ctx, "/workspace/app/src/index.js"). + Return(&workspace.ObjectInfo{Path: "/workspace/app/src/index.js"}, nil) + + // Execute + err := downloader.MarkDirectoryForDownload(ctx, &rootPath) + require.NoError(t, err) + + // Verify only 2 files were marked (not any from node_modules) + assert.Len(t, downloader.files, 2) + assert.Contains(t, downloader.files, filepath.Join(sourceDir, "app.py")) + assert.Contains(t, downloader.files, filepath.Join(sourceDir, "src/index.js")) +}