From 0a641f5db9ca663a246ff97957ab2795ecbe0dca Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 09:24:46 +0000 Subject: [PATCH] Make function name optional in function.json schema, fall back to path-based name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `name` is omitted from a function.json/function.jsonc config file, the function name is now derived from the directory path relative to the functions root — matching the existing behavior for zero-config (config-less) functions. Closes #373 Co-authored-by: Kfir Stri --- src/core/resources/function/config.ts | 12 +++++++++--- src/core/resources/function/schema.ts | 3 ++- tests/core/function-config.spec.ts | 13 ++++++++++++- .../function-discovery/no-name-in-config/entry.ts | 3 +++ .../no-name-in-config/function.jsonc | 3 +++ 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/function-discovery/no-name-in-config/entry.ts create mode 100644 tests/fixtures/function-discovery/no-name-in-config/function.jsonc diff --git a/src/core/resources/function/config.ts b/src/core/resources/function/config.ts index b07f5914..40b153ac 100644 --- a/src/core/resources/function/config.ts +++ b/src/core/resources/function/config.ts @@ -28,9 +28,15 @@ async function readFunctionConfig(configPath: string): Promise { return result.data; } -async function readFunction(configPath: string): Promise { +async function readFunction( + configPath: string, + functionsDir: string, +): Promise { const config = await readFunctionConfig(configPath); const functionDir = dirname(configPath); + const name = + config.name ?? + relative(functionsDir, functionDir).split(/[/\\]/).join("/"); const entryPath = join(functionDir, config.entry); if (!(await pathExists(entryPath))) { @@ -47,7 +53,7 @@ async function readFunction(configPath: string): Promise { absolute: true, }); - const functionData: BackendFunction = { ...config, entryPath, filePaths }; + const functionData: BackendFunction = { ...config, name, entryPath, filePaths }; return functionData; } @@ -76,7 +82,7 @@ export async function readAllFunctions( ); const functionsFromConfig = await Promise.all( - configFiles.map((configPath) => readFunction(configPath)), + configFiles.map((configPath) => readFunction(configPath, functionsDir)), ); const functionsWithoutConfig = await Promise.all( diff --git a/src/core/resources/function/schema.ts b/src/core/resources/function/schema.ts index 50f355cb..68f0b82c 100644 --- a/src/core/resources/function/schema.ts +++ b/src/core/resources/function/schema.ts @@ -74,12 +74,13 @@ const AutomationSchema = z.union([ ]); export const FunctionConfigSchema = z.object({ - name: FunctionNameSchema, + name: FunctionNameSchema.optional(), entry: z.string().min(1, "Entry point cannot be empty"), automations: z.array(AutomationSchema).optional(), }); const BackendFunctionSchema = FunctionConfigSchema.extend({ + name: FunctionNameSchema, entryPath: z.string().min(1, "Entry path cannot be empty"), filePaths: z.array(z.string()).min(1, "Function must have at least one file"), }); diff --git a/tests/core/function-config.spec.ts b/tests/core/function-config.spec.ts index ab58f3f5..f8390276 100644 --- a/tests/core/function-config.spec.ts +++ b/tests/core/function-config.spec.ts @@ -16,13 +16,14 @@ describe("readAllFunctions", () => { const functionsDir = resolve(FIXTURES_DIR, "function-discovery"); const result = await readAllFunctions(functionsDir); - expect(result).toHaveLength(4); // foo/bar, foo/kfir/hello, stam, with-config (config-based) + expect(result).toHaveLength(5); // foo/bar, foo/kfir/hello, stam, with-config (config-based), no-name-in-config (config-based, no name) const names = result.map((f) => f.name).sort(); expect(names).toContain("foo/bar"); expect(names).toContain("foo/kfir/hello"); expect(names).toContain("stam"); expect(names).toContain("custom-name"); + expect(names).toContain("no-name-in-config"); const fooBar = result.find((f) => f.name === "foo/bar"); expect(fooBar).toBeDefined(); @@ -46,6 +47,16 @@ describe("readAllFunctions", () => { expect(withConfig?.name).toBe("custom-name"); }); + it("uses path-based name when function.jsonc omits the name field", async () => { + const functionsDir = resolve(FIXTURES_DIR, "function-discovery"); + const result = await readAllFunctions(functionsDir); + + const noName = result.find((f) => f.name === "no-name-in-config"); + expect(noName).toBeDefined(); + expect(noName?.entry).toBe("entry.ts"); + expect(noName?.entryPath).toContain("no-name-in-config/entry.ts"); + }); + it("includes files recursively in filePaths for zero-config functions", async () => { const functionsDir = resolve(FIXTURES_DIR, "function-discovery"); const result = await readAllFunctions(functionsDir); diff --git a/tests/fixtures/function-discovery/no-name-in-config/entry.ts b/tests/fixtures/function-discovery/no-name-in-config/entry.ts new file mode 100644 index 00000000..d7a25cfc --- /dev/null +++ b/tests/fixtures/function-discovery/no-name-in-config/entry.ts @@ -0,0 +1,3 @@ +export default function handler() { + return { ok: true }; +} diff --git a/tests/fixtures/function-discovery/no-name-in-config/function.jsonc b/tests/fixtures/function-discovery/no-name-in-config/function.jsonc new file mode 100644 index 00000000..a4dda74d --- /dev/null +++ b/tests/fixtures/function-discovery/no-name-in-config/function.jsonc @@ -0,0 +1,3 @@ +{ + "entry": "entry.ts" +}