diff --git a/.gitignore b/.gitignore index 81c8412857..e38e6355de 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,8 @@ node_modules /dist /dist-demo /dist-site -# meta 派生产物:components.json 由 generate:meta 产出,各平台 llms 由 generate:llms 产出 -# (build:site / build:taro:site 前自动生成),整个目录均为构建产物,不落库 +# meta 派生产物:components.json 由 generate:meta 产出、semantic.json 由 generate:semantic 产出、 +# 各平台 llms 由 generate:llms 产出(build:site / build:taro:site 前自动生成),整个目录均为构建产物,不落库 /meta/ /release /libs @@ -66,4 +66,6 @@ pnpm-lock.*.yaml .claude/commands/nutui-v4-upgrade.md # Harmony CSS files -src/packages/**/*.harmony.css \ No newline at end of file +src/packages/**/*.harmony.css + +.playwright-cli/ \ No newline at end of file diff --git a/package.json b/package.json index 8cd5d6ef13..a1948714b1 100644 --- a/package.json +++ b/package.json @@ -75,9 +75,9 @@ "build:demo:jmapp": "npm run checked && VITE_APP_PROJECT_ID=jmapp vite build --config vite.config.demo.ts", "prebuild:taro:demo": "SKIPDD=1 pnpm run update:taro:entry", "build:taro:demo": "npm run checked:taro && npm run generate:file:taro:pages && pnpm --dir ./packages/nutui-taro-demo build:h5", - "prebuild:site": "npm run generate:meta && npm run generate:llms", + "prebuild:site": "npm run generate:meta && npm run generate:semantic && npm run generate:llms", "build:site": "vite build --config vite.config.site.mts", - "prebuild:taro:site": "npm run generate:meta && npm run generate:llms", + "prebuild:taro:site": "npm run generate:meta && npm run generate:semantic && npm run generate:llms", "build:taro:site": "vite build --config vite.config.site.taro.mts && node ./scripts/rename.js", "build:jdtaro:demo": "npm run checked:taro && npm run generate:file:taro:pages && JD=1 pnpm --dir ./packages/nutui-taro-demo build:h5", "build:taro:demo:jmapp": "npm run checked:taro && VITE_APP_PROJECT_ID=jmapp npm run generate:file:taro:pages && pnpm --dir ./packages/nutui-taro-demo build:jmapp h5", @@ -90,6 +90,7 @@ "generate:props_json": "PROPS_JSON=true node scripts/build-comments-to-dts.mjs", "generate:meta": "node scripts/build-meta.mjs", "generate:llms": "node scripts/build-llms.mjs", + "generate:semantic": "node scripts/build-semantic.mjs", "lint": "eslint ./src/packages/*/", "lint:fix": "eslint --fix ./src/packages", "prepare": "husky && npm run generate:file && npm run generate:file:taro && npm run generate:file:taro:pages", diff --git a/scripts/build-llms.mjs b/scripts/build-llms.mjs index 6004e8a452..d465c32858 100644 --- a/scripts/build-llms.mjs +++ b/scripts/build-llms.mjs @@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.resolve(__dirname, '..') const META_PATH = path.join(ROOT, 'meta/components.json') +const SEMANTIC_PATH = path.join(ROOT, 'meta/semantic.json') const OUTPUT_DIR = path.join(ROOT, 'meta') export const SITE_ORIGIN = 'https://nutui.jd.com' @@ -45,6 +46,16 @@ export function loadMeta() { const siteUrl = (platform) => `${SITE_ORIGIN}${PLATFORMS[platform].base}` +// 尝试读 semantic.json(需先 npm run generate:semantic)。未生成时返回 null, +// llms 产物据此跳过 semantic 入口/链接而非报错。直接读 JSON 避免与 build-semantic.mjs 循环依赖。 +function tryLoadSemantic() { + try { + return JSON.parse(fs.readFileSync(SEMANTIC_PATH, 'utf-8')) + } catch { + return null + } +} + // 仅纳入在该平台有中文文档的组件(= show:true 的展示组件),按 categories 的 nav 顺序输出。 function docComponents(meta, cnKey) { const list = [] @@ -62,6 +73,7 @@ export function generateLlmsTxt(meta, platform = 'h5') { const cnKey = conf.langDocKey.cn const url = siteUrl(platform) const groups = docComponents(meta, cnKey) + const semantic = tryLoadSemantic() const lines = [] lines.push(`# NutUI React - 京东风格轻量级移动端 React 组件库${conf.titleSuffix}`) lines.push('') @@ -79,6 +91,12 @@ export function generateLlmsTxt(meta, platform = 'h5') { if (conf.langDocKey.en) { lines.push(`- [Full Documentation (English, single file)](${url}/llms-full.txt)`) } + if (semantic) { + lines.push(`- [全部组件样式结构(中文,单文件)](${url}/llms-semantic-cn.txt)`) + if (conf.langDocKey.en) { + lines.push(`- [Component Style Structure (English, single file)](${url}/llms-semantic.txt)`) + } + } lines.push('') for (const { category, comps } of groups) { @@ -90,6 +108,24 @@ export function generateLlmsTxt(meta, platform = 'h5') { } lines.push('') } + + // per-component 语义文档分区(仅当 semantic.json 已生成)。列出每个有语义数据的组件的 semantic.md 链接。 + if (semantic) { + lines.push('## 样式结构 (Semantic)') + lines.push('') + lines.push( + '每个组件的 `semantic.md` 列出其渲染产物可用的 CSS class 与 CSS 变量,供自定义样式 / 主题覆盖时精确定位。' + ) + lines.push('') + for (const { comps } of groups) { + for (const c of comps) { + if (!semantic.components[c.id]) continue + const label = c.cName ? `${c.name} ${c.cName}` : c.name + lines.push(`- [${label} Semantic](${url}/components/${c.id}/semantic.md)`) + } + } + lines.push('') + } return `${lines.join('\n').trimEnd()}\n` } @@ -102,6 +138,9 @@ export function generateLlmsFull(meta, platform = 'h5', lang = 'cn') { ? '# NutUI React - Full Component Documentation' : `# NutUI React - 全部组件文档${conf.titleSuffix}` const summary = lang === 'en' ? SUMMARY_EN : SUMMARY + const url = siteUrl(platform) + const semantic = tryLoadSemantic() + const semanticHeading = lang === 'en' ? 'Semantic DOM' : '样式结构 (Semantic DOM)' const parts = [title, '', `> ${summary}`, ''] for (const cat of meta.categories) { for (const id of cat.components) { @@ -112,6 +151,10 @@ export function generateLlmsFull(meta, platform = 'h5', lang = 'cn') { if (!fs.existsSync(abs)) continue parts.push('---', '') parts.push(fs.readFileSync(abs, 'utf-8').trim()) + // 组件语义文档链接(不内联正文,与 llms-full 的文档主体分离,同 Ant Design 组织方式)。 + if (semantic && semantic.components[id]) { + parts.push('', `## ${semanticHeading}`, '', `${url}/components/${id}/semantic.md`) + } parts.push('') } } diff --git a/scripts/build-semantic.mjs b/scripts/build-semantic.mjs new file mode 100644 index 0000000000..ddfc27ff30 --- /dev/null +++ b/scripts/build-semantic.mjs @@ -0,0 +1,291 @@ +// 编译各组件 scss,提取真实 CSS class,按组件归属,输出 meta/semantic.json。 +// 面向 AI Coding 的「样式结构事实源」:告诉 LLM 每个组件有哪些可用 CSS class 与 tokens, +// 便于生成改样式 / 写主题覆盖的代码。md 呈现由 site 插件读本产物生成,避免二次编译。 +// class 只能靠编译得到:组件 scss 大量用 @each / &-#{$type} / @mixin,动态 class 源码里不存在。 +// DO NOT manual edit the output (meta/semantic.json). Run: npm run generate:semantic +import fs from 'fs' +import path from 'path' +import { fileURLToPath, pathToFileURL } from 'url' +import * as sass from 'sass' +import postcss from 'postcss' +import { loadMeta } from './build-llms.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(__dirname, '..') +const PACKAGES_DIR = path.join(ROOT, 'src/packages') +const STYLES_DIR = path.join(ROOT, 'src/styles') +const VARIABLES_PATH = path.join(STYLES_DIR, 'variables.scss') +const SEMANTIC_PATH = path.join(ROOT, 'meta/semantic.json') +const OUTPUT_DIR = path.join(ROOT, 'meta') + +const SCHEMA_VERSION = '1.0.0' + +// 组件 scss 靠外部注入变量/函数(scale-px 等),故先 @import variables 再 @import 组件。 +// 全库已验证 100% 可编译;静音无关 deprecation,避免污染日志。 +const SILENCE = [ + 'import', + 'global-builtin', + 'color-functions', + 'legacy-js-api', + 'mixed-decls', +] + +// 编译单个组件 scss,用 postcss 提取全部 .nut-* class 名(去 . 前缀)。失败返回 null。 +function extractClasses(scssPath) { + // Sass @import 字符串里反斜杠是转义字符,Windows 路径需转 POSIX 正斜杠(各平台通用)。 + const toPosix = (p) => p.replace(/\\/g, '/') + const entry = `@import '${toPosix(VARIABLES_PATH)}';\n@import '${toPosix(scssPath)}';` + let css + try { + ;({ css } = sass.compileString(entry, { + loadPaths: [STYLES_DIR, path.dirname(scssPath)], + silenceDeprecations: SILENCE, + logger: sass.Logger.silent, + })) + } catch (e) { + return { error: e.message.split('\n')[0] } + } + const classes = new Set() + postcss.parse(css).walkRules((rule) => { + const m = rule.selector.match(/\.nut-[a-zA-Z0-9_-]+/g) + if (m) m.forEach((c) => classes.add(c.slice(1))) + }) + return { classes } +} + +// class 前缀 → 组件 id 的手工别名。仅用于 class 名与目录 id 系统性不一致、 +// 且归一/文件归属都救不了的情况(如 Image 组件渲染出 nut-img-* 而目录 id 是 image, +// 且该 class 经 @import 出现在 avatar/imagepreview 等多个文件里,无法靠单一来源定位)。 +// 与 build-meta.mjs 的 COMPONENT_ALIAS 同类,保持最小化。 +const CLASS_PREFIX_ALIAS = { + 'nut-img': 'image', +} + +// class → 归一匹配组件 id。去 nut- 前缀、去连字符后,取「最长的、是其前缀的 valid id」。 +// 例:nut-cell-group-title → cellgrouptitle → 命中 cellgroup(长于 cell,取长者)。 +// 仅作辅助信号,用于父子 @import 去重(把共享 class 判给更具体的子组件)。 +// 对 class↔id 不一致的组件会失配(如 nut-countup-* 对应 id animatingnumbers),故归属主信号是「定义文件」。 +function makeResolver(validIdsByLen) { + return (className) => { + // 别名优先:命中前缀则直接归属(处理跨多文件的 class↔id 不一致)。 + for (const [prefix, id] of Object.entries(CLASS_PREFIX_ALIAS)) { + if (className === prefix || className.startsWith(`${prefix}-`)) return id + } + const body = className.replace(/^nut-/, '').replace(/-/g, '') + return validIdsByLen.find((id) => body === id || body.startsWith(id)) || null + } +} + +// 编译各组件 scss,按「定义文件 + 归一去重」把 class 归属到组件。返回 { semantic, stats }。 +export function generateSemantic(meta) { + const validIds = Object.keys(meta.components) + const validIds4Set = new Set(validIds) + const validIdsByLen = [...validIds].sort((a, b) => b.length - a.length) + const resolve = makeResolver(validIdsByLen) + + // 逐组件编译,记录每个 class 出现在哪些组件文件里(class → Set<定义它的组件id>)。 + // 父 scss 会因 @import 子 scss 而含子组件 class,故一个 class 可能有多个来源文件。 + const classSources = new Map() + const compileErrors = [] + let scssCount = 0 + for (const id of fs.readdirSync(PACKAGES_DIR)) { + const scssPath = path.join(PACKAGES_DIR, id, `${id}.scss`) + if (!fs.existsSync(scssPath)) continue + scssCount++ + const { classes, error } = extractClasses(scssPath) + if (error) { + compileErrors.push(`${id}: ${error}`) + continue + } + for (const c of classes) { + if (!classSources.has(c)) classSources.set(c, new Set()) + classSources.get(c).add(id) + } + } + + // 归属:优先归一(命中具体 id 时用它,实现父子 @import 去重); + // 归一失配时归给唯一定义文件(处理 class↔id 不一致,如 nut-countup-*→animatingnumbers); + // 归一失配且跨多文件、又无法定位唯一归属的,进 orphan(多为 nut-rtl 等全局 class,丢弃)。 + const classesByComp = {} + const orphan = new Set() + for (const [c, sources] of classSources) { + const byNorm = resolve(c) + let owner = null + if (byNorm && validIds4Set.has(byNorm)) { + owner = byNorm + } else if (sources.size === 1) { + owner = [...sources][0] + } else { + // 多来源且归一失配:取来源中作为 class 前缀最长者(最具体的定义方)。 + owner = + [...sources] + .filter((id) => c === `nut-${id}` || c.startsWith(`nut-${id}-`)) + .sort((a, b) => b.length - a.length)[0] || null + } + if (!owner) { + orphan.add(c) + continue + } + ;(classesByComp[owner] ||= []).push(c) + } + + // 组装产物:仅纳入至少有 class 或 tokens 的组件,class 去重排序,tokens 复用 meta。 + const components = {} + for (const id of validIds) { + const c = meta.components[id] + const classes = (classesByComp[id] || []).sort() + const tokens = c.tokens || [] + if (!classes.length && !tokens.length) continue + components[id] = { + id, + name: c.name, + cName: c.cName || null, + classes, + tokens, + } + } + + const semantic = { + schemaVersion: SCHEMA_VERSION, + libVersion: meta.libVersion, + componentCount: Object.keys(components).length, + components, + } + const stats = { + scssCount, + classTotal: classSources.size, + assigned: classSources.size - orphan.size, + orphan: [...orphan].sort(), + compileErrors, + emptyComps: validIds.filter( + (id) => !classesByComp[id]?.length && (meta.components[id].tokens || []).length + ), + } + return { semantic, stats } +} + +const SEMANTIC_I18N = { + h5: { + fullTitle: '# NutUI React - 全部组件样式结构', + fullIntro: + '本文件汇总所有组件渲染产物中可用的 CSS class 与 CSS 变量(design tokens),供自定义样式 / 主题覆盖时精确定位。', + mdIntro: [ + '> 本文件列出该组件渲染产物中可用的 CSS class 与 CSS 变量(design tokens),', + '> 供自定义样式 / 主题覆盖时精确定位。', + ], + suffix: '样式结构', + tokenHeading: 'CSS 变量 (Design Tokens)', + tokenTableHead: '| CSS 变量 | SCSS 变量 | 默认值 |', + noClass: '_该组件无独立样式 class。_', + noToken: '_该组件无独立 design token。_', + }, + enUS: { + fullTitle: '# NutUI React - Component Style Structure', + fullIntro: + 'Aggregated CSS classes and CSS variables (design tokens) available on each component’s rendered output, for precise custom styling / theme overrides.', + mdIntro: [ + '> CSS classes and CSS variables (design tokens) available on this component’s rendered output,', + '> for precise custom styling / theme overrides.', + ], + suffix: 'Style Structure', + tokenHeading: 'CSS Variables (Design Tokens)', + tokenTableHead: '| CSS Variable | SCSS Variable | Default |', + noClass: '_This component has no dedicated style class._', + noToken: '_This component has no dedicated design token._', + }, +} + +// 单组件语义主体:CSS Class 列表 + Design Tokens 表格。供单文件 md 与聚合文件复用。 +function semanticBody(c, lang) { + const t = SEMANTIC_I18N[lang] || SEMANTIC_I18N.h5 + const lines = ['## CSS Class', ''] + if (c.classes.length) { + for (const cls of c.classes) lines.push(`- \`${cls}\``) + } else { + lines.push(t.noClass) + } + lines.push('', `## ${t.tokenHeading}`, '') + if (c.tokens.length) { + lines.push(t.tokenTableHead, '| --- | --- | --- |') + for (const tk of c.tokens) { + lines.push(`| ${tk.cssVar} | ${tk.scssVar} | ${tk.default ?? ''} |`) + } + } else { + lines.push(t.noToken) + } + return lines +} + +// 单组件样式结构 md(人类可读 / URL 访问)。数据源为 semantic.json,无需再编译 scss。 +export function generateSemanticMd(semantic, id, lang = 'h5') { + const c = semantic.components[id] + if (!c) return null + const t = SEMANTIC_I18N[lang] || SEMANTIC_I18N.h5 + const title = c.cName ? `${c.name} ${c.cName}` : c.name + const lines = [`# ${title} - ${t.suffix}`, '', ...t.mdIntro, ''] + lines.push(...semanticBody(c, lang)) + return `${lines.join('\n').trimEnd()}\n` +} + +// 语义聚合单文件:按 categories 顺序拼接所有组件的 class+tokens。lang: 'h5'(中文) | 'enUS'(英文)。 +// 组件顺序与 llms-full 一致(沿用 meta.categories)。 +export function generateLlmsSemantic(semantic, meta, lang = 'h5') { + const t = SEMANTIC_I18N[lang] || SEMANTIC_I18N.h5 + const parts = [t.fullTitle, '', `> ${t.fullIntro}`, ''] + for (const cat of meta.categories) { + for (const id of cat.components) { + const c = semantic.components[id] + if (!c) continue + const title = c.cName ? `${c.name} ${c.cName}` : c.name + parts.push('---', '', `# ${title}`, '') + parts.push(...semanticBody(c, lang), '') + } + } + return `${parts.join('\n').trimEnd()}\n` +} + +// 读已生成的 semantic.json(供 site 插件消费,避免二次编译 scss)。 +export function loadSemantic() { + return JSON.parse(fs.readFileSync(SEMANTIC_PATH, 'utf-8')) +} + +function main() { + console.log('🚀 building meta/semantic.json ...') + const meta = loadMeta() + const { semantic, stats } = generateSemantic(meta) + + fs.mkdirSync(OUTPUT_DIR, { recursive: true }) + const files = { + 'semantic.json': `${JSON.stringify(semantic, null, 2)}\n`, + 'llms-semantic-cn.txt': generateLlmsSemantic(semantic, meta, 'h5'), + 'llms-semantic.txt': generateLlmsSemantic(semantic, meta, 'enUS'), + } + for (const [name, content] of Object.entries(files)) { + const out = path.join(OUTPUT_DIR, name) + fs.writeFileSync(out, content) + const kb = (Buffer.byteLength(content) / 1024).toFixed(0) + console.log(`✅ ${path.relative(ROOT, out)} (${kb}KB)`) + } + + console.log(` 编译 scss: ${stats.scssCount},产出组件: ${semantic.componentCount}`) + console.log(` class 总数: ${stats.classTotal},已归属: ${stats.assigned}`) + if (stats.emptyComps.length) { + console.log( + ` 仅 token 无 class 的组件 (${stats.emptyComps.length}): ${stats.emptyComps.join(', ')}` + ) + } + if (stats.orphan.length) { + console.log(`\n⚠️ ${stats.orphan.length} 个 class 未归属(预期为全局/跨组件 class,已丢弃):`) + console.log(` ${stats.orphan.join(', ')}`) + } + if (stats.compileErrors.length) { + console.log(`\n❌ ${stats.compileErrors.length} 个组件编译失败:`) + for (const e of stats.compileErrors) console.log(` - ${e}`) + } else { + console.log('\n✨ 全部 scss 编译成功') + } +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + main() +} diff --git a/scripts/site/vite-plugin-llms.mjs b/scripts/site/vite-plugin-llms.mjs index 4dac349b9c..5f88161b2b 100644 --- a/scripts/site/vite-plugin-llms.mjs +++ b/scripts/site/vite-plugin-llms.mjs @@ -8,6 +8,43 @@ import { collectLlmsFiles, PLATFORMS, } from '../build-llms.mjs' +import { + loadSemantic, + generateSemanticMd, + generateLlmsSemantic, +} from '../build-semantic.mjs' + +// 读 semantic.json(需先 npm run generate:semantic)。未生成时返回 null,跳过 semantic 产物而非报错。 +function tryLoadSemantic() { + try { + return loadSemantic() + } catch { + return null + } +} + +// 收集某平台要产出的全部虚拟文件:llms 文档(含单组件 md)+ 样式结构产物。 +// llms 部分复用 build-llms 的 collectLlmsFiles,semantic 部分在此叠加, +// 使 build(generateBundle)与 dev(中间件)两条路径产物完全一致。 +function collectFiles(platform) { + const meta = loadMeta() + const files = collectLlmsFiles(meta, platform) + // 每组件样式结构文档:components//semantic.md(对齐 Ant 的 /semantic.md 语义)。 + // 语义聚合单文件:llms-semantic-cn.txt / llms-semantic.txt(对齐 llms-full 的组织方式)。 + // semantic 数据与端无关,中文无条件产出;英文仅在该平台有英文文档时产出(同 llms-full 策略)。 + const semantic = tryLoadSemantic() + if (semantic) { + files['llms-semantic-cn.txt'] = generateLlmsSemantic(semantic, meta, 'h5') + if (PLATFORMS[platform].langDocKey.en) { + files['llms-semantic.txt'] = generateLlmsSemantic(semantic, meta, 'enUS') + } + for (const id of Object.keys(semantic.components)) { + const md = generateSemanticMd(semantic, id) + if (md) files[`components/${id}/semantic.md`] = md + } + } + return files +} const contentType = (p) => p.endsWith('.md') @@ -22,16 +59,17 @@ export default function llmsPlugin({ platform = 'h5' } = {}) { // build 阶段:把虚拟文件写入产物目录(相对 outDir,即 dist-site/)。 generateBundle() { - const files = collectLlmsFiles(loadMeta(), platform) + const files = collectFiles(platform) for (const [fileName, source] of Object.entries(files)) { this.emitFile({ type: 'asset', fileName, source }) } console.log( - `[nutui-llms:${platform}] emitted ${Object.keys(files).length} files (llms*.txt + components/*.md)` + `[nutui-llms:${platform}] emitted ${Object.keys(files).length} files (llms*.txt + components/*.md + semantic)` ) }, - // dev 阶段:中间件按 /llms.txt、/components/.md 实时返回。 + // dev 阶段:中间件按 /llms.txt、/components/.md、 + // /components//semantic.md 实时返回(后两者均命中 components/ + .md 判定)。 configureServer(server) { server.middlewares.use((req, res, next) => { const url = (req.url || '').split('?')[0] @@ -41,10 +79,12 @@ export default function llmsPlugin({ platform = 'h5' } = {}) { key === 'llms.txt' || key === 'llms-full-cn.txt' || key === 'llms-full.txt' || + key === 'llms-semantic-cn.txt' || + key === 'llms-semantic.txt' || (key.startsWith('components/') && key.endsWith('.md')) if (!isTarget) return next() - const files = collectLlmsFiles(loadMeta(), platform) + const files = collectFiles(platform) if (!(key in files)) return next() res.setHeader('Content-Type', contentType(key)) res.end(files[key]) diff --git a/src/sites/config/baseConfig.ts b/src/sites/config/baseConfig.ts index 18dd901c31..5738631669 100644 --- a/src/sites/config/baseConfig.ts +++ b/src/sites/config/baseConfig.ts @@ -41,6 +41,13 @@ export const SiteReact = { eName: 'Components', path: '/zh-CN/component/button', pathName: '/zh-CN/component/button' + }, + { + name: 'llms', + cName: 'AI', + eName: 'AI', + path: '/zh-CN/ai/llms', + pathName: 'llms' } ], language: 'react' @@ -88,6 +95,13 @@ export const SiteReactTaro = { eName: 'Components', path: '/zh-CN/component/button', pathName: '/zh-CN/component/button' + }, + { + name: 'llms', + cName: 'AI', + eName: 'AI', + path: '/zh-CN/ai/llms', + pathName: 'llms' } ], language: 'react' diff --git a/src/sites/sites-react/doc-taro/App.tsx b/src/sites/sites-react/doc-taro/App.tsx index a912e86eaf..34df55fe76 100644 --- a/src/sites/sites-react/doc-taro/App.tsx +++ b/src/sites/sites-react/doc-taro/App.tsx @@ -8,7 +8,7 @@ import Header from '../doc/components/header' import DemoPreview from '../doc/components/demo-preview' import Issue from '..//doc/components/issue' import { getComponentName } from '../../assets/util' -import { routes as routers, guideEnTaroRoutes, guideTaroRoutes } from './router' +import { routes as routers, guideEnTaroRoutes, guideTaroRoutes, aiTaroRoutes } from './router' import loadable from '@loadable/component' import CodeBlock from '../doc/components/demoblock/codeblock' import { BackTop } from '../../../packages/backtop/backtop' @@ -78,11 +78,12 @@ const Content = () => { setFixed(false) } } - const isGuide = location.pathname.includes('/guide') + const isGuide = + location.pathname.includes('/guide') || location.pathname.includes('/ai/') useEffect(() => { document.addEventListener('scroll', scrollTitle) }, []) - const routes = [...routers, ...guideTaroRoutes, ...guideEnTaroRoutes] + const routes = [...routers, ...guideTaroRoutes, ...guideEnTaroRoutes, ...aiTaroRoutes] return (
{!isGuide && ( diff --git a/src/sites/sites-react/doc-taro/router.ts b/src/sites/sites-react/doc-taro/router.ts index 4a4e7bfc7a..e861200f5c 100644 --- a/src/sites/sites-react/doc-taro/router.ts +++ b/src/sites/sites-react/doc-taro/router.ts @@ -43,4 +43,26 @@ for (const path in modulesEnTaroDocs) { name }); } -export {routes, guideRoutes, guideEnRoutes, guideTaroRoutes, guideEnTaroRoutes} + +const aiTaroRoutes: any[] = [] +const modulesAiTaroDocs = import.meta.glob('/src/sites/sites-react/doc/docs/ai-taro/*.md'); +for (const path in modulesAiTaroDocs) { + if (path.includes('.en-US.md')) continue; + let name = (/docs\/ai-taro\/(.*).md/.exec(path) as any[])[1]; + aiTaroRoutes.push({ + path: `/zh-CN/ai/${name}`, + component: modulesAiTaroDocs[path], + name + }); +} +const modulesAiEnTaroDocs = import.meta.glob('/src/sites/sites-react/doc/docs/ai-taro/*.en-US.md'); +for (const path in modulesAiEnTaroDocs) { + let name = (/docs\/ai-taro\/(.*).en-US.md/.exec(path) as any[])[1]; + aiTaroRoutes.push({ + path: `/en-US/ai/${name}`, + component: modulesAiEnTaroDocs[path], + name: `en-${name}` + }); +} + +export {routes, guideRoutes, guideEnRoutes, guideTaroRoutes, guideEnTaroRoutes, aiTaroRoutes} diff --git a/src/sites/sites-react/doc/App.tsx b/src/sites/sites-react/doc/App.tsx index 573f2ea55c..44e4e54717 100644 --- a/src/sites/sites-react/doc/App.tsx +++ b/src/sites/sites-react/doc/App.tsx @@ -14,7 +14,7 @@ import Header from '../doc/components/header' import DemoPreview from '../doc/components/demo-preview' import Issue from '..//doc/components/issue' import { getComponentName } from '../../assets/util' -import { routes as routers, guideEnRoutes, guideRoutes } from './router' +import { routes as routers, guideEnRoutes, guideRoutes, aiRoutes } from './router' import loadable from '@loadable/component' import CodeBlock from './components/demoblock/codeblock' import { BackTop } from '../../../packages/backtop/backtop' @@ -82,11 +82,12 @@ const Content = () => { setFixed(false) } } - const isGuide = location.pathname.includes('/guide') + const isGuide = + location.pathname.includes('/guide') || location.pathname.includes('/ai/') useEffect(() => { document.addEventListener('scroll', scrollTitle) }, []) - const routes = [...routers, ...guideRoutes, ...guideEnRoutes] + const routes = [...routers, ...guideRoutes, ...guideEnRoutes, ...aiRoutes] return (
{!isGuide && ( diff --git a/src/sites/sites-react/doc/components/header/header.tsx b/src/sites/sites-react/doc/components/header/header.tsx index ba773e8f96..45bd79c415 100644 --- a/src/sites/sites-react/doc/components/header/header.tsx +++ b/src/sites/sites-react/doc/components/header/header.tsx @@ -88,6 +88,8 @@ const Header = () => { useEffect(() => { if (location.pathname.includes('/component')) { setActiveLink('组件') + } else if (location.pathname.includes('/ai/')) { + setActiveLink('AI') } }, [location]) const toAnother = () => { diff --git a/src/sites/sites-react/doc/components/nav/nav.tsx b/src/sites/sites-react/doc/components/nav/nav.tsx index 31a818c2ea..13a4f7790d 100644 --- a/src/sites/sites-react/doc/components/nav/nav.tsx +++ b/src/sites/sites-react/doc/components/nav/nav.tsx @@ -9,6 +9,7 @@ const Nav = () => { const [lang] = useLocale() const [fixed, setFixed] = useState(false) const [isGuide, setIsGuide] = useState(false) + const [isAi, setIsAi] = useState(false) const [activeName, setActiveName] = useState('') const scrollNav = () => { let top = document.documentElement.scrollTop @@ -31,6 +32,7 @@ const Nav = () => { } else { setIsGuide(false) } + setIsAi(location.pathname.includes('/ai/')) setActiveName(location.pathname.slice(location.pathname.lastIndexOf('/') + 1)) }, [location]) const changeNav = (_nav: any) => { @@ -41,7 +43,22 @@ const Nav = () => { const isZh = lang === 'zh-CN' return (
- {isGuide ? ( + {isAi ? ( +
    +
      +
    • + + isPending ? '' : isActive ? 'active' : '' + } + to={`${lang ? `/${lang}` : ''}/ai/llms`} + > + LLMs.txt + +
    • +
    +
+ ) : isGuide ? (
      {docs.packages.map((_package) => ( diff --git a/src/sites/sites-react/doc/docs/ai-react/llms.en-US.md b/src/sites/sites-react/doc/docs/ai-react/llms.en-US.md new file mode 100644 index 0000000000..d9e6c49368 --- /dev/null +++ b/src/sites/sites-react/doc/docs/ai-react/llms.en-US.md @@ -0,0 +1,51 @@ +# LLMs.txt + +This guide explains how to enable AI tools to better understand NutUI-React, so they generate more accurate code when working with NutUI-React. + +## What is LLMs.txt? + +We support [LLMs.txt](https://llmstxt.org/) files for making the NutUI-React documentation available to large language models (LLMs). This helps AI tools such as Cursor, GitHub Copilot, Claude and others better understand our component library, its APIs, and usage patterns, reducing hallucinated or incorrect code. + +Each component ships a plain-text `.md` document, so AI tools can precisely fetch usage, APIs, and style structure on demand without parsing the site's HTML. + +## Available Resources + +### LLMs.txt Aggregated Files + +We provide several aggregated files to help AI tools access the NutUI-React (H5, `@nutui/nutui-react`) documentation: + +| File | Description | +| --- | --- | +| [llms.txt](https://nutui.jd.com/h5/react/4x/llms.txt) | Navigation index with links to all component docs and style-structure files | +| [llms-full-cn.txt](https://nutui.jd.com/h5/react/4x/llms-full-cn.txt) | Complete component documentation (Chinese) with usage and examples | +| [llms-full.txt](https://nutui.jd.com/h5/react/4x/llms-full.txt) | Complete component documentation (English) | +| [llms-semantic-cn.txt](https://nutui.jd.com/h5/react/4x/llms-semantic-cn.txt) | Component style structure (Chinese) with CSS classes and CSS variables | +| [llms-semantic.txt](https://nutui.jd.com/h5/react/4x/llms-semantic.txt) | Component style structure (English) | + +### Single Component Documentation + +Append the component id (lowercase) under the site's `components/` path with a `.md` suffix to fetch that component's plain-text documentation, without loading the full file: + +[`https://nutui.jd.com/h5/react/4x/components/button.md`](https://nutui.jd.com/h5/react/4x/components/button.md) +[`https://nutui.jd.com/h5/react/4x/components/cell.md`](https://nutui.jd.com/h5/react/4x/components/cell.md) + +### Semantic Documentation + +Each component has a semantic (style structure) description file: + + [`https://nutui.jd.com/h5/react/4x/components/button/semantic.md`](https://nutui.jd.com/h5/react/4x/components/button/semantic.md) + +Semantic documentation includes: + +- The CSS classes available on the component's rendered output and their purposes +- The overridable CSS variables (design tokens) +- Precise anchors for customizing styles and overriding themes + +## Usage with AI Tools + +| Tool | Description | Prompt | +| --- | --- | --- | +| **Cursor** | Use the `@Docs` feature to add the LLMs.txt URL, or add the prompt to `.cursor/rules`. [Documentation](https://docs.cursor.com/context/@-symbols/@-docs) | `Read https://nutui.jd.com/h5/react/4x/llms-full.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **GitHub Copilot** | Reference the `llms.txt` URL in chat, or add `llms-full.txt` content to context. | `Read https://nutui.jd.com/h5/react/4x/llms-full.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **Claude / Claude Code** | Add to `CLAUDE.md` or use `/memory` to persist. [Documentation](https://docs.anthropic.com/en/docs/claude-code) | `Read https://nutui.jd.com/h5/react/4x/llms-full.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **Custom RAG / Agent** | Fetch `llms.txt` to parse navigation, then request each component's `.md` on demand to build an index. | `Read https://nutui.jd.com/h5/react/4x/llms.txt for navigation, then fetch each component's .md on demand.` | diff --git a/src/sites/sites-react/doc/docs/ai-react/llms.md b/src/sites/sites-react/doc/docs/ai-react/llms.md new file mode 100644 index 0000000000..52e36022db --- /dev/null +++ b/src/sites/sites-react/doc/docs/ai-react/llms.md @@ -0,0 +1,51 @@ +# LLMs.txt + +本篇介绍如何让 AI 工具更好地理解 NutUI-React,从而在使用 NutUI-React 编写代码时获得更准确的建议。 + +## 什么是 LLMs.txt? + +我们支持 [LLMs.txt](https://llmstxt.org/) 规范,将 NutUI-React 的文档以对大语言模型(LLM)友好的形式提供出来。借助这些文件,Cursor、GitHub Copilot、Claude 等 AI 工具能够更准确地理解我们的组件库、API 与用法,减少「幻觉」式的错误代码。 + +每个组件都提供纯文本 `.md` 文档,AI 工具无需解析站点 HTML,即可按需精确拉取组件的用法、API 与样式结构。 + +## 可用资源 + +### LLMs.txt 聚合文件 + +我们提供了一组聚合文件,方便 AI 工具访问 NutUI-React(H5,`@nutui/nutui-react`)的文档: + +| 文件 | 说明 | +| --- | --- | +| [llms.txt](https://nutui.jd.com/h5/react/4x/llms.txt) | 文档导航索引,包含全部组件文档与样式结构文件的链接 | +| [llms-full-cn.txt](https://nutui.jd.com/h5/react/4x/llms-full-cn.txt) | 全部组件文档(中文),含用法说明与示例,可整体注入上下文 | +| [llms-full.txt](https://nutui.jd.com/h5/react/4x/llms-full.txt) | 全部组件文档(英文) | +| [llms-semantic-cn.txt](https://nutui.jd.com/h5/react/4x/llms-semantic-cn.txt) | 全部组件样式结构(中文),含 CSS class 与 CSS 变量 | +| [llms-semantic.txt](https://nutui.jd.com/h5/react/4x/llms-semantic.txt) | 全部组件样式结构(英文) | + +### 单组件文档 + +将组件的 id(小写)拼接到站点路径下的 `components/` 后,并加上 `.md` 后缀,即可获取该组件的纯文本文档,无需加载全量文件: + +[`https://nutui.jd.com/h5/react/4x/components/button.md`](https://nutui.jd.com/h5/react/4x/components/button.md) +[`https://nutui.jd.com/h5/react/4x/components/cell.md`](https://nutui.jd.com/h5/react/4x/components/cell.md) + +### 样式结构(Semantic)文档 + +每个组件都提供一份样式结构文档: + +[`https://nutui.jd.com/h5/react/4x/components/button/semantic.md`](https://nutui.jd.com/h5/react/4x/components/button/semantic.md) + +样式结构文档包含: + +- 组件渲染产物中可用的 CSS class 及其用途 +- 可覆盖的 CSS 变量(设计 token) +- 便于自定义样式与主题覆盖时精确定位 + +## 配合 AI 工具使用 + +| 工具 | 使用方式 | 提示词 | +| --- | --- | --- | +| **Cursor** | 使用 `@Docs` 功能添加 LLMs.txt 地址,或将提示词写入 `.cursor/rules`。[文档](https://docs.cursor.com/context/@-symbols/@-docs) | `阅读 https://nutui.jd.com/h5/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **GitHub Copilot** | 在对话中引用 `llms.txt` 链接,或将 `llms-full-cn.txt` 内容加入上下文。 | `阅读 https://nutui.jd.com/h5/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **Claude / Claude Code** | 添加到 `CLAUDE.md` 或使用 `/memory` 持久化。[文档](https://docs.anthropic.com/en/docs/claude-code) | `阅读 https://nutui.jd.com/h5/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **自建 RAG / Agent** | 抓取 `llms.txt` 解析导航,再按需请求各组件 `.md` 建立索引。 | `阅读 https://nutui.jd.com/h5/react/4x/llms.txt 获取文档导航,并按需拉取各组件的 .md 文档。` | diff --git a/src/sites/sites-react/doc/docs/ai-taro/llms.en-US.md b/src/sites/sites-react/doc/docs/ai-taro/llms.en-US.md new file mode 100644 index 0000000000..c4481db724 --- /dev/null +++ b/src/sites/sites-react/doc/docs/ai-taro/llms.en-US.md @@ -0,0 +1,51 @@ +# LLMs.txt + +This guide explains how to enable AI tools to better understand NutUI-React (Mini Program / Taro multi-platform), so they generate more accurate code when working with NutUI-React. + +## What is LLMs.txt? + +We support [LLMs.txt](https://llmstxt.org/) files for making the NutUI-React documentation available to large language models (LLMs). This helps AI tools such as Cursor, GitHub Copilot, Claude and others better understand our component library, its APIs, and usage patterns, reducing hallucinated or incorrect code. + +Each component ships a plain-text `.md` document, so AI tools can precisely fetch usage, APIs, and style structure on demand without parsing the site's HTML. + +## Available Resources + +### LLMs.txt Aggregated Files + +We provide several aggregated files to help AI tools access the NutUI-React (Mini Program, `@nutui/nutui-react-taro`) documentation: + +| File | Description | +| --- | --- | +| [llms.txt](https://nutui.jd.com/taro/react/4x/llms.txt) | Navigation index with links to all component docs and style-structure files | +| [llms-full-cn.txt](https://nutui.jd.com/taro/react/4x/llms-full-cn.txt) | Complete component documentation (Chinese) with usage and examples | +| [llms-semantic-cn.txt](https://nutui.jd.com/taro/react/4x/llms-semantic-cn.txt) | Component style structure (Chinese) with CSS classes and CSS variables | + +> The Mini Program platform does not yet provide the English full-text file (`llms-full.txt`) or the English style-structure file (`llms-semantic.txt`). For English documentation, refer to the [H5 platform](https://nutui.jd.com/h5/react/4x/llms.txt). + +### Single Component Documentation + +Append the component id (lowercase) under the site's `components/` path with a `.md` suffix to fetch that component's plain-text documentation, without loading the full file: + +[`https://nutui.jd.com/taro/react/4x/components/button.md`](https://nutui.jd.com/taro/react/4x/components/button.md) +[`https://nutui.jd.com/taro/react/4x/components/cell.md`](https://nutui.jd.com/taro/react/4x/components/cell.md) + +### Semantic Documentation + +Each component has a semantic (style structure) description file: + +[`https://nutui.jd.com/taro/react/4x/components/button/semantic.md`](https://nutui.jd.com/taro/react/4x/components/button/semantic.md) + +Semantic documentation includes: + +- The CSS classes available on the component's rendered output and their purposes +- The overridable CSS variables (design tokens) +- Precise anchors for customizing styles and overriding themes + +## Usage with AI Tools + +| Tool | Description | Prompt | +| --- | --- | --- | +| **Cursor** | Use the `@Docs` feature to add the LLMs.txt URL, or add the prompt to `.cursor/rules`. [Documentation](https://docs.cursor.com/context/@-symbols/@-docs) | `Read https://nutui.jd.com/taro/react/4x/llms-full-cn.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **GitHub Copilot** | Reference the `llms.txt` URL in chat, or add `llms-full-cn.txt` content to context. | `Read https://nutui.jd.com/taro/react/4x/llms-full-cn.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **Claude / Claude Code** | Add to `CLAUDE.md` or use `/memory` to persist. [Documentation](https://docs.anthropic.com/en/docs/claude-code) | `Read https://nutui.jd.com/taro/react/4x/llms-full-cn.txt and understand NutUI-React components. Use this knowledge when writing code with NutUI-React.` | +| **Custom RAG / Agent** | Fetch `llms.txt` to parse navigation, then request each component's `.md` on demand to build an index. | `Read https://nutui.jd.com/taro/react/4x/llms.txt for navigation, then fetch each component's .md on demand.` | diff --git a/src/sites/sites-react/doc/docs/ai-taro/llms.md b/src/sites/sites-react/doc/docs/ai-taro/llms.md new file mode 100644 index 0000000000..91a79c335e --- /dev/null +++ b/src/sites/sites-react/doc/docs/ai-taro/llms.md @@ -0,0 +1,51 @@ +# LLMs.txt + +本篇介绍如何让 AI 工具更好地理解 NutUI-React(小程序 / Taro 多端),从而在使用 NutUI-React 编写代码时获得更准确的建议。 + +## 什么是 LLMs.txt? + +我们支持 [LLMs.txt](https://llmstxt.org/) 规范,将 NutUI-React 的文档以对大语言模型(LLM)友好的形式提供出来。借助这些文件,Cursor、GitHub Copilot、Claude、通义灵码等 AI 工具能够更准确地理解我们的组件库、API 与用法,减少「幻觉」式的错误代码。 + +每个组件都提供纯文本 `.md` 文档,AI 工具无需解析站点 HTML,即可按需精确拉取组件的用法、API 与样式结构。 + +## 可用资源 + +### LLMs.txt 聚合文件 + +我们提供了一组聚合文件,方便 AI 工具访问 NutUI-React(小程序,`@nutui/nutui-react-taro`)的文档: + +| 文件 | 说明 | +| --- | --- | +| [llms.txt](https://nutui.jd.com/taro/react/4x/llms.txt) | 文档导航索引,包含全部组件文档与样式结构文件的链接 | +| [llms-full-cn.txt](https://nutui.jd.com/taro/react/4x/llms-full-cn.txt) | 全部组件文档(中文),含用法说明与示例,可整体注入上下文 | +| [llms-semantic-cn.txt](https://nutui.jd.com/taro/react/4x/llms-semantic-cn.txt) | 全部组件样式结构(中文),含 CSS class 与 CSS 变量 | + +> 小程序端暂无英文全量文档(`llms-full.txt`)与英文样式结构文件(`llms-semantic.txt`)。如需英文文档,请参考 [H5 端](https://nutui.jd.com/h5/react/4x/llms.txt)。 + +### 单组件文档 + +将组件的 id(小写)拼接到站点路径下的 `components/` 后,并加上 `.md` 后缀,即可获取该组件的纯文本文档,无需加载全量文件: + +[`https://nutui.jd.com/taro/react/4x/components/button.md`](https://nutui.jd.com/taro/react/4x/components/button.md)(中文文档) +[`https://nutui.jd.com/taro/react/4x/components/cell.md`](https://nutui.jd.com/taro/react/4x/components/cell.md) + +### 样式结构(Semantic)文档 + +每个组件都提供一份样式结构文档: + +[`https://nutui.jd.com/taro/react/4x/components/button/semantic.md`](https://nutui.jd.com/taro/react/4x/components/button/semantic.md) + +样式结构文档包含: + +- 组件渲染产物中可用的 CSS class 及其用途 +- 可覆盖的 CSS 变量(设计 token) +- 便于自定义样式与主题覆盖时精确定位 + +## 配合 AI 工具使用 + +| 工具 | 使用方式 | 提示词 | +| --- | --- | --- | +| **Cursor** | 使用 `@Docs` 功能添加 LLMs.txt 地址,或将提示词写入 `.cursor/rules`。[文档](https://docs.cursor.com/context/@-symbols/@-docs) | `阅读 https://nutui.jd.com/taro/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **GitHub Copilot** | 在对话中引用 `llms.txt` 链接,或将 `llms-full-cn.txt` 内容加入上下文。 | `阅读 https://nutui.jd.com/taro/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **Claude / Claude Code** | 添加到 `CLAUDE.md` 或使用 `/memory` 持久化。[文档](https://docs.anthropic.com/en/docs/claude-code) | `阅读 https://nutui.jd.com/taro/react/4x/llms-full-cn.txt 并理解 NutUI-React 组件,在编写 NutUI-React 代码时运用这些知识。` | +| **自建 RAG / Agent** | 抓取 `llms.txt` 解析导航,再按需请求各组件 `.md` 建立索引。 | `阅读 https://nutui.jd.com/taro/react/4x/llms.txt 获取文档导航,并按需拉取各组件的 .md 文档。` | diff --git a/src/sites/sites-react/doc/router.ts b/src/sites/sites-react/doc/router.ts index fa7e1b2b66..cba8ef7a8f 100644 --- a/src/sites/sites-react/doc/router.ts +++ b/src/sites/sites-react/doc/router.ts @@ -67,4 +67,25 @@ for (const path in modulesEnDocs) { }); } -export {routes, guideRoutes, guideEnRoutes} \ No newline at end of file +const aiRoutes: any[] = [] +const modulesAiDocs = import.meta.glob('/src/sites/sites-react/doc/docs/ai-react/*.md'); +for (const path in modulesAiDocs) { + if (path.includes('.en-US.md')) continue; + let name = (/docs\/ai-react\/(.*).md/.exec(path) as any[])[1]; + aiRoutes.push({ + path: `/zh-CN/ai/${name}`, + component: modulesAiDocs[path], + name + }); +} +const modulesAiEnDocs = import.meta.glob('/src/sites/sites-react/doc/docs/ai-react/*.en-US.md'); +for (const path in modulesAiEnDocs) { + let name = (/docs\/ai-react\/(.*).en-US.md/.exec(path) as any[])[1]; + aiRoutes.push({ + path: `/en-US/ai/${name}`, + component: modulesAiEnDocs[path], + name: `en-${name}` + }); +} + +export {routes, guideRoutes, guideEnRoutes, aiRoutes} \ No newline at end of file