Skip to content

Commit 103130c

Browse files
fjakobsclaude
andauthored
Skip node_modules when downloading app source code (#4274)
## Summary Modifies `databricks bundle generate app` to skip `node_modules` directories when downloading app source code. ## Changes - Added custom recursive listing that checks directories before recursing - Skips any directory named `node_modules` without listing its contents - Reduces API calls and avoids downloading thousands of dependency files ## Implementation The implementation uses `path.Base()` to check if a directory name is "node_modules" before recursing: ```go if path.Base(obj.Path) == "node_modules" { continue } ``` Since we check directories as we encounter them (not after recursing), we only need this simple one-line check. ## Benefits - Faster downloads (skips potentially thousands of files) - Fewer API calls (never lists node_modules contents) - Cleaner bundles (dependencies recreated with npm install) - Standard practice (aligns with .gitignore conventions) ## Testing - Added integration test verifying no recursion into node_modules - All existing tests pass 🤖 Generated with Claude Code Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c6ae350 commit 103130c

File tree

2 files changed

+86
-1
lines changed

2 files changed

+86
-1
lines changed

bundle/generate/downloader.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (n *Downloader) MarkDirectoryForDownload(ctx context.Context, dirPath *stri
8888
n.basePath = *dirPath
8989
}
9090

91-
objects, err := n.w.Workspace.RecursiveList(ctx, *dirPath)
91+
objects, err := n.recursiveListWithExclusions(ctx, *dirPath)
9292
if err != nil {
9393
return err
9494
}
@@ -113,6 +113,37 @@ func (n *Downloader) MarkDirectoryForDownload(ctx context.Context, dirPath *stri
113113
return nil
114114
}
115115

116+
// recursiveListWithExclusions recursively lists all files in a directory,
117+
// but skips recursing into directories that should be excluded (like node_modules).
118+
func (n *Downloader) recursiveListWithExclusions(ctx context.Context, dirPath string) ([]workspace.ObjectInfo, error) {
119+
var result []workspace.ObjectInfo
120+
121+
objects, err := n.w.Workspace.ListAll(ctx, workspace.ListWorkspaceRequest{
122+
Path: dirPath,
123+
})
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
for _, obj := range objects {
129+
if obj.ObjectType == workspace.ObjectTypeDirectory {
130+
if path.Base(obj.Path) == "node_modules" {
131+
continue
132+
}
133+
134+
subObjects, err := n.recursiveListWithExclusions(ctx, obj.Path)
135+
if err != nil {
136+
return nil, err
137+
}
138+
result = append(result, subObjects...)
139+
} else {
140+
result = append(result, obj)
141+
}
142+
}
143+
144+
return result, nil
145+
}
146+
116147
type workspaceStatus struct {
117148
Language workspace.Language `json:"language,omitempty"`
118149
ObjectType workspace.ObjectType `json:"object_type,omitempty"`

bundle/generate/downloader_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,57 @@ func TestDownloader_MarkFileReturnsRelativePath(t *testing.T) {
4040
require.NoError(t, err)
4141
assert.Equal(t, filepath.FromSlash("../source/d"), f2)
4242
}
43+
44+
func TestDownloader_DoesNotRecurseIntoNodeModules(t *testing.T) {
45+
ctx := context.Background()
46+
m := mocks.NewMockWorkspaceClient(t)
47+
48+
dir := "base/dir"
49+
sourceDir := filepath.Join(dir, "source")
50+
configDir := filepath.Join(dir, "config")
51+
downloader := NewDownloader(m.WorkspaceClient, sourceDir, configDir)
52+
53+
rootPath := "/workspace/app"
54+
55+
// Mock the root directory listing
56+
m.GetMockWorkspaceAPI().EXPECT().
57+
GetStatusByPath(ctx, rootPath).
58+
Return(&workspace.ObjectInfo{Path: rootPath}, nil)
59+
60+
// Root directory contains: app.py, src/, node_modules/
61+
m.GetMockWorkspaceAPI().EXPECT().
62+
ListAll(ctx, workspace.ListWorkspaceRequest{Path: rootPath}).
63+
Return([]workspace.ObjectInfo{
64+
{Path: "/workspace/app/app.py", ObjectType: workspace.ObjectTypeFile},
65+
{Path: "/workspace/app/src", ObjectType: workspace.ObjectTypeDirectory},
66+
{Path: "/workspace/app/node_modules", ObjectType: workspace.ObjectTypeDirectory},
67+
}, nil)
68+
69+
// src/ directory contains: index.js
70+
m.GetMockWorkspaceAPI().EXPECT().
71+
ListAll(ctx, workspace.ListWorkspaceRequest{Path: "/workspace/app/src"}).
72+
Return([]workspace.ObjectInfo{
73+
{Path: "/workspace/app/src/index.js", ObjectType: workspace.ObjectTypeFile},
74+
}, nil)
75+
76+
// We should NOT list node_modules directory - this is the key assertion
77+
// If this expectation is not met, the test will fail
78+
79+
// Mock file downloads to make markFileForDownload work
80+
m.GetMockWorkspaceAPI().EXPECT().
81+
GetStatusByPath(ctx, "/workspace/app/app.py").
82+
Return(&workspace.ObjectInfo{Path: "/workspace/app/app.py"}, nil)
83+
84+
m.GetMockWorkspaceAPI().EXPECT().
85+
GetStatusByPath(ctx, "/workspace/app/src/index.js").
86+
Return(&workspace.ObjectInfo{Path: "/workspace/app/src/index.js"}, nil)
87+
88+
// Execute
89+
err := downloader.MarkDirectoryForDownload(ctx, &rootPath)
90+
require.NoError(t, err)
91+
92+
// Verify only 2 files were marked (not any from node_modules)
93+
assert.Len(t, downloader.files, 2)
94+
assert.Contains(t, downloader.files, filepath.Join(sourceDir, "app.py"))
95+
assert.Contains(t, downloader.files, filepath.Join(sourceDir, "src/index.js"))
96+
}

0 commit comments

Comments
 (0)