diff --git a/packages/route-action-gen/README.md b/packages/route-action-gen/README.md index 1e7b349..94b77e2 100644 --- a/packages/route-action-gen/README.md +++ b/packages/route-action-gen/README.md @@ -104,6 +104,7 @@ Options: --version Show version number --framework 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: @@ -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 @@ -243,11 +250,11 @@ type AuthFunc = (request?: Request) => Promise; ## 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";` @@ -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 @@ -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 diff --git a/packages/route-action-gen/src/cli/frameworks/__fixtures__/delete-with-params/README.md b/packages/route-action-gen/src/cli/frameworks/__fixtures__/delete-with-params/README.md index 0d6743a..6245396 100644 --- a/packages/route-action-gen/src/cli/frameworks/__fixtures__/delete-with-params/README.md +++ b/packages/route-action-gen/src/cli/frameworks/__fixtures__/delete-with-params/README.md @@ -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` diff --git a/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-and-post-combined/README.md b/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-and-post-combined/README.md index 9511e24..67340ba 100644 --- a/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-and-post-combined/README.md +++ b/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-and-post-combined/README.md @@ -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` diff --git a/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-with-params/README.md b/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-with-params/README.md index c591ce6..631dd5d 100644 --- a/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-with-params/README.md +++ b/packages/route-action-gen/src/cli/frameworks/__fixtures__/get-with-params/README.md @@ -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` diff --git a/packages/route-action-gen/src/cli/frameworks/__fixtures__/post-with-body-params-auth/README.md b/packages/route-action-gen/src/cli/frameworks/__fixtures__/post-with-body-params-auth/README.md index b36297d..c7ce8d5 100644 --- a/packages/route-action-gen/src/cli/frameworks/__fixtures__/post-with-body-params-auth/README.md +++ b/packages/route-action-gen/src/cli/frameworks/__fixtures__/post-with-body-params-auth/README.md @@ -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` diff --git a/packages/route-action-gen/src/cli/frameworks/next-app-router/templates/readme.md.ts b/packages/route-action-gen/src/cli/frameworks/next-app-router/templates/readme.md.ts index 20c05b2..f16c254 100644 --- a/packages/route-action-gen/src/cli/frameworks/next-app-router/templates/readme.md.ts +++ b/packages/route-action-gen/src/cli/frameworks/next-app-router/templates/readme.md.ts @@ -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\``, ``, diff --git a/packages/route-action-gen/src/cli/index.test.ts b/packages/route-action-gen/src/cli/index.test.ts index 651e97d..53db696 100644 --- a/packages/route-action-gen/src/cli/index.test.ts +++ b/packages/route-action-gen/src/cli/index.test.ts @@ -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 @@ -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", () => { @@ -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"); @@ -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", @@ -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); @@ -581,8 +655,6 @@ 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', @@ -590,9 +662,9 @@ describe("main", () => { ); }); - 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([ @@ -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([ @@ -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([ @@ -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); }); }); diff --git a/packages/route-action-gen/src/cli/index.ts b/packages/route-action-gen/src/cli/index.ts index 52e4be7..0792500 100644 --- a/packages/route-action-gen/src/cli/index.ts +++ b/packages/route-action-gen/src/cli/index.ts @@ -82,6 +82,7 @@ Options: --version Show version number --framework Framework target (default: ${DEFAULT_FRAMEWORK}) 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: @@ -98,6 +99,8 @@ export interface CliArgs { help: boolean; version: boolean; framework: string; + /** Create missing entry-point files for generated routes */ + withEntrypoint: boolean; /** HTTP method for the `create` command */ createMethod?: HttpMethod; /** Target directory for the `create` command */ @@ -106,12 +109,19 @@ export interface CliArgs { force: boolean; } +/** + * Parse CLI arguments into a structured configuration object. + * + * @param argv - Raw command-line args, excluding `node` and script path + * @returns Parsed CLI arguments for command routing and generation behavior + */ export function parseArgs(argv: string[]): CliArgs { const args: CliArgs = { command: "generate", help: false, version: false, framework: DEFAULT_FRAMEWORK, + withEntrypoint: false, force: false, }; @@ -123,6 +133,8 @@ export function parseArgs(argv: string[]): CliArgs { args.version = true; } else if (arg === "--force") { args.force = true; + } else if (arg === "--with-entrypoint") { + args.withEntrypoint = true; } else if (arg === "--framework" || arg === "-f") { const next = argv[i + 1]; if (!next || next.startsWith("-")) { @@ -181,6 +193,7 @@ export function parseArgs(argv: string[]): CliArgs { export function generate( deps: CliDeps, frameworkName: string, + options: { withEntrypoint?: boolean } = {}, ): { success: boolean; generated?: { @@ -193,6 +206,7 @@ export function generate( }[]; error?: string; } { + const withEntrypoint = options.withEntrypoint ?? false; const isAuto = frameworkName === "auto"; // When not auto, resolve the generator once up-front @@ -269,7 +283,7 @@ export function generate( writtenFiles.push(file.fileName); } - // Create entry point file if it doesn't exist + // Compute entry point metadata for reporting const generatedDirRelPath = path.relative(group.directory, generatedDir); // Ensure the path starts with "./" or "../" for a valid import specifier const importPath = generatedDirRelPath.startsWith(".") @@ -277,9 +291,11 @@ export function generate( : `./${generatedDirRelPath}`; const entryPoint = generator.getEntryPointFile(importPath); const entryPointPath = path.join(group.directory, entryPoint.fileName); + let entryPointCreated = false; + const entryPointFile = entryPoint.fileName; - if (!deps.existsSync(entryPointPath)) { + if (withEntrypoint && !deps.existsSync(entryPointPath)) { deps.writeFileSync(entryPointPath, entryPoint.content); entryPointCreated = true; } @@ -288,7 +304,7 @@ export function generate( directory: group.directory, generatedDir, files: writtenFiles, - entryPointFile: entryPoint.fileName, + entryPointFile, entryPointCreated, framework: generator.name, }); @@ -354,7 +370,9 @@ export function main() { ); console.log(`Scanning for config files in: ${deps.cwd()}\n`); - const result = generate(deps, args.framework); + const result = generate(deps, args.framework, { + withEntrypoint: args.withEntrypoint, + }); if (!result.success) { console.error(`Error: ${result.error}`); @@ -367,10 +385,20 @@ export function main() { for (const file of group.files) { console.log(` - ${file}`); } - if (group.entryPointCreated && group.entryPointFile) { - console.log( - ` Created entry point: ${group.directory}/${group.entryPointFile}`, - ); + if (group.entryPointFile) { + const entryPointPath = `${group.directory}/${group.entryPointFile}`; + if (args.withEntrypoint) { + if (group.entryPointCreated) { + console.log(` Created entry point: ${entryPointPath}`); + } else { + console.log(` Entry point exists: ${entryPointPath}`); + } + } else { + console.log( + `To create a route handler or an API end point, create ${entryPointPath} file`, + ` or run with --with-entrypoint to create it automatically. Read the generated README.md for more information.`, + ); + } } console.log(); }