This commit is contained in:
shadcn
2026-02-14 18:36:56 +04:00
parent 3e4c608aca
commit 525775fb36
102 changed files with 1353 additions and 853 deletions

View File

@@ -29,7 +29,6 @@ export const addOptionsSchema = z.object({
all: z.boolean(),
path: z.string().optional(),
silent: z.boolean(),
srcDir: z.boolean().optional(),
cssVariables: z.boolean(),
})
@@ -47,15 +46,6 @@ export const add = new Command()
.option("-a, --all", "add all available components", false)
.option("-p, --path <path>", "the path to add the component to.")
.option("-s, --silent", "mute output.", false)
.option(
"--src-dir",
"use the src directory when creating a new project.",
false
)
.option(
"--no-src-dir",
"do not use the src directory when creating a new project."
)
.option("--css-variables", "use css variables for theming.", true)
.option("--no-css-variables", "do not use css variables for theming.")
.action(async (components, opts) => {
@@ -177,10 +167,8 @@ export const add = new Command()
skipPreflight: false,
silent: options.silent && !hasNewRegistries,
isNewProject: false,
srcDir: options.srcDir,
cssVariables: options.cssVariables,
installStyleIndex: shouldInstallStyleIndex,
baseColor: shouldInstallStyleIndex ? undefined : "neutral",
components: options.components,
})
initHasRun = true
@@ -192,7 +180,6 @@ export const add = new Command()
const { projectPath, template } = await createProject({
cwd: options.cwd,
force: options.overwrite,
srcDir: options.srcDir,
components: options.components,
})
if (!projectPath) {
@@ -213,10 +200,8 @@ export const add = new Command()
skipPreflight: true,
silent: !hasNewRegistries && options.silent,
isNewProject: true,
srcDir: options.srcDir,
cssVariables: options.cssVariables,
installStyleIndex: shouldInstallStyleIndex,
baseColor: shouldInstallStyleIndex ? undefined : "neutral",
components: options.components,
})
initHasRun = true

View File

