mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-30 16:14:13 +00:00
feat: refactor to templates
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
import {
|
||||
ComputerTerminal01Icon,
|
||||
Copy01Icon,
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
|
||||
const TEMPLATES = [
|
||||
{
|
||||
@@ -82,31 +82,31 @@ export function ProjectForm() {
|
||||
if (!params.new) {
|
||||
return isLocalDev
|
||||
? {
|
||||
pnpm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
pnpm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
pnpm: `pnpm dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}
|
||||
return isLocalDev
|
||||
? {
|
||||
pnpm: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
pnpm: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `pnpm shadcn create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}, [
|
||||
params.new,
|
||||
params.base,
|
||||
@@ -168,17 +168,16 @@ export function ProjectForm() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dialog-ring min-w-0 overflow-hidden rounded-xl sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create Project
|
||||
</DialogTitle>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription className="text-balance">
|
||||
Configure your project to use shadcn/ui.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup className="**:data-[slot=field-label]:has-data-[state=checked]:border-blue-600 **:data-[slot=field-label]:has-data-[state=checked]:bg-blue-50/50 dark:**:data-[slot=field-label]:has-data-[state=checked]:bg-primary/10 dark:**:data-[slot=field-label]:has-data-[state=checked]:border-primary **:data-[slot=radio-group-item]:sr-only **:data-[slot=radio-group-item]:absolute **:data-[slot=field-label]:rounded-lg! **:data-[slot=field-description]:text-balance">
|
||||
|
||||
<FieldGroup className="dark:**:data-[slot=field-label]:has-data-[state=checked]:bg-primary/10 dark:**:data-[slot=field-label]:has-data-[state=checked]:border-primary **:data-[slot=field-description]:text-balance **:data-[slot=field-label]:rounded-lg! **:data-[slot=field-label]:has-data-[state=checked]:border-blue-600 **:data-[slot=field-label]:has-data-[state=checked]:bg-blue-50/50 **:data-[slot=radio-group-item]:sr-only **:data-[slot=radio-group-item]:absolute">
|
||||
<Field>
|
||||
<FieldLabel className="text-base">Are you creating a new project?</FieldLabel>
|
||||
<FieldLabel className="text-base">
|
||||
Are you creating a new project?
|
||||
</FieldLabel>
|
||||
<RadioGroup
|
||||
value={params.new ? "new" : "existing"}
|
||||
onValueChange={(value) => setParams({ new: value === "new" })}
|
||||
@@ -188,7 +187,9 @@ export function ProjectForm() {
|
||||
<Field orientation="horizontal" className="p-3!">
|
||||
<FieldContent className="gap-1">
|
||||
<FieldTitle>Yes</FieldTitle>
|
||||
<FieldDescription>I'm creating a new project.</FieldDescription>
|
||||
<FieldDescription>
|
||||
I'm creating a new project.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="new" id="project-new" />
|
||||
</Field>
|
||||
@@ -213,7 +214,7 @@ export function ProjectForm() {
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="template" className="text-base ">
|
||||
<FieldLabel htmlFor="template" className="text-base">
|
||||
{params.new
|
||||
? "Choose a starter template"
|
||||
: "What framework is your existing project using?"}
|
||||
@@ -233,13 +234,16 @@ export function ProjectForm() {
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((template) => {
|
||||
const isDisabled = !params.new && template.value === "next-monorepo"
|
||||
const isDisabled =
|
||||
!params.new && template.value === "next-monorepo"
|
||||
|
||||
return (
|
||||
<FieldLabel
|
||||
key={template.value}
|
||||
htmlFor={template.value}
|
||||
className={isDisabled ? "cursor-not-allowed opacity-50" : undefined}
|
||||
className={
|
||||
isDisabled ? "cursor-not-allowed opacity-50" : undefined
|
||||
}
|
||||
>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-4! text-center *:w-auto!">
|
||||
<RadioGroupItem
|
||||
@@ -263,20 +267,29 @@ export function ProjectForm() {
|
||||
})}
|
||||
</RadioGroup>
|
||||
<FieldDescription>
|
||||
See the <a href="/docs/installation" className="text-foreground underline" target="_blank" rel="noopener noreferrer">installation guides</a> for more templates and frameworks.
|
||||
See the{" "}
|
||||
<a
|
||||
href="/docs/installation"
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
installation guides
|
||||
</a>{" "}
|
||||
for more templates and frameworks.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field>
|
||||
<FieldLabel className="text-base">Do you want to enable RTL?</FieldLabel>
|
||||
<FieldLabel className="text-base">
|
||||
Do you want to enable RTL?
|
||||
</FieldLabel>
|
||||
<RadioGroup
|
||||
value={params.rtl ? "yes" : "no"}
|
||||
onValueChange={(value) => setParams({ rtl: value === "yes" })}
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
<FieldLabel
|
||||
htmlFor="rtl-no"
|
||||
>
|
||||
<FieldLabel htmlFor="rtl-no">
|
||||
<Field orientation="horizontal" className="p-3!">
|
||||
<FieldContent className="gap-1">
|
||||
<FieldTitle>No</FieldTitle>
|
||||
@@ -287,9 +300,7 @@ export function ProjectForm() {
|
||||
<RadioGroupItem value="no" id="rtl-no" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor="rtl-yes"
|
||||
>
|
||||
<FieldLabel htmlFor="rtl-yes">
|
||||
<Field orientation="horizontal" className="p-3!">
|
||||
<FieldContent className="gap-1">
|
||||
<FieldTitle>Yes</FieldTitle>
|
||||
@@ -302,19 +313,20 @@ export function ProjectForm() {
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
<FieldDescription className="text-balance">
|
||||
To learn more about RTL, see the <a
|
||||
To learn more about RTL, see the{" "}
|
||||
<a
|
||||
href={`/docs/rtl/${params.template === "next-monorepo" ? "next" : params.template}`}
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
RTL setup guide
|
||||
</a> for {selectedTemplate?.title}.
|
||||
</a>{" "}
|
||||
for {selectedTemplate?.title}.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
|
||||
</FieldGroup>
|
||||
<DialogFooter className="bg-muted/30 -mx-6 mt-2 -mb-6 flex flex-col gap-3 border-t p-6 sm:flex-col min-w-0">
|
||||
<DialogFooter className="bg-muted/30 -mx-6 mt-2 -mb-6 flex min-w-0 flex-col gap-3 border-t p-6 sm:flex-col">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
import { Preview } from "@/app/(create)/components/preview"
|
||||
import { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { ShareButton } from "@/app/(create)/components/share-button"
|
||||
import { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
|
||||
import { getItemsForBase } from "@/app/(create)/lib/api"
|
||||
|
||||
@@ -12,24 +12,15 @@ 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 dedent from "dedent"
|
||||
import open from "open"
|
||||
import prompts from "prompts"
|
||||
import validateProjectName from "validate-npm-package-name"
|
||||
|
||||
import { initOptionsSchema, runInit } from "./init"
|
||||
|
||||
const CREATE_TEMPLATES = {
|
||||
next: "Next.js",
|
||||
"next-monorepo": "Next.js (Monorepo)",
|
||||
vite: "Vite",
|
||||
start: "TanStack Start",
|
||||
} as const
|
||||
|
||||
type Template = keyof typeof CREATE_TEMPLATES
|
||||
|
||||
export const create = new Command()
|
||||
.name("create")
|
||||
.description("create a new project with shadcn/ui")
|
||||
@@ -131,8 +122,8 @@ export const create = new Command()
|
||||
message: `Which ${highlighter.info(
|
||||
"template"
|
||||
)} would you like to use?`,
|
||||
choices: Object.entries(CREATE_TEMPLATES).map(([key, value]) => ({
|
||||
title: value,
|
||||
choices: Object.entries(templates).map(([key, t]) => ({
|
||||
title: t.title,
|
||||
value: key,
|
||||
})),
|
||||
})
|
||||
@@ -224,7 +215,8 @@ export const create = new Command()
|
||||
overwrite: true,
|
||||
})
|
||||
|
||||
const templateFiles = getTemplateFiles(template as Template)
|
||||
const templateFiles =
|
||||
templates[template as keyof typeof templates]?.files ?? []
|
||||
if (templateFiles.length > 0) {
|
||||
await updateFiles(templateFiles, config, {
|
||||
overwrite: true,
|
||||
@@ -246,60 +238,3 @@ export const create = new Command()
|
||||
clearRegistryContext()
|
||||
}
|
||||
})
|
||||
|
||||
function getTemplateFiles(template: Template) {
|
||||
switch (template) {
|
||||
case "vite":
|
||||
return [
|
||||
{
|
||||
type: "registry:file" as const,
|
||||
path: "src/App.tsx",
|
||||
target: "src/App.tsx",
|
||||
content: dedent`import { ComponentExample } from "@/components/component-example";
|
||||
|
||||
export function App() {
|
||||
return <ComponentExample />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
`,
|
||||
},
|
||||
]
|
||||
case "next":
|
||||
case "next-monorepo":
|
||||
return [
|
||||
{
|
||||
type: "registry:page" as const,
|
||||
path: "app/page.tsx",
|
||||
target: "app/page.tsx",
|
||||
content: dedent`import { ComponentExample } from "@/components/component-example";
|
||||
|
||||
export default function Page() {
|
||||
return <ComponentExample />;
|
||||
}
|
||||
`,
|
||||
},
|
||||
]
|
||||
case "start":
|
||||
return [
|
||||
{
|
||||
type: "registry:file" as const,
|
||||
path: "src/routes/index.tsx",
|
||||
target: "src/routes/index.tsx",
|
||||
content: dedent`import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ComponentExample } from "@/components/component-example";
|
||||
|
||||
export const Route = createFileRoute("/")({ component: App });
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ComponentExample />
|
||||
);
|
||||
}
|
||||
`,
|
||||
},
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { BASE_COLORS, BUILTIN_REGISTRIES } from "@/src/registry/constants"
|
||||
import { clearRegistryContext } from "@/src/registry/context"
|
||||
import { rawConfigSchema } from "@/src/schema"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
||||
import { createProject } from "@/src/utils/create-project"
|
||||
import { loadEnvFiles } from "@/src/utils/env-loader"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
import {
|
||||
@@ -43,15 +43,16 @@ import { logger } from "@/src/utils/logger"
|
||||
import {
|
||||
buildInitUrl,
|
||||
getShadcnCreateUrl,
|
||||
getShadcnInitUrl,
|
||||
handlePresetOption,
|
||||
} 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"
|
||||
import { bold, gray, green } from "kleur/colors"
|
||||
import open from "open"
|
||||
import prompts from "prompts"
|
||||
import { z } from "zod"
|
||||
@@ -86,7 +87,7 @@ export const initOptionsSchema = z.object({
|
||||
.refine(
|
||||
(val) => {
|
||||
if (val) {
|
||||
return TEMPLATES[val as keyof typeof TEMPLATES]
|
||||
return val in templates
|
||||
}
|
||||
return true
|
||||
},
|
||||
@@ -162,12 +163,12 @@ export const init = new Command()
|
||||
}
|
||||
|
||||
// Validate template early.
|
||||
if (opts.template && !TEMPLATES[opts.template as keyof typeof TEMPLATES]) {
|
||||
if (opts.template && !(opts.template in templates)) {
|
||||
logger.break()
|
||||
logger.error(
|
||||
`Invalid template: ${highlighter.info(
|
||||
opts.template
|
||||
)}. Use ${Object.keys(TEMPLATES)
|
||||
)}. Use ${Object.keys(templates)
|
||||
.map((t) => highlighter.info(t))
|
||||
.join(", ")}.`
|
||||
)
|
||||
@@ -242,31 +243,9 @@ export const init = new Command()
|
||||
path.resolve(cwd, "package.json")
|
||||
)
|
||||
|
||||
if (!hasPackageJson) {
|
||||
// New project: prompt for template (skip if already set via -t flag).
|
||||
const { template } = await prompts({
|
||||
type: opts.template ? null : "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Next.js", value: "next" },
|
||||
{ title: "Next.js (Monorepo)", value: "next-monorepo" },
|
||||
{ title: "Vite", value: "vite" },
|
||||
{ title: "TanStack Start", value: "start" },
|
||||
],
|
||||
})
|
||||
|
||||
if (template) {
|
||||
opts.template = template
|
||||
}
|
||||
|
||||
if (!opts.template) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// For existing projects, detect framework for the create URL.
|
||||
if (hasPackageJson && !opts.template) {
|
||||
// Detect framework for existing projects.
|
||||
let detectedTemplate: string | undefined
|
||||
if (hasPackageJson) {
|
||||
const frameworkTemplateMap: Record<string, string> = {
|
||||
"next-app": "next",
|
||||
"next-pages": "next",
|
||||
@@ -275,7 +254,39 @@ export const init = new Command()
|
||||
}
|
||||
const projectInfo = await getProjectInfo(cwd)
|
||||
if (projectInfo) {
|
||||
opts.template = frameworkTemplateMap[projectInfo.framework.name]
|
||||
detectedTemplate = frameworkTemplateMap[projectInfo.framework.name]
|
||||
}
|
||||
}
|
||||
|
||||
// Use detected framework or prompt for template.
|
||||
const templateChoices = Object.entries(templates).map(([value, t]) => ({
|
||||
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 (!template) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
opts.template = template
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +300,7 @@ export const init = new Command()
|
||||
const { preset } = await prompts({
|
||||
type: "select",
|
||||
name: "preset",
|
||||
message: "Which preset would you like to use?",
|
||||
message: "Select a preset",
|
||||
choices: [
|
||||
{
|
||||
title: "Build your own",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type MockInstance,
|
||||
} from "vitest"
|
||||
|
||||
import { TEMPLATES, createProject } from "./create-project"
|
||||
import { createProject } from "./create-project"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs-extra")
|
||||
@@ -116,7 +116,7 @@ describe("createProject", () => {
|
||||
expect(result).toEqual({
|
||||
projectPath: "/test/my-app",
|
||||
projectName: "my-app",
|
||||
template: TEMPLATES.next,
|
||||
template: "next",
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenCalledWith(
|
||||
@@ -141,7 +141,7 @@ describe("createProject", () => {
|
||||
expect(result).toEqual({
|
||||
projectPath: "/test/my-monorepo",
|
||||
projectName: "my-monorepo",
|
||||
template: TEMPLATES["next-monorepo"],
|
||||
template: "next-monorepo",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -158,7 +158,7 @@ describe("createProject", () => {
|
||||
components: ["/chat/b/some-component"],
|
||||
})
|
||||
|
||||
expect(result.template).toBe(TEMPLATES.next)
|
||||
expect(result.template).toBe("next")
|
||||
})
|
||||
|
||||
it("should throw error if project path already exists", async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { initOptionsSchema } from "@/src/commands/init"
|
||||
import { fetchRegistry } from "@/src/registry/fetcher"
|
||||
@@ -6,22 +5,12 @@ 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 { spinner } from "@/src/utils/spinner"
|
||||
import { templates } from "@/src/utils/templates/index"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
import prompts from "prompts"
|
||||
import { z } from "zod"
|
||||
|
||||
const GITHUB_TEMPLATE_URL =
|
||||
"https://codeload.github.com/shadcn-ui/ui/tar.gz/main"
|
||||
|
||||
export const TEMPLATES = {
|
||||
next: "next",
|
||||
"next-monorepo": "next-monorepo",
|
||||
vite: "vite",
|
||||
start: "start",
|
||||
} as const
|
||||
|
||||
export async function createProject(
|
||||
options: Pick<
|
||||
z.infer<typeof initOptionsSchema>,
|
||||
@@ -33,17 +22,12 @@ export async function createProject(
|
||||
...options,
|
||||
}
|
||||
|
||||
let template: keyof typeof TEMPLATES =
|
||||
options.template && TEMPLATES[options.template as keyof typeof TEMPLATES]
|
||||
? (options.template as keyof typeof TEMPLATES)
|
||||
let template: keyof typeof templates =
|
||||
options.template && options.template in templates
|
||||
? (options.template as keyof typeof templates)
|
||||
: "next"
|
||||
let projectName: string =
|
||||
options.name ??
|
||||
(template === TEMPLATES.next ||
|
||||
template === TEMPLATES.vite ||
|
||||
template === TEMPLATES.start
|
||||
? "my-app"
|
||||
: "my-monorepo")
|
||||
options.name ?? templates[template].defaultProjectName
|
||||
let nextVersion = "latest"
|
||||
|
||||
const isRemoteComponent =
|
||||
@@ -63,7 +47,7 @@ export async function createProject(
|
||||
nextVersion = meta.nextVersion
|
||||
|
||||
// Force template to next for remote components.
|
||||
template = TEMPLATES.next
|
||||
template = "next"
|
||||
} catch (error) {
|
||||
logger.break()
|
||||
handleError(error)
|
||||
@@ -78,12 +62,10 @@ export async function createProject(
|
||||
message: `The path ${highlighter.info(
|
||||
options.cwd
|
||||
)} does not contain a package.json file.\n Would you like to start a new project?`,
|
||||
choices: [
|
||||
{ title: "Next.js", value: "next" },
|
||||
{ title: "Next.js (Monorepo)", value: "next-monorepo" },
|
||||
{ title: "Vite", value: "vite" },
|
||||
{ title: "TanStack Start", value: "start" },
|
||||
],
|
||||
choices: Object.entries(templates).map(([key, t]) => ({
|
||||
title: t.title,
|
||||
value: key,
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
@@ -134,32 +116,13 @@ export async function createProject(
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (template === TEMPLATES.next) {
|
||||
await createNextProject(projectPath, {
|
||||
version: nextVersion,
|
||||
cwd: options.cwd,
|
||||
packageManager,
|
||||
srcDir: !!options.srcDir,
|
||||
})
|
||||
}
|
||||
|
||||
if (template === TEMPLATES["next-monorepo"]) {
|
||||
await createMonorepoProject(projectPath, {
|
||||
packageManager,
|
||||
})
|
||||
}
|
||||
|
||||
if (template === TEMPLATES.vite) {
|
||||
await createViteProject(projectPath, {
|
||||
packageManager,
|
||||
})
|
||||
}
|
||||
|
||||
if (template === TEMPLATES.start) {
|
||||
await createStartProject(projectPath, {
|
||||
packageManager,
|
||||
})
|
||||
}
|
||||
await templates[template].init({
|
||||
projectPath,
|
||||
packageManager,
|
||||
cwd: options.cwd,
|
||||
srcDir: !!options.srcDir,
|
||||
version: nextVersion,
|
||||
})
|
||||
|
||||
return {
|
||||
projectPath,
|
||||
@@ -167,309 +130,3 @@ export async function createProject(
|
||||
template,
|
||||
}
|
||||
}
|
||||
|
||||
async function createNextProject(
|
||||
projectPath: string,
|
||||
options: {
|
||||
version: string
|
||||
cwd: string
|
||||
packageManager: string
|
||||
srcDir: boolean
|
||||
}
|
||||
) {
|
||||
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",
|
||||
options.srcDir ? "--src-dir" : "--no-src-dir",
|
||||
"--no-import-alias",
|
||||
`--use-${options.packageManager}`,
|
||||
]
|
||||
|
||||
if (
|
||||
options.version.startsWith("15") ||
|
||||
options.version.startsWith("latest") ||
|
||||
options.version.startsWith("canary")
|
||||
) {
|
||||
args.push("--turbopack")
|
||||
}
|
||||
|
||||
if (
|
||||
options.version.startsWith("latest") ||
|
||||
options.version.startsWith("canary")
|
||||
) {
|
||||
args.push("--no-react-compiler")
|
||||
}
|
||||
|
||||
try {
|
||||
await execa(
|
||||
"npx",
|
||||
[`create-next-app@${options.version}`, projectPath, "--silent", ...args],
|
||||
{
|
||||
cwd: options.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.")
|
||||
}
|
||||
|
||||
async function createMonorepoProject(
|
||||
projectPath: string,
|
||||
options: {
|
||||
packageManager: string
|
||||
}
|
||||
) {
|
||||
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, "monorepo-next")
|
||||
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/monorepo-next",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "monorepo-next")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Run install. Disable frozen lockfile since the template's lockfile may not match.
|
||||
await execa(options.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))
|
||||
}
|
||||
|
||||
// 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.")
|
||||
} catch (error) {
|
||||
createSpinner?.fail("Something went wrong creating a new Next.js monorepo.")
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function createViteProject(
|
||||
projectPath: string,
|
||||
options: {
|
||||
packageManager: string
|
||||
}
|
||||
) {
|
||||
const createSpinner = spinner(
|
||||
`Creating a new Vite 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, "vite-app")
|
||||
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/vite-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "vite-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Remove pnpm-lock.yaml if using a different package manager.
|
||||
if (options.packageManager !== "pnpm") {
|
||||
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
|
||||
if (fs.existsSync(lockFilePath)) {
|
||||
await fs.remove(lockFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Run install.
|
||||
await execa(options.packageManager, ["install"], {
|
||||
cwd: projectPath,
|
||||
})
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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.")
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function createStartProject(
|
||||
projectPath: string,
|
||||
options: {
|
||||
packageManager: string
|
||||
}
|
||||
) {
|
||||
const createSpinner = spinner(
|
||||
`Creating a new TanStack Start 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, "start-app")
|
||||
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/start-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "start-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Remove pnpm-lock.yaml if using a different package manager.
|
||||
if (options.packageManager !== "pnpm") {
|
||||
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
|
||||
if (fs.existsSync(lockFilePath)) {
|
||||
await fs.remove(lockFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Run install.
|
||||
await execa(options.packageManager, ["install"], {
|
||||
cwd: projectPath,
|
||||
})
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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(
|
||||
"Something went wrong creating a new TanStack Start project."
|
||||
)
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
23
packages/shadcn/src/utils/templates/create-template.ts
Normal file
23
packages/shadcn/src/utils/templates/create-template.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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"
|
||||
14
packages/shadcn/src/utils/templates/index.ts
Normal file
14
packages/shadcn/src/utils/templates/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { next } from "./next"
|
||||
import { nextMonorepo } from "./next-monorepo"
|
||||
import { start } from "./start"
|
||||
import { vite } from "./vite"
|
||||
|
||||
export { createTemplate, GITHUB_TEMPLATE_URL } from "./create-template"
|
||||
export type { TemplateOptions } from "./create-template"
|
||||
|
||||
export const templates = {
|
||||
next,
|
||||
"next-monorepo": nextMonorepo,
|
||||
vite,
|
||||
start,
|
||||
} as const
|
||||
113
packages/shadcn/src/utils/templates/next-monorepo.ts
Normal file
113
packages/shadcn/src/utils/templates/next-monorepo.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import dedent from "dedent"
|
||||
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: "my-monorepo",
|
||||
init: 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,
|
||||
"monorepo-next"
|
||||
)
|
||||
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/monorepo-next",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "monorepo-next")
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
// 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.")
|
||||
} catch (error) {
|
||||
createSpinner?.fail(
|
||||
"Something went wrong creating a new Next.js monorepo."
|
||||
)
|
||||
handleError(error)
|
||||
}
|
||||
},
|
||||
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 />;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
75
packages/shadcn/src/utils/templates/next.ts
Normal file
75
packages/shadcn/src/utils/templates/next.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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 />;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
118
packages/shadcn/src/utils/templates/start.ts
Normal file
118
packages/shadcn/src/utils/templates/start.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import dedent from "dedent"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
|
||||
import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
|
||||
|
||||
export const start = createTemplate({
|
||||
name: "start",
|
||||
title: "TanStack Start",
|
||||
defaultProjectName: "my-app",
|
||||
init: async ({ projectPath, packageManager }) => {
|
||||
const createSpinner = spinner(
|
||||
`Creating a new TanStack Start 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, "start-app")
|
||||
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/start-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "start-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
// 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(
|
||||
"Something went wrong creating a new TanStack Start project."
|
||||
)
|
||||
handleError(error)
|
||||
}
|
||||
},
|
||||
create: async () => {
|
||||
// Empty for now.
|
||||
},
|
||||
files: [
|
||||
{
|
||||
type: "registry:file",
|
||||
path: "src/routes/index.tsx",
|
||||
target: "src/routes/index.tsx",
|
||||
content: dedent`import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ComponentExample } from "@/components/component-example";
|
||||
|
||||
export const Route = createFileRoute("/")({ component: App });
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ComponentExample />
|
||||
);
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
113
packages/shadcn/src/utils/templates/vite.ts
Normal file
113
packages/shadcn/src/utils/templates/vite.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import dedent from "dedent"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
|
||||
import { GITHUB_TEMPLATE_URL, createTemplate } from "./create-template"
|
||||
|
||||
export const vite = createTemplate({
|
||||
name: "vite",
|
||||
title: "Vite",
|
||||
defaultProjectName: "my-app",
|
||||
init: async ({ projectPath, packageManager }) => {
|
||||
const createSpinner = spinner(
|
||||
`Creating a new Vite 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, "vite-app")
|
||||
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/vite-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "vite-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
// 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.")
|
||||
handleError(error)
|
||||
}
|
||||
},
|
||||
create: async () => {
|
||||
// Empty for now.
|
||||
},
|
||||
files: [
|
||||
{
|
||||
type: "registry:file",
|
||||
path: "src/App.tsx",
|
||||
target: "src/App.tsx",
|
||||
content: dedent`import { ComponentExample } from "@/components/component-example";
|
||||
|
||||
export function App() {
|
||||
return <ComponentExample />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user