Skip to content

Commit 5755d6a

Browse files
authored
Merge pull request #9903 from shadcn-ui/shadcn/fix-template-scaffold
feat(shadcn): scaffold projects from github remote
2 parents 97ed7eb + e363e34 commit 5755d6a

6 files changed

Lines changed: 297 additions & 111 deletions

File tree

.changeset/sixty-rice-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": patch
3+
---
4+
5+
scaffold templates from github remote

apps/v4/scripts/build-registry.mts

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,6 @@ import { STYLES } from "@/registry/styles"
2222
// This is used by the v4 site.
2323
const WHITELISTED_STYLES = ["new-york-v4"]
2424

25-
// Template directories to archive during build.
26-
const TEMPLATE_NAMES = [
27-
"next-app",
28-
"vite-app",
29-
"react-router-app",
30-
"start-app",
31-
"astro-app",
32-
"next-monorepo",
33-
"vite-monorepo",
34-
"react-router-monorepo",
35-
"start-monorepo",
36-
"astro-monorepo",
37-
]
38-
3925
// Collect paths for batch prettier formatting at the end.
4026
const prettierPaths: string[] = []
4127

@@ -101,9 +87,6 @@ try {
10187
console.log("\n⚙️ Building public/r/config.json...")
10288
await buildConfig()
10389

104-
console.log("\n📦 Building public/r/templates...")
105-
await buildTemplates()
106-
10790
// Copy UI to examples before cleanup.
10891
console.log("\n📋 Copying UI to examples...")
10992
await copyUIToExamples()
@@ -746,71 +729,6 @@ async function batchPrettier(paths: string[]) {
746729
})
747730
}
748731

