From cbb297814472ca7070e7dab40617746ae824063a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:29:36 +0000 Subject: [PATCH] feat: support function folders without function.jsonc file Add auto-detection for function folders that contain only index.ts. The function name is automatically derived from the folder name using kebab-case. Changes: - Added readFunctionFromDirectory() to detect functions without config files - Updated readAllFunctions() to scan for both configured and auto-detected functions - Added test fixtures and test case for mixed function types - Maintains backward compatibility with existing function.json/jsonc files Resolves #130 Co-authored-by: Kfir Stri --- src/core/resources/function/config.ts | 62 ++++++++++++++++++- tests/core/project.test.ts | 26 ++++++++ .../with-mixed-functions/config.jsonc | 5 ++ .../functions/MyComplexFunc/index.ts | 3 + .../functions/my-func/function.jsonc | 4 ++ .../functions/my-func/index.ts | 3 + .../functions/other-func/index.ts | 3 + 7 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/with-mixed-functions/config.jsonc create mode 100644 tests/fixtures/with-mixed-functions/functions/MyComplexFunc/index.ts create mode 100644 tests/fixtures/with-mixed-functions/functions/my-func/function.jsonc create mode 100644 tests/fixtures/with-mixed-functions/functions/my-func/index.ts create mode 100644 tests/fixtures/with-mixed-functions/functions/other-func/index.ts diff --git a/src/core/resources/function/config.ts b/src/core/resources/function/config.ts index 137e3b72..7c0d4b60 100644 --- a/src/core/resources/function/config.ts +++ b/src/core/resources/function/config.ts @@ -1,5 +1,6 @@ -import { dirname, join } from "node:path"; +import { dirname, join, basename } from "node:path"; import { globby } from "globby"; +import kebabCase from "lodash.kebabcase"; import { FUNCTION_CONFIG_FILE } from "@/core/consts.js"; import { readJsonFile, pathExists } from "@/core/utils/fs.js"; import { FunctionConfigSchema, FunctionSchema } from "@/core/resources/function/schema.js"; @@ -42,6 +43,38 @@ export async function readFunction(configPath: string): Promise { return result.data; } +/** + * Creates a function from a directory without a config file. + * Looks for index.ts and uses the folder name as the function name. + */ +export async function readFunctionFromDirectory( + functionDir: string +): Promise { + const indexPath = join(functionDir, "index.ts"); + + if (!(await pathExists(indexPath))) { + return null; + } + + const folderName = basename(functionDir); + const functionName = kebabCase(folderName); + + const functionData = { + name: functionName, + entry: "index.ts", + codePath: indexPath, + }; + + const result = FunctionSchema.safeParse(functionData); + if (!result.success) { + throw new Error( + `Invalid auto-detected function in ${functionDir}: ${result.error.message}` + ); + } + + return result.data; +} + export async function readAllFunctions( functionsDir: string ): Promise { @@ -49,15 +82,40 @@ export async function readAllFunctions( return []; } + // Read functions with config files const configFiles = await globby(`*/${FUNCTION_CONFIG_FILE}`, { cwd: functionsDir, absolute: true, }); - const functions = await Promise.all( + const functionsWithConfig = await Promise.all( configFiles.map((configPath) => readFunction(configPath)) ); + // Find all directories that don't have config files + const allDirs = await globby("*/", { + cwd: functionsDir, + absolute: true, + onlyDirectories: true, + }); + + const dirsWithConfig = new Set( + configFiles.map((configPath) => dirname(configPath)) + ); + + const dirsWithoutConfig = allDirs.filter((dir) => !dirsWithConfig.has(dir)); + + // Try to read functions from directories without config files + const autoDetectedFunctions = ( + await Promise.all( + dirsWithoutConfig.map((dir) => readFunctionFromDirectory(dir)) + ) + ).filter((fn): fn is Function => fn !== null); + + // Combine both types of functions + const functions = [...functionsWithConfig, ...autoDetectedFunctions]; + + // Check for duplicate names const names = new Set(); for (const fn of functions) { if (names.has(fn.name)) { diff --git a/tests/core/project.test.ts b/tests/core/project.test.ts index 420b7f26..06534d3c 100644 --- a/tests/core/project.test.ts +++ b/tests/core/project.test.ts @@ -37,6 +37,32 @@ describe("readProjectConfig", () => { expect(result.functions[0].entry).toBe("index.ts"); }); + it("reads project with mixed functions (with and without config files)", async () => { + const result = await readProjectConfig( + resolve(FIXTURES_DIR, "with-mixed-functions") + ); + + expect(result.functions).toHaveLength(3); + + const functionNames = result.functions.map((f) => f.name).sort(); + expect(functionNames).toEqual(["my-complex-func", "my-func", "other-func"]); + + // Check function with config file + const myFunc = result.functions.find((f) => f.name === "my-func"); + expect(myFunc).toBeDefined(); + expect(myFunc?.entry).toBe("index.ts"); + + // Check auto-detected function + const otherFunc = result.functions.find((f) => f.name === "other-func"); + expect(otherFunc).toBeDefined(); + expect(otherFunc?.entry).toBe("index.ts"); + + // Check auto-detected function with kebab-cased name + const myComplexFunc = result.functions.find((f) => f.name === "my-complex-func"); + expect(myComplexFunc).toBeDefined(); + expect(myComplexFunc?.entry).toBe("index.ts"); + }); + // Error cases it("throws when no config file exists", async () => { await expect( diff --git a/tests/fixtures/with-mixed-functions/config.jsonc b/tests/fixtures/with-mixed-functions/config.jsonc new file mode 100644 index 00000000..ddcea682 --- /dev/null +++ b/tests/fixtures/with-mixed-functions/config.jsonc @@ -0,0 +1,5 @@ +{ + "name": "Mixed Functions Test Project", + "entitiesDir": "entities", + "functionsDir": "functions" +} diff --git a/tests/fixtures/with-mixed-functions/functions/MyComplexFunc/index.ts b/tests/fixtures/with-mixed-functions/functions/MyComplexFunc/index.ts new file mode 100644 index 00000000..95671534 --- /dev/null +++ b/tests/fixtures/with-mixed-functions/functions/MyComplexFunc/index.ts @@ -0,0 +1,3 @@ +export default async function main(req: Request) { + return new Response(JSON.stringify({ message: "my-complex-func" })); +} diff --git a/tests/fixtures/with-mixed-functions/functions/my-func/function.jsonc b/tests/fixtures/with-mixed-functions/functions/my-func/function.jsonc new file mode 100644 index 00000000..82387604 --- /dev/null +++ b/tests/fixtures/with-mixed-functions/functions/my-func/function.jsonc @@ -0,0 +1,4 @@ +{ + "name": "my-func", + "entry": "index.ts" +} diff --git a/tests/fixtures/with-mixed-functions/functions/my-func/index.ts b/tests/fixtures/with-mixed-functions/functions/my-func/index.ts new file mode 100644 index 00000000..473af1b2 --- /dev/null +++ b/tests/fixtures/with-mixed-functions/functions/my-func/index.ts @@ -0,0 +1,3 @@ +export default async function main(req: Request) { + return new Response(JSON.stringify({ message: "my-func" })); +} diff --git a/tests/fixtures/with-mixed-functions/functions/other-func/index.ts b/tests/fixtures/with-mixed-functions/functions/other-func/index.ts new file mode 100644 index 00000000..5559b2a8 --- /dev/null +++ b/tests/fixtures/with-mixed-functions/functions/other-func/index.ts @@ -0,0 +1,3 @@ +export default async function main(req: Request) { + return new Response(JSON.stringify({ message: "other-func" })); +}