diff --git a/.gitignore b/.gitignore
index 7c87415936..81c8412857 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,9 @@ node_modules
/dist
/dist-demo
/dist-site
+# meta 派生产物:components.json 由 generate:meta 产出,各平台 llms 由 generate:llms 产出
+# (build:site / build:taro:site 前自动生成),整个目录均为构建产物,不落库
+/meta/
/release
/libs
/jd/upload.js
diff --git a/package.json b/package.json
index 08213ba72b..8cd5d6ef13 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +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",
"build:site": "vite build --config vite.config.site.mts",
+ "prebuild:taro:site": "npm run generate:meta && 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",
@@ -86,6 +88,8 @@
"generate:file:taro": "node scripts/taro/generate-nutui-taro.js",
"generate:file:taro:pages": "node scripts/taro/generate-taro-pages.js",
"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",
"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
new file mode 100644
index 0000000000..6004e8a452
--- /dev/null
+++ b/scripts/build-llms.mjs
@@ -0,0 +1,173 @@
+// 读 meta/components.json,为每个文档站平台(H5 / Taro)生成面向 LLM 的文档入口产物:
+// - llms.txt 导航索引(仿 llmstxt.org 标准)
+// - llms-full-cn.txt 全部中文 doc 拼接,可整体注入上下文
+// - llms-full.txt 全部英文 doc 拼接(仅 H5,Taro 端无英文文档)
+// 核心生成逻辑按平台参数化并导出为纯函数,供 vite.config.site(.taro) 的插件复用,避免多处实现。
+// DO NOT manual edit the outputs. Run: npm run generate:llms
+import fs from 'fs'
+import path from 'path'
+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 OUTPUT_DIR = path.join(ROOT, 'meta')
+
+export const SITE_ORIGIN = 'https://nutui.jd.com'
+
+const SUMMARY =
+ 'NutUI React 是京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序(Taro 多端)。'
+const SUMMARY_EN =
+ 'NutUI React is a lightweight, JD-style mobile React UI library that generates both H5 and mini-program (Taro multi-platform) apps from a single codebase.'
+
+// 各文档站平台的差异集中于此:站点 base、文档来源(meta.docs 的哪个 key)、标题落款、是否有英文文档。
+// 单组件 .md 路由由 site 插件产出到 /components/.md。
+export const PLATFORMS = {
+ h5: {
+ base: '/h5/react/4x',
+ // 中/英文正文各取哪个 docs key。H5 端中文用 doc.md、英文用 doc.en-US.md。
+ langDocKey: { cn: 'h5', en: 'enUS' },
+ titleSuffix: '',
+ pkgName: '@nutui/nutui-react',
+ },
+ taro: {
+ base: '/taro/react/4x',
+ // Taro 端中文用 doc.taro.md;暂无 Taro 英文文档,故不产出 llms-full.txt。
+ langDocKey: { cn: 'taro' },
+ titleSuffix: '(Taro 多端)',
+ pkgName: '@nutui/nutui-react-taro',
+ },
+}
+
+export function loadMeta() {
+ return JSON.parse(fs.readFileSync(META_PATH, 'utf-8'))
+}
+
+const siteUrl = (platform) => `${SITE_ORIGIN}${PLATFORMS[platform].base}`
+
+// 仅纳入在该平台有中文文档的组件(= show:true 的展示组件),按 categories 的 nav 顺序输出。
+function docComponents(meta, cnKey) {
+ const list = []
+ for (const cat of meta.categories) {
+ const comps = cat.components
+ .map((id) => meta.components[id])
+ .filter((c) => c && c.docs && c.docs[cnKey])
+ if (comps.length) list.push({ category: cat, comps })
+ }
+ return list
+}
+
+export function generateLlmsTxt(meta, platform = 'h5') {
+ const conf = PLATFORMS[platform]
+ const cnKey = conf.langDocKey.cn
+ const url = siteUrl(platform)
+ const groups = docComponents(meta, cnKey)
+ const lines = []
+ lines.push(`# NutUI React - 京东风格轻量级移动端 React 组件库${conf.titleSuffix}`)
+ lines.push('')
+ lines.push(
+ `> ${SUMMARY}当前为 ${conf.pkgName}(\`${platform}\` 端),版本 ${meta.libVersion},共 ${meta.componentCount} 个组件。`
+ )
+ lines.push('')
+ lines.push(
+ `本文件遵循 llmstxt.org 标准,为 AI 编程助手提供权威、离线可查的组件文档入口。每个组件的 \`.md\` 链接返回其纯文档原文(无站点 HTML 噪音),可按需精确拉取。`
+ )
+ lines.push('')
+ lines.push('## Navigation')
+ lines.push('')
+ lines.push(`- [全部组件文档(中文,单文件)](${url}/llms-full-cn.txt)`)
+ if (conf.langDocKey.en) {
+ lines.push(`- [Full Documentation (English, single file)](${url}/llms-full.txt)`)
+ }
+ lines.push('')
+
+ for (const { category, comps } of groups) {
+ lines.push(`## ${category.name}${category.enName ? ` (${category.enName})` : ''}`)
+ lines.push('')
+ for (const c of comps) {
+ const label = c.cName ? `${c.name} ${c.cName}` : c.name
+ lines.push(`- [${label}](${url}/components/${c.id}.md)`)
+ }
+ lines.push('')
+ }
+ return `${lines.join('\n').trimEnd()}\n`
+}
+
+// 拼接该平台全部组件文档为单文件。lang: 'cn' | 'en'(en 仅 H5 有)。
+export function generateLlmsFull(meta, platform = 'h5', lang = 'cn') {
+ const conf = PLATFORMS[platform]
+ const docKey = conf.langDocKey[lang]
+ const title =
+ lang === 'en'
+ ? '# NutUI React - Full Component Documentation'
+ : `# NutUI React - 全部组件文档${conf.titleSuffix}`
+ const summary = lang === 'en' ? SUMMARY_EN : SUMMARY
+ const parts = [title, '', `> ${summary}`, '']
+ for (const cat of meta.categories) {
+ for (const id of cat.components) {
+ const c = meta.components[id]
+ const rel = c && c.docs ? c.docs[docKey] : null
+ if (!rel) continue
+ const abs = path.join(ROOT, rel)
+ if (!fs.existsSync(abs)) continue
+ parts.push('---', '')
+ parts.push(fs.readFileSync(abs, 'utf-8').trim())
+ parts.push('')
+ }
+ }
+ return `${parts.join('\n').trimEnd()}\n`
+}
+
+// 汇总某平台要产出的全部 llms 文件:{ 相对站点根的路径 -> 文本内容 }。
+// 含 llms.txt / llms-full*.txt 与每组件的 components/.md(取该平台对应端文档)。
+// 供 build-llms 的 main 与 vite 插件共用,保证两处产物完全一致。
+export function collectLlmsFiles(meta, platform = 'h5') {
+ const conf = PLATFORMS[platform]
+ const cnKey = conf.langDocKey.cn
+ const files = {
+ 'llms.txt': generateLlmsTxt(meta, platform),
+ 'llms-full-cn.txt': generateLlmsFull(meta, platform, 'cn'),
+ }
+ if (conf.langDocKey.en) {
+ files['llms-full.txt'] = generateLlmsFull(meta, platform, 'en')
+ }
+ for (const c of Object.values(meta.components)) {
+ const rel = c.docs && c.docs[cnKey]
+ if (!rel) continue
+ const abs = path.join(ROOT, rel)
+ if (!fs.existsSync(abs)) continue
+ files[`components/${c.id}.md`] = fs.readFileSync(abs, 'utf-8')
+ }
+ return files
+}
+
+function main() {
+ console.log('🚀 building llms.txt ...')
+ const meta = loadMeta()
+ for (const platform of Object.keys(PLATFORMS)) {
+ const outDir = path.join(OUTPUT_DIR, platform)
+ fs.mkdirSync(outDir, { recursive: true })
+ // 磁盘产物仅保留导航与全量文件;单组件 md 由站点构建时按需产出,不落 meta。
+ const files = {
+ 'llms.txt': generateLlmsTxt(meta, platform),
+ 'llms-full-cn.txt': generateLlmsFull(meta, platform, 'cn'),
+ }
+ if (PLATFORMS[platform].langDocKey.en) {
+ files['llms-full.txt'] = generateLlmsFull(meta, platform, 'en')
+ }
+ for (const [name, content] of Object.entries(files)) {
+ const out = path.join(outDir, name)
+ fs.writeFileSync(out, content)
+ const kb = (Buffer.byteLength(content) / 1024).toFixed(0)
+ console.log(`✅ ${path.relative(ROOT, out)} (${kb}KB)`)
+ }
+ const cnKey = PLATFORMS[platform].langDocKey.cn
+ const docCount = docComponents(meta, cnKey).reduce((n, g) => n + g.comps.length, 0)
+ console.log(` [${platform}] 索引组件数: ${docCount}`)
+ }
+}
+
+// 仅作为脚本直接运行时执行 main,被 import 时不执行。
+if (import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main()
+}
diff --git a/scripts/build-meta.mjs b/scripts/build-meta.mjs
new file mode 100644
index 0000000000..72cf0b0588
--- /dev/null
+++ b/scripts/build-meta.mjs
@@ -0,0 +1,300 @@
+// 聚合 config.json + properties.json + demos + variables.scss,输出 meta/components.json。
+// 面向 AI Coding 的「单一事实源」中间产物,供后续 llms.txt / CLI / MCP 共同消费。
+// DO NOT manual edit the output (meta/components.json). Run: npm run generate:meta
+import fs from 'fs'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const ROOT = path.resolve(__dirname, '..')
+
+const CONFIG_PATH = path.join(ROOT, 'src/config.json')
+const PROPERTIES_PATH = path.join(ROOT, 'scripts/properties.json')
+const VARIABLES_PATH = path.join(ROOT, 'src/styles/variables.scss')
+const PACKAGES_DIR = path.join(ROOT, 'src/packages')
+const SPEC_DIR = path.join(ROOT, 'src/types/spec')
+const PKG_JSON_PATH = path.join(ROOT, 'package.json')
+const OUTPUT_PATH = path.join(ROOT, 'meta/components.json')
+
+const SCHEMA_VERSION = '1.0.0'
+
+// properties.json 的「组件名」经三级归一后仍无法匹配 config 的特例,手工兜底。
+// 注:SideNavBar 系列在 src/packages 有目录但未注册进 config.nav,故不设别名,
+// 让其如实落入 orphan-api 警告(提示「有文档但组件未注册」)。
+const COMPONENT_ALIAS = {
+ IconFont: 'icon',
+}
+
+const warnings = []
+const warn = (msg) => warnings.push(msg)
+
+// 把 properties 的中文位置式列映射为语义字段。语义严格只对 props 表成立,
+// 枚举/结构表(kind !== 'props')的四列语义不同,消费方应按 kind 重新解释。
+const mapRow = (rec) => ({
+ prop: (rec['第一列'] || '').trim(),
+ desc: (rec['第二列'] || '').trim(),
+ type: (rec['第三列'] || '').trim(),
+ default: (rec['第四列'] || '').trim(),
+})
+
+// 归一表格名,仅用于合并大小写差异(Props vs props),不合并语义变体(Props(direction=...))。
+const normalizeTableName = (name) => (name || '').trim().toLowerCase()
+
+const classifyKind = (name) => {
+ const t = normalizeTableName(name)
+ if (t === 'ref') return 'ref'
+ if (t === 'methods' || t === 'forminstance') return 'methods'
+ if (t.startsWith('props')) return 'props'
+ return 'other'
+}
+
+// config 的组件 name(PascalCase)→ 目录名(既有约定:name.toLowerCase())。
+const toDirId = (name) => String(name).toLowerCase()
+
+// 把 properties 的「组件名」归属到某个 config 组件 id。三级优先级:
+// 1) 去点拼接小写:子组件多在 config 有独立条目(Checkbox.Group→checkboxgroup)
+// 2) 首段小写:无独立条目时归到父组件(Tabs.Tabpane→tabs)
+// 3) 别名表兜底(IconFont→icon、SideNavBar 系列→sidenavbar)
+const resolveApiId = (rawComponentName, validIds) => {
+ const raw = String(rawComponentName)
+ if (COMPONENT_ALIAS[raw]) return COMPONENT_ALIAS[raw]
+ const joined = raw.replace(/\./g, '').toLowerCase()
+ if (validIds.has(joined)) return joined
+ const firstSeg = raw.split('.')[0].toLowerCase()
+ if (validIds.has(firstSeg)) return firstSeg
+ return joined // 落 orphan,保留去点形式便于诊断
+}
+
+// ---- A. properties.json → bucket[id] = { normKey -> table } ----
+function buildApiBucket(properties, validIds) {
+ const bucket = {}
+ const orphan = new Set()
+
+ for (const rec of properties) {
+ const raw = rec['组件名']
+ const id = resolveApiId(raw, validIds)
+ if (!validIds.has(id)) {
+ orphan.add(`${raw} (id=${id})`)
+ continue
+ }
+ const tableNameRaw = rec['表格名'] || 'Props'
+ const normKey = normalizeTableName(tableNameRaw)
+ // 仅当表来自父组件视角(raw 含点且归到了父组件 id)时才标记子组件来源;
+ // 归到独立子组件条目时它就是该组件本身,subComponent 记 null。
+ const hasDot = String(raw).includes('.')
+ const joinedId = String(raw).replace(/\./g, '').toLowerCase()
+ const subComponent =
+ hasDot && id !== joinedId ? String(raw).split('.').slice(1).join('.') : null
+ // 合并键含来源组件:同一 id 下不同原始组件的同名表(如 Icon 与 IconFont 的 Props)
+ // 应保持为独立表格,只有完全同源的表才合并大小写差异。
+ const groupKey = `${raw}@@${normKey}`
+
+ if (!bucket[id]) bucket[id] = {}
+ if (!bucket[id][groupKey]) {
+ bucket[id][groupKey] = {
+ name: tableNameRaw,
+ kind: classifyKind(tableNameRaw),
+ sourceComponent: raw,
+ subComponent,
+ rows: [],
+ }
+ } else if (bucket[id][groupKey].name !== tableNameRaw) {
+ warn(
+ `[table-case] "${id}" 合并大小写差异表格:「${bucket[id][groupKey].name}」与「${tableNameRaw}」`
+ )
+ }
+ bucket[id][groupKey].rows.push(mapRow(rec))
+ }
+
+ if (orphan.size) {
+ warn(
+ `[orphan-api] ${orphan.size} 个 properties 组件名无法匹配 config,未注入:${[...orphan].join(',')}`
+ )
+ }
+ return bucket
+}
+
+// bucket[id] → api.tables 数组 + Props 行内质量校验
+function bucketToApi(id, tablesMap) {
+ if (!tablesMap) return { tables: [] }
+ const tables = Object.values(tablesMap)
+ for (const t of tables) {
+ if (t.kind !== 'props') continue
+ const seen = new Set()
+ for (const row of t.rows) {
+ if (!row.prop || !row.type) {
+ warn(`[props-field] "${id}" 表「${t.name}」存在缺列行:prop="${row.prop}" type="${row.type}"`)
+ }
+ if (row.prop) {
+ if (seen.has(row.prop)) warn(`[dup-prop] "${id}" 表「${t.name}」prop 重复:${row.prop}`)
+ seen.add(row.prop)
+ }
+ }
+ }
+ return { tables }
+}
+
+// ---- B. variables.scss → tokens(按最长组件目录前缀归属,否则 global)----
+function parseTokens(validIdsSorted) {
+ const content = fs.readFileSync(VARIABLES_PATH, 'utf-8')
+ // 仅匹配「$scssVar: var(--nutui-xxx[, default]) !default;」的定义行(单行)。
+ const re = /^\s*\$([\w-]+):\s*var\((--nutui-[\w-]+)\s*(?:,\s*([^;]*?))?\)\s*!default/gm
+ const perComponent = {}
+ const globalTokens = []
+ let m
+ while ((m = re.exec(content)) !== null) {
+ const scssVar = `$${m[1]}`
+ const cssVar = m[2]
+ const def = m[3] != null ? m[3].trim() : null
+ const token = { cssVar, scssVar, default: def }
+ const rest = cssVar.slice('--nutui-'.length)
+ // 最长匹配:validIdsSorted 已按长度降序
+ const owner = validIdsSorted.find(
+ (id) => rest === id || rest.startsWith(`${id}-`)
+ )
+ if (owner) {
+ ;(perComponent[owner] ||= []).push(token)
+ } else {
+ globalTokens.push(token)
+ }
+ }
+ return { perComponent, globalTokens }
+}
+
+// ---- demos 扫描 ----
+function scanDemos(id, sub) {
+ const dir = path.join(PACKAGES_DIR, id, 'demos', sub)
+ if (!fs.existsSync(dir)) return []
+ return fs
+ .readdirSync(dir)
+ .map((f) => f.match(/^demo(\d+)\.tsx$/))
+ .filter(Boolean)
+ .sort((a, b) => Number(a[1]) - Number(b[1]))
+ .map((mt) => `src/packages/${id}/demos/${sub}/${mt[0]}`)
+}
+
+// ---- docs 存在性 ----
+function resolveDocs(id, showFlag) {
+ const map = {
+ h5: 'doc.md',
+ enUS: 'doc.en-US.md',
+ zhTW: 'doc.zh-TW.md',
+ taro: 'doc.taro.md',
+ }
+ const docs = {}
+ for (const [key, file] of Object.entries(map)) {
+ const rel = `src/packages/${id}/${file}`
+ docs[key] = fs.existsSync(path.join(ROOT, rel)) ? rel : null
+ }
+ if (showFlag && !docs.h5) warn(`[doc-missing] "${id}" show:true 但缺 doc.md`)
+ return docs
+}
+
+// ---- spec 引用(可选,仅存在时输出)----
+function resolveSpec(id) {
+ const base = path.join(SPEC_DIR, id)
+ if (!fs.existsSync(base)) return null
+ const spec = {}
+ for (const key of ['base', 'h5', 'taro']) {
+ const rel = `src/types/spec/${id}/${key}.ts`
+ if (fs.existsSync(path.join(ROOT, rel))) spec[key] = rel
+ }
+ return Object.keys(spec).length ? spec : null
+}
+
+function main() {
+ console.log('🚀 building meta/components.json ...')
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
+ const properties = JSON.parse(fs.readFileSync(PROPERTIES_PATH, 'utf-8'))
+ const pkg = JSON.parse(fs.readFileSync(PKG_JSON_PATH, 'utf-8'))
+
+ // config.nav → 主索引
+ const validIds = new Set()
+ const navPairs = [] // { cat, pkg, id }
+ for (const cat of config.nav) {
+ for (const p of cat.packages) {
+ const id = toDirId(p.name)
+ validIds.add(id)
+ navPairs.push({ cat, p, id })
+ }
+ }
+ const validIdsSorted = [...validIds].sort((a, b) => b.length - a.length)
+
+ const bucket = buildApiBucket(properties, validIds)
+ const { perComponent: tokensByComp, globalTokens } = parseTokens(validIdsSorted)
+
+ const components = {}
+ const categories = []
+ const noApiIds = []
+
+ for (const cat of config.nav) {
+ const catRef = { name: cat.name, enName: cat.enName, components: [] }
+ for (const p of cat.packages) {
+ const id = toDirId(p.name)
+ const api = bucketToApi(id, bucket[id])
+ if (!api.tables.length) noApiIds.push(id)
+
+ const entry = {
+ id,
+ name: p.name,
+ cName: p.cName,
+ version: p.version,
+ category: { name: cat.name, enName: cat.enName },
+ taro: !!p.taro,
+ v15: !!p.v15,
+ v16: !!p.v16,
+ }
+ // 可选原样字段
+ for (const k of ['author', 'type', 'desc', 'sort', 'show', 'rn', 'exclude', 'jdtaro']) {
+ if (p[k] !== undefined) entry[k] = p[k]
+ }
+ entry.docs = resolveDocs(id, p.show)
+ entry.demos = { h5: scanDemos(id, 'h5'), taro: scanDemos(id, 'taro') }
+ const spec = resolveSpec(id)
+ if (spec) entry.spec = spec
+ entry.api = api
+ entry.tokens = tokensByComp[id] || []
+
+ if (p.show && !entry.demos.h5.length && !entry.demos.taro.length) {
+ warn(`[demo-empty] "${id}" show:true 但无 demo`)
+ }
+ if (!cat.enName) warn(`[category] 分类「${cat.name}」缺 enName`)
+
+ components[id] = entry
+ catRef.components.push(id)
+ }
+ categories.push(catRef)
+ }
+
+ const componentCount = Object.keys(components).length
+ const apiComponentCount = componentCount - noApiIds.length
+
+ const output = {
+ schemaVersion: SCHEMA_VERSION,
+ libVersion: pkg.version,
+ componentCount,
+ apiComponentCount,
+ categories,
+ globalTokens,
+ components,
+ }
+
+ fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true })
+ fs.writeFileSync(OUTPUT_PATH, `${JSON.stringify(output, null, 2)}\n`)
+
+ // ---- 对账 & 汇总 ----
+ console.log(`✅ 写入 ${path.relative(ROOT, OUTPUT_PATH)}`)
+ console.log(` 组件总数: ${componentCount},有 API 表: ${apiComponentCount}`)
+ console.log(` 全局 token: ${globalTokens.length}`)
+ if (noApiIds.length) {
+ console.log(` 无 API 表的组件 (${noApiIds.length}): ${noApiIds.join(', ')}`)
+ }
+ if (warnings.length) {
+ console.log(`\n⚠️ ${warnings.length} 条 WARNING:`)
+ for (const w of warnings) console.log(` - ${w}`)
+ } else {
+ console.log('\n✨ 无 WARNING')
+ }
+}
+
+main()
diff --git a/scripts/site/vite-plugin-llms.mjs b/scripts/site/vite-plugin-llms.mjs
new file mode 100644
index 0000000000..4dac349b9c
--- /dev/null
+++ b/scripts/site/vite-plugin-llms.mjs
@@ -0,0 +1,54 @@
+// Vite 插件(用于文档站 vite.config.site.mts 与 vite.config.site.taro.mts):
+// - build 时:把每个组件对应端的 doc 原文与 llms.txt/llms-full*.txt 通过 emitFile
+// 输出为静态文件,最终落到 dist-site/<...>/components/.md 与 dist-site/<...>/*.txt
+// - dev 时:configureServer 中间件按同样路径实时返回,便于本地联调
+// 平台差异(base、取哪一端文档)由 build-llms.mjs 的 PLATFORMS 决定,此处仅按 platform 透传。
+import {
+ loadMeta,
+ collectLlmsFiles,
+ PLATFORMS,
+} from '../build-llms.mjs'
+
+const contentType = (p) =>
+ p.endsWith('.md')
+ ? 'text/markdown; charset=utf-8'
+ : 'text/plain; charset=utf-8'
+
+// platform: 'h5' | 'taro',决定站点 base 与单组件 md 取哪一端文档。
+export default function llmsPlugin({ platform = 'h5' } = {}) {
+ const base = PLATFORMS[platform].base.replace(/\/$/, '')
+ return {
+ name: 'nutui-llms',
+
+ // build 阶段:把虚拟文件写入产物目录(相对 outDir,即 dist-site/)。
+ generateBundle() {
+ const files = collectLlmsFiles(loadMeta(), 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)`
+ )
+ },
+
+ // dev 阶段:中间件按 /llms.txt、/components/.md 实时返回。
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ const url = (req.url || '').split('?')[0]
+ const rel = url.startsWith(base) ? url.slice(base.length) : url
+ const key = rel.replace(/^\//, '')
+ const isTarget =
+ key === 'llms.txt' ||
+ key === 'llms-full-cn.txt' ||
+ key === 'llms-full.txt' ||
+ (key.startsWith('components/') && key.endsWith('.md'))
+ if (!isTarget) return next()
+
+ const files = collectLlmsFiles(loadMeta(), platform)
+ if (!(key in files)) return next()
+ res.setHeader('Content-Type', contentType(key))
+ res.end(files[key])
+ })
+ },
+ }
+}
diff --git a/vite.config.site.mts b/vite.config.site.mts
index b0a8ace5b4..67c9afd415 100644
--- a/vite.config.site.mts
+++ b/vite.config.site.mts
@@ -6,6 +6,8 @@ import { join, resolve } from 'path'
import atImport from 'postcss-import'
import rehypeHighlight from 'rehype-highlight'
import config from './package.json'
+// @ts-ignore
+import llmsPlugin from './scripts/site/vite-plugin-llms.mjs'
const projectID = process.env.VITE_APP_PROJECT_ID || ''
@@ -64,6 +66,7 @@ export default defineConfig(async (): Promise => {
}),
},
reactRefresh(),
+ llmsPlugin({ platform: 'h5' }),
],
build: {
outDir: './dist-site/h5',
diff --git a/vite.config.site.taro.mts b/vite.config.site.taro.mts
index 022b4c6d17..5d4fd3e9fd 100644
--- a/vite.config.site.taro.mts
+++ b/vite.config.site.taro.mts
@@ -6,6 +6,8 @@ import { join, resolve } from 'path'
import atImport from 'postcss-import'
import rehypeHighlight from 'rehype-highlight'
import config from './package.json'
+// @ts-ignore
+import llmsPlugin from './scripts/site/vite-plugin-llms.mjs'
const projectID = process.env.VITE_APP_PROJECT_ID || ''
@@ -64,6 +66,7 @@ export default defineConfig(async (): Promise => {
}),
},
reactRefresh(),
+ llmsPlugin({ platform: 'taro' }),
],
build: {
outDir: './dist-site/taro',