mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
46
packages/shadcn/src/templates/create-template.ts
Normal file
46
packages/shadcn/src/templates/create-template.ts
Normal 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"
|
||||
@@ -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,
|
||||
}
|
||||
213
packages/shadcn/src/templates/next-monorepo.ts
Normal file
213
packages/shadcn/src/templates/next-monorepo.ts
Normal 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 />;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
@@ -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(
|
||||
@@ -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.")
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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 />;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
36
templates/next-app/.gitignore
vendored
Normal 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
|
||||
11
templates/next-app/.prettierrc
Normal file
11
templates/next-app/.prettierrc
Normal 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"]
|
||||
}
|
||||
21
templates/next-app/README.md
Normal file
21
templates/next-app/README.md
Normal 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";
|
||||
```
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
1
templates/next-app/app/globals.css
Normal file
1
templates/next-app/app/globals.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
32
templates/next-app/app/layout.tsx
Normal file
32
templates/next-app/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
templates/next-app/app/page.tsx
Normal file
7
templates/next-app/app/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
71
templates/next-app/components/theme-provider.tsx
Normal file
71
templates/next-app/components/theme-provider.tsx
Normal 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 }
|
||||
18
templates/next-app/eslint.config.mjs
Normal file
18
templates/next-app/eslint.config.mjs
Normal 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;
|
||||
4
templates/next-app/next.config.mjs
Normal file
4
templates/next-app/next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
export default nextConfig
|
||||
34
templates/next-app/package.json
Normal file
34
templates/next-app/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
templates/next-app/postcss.config.mjs
Normal file
8
templates/next-app/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
34
templates/next-app/tsconfig.json
Normal file
34
templates/next-app/tsconfig.json
Normal 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"]
|
||||
}
|
||||
11
templates/next-monorepo/.prettierrc
Normal file
11
templates/next-monorepo/.prettierrc
Normal 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"]
|
||||
}
|
||||
@@ -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";
|
||||
```
|
||||
BIN
templates/next-monorepo/apps/web/app/favicon.ico
Normal file
BIN
templates/next-monorepo/apps/web/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -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>
|
||||
)
|
||||
7
templates/next-monorepo/apps/web/app/page.tsx
Normal file
7
templates/next-monorepo/apps/web/app/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
0
templates/next-monorepo/apps/web/lib/.gitkeep
Normal file
0
templates/next-monorepo/apps/web/lib/.gitkeep
Normal 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.
|
||||
@@ -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": {
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -10,8 +10,11 @@
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"check-types": {
|
||||
"dependsOn": ["^check-types"]
|
||||
"format": {
|
||||
"dependsOn": ["^format"]
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
@@ -13,4 +13,4 @@
|
||||
"nitro",
|
||||
"start"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
templates/start-app/.gitignore
vendored
1
templates/start-app/.gitignore
vendored
@@ -10,4 +10,5 @@ count.txt
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
__unconfig*
|
||||
todos.json
|
||||
|
||||
11
templates/start-app/.prettierrc
Normal file
11
templates/start-app/.prettierrc
Normal 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"]
|
||||
}
|
||||
@@ -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";
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
import { tanstackConfig } from '@tanstack/eslint-config'
|
||||
import { tanstackConfig } from "@tanstack/eslint-config"
|
||||
|
||||
export default [...tanstackConfig]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('prettier').Config} */
|
||||
const config = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
};
|
||||
|
||||
export default config;
|
||||
7
templates/start-app/src/lib/utils.ts
Normal file
7
templates/start-app/src/lib/utils.ts
Normal 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))
|
||||
}
|
||||
@@ -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>>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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/*"]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
11
templates/vite-app/.prettierrc
Normal file
11
templates/vite-app/.prettierrc
Normal 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"]
|
||||
}
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user