-
Notifications
You must be signed in to change notification settings - Fork 299
feat: 为 @nutui/nutui-react-taro 补齐 AI-Coding 能力(CLI / MCP / Skill + 站点文档) #3500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| node_modules | ||
| .DS_Store | ||
| *.log |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "name": "@nutui/nutui-react-cli-core", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "description": "NutUI React CLI 的共享核心:命令 / MCP / 数据查询原语,由 H5 与 Taro 两个发布 CLI 包 bundle 复用(不单独发布)。", | ||
| "license": "MIT", | ||
| "author": "jdcfe", | ||
| "exports": { | ||
| ".": "./src/index.ts", | ||
| "./scripts/prepare-data.mjs": "./scripts/prepare-data.mjs" | ||
| }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "yargs": "^17.7.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.5.5", | ||
| "@types/yargs": "^17.0.33", | ||
| "typescript": "^5.6.2" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // 读仓库根 meta/components.json,为某个 CLI 叶子包产出自包含的 data/ 快照。 | ||
| // 平台差异通过参数注入:H5 取 docs.h5/enUS + demos.h5 + api;Taro 取 docs.taro + demos.taro + apiTaro。 | ||
| // 会先自动重生成 meta(generate:meta),无需手动同步。 | ||
| // 各叶子包的 scripts/prepare-data.mjs 只需 import 本函数并传入自己的参数。 | ||
| // DO NOT manual edit the output (data/). Run: pnpm run prepare-data | ||
| import fs from 'node:fs' | ||
| import path from 'node:path' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { execFileSync } from 'node:child_process' | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
| // core 包目录:packages/nutui-react-cli-core/scripts -> .. | ||
| const CORE_DIR = path.resolve(__dirname, '..') | ||
| // 仓库根:packages/nutui-react-cli-core -> ../.. | ||
| const REPO_ROOT = path.resolve(CORE_DIR, '..', '..') | ||
| const META_PATH = path.join(REPO_ROOT, 'meta/components.json') | ||
| const BUILD_META_SCRIPT = path.join(REPO_ROOT, 'scripts/build-meta.mjs') | ||
|
|
||
| // 先重生成 meta 再读,确保快照与仓库源码(config/properties/demos/...)同步, | ||
| // 免去手动 generate:meta 的漏同步风险。build-meta 纯读源码且幂等,可安全重复执行。 | ||
| function regenerateMeta() { | ||
| if (!fs.existsSync(BUILD_META_SCRIPT)) { | ||
| // 脱离 monorepo(如已发布包被重新 build),无源码可生成,沿用现有快照。 | ||
| console.log('ℹ️ 未找到 build-meta 脚本,跳过 meta 重生成(沿用现有快照)') | ||
| return | ||
| } | ||
| console.log('🔄 重新生成 meta/components.json(generate:meta)...') | ||
| execFileSync(process.execPath, [BUILD_META_SCRIPT], { stdio: 'inherit' }) | ||
| } | ||
|
|
||
| function readMeta() { | ||
| try { | ||
| regenerateMeta() | ||
| } catch (err) { | ||
| // meta 生成失败:有旧快照则告警后沿用,否则无从继续。 | ||
| if (fs.existsSync(META_PATH)) { | ||
| console.warn( | ||
| `⚠️ meta 重生成失败(${err.message}),沿用现有 ${path.relative(REPO_ROOT, META_PATH)}` | ||
| ) | ||
| } else { | ||
| console.error( | ||
| `❌ meta 重生成失败且无现有快照:${err.message}\n` + | ||
| ` 请在仓库根手动执行:npm run generate:meta` | ||
| ) | ||
| process.exit(1) | ||
| } | ||
| } | ||
| if (!fs.existsSync(META_PATH)) { | ||
| console.error( | ||
| `❌ 未找到 ${path.relative(REPO_ROOT, META_PATH)}。\n` + | ||
| ` 它是构建产物,请先在仓库根执行:npm run generate:meta` | ||
| ) | ||
| process.exit(1) | ||
| } | ||
| return JSON.parse(fs.readFileSync(META_PATH, 'utf-8')) | ||
| } | ||
|
Comment on lines
+21
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 共享文件
此外, 建议:
请确认这两个叶子包的 #!/bin/bash
# 检查是否存在跨包并行调用 prepare-data 的编排脚本
rg -n 'prepare-data' --type=json --type=yaml -g '!node_modules' -C2
rg -nP '"prepare-data"' package.json packages/*/package.json -C2
fd -g 'turbo.json' -x cat {}🤖 Prompt for AI Agents |
||
|
|
||
| // meta 里的路径始终是 posix 相对仓库根路径,用 posix 取 basename,再 join 到本地。 | ||
| function copyRepoFile(relPosixPath, destAbs) { | ||
| const srcAbs = path.join(REPO_ROOT, relPosixPath) | ||
| if (!fs.existsSync(srcAbs)) return false | ||
| fs.mkdirSync(path.dirname(destAbs), { recursive: true }) | ||
| fs.copyFileSync(srcAbs, destAbs) | ||
| return true | ||
| } | ||
|
|
||
| /** | ||
| * 生成某叶子包的 data/ 快照。 | ||
| * @param {object} opts | ||
| * @param {string} opts.pkgDir 叶子包根目录绝对路径(其下写入 data/) | ||
| * @param {Record<string,string>} opts.docKeys lang -> meta.docs 的 key。如 {zh:'h5', en:'enUS'} 或 {zh:'taro'} | ||
| * @param {'h5'|'taro'} opts.demoKey 取 meta.demos 的哪一端 | ||
| * @param {'api'|'apiTaro'} opts.apiField 取哪一端的 API 表,归一写入快照的 `api` 字段 | ||
| */ | ||
| export function prepareData({ pkgDir, docKeys, demoKey, apiField }) { | ||
| console.log( | ||
| `🚀 building data snapshot (${apiField === 'apiTaro' ? 'Taro' : 'React/H5'}) for ${path.relative(REPO_ROOT, pkgDir)} ...` | ||
| ) | ||
| const meta = readMeta() | ||
| const DATA_DIR = path.join(pkgDir, 'data') | ||
|
|
||
| // 全量重建,避免残留上一版删掉的组件。 | ||
| fs.rmSync(DATA_DIR, { recursive: true, force: true }) | ||
| fs.mkdirSync(DATA_DIR, { recursive: true }) | ||
|
|
||
| // 1. meta.json:把对应端的 API 归一到 `api`,删除 apiTaro,缩减 docs/demos 到本端, | ||
| // 使运行时代码只读 `api` / 只需 <lang>.md,无需感知平台。 | ||
| const langKeys = Object.keys(docKeys) | ||
| const slimComponents = {} | ||
| for (const [id, c] of Object.entries(meta.components)) { | ||
| const api = c[apiField] || { tables: [] } | ||
| // docs 归一:只保留本端支持的 lang,key 换成 lang 名(zh/en),值为原相对路径(仅用于存在性)。 | ||
| const docs = {} | ||
| for (const lang of langKeys) docs[lang] = c.docs?.[docKeys[lang]] ?? null | ||
| slimComponents[id] = { | ||
| ...c, | ||
| api, | ||
| docs, | ||
| demos: c.demos?.[demoKey] ?? [], | ||
| } | ||
| delete slimComponents[id].apiTaro | ||
| } | ||
| const slimMeta = { ...meta, components: slimComponents } | ||
| fs.writeFileSync( | ||
| path.join(DATA_DIR, 'meta.json'), | ||
| `${JSON.stringify(slimMeta, null, 2)}\n` | ||
| ) | ||
|
|
||
| let docCount = 0 | ||
| let demoCount = 0 | ||
| const missing = [] | ||
|
|
||
| for (const c of Object.values(meta.components)) { | ||
| // 2. docs:按 lang 复制为 data/docs/<id>/<lang>.md(zh.md / en.md)。 | ||
| for (const lang of langKeys) { | ||
| const rel = c.docs && c.docs[docKeys[lang]] | ||
| if (!rel) continue | ||
| const ok = copyRepoFile(rel, path.join(DATA_DIR, 'docs', c.id, `${lang}.md`)) | ||
| if (ok) docCount++ | ||
| else missing.push(rel) | ||
| } | ||
|
|
||
| // 3. demos:扁平放到 data/demos/<id>/<basename>.tsx(无 platform 子层)。 | ||
| const demoList = (c.demos && c.demos[demoKey]) || [] | ||
| for (const rel of demoList) { | ||
| const base = path.posix.basename(rel) | ||
| const ok = copyRepoFile(rel, path.join(DATA_DIR, 'demos', c.id, base)) | ||
| if (ok) demoCount++ | ||
| else missing.push(rel) | ||
| } | ||
| } | ||
|
|
||
| console.log(`✅ 写入 ${path.relative(pkgDir, DATA_DIR)}/`) | ||
| console.log(` meta.json + docs ${docCount} 份 + demos ${demoCount} 份`) | ||
| if (missing.length) { | ||
| console.log(`\n⚠️ ${missing.length} 个 meta 引用的文件在仓库中缺失(已跳过):`) | ||
| for (const m of missing.slice(0, 20)) console.log(` - ${m}`) | ||
| if (missing.length > 20) console.log(` … 其余 ${missing.length - 20} 条省略`) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // core CLI 入口:各叶子包构造 CliConfig 后调 runCli。命令 / 文案 / 语言选项均随 config 参数化。 | ||
| import yargs from 'yargs' | ||
| import { runList } from './commands/list.js' | ||
| import { runInfo } from './commands/info.js' | ||
| import { runDoc } from './commands/doc.js' | ||
| import { runDemo } from './commands/demo.js' | ||
| import { runToken } from './commands/token.js' | ||
| import { runMcp } from './commands/mcp.js' | ||
| import type { CliConfig } from './config.js' | ||
| import type { Lang, OutputFormat } from './types.js' | ||
|
|
||
| export type { CliConfig } from './config.js' | ||
|
|
||
| export function runCli(config: CliConfig, argv: string[]): void { | ||
| // 仅当该端支持多语言时才暴露 --lang(Taro 只有 zh,隐藏该选项)。 | ||
| const multiLang = config.langs.length > 1 | ||
|
|
||
| const cli = yargs(argv) | ||
| .scriptName(config.binName) | ||
| .usage('$0 <command> [options]') | ||
| .option('format', { | ||
| alias: 'f', | ||
| describe: '输出格式', | ||
| choices: ['text', 'json'] as const, | ||
| default: 'text' as OutputFormat, | ||
| global: true, | ||
| }) | ||
| .command( | ||
| ['list', 'ls'], | ||
| '列出全部组件(按分类)', | ||
| (y) => | ||
| y.option('category', { | ||
| alias: 'c', | ||
| type: 'string', | ||
| describe: '按分类 enName 筛选(如 base / feedback)', | ||
| }), | ||
| (a) => | ||
| runList({ | ||
| config, | ||
| category: a.category, | ||
| format: a.format as OutputFormat, | ||
| }) | ||
| ) | ||
| .command( | ||
| 'info <component>', | ||
| '查看组件 Props 表', | ||
| (y) => | ||
| y.positional('component', { | ||
| type: 'string', | ||
| describe: '组件名(大小写不敏感,如 Button)', | ||
| demandOption: true, | ||
| }), | ||
| (a) => | ||
| runInfo({ | ||
| config, | ||
| component: a.component as string, | ||
| format: a.format as OutputFormat, | ||
| }) | ||
| ) | ||
| .command( | ||
| 'doc <component>', | ||
| '查看组件完整文档', | ||
| (y) => { | ||
| let b = y.positional('component', { | ||
| type: 'string', | ||
| describe: '组件名(大小写不敏感)', | ||
| demandOption: true, | ||
| }) | ||
| if (multiLang) { | ||
| b = b.option('lang', { | ||
| alias: 'l', | ||
| choices: config.langs, | ||
| default: config.defaultLang, | ||
| describe: '文档语言', | ||
| }) as typeof b | ||
| } | ||
| return b | ||
| }, | ||
| (a) => | ||
| runDoc({ | ||
| config, | ||
| component: a.component as string, | ||
| lang: (a.lang as Lang) ?? config.defaultLang, | ||
| format: a.format as OutputFormat, | ||
| }) | ||
| ) | ||
| .command( | ||
| 'demo <component> [name]', | ||
| `列出或查看组件 ${config.demoLabel} 示例源码`, | ||
| (y) => | ||
| y | ||
| .positional('component', { | ||
| type: 'string', | ||
| describe: '组件名(大小写不敏感)', | ||
| demandOption: true, | ||
| }) | ||
| .positional('name', { | ||
| type: 'string', | ||
| describe: '示例名(如 demo1);省略则列出全部', | ||
| }), | ||
| (a) => | ||
| runDemo({ | ||
| config, | ||
| component: a.component as string, | ||
| name: a.name as string | undefined, | ||
| format: a.format as OutputFormat, | ||
| }) | ||
| ) | ||
| .command( | ||
| 'token [component]', | ||
| '查看 Design Token(省略组件名则列全局 token)', | ||
| (y) => | ||
| y.positional('component', { | ||
| type: 'string', | ||
| describe: '组件名(大小写不敏感);省略则列出全局 token', | ||
| }), | ||
| (a) => | ||
| runToken({ | ||
| config, | ||
| component: a.component as string | undefined, | ||
| format: a.format as OutputFormat, | ||
| }) | ||
| ) | ||
| .command( | ||
| 'mcp', | ||
| '启动本地 MCP 服务(stdio),供 Claude Code / Cursor / VS Code / Codex 等调用', | ||
| (y) => y, | ||
| () => runMcp({ config }) | ||
| ) | ||
| .demandCommand( | ||
| 1, | ||
| `请指定一个命令。运行 ${config.binName} --help 查看用法。` | ||
| ) | ||
| .strict() | ||
| .alias('h', 'help') | ||
| .alias('v', 'version') | ||
| .version(config.version) | ||
| .wrap(null) | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
| cli.parse() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,19 @@ | ||
| // 命令间共享:解析组件名,未命中则打印 did-you-mean 并退出。 | ||
| import { resolveComponent, suggestComponents } from '../data.js' | ||
| import type { CliConfig } from '../config.js' | ||
| import type { Component, Meta } from '../types.js' | ||
|
|
||
| export function resolveOrExit(meta: Meta, query: string): Component { | ||
| export function resolveOrExit( | ||
| config: CliConfig, | ||
| meta: Meta, | ||
| query: string | ||
| ): Component { | ||
| const comp = resolveComponent(meta, query) | ||
| if (comp) return comp | ||
| const suggestions = suggestComponents(meta, query) | ||
| let msg = `未找到组件「${query}」。` | ||
| if (suggestions.length) msg += ` 你是否想找:${suggestions.join(' / ')}?` | ||
| msg += `\n运行 nutui-react list 查看全部组件。` | ||
| msg += `\n运行 ${config.binName} list 查看全部组件。` | ||
| process.stderr.write(`${msg}\n`) | ||
| process.exit(1) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.