Skip to content

Commit

Permalink
feat(cli): add min-config support for Next.js (#2454)
Browse files Browse the repository at this point in the history
* feat(cli): add zero-config support for Next.js

* chore: add changeset

* feat(cli): add preflight
  • Loading branch information
shadcn authored Jan 16, 2024
1 parent 59b2cc8 commit 0374ba8
Show file tree
Hide file tree
Showing 75 changed files with 12,098 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-pandas-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn-ui": minor
---

minimal config for Next.js
11 changes: 10 additions & 1 deletion apps/www/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
domains: ["avatars.githubusercontent.com", "images.unsplash.com"],
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
redirects() {
return [
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"build:cli": "turbo --filter=shadcn-ui build",
"build:registry": "pnpm --filter=www build:registry",
"dev": "turbo run dev --parallel",
"dev:cli": "turbo --filter=shadcn-ui dev",
"start:cli": "pnpm --filter=shadcn-ui start:dev",
"cli:dev": "turbo --filter=shadcn-ui dev",
"cli:start": "pnpm --filter=shadcn-ui start:dev",
"www:dev": "pnpm --filter=www dev",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"preview": "turbo run preview",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"cosmiconfig": "^8.1.3",
"diff": "^5.1.0",
"execa": "^7.0.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.1.0",
"https-proxy-agent": "^6.2.0",
"lodash.template": "^4.5.0",
Expand Down
101 changes: 96 additions & 5 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type Config,
} from "@/src/utils/get-config"
import { getPackageManager } from "@/src/utils/get-package-manager"
import { getProjectConfig, preFlight } from "@/src/utils/get-project-info"
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
import {
Expand Down Expand Up @@ -39,12 +40,14 @@ const PROJECT_DEPENDENCIES = [
const initOptionsSchema = z.object({
cwd: z.string(),
yes: z.boolean(),
defaults: z.boolean(),
})

export const init = new Command()
.name("init")
.description("initialize your project and install dependencies")
.option("-y, --yes", "skip confirmation prompt.", false)
.option("-d, --defaults,", "use default configuration.", false)
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
Expand All @@ -61,15 +64,28 @@ export const init = new Command()
process.exit(1)
}

// Read config.
const existingConfig = await getConfig(cwd)
const config = await promptForConfig(cwd, existingConfig, options.yes)
preFlight(cwd)

await runInit(cwd, config)
const projectConfig = await getProjectConfig(cwd)
if (projectConfig) {
const config = await promptForMinimalConfig(
cwd,
projectConfig,
opts.defaults
)
await runInit(cwd, config)
} else {
// Read config.
const existingConfig = await getConfig(cwd)
const config = await promptForConfig(cwd, existingConfig, options.yes)
await runInit(cwd, config)
}

logger.info("")
logger.info(
`${chalk.green("Success!")} Project initialization completed.`
`${chalk.green(
"Success!"
)} Project initialization completed. You may now add components.`
)
logger.info("")
} catch (error) {
Expand Down Expand Up @@ -213,6 +229,81 @@ export async function promptForConfig(
return await resolveConfigPaths(cwd, config)
}

export async function promptForMinimalConfig(
cwd: string,
defaultConfig: Config,
defaults = false
) {
const highlight = (text: string) => chalk.cyan(text)
let style = defaultConfig.style
let baseColor = defaultConfig.tailwind.baseColor
let cssVariables = defaultConfig.tailwind.cssVariables

if (!defaults) {
const styles = await getRegistryStyles()
const baseColors = await getRegistryBaseColors()

const options = await prompts([
{
type: "select",
name: "style",
message: `Which ${highlight("style")} would you like to use?`,
choices: styles.map((style) => ({
title: style.label,
value: style.name,
})),
},
{
type: "select",
name: "tailwindBaseColor",
message: `Which color would you like to use as ${highlight(
"base color"
)}?`,
choices: baseColors.map((color) => ({
title: color.label,
value: color.name,
})),
},
{
type: "toggle",
name: "tailwindCssVariables",
message: `Would you like to use ${highlight(
"CSS variables"
)} for colors?`,
initial: defaultConfig?.tailwind.cssVariables,
active: "yes",
inactive: "no",
},
])

style = options.style
baseColor = options.tailwindBaseColor
cssVariables = options.tailwindCssVariables
}

const config = rawConfigSchema.parse({
$schema: defaultConfig?.$schema,
style,
tailwind: {
...defaultConfig?.tailwind,
baseColor,
cssVariables,
},
rsc: defaultConfig?.rsc,
tsx: defaultConfig?.tsx,
aliases: defaultConfig?.aliases,
})

// Write to file.
logger.info("")
const spinner = ora(`Writing components.json...`).start()
const targetPath = path.resolve(cwd, "components.json")
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8")
spinner.succeed()

return await resolveConfigPaths(cwd, config)
}

export async function runInit(cwd: string, config: Config) {
const spinner = ora(`Initializing project...`)?.start()

Expand Down
152 changes: 151 additions & 1 deletion packages/cli/src/utils/get-project-info.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { existsSync } from "fs"
import path from "path"
import fs from "fs-extra"
import {
Config,
RawConfig,
getConfig,
resolveConfigPaths,
} from "@/src/utils/get-config"
import fg from "fast-glob"
import fs, { pathExists } from "fs-extra"
import { loadConfig } from "tsconfig-paths"

// TODO: Add support for more frameworks.
// We'll start with Next.js for now.
const PROJECT_TYPES = [
"next-app",
"next-app-src",
"next-pages",
"next-pages-src",
] as const

type ProjectType = (typeof PROJECT_TYPES)[number]

const PROJECT_SHARED_IGNORE = [
"**/node_modules/**",
".next",
"public",
"dist",
"build",
]

export async function getProjectInfo() {
const info = {
Expand Down Expand Up @@ -42,3 +69,126 @@ export async function getTsConfig() {
return null
}
}

export async function getProjectConfig(cwd: string): Promise<Config | null> {
// Check for existing component config.
const existingConfig = await getConfig(cwd)
if (existingConfig) {
return existingConfig
}

const projectType = await getProjectType(cwd)
const tailwindCssFile = await getTailwindCssFile(cwd)
const tsConfigAliasPrefix = await getTsConfigAliasPrefix(cwd)

if (!projectType || !tailwindCssFile || !tsConfigAliasPrefix) {
return null
}

const isTsx = await isTypeScriptProject(cwd)

const config: RawConfig = {
$schema: "https://ui.shadcn.com/schema.json",
rsc: ["next-app", "next-app-src"].includes(projectType),
tsx: isTsx,
style: "new-york",
tailwind: {
config: isTsx ? "tailwind.config.ts" : "tailwind.config.js",
baseColor: "zinc",
css: tailwindCssFile,
cssVariables: true,
prefix: "",
},
aliases: {
utils: `${tsConfigAliasPrefix}/lib/utils`,
components: `${tsConfigAliasPrefix}/components`,
},
}

return await resolveConfigPaths(cwd, config)
}

export async function getProjectType(cwd: string): Promise<ProjectType | null> {
const files = await fg.glob("**/*", {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,
})

const isNextProject = files.find((file) => file.startsWith("next.config."))
if (!isNextProject) {
return null
}

const isUsingSrcDir = await fs.pathExists(path.resolve(cwd, "src"))
const isUsingAppDir = await fs.pathExists(
path.resolve(cwd, `${isUsingSrcDir ? "src/" : ""}app`)
)

if (isUsingAppDir) {
return isUsingSrcDir ? "next-app-src" : "next-app"
}

return isUsingSrcDir ? "next-pages-src" : "next-pages"
}

export async function getTailwindCssFile(cwd: string) {
const files = await fg.glob("**/*.css", {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,
})

if (!files.length) {
return null
}

for (const file of files) {
const contents = await fs.readFile(path.resolve(cwd, file), "utf8")
// Assume that if the file contains `@tailwind base` it's the main css file.
if (contents.includes("@tailwind base")) {
return file
}
}

return null
}

export async function getTsConfigAliasPrefix(cwd: string) {
const tsConfig = await loadConfig(cwd)

if (tsConfig?.resultType === "failed" || !tsConfig?.paths) {
return null
}

// This assume that the first alias is the prefix.
for (const [alias, paths] of Object.entries(tsConfig.paths)) {
if (paths.includes("./*") || paths.includes("./src/*")) {
return alias.at(0)
}
}

return null
}

export async function isTypeScriptProject(cwd: string) {
// Check if cwd has a tsconfig.json file.
return pathExists(path.resolve(cwd, "tsconfig.json"))
}

export async function preFlight(cwd: string) {
// We need Tailwind CSS to be configured.
const tailwindConfig = await fg.glob("tailwind.config.*", {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,
})

if (!tailwindConfig.length) {
throw new Error(
"Tailwind CSS is not installed. Visit https://tailwindcss.com/docs/installation to get started."
)
}

return true
}
Binary file not shown.
27 changes: 27 additions & 0 deletions packages/cli/test/fixtures/next-app-js/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
17 changes: 17 additions & 0 deletions packages/cli/test/fixtures/next-app-js/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
Loading

1 comment on commit 0374ba8

@vercel
Copy link

@vercel vercel bot commented on 0374ba8 Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./apps/www

example-playground.vercel.app
ui-git-main-shadcn-pro.vercel.app
ui-shadcn-pro.vercel.app
ui.shadcn.com

Please sign in to comment.