From 73176fcd6df70efc92ea81f748e154dd11114693 Mon Sep 17 00:00:00 2001 From: Oliver Gilan Date: Mon, 13 Apr 2026 18:38:20 -0700 Subject: [PATCH] Allow skipping directories during type generation Add an app-level typeIgnore config and thread it into SST's project discovery so JavaScript and Python projects can opt out of generated type files. --- internal/fs/fs.go | 31 ++++++++++++++++++++++ internal/fs/fs_test.go | 14 ++++++++++ pkg/project/project.go | 1 + pkg/project/run.go | 2 +- pkg/types/python/python.go | 4 +-- pkg/types/python/python_test.go | 25 +++++++++++++++--- pkg/types/rails/rails.go | 5 +++- pkg/types/types.go | 6 ++--- pkg/types/typescript/typescript.go | 4 +-- pkg/types/typescript/typescript_test.go | 35 ++++++++++++++++++++----- platform/src/config.ts | 19 ++++++++++++++ 11 files changed, 126 insertions(+), 20 deletions(-) diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 79e2316ac4..a87b99083b 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -43,7 +43,22 @@ func IsGitSubmodule(dir string) bool { } func FindDown(dir, filename string) []string { + return FindDownWithIgnore(dir, filename, nil) +} + +func FindDownWithIgnore(dir, filename string, ignore []string) []string { var result []string + ignored := make([]string, 0, len(ignore)) + for _, item := range ignore { + if item == "" { + continue + } + if filepath.IsAbs(item) { + ignored = append(ignored, filepath.Clean(item)) + continue + } + ignored = append(ignored, filepath.Clean(filepath.Join(dir, item))) + } filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -57,6 +72,11 @@ func FindDown(dir, filename string) []string { if path != dir && IsGitSubmodule(path) { return filepath.SkipDir } + for _, ignoredPath := range ignored { + if isWithin(ignoredPath, path) { + return filepath.SkipDir + } + } } if !info.IsDir() && info.Name() == filename { result = append(result, path) @@ -66,3 +86,14 @@ func FindDown(dir, filename string) []string { return result } + +func isWithin(parent, path string) bool { + rel, err := filepath.Rel(parent, path) + if err != nil { + return false + } + if rel == "." { + return true + } + return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) +} diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go index faadbc7428..328f1cc9f7 100644 --- a/internal/fs/fs_test.go +++ b/internal/fs/fs_test.go @@ -90,6 +90,20 @@ func TestFindDown(t *testing.T) { assert.Equal(t, []string{filepath.Join(normal, "package.json")}, results) }) + t.Run("skips ignored directories", func(t *testing.T) { + dir := t.TempDir() + ignored := filepath.Join(dir, "packages", "docs") + included := filepath.Join(dir, "packages", "web") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "package.json"), []byte("{}"), 0644) + includedFile := filepath.Join(included, "package.json") + os.WriteFile(includedFile, []byte("{}"), 0644) + + results := fs.FindDownWithIgnore(dir, "package.json", []string{"packages/docs"}) + assert.Equal(t, []string{includedFile}, results) + }) + t.Run("does not skip dirs with .git directory", func(t *testing.T) { dir := t.TempDir() diff --git a/pkg/project/project.go b/pkg/project/project.go index d3685a511f..018d26ba62 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -36,6 +36,7 @@ type App struct { Version string `json:"version"` Protect bool `json:"protect"` Watch []string `json:"watch"` + TypeIgnore []string `json:"typeIgnore"` // Deprecated: Backend is now Home Backend string `json:"backend"` // Deprecated: RemovalPolicy is now Removal diff --git a/pkg/project/run.go b/pkg/project/run.go index a21c54280d..549e08b2a5 100644 --- a/pkg/project/run.go +++ b/pkg/project/run.go @@ -634,7 +634,7 @@ loop: complete.Finished = finished complete.Errors = errors complete.ImportDiffs = importDiffs - types.Generate(p.PathConfig(), complete.Links) + types.Generate(p.PathConfig(), complete.Links, p.App().TypeIgnore) defer bus.Publish(complete) if input.Command != "diff" { diff --git a/pkg/types/python/python.go b/pkg/types/python/python.go index 2735ad9e34..b7bd7b04df 100644 --- a/pkg/types/python/python.go +++ b/pkg/types/python/python.go @@ -11,8 +11,8 @@ import ( "github.com/sst/sst/v3/pkg/project/common" ) -func Generate(root string, links common.Links) error { - projects := fs.FindDown(root, "pyproject.toml") +func Generate(root string, links common.Links, ignore []string) error { + projects := fs.FindDownWithIgnore(root, "pyproject.toml", ignore) files := []io.Writer{} for _, project := range projects { path := filepath.Join(filepath.Dir(project), "sst.pyi") diff --git a/pkg/types/python/python_test.go b/pkg/types/python/python_test.go index 9d8c712b53..f7146cdfa3 100644 --- a/pkg/types/python/python_test.go +++ b/pkg/types/python/python_test.go @@ -14,7 +14,7 @@ import ( func TestGenerate(t *testing.T) { t.Run("no pyproject.toml returns nil", func(t *testing.T) { dir := t.TempDir() - err := python.Generate(dir, common.Links{}) + err := python.Generate(dir, common.Links{}, nil) require.NoError(t, err) }) @@ -31,7 +31,7 @@ func TestGenerate(t *testing.T) { }, } - err := python.Generate(dir, links) + err := python.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst.pyi")) @@ -60,7 +60,7 @@ func TestGenerate(t *testing.T) { }, } - err := python.Generate(dir, links) + err := python.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst.pyi")) @@ -79,11 +79,28 @@ func TestGenerate(t *testing.T) { os.MkdirAll(sub, 0755) os.WriteFile(filepath.Join(sub, "pyproject.toml"), []byte("[project]"), 0644) - err := python.Generate(dir, common.Links{}) + err := python.Generate(dir, common.Links{}, nil) require.NoError(t, err) assert.FileExists(t, filepath.Join(sub, "sst.pyi")) }) + + t.Run("ignores configured directories", func(t *testing.T) { + dir := t.TempDir() + ignored := filepath.Join(dir, "services", "legacy") + included := filepath.Join(dir, "services", "api") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "pyproject.toml"), []byte("[project]"), 0644) + os.WriteFile(filepath.Join(included, "pyproject.toml"), []byte("[project]"), 0644) + + err := python.Generate(dir, common.Links{}, []string{"services/legacy"}) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(included, "sst.pyi")) + _, err = os.Stat(filepath.Join(ignored, "sst.pyi")) + assert.True(t, os.IsNotExist(err)) + }) } func indexOf(s, substr string) int { diff --git a/pkg/types/rails/rails.go b/pkg/types/rails/rails.go index 1ed743b87b..a33f018b83 100644 --- a/pkg/types/rails/rails.go +++ b/pkg/types/rails/rails.go @@ -5,7 +5,10 @@ import ( ) // Generate is stubbed out — Rails support is not yet implemented. -func Generate(root string, links common.Links) error { +func Generate(root string, links common.Links, ignore []string) error { + _ = root + _ = links + _ = ignore return nil // projects := fs.FindDown(root, "config.ru") // files := []io.Writer{} diff --git a/pkg/types/types.go b/pkg/types/types.go index 36fd70cff6..3b80deacfc 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -10,9 +10,9 @@ import ( "github.com/sst/sst/v3/pkg/types/typescript" ) -type Generator = func(root string, complete common.Links) error +type Generator = func(root string, complete common.Links, ignore []string) error -func Generate(cfgPath string, complete common.Links) error { +func Generate(cfgPath string, complete common.Links, ignore []string) error { root := path.ResolveRootDir(cfgPath) // gitroot, err := fs.FindUp(root, ".git") // if err == nil { @@ -20,7 +20,7 @@ func Generate(cfgPath string, complete common.Links) error { // } slog.Info("generating types", "root", root) for _, generator := range All { - err := generator(root, complete) + err := generator(root, complete, ignore) if err != nil { return err } diff --git a/pkg/types/typescript/typescript.go b/pkg/types/typescript/typescript.go index 967b852ab6..ebc55ff55c 100644 --- a/pkg/types/typescript/typescript.go +++ b/pkg/types/typescript/typescript.go @@ -23,7 +23,7 @@ var mapping = map[string]string{ "versionMetadataBindings": "WorkerVersionMetadata", } -func Generate(root string, links common.Links) error { +func Generate(root string, links common.Links, ignore []string) error { cloudflareBindings := map[string]string{} for name, link := range links { for _, include := range link.Include { @@ -50,7 +50,7 @@ func Generate(root string, links common.Links) error { "export {}", }, "\n")) - packageJsons := fs.FindDown(root, "package.json") + packageJsons := fs.FindDownWithIgnore(root, "package.json", ignore) rootEnv := filepath.Join(root, "sst-env.d.ts") for _, packageJson := range packageJsons { packageJsonFile, err := os.Open(packageJson) diff --git a/pkg/types/typescript/typescript_test.go b/pkg/types/typescript/typescript_test.go index 7c1d18db1c..3d37baa0a9 100644 --- a/pkg/types/typescript/typescript_test.go +++ b/pkg/types/typescript/typescript_test.go @@ -24,7 +24,7 @@ func setupProject(t *testing.T, deps map[string]string) string { func TestGenerate(t *testing.T) { t.Run("no package.json returns nil", func(t *testing.T) { dir := t.TempDir() - err := typescript.Generate(dir, common.Links{}) + err := typescript.Generate(dir, common.Links{}, nil) require.NoError(t, err) }) @@ -40,7 +40,7 @@ func TestGenerate(t *testing.T) { }, } - err := typescript.Generate(dir, links) + err := typescript.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) @@ -66,7 +66,7 @@ func TestGenerate(t *testing.T) { }, } - err := typescript.Generate(dir, links) + err := typescript.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) @@ -91,7 +91,7 @@ func TestGenerate(t *testing.T) { }, } - err := typescript.Generate(dir, links) + err := typescript.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) @@ -114,7 +114,7 @@ func TestGenerate(t *testing.T) { }, } - err := typescript.Generate(dir, links) + err := typescript.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) @@ -140,7 +140,7 @@ func TestGenerate(t *testing.T) { }, } - err := typescript.Generate(dir, links) + err := typescript.Generate(dir, links, nil) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) @@ -160,12 +160,33 @@ func TestGenerate(t *testing.T) { os.MkdirAll(sub, 0755) os.WriteFile(filepath.Join(sub, "package.json"), pkg, 0644) - err := typescript.Generate(dir, common.Links{}) + err := typescript.Generate(dir, common.Links{}, nil) require.NoError(t, err) assert.FileExists(t, filepath.Join(dir, "sst-env.d.ts")) assert.FileExists(t, filepath.Join(sub, "sst-env.d.ts")) }) + + t.Run("ignores configured directories", func(t *testing.T) { + dir := t.TempDir() + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + ignored := filepath.Join(dir, "packages", "docs") + included := filepath.Join(dir, "packages", "web") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "package.json"), pkg, 0644) + os.WriteFile(filepath.Join(included, "package.json"), pkg, 0644) + + err := typescript.Generate(dir, common.Links{}, []string{"packages/docs"}) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(dir, "sst-env.d.ts")) + assert.FileExists(t, filepath.Join(included, "sst-env.d.ts")) + _, err = os.Stat(filepath.Join(ignored, "sst-env.d.ts")) + assert.True(t, os.IsNotExist(err)) + }) } func indexOf(s, substr string) int { diff --git a/platform/src/config.ts b/platform/src/config.ts index c8297e4326..c7d54944df 100644 --- a/platform/src/config.ts +++ b/platform/src/config.ts @@ -286,6 +286,25 @@ export interface App { * The paths are relative to the project root. */ watch?: string[]; + + /** + * Configure which directories should be ignored when generating `sst-env.d.ts` + * and `sst.pyi` files. + * By default, type files are generated for all JavaScript and Python projects + * (except node_modules and hidden directories). + * + * @example + * ```ts + * { + * typeIgnore: ["packages/docs", "services/legacy-python"] + * } + * ``` + * + * This will skip generating type files inside the `packages/docs` and + * `services/legacy-python` directories. + * The paths are relative to the project root. + */ + typeIgnore?: string[]; } export interface AppInput {