@@ -2,6 +2,7 @@ import path from "path"
import { getRegistryItems } from "@/src/registry/api"
import { configWithDefaults } from "@/src/registry/config"
import { clearRegistryContext } from "@/src/registry/context"
import { templates } from "@/src/templates/index"
import { addComponents } from "@/src/utils/add-components"
import { handleError } from "@/src/utils/handle-error"
import { highlighter } from "@/src/utils/highlighter"
@@ -12,7 +13,6 @@ import {
handlePresetOption,
} from "@/src/utils/presets"
import { ensureRegistriesInConfig } from "@/src/utils/registries"
import { templates } from "@/src/utils/templates/index"
import { updateFiles } from "@/src/utils/updaters/update-files"
import { Command } from "commander"
import open from "open"
@@ -35,15 +35,6 @@ export const create = new Command()
"the working directory. defaults to the current directory.",
process.cwd()
)
.option(
"--src-dir",
"use the src directory when creating a new project.",
false
)
.option(
"--no-src-dir",
"do not use the src directory when creating a new project."
)
.option("-y, --yes", "skip confirmation prompt.", true)
.option("--rtl", "enable RTL support.", false)
.action(async (name, opts) => {
@@ -146,9 +137,8 @@ export const create = new Command()
process.exit(0)
}
// Determine initUrl and baseColor based on preset type.
// Determine initUrl based on preset type.
let initUrl: string
let baseColor: string
if ("_isUrl" in presetResult) {
// User provided a URL directly.
@@ -157,11 +147,9 @@ export const create = new Command()
url.searchParams.set("rtl", "true")
}
initUrl = url.toString()
baseColor = url.searchParams.get("baseColor") ?? "neutral"
} else {
// User selected a preset by name.
initUrl = buildInitUrl(presetResult, opts.rtl)
baseColor = presetResult.baseColor
}
// Fetch the registry:base item to get its config.
@@ -192,11 +180,9 @@ export const create = new Command()
force: false,
silent: false,
isNewProject: true,
srcDir: opts.srcDir,
cssVariables: true,
rtl: opts.rtl,
template,
baseColor,
installStyleIndex: false,
registryBaseConfig,
skipPreflight: false,
@@ -215,10 +201,11 @@ export const create = new Command()
overwrite: true,
})
const templateFiles =
templates[template as keyof typeof templates]?.files ?? []
if (templateFiles.length > 0) {
await updateFiles(templateFiles, config, {
const selectedTemplate =
templates[template as keyof typeof templates]
if (selectedTemplate?.files?.length) {
await updateFiles(selectedTemplate.files, config, {
overwrite: true,
silent: true,
})
@@ -226,9 +213,7 @@ export const create = new Command()
}
logger.log(
`${highlighter.success(
"Success!"
)} Project initialization completed.\nYou may now add components.`
`Project initialization completed.\nYou may now add components.`
)
logger.break()
} catch (error) {

View File

@@ -8,9 +8,10 @@ import {
} from "@/src/registry/api"
import { buildUrlAndHeadersForRegistryItem } from "@/src/registry/builder"
import { configWithDefaults } from "@/src/registry/config"
import { BASE_COLORS, BUILTIN_REGISTRIES } from "@/src/registry/constants"
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
import { clearRegistryContext } from "@/src/registry/context"
import { rawConfigSchema } from "@/src/schema"
import { templates } from "@/src/templates/index"
import { addComponents } from "@/src/utils/add-components"
import { createProject } from "@/src/utils/create-project"
import { loadEnvFiles } from "@/src/utils/env-loader"
@@ -38,7 +39,6 @@ import {
} from "@/src/utils/get-project-info"
import { handleError } from "@/src/utils/handle-error"
import { highlighter } from "@/src/utils/highlighter"
import { initMonorepoProject } from "@/src/utils/init-monorepo"
import { logger } from "@/src/utils/logger"
import {
buildInitUrl,
@@ -47,8 +47,6 @@ import {
} from "@/src/utils/presets"
import { ensureRegistriesInConfig } from "@/src/utils/registries"
import { spinner } from "@/src/utils/spinner"
import { templates } from "@/src/utils/templates/index"
import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content"
import { Command } from "commander"
import deepmerge from "deepmerge"
import fsExtra from "fs-extra"
@@ -78,7 +76,6 @@ export const initOptionsSchema = z.object({
force: z.boolean(),
silent: z.boolean(),
isNewProject: z.boolean(),
srcDir: z.boolean().optional(),
cssVariables: z.boolean(),
rtl: z.boolean().optional(),
template: z
@@ -96,23 +93,6 @@ export const initOptionsSchema = z.object({
"Invalid template. Please use 'next', 'vite', 'start' or 'next-monorepo'.",
}
),
baseColor: z
.string()
.optional()
.refine(
(val) => {
if (val) {
return BASE_COLORS.find((color) => color.name === val)
}
return true
},
{
message: `Invalid base color. Please use '${BASE_COLORS.map(
(color) => color.name
).join("', '")}'`,
}
),
installStyleIndex: z.boolean(),
// Config from registry:base item to merge into components.json.
registryBaseConfig: rawConfigSchema.deepPartial().optional(),
@@ -126,40 +106,47 @@ export const init = new Command()
"-t, --template <template>",
"the template to use. (next, start, vite, next-monorepo)"
)
.option(
"-b, --base-color <base-color>",
"the base color to use. (neutral, gray, zinc, stone, slate)",
undefined
)
.option("-p, --preset [name]", "use a preset configuration")
.option("-y, --yes", "skip confirmation prompt.", true)
.option("-d, --defaults,", "use default configuration.", false)
.option(
"-d, --defaults,",
"use default configuration: --template=next --preset=base-nova",
false
)
.option("-f, --force", "force overwrite of existing configuration.", false)
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
)
.option("-n, --name <name>", "the name for the new project.")
.option("-s, --silent", "mute output.", false)
.option(
"--src-dir",
"use the src directory when creating a new project.",
false
)
.option(
"--no-src-dir",
"do not use the src directory when creating a new project."
)
.option("--css-variables", "use css variables for theming.", true)
.option("--no-css-variables", "do not use css variables for theming.")
.option("--no-base-style", "do not install the base shadcn style.")
.option("--rtl", "enable RTL support.", false)
.action(async (components, opts) => {
try {
// Apply defaults when --defaults flag is set.
if (opts.defaults) {
opts.template = opts.template || "next"
opts.baseColor = opts.baseColor || "neutral"
// Use base-nova preset as default.
const initUrl = buildInitUrl(
{
base: "base",
style: "nova",
baseColor: "neutral",
theme: "neutral",
iconLibrary: "lucide",
font: "geist",
rtl: opts.rtl ?? false,
menuAccent: "subtle",
menuColor: "default",
radius: "default",
},
opts.rtl ?? false
)
components = [initUrl, ...components]
}
// Validate template early.
@@ -209,7 +196,6 @@ export const init = new Command()
}
let initUrl: string
let baseColor: string
if ("_isUrl" in presetResult) {
const url = new URL(presetResult.url)
@@ -217,19 +203,12 @@ export const init = new Command()
url.searchParams.set("rtl", "true")
}
initUrl = url.toString()
baseColor = url.searchParams.get("baseColor") ?? "neutral"
} else {
initUrl = buildInitUrl(presetResult, opts.rtl ?? false)
baseColor = presetResult.baseColor
}
// Prepend the preset URL to the components list.
components = [initUrl, ...components]
// Set baseColor from preset if not explicitly provided.
if (!opts.baseColor) {
opts.baseColor = baseColor
}
}
// Prompt for preset when no preset, no components, and no defaults.
@@ -263,31 +242,31 @@ export const init = new Command()
title: t.title,
value,
}))
if (!opts.template) {
if (detectedTemplate) {
opts.template = detectedTemplate
const title =
templates[detectedTemplate as keyof typeof templates]?.title ??
detectedTemplate
logger.log(
`${green("✔")} ${bold("Select a template")} ${gray(
""
)} ${title} ${gray("(detected)")}`
)
} else {
const { template } = await prompts({
type: "select",
name: "template",
message: "Select a template",
choices: templateChoices,
})
if (opts.template) {
// Template provided via -t flag, use it directly.
} else if (detectedTemplate) {
opts.template = detectedTemplate
const title =
templates[detectedTemplate as keyof typeof templates]?.title ??
detectedTemplate
logger.log(
`${green("✔")} ${bold("Select a template")} ${gray(
""
)} ${title} ${gray("(detected)")}`
)
} else {
const { template } = await prompts({
type: "select",
name: "template",
message: "Select a template",
choices: templateChoices,
})
if (!template) {
process.exit(0)
}
opts.template = template
if (!template) {
process.exit(0)
}
opts.template = template
}
// Build create URL with template param.
@@ -349,7 +328,6 @@ export const init = new Command()
opts.rtl ?? false
)
components = [initUrl, ...components]
opts.baseColor = "neutral"
}
const options = initOptionsSchema.parse({
@@ -357,7 +335,7 @@ export const init = new Command()
isNewProject: false,
components,
...opts,
installStyleIndex: opts.baseStyle,
installStyleIndex: true,
})
await loadEnvFiles(options.cwd)
@@ -434,27 +412,16 @@ export const init = new Command()
}
if (item?.type === "registry:style") {
// Set a default base color so we're not prompted.
// The style will extend or override it.
options.baseColor = "neutral"
// If the style extends none, we don't want to install the base style.
options.installStyleIndex =
item.extends === "none" ? false : options.installStyleIndex
}
}
// If --no-base-style, we don't want to prompt for a base color either.
if (!options.installStyleIndex) {
options.baseColor = "neutral"
}
await runInit(options)
logger.log(
`${highlighter.success(
"Success!"
)} Project initialization completed.\nYou may now add components.`
`Project initialization completed.\nYou may now add components.`
)
// We need when running with custom cwd.
@@ -494,155 +461,141 @@ export async function runInit(
projectInfo = await getProjectInfo(options.cwd)
}
if (newProjectTemplate === "next-monorepo") {
// Prompt for base color if not set.
if (!options.baseColor) {
const baseColors = await getRegistryBaseColors()
const { tailwindBaseColor } = await prompts({
type: "select",
name: "tailwindBaseColor",
message: "Which color would you like to use as the base color?",
choices: baseColors.map((color) => ({
title: color.label,
value: color.name,
})),
})
options.baseColor = tailwindBaseColor
}
const selectedTemplate = newProjectTemplate
? templates[newProjectTemplate as keyof typeof templates]
: undefined
let result
if (selectedTemplate?.init) {
const components = [
...(options.installStyleIndex ? ["index"] : []),
...(options.components ?? []),
]
return await initMonorepoProject({
result = await selectedTemplate.init({
projectPath: options.cwd,
components,
installStyleIndex: options.installStyleIndex,
baseColor: options.baseColor ?? "neutral",
registryBaseConfig: options.registryBaseConfig,
rtl: options.rtl ?? false,
silent: options.silent,
})
}
} else {
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
let config = projectConfig
? await promptForMinimalConfig(projectConfig, options)
: await promptForConfig(await getConfig(options.cwd))
let config = projectConfig
? await promptForMinimalConfig(projectConfig, options)
: await promptForConfig(await getConfig(options.cwd))
if (!options.yes) {
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: `Write configuration to ${highlighter.info(
"components.json"
)}. Proceed?`,
initial: true,
})
if (!options.yes) {
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: `Write configuration to ${highlighter.info(
"components.json"
)}. Proceed?`,
initial: true,
if (!proceed) {
process.exit(0)
}
}
// Prepare the list of components to be added.
const components = [
// "index" is the default shadcn style.
// Why index? Because when style is true, we read style from components.json and fetch that.
// i.e new-york from components.json then fetch /styles/new-york/index.
// TODO: Fix this so that we can extend any style i.e --style=new-york.
...(options.installStyleIndex ? ["index"] : []),
...(options.components ?? []),
]
// Ensure registries are configured for the components we're about to add.
const fullConfigForRegistry = await resolveConfigPaths(options.cwd, config)
const { config: configWithRegistries } = await ensureRegistriesInConfig(
components,
fullConfigForRegistry,
{
silent: true,
}
)
// Update config with any new registries found.
if (configWithRegistries.registries) {
config.registries = configWithRegistries.registries
}
const componentSpinner = spinner(`Writing components.json.`).start()
const targetPath = path.resolve(options.cwd, "components.json")
const backupPath = `${targetPath}${FILE_BACKUP_SUFFIX}`
// Merge and keep registries at the end.
const mergeConfig = (base: typeof config, override: object) => {
const { registries, ...merged } = deepmerge(base, override)
return { ...merged, registries } as typeof config
}
// Merge with backup config if it exists.
if (fsExtra.existsSync(backupPath)) {
const existingConfig = await fsExtra.readJson(backupPath)
if (options.force) {
// With --force, only preserve registries from existing config.
if (existingConfig.registries) {
config.registries = {
...existingConfig.registries,
...(config.registries || {}),
}
}
} else {
config = mergeConfig(existingConfig, config)
}
}
// Merge config from registry:base item.
if (options.registryBaseConfig) {
config = mergeConfig(config, options.registryBaseConfig)
}
// Ensure rtl is set from CLI option (takes priority over registryBaseConfig).
if (options.rtl !== undefined) {
config.rtl = options.rtl
}
// Make sure to filter out built-in registries.
// TODO: fix this in ensureRegistriesInConfig.
config.registries = Object.fromEntries(
Object.entries(config.registries || {}).filter(
([key]) => !Object.keys(BUILTIN_REGISTRIES).includes(key)
)
)
// Write components.json.
await fs.writeFile(
targetPath,
`${JSON.stringify(config, null, 2)}\n`,
"utf8"
)
componentSpinner.succeed()
// Add components.
const fullConfig = await resolveConfigPaths(options.cwd, config)
await addComponents(components, fullConfig, {
// Init will always overwrite files.
overwrite: true,
silent: options.silent,
isNewProject:
options.isNewProject || projectInfo?.framework.name === "next-app",
})
if (!proceed) {
process.exit(0)
}
result = fullConfig
}
// Prepare the list of components to be added.
const components = [
// "index" is the default shadcn style.
// Why index? Because when style is true, we read style from components.json and fetch that.
// i.e new-york from components.json then fetch /styles/new-york/index.
// TODO: Fix this so that we can extend any style i.e --style=new-york.
...(options.installStyleIndex ? ["index"] : []),
...(options.components ?? []),
]
// Ensure registries are configured for the components we're about to add.
const fullConfigForRegistry = await resolveConfigPaths(options.cwd, config)
const { config: configWithRegistries } = await ensureRegistriesInConfig(
components,
fullConfigForRegistry,
{
silent: true,
}
)
// Update config with any new registries found.
if (configWithRegistries.registries) {
config.registries = configWithRegistries.registries
// Run postInit for new projects.
if (selectedTemplate?.postInit) {
await selectedTemplate.postInit({ projectPath: options.cwd })
}
const componentSpinner = spinner(`Writing components.json.`).start()
const targetPath = path.resolve(options.cwd, "components.json")
const backupPath = `${targetPath}${FILE_BACKUP_SUFFIX}`
// Merge and keep registries at the end.
const mergeConfig = (base: typeof config, override: object) => {
const { registries, ...merged } = deepmerge(base, override)
return { ...merged, registries } as typeof config
}
// Merge with backup config if it exists.
if (fsExtra.existsSync(backupPath)) {
const existingConfig = await fsExtra.readJson(backupPath)
if (options.force) {
// With --force, only preserve registries from existing config.
if (existingConfig.registries) {
config.registries = {
...existingConfig.registries,
...(config.registries || {}),
}
}
} else {
config = mergeConfig(existingConfig, config)
}
}
// Merge config from registry:base item.
if (options.registryBaseConfig) {
config = mergeConfig(config, options.registryBaseConfig)
}
// Ensure rtl is set from CLI option (takes priority over registryBaseConfig).
if (options.rtl !== undefined) {
config.rtl = options.rtl
}
// Make sure to filter out built-in registries.
// TODO: fix this in ensureRegistriesInConfig.
config.registries = Object.fromEntries(
Object.entries(config.registries || {}).filter(
([key]) => !Object.keys(BUILTIN_REGISTRIES).includes(key)
)
)
// Write components.json.
await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
componentSpinner.succeed()
// Add components.
const fullConfig = await resolveConfigPaths(options.cwd, config)
await addComponents(components, fullConfig, {
// Init will always overwrite files.
overwrite: true,
silent: options.silent,
isNewProject:
options.isNewProject || projectInfo?.framework.name === "next-app",
})
// If a new project is using src dir, let's update the tailwind content config.
// TODO: Handle this per framework.
if (options.isNewProject && options.srcDir) {
await updateTailwindContent(
["./src/**/*.{js,ts,jsx,tsx,mdx}"],
fullConfig,
{
silent: options.silent,
}
)
}
return fullConfig
return result
}
async function promptForConfig(defaultConfig: Config | null = null) {
@@ -766,14 +719,13 @@ async function promptForMinimalConfig(
opts: z.infer<typeof initOptionsSchema>
) {
let style = defaultConfig.style
let baseColor = opts.baseColor
let baseColor = "neutral"
let cssVariables = defaultConfig.tailwind.cssVariables
let iconLibrary = defaultConfig.iconLibrary ?? "lucide"
if (!opts.defaults) {
const [styles, baseColors, tailwindVersion] = await Promise.all([
const [styles, tailwindVersion] = await Promise.all([
getRegistryStyles(),
getRegistryBaseColors(),
getProjectTailwindVersionFromConfig(defaultConfig),
])
@@ -790,21 +742,9 @@ async function promptForMinimalConfig(
})),
initial: 0,
},
{
type: opts.baseColor ? null : "select",
name: "tailwindBaseColor",
message: `Which color would you like to use as the ${highlighter.info(
"base color"
)}?`,
choices: baseColors.map((color) => ({
title: color.label,
value: color.name,
})),
},
])
style = options.style ?? style ?? "new-york"
baseColor = options.tailwindBaseColor ?? baseColor
}
// Always respect the explicit --css-variables / --no-css-variables flag.

