Skip to content

Commit b77848a

Browse files
authored
Add --with-entrypoint option to CLI for route entry-point file generation (#20)
- Introduced a new command-line option `--with-entrypoint` to create missing route entry-point files during the generation process. - Updated documentation to reflect the new option and its usage. - Modified CLI behavior to conditionally create entry-point files based on the presence of the `--with-entrypoint` flag. - Enhanced tests to verify the correct functionality of the new option and its impact on entry-point file creation. This change improves the flexibility of the route-action-gen tool by allowing users to control entry-point file generation explicitly. Co-authored-by: Nico Prananta <311343+nicnocquee@users.noreply.github.com>
1 parent 43298d4 commit b77848a

8 files changed

Lines changed: 159 additions & 34 deletions

File tree

packages/route-action-gen/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Options:
104104
--version Show version number
105105
--framework <name> Framework target (default: auto)
106106
Use "auto" to detect per directory (pages/ vs app/).
107+
--with-entrypoint Create missing route entry-point files during generate
107108
--force Overwrite existing file (for create command)
108109
109110
Available frameworks:
@@ -121,7 +122,13 @@ When run without a command, the CLI scans for config files and generates code.
121122
3. **Parse** - Extracts metadata from each config file (validators, fields, auth presence)
122123
4. **Generate** - Produces framework-specific files using templates
123124
5. **Write** - Outputs generated files to a `.generated/` subdirectory alongside the config files
124-
6. **Entry Point** - Creates an entry point file (`route.ts` for App Router, `index.ts` for Pages Router) if one doesn't already exist
125+
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
126+
127+
If you were relying on the previous default behavior, run:
128+
129+
```bash
130+
npx route-action-gen --with-entrypoint
131+
```
125132

126133
#### Example Output
127134

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

244251
## Generated Files
245252

246-
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.
253+
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.
247254

248255
### Entry Point File
249256

250-
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:
257+
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:
251258

252259
- **App Router**: `route.ts` containing `export * from "./.generated/route";`
253260
- **Pages Router**: `index.ts` containing `export { default } from "./.generated/route";`
@@ -627,7 +634,7 @@ app/
627634
[postId]/
628635
route.get.config.ts # Your config (you write this)
629636
route.post.config.ts # Your config (you write this)
630-
route.ts # Entry point (auto-created if missing)
637+
route.ts # Entry point (optional; use --with-entrypoint)
631638
.generated/ # Auto-generated (do not edit)
632639
route.ts # Next.js route handler (named exports)
633640
client.ts # RouteClient class
@@ -652,7 +659,7 @@ pages/
652659
[userId]/
653660
route.get.config.ts # Your config (you write this)
654661
route.post.config.ts # Your config (you write this)
655-
index.ts # Entry point (auto-created if missing)
662+
index.ts # Entry point (optional; use --with-entrypoint)
656663
.generated/ # Auto-generated (do not edit)
657664
route.ts # API route handler (default export)
658665
client.ts # RouteClient class

packages/route-action-gen/src/cli/frameworks/__fixtures__/delete-with-params/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **DELETE**
1010

1111
### `route.ts`
1212

13-
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.
13+
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:
14+
15+
```ts
16+
export * from "./.generated/route";
17+
```
1418

1519
### `client.ts`
1620

packages/route-action-gen/src/cli/frameworks/__fixtures__/get-and-post-combined/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **GET, POST
1010

1111
### `route.ts`
1212

13-
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.
13+
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:
14+
15+
```ts
16+
export * from "./.generated/route";
17+
```
1418

1519
### `client.ts`
1620

packages/route-action-gen/src/cli/frameworks/__fixtures__/get-with-params/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **GET** rou
1010

1111
### `route.ts`
1212

13-
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.
13+
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:
14+
15+
```ts
16+
export * from "./.generated/route";
17+
```
1418

1519
### `client.ts`
1620

packages/route-action-gen/src/cli/frameworks/__fixtures__/post-with-body-params-auth/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ This directory contains auto-generated TypeScript/React code for the **POST** ro
1010

1111
### `route.ts`
1212

13-
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.
13+
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:
14+
15+
```ts
16+
export * from "./.generated/route";
17+
```
1418

1519
### `client.ts`
1620

packages/route-action-gen/src/cli/frameworks/next-app-router/templates/readme.md.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export function readmeTemplate(input: ReadmeTemplateInput): string {
3535
``,
3636
`### \`route.ts\``,
3737
``,
38-
`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.`,
38+
`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:`,
39+
``,
40+
`\`\`\`ts`,
41+
`export * from "./.generated/route";`,
42+
`\`\`\``,
3943
``,
4044
`### \`client.ts\``,
4145
``,

packages/route-action-gen/src/cli/index.test.ts

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,16 @@ describe("parseArgs", () => {
110110
expect(result.help).toBe(false);
111111
expect(result.version).toBe(false);
112112
expect(result.framework).toBe("auto");
113+
expect(result.withEntrypoint).toBe(false);
113114
expect(result.force).toBe(false);
114115
});
116+
it("enables entry-point creation when --with-entrypoint flag is passed", () => {
117+
// Act
118+
const result = parseArgs(["--with-entrypoint"]);
119+
120+
// Assert
121+
expect(result.withEntrypoint).toBe(true);
122+
});
115123

116124
it("sets help to true when --help flag is passed", () => {
117125
// Act
@@ -202,12 +210,14 @@ describe("parseArgs", () => {
202210
"--version",
203211
"--framework",
204212
"my-framework",
213+
"--with-entrypoint",
205214
]);
206215

207216
// Assert
208217
expect(result.help).toBe(true);
209218
expect(result.version).toBe(true);
210219
expect(result.framework).toBe("my-framework");
220+
expect(result.withEntrypoint).toBe(true);
211221
});
212222

213223
it("ignores unrecognized arguments", () => {
@@ -334,6 +344,7 @@ describe("HELP_TEXT", () => {
334344
expect(HELP_TEXT).toContain("--help");
335345
expect(HELP_TEXT).toContain("--version");
336346
expect(HELP_TEXT).toContain("--framework");
347+
expect(HELP_TEXT).toContain("--with-entrypoint");
337348
expect(HELP_TEXT).toContain("auto");
338349
expect(HELP_TEXT).toContain("next-app-router");
339350
expect(HELP_TEXT).toContain("next-pages-router");
@@ -544,10 +555,74 @@ describe("main", () => {
544555
);
545556
});
546557

547-
it("creates app router entry point file when it does not exist", () => {
558+
it("does not create app router entry point file by default", () => {
559+
// Setup
560+
process.argv = ["node", "index.js"];
561+
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
562+
vi.spyOn(console, "log").mockImplementation(() => {});
563+
vi.mocked(globSync).mockReturnValue([
564+
"app/api/posts/route.post.config.ts",
565+
] as never);
566+
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);
567+
vi.mocked(fs.existsSync).mockReturnValue(false);
568+
569+
// Act
570+
main();
571+
572+
// Assert
573+
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
574+
"/test-project/app/api/posts/route.ts",
575+
expect.any(String),
576+
expect.any(String),
577+
);
578+
});
579+
580+
it("does not create pages router entry point file by default", () => {
581+
// Setup
582+
process.argv = ["node", "index.js"];
583+
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
584+
vi.spyOn(console, "log").mockImplementation(() => {});
585+
vi.mocked(globSync).mockReturnValue([
586+
"pages/api/users/route.post.config.ts",
587+
] as never);
588+
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);
589+
vi.mocked(fs.existsSync).mockReturnValue(false);
590+
591+
// Act
592+
main();
593+
594+
// Assert
595+
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
596+
"/test-project/pages/api/users/index.ts",
597+
expect.any(String),
598+
expect.any(String),
599+
);
600+
});
601+
602+
it("prints entry point info by default with --with-entrypoint hint", () => {
548603
// Setup
549604
process.argv = ["node", "index.js"];
550605
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
606+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
607+
vi.mocked(globSync).mockReturnValue([
608+
"app/api/posts/route.post.config.ts",
609+
] as never);
610+
vi.mocked(fs.readFileSync).mockReturnValue(samplePostConfig as never);
611+
612+
// Act
613+
main();
614+
615+
// Assert
616+
expect(logSpy).toHaveBeenCalledWith(
617+
"To create a route handler or an API end point, create /test-project/app/api/posts/route.ts file",
618+
" or run with --with-entrypoint to create it automatically. Read the generated README.md for more information.",
619+
);
620+
});
621+
622+
it("creates app router entry point file when --with-entrypoint is passed", () => {
623+
// Setup
624+
process.argv = ["node", "index.js", "--with-entrypoint"];
625+
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
551626
vi.spyOn(console, "log").mockImplementation(() => {});
552627
vi.mocked(globSync).mockReturnValue([
553628
"app/api/posts/route.post.config.ts",
@@ -566,11 +641,10 @@ describe("main", () => {
566641
);
567642
});
568643

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

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

593-
it("does not overwrite existing entry point file", () => {
665+
it("does not overwrite existing entry point file when --with-entrypoint is passed", () => {
594666
// Setup
595-
process.argv = ["node", "index.js"];
667+
process.argv = ["node", "index.js", "--with-entrypoint"];
596668
vi.spyOn(process, "cwd").mockReturnValue("/test-project");
597669
vi.spyOn(console, "log").mockImplementation(() => {});
598670
vi.mocked(globSync).mockReturnValue([
@@ -612,9 +684,9 @@ describe("main", () => {
612684
);
613685
});
614686

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

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

649721
// Assert
650-
const entryPointCalls = logSpy.mock.calls.filter(
651-
(call) =>
652-
typeof call[0] === "string" && call[0].includes("Created entry point"),
722+
expect(logSpy).toHaveBeenCalledWith(
723+
" Entry point exists: /test-project/app/api/posts/route.ts",
653724
);
654-
expect(entryPointCalls).toHaveLength(0);
655725
});
656726
});

0 commit comments

Comments
 (0)