Skip to content
Open
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
62 changes: 60 additions & 2 deletions src/core/resources/function/config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -42,22 +43,79 @@ export async function readFunction(configPath: string): Promise<Function> {
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<Function | null> {
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<Function[]> {
if (!(await pathExists(functionsDir))) {
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<string>();
for (const fn of functions) {
if (names.has(fn.name)) {
Expand Down
26 changes: 26 additions & 0 deletions tests/core/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/with-mixed-functions/config.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Mixed Functions Test Project",
"entitiesDir": "entities",
"functionsDir": "functions"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function main(req: Request) {
return new Response(JSON.stringify({ message: "my-complex-func" }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "my-func",
"entry": "index.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function main(req: Request) {
return new Response(JSON.stringify({ message: "my-func" }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function main(req: Request) {
return new Response(JSON.stringify({ message: "other-func" }));
}