Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/shadcn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"recast": "^0.23.11",
"stringify-object": "^5.0.0",
"ts-morph": "^26.0.0",
"ts-morph-react": "^1.0.6",
"tsconfig-paths": "^4.2.0",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.6"
Expand Down
14 changes: 14 additions & 0 deletions packages/shadcn/src/registry/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ export const rawConfigSchema = z
iconLibrary: z.string().optional(),
menuColor: z.enum(["default", "inverted"]).default("default").optional(),
menuAccent: z.enum(["subtle", "bold"]).default("subtle").optional(),
transform: z
.object({
enforceDirectExports: z.boolean().default(false).optional(),
enforceFunctionComponent: z.boolean().default(false).optional(),
enforceNamedImports: z.boolean().default(false).optional(),
enforceFormat: z.boolean().default(false).optional(),
enforceEslint: z.boolean().default(false).optional(),
enforcePrettier: z.boolean().default(false).optional(),
enforceLineSeparation: z.boolean().default(false).optional(),
format: z.any(),
eslint: z.any(),
prettier: z.any(),
})
.optional(),
aliases: z.object({
components: z.string(),
utils: z.string(),
Expand Down
2 changes: 2 additions & 0 deletions packages/shadcn/src/utils/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
import { transformIcons } from "@/src/utils/transformers/transform-icons"
import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformJsx } from "@/src/utils/transformers/transform-jsx"
import { transformReact } from "@/src/utils/transformers/transform-react"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import { z } from "zod"
Expand Down Expand Up @@ -45,6 +46,7 @@ export async function transform(
transformCssVars,
transformTwPrefixes,
transformIcons,
transformReact,
]
) {
const tempFile = await createTempSourceFile(opts.filename)
Expand Down
10 changes: 10 additions & 0 deletions packages/shadcn/src/utils/transformers/transform-react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Transformer } from "@/src/utils/transformers"
import { transform } from "ts-morph-react"

export const transformReact: Transformer = async ({ sourceFile, config }) => {
if (!config.tsx) {
return sourceFile
}
await transform(sourceFile, { ...config.transform })
return sourceFile
}
2 changes: 2 additions & 0 deletions packages/shadcn/src/utils/updaters/update-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { transformIcons } from "@/src/utils/transformers/transform-icons"
import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformMenu } from "@/src/utils/transformers/transform-menu"
import { transformNext } from "@/src/utils/transformers/transform-next"
import { transformReact } from "@/src/utils/transformers/transform-react"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
import prompts from "prompts"
Expand Down Expand Up @@ -146,6 +147,7 @@ export async function updateFiles(
...(_isNext16Middleware(filePath, projectInfo, config)
? [transformNext]
: []),
transformReact,
]
)

Expand Down
42 changes: 42 additions & 0 deletions packages/shadcn/test/fixtures/transform-react/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const example = `"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"

import { cn } from "@/lib/utils"

type ExampleContextProps = {
content: string
}

const ExampleContext = React.createContext<ExampleContextProps | null>(null)

function ExampleProvider({
value,
...props
}: React.PropsWithChildren<{
value: ExampleContextProps
}>) {
return (
<ExampleContext.Provider value={value}>
{props.children}
</ExampleContext.Provider>
)
}

function Example({
content,
className,
...props
}: ButtonPrimitive.Props & {
content: string
}) {
return (
<ExampleProvider value={{ content }}>
<ButtonPrimitive className={cn('p-2', className)} {...props} />
</ExampleProvider>
)
}

export { ExampleProvider, Example }
`
118 changes: 118 additions & 0 deletions packages/shadcn/test/utils/__snapshots__/transform-react.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`combined 1`] = `
"'use client'

import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { type FunctionComponent, type PropsWithChildren, createContext } from 'react'

import { cn } from '@/lib/utils'

type ExampleContextProps = { content: string }

const ExampleContext = createContext<ExampleContextProps | null>(null)
export const ExampleProvider: FunctionComponent<PropsWithChildren<{ value: ExampleContextProps }>> = ({
value,
...props
}) => {
return <ExampleContext.Provider value={value}>{props.children}</ExampleContext.Provider>
}
export const Example: FunctionComponent<ButtonPrimitive.Props & { content: string }> = ({
content,
className,
...props
}) => {
return (
<ExampleProvider value={{ content }}>
<ButtonPrimitive className={cn('p-2', className)} {...props} />
</ExampleProvider>
)
}
"
`;

