diff --git a/.changeset/early-starfishes-heal.md b/.changeset/early-starfishes-heal.md new file mode 100644 index 00000000000..148cb25e02d --- /dev/null +++ b/.changeset/early-starfishes-heal.md @@ -0,0 +1,5 @@ +--- +"create-remix": patch +--- + +redirect users to create-react-router instead of create-remix diff --git a/packages/create-remix/__tests__/create-remix-test.ts b/packages/create-remix/__tests__/create-remix-test.ts index 5d21bb0b13a..73e9ca82f3d 100644 --- a/packages/create-remix/__tests__/create-remix-test.ts +++ b/packages/create-remix/__tests__/create-remix-test.ts @@ -1,363 +1,67 @@ -import * as readline from "readline"; -import crossSpawn from "cross-spawn"; - import { createRemix } from "../index"; -// Mock dependencies -jest.mock("readline"); -jest.mock("cross-spawn"); - describe("createRemix", () => { - let mockReadline: any; - let mockSpawn: any; - let mockChildProcess: any; let consoleLogSpy: jest.SpyInstance; beforeEach(() => { - // Reset all mocks - jest.clearAllMocks(); - // Mock console.log consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); - - // Setup readline mock - mockReadline = { - question: jest.fn(), - close: jest.fn(), - on: jest.fn(), - }; - (readline.createInterface as jest.Mock).mockReturnValue(mockReadline); - - // Setup child process mock - mockChildProcess = { - on: jest.fn(), - }; - mockSpawn = crossSpawn as jest.MockedFunction; - mockSpawn.mockReturnValue(mockChildProcess as any); }); afterEach(() => { consoleLogSpy.mockRestore(); }); - describe("when user confirms with 'y'", () => { - it("should spawn the command and resolve on successful exit", async () => { - // Arrange - let argv = ["--template", "my-template"]; - let questionCallback: Function; - let exitHandler: Function; - - // Capture the question callback - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - // Capture the child process event handlers - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "exit") { - exitHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix(argv); - - // Simulate user answering 'y' - questionCallback!("y"); - - // Simulate successful exit - exitHandler!(0); - - await promise; - - // Assert - expect(readline.createInterface).toHaveBeenCalledWith({ - input: process.stdin, - output: process.stdout, - }); - - expect(consoleLogSpy).toHaveBeenCalledWith( - "\nDid you mean `npx create-react-router@latest`?\n" - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - "\nRunning: npx create-react-router@latest\n" - ); - - expect(mockSpawn).toHaveBeenCalledWith( - "npx", - ["create-react-router@latest", "--template", "my-template"], - { - stdio: "inherit", - env: process.env, - } - ); - - expect(mockReadline.close).toHaveBeenCalled(); - }); - - it("should reject when child process exits with non-zero code", async () => { - // Arrange - let questionCallback: Function; - let exitHandler: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "exit") { - exitHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix([]); - questionCallback!("y"); - exitHandler!(1); - - // Assert - await expect(promise).rejects.toThrow("Command failed with exit code 1"); - expect(mockReadline.close).toHaveBeenCalled(); - }); - - it("should reject when child process emits error", async () => { - // Arrange - let error = new Error("Spawn error"); - let questionCallback: Function; - let errorHandler: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "error") { - errorHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix([]); - questionCallback!("y"); - errorHandler!(error); - - // Assert - await expect(promise).rejects.toThrow("Spawn error"); - expect(mockReadline.close).toHaveBeenCalled(); - }); + it("should display migration message with basic command", async () => { + await createRemix([]); + + expect(consoleLogSpy).toHaveBeenCalledWith( + "\nšŸ”„ Remix v2 is now part of React Router!" + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\nRemix v2 has been upstreamed into React Router and is now in maintenance mode." + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "For new projects, please use React Router instead." + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\nTo create a new React Router project, run:" + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\n npx create-react-router@latest\n" + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Learn more: https://reactrouter.com\n" + ); }); - describe("when user confirms with 'yes'", () => { - it("should spawn the command (case insensitive)", async () => { - // Arrange - let questionCallback: Function; - let exitHandler: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "exit") { - exitHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix([]); - questionCallback!("YES"); - exitHandler!(0); + it("should display migration message with template argument", async () => { + await createRemix(["--template", "my-template"]); - await promise; - - // Assert - expect(mockSpawn).toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith( - "\nRunning: npx create-react-router@latest\n" - ); - }); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\nšŸ”„ Remix v2 is now part of React Router!" + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\n npx create-react-router@latest --template my-template\n" + ); }); - describe("when user declines", () => { - it("should not spawn command when user answers 'n'", async () => { - // Arrange - let questionCallback: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - // Act - let promise = createRemix([]); - questionCallback!("n"); - - await promise; - - // Assert - expect(mockSpawn).not.toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith("\nCommand not executed."); - expect(mockReadline.close).toHaveBeenCalled(); - }); - - it("should not spawn command when user answers anything else", async () => { - // Arrange - let questionCallback: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); + it("should display migration message with multiple arguments", async () => { + await createRemix(["--template", "remix", "--install", "--typescript"]); - // Act - let promise = createRemix([]); - questionCallback!("maybe"); - - await promise; - - // Assert - expect(mockSpawn).not.toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith("\nCommand not executed."); - expect(mockReadline.close).toHaveBeenCalled(); - }); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\nšŸ”„ Remix v2 is now part of React Router!" + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\n npx create-react-router@latest --template remix --install --typescript\n" + ); }); - describe("when readline is closed without answer", () => { - it("should reject with appropriate error", async () => { - // Arrange - let rlCloseHandler: Function; - let hasAnswered = false; - - mockReadline.on.mockImplementation((event: string, handler: Function) => { - if (event === "close") { - rlCloseHandler = handler; - } - return mockReadline; - }); - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - // Don't call the callback - simulate closing without answering - } - ); - - mockReadline.close.mockImplementation(() => { - if (!hasAnswered && rlCloseHandler) { - rlCloseHandler(); - } - }); - - // Act - let promise = createRemix([]); - - // Simulate closing readline without answering - mockReadline.close(); - - // Assert - await expect(promise).rejects.toThrow( - "User did not confirm command execution" - ); - }); - }); - - describe("edge cases", () => { - it("should handle empty argv array", async () => { - // Arrange - let questionCallback: Function; - let exitHandler: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "exit") { - exitHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix([]); - questionCallback!("y"); - exitHandler!(0); - - await promise; - - // Assert - expect(mockSpawn).toHaveBeenCalledWith( - "npx", - ["create-react-router@latest"], - expect.any(Object) - ); - }); - - it("should pass through multiple arguments", async () => { - // Arrange - let argv = ["--template", "remix", "--install", "--typescript"]; - let questionCallback: Function; - let exitHandler: Function; - - mockReadline.question.mockImplementation( - (question: string, callback: Function) => { - questionCallback = callback; - } - ); - - mockChildProcess.on.mockImplementation( - (event: string, handler: Function) => { - if (event === "exit") { - exitHandler = handler; - } - return mockChildProcess; - } - ); - - // Act - let promise = createRemix(argv); - questionCallback!("y"); - exitHandler!(0); - - await promise; + it("should handle empty argv array", async () => { + await createRemix([]); - // Assert - expect(mockSpawn).toHaveBeenCalledWith( - "npx", - [ - "create-react-router@latest", - "--template", - "remix", - "--install", - "--typescript", - ], - expect.any(Object) - ); - }); + expect(consoleLogSpy).toHaveBeenCalledWith( + "\n npx create-react-router@latest\n" + ); }); }); diff --git a/packages/create-remix/cli.ts b/packages/create-remix/cli.ts old mode 100644 new mode 100755 diff --git a/packages/create-remix/index.ts b/packages/create-remix/index.ts index ba48bd9997a..996114cb845 100644 --- a/packages/create-remix/index.ts +++ b/packages/create-remix/index.ts @@ -1,47 +1,13 @@ -import * as readline from "readline"; -import crossSpawn from "cross-spawn"; - export async function createRemix(argv: string[]) { - return new Promise((resolve, reject) => { - let rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - console.log("\nDid you mean `npx create-react-router@latest`?\n"); - - rl.question("Would you like to run this command? (y/n): ", (answer) => { - if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") { - console.log("\nRunning: npx create-react-router@latest\n"); - - // Use cross-spawn for better cross-platform support - let child = crossSpawn("npx", ["create-react-router@latest", ...argv], { - stdio: "inherit", - env: process.env, - }); - - child.on("error", (error) => { - reject(error); - rl.close(); - }); - - child.on("exit", (code) => { - rl.close(); - if (code !== 0) { - reject(new Error(`Command failed with exit code ${code}`)); - } else { - resolve(); - } - }); - } else { - console.log("\nCommand not executed."); - rl.close(); - resolve(); - } - }); - - rl.on("close", () => { - reject(new Error("User did not confirm command execution")); - }); - }); + console.log("\nšŸ”„ Remix v2 is now part of React Router!"); + console.log("\nRemix v2 has been upstreamed into React Router and is now in maintenance mode."); + console.log("For new projects, please use React Router instead."); + console.log("\nTo create a new React Router project, run:"); + + const command = argv.length > 0 + ? `npx create-react-router@latest ${argv.join(" ")}` + : "npx create-react-router@latest"; + + console.log(`\n ${command}\n`); + console.log("Learn more: https://reactrouter.com\n"); } diff --git a/packages/create-remix/package.json b/packages/create-remix/package.json index 1bb4506fc53..ec072e4949c 100644 --- a/packages/create-remix/package.json +++ b/packages/create-remix/package.json @@ -19,9 +19,6 @@ "scripts": { "tsc": "tsc" }, - "dependencies": { - "cross-spawn": "^7.0.6" - }, "engines": { "node": ">=18.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd2bd5e19d0..e19292721ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -749,11 +749,7 @@ importers: specifier: ^3.24.0 version: 3.74.0(@cloudflare/workers-types@4.20240208.0) - packages/create-remix: - dependencies: - cross-spawn: - specifier: ^7.0.6 - version: 7.0.6 + packages/create-remix: {} packages/remix: {}