التطبيق · فصل ٥

ابني الـ Registry وقدّمه وجرّبه

shadcn build · public/r · loadRegistry · loadRegistryItem · content negotiation · universal items
🎯 الهدف: تطلّع الـ JSON بـ build أو تقدّمه dynamic، وتجرّب الـ registry بنفس الأوامر اللي المستخدم هيستخدمها، وتنشره.

الطريقة الأولى: JSON static بـ build

terminal
npx shadcn@latest build
  • بيقرا ./registry.json (أو مسار تديهوله كـ argument).
  • بيكتب ملف لكل item في ./public/r، زي public/r/money-input.json، ومعاهم public/r/registry.json.
  • جوه ملف الـ item: محتوى كل ملف بيتحط في content. وده الفرق بين الـ source والـ output.
  • لو فيه include: بيتحل، والفهرس الناتج flat.
  • مكان تاني: --output ./public/registry أو -o.
  • فولدر مختلف: --cwd أو -c.
public/r/├── registry.json        ← الفهرس، من غير content├── money-input.json     ← فيه content كامل└── checkout-card.json

بعدها أي static hosting بيقدّم الفولدر. في Next مثلًا npm run dev وبعدها الملفات على http://localhost:3000/r/money-input.json.

الطريقة التانية: route handlers من غير build

لو عايز الـ JSON يتبني وقت الطلب من الـ source، استخدم الـ loaders من shadcn/registry. ولازم shadcn يبقى runtime dependency:

terminal
npm install shadcn
app/r/registry.json/route.ts
import { loadRegistry } from "shadcn/registry"export async function GET() {  try {    return Response.json(await loadRegistry())  } catch (error) {    console.error(error)    return Response.json({ error: "الـ registry مش راضي يتحمل." }, { status: 500 })  }}
app/r/[name].json/route.ts
import { loadRegistryItem, RegistryItemNotFoundError } from "shadcn/registry"export async function GET(_req: Request, ctx: { params: Promise<{ name: string }> }) {  const { name } = await ctx.params  try {    return Response.json(await loadRegistryItem(name))  } catch (error) {    if (error instanceof RegistryItemNotFoundError) {      return Response.json({ error: `مفيش item اسمه "${name}".` }, { status: 404 })    }    console.error(error)    return Response.json({ error: "حصلت مشكلة." }, { status: 500 })  }}
  • loadRegistry: بيرجّع الفهرس من غير محتوى الملفات، زي registry.json الناتج من الـ build.
  • loadRegistryItem: بيرجّع item واحد، ومعاه محتوى كل ملف متقري من الـ disk.
  • الاتنين بيحلّوا include لوحدهم، فمش محتاج build خالص.

Content negotiation: URL واحد لـ 3 جماهير

الـ CLI بيدعم HTTP content negotiation. يعني نفس الـ URL يقدر يرجّع:

  • HTML للمتصفح: landing page أو docs.
  • JSON للـ shadcn CLI: item يتسطب.
  • Markdown للـ AI agents.

الـ CLI بيعرّف نفسه بالـ headers دي:

