Skip to content

Commit 0374ba8

Browse files
authored
feat(cli): add min-config support for Next.js (#2454)
* feat(cli): add zero-config support for Next.js * chore: add changeset * feat(cli): add preflight
1 parent 59b2cc8 commit 0374ba8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+12098
-17
lines changed

.changeset/grumpy-pandas-smell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn-ui": minor
3+
---
4+
5+
minimal config for Next.js

apps/www/next.config.mjs

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ const nextConfig = {
55
reactStrictMode: true,
66
swcMinify: true,
77
images: {
8-
domains: ["avatars.githubusercontent.com", "images.unsplash.com"],
8+
remotePatterns: [
9+
{
10+
protocol: "https",
11+
hostname: "avatars.githubusercontent.com",
12+
},
13+
{
14+
protocol: "https",
15+
hostname: "images.unsplash.com",
16+
},
17+
],
918
},
1019
redirects() {
1120
return [

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
"build:cli": "turbo --filter=shadcn-ui build",
1919
"build:registry": "pnpm --filter=www build:registry",
2020
"dev": "turbo run dev --parallel",
21-
"dev:cli": "turbo --filter=shadcn-ui dev",
22-
"start:cli": "pnpm --filter=shadcn-ui start:dev",
21+
"cli:dev": "turbo --filter=shadcn-ui dev",
22+
"cli:start": "pnpm --filter=shadcn-ui start:dev",
23+
"www:dev": "pnpm --filter=www dev",
2324
"lint": "turbo run lint",
2425
"lint:fix": "turbo run lint:fix",
2526
"preview": "turbo run preview",

packages/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"cosmiconfig": "^8.1.3",
5454
"diff": "^5.1.0",
5555
"execa": "^7.0.0",
56+
"fast-glob": "^3.3.2",
5657
"fs-extra": "^11.1.0",
5758
"https-proxy-agent": "^6.2.0",
5859
"lodash.template": "^4.5.0",

packages/cli/src/commands/init.ts

+96-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type Config,
1212
} from "@/src/utils/get-config"
1313
import { getPackageManager } from "@/src/utils/get-package-manager"
14+
import { getProjectConfig, preFlight } from "@/src/utils/get-project-info"
1415
import { handleError } from "@/src/utils/handle-error"
1516
import { logger } from "@/src/utils/logger"
1617
import {
@@ -39,12 +40,14 @@ const PROJECT_DEPENDENCIES = [
3940
const initOptionsSchema = z.object({
4041
cwd: z.string(),
4142
yes: z.boolean(),
43+
defaults: z.boolean(),
4244
})
4345

4446
export const init = new Command()
4547
.name("init")
4648
.description("initialize your project and install dependencies")
4749
.option("-y, --yes", "skip confirmation prompt.", false)
50+
.option("-d, --defaults,", "use default configuration.", false)
4851
.option(
4952
"-c, --cwd <cwd>",
5053
"the working directory. defaults to the current directory.",
@@ -61,15 +64,28 @@ export const init = new Command()
6164
process.exit(1)
6265
}
6366

64-
// Read config.
65-
const existingConfig = await getConfig(cwd)
66-
const config = await promptForConfig(cwd, existingConfig, options.yes)
67+
preFlight(cwd)
6768

68-
await runInit(cwd, config)
69+
const projectConfig = await getProjectConfig(cwd)
70+
if (projectConfig) {
71+
const config = await promptForMinimalConfig(
72+
cwd,
73+
projectConfig,
74+
opts.defaults
75+
)
76+
await runInit(cwd, config)
77+
} else {
78+
// Read config.
79+
const existingConfig = await getConfig(cwd)
80+
const config = await promptForConfig(cwd, existingConfig, options.yes)
81+
await runInit(cwd, config)
82+
}
6983

7084
logger.info("")
7185
logger.info(
72-
`${chalk.green("Success!")} Project initialization completed.`
86+
`${chalk.green(
87+
"Success!"
88+
)} Project initialization completed. You may now add components.`
7389
)
7490
logger.info("")
7591
} catch (error) {
@@ -213,6 +229,81 @@ export async function promptForConfig(
213229
return await resolveConfigPaths(cwd, config)
214230
}
215231

232+
export async function promptForMinimalConfig(
233+
cwd: string,
234+
defaultConfig: Config,
235+
defaults = false
236+
) {
237+
const highlight = (text: string) => chalk.cyan(text)
238+
let style = defaultConfig.style
239+
let baseColor = defaultConfig.tailwind.baseColor
240+
let cssVariables = defaultConfig.tailwind.cssVariables
241+
242+
if (!defaults) {
243+
const styles = await getRegistryStyles()
244+
const baseColors = await getRegistryBaseColors()
245+
246+
const options = await prompts([
247+
{
248+
type: "select",
249+
name: "style",
250+
message: `Which ${highlight("style")} would you like to use?`,
251+
choices: styles.map((style) => ({
252+
title: style.label,
253+
value: style.name,
254+
})),
255+
},
256+
{
257+
type: "select",
258+
name: "tailwindBaseColor",
259+
message: `Which color would you like to use as ${highlight(
260+
"base color"
261+
)}?`,
262+
choices: baseColors.map((color) => ({
263+
title: color.label,
264+
value: color.name,
265+
})),
266+
},
267+
{
268+
type: "toggle",
269+
name: "tailwindCssVariables",
270+
message: `Would you like to use ${highlight(
271+
"CSS variables"
272+
)} for colors?`,
273+
initial: defaultConfig?.tailwind.cssVariables,
274+
active: "yes",
275+
inactive: "no",
276+
},
277+
])
278+
279+
style = options.style
280+
baseColor = options.tailwindBaseColor
281+
cssVariables = options.tailwindCssVariables
282+
}
283+
284+
const config = rawConfigSchema.parse({
285+
$schema: defaultConfig?.$schema,
286+
style,
287+
tailwind: {
288+
...defaultConfig?.tailwind,
289+
baseColor,
290+
cssVariables,
291+
},
292+
rsc: defaultConfig?.rsc,
293+
tsx: defaultConfig?.tsx,
294+
aliases: defaultConfig?.aliases,
295+
})
296+
297+
// Write to file.
298+
logger.info("")
299+
const spinner = ora(`Writing components.json...`).start()
300+
const targetPath = path.resolve(cwd, "components.json")
301+
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8")
302+
spinner.succeed()
303+
304+
return await resolveConfigPaths(cwd, config)
305+
}
306+
216307
export async function runInit(cwd: string, config: Config) {
217308
const spinner = ora(`Initializing project...`)?.start()
218309

packages/cli/src/utils/get-project-info.ts

+151-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
import { existsSync } from "fs"
22
import path from "path"
3-
import fs from "fs-extra"
3+
import {
4+
Config,
5+
RawConfig,
6+
getConfig,
7+
resolveConfigPaths,
8+
} from "@/src/utils/get-config"
9+
import fg from "fast-glob"
10+
import fs, { pathExists } from "fs-extra"
11+
import { loadConfig } from "tsconfig-paths"
12+
13+
// TODO: Add support for more frameworks.
14+
// We'll start with Next.js for now.
15+
const PROJECT_TYPES = [
16+
"next-app",
17+
"next-app-src",
18+
"next-pages",
19+
"next-pages-src",
20+
] as const
21+
22+
type ProjectType = (typeof PROJECT_TYPES)[number]
23+
24+
const PROJECT_SHARED_IGNORE = [
25+
"**/node_modules/**",
26+
".next",
27+
"public",
28+
"dist",
29+
"build",
30+
]
431

532
export async function getProjectInfo() {
633
const info = {
@@ -42,3 +69,126 @@ export async function getTsConfig() {
4269
return null
4370
}
4471
}
72+
73+
export async function getProjectConfig(cwd: string): Promise<Config | null> {
74+
// Check for existing component config.
75+
const existingConfig = await getConfig(cwd)
76+
if (existingConfig) {
77+
return existingConfig
78+
}
79+
80+
const projectType = await getProjectType(cwd)
81+
const tailwindCssFile = await getTailwindCssFile(cwd)
82+
const tsConfigAliasPrefix = await getTsConfigAliasPrefix(cwd)
83+
84+
if (!projectType || !tailwindCssFile || !tsConfigAliasPrefix) {
85+
return null
86+
}
87+
88+
const isTsx = await isTypeScriptProject(cwd)
89+
90+
const config: RawConfig = {
91+
$schema: "https://ui.shadcn.com/schema.json",
92+
rsc: ["next-app", "next-app-src"].includes(projectType),
93+
tsx: isTsx,
94+
style: "new-york",
95+
tailwind: {
96+
config: isTsx ? "tailwind.config.ts" : "tailwind.config.js",
97+
baseColor: "zinc",
98+
css: tailwindCssFile,
99+
cssVariables: true,
100+
prefix: "",
101+
},
102+
aliases: {
103+
utils: `${tsConfigAliasPrefix}/lib/utils`,
104+
components: `${tsConfigAliasPrefix}/components`,
105+
},
106+
}
107+
108+
return await resolveConfigPaths(cwd, config)
109+
}
110+
111+
export async function getProjectType(cwd: string): Promise<ProjectType | null> {
112+
const files = await fg.glob("**/*", {
113+
cwd,
114+
deep: 3,
115+
ignore: PROJECT_SHARED_IGNORE,
116+
})
117+
118+
const isNextProject = files.find((file) => file.startsWith("next.config."))
119+
if (!isNextProject) {
120+
return null
121+
}
122+
123+
const isUsingSrcDir = await fs.pathExists(path.resolve(cwd, "src"))
124+
const isUsingAppDir = await fs.pathExists(
125+
path.resolve(cwd, `${isUsingSrcDir ? "src/" : ""}app`)
126+
)
127+
128+
if (isUsingAppDir) {
129+
return isUsingSrcDir ? "next-app-src" : "next-app"
130+
}
131+
132+
return isUsingSrcDir ? "next-pages-src" : "next-pages"
133+
}
134+
135+
export async function getTailwindCssFile(cwd: string) {
136+
const files = await fg.glob("**/*.css", {
137+
cwd,
138+
deep: 3,
139+
ignore: PROJECT_SHARED_IGNORE,
140+
})
141+
142+
if (!files.length) {
143+
return null
144+
}
145+
146+
for (const file of files) {
147+
const contents = await fs.readFile(path.resolve(cwd, file), "utf8")
148+
// Assume that if the file contains `@tailwind base` it's the main css file.
149+
if (contents.includes("@tailwind base")) {
150+
return file
151+
}
152+
}
153+
154+
return null
155+
}
156+
157+
export async function getTsConfigAliasPrefix(cwd: string) {
158+
const tsConfig = await loadConfig(cwd)
159+
160+
if (tsConfig?.resultType === "failed" || !tsConfig?.paths) {
161+
return null
162+
}
163+
164+
// This assume that the first alias is the prefix.
165+
for (const [alias, paths] of Object.entries(tsConfig.paths)) {
166+
if (paths.includes("./*") || paths.includes("./src/*")) {
167+
return alias.at(0)
168+
}
169+
}
170+
171+
return null
172+
}
173+
174+
export async function isTypeScriptProject(cwd: string) {
175+
// Check if cwd has a tsconfig.json file.
176+
return pathExists(path.resolve(cwd, "tsconfig.json"))
177+
}
178+
179+
export async function preFlight(cwd: string) {
180+
// We need Tailwind CSS to be configured.
181+
const tailwindConfig = await fg.glob("tailwind.config.*", {
182+
cwd,
183+
deep: 3,
184+
ignore: PROJECT_SHARED_IGNORE,
185+
})
186+
187+
if (!tailwindConfig.length) {
188+
throw new Error(
189+
"Tailwind CSS is not installed. Visit https://tailwindcss.com/docs/installation to get started."
190+
)
191+
}
192+
193+
return true
194+
}
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--foreground-rgb: 0, 0, 0;
7+
--background-start-rgb: 214, 219, 220;
8+
--background-end-rgb: 255, 255, 255;
9+
}
10+
11+
@media (prefers-color-scheme: dark) {
12+
:root {
13+
--foreground-rgb: 255, 255, 255;
14+
--background-start-rgb: 0, 0, 0;
15+
--background-end-rgb: 0, 0, 0;
16+
}
17+
}
18+
19+
body {
20+
color: rgb(var(--foreground-rgb));
21+
background: linear-gradient(
22+
to bottom,
23+
transparent,
24+
rgb(var(--background-end-rgb))
25+
)
26+
rgb(var(--background-start-rgb));
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Inter } from 'next/font/google'
2+
import './globals.css'
3+
4+
const inter = Inter({ subsets: ['latin'] })
5+
6+
export const metadata = {
7+
title: 'Create Next App',
8+
description: 'Generated by create next app',
9+
}
10+
11+
export default function RootLayout({ children }) {
12+
return (
13+
<html lang="en">
14+
<body className={inter.className}>{children}</body>
15+
</html>
16+
)
17+
}

0 commit comments

Comments
 (0)