#!/usr/bin/env python3 """用官方 shadcn CLI 同步组件到 src/components/ui, 并把 import 改写成包内相对路径。 用法: python3 scripts/sync-shadcn.py (style 固定 base-nova = Base UI) 升级流程: 跑本脚本 → git diff 审查 → npx tsc → 改版本号 → npm publish。 依赖: node + npm 可联网 (registry 用 $NPM_REGISTRY, 默认 npmmirror)。""" import json, os, re, shutil, subprocess, sys, tempfile STYLE = "base-nova" REG = os.environ.get("NPM_REGISTRY", "https://registry.npmmirror.com") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) UI = os.path.join(ROOT, "src/components/ui") HOOKS = os.path.join(ROOT, "src/hooks") COMPONENTS = """accordion alert alert-dialog aspect-ratio avatar badge breadcrumb button button-group calendar card carousel chart checkbox collapsible combobox command dialog drawer dropdown-menu empty field hover-card input input-group input-otp item kbd label native-select pagination popover progress radio-group scroll-area select separator sheet sidebar skeleton slider sonner spinner switch table tabs textarea toggle toggle-group tooltip""".split() def run(cmd, cwd): r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=280) return r.returncode, (r.stdout + r.stderr)[-1500:] def rewrite(c): c = c.replace('from "cn"', 'from "../../lib/utils"') c = c.replace('from "@/lib/utils"', 'from "../../lib/utils"') c = re.sub(r'from "@/components/ui/([\w-]+)"', r'from "./\1"', c) c = re.sub(r'from "@/hooks/([\w-]+)"', r'from "../../hooks/\1"', c) return c def main(): tmp = tempfile.mkdtemp(prefix="shsync-", dir=os.environ.get("TMPDIR")) open(f"{tmp}/package.json", "w").write(json.dumps({ "name": "scaf", "private": True, "type": "module", "dependencies": {"react": "^19.0.0", "react-dom": "^19.0.0"}, "devDependencies": {"tailwindcss": "^4.1.0", "typescript": "^5.9.0", "shadcn": "latest"}})) os.makedirs(f"{tmp}/src") open(f"{tmp}/src/index.css", "w").write('@import "tailwindcss";\n') open(f"{tmp}/tsconfig.json", "w").write(json.dumps({"compilerOptions": { "jsx": "react-jsx", "baseUrl": ".", "paths": {"@/*": ["./src/*"]}}})) open(f"{tmp}/components.json", "w").write(json.dumps({ "$schema": "https://ui.shadcn.com/schema.json", "style": STYLE, "rsc": False, "tsx": True, "tailwind": {"config": "", "css": "src/index.css", "baseColor": "neutral", "cssVariables": True, "prefix": ""}, "iconLibrary": "lucide", "aliases": {"components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks"}})) print("npm i:", run(["npm", "i", "--registry", REG, "--no-audit", "--no-fund"], tmp)[0]) code, out = run(["npx", "shadcn", "add", *COMPONENTS, "--yes", "--overwrite"], tmp) print("shadcn add:", code) src_ui = f"{tmp}/src/components/ui" if not os.path.isdir(src_ui): sys.exit("CLI 未产出组件, 中止 (未改动包内文件)") shutil.rmtree(UI, ignore_errors=True) os.makedirs(UI) os.makedirs(HOOKS, exist_ok=True) for f in os.listdir(src_ui): open(f"{UI}/{f}", "w").write(rewrite(open(f"{src_ui}/{f}").read())) if os.path.isdir(f"{tmp}/src/hooks"): for f in os.listdir(f"{tmp}/src/hooks"): open(f"{HOOKS}/{f}", "w").write(rewrite(open(f"{tmp}/src/hooks/{f}").read())) pkg = json.load(open(f"{tmp}/node_modules/shadcn/package.json")) json.dump({"shadcn": pkg["version"], "style": STYLE}, open(f"{ROOT}/src/UPSTREAM.json", "w"), indent=2) left = [] for f in os.listdir(UI): left += re.findall(r'from "(@/[^"]+)"', open(f"{UI}/{f}").read()) print("components:", len(os.listdir(UI)), "| shadcn", pkg["version"], "| unresolved @/ imports:", left) shutil.rmtree(tmp, ignore_errors=True) main()