749-
async function buildTemplates() {
750-
const templatesDir = path.resolve(process.cwd(), "../../templates")
751-
const outputDir = path.join(process.cwd(), "public/r/templates")
752-
await fs.mkdir(outputDir, { recursive: true })
753-
754-
await Promise.all(
755-
TEMPLATE_NAMES.map(async (name) => {
756-
const templatePath = path.join(templatesDir, name)
757-
758-
// Verify the template directory exists.
759-
try {
760-
await fs.access(templatePath)
761-
} catch {
762-
console.log(` ⚠️ templates/${name} not found, skipping`)
763-
return
764-
}
765-
766-
const outputPath = path.join(outputDir, `${name}.tar.gz`)
767-
768-
await new Promise<void>((resolve, reject) => {
769-
const proc = spawn(
770-
"tar",
771-
[
772-
"-czf",
773-
outputPath,
774-
"--exclude",
775-
"node_modules",
776-
"--exclude",
777-
".git",
778-
"--exclude",
779-
"pnpm-lock.yaml",
780-
"--exclude",
781-
"._*",
782-
"-C",
783-
templatesDir,
784-
name,
785-
],
786-
{
787-
cwd: process.cwd(),
788-
stdio: "pipe",
789-
env: { ...process.env, COPYFILE_DISABLE: "1" },
790-
}
791-
)
792-
let stderr = ""
793-
proc.stderr?.on("data", (data) => (stderr += data))
794-
proc.on("close", (code) => {
795-
if (code !== 0) {
796-
reject(new Error(`tar exited with code ${code}: ${stderr}`))
797-
} else {
798-
resolve()
799-
}
800-
})
801-
proc.on("error", reject)
802-
})
803-
804-
// Zero out the gzip mtime header (bytes 4-7) for deterministic output.
805-
const buf = await fs.readFile(outputPath)
806-
buf[4] = buf[5] = buf[6] = buf[7] = 0
807-
await fs.writeFile(outputPath, buf)
808-
809-
console.log(` ✅ ${name}.tar.gz`)
810-
})
811-
)
812-
}
813-
814732
async function buildColors() {
815733
const colorsTargetPath = path.join(process.cwd(), "public/r/colors")
816734
await fs.mkdir(colorsTargetPath, { recursive: true })

packages/shadcn/src/templates/create-template.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import os from "os"
22
import path from "path"
3-
import { REGISTRY_URL } from "@/src/registry/constants"
43
import type { RegistryItem } from "@/src/registry/schema"
54
import type { Config } from "@/src/utils/get-config"
65
import { handleError } from "@/src/utils/handle-error"
76
import { spinner } from "@/src/utils/spinner"
87
import { execa } from "execa"
98
import fs from "fs-extra"
109

11-
export const TEMPLATE_BASE_URL = `${REGISTRY_URL}/templates`
10+
const GITHUB_REPO_URL =
11+
process.env.SHADCN_GITHUB_URL ?? "https://github.com/shadcn-ui/ui.git"
1212

1313
export interface TemplateOptions {
1414
projectPath: string
@@ -99,7 +99,7 @@ export function resolveTemplate(
9999
return resolved
100100
}
101101

102-
// Default scaffold that fetches a pre-built template archive.
102+
// Default scaffold that downloads a template from GitHub.
103103
function defaultScaffold({
104104
title,
105105
templateDir,
@@ -123,24 +123,33 @@ function defaultScaffold({
123123
filter: (src) => !src.includes("node_modules"),
124124
})
125125
} else {
126-
// Fetch the pre-built template archive.
126+
// Clone only the template directory from GitHub using sparse checkout.
127127
const templatePath = path.join(
128128
os.tmpdir(),
129129
`shadcn-template-${Date.now()}`
130130
)
131-
await fs.ensureDir(templatePath)
132-
const response = await fetch(
133-
`${TEMPLATE_BASE_URL}/${templateDir}.tar.gz`
131+
await execa("git", [
132+
"clone",
133+
"--depth",
134+
"1",
135+
"--filter=blob:none",
136+
"--sparse",
137+
GITHUB_REPO_URL,
138+
templatePath,
139+
])
140+
await execa("git", [
141+
"-C",
142+
templatePath,
143+
"sparse-checkout",
144+
"set",
145+
`templates/${templateDir}`,
146+
])
147+
148+
const extractedPath = path.resolve(
149+
templatePath,
150+
"templates",
151+
templateDir
134152
)
135-
if (!response.ok) {
136-
throw new Error(`Failed to download template: ${response.statusText}`)
137-
}
138-
139-
// Write and extract the tar file.
140-
const tarPath = path.resolve(templatePath, "template.tar.gz")
141-
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
142-
await execa("tar", ["-xzf", tarPath, "-C", templatePath])
143-
const extractedPath = path.resolve(templatePath, templateDir)
144153
await fs.move(extractedPath, projectPath)
145154
await fs.remove(templatePath)
146155
}

packages/shadcn/src/templates/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { reactRouter } from "./react-router"
55
import { start } from "./start"
66
import { vite } from "./vite"
77

8-
export {
9-
createTemplate,
10-
resolveTemplate,
11-
TEMPLATE_BASE_URL,
12-
} from "./create-template"
8+
export { createTemplate, resolveTemplate } from "./create-template"
139
export type { TemplateInitOptions, TemplateOptions } from "./create-template"
1410

1511
export const templates = {

packages/shadcn/src/utils/create-project.test.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe("createProject", () => {
4444
vi.mocked(fs.move).mockResolvedValue(undefined)
4545
vi.mocked(fs.remove).mockResolvedValue(undefined)
4646

47-
// Mock execa for template scaffold commands.
47+
// Mock execa for git clone and package manager install.
4848
vi.mocked(execa).mockResolvedValue({
4949
stdout: "",
5050
stderr: "",
@@ -59,12 +59,6 @@ describe("createProject", () => {
5959
killed: false,
6060
} as any)
6161

62-
// Mock fetch for template download.
63-
global.fetch = vi.fn().mockResolvedValue({
64-
ok: true,
65-
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
66-
} as any)
67-
6862
// Reset prompts mock
6963
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
7064

@@ -96,7 +90,6 @@ describe("createProject", () => {
9690
afterEach(() => {
9791
vi.resetAllMocks()
9892
mockExit?.mockRestore()
99-
delete (global as any).fetch
10093
})
10194

10295
it("should create a Next.js project with default options", async () => {

0 commit comments

Comments
 (0)