headers
User-Agent: shadcnAccept: application/vnd.shadcn.v1+json, application/json;q=0.9
next.config.ts
import type { NextConfig } from "next"const cliHeader = { type: "header" as const, key: "accept", value: "(.*)application/vnd\\.shadcn\\.v1\\+json(.*)" }const cliAgent = { type: "header" as const, key: "user-agent", value: "shadcn" }const nextConfig: NextConfig = {  async rewrites() {    return {      beforeFiles: [        { source: "/", has: [cliHeader], destination: "/r/index.json" },        { source: "/", has: [cliAgent], destination: "/r/index.json" },      ],    }  },  async headers() {    // عشان أي CDN ميخزنش نسخة الـ HTML ويديها للـ CLI    return [{ source: "/", headers: [{ key: "Vary", value: "Accept, User-Agent" }] }]  },}export default nextConfig
server.js (Express)
app.get("/", (req, res) => {  res.vary("Accept")  res.vary("User-Agent")  if (req.accepts("application/vnd.shadcn.v1+json")) return res.json(rootItem)  if (req.get("User-Agent") === "shadcn") return res.json(rootItem)  res.send(landingHtml)})
  • في Next الأحسن rewrite: القرار بيتاخد في الـ routing، فمش محتاج Proxy function.
  • الـ item اللي بيتقدم على الـ root هنا مفترض إنه /r/index.json، فغيّره حسب الـ output بتاعك.
  • النتيجة: المستخدم يكتب shadcn add https://ui.nile.dev، فالـ URL أقصر، ومن غير /r/، وأسهل يتفتكر.

جرّب زي المستخدم بالظبط

القاعدة: الفهرس لـ list وsearch، وملف الـ item لـ view وadd.

terminal (بالـ URL)
npx shadcn@latest list   http://localhost:3000/r/registry.jsonnpx shadcn@latest search http://localhost:3000/r/registry.json --query moneynpx shadcn@latest view   http://localhost:3000/r/money-input.jsonnpx shadcn@latest add    http://localhost:3000/r/money-input.json   # من مشروع تاني
terminal (بالـ namespace)
npx shadcn@latest registry add @nile=http://localhost:3000/r/{name}.jsonnpx shadcn@latest list   @nilenpx shadcn@latest search @nile --query moneynpx shadcn@latest view   @nile/money-inputnpx shadcn@latest add    @nile/money-input
  • registry add محتاج مشروع فيه components.json، وبيكتب الـ namespace جواه.
  • {name} لازم يتحل لملف item. يعني @nile/money-input يبقى .../r/money-input.json.
  • الفهرس بيتقدم لوحده على .../r/registry.json.

Universal items — من غير framework

من إصدار 2.9.0 تقدر تعمل item يتسطب في أي فولدر، حتى لو مفيش components.json ومفيش framework.

  • الشرط: كل ملف في الـ item يكون ليه target صريح.
  • الاستخدامات: Cursor rules أو ESLint config أو starter template فيه ملفات Vue.
agent-rules.json
{  "$schema": "https://ui.shadcn.com/schema/registry-item.json",  "name": "agent-rules",  "type": "registry:item",  "files": [    { "path": "rules/team.md", "type": "registry:file", "target": "~/AGENTS.md" },    { "path": "rules/python.mdc", "type": "registry:file", "target": "~/.cursor/rules/python.mdc" }  ]}

وأنا جربت item زي ده على فولدر فاضي: add كتب AGENTS.md على طول من غير ما يسأل عن components.json.

انشر

  • ارفع المشروع على URL public.
  • المستخدم يقدر يسطّب بالـ URL على طول، أو يضيف الـ namespace مرة واحدة:
terminal
npx shadcn@latest registry add @nile=https://nile.dev/r/{name}.json

أو بإيده في components.json:

components.json
{  "registries": {    "@nile": "https://nile.dev/r/{name}.json"  }}

ولو الـ registry open source، تقدر تسجّل الـ namespace في الـ Registry Directory، فالمستخدم يكتب @nile/... من غير ما يعرّف حاجة. وده في فصل ١٠.

🧪 Lab: registry من الصفر في 10 دقايق

terminal
mkdir nile-registry && cd nile-registryecho '{"name":"nile-registry","private":true}' > package.jsonmkdir -p components/ui rules

اكتب components/ui/money-input.tsx بأي component، وrules/team.md بأي كلام، وبعدين الفهرس:

registry.json
{  "$schema": "https://ui.shadcn.com/schema/registry.json",  "name": "nile",  "homepage": "https://nile.dev",  "items": [    {      "name": "money-input",      "type": "registry:ui",      "description": "Input للمبالغ بالجنيه.",      "files": [{ "path": "components/ui/money-input.tsx", "type": "registry:ui" }]    },    {      "name": "agent-rules",      "type": "registry:item",      "description": "قواعد الفريق للـ AI agents.",      "files": [{ "path": "rules/team.md", "type": "registry:file", "target": "~/AGENTS.md" }]    }  ]}
terminal
npx shadcn@latest registry validate          # ✔ Registry is valid.npx shadcn@latest build                      # public/r/*.jsonpython3 -m http.server 3999 --directory public &npx shadcn@latest list http://localhost:3999/r/registry.json# - http://localhost:3999/r/money-input.json (ui) — Input للمبالغ بالجنيه.# - http://localhost:3999/r/agent-rules.json (item) — قواعد الفريق للـ AI agents.mkdir ../empty && cd ../emptynpx shadcn@latest add http://localhost:3999/r/agent-rules.json --yescat AGENTS.md                                # اتكتب في فولدر فاضي
  • افتح public/r/money-input.json وشوف الـ content اللي اتضاف.
  • جرّب add للـ money-input في الفولدر الفاضي، وشوف الفرق: ده مش universal، فمحتاج مشروع فيه components.json.

خلّصت الفصل لما

🧠 اختبر نفسك
١) عايز view لـ item. تدي الـ CLI رابط registry.json؟
لأ. view وadd محتاجين رابط ملف الـ item نفسه أو @ns/name.
٢) ليه Vary: Accept, User-Agent مهم في content negotiation؟
عشان الـ cache ميرجّعش HTML للـ CLI أو JSON للمتصفح من نفس الـ URL.
٣) عندي route handlers بـ loadRegistry والـ source فيه include. محتاج build؟
لأ. الـ loaders بيحلّوا الـ include لوحدهم.
٤) إيه شرط الـ universal item؟
كل ملف فيه يكون ليه target صريح.