View File

@@ -770,9 +770,9 @@ describe("resolveRegistryTree - dependency ordering", () => {
expect(hasCircularB).toBe(true)
// Should have logged a warning about circular dependency
expect(consoleSpy).toHaveBeenCalledWith(
"Circular dependency detected in registry items"
)
// expect(consoleSpy).toHaveBeenCalledWith(
// "Circular dependency detected in registry items"
// )
consoleSpy.mockRestore()
})

View File

@@ -720,7 +720,7 @@ function topologicalSortRegistryItems(
}
if (sorted.length !== items.length) {
console.warn("Circular dependency detected in registry items")
// console.warn("Circular dependency detected in registry items")
// Return all items even if there are circular dependencies
// Items not in sorted are part of circular dependencies
const sortedHashes = new Set(

View File

@@ -0,0 +1,46 @@
import type { RegistryItem } from "@/src/registry/schema"
import type { Config } from "@/src/utils/get-config"
import { execa } from "execa"
export interface TemplateOptions {
projectPath: string
packageManager: string
cwd: string
}
export interface TemplateInitOptions {
projectPath: string
components: string[]
registryBaseConfig?: Record<string, unknown>
rtl: boolean
silent: boolean
}
export function createTemplate(config: {
name: string
title: string
defaultProjectName: string
scaffold: (options: TemplateOptions) => Promise<void>
create: (options: TemplateOptions) => Promise<void>
init?: (options: TemplateInitOptions) => Promise<Config>
files?: RegistryItem["files"]
postInit?: (options: { projectPath: string }) => Promise<void>
}) {
return {
...config,
postInit: config.postInit ?? defaultPostInit,
}
}
async function defaultPostInit({ projectPath }: { projectPath: string }) {
try {
await execa("git", ["init"], { cwd: projectPath })
await execa("git", ["add", "-A"], { cwd: projectPath })
await execa("git", ["commit", "-m", "Initial commit"], {
cwd: projectPath,
})
} catch {}
}
export const GITHUB_TEMPLATE_URL =
"https://codeload.github.com/shadcn-ui/ui/tar.gz/main"

View File

@@ -4,11 +4,14 @@ import { start } from "./start"
import { vite } from "./vite"
export { createTemplate, GITHUB_TEMPLATE_URL } from "./create-template"
export type { TemplateOptions } from "./create-template"
export type {
TemplateInitOptions,
TemplateOptions,
} from "./create-template"
export const templates = {
next,
"next-monorepo": nextMonorepo,
vite,
start,
} as const
"next-monorepo": nextMonorepo,
}

View File

@@ -0,0 +1,213 @@
import os from "os"
import path from "path"
import { iconLibraries, type IconLibraryName } from "@/src/icons/libraries"
import { configWithDefaults } from "@/src/registry/config"
import { resolveRegistryTree } from "@/src/registry/resolver"
import { rawConfigSchema } from "@/src/schema"
import { addComponents } from "@/src/utils/add-components"
import { resolveConfigPaths } from "@/src/utils/get-config"
import { handleError } from "@/src/utils/handle-error"
import { ensureRegistriesInConfig } from "@/src/utils/registries"
import { spinner } from "@/src/utils/spinner"
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
import { updateFonts } from "@/src/utils/updaters/update-fonts"
import dedent from "dedent"
import deepmerge from "deepmerge"
import { execa } from "execa"
import fs from "fs-extra"
import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
export const nextMonorepo = createTemplate({
name: "next-monorepo",
title: "Next.js (Monorepo)",
defaultProjectName: "next-monorepo",
scaffold: async ({ projectPath, packageManager }) => {
const createSpinner = spinner(
`Creating a new Next.js monorepo. This may take a few minutes.`
).start()
try {
const localTemplateDir = process.env.SHADCN_TEMPLATE_DIR
if (localTemplateDir) {
// Use local template directory for development.
const localTemplatePath = path.resolve(
localTemplateDir,
"next-monorepo"
)
await fs.copy(localTemplatePath, projectPath, {
filter: (src) => !src.includes("node_modules"),
})
} else {
// Get the template from GitHub.
const templatePath = path.join(
os.tmpdir(),
`shadcn-template-${Date.now()}`
)
await fs.ensureDir(templatePath)
const response = await fetch(GITHUB_TEMPLATE_URL)
if (!response.ok) {
throw new Error(`Failed to download template: ${response.statusText}`)
}
// Write the tar file.
const tarPath = path.resolve(templatePath, "template.tar.gz")
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
await execa("tar", [
"-xzf",
tarPath,
"-C",
templatePath,
"--strip-components=2",
"ui-main/templates/next-monorepo",
])
const extractedPath = path.resolve(templatePath, "next-monorepo")
await fs.move(extractedPath, projectPath)
await fs.remove(templatePath)
}
// Run install. Disable frozen lockfile since the template's lockfile may not match.
await execa(packageManager, ["install"], {
cwd: projectPath,
env: {
...process.env,
CI: "",
},
})
// Write project name to the package.json.
const packageJsonPath = path.join(projectPath, "package.json")
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8")
const packageJson = JSON.parse(packageJsonContent)
packageJson.name = projectPath.split("/").pop()
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2)
)
}
createSpinner?.succeed("Creating a new Next.js monorepo.")
} catch (error) {
createSpinner?.fail(
"Something went wrong creating a new Next.js monorepo."
)
handleError(error)
}
},
create: async () => {
// Empty for now.
},
init: async (options) => {
const packagesUiPath = path.resolve(options.projectPath, "packages/ui")
const appsWebPath = path.resolve(options.projectPath, "apps/web")
// Update packages/ui/components.json.
const packagesUiConfigPath = path.resolve(packagesUiPath, "components.json")
let packagesUiConfig = await fs.readJson(packagesUiConfigPath)
if (options.registryBaseConfig) {
packagesUiConfig = deepmerge(packagesUiConfig, options.registryBaseConfig)
}
packagesUiConfig.tailwind.baseColor = "neutral"
if (options.rtl) {
packagesUiConfig.rtl = true
}
await fs.writeJson(packagesUiConfigPath, packagesUiConfig, {
spaces: 2,
})
// Update apps/web/components.json.
const appsWebConfigPath = path.resolve(appsWebPath, "components.json")
let appsWebConfig = await fs.readJson(appsWebConfigPath)
if (options.registryBaseConfig) {
appsWebConfig = deepmerge(appsWebConfig, options.registryBaseConfig)
}
appsWebConfig.tailwind.baseColor = "neutral"
if (options.rtl) {
appsWebConfig.rtl = true
}
await fs.writeJson(appsWebConfigPath, appsWebConfig, { spaces: 2 })
// Apply preset CSS/style to packages/ui directly.
// We use the packages/ui config (not apps/web) so addProjectComponents runs
// instead of addWorkspaceComponents. This keeps CSS/deps in packages/ui.
const resolvedPackagesUiConfig = await resolveConfigPaths(
packagesUiPath,
rawConfigSchema.parse(packagesUiConfig)
)
const { config: packagesUiWithRegistries } = await ensureRegistriesInConfig(
options.components,
resolvedPackagesUiConfig,
{ silent: true }
)
// Skip fonts here — we handle them explicitly below using the apps/web config.
await addComponents(options.components, packagesUiWithRegistries, {
overwrite: true,
silent: options.silent,
isNewProject: true,
skipFonts: true,
})
const resolvedAppsWebConfig = await resolveConfigPaths(
appsWebPath,
rawConfigSchema.parse(appsWebConfig)
)
// Handle fonts at the apps/web level.
// packages/ui has no next.config so massageTreeForFonts can't detect Next.js.
// We resolve the tree to get fonts, then apply them using the apps/web config
// which has next.config and layout.tsx.
const tree = await resolveRegistryTree(
options.components,
configWithDefaults(packagesUiWithRegistries)
)
if (tree?.fonts?.length) {
const [fontSans] = tree.fonts
// Add font CSS variable to packages/ui CSS (same as massageTreeForFonts for Next.js).
await updateCssVars(
{
theme: { [fontSans.font.variable]: `var(${fontSans.font.variable})` },
},
resolvedPackagesUiConfig,
{
silent: options.silent,
overwriteCssVars: false,
}
)
// Update layout.tsx in apps/web with the font import and className.
await updateFonts(tree.fonts, resolvedAppsWebConfig, {
silent: options.silent,
})
}
// Install icon library packages in both workspaces.
const iconLibrary = resolvedPackagesUiConfig.iconLibrary as IconLibraryName
if (iconLibrary && iconLibrary in iconLibraries) {
const iconPackages = [...iconLibraries[iconLibrary].packages]
await updateDependencies(iconPackages, [], resolvedPackagesUiConfig, {
silent: true,
})
await updateDependencies(iconPackages, [], resolvedAppsWebConfig, {
silent: true,
})
}
return resolvedAppsWebConfig
},
files: [
{
type: "registry:page",
path: "app/page.tsx",
target: "app/page.tsx",
content: dedent`import { ComponentExample } from "@/components/component-example";
export default function Page() {
return <ComponentExample />;
}
`,
},
],
})

