Skip to content
Merged
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
17 changes: 12 additions & 5 deletions packages/route-action-gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Options:
--version Show version number
--framework <name> Framework target (default: auto)
Use "auto" to detect per directory (pages/ vs app/).
--with-entrypoint Create missing route entry-point files during generate
--force Overwrite existing file (for create command)

Available frameworks:
Expand All @@ -121,7 +122,13 @@ When run without a command, the CLI scans for config files and generates code.
3. **Parse** - Extracts metadata from each config file (validators, fields, auth presence)
4. **Generate** - Produces framework-specific files using templates
5. **Write** - Outputs generated files to a `.generated/` subdirectory alongside the config files
6. **Entry Point** - Creates an entry point file (`route.ts` for App Router, `index.ts` for Pages Router) if one doesn't already exist
6. **Entry Point (optional)** - Creates an entry point file (`route.ts` for App Router, `index.ts` for Pages Router) only when `--with-entrypoint` is passed

If you were relying on the previous default behavior, run:

```bash
npx route-action-gen --with-entrypoint
```

#### Example Output

Expand Down Expand Up @@ -243,11 +250,11 @@ type AuthFunc<TUser> = (request?: Request) => Promise<TUser>;

## Generated Files

The files generated depend on the HTTP method and the framework. All files are output to a `.generated/` subdirectory. An entry point file is also created in the parent directory if it doesn't already exist.
The files generated depend on the HTTP method and the framework. All files are output to a `.generated/` subdirectory. Entry point files are optional and only created when `--with-entrypoint` is passed.

### Entry Point File

The CLI creates an entry point file in the same directory as the config files (not inside `.generated/`) the first time it runs. This file re-exports from the generated route handler so Next.js can discover it:
When `--with-entrypoint` is used, the CLI creates an entry point file in the same directory as the config files (not inside `.generated/`). This file re-exports from the generated route handler so Next.js can discover it:

- **App Router**: `route.ts` containing `export * from "./.generated/route";`
- **Pages Router**: `index.ts` containing `export { default } from "./.generated/route";`
Expand Down Expand Up @@ -627,7 +634,7 @@ app/
[postId]/
route.get.config.ts # Your config (you write this)
route.post.config.ts # Your config (you write this)
route.ts # Entry point (auto-created if missing)
route.ts # Entry point (optional; use --with-entrypoint)
.generated/ # Auto-generated (do not edit)
route.ts # Next.js route handler (named exports)
client.ts # RouteClient class
Expand All @@ -652,7 +659,7 @@ pages/
[userId]/
route.get.config.ts # Your config (you write this)
route.post.config.ts # Your config (you write this)
index.ts # Entry point (auto-created if missing)
index.ts # Entry point (optional; use --with-entrypoint)
.generated/ # Auto-generated (do not edit)
route.ts # API route handler (default export)
client.ts # RouteClient class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **DELETE**

### `route.ts`

Next.js App Router route handler. Exports a named handler for each HTTP method (DELETE) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response.
Next.js App Router route handler. Exports a named handler for each HTTP method (DELETE) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response. Use this to create a route handler or an API end point. For example, create `app/api/posts/[postId]/route.ts` file and add the following content:

```ts
export * from "./.generated/route";
```

### `client.ts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **GET, POST

### `route.ts`

Next.js App Router route handler. Exports a named handler for each HTTP method (GET, POST) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response.
Next.js App Router route handler. Exports a named handler for each HTTP method (GET, POST) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response. Use this to create a route handler or an API end point. For example, create `app/api/posts/[postId]/route.ts` file and add the following content:

```ts
export * from "./.generated/route";
```

### `client.ts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **GET** rou

### `route.ts`

Next.js App Router route handler. Exports a named handler for each HTTP method (GET) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response.
Next.js App Router route handler. Exports a named handler for each HTTP method (GET) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response. Use this to create a route handler or an API end point. For example, create `app/api/posts/[postId]/route.ts` file and add the following content:

```ts
export * from "./.generated/route";
```

### `client.ts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **POST** ro

### `route.ts`

Next.js App Router route handler. Exports a named handler for each HTTP method (POST) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response.
Next.js App Router route handler. Exports a named handler for each HTTP method (POST) that validates incoming requests, delegates to your `route.[method].config.ts` handler, and returns a validated JSON response. Use this to create a route handler or an API end point. For example, create `app/api/posts/[postId]/route.ts` file and add the following content:

```ts
export * from "./.generated/route";
```

### `client.ts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export function readmeTemplate(input: ReadmeTemplateInput): string {
``,
`### \`route.ts\``,
``,
`Next.js App Router route handler. Exports a named handler for each HTTP method (${methodList}) that validates incoming requests, delegates to your \`route.[method].config.ts\` handler, and returns a validated JSON response.`,
`Next.js App Router route handler. Exports a named handler for each HTTP method (${methodList}) that validates incoming requests, delegates to your \`route.[method].config.ts\` handler, and returns a validated JSON response. Use this to create a route handler or an API end point. For example, create \`app/api/posts/[postId]/route.ts\` file and add the following content:`,
``,
`\`\`\`ts`,
`export * from "./.generated/route";`,
`\`\`\``,
``,
`### \`client.ts\``,
``,
Expand Down
102 changes: 86 additions & 16 deletions packages/route-action-gen/src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,16 @@ describe("parseArgs", () => {
expect(result.help).toBe(false);
expect(result.version).toBe(false);
expect(result.framework).toBe("auto");
expect(result.withEntrypoint).toBe(false);
expect(result.force).toBe(false);
});
it("enables entry-point creation when --with-entrypoint flag is passed", () => {
// Act
const result = parseArgs(["--with-entrypoint"]);

// Assert
expect(result.withEntrypoint).toBe(true);
});

