From 0befe4c6e67dde2ad10dab073456d1e5ceb59a73 Mon Sep 17 00:00:00 2001 From: YusukeSano <54178415+YusukeSano@users.noreply.github.com> Date: Wed, 21 Aug 2024 06:51:05 +0900 Subject: [PATCH] feat(cli): add Remix SPA mode support (#6246) (fixes #6127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ali Emir Şen --- .changeset/wicked-cherries-grow.md | 11 ++ .../integration/packages/ant-design.ts | 9 +- .../integration/packages/react-router.ts | 6 +- .../commands/runner/projectScripts.test.ts | 139 +++++++++++++++--- .../cli/src/commands/runner/projectScripts.ts | 37 +++++ .../cli/src/commands/runner/utils/index.ts | 12 +- packages/cli/src/definitions/projectTypes.ts | 2 + packages/cli/src/utils/project/index.ts | 5 + packages/cli/src/utils/resource/index.spec.ts | 10 ++ packages/cli/src/utils/resource/index.ts | 6 + 10 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 .changeset/wicked-cherries-grow.md diff --git a/.changeset/wicked-cherries-grow.md b/.changeset/wicked-cherries-grow.md new file mode 100644 index 0000000000000..3a3bf44054df0 --- /dev/null +++ b/.changeset/wicked-cherries-grow.md @@ -0,0 +1,11 @@ +--- +"@refinedev/cli": patch +--- + +feat: added scripts for Remix SPA Mode + +It is now possible to execute the Remix SPA Mode script by selecting it from the platform options. + +Two new project types are added `remix-vite` and `remix-spa`. `remix-vite` is Remix + Vite and `remix-spa` is Remix + Vite SPA Mode. While `remix-vite` type can be inferred from the project configuration without needing to specify it in the command, `remix-spa` type needs to be specified explicitly. + +[Resolves #6127](https://github.com/refinedev/refine/issues/6127) diff --git a/packages/cli/src/commands/add/sub-commands/integration/packages/ant-design.ts b/packages/cli/src/commands/add/sub-commands/integration/packages/ant-design.ts index 2f394d50681b6..a6b6ce651f948 100644 --- a/packages/cli/src/commands/add/sub-commands/integration/packages/ant-design.ts +++ b/packages/cli/src/commands/add/sub-commands/integration/packages/ant-design.ts @@ -17,7 +17,14 @@ export const AntDesignIntegration: Integration = { const description = "Setup Ant Design with Refine"; let disabled; - if ([ProjectTypes.NEXTJS, ProjectTypes.REMIX].includes(projectType)) { + if ( + [ + ProjectTypes.NEXTJS, + ProjectTypes.REMIX, + ProjectTypes.REMIX_VITE, + ProjectTypes.REMIX_SPA, + ].includes(projectType) + ) { disabled = "Automatic setup only available Vite for now. See the documentation for manual installation: https://refine.dev/docs/ui-integrations/ant-design/introduction/#installation"; } diff --git a/packages/cli/src/commands/add/sub-commands/integration/packages/react-router.ts b/packages/cli/src/commands/add/sub-commands/integration/packages/react-router.ts index 133022c33b0cc..ff1769130b391 100644 --- a/packages/cli/src/commands/add/sub-commands/integration/packages/react-router.ts +++ b/packages/cli/src/commands/add/sub-commands/integration/packages/react-router.ts @@ -21,7 +21,11 @@ export const ReactRouterIntegration: Integration = { disabled = `Can't be used with Next.js. https://nextjs.org/docs/app/building-your-application/routing`; } - if (projectType === ProjectTypes.REMIX) { + if ( + projectType === ProjectTypes.REMIX || + projectType === ProjectTypes.REMIX_VITE || + projectType === ProjectTypes.REMIX_SPA + ) { disabled = `Can't be used with Remix. https://remix.run/docs/en/main/discussion/routes`; } diff --git a/packages/cli/src/commands/runner/projectScripts.test.ts b/packages/cli/src/commands/runner/projectScripts.test.ts index 7cb8b190d32c0..f93de7065bf17 100644 --- a/packages/cli/src/commands/runner/projectScripts.test.ts +++ b/packages/cli/src/commands/runner/projectScripts.test.ts @@ -53,17 +53,17 @@ describe("REACT_SCRIPT project type", () => { }); }); -describe("Vite project type", () => { +describe("VITE project type", () => { const projectType = ProjectTypes.VITE; describe("getDev with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test('should return array with only "dev" if args is empty', () => { expect(projectScripts[projectType].getDev([])).toEqual(["dev"]); }); }); describe("getStart with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test('should return array with only "preview" if args is empty', () => { expect(projectScripts[projectType].getStart([])).toEqual(["preview"]); }); }); @@ -75,7 +75,7 @@ describe("Vite project type", () => { }); describe("getDev", () => { - test('should prepend "start" to the args array', () => { + test('should prepend "dev" to the args array', () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getDev(args)).toEqual([ "dev", @@ -85,7 +85,7 @@ describe("Vite project type", () => { }); describe("getStart", () => { - test('should prepend "start" to the args array', () => { + test('should prepend "preview" to the args array', () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getStart(args)).toEqual([ "preview", @@ -105,11 +105,11 @@ describe("Vite project type", () => { }); }); -describe("Next.js project type", () => { +describe("NEXTJS project type", () => { const projectType = ProjectTypes.NEXTJS; describe("getDev with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test('should return array with only "dev" if args is empty', () => { expect(projectScripts[projectType].getDev([])).toEqual(["dev"]); }); }); @@ -127,7 +127,7 @@ describe("Next.js project type", () => { }); describe("getDev", () => { - test('should prepend "start" to the args array', () => { + test('should prepend "dev" to the args array', () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getDev(args)).toEqual([ "dev", @@ -157,11 +157,11 @@ describe("Next.js project type", () => { }); }); -describe("Remix project type", () => { +describe("REMIX project type", () => { const projectType = ProjectTypes.REMIX; describe("getDev with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test('should return array with only "dev" if args is empty', () => { expect(projectScripts[projectType].getDev([])).toEqual(["dev"]); }); }); @@ -183,7 +183,7 @@ describe("Remix project type", () => { }); describe("getDev", () => { - test('should prepend "start" to the args array', () => { + test('should prepend "dev" to the args array', () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getDev(args)).toEqual([ "dev", @@ -210,6 +210,111 @@ describe("Remix project type", () => { }); }); +describe("REMIX_VITE project type", () => { + const projectType = ProjectTypes.REMIX_VITE; + + describe("getDev with empty args", () => { + test('should return array with only "vite:dev" if args is empty', () => { + expect(projectScripts[projectType].getDev([])).toEqual(["vite:dev"]); + }); + }); + + describe("getStart with empty args", () => { + test("should return default", () => { + const logSpy = jest.spyOn(console, "warn"); + expect(projectScripts[projectType].getStart([])).toEqual([ + "./build/server/index.js", + ]); + expect(logSpy).toHaveBeenCalled(); + }); + }); + + describe("getBuild with empty args", () => { + test('should return array with only "vite:build" if args is empty', () => { + expect(projectScripts[projectType].getBuild([])).toEqual(["vite:build"]); + }); + }); + + describe("getDev", () => { + test('should prepend "vite:dev" to the args array', () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getDev(args)).toEqual([ + "vite:dev", + ...args, + ]); + }); + }); + + describe("getStart", () => { + test("should return args", () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getStart(args)).toEqual([...args]); + }); + }); + + describe("getBuild", () => { + test('should prepend "vite:build" to the args array', () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getBuild(args)).toEqual([ + "vite:build", + ...args, + ]); + }); + }); +}); + +describe("REMIX_SPA project type", () => { + const projectType = ProjectTypes.REMIX_SPA; + + describe("getDev with empty args", () => { + test('should return array with only "vite:dev" if args is empty', () => { + expect(projectScripts[projectType].getDev([])).toEqual(["vite:dev"]); + }); + }); + + describe("getStart with empty args", () => { + test('should return array with only "preview" if args is empty', () => { + expect(projectScripts[projectType].getStart([])).toEqual(["preview"]); + }); + }); + + describe("getBuild with empty args", () => { + test('should return array with only "vite:build" if args is empty', () => { + expect(projectScripts[projectType].getBuild([])).toEqual(["vite:build"]); + }); + }); + + describe("getDev", () => { + test('should prepend "vite:dev" to the args array', () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getDev(args)).toEqual([ + "vite:dev", + ...args, + ]); + }); + }); + + describe("getStart", () => { + test('should prepend "preview" to the args array', () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getStart(args)).toEqual([ + "preview", + ...args, + ]); + }); + }); + + describe("getBuild", () => { + test('should prepend "vite:build" to the args array', () => { + const args = ["--arg1", "--arg2"]; + expect(projectScripts[projectType].getBuild(args)).toEqual([ + "vite:build", + ...args, + ]); + }); + }); +}); + describe("CRACO project type", () => { const projectType = ProjectTypes.CRACO; @@ -318,39 +423,39 @@ describe("UNKNOWN project type", () => { const projectType = ProjectTypes.UNKNOWN; describe("getDev with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test("should return empty array if args is empty", () => { expect(projectScripts[projectType].getDev([])).toEqual([]); }); }); describe("getStart with empty args", () => { - test('should return array with only "start" if args is empty', () => { + test("should return empty array if args is empty", () => { expect(projectScripts[projectType].getStart([])).toEqual([]); }); }); describe("getBuild with empty args", () => { - test('should return array with only "build" if args is empty', () => { + test("should return empty array if args is empty", () => { expect(projectScripts[projectType].getBuild([])).toEqual([]); }); }); describe("getDev", () => { - test('should prepend "start" to the args array', () => { + test("should return the args array as is", () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getDev(args)).toEqual([...args]); }); }); describe("getStart", () => { - test('should prepend "start" to the args array', () => { + test("should return the args array as is", () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getStart(args)).toEqual([...args]); }); }); describe("getBuild", () => { - test('should prepend "build" to the args array', () => { + test("should return the args array as is", () => { const args = ["--arg1", "--arg2"]; expect(projectScripts[projectType].getBuild(args)).toEqual([...args]); }); diff --git a/packages/cli/src/commands/runner/projectScripts.ts b/packages/cli/src/commands/runner/projectScripts.ts index b3e9547e509c7..47d384be79b68 100644 --- a/packages/cli/src/commands/runner/projectScripts.ts +++ b/packages/cli/src/commands/runner/projectScripts.ts @@ -50,6 +50,43 @@ export const projectScripts = { return require.resolve(`.bin/${binName}`); }, }, + [ProjectTypes.REMIX_VITE]: { + getDev: (args: string[]) => ["vite:dev", ...args], + getStart: (args: string[]) => { + // remix-serve accepts a path to the entry file as an argument + // if we have arguments, we will pass them to remix-serve and do nothing. + // ex: `refine start ./build/server/index.js` + const hasArgs = args?.length; + if (hasArgs) { + return args; + } + + // otherwise print a warning and use `./build/server/index.js` as default + console.log(); + console.warn( + "🚨 Remix requires a path to the entry file. Please provide it as an argument to `refine start` command in package.json scripts", + ); + console.warn("Refine will use `./build/server/index.js` as default"); + console.warn("Example: `refine start ./build/server/index.js`"); + console.log(); + + return ["./build/server/index.js"]; + }, + getBuild: (args: string[]) => ["vite:build", ...args], + getBin: (type?: "dev" | "start" | "build") => { + const binName = type === "start" ? "remix-serve" : "remix"; + return require.resolve(`.bin/${binName}`); + }, + }, + [ProjectTypes.REMIX_SPA]: { + getDev: (args: string[]) => ["vite:dev", ...args], + getStart: (args: string[]) => ["preview", ...args], + getBuild: (args: string[]) => ["vite:build", ...args], + getBin: (type?: "dev" | "start" | "build") => { + const binName = type === "start" ? "vite" : "remix"; + return require.resolve(`.bin/${binName}`); + }, + }, [ProjectTypes.CRACO]: { getDev: (args: string[]) => ["start", ...args], getStart: (args: string[]) => ["start", ...args], diff --git a/packages/cli/src/commands/runner/utils/index.ts b/packages/cli/src/commands/runner/utils/index.ts index 543eeab0d8e9e..59b0deeed82b9 100644 --- a/packages/cli/src/commands/runner/utils/index.ts +++ b/packages/cli/src/commands/runner/utils/index.ts @@ -18,8 +18,16 @@ export const getRunnerDescription = (runner: "dev" | "start" | "build") => { break; } - if (projectType === ProjectTypes.REMIX && runner === "start") { - projectType = "remix-serve" as ProjectTypes; + if (runner === "start") { + switch (projectType) { + case ProjectTypes.REMIX: + case ProjectTypes.REMIX_VITE: + projectType = "remix-serve" as ProjectTypes; + break; + case ProjectTypes.REMIX_SPA: + projectType = ProjectTypes.VITE; + break; + } } return `It runs: \`${projectType} ${command.join( diff --git a/packages/cli/src/definitions/projectTypes.ts b/packages/cli/src/definitions/projectTypes.ts index 1899217e26c85..ec3b6f5cf91ab 100644 --- a/packages/cli/src/definitions/projectTypes.ts +++ b/packages/cli/src/definitions/projectTypes.ts @@ -1,6 +1,8 @@ export enum ProjectTypes { REACT_SCRIPT = "react-scripts", REMIX = "remix", + REMIX_VITE = "remix-vite", + REMIX_SPA = "remix-spa", NEXTJS = "nextjs", VITE = "vite", CRACO = "craco", diff --git a/packages/cli/src/utils/project/index.ts b/packages/cli/src/utils/project/index.ts index 5536218659ffb..d03608dac80ba 100644 --- a/packages/cli/src/utils/project/index.ts +++ b/packages/cli/src/utils/project/index.ts @@ -36,6 +36,11 @@ export const getProjectType = (platform?: ProjectTypes): ProjectTypes => { dependencies.includes("@remix-run/react") || devDependencies.includes("@remix-run/react") ) { + // check for remix-vite + if (dependencies.includes("vite") || devDependencies.includes("vite")) { + return ProjectTypes.REMIX_VITE; + } + return ProjectTypes.REMIX; } diff --git a/packages/cli/src/utils/resource/index.spec.ts b/packages/cli/src/utils/resource/index.spec.ts index 1b4c0c8130aa0..f92cdb0c27a5d 100644 --- a/packages/cli/src/utils/resource/index.spec.ts +++ b/packages/cli/src/utils/resource/index.spec.ts @@ -12,6 +12,16 @@ it("should get provider path", () => { alias: "~/providers", }); + expect(getProviderPath(ProjectTypes.REMIX_VITE)).toEqual({ + path: "app/providers", + alias: "~/providers", + }); + + expect(getProviderPath(ProjectTypes.REMIX_SPA)).toEqual({ + path: "app/providers", + alias: "~/providers", + }); + expect(getProviderPath(ProjectTypes.VITE)).toEqual({ path: "src/providers", alias: "providers", diff --git a/packages/cli/src/utils/resource/index.ts b/packages/cli/src/utils/resource/index.ts index 0fe9f4354c841..091165e87ed48 100644 --- a/packages/cli/src/utils/resource/index.ts +++ b/packages/cli/src/utils/resource/index.ts @@ -11,6 +11,8 @@ export const getResourcePath = ( alias: "../src/components", }; case ProjectTypes.REMIX: + case ProjectTypes.REMIX_VITE: + case ProjectTypes.REMIX_SPA: return { path: "app/components", alias: "~/components", @@ -34,6 +36,8 @@ export const getProviderPath = ( alias: "../src/providers", }; case ProjectTypes.REMIX: + case ProjectTypes.REMIX_VITE: + case ProjectTypes.REMIX_SPA: return { path: "app/providers", alias: "~/providers", @@ -50,6 +54,8 @@ export const getProviderPath = ( export const getFilesPathByProject = (projectType?: ProjectTypes) => { switch (projectType) { case ProjectTypes.REMIX: + case ProjectTypes.REMIX_VITE: + case ProjectTypes.REMIX_SPA: return "./app"; default: return "./src";