View File

@@ -8,23 +8,20 @@ import fs from "fs-extra"
import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
export const nextMonorepo = createTemplate({
name: "next-monorepo",
title: "Next.js (Monorepo)",
defaultProjectName: "my-monorepo",
init: async ({ projectPath, packageManager }) => {
export const next = createTemplate({
name: "next",
title: "Next.js",
defaultProjectName: "next-app",
scaffold: async ({ projectPath, packageManager }) => {
const createSpinner = spinner(
`Creating a new Next.js monorepo. This may take a few minutes.`
`Creating a new Next.js project. This may take a few minutes.`
).start()
try {
const localTemplateDir = process.env.SHADCN_TEMPLATE_DIR
if (localTemplateDir) {
// Use local template directory for development.
const localTemplatePath = path.resolve(
localTemplateDir,
"monorepo-next"
)
const localTemplatePath = path.resolve(localTemplateDir, "next-app")
await fs.copy(localTemplatePath, projectPath, {
filter: (src) => !src.includes("node_modules"),
})
@@ -49,20 +46,24 @@ export const nextMonorepo = createTemplate({
"-C",
templatePath,
"--strip-components=2",
"ui-main/templates/monorepo-next",
"ui-main/templates/next-app",
])
const extractedPath = path.resolve(templatePath, "monorepo-next")
const extractedPath = path.resolve(templatePath, "next-app")
await fs.move(extractedPath, projectPath)
await fs.remove(templatePath)
}
// Run install. Disable frozen lockfile since the template's lockfile may not match.
// Remove pnpm-lock.yaml if using a different package manager.
if (packageManager !== "pnpm") {
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
if (fs.existsSync(lockFilePath)) {
await fs.remove(lockFilePath)
}
}
// Run install.
await execa(packageManager, ["install"], {
cwd: projectPath,
env: {
...process.env,
CI: "",
},
})
// Write project name to the package.json.
@@ -77,20 +78,9 @@ export const nextMonorepo = createTemplate({
)
}
// Try git init.
const cwd = process.cwd()
await execa("git", ["--version"], { cwd: projectPath })
await execa("git", ["init"], { cwd: projectPath })
await execa("git", ["add", "-A"], { cwd: projectPath })
await execa("git", ["commit", "-m", "Initial commit"], {
cwd: projectPath,
})
createSpinner?.succeed("Creating a new Next.js monorepo.")
createSpinner?.succeed("Creating a new Next.js project.")
} catch (error) {
createSpinner?.fail(
"Something went wrong creating a new Next.js monorepo."
)
createSpinner?.fail("Something went wrong creating a new Next.js project.")
handleError(error)
}
},

View File

@@ -11,8 +11,8 @@ import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
export const start = createTemplate({
name: "start",
title: "TanStack Start",
defaultProjectName: "my-app",
init: async ({ projectPath, packageManager }) => {
defaultProjectName: "start-app",
scaffold: async ({ projectPath, packageManager }) => {
const createSpinner = spinner(
`Creating a new TanStack Start project. This may take a few minutes.`
).start()
@@ -78,14 +78,6 @@ export const start = createTemplate({
)
}
// Try git init.
await execa("git", ["--version"], { cwd: projectPath })
await execa("git", ["init"], { cwd: projectPath })
await execa("git", ["add", "-A"], { cwd: projectPath })
await execa("git", ["commit", "-m", "Initial commit"], {
cwd: projectPath,
})
createSpinner?.succeed("Creating a new TanStack Start project.")
} catch (error) {
createSpinner?.fail(

View File

@@ -11,8 +11,8 @@ import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
export const vite = createTemplate({
name: "vite",
title: "Vite",
defaultProjectName: "my-app",
init: async ({ projectPath, packageManager }) => {
defaultProjectName: "vite-app",
scaffold: async ({ projectPath, packageManager }) => {
const createSpinner = spinner(
`Creating a new Vite project. This may take a few minutes.`
).start()
@@ -78,14 +78,6 @@ export const vite = createTemplate({
)
}
// Try git init.
await execa("git", ["--version"], { cwd: projectPath })
await execa("git", ["init"], { cwd: projectPath })
await execa("git", ["add", "-A"], { cwd: projectPath })
await execa("git", ["commit", "-m", "Initial commit"], {
cwd: projectPath,
})
createSpinner?.succeed("Creating a new Vite project.")
} catch (error) {
createSpinner?.fail("Something went wrong creating a new Vite project.")

View File

@@ -38,6 +38,7 @@ export async function addComponents(
overwrite?: boolean
silent?: boolean
isNewProject?: boolean
skipFonts?: boolean
registryHeaders?: Record<string, Record<string, string>>
path?: string
}
@@ -62,7 +63,10 @@ export async function addComponents(
})
}
return await addProjectComponents(components, config, options)
return await addProjectComponents(components, config, {
...options,
skipFonts: options.skipFonts,
})
}
async function addProjectComponents(
@@ -72,6 +76,7 @@ async function addProjectComponents(
overwrite?: boolean
silent?: boolean
isNewProject?: boolean
skipFonts?: boolean
path?: string
}
) {
@@ -100,7 +105,9 @@ async function addProjectComponents(
const tailwindVersion = await getProjectTailwindVersionFromConfig(config)
tree = await massageTreeForFonts(tree, config)
if (!options.skipFonts) {
tree = await massageTreeForFonts(tree, config)
}
await updateTailwindConfig(tree.tailwind?.config, config, {
silent: options.silent,
@@ -129,9 +136,11 @@ async function addProjectComponents(
silent: options.silent,
})
await updateFonts(tree.fonts, config, {
silent: options.silent,
})
if (!options.skipFonts) {
await updateFonts(tree.fonts, config, {
silent: options.silent,
})
}
await updateFiles(tree.files, config, {
overwrite: options.overwrite,

View File

@@ -1,4 +1,3 @@
import { fetchRegistry } from "@/src/registry/fetcher"
import { spinner } from "@/src/utils/spinner"
import { execa } from "execa"
import fs from "fs-extra"
@@ -19,7 +18,6 @@ import { createProject } from "./create-project"
vi.mock("fs-extra")
vi.mock("execa")
vi.mock("prompts")
vi.mock("@/src/registry/fetcher")
vi.mock("@/src/utils/get-package-manager", () => ({
getPackageManager: vi.fn().mockResolvedValue("npm"),
}))
@@ -46,7 +44,7 @@ describe("createProject", () => {
vi.mocked(fs.move).mockResolvedValue(undefined)
vi.mocked(fs.remove).mockResolvedValue(undefined)
// Mock execa to resolve immediately without actual execution
// Mock execa for template scaffold commands.
vi.mocked(execa).mockResolvedValue({
stdout: "",
stderr: "",
@@ -61,7 +59,7 @@ describe("createProject", () => {
killed: false,
} as any)
// Mock fetch for monorepo template
// Mock fetch for template download.
global.fetch = vi.fn().mockResolvedValue({
ok: true,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
@@ -70,9 +68,6 @@ describe("createProject", () => {
// Reset prompts mock
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
// Reset registry mock
vi.mocked(fetchRegistry).mockResolvedValue([])
// Mock spinner function
const mockSpinner = {
start: vi.fn().mockReturnThis(),
@@ -110,7 +105,6 @@ describe("createProject", () => {
const result = await createProject({
cwd: "/test",
force: false,
srcDir: false,
})
expect(result).toEqual({
@@ -118,12 +112,6 @@ describe("createProject", () => {
projectName: "my-app",
template: "next",
})
expect(execa).toHaveBeenCalledWith(
"npx",
expect.arrayContaining(["create-next-app@latest", "/test/my-app"]),
expect.any(Object)
)
})
it("should create a monorepo project when selected", async () => {
@@ -135,7 +123,6 @@ describe("createProject", () => {
const result = await createProject({
cwd: "/test",
force: false,
srcDir: false,
})
expect(result).toEqual({
@@ -145,13 +132,7 @@ describe("createProject", () => {
})
})
it("should handle remote components and force next template", async () => {
vi.mocked(fetchRegistry).mockResolvedValue([
{
meta: { nextVersion: "13.0.0" },
},
])
it("should force next template for remote components", async () => {
const result = await createProject({
cwd: "/test",
force: true,
@@ -195,20 +176,4 @@ describe("createProject", () => {
expect(mockExit).toHaveBeenCalledWith(1)
})
it("should include --no-react-compiler flag for Next.js (latest)", async () => {
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
await createProject({
cwd: "/test",
force: false,
srcDir: false,
})
expect(execa).toHaveBeenCalledWith(
"npx",
expect.arrayContaining(["--no-react-compiler"]),
expect.any(Object)
)
})
})

View File

@@ -1,12 +1,9 @@
import path from "path"
import { initOptionsSchema } from "@/src/commands/init"
import { fetchRegistry } from "@/src/registry/fetcher"
import { templates } from "@/src/templates/index"
import { getPackageManager } from "@/src/utils/get-package-manager"
import { handleError } from "@/src/utils/handle-error"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import { templates } from "@/src/utils/templates/index"
import { execa } from "execa"
import fs from "fs-extra"
import prompts from "prompts"
import { z } from "zod"
@@ -14,44 +11,23 @@ import { z } from "zod"
export async function createProject(
options: Pick<
z.infer<typeof initOptionsSchema>,
"cwd" | "name" | "force" | "srcDir" | "components" | "template"
"cwd" | "name" | "force" | "components" | "template"
>
) {
options = {
srcDir: false,
...options,
}
let template: keyof typeof templates =
options.template && options.template in templates
? (options.template as keyof typeof templates)
: "next"
let projectName: string =
options.name ?? templates[template].defaultProjectName
let nextVersion = "latest"
const isRemoteComponent =
options.components?.length === 1 &&
!!options.components[0].match(/\/chat\/b\//)
if (options.components && isRemoteComponent) {
try {
const [result] = await fetchRegistry(options.components)
const { meta } = z
.object({
meta: z.object({
nextVersion: z.string(),
}),
})
.parse(result)
nextVersion = meta.nextVersion
// Force template to next for remote components.
template = "next"
} catch (error) {
logger.break()
handleError(error)
}
// Force template to next for remote components.
if (isRemoteComponent) {
template = "next"
}
if (!options.force) {
@@ -116,12 +92,10 @@ export async function createProject(
process.exit(1)
}
await templates[template].init({
await templates[template].scaffold({
projectPath,
packageManager,
cwd: options.cwd,
srcDir: !!options.srcDir,
version: nextVersion,
})
return {

View File

@@ -1,116 +0,0 @@
import path from "path"
import { iconLibraries, type IconLibraryName } from "@/src/icons/libraries"
import { configWithDefaults } from "@/src/registry/config"
import { resolveRegistryTree } from "@/src/registry/resolver"
import { rawConfigSchema } from "@/src/schema"
import { addComponents } from "@/src/utils/add-components"
import { resolveConfigPaths } from "@/src/utils/get-config"
import { ensureRegistriesInConfig } from "@/src/utils/registries"
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
import { updateFonts } from "@/src/utils/updaters/update-fonts"
import deepmerge from "deepmerge"
import fsExtra from "fs-extra"
export async function initMonorepoProject(options: {
projectPath: string
components: string[]
installStyleIndex: boolean
baseColor: string
registryBaseConfig?: Record<string, unknown>
rtl: boolean
silent: boolean
}) {
const packagesUiPath = path.resolve(options.projectPath, "packages/ui")
const appsWebPath = path.resolve(options.projectPath, "apps/web")
// Update packages/ui/components.json.
const packagesUiConfigPath = path.resolve(packagesUiPath, "components.json")
let packagesUiConfig = await fsExtra.readJson(packagesUiConfigPath)
if (options.registryBaseConfig) {
packagesUiConfig = deepmerge(packagesUiConfig, options.registryBaseConfig)
}
packagesUiConfig.tailwind.baseColor = options.baseColor
if (options.rtl) {
packagesUiConfig.rtl = true
}
await fsExtra.writeJson(packagesUiConfigPath, packagesUiConfig, {
spaces: 2,
})
// Update apps/web/components.json.
const appsWebConfigPath = path.resolve(appsWebPath, "components.json")
let appsWebConfig = await fsExtra.readJson(appsWebConfigPath)
if (options.registryBaseConfig) {
appsWebConfig = deepmerge(appsWebConfig, options.registryBaseConfig)
}
appsWebConfig.tailwind.baseColor = options.baseColor
if (options.rtl) {
appsWebConfig.rtl = true
}
await fsExtra.writeJson(appsWebConfigPath, appsWebConfig, { spaces: 2 })
// Apply preset CSS/style to packages/ui directly.
// We use the packages/ui config (not apps/web) so addProjectComponents runs
// instead of addWorkspaceComponents. This keeps CSS/deps in packages/ui.
const resolvedPackagesUiConfig = await resolveConfigPaths(
packagesUiPath,
rawConfigSchema.parse(packagesUiConfig)
)
const { config: packagesUiWithRegistries } = await ensureRegistriesInConfig(
options.components,
resolvedPackagesUiConfig,
{ silent: true }
)
await addComponents(options.components, packagesUiWithRegistries, {
overwrite: true,
silent: options.silent,
isNewProject: true,
})
const resolvedAppsWebConfig = await resolveConfigPaths(
appsWebPath,
rawConfigSchema.parse(appsWebConfig)
)
// Handle fonts at the apps/web level.
// packages/ui has no next.config so massageTreeForFonts can't detect Next.js.
// We resolve the tree to get fonts, then apply them using the apps/web config
// which has next.config and layout.tsx.
const tree = await resolveRegistryTree(
options.components,
configWithDefaults(packagesUiWithRegistries)
)
if (tree?.fonts?.length) {
const [fontSans] = tree.fonts
// Add font CSS variable to packages/ui CSS (same as massageTreeForFonts for Next.js).
await updateCssVars(
{ theme: { [fontSans.font.variable]: `var(${fontSans.font.variable})` } },
resolvedPackagesUiConfig,
{
silent: options.silent,
overwriteCssVars: false,
}
)
// Update layout.tsx in apps/web with the font import and className.
await updateFonts(tree.fonts, resolvedAppsWebConfig, {
silent: options.silent,
})
}
// Install icon library packages in both workspaces.
const iconLibrary = resolvedPackagesUiConfig.iconLibrary as IconLibraryName
if (iconLibrary && iconLibrary in iconLibraries) {
const iconPackages = [...iconLibraries[iconLibrary].packages]
await updateDependencies(iconPackages, [], resolvedPackagesUiConfig, {
silent: true,
})
await updateDependencies(iconPackages, [], resolvedAppsWebConfig, {
silent: true,
})
}
return resolvedAppsWebConfig
}

View File

@@ -1,23 +0,0 @@
import type { RegistryItem } from "@/src/registry/schema"
export interface TemplateOptions {
projectPath: string
packageManager: string
cwd: string
srcDir: boolean
version: string
}
export function createTemplate(config: {
name: string
title: string
defaultProjectName: string
init: (options: TemplateOptions) => Promise<void>
create: (options: TemplateOptions) => Promise<void>
files?: RegistryItem["files"]
}) {
return config
}
export const GITHUB_TEMPLATE_URL =
"https://codeload.github.com/shadcn-ui/ui/tar.gz/main"

View File

@@ -1,75 +0,0 @@
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
import { spinner } from "@/src/utils/spinner"
import dedent from "dedent"
import { execa } from "execa"
import { createTemplate } from "./create-template"
export const next = createTemplate({
name: "next",
title: "Next.js",
defaultProjectName: "my-app",
init: async ({ projectPath, packageManager, cwd, srcDir, version }) => {
const createSpinner = spinner(
`Creating a new Next.js project. This may take a few minutes.`
).start()
// Note: pnpm fails here. Fallback to npx with --use-PACKAGE-MANAGER.
const args = [
"--tailwind",
"--eslint",
"--typescript",
"--app",
srcDir ? "--src-dir" : "--no-src-dir",
"--no-import-alias",
`--use-${packageManager}`,
]
if (
version.startsWith("15") ||
version.startsWith("latest") ||
version.startsWith("canary")
) {
args.push("--turbopack")
}
if (version.startsWith("latest") || version.startsWith("canary")) {
args.push("--no-react-compiler")
}
try {
await execa(
"npx",
[`create-next-app@${version}`, projectPath, "--silent", ...args],
{
cwd,
}
)
} catch (error) {
logger.break()
logger.error(
`Something went wrong creating a new Next.js project. Please try again.`
)
process.exit(1)
}
createSpinner?.succeed("Creating a new Next.js project.")
},
create: async () => {
// Empty for now.
},
files: [
{
type: "registry:page",
path: "app/page.tsx",
target: "app/page.tsx",
content: dedent`import { ComponentExample } from "@/components/component-example";
export default function Page() {
return <ComponentExample />;
}
`,
},
],
})

View File

@@ -1,6 +1,7 @@
import os from "os"
import path from "path"
import fs from "fs-extra"
import { describe, expect, it } from "vitest"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
createFixtureTestDirectory,
@@ -15,14 +16,14 @@ describe("shadcn init - next-app", () => {
await new Promise((resolve) => setTimeout(resolve, 2000))
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "--base-color=neutral"])
await npxShadcn(fixturePath, ["init", "--defaults"])
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
const componentsJson = await fs.readJson(componentsJsonPath)
expect(componentsJson).toMatchObject({
style: "new-york",
style: "base-nova",
rsc: true,
tsx: true,
tailwind: {
@@ -54,23 +55,11 @@ describe("shadcn init - next-app", () => {
expect(cssContent).toContain("--foreground")
})
it("should init with custom base color", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "--base-color=zinc"])
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
)
expect(componentsJson.style).toBe("new-york")
expect(componentsJson.tailwind.baseColor).toBe("zinc")
})
it("should init without CSS variables", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, [
"init",
"--defaults",
"--base-color=stone",
"--no-css-variables",
])
@@ -78,16 +67,11 @@ describe("shadcn init - next-app", () => {
path.join(fixturePath, "components.json")
)
expect(componentsJson.tailwind.cssVariables).toBe(false)
const cssPath = path.join(fixturePath, "app/globals.css")
const cssContent = await fs.readFile(cssPath, "utf-8")
expect(cssContent).not.toContain("--background")
expect(cssContent).not.toContain("--foreground")
})
it("should init with components", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--base-color=neutral", "button"])
await npxShadcn(fixturePath, ["init", "--defaults", "button"])
expect(
await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx"))
@@ -98,13 +82,13 @@ describe("shadcn init - next-app", () => {
describe("shadcn init - vite-app", () => {
it("should init with custom alias and src", async () => {
const fixturePath = await createFixtureTestDirectory("vite-app")
await npxShadcn(fixturePath, ["init", "--base-color=gray", "alert-dialog"])
await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"])
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
)
expect(componentsJson.style).toBe("new-york")
expect(componentsJson.tailwind.baseColor).toBe("gray")
expect(componentsJson.style).toBe("base-nova")
expect(componentsJson.tailwind.baseColor).toBe("neutral")
expect(componentsJson.aliases).toMatchObject({
components: "#custom/components",
utils: "#custom/lib/utils",
@@ -359,88 +343,6 @@ describe("shadcn init - custom style", async () => {
).toBe(true)
})
it("should init with --no-base-style", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults", "--no-base-style"])
// We still expect components.json to be created.
// With some defaults.
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
)
expect(componentsJson.style).toBe("new-york")
expect(componentsJson.tailwind.baseColor).toBe("neutral")
// No utils should be installed.
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
false
)
// The css file should only have tailwind imports.
expect(
await fs.readFile(path.join(fixturePath, "app/globals.css"), "utf-8")
).toMatchInlineSnapshot(`
"@import "tailwindcss";
"
`)
})
it("should init with custom style and --no-base-style", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, [
"init",
"http://localhost:4445/r/style-extended.json",
"--no-base-style",
])
// We still expect components.json to be created.
// With some defaults.
const componentsJson = await fs.readJson(
path.join(fixturePath, "components.json")
)
expect(componentsJson.style).toBe("new-york")
expect(componentsJson.tailwind.baseColor).toBe("neutral")
// No utils should be installed.
expect(await fs.pathExists(path.join(fixturePath, "lib/utils.ts"))).toBe(
false
)
// But we should have the foo.ts from the custom style.
expect(
await fs.readFile(path.join(fixturePath, "lib/foo.ts"), "utf-8")
).toBe("const foo = 'baz-qux'")
expect(
await fs.readFile(path.join(fixturePath, "app/globals.css"), "utf-8")
).toMatchInlineSnapshot(`
"@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: Geist Sans, sans-serif;
--font-mono: Geist Mono, monospace;
--color-custom-brand: var(--custom-brand);
--color-secondary: var(--secondary);
--foo-var: var(--foo-var);
--color-primary: var(--primary);
}
:root {
--primary: #059669;
--foo-var: 3rem;
--secondary: #06b6d4;
}
.dark {
--custom-brand: #fef3c7;
--foo-var: 2rem;
}
"
`)
})
it("should init with custom style extended none", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, [
@@ -513,18 +415,100 @@ describe("shadcn init - template flag", () => {
"-t",
"next",
"--defaults",
"--base-color=neutral",
])
const componentsJsonPath = path.join(fixturePath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
const componentsJson = await fs.readJson(componentsJsonPath)
expect(componentsJson.style).toBe("new-york")
expect(componentsJson.style).toBe("base-nova")
expect(componentsJson.tailwind.baseColor).toBe("neutral")
})
})
describe("shadcn init - --name flag", () => {
// Use os.tmpdir() to create projects outside the monorepo tree.
// This prevents pnpm from detecting the monorepo workspace root.
let testBaseDir: string
beforeAll(async () => {
testBaseDir = path.join(os.tmpdir(), `shadcn-name-test-${process.pid}`)
await fs.ensureDir(testBaseDir)
})
afterAll(async () => {
await fs.remove(testBaseDir)
})
it("should create a new project with the specified name", async () => {
const projectName = "my-named-app"
const emptyDir = path.join(testBaseDir, "empty-next")
await fs.ensureDir(emptyDir)
await npxShadcn(
emptyDir,
["init", "--defaults", "--name", projectName],
{ timeout: 120000 }
)
const projectPath = path.join(emptyDir, projectName)
// Verify project was created with the correct name.
expect(await fs.pathExists(projectPath)).toBe(true)
expect(
await fs.pathExists(path.join(projectPath, "package.json"))
).toBe(true)
// Verify components.json was created.
const componentsJsonPath = path.join(projectPath, "components.json")
expect(await fs.pathExists(componentsJsonPath)).toBe(true)
// Verify theme-provider is included from the template.
expect(
await fs.pathExists(
path.join(projectPath, "components/theme-provider.tsx")
)
).toBe(true)
})
it("should create a new project with --name and -t vite", async () => {
const projectName = "my-vite-app"
const emptyDir = path.join(testBaseDir, "empty-vite")
await fs.ensureDir(emptyDir)
await npxShadcn(
emptyDir,
["init", "--defaults", "--name", projectName, "-t", "vite"],
{ timeout: 120000 }
)
const projectPath = path.join(emptyDir, projectName)
// Verify project was created.
expect(await fs.pathExists(projectPath)).toBe(true)
// Verify it's a vite project with the correct name.
const packageJson = await fs.readJson(
path.join(projectPath, "package.json")
)
expect(packageJson.name).toBe(projectName)
expect(packageJson.dependencies).toHaveProperty("react")
})
})
describe("shadcn init - deprecated --src-dir", () => {
it("should reject --src-dir as unknown option", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
const result = await npxShadcn(fixturePath, [
"init",
"--defaults",
"--src-dir",
])
expect(result.exitCode).toBe(1)
})
})
describe("shadcn init - existing components.json", () => {
// TODO: Revisit --force behavior. Currently it only skips backup merge,
// but doesn't reset config values like style. Need to decide intended behavior.
@@ -532,7 +516,7 @@ describe("shadcn init - existing components.json", () => {
const fixturePath = await createFixtureTestDirectory("next-app")
// Run init with default configuration.
await npxShadcn(fixturePath, ["init", "--defaults", "--base-color=neutral"])
await npxShadcn(fixturePath, ["init", "--defaults"])
// Override style in components.json.
const componentsJsonPath = path.join(fixturePath, "components.json")
@@ -540,17 +524,16 @@ describe("shadcn init - existing components.json", () => {
config.style = "custom-style"
await fs.writeJson(componentsJsonPath, config)
// Reinit with --force and different base color.
// Reinit with --force.
await npxShadcn(fixturePath, [
"init",
"--force",
"--defaults",
"--base-color=zinc",
])
const newConfig = await fs.readJson(componentsJsonPath)
expect(newConfig.style).toBe("new-york")
expect(newConfig.tailwind.baseColor).toBe("zinc")
expect(newConfig.tailwind.baseColor).toBe("neutral")
expect(await fs.pathExists(componentsJsonPath + ".bak")).toBe(false)
})
@@ -575,7 +558,7 @@ describe("shadcn init - existing components.json", () => {
const componentsJsonPath = path.join(fixturePath, "components.json")
await fs.writeJson(componentsJsonPath, existingConfig)
// Run init with an invalid component - this should fail and restore
// Run init with an invalid component - this should fail and restore.
await npxShadcn(fixturePath, [
"init",
"invalid-component-that-does-not-exist",

View File

@@ -1,3 +0,0 @@
{
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
}

View File

@@ -1,9 +0,0 @@
export default function Page() {
return (
<div className="flex items-center justify-center min-h-svh">
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Hello World</h1>
</div>
</div>
)
}

View File

@@ -1,18 +0,0 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
{children}
</NextThemesProvider>
)
}

36
templates/next-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# env files
.env*.local
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,11 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "app/globals.css",
"tailwindFunctions": ["cn"]
}

View File

@@ -0,0 +1,21 @@
# Next.js template
This is a Next.js template with shadcn/ui.
## Adding components
To add components to your app, run the following command:
```bash
npx shadcn@latest add button
```
This will place the ui components in the `components` directory.
## Using components
To use the components in your app, import them as follows:
```tsx
import { Button } from "@/components/ui/button";
```

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,32 @@
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
const fontSans = Geist({
subsets: ["latin"],
variable: "--font-sans",
})
const fontMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
})
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html
lang="en"
suppressHydrationWarning
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="flex min-h-svh items-center justify-center p-4">
<p>Hello World</p>
</div>
)
}

View File

@@ -0,0 +1,71 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
<ThemeHotkey />
{children}
</NextThemesProvider>
)
}
function isTypingTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
}
return (
target.isContentEditable ||
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT"
)
}
function ThemeHotkey() {
const { resolvedTheme, setTheme } = useTheme()
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || event.repeat) {
return
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return
}
if (event.key.toLowerCase() !== "d") {
return
}
if (isTypingTarget(event.target)) {
return
}
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
window.addEventListener("keydown", onKeyDown)
return () => {
window.removeEventListener("keydown", onKeyDown)
}
}, [resolvedTheme, setTheme])
return null
}
export { ThemeProvider }

View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default nextConfig

View File

@@ -0,0 +1,34 @@
{
"name": "next-app",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.1.6",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.2",
"eslint-config-next": "16.1.6",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"postcss": "^8",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
}
export default config

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,11 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "packages/ui/src/styles/globals.css",
"tailwindFunctions": ["cn", "cva"]
}

View File

@@ -1,12 +1,6 @@
# shadcn/ui monorepo template
This template is for creating a monorepo with shadcn/ui.
## Usage
```bash
pnpm dlx shadcn@latest init
```
This is a Next.js monorepo template with shadcn/ui.
## Adding components
@@ -18,14 +12,10 @@ pnpm dlx shadcn@latest add button -c apps/web
This will place the ui components in the `packages/ui/src/components` directory.
## Tailwind
Your `tailwind.config.ts` and `globals.css` are already set up to use the components from the `ui` package.
## Using components
To use the components in your app, import them from the `ui` package.
```tsx
import { Button } from "@workspace/ui/components/button"
import { Button } from "@workspace/ui/components/button";
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,7 +1,7 @@
import { Geist, Geist_Mono } from "next/font/google"
import "@workspace/ui/globals.css"
import { Providers } from "@/components/providers"
import { ThemeProvider } from "@/components/theme-provider"
const fontSans = Geist({
subsets: ["latin"],
@@ -19,11 +19,13 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased `}
>
<Providers>{children}</Providers>
<html
lang="en"
suppressHydrationWarning
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="flex min-h-svh items-center justify-center p-4">
<p>Hello World</p>
</div>
)
}

View File

@@ -0,0 +1,71 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
<ThemeHotkey />
{children}
</NextThemesProvider>
)
}
function isTypingTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
}
return (
target.isContentEditable ||
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT"
)
}
function ThemeHotkey() {
const { resolvedTheme, setTheme } = useTheme()
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || event.repeat) {
return
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return
}
if (event.key.toLowerCase() !== "d") {
return
}
if (isTypingTarget(event.target)) {
return
}
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
window.addEventListener("keydown", onKeyDown)
return () => {
window.removeEventListener("keydown", onKeyDown)
}
}, [resolvedTheme, setTheme])
return null
}
export { ThemeProvider }

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts"
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -7,8 +7,8 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"lint": "eslint",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {

View File

@@ -1,18 +1,20 @@
{
"name": "shadcn-ui-monorepo",
"name": "next-monorepo",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
"format": "turbo format",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"prettier": "^3.8.1",
"turbo": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"turbo": "^2.8.8",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.4.1",

View File

@@ -4,7 +4,9 @@
"type": "module",
"private": true,
"scripts": {
"lint": "eslint . --max-warnings 0"
"lint": "eslint",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next-themes": "^0.4.6",

View File

@@ -10,8 +10,11 @@
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
"format": {
"dependsOn": ["^format"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
},
"dev": {
"cache": false,

View File

@@ -13,4 +13,4 @@
"nitro",
"start"
]
}
}

View File

@@ -10,4 +10,5 @@ count.txt
.wrangler
.output
.vinxi
__unconfig*
todos.json

View File

@@ -0,0 +1,11 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "src/styles.css",
"tailwindFunctions": ["cn", "cva"]
}

View File

@@ -1,3 +1,21 @@
# TanStack Start + shadcn/ui
This is a template for a new TanStack Start project with React, TypeScript, and shadcn/ui.
## Adding components
To add components to your app, run the following command:
```bash
npx shadcn@latest add button
```
This will place the ui components in the `components` directory.
## Using components
To use the components in your app, import them as follows:
```tsx
import { Button } from "@/components/ui/button";
```

View File

@@ -1,5 +1,5 @@
// @ts-check
import { tanstackConfig } from '@tanstack/eslint-config'
import { tanstackConfig } from "@tanstack/eslint-config"
export default [...tanstackConfig]

View File

@@ -8,11 +8,11 @@
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
"format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
@@ -36,6 +36,7 @@
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",

View File

@@ -1,10 +0,0 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: false,
singleQuote: true,
trailingComma: "all",
};
export default config;

View File

@@ -0,0 +1,7 @@
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import type {ClassValue} from "clsx";
export function cn(...inputs: Array<ClassValue>) {
return twMerge(clsx(inputs))
}

View File

@@ -8,43 +8,43 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as rootRouteImport } from "./routes/__root"
import { Route as IndexRouteImport } from "./routes/index"
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
id: "/",
path: "/",
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
"/": typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
"/": typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
"/": typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fullPaths: "/"
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
to: "/"
id: "__root__" | "/"
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}
declare module '@tanstack/react-router' {
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
"/": {
id: "/"
path: "/"
fullPath: "/"
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -58,9 +58,9 @@ export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
import type { getRouter } from "./router.tsx"
import type { createStart } from "@tanstack/react-start"
declare module "@tanstack/react-start" {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>

View File

@@ -1,15 +1,20 @@
import { createRouter } from '@tanstack/react-router'
import { createRouter as createTanStackRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
export const getRouter = () => {
const router = createRouter({
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
})
return router
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>
}
}

View File

@@ -1,31 +1,32 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
import { TanStackDevtools } from "@tanstack/react-devtools"
import appCss from '../styles.css?url'
import Header from "../components/Header"
import appCss from "../styles.css?url"
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
charSet: "utf-8",
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: 'TanStack Start Starter',
title: "TanStack Start Starter",
},
],
links: [
{
rel: 'stylesheet',
rel: "stylesheet",
href: appCss,
},
],
}),
shellComponent: RootDocument,
})
@@ -36,14 +37,15 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<HeadContent />
</head>
<body>
<Header />
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
position: "bottom-right",
}}
plugins={[
{
name: 'Tanstack Router',
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
]}

View File

@@ -1,11 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/")({ component: App });
export const Route = createFileRoute("/")({ component: App })
function App() {
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div className="font-medium">Hello World</div>
<div className="flex min-h-screen items-center justify-center">
<p>Hello World</p>
</div>
);
)
}

View File

@@ -1,6 +1,6 @@
{
"include": ["**/*.ts", "**/*.tsx", "eslint.config.js", "prettier.config.js", "vite.config.js"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
@@ -11,7 +11,7 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
@@ -21,6 +21,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@@ -1,10 +1,10 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import viteTsConfigPaths from 'vite-tsconfig-paths'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'
import { defineConfig } from "vite"
import { devtools } from "@tanstack/devtools-vite"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import viteReact from "@vitejs/plugin-react"
import viteTsConfigPaths from "vite-tsconfig-paths"
import tailwindcss from "@tailwindcss/vite"
import { nitro } from "nitro/vite"
const config = defineConfig({
plugins: [
@@ -12,7 +12,7 @@ const config = defineConfig({
nitro(),
// this is the plugin that enables path aliases
viteTsConfigPaths({
projects: ['./tsconfig.json'],
projects: ["./tsconfig.json"],
}),
tailwindcss(),
tanstackStart(),

View File

@@ -0,0 +1,11 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "src/index.css",
"tailwindFunctions": ["cn", "cva"]
}

View File

@@ -1,3 +1,21 @@
# React + TypeScript + Vite + shadcn/ui
This is a template for a new Vite project with React, TypeScript, and shadcn/ui.
## Adding components
To add components to your app, run the following command:
```bash
npx shadcn@latest add button
```
This will place the ui components in the `src/components` directory.
## Using components
To use the components in your app, import them as follows:
```tsx
import { Button } from "@/components/ui/button"
```

View File

@@ -1,12 +1,14 @@
{
"name": "vite-app",
"private": true,
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
@@ -25,6 +27,8 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"

View File

@@ -1,7 +1,7 @@
export function App() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="font-medium">Hello World</div>
<div className="flex min-h-screen items-center justify-center">
<p>Hello World</p>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More