Skip to content

Commit

Permalink
feat(shadcn): install routes for next-pages, laravel and react-router (
Browse files Browse the repository at this point in the history
…#6811)

* feat(shadcn): install routes for next-pages, laravel and react-router

* chore: changeset
  • Loading branch information
shadcn authored Feb 28, 2025
1 parent 1832f25 commit bc7df68
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/tasty-walls-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn": minor
---

add support for route install for react-router and laravel
9 changes: 9 additions & 0 deletions packages/shadcn/src/utils/frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 17 additions & 6 deletions packages/shadcn/src/utils/get-project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,11 +50,14 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
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),
Expand Down Expand Up @@ -128,6 +131,14 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
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.
Expand Down
62 changes: 55 additions & 7 deletions packages/shadcn/src/utils/updaters/update-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -216,20 +222,26 @@ export function resolveFilePath(
options: {
isSrcDir?: boolean
commonRoot?: string
framework?: ProjectInfo["framework"]["name"]
}
) {
if (file.target) {
if (file.target.startsWith("~/")) {
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)
Expand Down Expand Up @@ -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 ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
Expand Down
172 changes: 172 additions & 0 deletions packages/shadcn/test/utils/updaters/update-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,178 @@ describe("resolveFilePath", () => {
})
})

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([
{
Expand Down

0 comments on commit bc7df68

Please sign in to comment.