Skip to content

fix: support main.ts as an entrypoint #3201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions internal/functions/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool,
}

func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) {
pattern := filepath.Join(utils.FunctionsDir, "*", "index.ts")
pattern := filepath.Join(utils.FunctionsDir, "*", "*.ts")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A wildcard match here could open a can of worms with existing CI/CD pipelines as shared modules may be unintentionally deployed. I'd rather let users opt-in by declaring in config. For eg. if they declare [functions.some-slug] in config.toml, we can automatically check for both index.ts followed by main.ts.

Copy link
Contributor Author

@laktek laktek Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm..don't we already have a check to exclude shared modules?

if utils.FuncSlugPattern.MatchString(slug) {
(we already do a wildcard match on the directory, I don't think this makes it worse than that)

If there's a Glob to explicitly match both index.ts and main.ts, I'm happy to change that (tried, but I couldn't get it to work).

The problem with having [functions.some-slug] in config.toml is users have to add it manually. And if they do deno init --serve supabase/functions/my-func and then try to access the function it wouldn't work. If we can auto-populate the valid function slugs, it would improve the DX a lot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we already do a wildcard match on the directory, I don't think this makes it worse than that)

The existing wildcard match does NOT eliminate shared modules like functions/_shared/index.ts. It will deploy a function named _shared.

Unfortunately this isn't something we can easily change now without risking breaking user's CI/CD pipeline. Because users may very well have a function named _something and expects it to be deployed.

IMO, the root of this evil is due to the implicit behaviour of finding entrypoints by glob patterns. It was ok for small scale local development, but not acceptable for CI/CD when you have 100s of functions to manage.

And if they do deno init --serve supabase/functions/my-func and then try to access the function it wouldn't work.

I suspect you've misunderstood my suggestion. I'm saying to parse main.ts as fallback entrypoint in pkg/config, not to completely ignore it. That way, both deno init --serve and functions deploy work.

paths, err := afero.Glob(fsys, pattern)
if err != nil {
return nil, errors.Errorf("failed to glob function slugs: %w", err)
Expand Down Expand Up @@ -97,7 +97,15 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool,
// Precedence order: flag > config > fallback
functionDir := filepath.Join(utils.FunctionsDir, name)
if len(function.Entrypoint) == 0 {
function.Entrypoint = filepath.Join(functionDir, "index.ts")
indexEntrypoint := filepath.Join(functionDir, "index.ts")
mainEntrypoint := filepath.Join(functionDir, "main.ts")
if _, err := fsys.Stat(indexEntrypoint); err == nil {
function.Entrypoint = indexEntrypoint
} else if _, err := fsys.Stat(mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
} else {
return nil, errors.Errorf("Cannot find a valid entrypoint file (index.ts or main.ts) for the '%s' function. Set the custom entrypoint path in config.toml", name)
}
Comment on lines +100 to +108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
indexEntrypoint := filepath.Join(functionDir, "index.ts")
mainEntrypoint := filepath.Join(functionDir, "main.ts")
if _, err := fsys.Stat(indexEntrypoint); err == nil {
function.Entrypoint = indexEntrypoint
} else if _, err := fsys.Stat(mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
} else {
return nil, errors.Errorf("Cannot find a valid entrypoint file (index.ts or main.ts) for the '%s' function. Set the custom entrypoint path in config.toml", name)
}
function.Entrypoint = filepath.Join(functionDir, "index.ts")
if _, err := fs.Stat(fsys, function.Entrypoint); errors.Is(err, os.ErrNotExist) {
mainEntrypoint := filepath.Join(functionDir, "main.ts")
if _, err := fs.Stat(fsys, mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
}
}

ditto

One way to reduce duplication is to add a function.ResolvePaths method. I can work on that separately.

}
if len(importMapPath) > 0 {
function.ImportMap = importMapPath
Expand Down
113 changes: 113 additions & 0 deletions internal/functions/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func TestDeployCommand(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))

require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug+"-2", "index.ts"), []byte{}, 0644))

// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
Expand Down Expand Up @@ -120,6 +124,47 @@ import_map = "./import_map.json"
assert.Empty(t, apitest.ListUnmatchedRequests())
})

t.Run("deploys functions with main.ts as entrypoint", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()

// Setup function entrypoint
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "main.ts")
require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644))

// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err := fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("slug", slug).
ParamPresent("import_map_path").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
err = Run(context.Background(), []string{slug}, true, nil, "", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})

t.Run("skip disabled functions from config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
Expand Down Expand Up @@ -190,6 +235,47 @@ import_map = "./import_map.json"
assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions")
})

t.Run("throws error on missing entrypoint", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))

// Setup function entrypoint
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "not-entrypoint.ts")
require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644))

// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err := fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("slug", slug).
ParamPresent("import_map_path").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
err = Run(context.Background(), []string{slug}, true, nil, "", 1, fsys)
// Check error
assert.ErrorContains(t, err, "Cannot find a valid entrypoint file")
})

t.Run("verify_jwt param falls back to config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
Expand All @@ -203,6 +289,10 @@ verify_jwt = false
`)
require.NoError(t, err)
require.NoError(t, f.Close())

// Setup function entrypoint
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644))

// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
Expand Down Expand Up @@ -246,6 +336,10 @@ verify_jwt = false
`)
require.NoError(t, err)
require.NoError(t, f.Close())

// Setup function entrypoint
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644))

// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
Expand Down Expand Up @@ -283,6 +377,10 @@ func TestImportMapPath(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))

// Write function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte{}, 0644))

// Run test
fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys)
// Check error
Expand All @@ -299,6 +397,10 @@ func TestImportMapPath(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))

// Write function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644))

// Run test
fc, err := GetFunctionConfig([]string{slug}, "", nil, fsys)
// Check error
Expand All @@ -319,6 +421,9 @@ func TestImportMapPath(t *testing.T) {
require.NoError(t, afero.WriteFile(fsys, customImportMapPath, []byte("{}"), 0644))
// Create fallback import map to test precedence order
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))
// Write function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644))

// Run test
fc, err := GetFunctionConfig([]string{slug}, customImportMapPath, cast.Ptr(false), fsys)
// Check error
Expand All @@ -329,6 +434,10 @@ func TestImportMapPath(t *testing.T) {
t.Run("returns empty string if no fallback", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()

// Write function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte{}, 0644))

// Run test
fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys)
// Check error
Expand All @@ -341,6 +450,10 @@ func TestImportMapPath(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))

// Write function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte{}, 0644))

// Run test
fc, err := GetFunctionConfig([]string{"test"}, path, nil, fsys)
// Check error
Expand Down
8 changes: 7 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,13 @@ func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error {
// Resolve functions config
for slug, function := range c.Functions {
if len(function.Entrypoint) == 0 {
function.Entrypoint = filepath.Join(builder.FunctionsDir, slug, "index.ts")
indexEntrypoint := filepath.Join(builder.FunctionsDir, slug, "index.ts")
mainEntrypoint := filepath.Join(builder.FunctionsDir, slug, "main.ts")
if _, err := fs.Stat(fsys, indexEntrypoint); err == nil {
function.Entrypoint = indexEntrypoint
} else if _, err := fs.Stat(fsys, mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
}
Comment on lines +657 to +663
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
indexEntrypoint := filepath.Join(builder.FunctionsDir, slug, "index.ts")
mainEntrypoint := filepath.Join(builder.FunctionsDir, slug, "main.ts")
if _, err := fs.Stat(fsys, indexEntrypoint); err == nil {
function.Entrypoint = indexEntrypoint
} else if _, err := fs.Stat(fsys, mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
}
function.Entrypoint = filepath.Join(builder.FunctionsDir, slug, "index.ts")
if _, err := fs.Stat(fsys, function.Entrypoint); errors.Is(err, os.ErrNotExist) {
mainEntrypoint := filepath.Join(builder.FunctionsDir, slug, "main.ts")
if _, err := fs.Stat(fsys, mainEntrypoint); err == nil {
function.Entrypoint = mainEntrypoint
}
}

We should still pass down default entrypoint path.

} else if !filepath.IsAbs(function.Entrypoint) {
// Append supabase/ because paths in configs are specified relative to config.toml
function.Entrypoint = filepath.Join(builder.SupabaseDirPath, function.Entrypoint)
Expand Down
48 changes: 48 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,54 @@ func TestLoadEnv(t *testing.T) {
assert.Equal(t, "test-root-key", config.Db.RootKey)
}

func TestLoadFunctionEntrypoint(t *testing.T) {
t.Run("uses custom entrypoint when defined", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
entrypoint = "./functions/hello/foo.ts"
`)},
"supabase/functions/hello/foo.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that deno.json was set as import map
assert.Equal(t, "supabase/functions/hello/foo.ts", config.Functions["hello"].Entrypoint)
})

t.Run("set index.ts as the entrypoint when file is present and no explicit entrypoint set", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/index.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that deno.json was set as import map
assert.Equal(t, "supabase/functions/hello/index.ts", config.Functions["hello"].Entrypoint)
})

t.Run("set main.ts as the entrypoint when file is present and no explicit entrypoint set", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/main.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that deno.json was set as import map
assert.Equal(t, "supabase/functions/hello/main.ts", config.Functions["hello"].Entrypoint)
})
}

func TestLoadFunctionImportMap(t *testing.T) {
t.Run("uses deno.json as import map when present", func(t *testing.T) {
config := NewConfig()
Expand Down