Skip to content

Commit

Permalink
Add Next.js Support to ts-codegenerator (#12)
Browse files Browse the repository at this point in the history
* feat: Add Next.js codebase scanning and server action generation

- Add Next.js specific types, scanner, and generator
- Update root index.ts to use consistent export strategy

* feat: add type support for jest

- Add new function `analyzeNextjsSourceFiles` to handle the source files
- Update `scanNextjsCodebase` to call `analyzeNextjsSourceFiles` and return the result
- Add tests
- Update imports
- add @next/eslint-plugin-next to next project
  • Loading branch information
ozhanefemeral authored Jul 23, 2024
1 parent 52b694c commit 6ebd3fc
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 1 deletion.
1 change: 1 addition & 0 deletions apps/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"react-dom": "^18"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.2.5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
5 changes: 5 additions & 0 deletions packages/ts-generator/.changeset/tame-worms-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ozhanefe/ts-codegenerator": minor
---

nextjs support
5 changes: 5 additions & 0 deletions packages/ts-generator/.changeset/warm-pots-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ozhanefe/ts-codegenerator": minor
---

add nextjs parsing/generator features
2 changes: 2 additions & 0 deletions packages/ts-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.7",
"@jest/types": "^29.6.3",
"@types/jest": "^29.5.12",
"tsup": "^8.1.2",
"eslint": "^8.57.0",
"jest": "^29.7.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/ts-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ export type {
} from "./types";

export { scanCodebase } from "./codebase-scanner";

export type { NextCodebaseInfo, ServerActionInfo } from "./nextjs";
export { scanNextjsCodebase, generateServerAction } from "./nextjs";
export * as NextJS from "./nextjs";
6 changes: 5 additions & 1 deletion packages/ts-generator/src/module-parser/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { getFunctionInfoFromNode, parseFunctionsFromFile } from "./parser";
export {
getFunctionInfoFromNode,
parseFunctionsFromFile,
parseFunctionsFromText,
} from "./parser";
export { getFunctionVariables } from "./utils";
96 changes: 96 additions & 0 deletions packages/ts-generator/src/nextjs/__tests__/nextjs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Project } from "ts-morph";
import {
analyzeNextjsSourceFiles,
generateServerAction,
ServerActionInfo,
} from "../index";

describe("Next.js functionality", () => {
let project: Project;

beforeEach(() => {
project = new Project({ useInMemoryFileSystem: true });
});

describe("analyzeNextjsSourceFiles", () => {
it("should correctly identify and parse server actions", () => {
const serverActionCode = `
"use server";
export async function submitForm(data: FormData) {
// Server action logic
}
export async function deleteItem(id: string) {
// Delete item logic
}
`;

const sourceFile = project.createSourceFile(
"app/actions.ts",
serverActionCode
);

const result = analyzeNextjsSourceFiles([sourceFile]);

expect(result.serverActions).toHaveLength(2);
expect(result.serverActions[0]?.name).toBe("submitForm");
expect(result.serverActions[1]?.name).toBe("deleteItem");
});

it("should not identify non-server actions", () => {
const regularCode = `
export function regularFunction() {
// Regular function logic
}
`;

const sourceFile = project.createSourceFile(
"app/regular.ts",
regularCode
);

const result = analyzeNextjsSourceFiles([sourceFile]);

expect(result.serverActions).toHaveLength(0);
});
});

describe("generateServerAction", () => {
it("should generate correct server action code", () => {
const serverActionInfo: ServerActionInfo = {
name: "testAction",
returnType: "Promise<string>", // This could be 'string' or 'Promise<string>', the function should handle both
parameters: [
{ name: "data", type: "FormData" },
{ name: "userId", type: "string" },
],
filePath: "app/actions.ts",
};

const generatedCode = generateServerAction(serverActionInfo);

expect(generatedCode).toContain('"use server";');
expect(generatedCode).toContain(
"export async function testAction(data: FormData, userId: string): Promise<string>"
);
expect(generatedCode).toContain("// TODO: Implement server action logic");
expect(generatedCode).toContain('throw new Error("Not implemented");');
});

it("should handle non-Promise return types", () => {
const serverActionInfo: ServerActionInfo = {
name: "testAction",
returnType: "void",
parameters: [],
filePath: "app/actions.ts",
};

const generatedCode = generateServerAction(serverActionInfo);

expect(generatedCode).toContain(
"export async function testAction(): Promise<void>"
);
});
});
});
18 changes: 18 additions & 0 deletions packages/ts-generator/src/nextjs/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ServerActionInfo } from "./types";

export function generateServerAction(info: ServerActionInfo): string {
const parameters = (info.parameters ?? [])
.map((param) => `${param.name}: ${param.type}`)
.join(", ");

let returnType = info.returnType.replace(/Promise<(.*)>/, "$1");

returnType = `Promise<${returnType}>`;

return `"use server";
export async function ${info.name}(${parameters}): ${returnType} {
// TODO: Implement server action logic
throw new Error("Not implemented");
}`;
}
3 changes: 3 additions & 0 deletions packages/ts-generator/src/nextjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { NextCodebaseInfo, ServerActionInfo } from "./types";
export { scanNextjsCodebase, analyzeNextjsSourceFiles } from "./scanner";
export { generateServerAction } from "./generator";
48 changes: 48 additions & 0 deletions packages/ts-generator/src/nextjs/scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Project, SourceFile } from "ts-morph";
import { ServerActionInfo, NextCodebaseInfo } from "./types";

export function scanNextjsCodebase(projectPath: string): NextCodebaseInfo {
const project = new Project();
project.addSourceFilesAtPaths(`${projectPath}/**/*.ts`);
return analyzeNextjsSourceFiles(project.getSourceFiles());
}

export function analyzeNextjsSourceFiles(
sourceFiles: SourceFile[]
): NextCodebaseInfo {
const serverActions: ServerActionInfo[] = [];

sourceFiles.forEach((sourceFile) => {
if (isServerActionFile(sourceFile)) {
serverActions.push(...extractServerActions(sourceFile));
}
});

return { serverActions };
}

function isServerActionFile(sourceFile: SourceFile): boolean {
return sourceFile.getFullText().trim().startsWith('"use server";');
}

function extractServerActions(sourceFile: SourceFile): ServerActionInfo[] {
const serverActions: ServerActionInfo[] = [];

sourceFile.getFunctions().forEach((func) => {
if (func.isAsync()) {
const functionInfo: ServerActionInfo = {
name: func.getName() || "anonymous",
returnType: func.getReturnType().getText(),
parameters: func.getParameters().map((param) => ({
name: param.getName(),
type: param.getType().getText(),
})),
filePath: sourceFile.getFilePath(),
};

serverActions.push(functionInfo);
}
});

return serverActions;
}
9 changes: 9 additions & 0 deletions packages/ts-generator/src/nextjs/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FunctionInfo } from "../types";

export interface ServerActionInfo extends FunctionInfo {
filePath: string;
}

export interface NextCodebaseInfo {
serverActions: ServerActionInfo[];
}

0 comments on commit 6ebd3fc

Please sign in to comment.