From bc7df68620f242ce6aa640839c80ddc8afc7e091 Mon Sep 17 00:00:00 2001 From: shadcn Date: Fri, 28 Feb 2025 18:06:06 +0400 Subject: [PATCH] feat(shadcn): install routes for next-pages, laravel and react-router (#6811) * feat(shadcn): install routes for next-pages, laravel and react-router * chore: changeset --- .changeset/tasty-walls-drum.md | 5 + packages/shadcn/src/utils/frameworks.ts | 9 + packages/shadcn/src/utils/get-project-info.ts | 23 ++- .../shadcn/src/utils/updaters/update-files.ts | 62 ++++++- .../registry-resolve-items-tree.test.ts.snap | 2 +- .../test/utils/updaters/update-files.test.ts | 172 ++++++++++++++++++ 6 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 .changeset/tasty-walls-drum.md diff --git a/.changeset/tasty-walls-drum.md b/.changeset/tasty-walls-drum.md new file mode 100644 index 00000000000..ea8d9082888 --- /dev/null +++ b/.changeset/tasty-walls-drum.md @@ -0,0 +1,5 @@ +--- +"shadcn": minor +--- + +add support for route install for react-router and laravel diff --git a/packages/shadcn/src/utils/frameworks.ts b/packages/shadcn/src/utils/frameworks.ts index e1bb218cd05..3a5b9c57334 100644 --- a/packages/shadcn/src/utils/frameworks.ts +++ b/packages/shadcn/src/utils/frameworks.ts @@ -23,6 +23,15 @@ export const FRAMEWORKS = { tailwind: "https://tailwindcss.com/docs/guides/remix", }, }, + "react-router": { + name: "react-router", + label: "React Router", + links: { + installation: "https://ui.shadcn.com/docs/installation/react-router", + tailwind: + "https://tailwindcss.com/docs/installation/framework-guides/react-router", + }, + }, vite: { name: "vite", label: "Vite", diff --git a/packages/shadcn/src/utils/get-project-info.ts b/packages/shadcn/src/utils/get-project-info.ts index 0210119b5ea..c82d6f77c18 100644 --- a/packages/shadcn/src/utils/get-project-info.ts +++ b/packages/shadcn/src/utils/get-project-info.ts @@ -14,7 +14,7 @@ import { z } from "zod" export type TailwindVersion = "v3" | "v4" | null -type ProjectInfo = { +export type ProjectInfo = { framework: Framework isSrcDir: boolean isRSC: boolean @@ -50,11 +50,14 @@ export async function getProjectInfo(cwd: string): Promise { aliasPrefix, packageJson, ] = await Promise.all([ - fg.glob("**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json", { - cwd, - deep: 3, - ignore: PROJECT_SHARED_IGNORE, - }), + fg.glob( + "**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", + { + cwd, + deep: 3, + ignore: PROJECT_SHARED_IGNORE, + } + ), fs.pathExists(path.resolve(cwd, "src")), isTypeScriptProject(cwd), getTailwindConfigFile(cwd), @@ -128,6 +131,14 @@ export async function getProjectInfo(cwd: string): Promise { return type } + // React Router. + if ( + configFiles.find((file) => file.startsWith("react-router.config."))?.length + ) { + type.framework = FRAMEWORKS["react-router"] + return type + } + // Vite. // Some Remix templates also have a vite.config.* file. // We'll assume that it got caught by the Remix check above. diff --git a/packages/shadcn/src/utils/updaters/update-files.ts b/packages/shadcn/src/utils/updaters/update-files.ts index 7dd3188c33b..5ad751cbab6 100644 --- a/packages/shadcn/src/utils/updaters/update-files.ts +++ b/packages/shadcn/src/utils/updaters/update-files.ts @@ -3,7 +3,7 @@ import path, { basename } from "path" import { getRegistryBaseColor } from "@/src/registry/api" import { RegistryItem, registryItemFileSchema } from "@/src/registry/schema" import { Config } from "@/src/utils/get-config" -import { getProjectInfo } from "@/src/utils/get-project-info" +import { ProjectInfo, getProjectInfo } from "@/src/utils/get-project-info" import { highlighter } from "@/src/utils/highlighter" import { logger } from "@/src/utils/logger" import { spinner } from "@/src/utils/spinner" @@ -61,11 +61,17 @@ export async function updateFiles( let filePath = resolveFilePath(file, config, { isSrcDir: projectInfo?.isSrcDir, + framework: projectInfo?.framework.name, commonRoot: findCommonRoot( files.map((f) => f.path), file.path ), }) + + if (!filePath) { + continue + } + const fileName = basename(file.path) const targetDir = path.dirname(filePath) @@ -216,6 +222,7 @@ export function resolveFilePath( options: { isSrcDir?: boolean commonRoot?: string + framework?: ProjectInfo["framework"]["name"] } ) { if (file.target) { @@ -223,13 +230,18 @@ export function resolveFilePath( return path.join(config.resolvedPaths.cwd, file.target.replace("~/", "")) } + let target = file.target + + if (file.type === "registry:page") { + target = resolvePageTarget(target, options.framework) + if (!target) { + return "" + } + } + return options.isSrcDir - ? path.join( - config.resolvedPaths.cwd, - "src", - file.target.replace("src/", "") - ) - : path.join(config.resolvedPaths.cwd, file.target.replace("src/", "")) + ? path.join(config.resolvedPaths.cwd, "src", target.replace("src/", "")) + : path.join(config.resolvedPaths.cwd, target.replace("src/", "")) } const targetDir = resolveFileTargetDirectory(file, config) @@ -323,3 +335,39 @@ export function resolveNestedFilePath( export async function getNormalizedFileContent(content: string) { return content.replace(/\r\n/g, "\n").trim() } + +export function resolvePageTarget( + target: string, + framework?: ProjectInfo["framework"]["name"] +) { + if (!framework) { + return "" + } + + if (framework === "next-app") { + return target + } + + if (framework === "next-pages") { + let result = target.replace(/^app\//, "pages/") + result = result.replace(/\/page(\.[jt]sx?)$/, "$1") + + return result + } + + if (framework === "react-router") { + let result = target.replace(/^app\//, "app/routes/") + result = result.replace(/\/page(\.[jt]sx?)$/, "$1") + + return result + } + + if (framework === "laravel") { + let result = target.replace(/^app\//, "resources/js/pages/") + result = result.replace(/\/page(\.[jt]sx?)$/, "$1") + + return result + } + + return "" +} diff --git a/packages/shadcn/test/utils/schema/__snapshots__/registry-resolve-items-tree.test.ts.snap b/packages/shadcn/test/utils/schema/__snapshots__/registry-resolve-items-tree.test.ts.snap index 4f950ec2b3d..9a1725f7be5 100644 --- a/packages/shadcn/test/utils/schema/__snapshots__/registry-resolve-items-tree.test.ts.snap +++ b/packages/shadcn/test/utils/schema/__snapshots__/registry-resolve-items-tree.test.ts.snap @@ -445,7 +445,7 @@ const DialogOverlay = React.forwardRef< { }) }) +describe("resolveFilePath with framework", () => { + test("should not resolve for unknown or unsupported framework", () => { + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/components", + ui: "/foo/bar/components/ui", + lib: "/foo/bar/lib", + hooks: "/foo/bar/hooks", + }, + }, + { + isSrcDir: false, + } + ) + ).toBe("") + + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/components", + ui: "/foo/bar/components/ui", + lib: "/foo/bar/lib", + hooks: "/foo/bar/hooks", + }, + }, + { + isSrcDir: false, + framework: "vite", + } + ) + ).toBe("") + }) + + test("should resolve for next-app", () => { + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/components", + ui: "/foo/bar/components/ui", + lib: "/foo/bar/lib", + hooks: "/foo/bar/hooks", + }, + }, + { + isSrcDir: false, + framework: "next-app", + } + ) + ).toBe("/foo/bar/app/login/page.tsx") + }) + + test("should resolve for next-pages", () => { + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/src/components", + ui: "/foo/bar/src/primitives", + lib: "/foo/bar/src/lib", + hooks: "/foo/bar/src/hooks", + }, + }, + { + isSrcDir: true, + framework: "next-pages", + } + ) + ).toBe("/foo/bar/src/pages/login.tsx") + + expect( + resolveFilePath( + { + path: "hello-world/app/blog/[slug]/page.tsx", + type: "registry:page", + target: "app/blog/[slug]/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/components", + ui: "/foo/bar/primitives", + lib: "/foo/bar/lib", + hooks: "/foo/bar/hooks", + }, + }, + { + isSrcDir: false, + framework: "next-pages", + } + ) + ).toBe("/foo/bar/pages/blog/[slug].tsx") + }) + + test("should resolve for react-router", () => { + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/app/components", + ui: "/foo/bar/app/components/ui", + lib: "/foo/bar/app/lib", + hooks: "/foo/bar/app/hooks", + }, + }, + { + isSrcDir: false, + framework: "react-router", + } + ) + ).toBe("/foo/bar/app/routes/login.tsx") + }) + + test("should resolve for laravel", () => { + expect( + resolveFilePath( + { + path: "hello-world/app/login/page.tsx", + type: "registry:page", + target: "app/login/page.tsx", + }, + { + resolvedPaths: { + cwd: "/foo/bar", + components: "/foo/bar/resources/js/components", + ui: "/foo/bar/resources/js/components/ui", + lib: "/foo/bar/resources/js/lib", + hooks: "/foo/bar/resources/js/hooks", + }, + }, + { + isSrcDir: false, + framework: "laravel", + } + ) + ).toBe("/foo/bar/resources/js/pages/login.tsx") + }) +}) + describe("findCommonRoot", () => { test.each([ {