exports[`enforceDirectExports 1`] = `
"'use client'

import { Button as ButtonPrimitive } from '@base-ui/react/button'
import * as React from 'react'

import { cn } from '@/lib/utils'

type ExampleContextProps = { content: string }

const ExampleContext = React.createContext<ExampleContextProps | null>(null)

export function ExampleProvider({ value, ...props }: React.PropsWithChildren<{ value: ExampleContextProps }>) {
return <ExampleContext.Provider value={value}>{props.children}</ExampleContext.Provider>
}

export function Example({ content, className, ...props }: ButtonPrimitive.Props & { content: string }) {
return (
<ExampleProvider value={{ content }}>
<ButtonPrimitive className={cn('p-2', className)} {...props} />
</ExampleProvider>
)
}
"
`;

exports[`enforceFunctionComponent 1`] = `
"'use client'

import { Button as ButtonPrimitive } from '@base-ui/react/button'
import * as React from 'react'

import { cn } from '@/lib/utils'

type ExampleContextProps = { content: string }

const ExampleContext = React.createContext<ExampleContextProps | null>(null)
const ExampleProvider: React.FunctionComponent<React.PropsWithChildren<{ value: ExampleContextProps }>> = ({
value,
...props
}) => {
return <ExampleContext.Provider value={value}>{props.children}</ExampleContext.Provider>
}
const Example: React.FunctionComponent<ButtonPrimitive.Props & { content: string }> = ({
content,
className,
...props
}) => {
return (
<ExampleProvider value={{ content }}>
<ButtonPrimitive className={cn('p-2', className)} {...props} />
</ExampleProvider>
)
}
export { Example, ExampleProvider }
"
`;

exports[`enforceNamedImports 1`] = `
"'use client'

import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { type PropsWithChildren, createContext } from 'react'

import { cn } from '@/lib/utils'

type ExampleContextProps = { content: string }

const ExampleContext = createContext<ExampleContextProps | null>(null)

function ExampleProvider({ value, ...props }: PropsWithChildren<{ value: ExampleContextProps }>) {
return <ExampleContext.Provider value={value}>{props.children}</ExampleContext.Provider>
}

function Example({ content, className, ...props }: ButtonPrimitive.Props & { content: string }) {
return (
<ExampleProvider value={{ content }}>
<ButtonPrimitive className={cn('p-2', className)} {...props} />
</ExampleProvider>
)
}

export { Example, ExampleProvider }
"
`;
85 changes: 85 additions & 0 deletions packages/shadcn/test/utils/transform-react.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { TransformerConfig } from "ts-morph-react"
import { expect, test } from "vitest"
import { transform } from "../../src/utils/transformers"
import { example } from "../fixtures/transform-react/example"

const sharedConfig = {
tsx: true,
rsc: true,
style: "base-lyra",
tailwind: { baseColor: "neutral", cssVariables: true },
aliases: { components: "@/components", lib: "@/lib", utils: "@/lib/utils" }
}

const transformConfig = {
enforceFormat: true,
enforceEslint: true,
enforcePrettier: true,
eslint: {},
format: {},
prettier: {}
}

test("enforceNamedImports", async () => {
expect(await transform({
filename: "test.tsx",
raw: example,
config: {
...sharedConfig,
transform: {
enforceNamedImports: true,
enforceDirectExports: false,
enforceFunctionComponent: false,
...transformConfig
} satisfies TransformerConfig
}
})).toMatchSnapshot()
})

test("enforceDirectExports", async () => {
expect(await transform({
filename: "test.tsx",
raw: example,
config: {
...sharedConfig,
transform: {
enforceNamedImports: false,
enforceDirectExports: true,
enforceFunctionComponent: false,
...transformConfig
} satisfies TransformerConfig
}
})).toMatchSnapshot()
})

test("enforceFunctionComponent", async () => {
expect(await transform({
filename: "test.tsx",
raw: example,
config: {
...sharedConfig,
transform: {
enforceNamedImports: false,
enforceDirectExports: false,
enforceFunctionComponent: true,
...transformConfig
} satisfies TransformerConfig
}
})).toMatchSnapshot()
})

test("combined", async () => {
expect(await transform({
filename: "test.tsx",
raw: example,
config: {
...sharedConfig,
transform: {
enforceNamedImports: true,
enforceDirectExports: true,
enforceFunctionComponent: true,
...transformConfig
} satisfies TransformerConfig
}
})).toMatchSnapshot()
})
Loading