it("sets help to true when --help flag is passed", () => {
// Act
Expand Down Expand Up @@ -202,12 +210,14 @@ describe("parseArgs", () => {
"--version",
"--framework",
"my-framework",
"--with-entrypoint",
]);

// Assert
expect(result.help).toBe(true);
expect(result.version).toBe(true);
expect(result.framework).toBe("my-framework");
expect(result.withEntrypoint).toBe(true);
});

it("ignores unrecognized arguments", () => {
Expand Down Expand Up @@ -334,6 +344,7 @@ describe("HELP_TEXT", () => {
expect(HELP_TEXT).toContain("--help");
expect(HELP_TEXT).toContain("--version");
expect(HELP_TEXT).toContain("--framework");
expect(HELP_TEXT).toContain("--with-entrypoint");
expect(HELP_TEXT).toContain("auto");
expect(HELP_TEXT).toContain("next-app-router");
expect(HELP_TEXT).toContain("next-pages-router");
Expand Down Expand Up @@ -544,10 +555,74 @@ describe("main", () => {
);
});

it("creates app router entry point file when it does not exist", () => {
it("does not create app router entry point file by default", () => {
// Setup
process.argv = ["node", "index.js"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
"app/api/posts/route.post.config.ts",
] as never);
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);
vi.mocked(fs.existsSync).mockReturnValue(false);

// Act
main();

// Assert
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
"/test-project/app/api/posts/route.ts",
expect.any(String),
expect.any(String),
);
});

it("does not create pages router entry point file by default", () => {
// Setup
process.argv = ["node", "index.js"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
"pages/api/users/route.post.config.ts",
] as never);
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);
vi.mocked(fs.existsSync).mockReturnValue(false);

// Act
main();

// Assert
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
"/test-project/pages/api/users/index.ts",
expect.any(String),
expect.any(String),
);
});

it("prints entry point info by default with --with-entrypoint hint", () => {
// Setup
process.argv = ["node", "index.js"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
"app/api/posts/route.post.config.ts",
] as never);
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);

// Act
main();

// Assert
expect(logSpy).toHaveBeenCalledWith(
"To create a route handler or an API end point, create /test-project/app/api/posts/route.ts file",
" or run with --with-entrypoint to create it automatically. Read the generated README.md for more information.",
);
});

it("creates app router entry point file when --with-entrypoint is passed", () => {
// Setup
process.argv = ["node", "index.js", "--with-entrypoint"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
"app/api/posts/route.post.config.ts",
Expand All @@ -566,11 +641,10 @@ describe("main", () => {
);
});

it("creates pages router entry point file when it does not exist", () => {
it("creates pages router entry point file when --with-entrypoint is passed", () => {
// Setup
process.argv = ["node", "index.js"];
process.argv = ["node", "index.js", "--with-entrypoint"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
"pages/api/users/route.post.config.ts",
] as never);
Expand All @@ -581,18 +655,16 @@ describe("main", () => {
main();

// Assert
// Pages Router generates files to .generated/ at the project root,
// so the entry point import path is relative from pages/api/users/ to .generated/pages/api/users/
expect(fs.writeFileSync).toHaveBeenCalledWith(
"/test-project/pages/api/users/index.ts",
'export { default } from "../../../.generated/pages/api/users/route";\n',
"utf-8",
);
});

it("does not overwrite existing entry point file", () => {
it("does not overwrite existing entry point file when --with-entrypoint is passed", () => {
// Setup
process.argv = ["node", "index.js"];
process.argv = ["node", "index.js", "--with-entrypoint"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
Expand All @@ -612,9 +684,9 @@ describe("main", () => {
);
});

it("prints entry point creation message when file is created", () => {
it("prints entry point creation message when --with-entrypoint creates a file", () => {
// Setup
process.argv = ["node", "index.js"];
process.argv = ["node", "index.js", "--with-entrypoint"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
Expand All @@ -632,9 +704,9 @@ describe("main", () => {
);
});

it("does not print entry point creation message when file already exists", () => {
it("prints entry point exists message when --with-entrypoint finds existing file", () => {
// Setup
process.argv = ["node", "index.js"];
process.argv = ["node", "index.js", "--with-entrypoint"];
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(globSync).mockReturnValue([
Expand All @@ -647,10 +719,8 @@ describe("main", () => {
main();

// Assert
const entryPointCalls = logSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("Created entry point"),
expect(logSpy).toHaveBeenCalledWith(
" Entry point exists: /test-project/app/api/posts/route.ts",
);
expect(entryPointCalls).toHaveLength(0);
});
});
Loading