diff --git a/README.md b/README.md index 5118063..ffdcb50 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,37 @@ - # modules-dev - ### 描述 用于辅助在项目内启动一个规范化组件开发的环境 - ### 安装 ```shell npm i --save @kne/modules-dev ``` - ### 概述 modules-dev 是一个强大的组件开发辅助工具,为你的远程组件或前端库项目提供完整的开发环境。无论是实时预览组件示例,还是生成可集成到文档系统的代码,modules-dev 都能轻松应对。 -### 核心特性 +#### 核心特性 - **零配置开箱即用**:基于 Craco 封装的构建插件,支持远程组件和组件库两种场景 - **实时预览**:开发环境支持组件示例的实时预览,所见即所得 -- **文档自动生成**:自动解析 `doc/` 目录下的文档和示例代码 +- **文档自动生成**:解析各组件 `doc/`,生成组件 README,并写入项目根 `docs/{PascalName}.md`;根 README 的 DOC_MD 段仅保留名称、简介与相对链接(GitHub 可点) - **命令行工具**:提供 `modules-dev-create` 和 `modules-dev-libs-init` 快速创建组件目录结构 - **模块联邦支持**:内置 Webpack Module Federation 支持,方便远程组件开发 - **灵活的路径配置**:通过环境变量自定义组件目录和别名 -### 应用场景 +#### 应用场景 1. **远程组件开发**:在开发远程组件时提供实时预览环境,组件发布后可集成到 kne-union 文档系统 2. **组件库开发**:为前端组件库提供统一的示例展示和文档生成能力 3. **业务项目调试**:在开发模式下集成示例预览,快速验证组件功能 -### 构建工具使用 +#### 构建工具使用 -#### 远程组件项目 +##### 远程组件项目 在项目的 `craco.config.js` 中配置 CracoRemoteComponentsPlugin: @@ -49,7 +45,7 @@ module.exports = { }; ``` -#### 组件库项目 +##### 组件库项目 在示例目录的 `example/craco.config.js` 中配置 CracoLibsExamplePlugin: @@ -63,9 +59,9 @@ module.exports = { }; ``` -### Example 组件使用 +#### Example 组件使用 -#### 远程组件库项目 +##### 远程组件库项目 ```jsx import createEntry from "@kne/modules-dev/dist/create-entry"; @@ -89,7 +85,7 @@ const App = ({preset, themeToken, ...props}) => { }; ``` -#### 业务项目集成开发模式 +##### 业务项目集成开发模式 ```jsx if (process.env.NODE_ENV === 'development') { @@ -107,12 +103,12 @@ if (process.env.NODE_ENV === 'development') { } ``` -### 命令行工具 +#### 命令行工具 - `modules-dev-create`:在远程组件项目中创建新的组件目录结构和文档模板 - `modules-dev-libs-init`:为前端库项目初始化示例开发环境 -### 环境变量配置 +#### 环境变量配置 | 变量名 | 说明 | 默认值 | |--------|------|--------| @@ -294,7 +290,7 @@ const { Highlight } = _ModulesDev; const { Space, Typography, Card, Input } = antd; const { useState } = React; -const codeTemplate = `import React from 'react'; +const codeTemplate = `import React from 'react'; function App() { return ( @@ -305,7 +301,7 @@ function App() { ); } -export default App;`; +export default App;`; const HighlightExample = () => { const [code, setCode] = useState(codeTemplate); @@ -325,7 +321,7 @@ const HighlightExample = () => { /> - ${escapeHtml(code)}`} /> + ${escapeHtml(code)}`} /> ); @@ -344,14 +340,13 @@ render(); ``` - ### API ```js const {CracoRemoteComponentsPlugin, CracoLibsExamplePlugin, env} = require('@kne/modules-dev'); ``` -### CracoRemoteComponentsPlugin +#### CracoRemoteComponentsPlugin 用于远程组件项目的 Craco 插件,自动配置文档解析、模块联邦和 CSS Modules 支持。 @@ -359,7 +354,7 @@ const {CracoRemoteComponentsPlugin, CracoLibsExamplePlugin, env} = require('@kne |-----|----|----|-----| | middleware | 中间件配置 | object | undefined | -### CracoLibsExamplePlugin +#### CracoLibsExamplePlugin 用于组件库示例项目的 Craco 插件,自动配置文档解析、版本管理和模块联邦支持。 @@ -367,7 +362,7 @@ const {CracoRemoteComponentsPlugin, CracoLibsExamplePlugin, env} = require('@kne |-----|----|----|-----| | middleware | 中间件配置 | object | undefined | -### env +#### env 环境变量配置对象,提供项目构建所需的各种路径和配置信息。 @@ -386,13 +381,30 @@ const {CracoRemoteComponentsPlugin, CracoLibsExamplePlugin, env} = require('@kne | componentsVersion | 组件版本号 | string | | publicUrl | 公共 URL | string | +#### buildComponentDocs + +生成各组件 README,并将完整文档写入项目根 `docs/{PascalName}.md`(文件名强制大驼峰,如 `button-group` → `ButtonGroup.md`);根 README 的 `` 段只写入组件目录表(名称 + 简介 + 相对链接)。production 构建时另生成**完整聚合** `build/README.md`(含全部组件文档,与根目录 README 无关);`docs/` 不进入 `build/`。 + +| 属性名 | 说明 | 类型 | 默认值 | +|-----|----|----|-----| +| moduleBaseDir | 组件目录 | string | `env.moduleBaseDir` | +| rootReadme | 根 README 路径 | string | `{appDir}/README.md` | +| docsDir | 文档输出目录 | string | `{appDir}/docs`(libs-example 为 `../docs`) | +| getModuleList | 自定义模块列表加载 | function | 内置 `getModuleList` | + +```js +const { buildComponentDocs } = require('@kne/modules-dev'); +await buildComponentDocs(); +// 或 CLI:modules-dev-build-docs / npm run build:docs +``` + ```js import createEntry from '@kne/modules-dev/dist/create-entry'; const Entry = createEntry(children); ``` -### createEntry +#### createEntry 高阶组件工厂函数,用于包装业务组件,集成开发环境的文档预览功能。 @@ -405,7 +417,7 @@ const Entry = createEntry(children); | pageProps | 页面属性 | object | - | | baseUrl | 基础路径 | string | '' | -### createEntry.ExampleRoutes +#### createEntry.ExampleRoutes 示例路由组件,用于渲染组件示例页面。 @@ -420,7 +432,7 @@ const Entry = createEntry(children); | pageProps | 页面属性 | object | - | | children | 子路由内容 | ReactNode | - | -### createEntry.Example +#### createEntry.Example 单个组件示例展示组件。 @@ -430,7 +442,7 @@ const Entry = createEntry(children); | readme | 组件 README 数据 | object | - | | pageProps | 页面属性 | object | - | -### createEntry.ExamplePage +#### createEntry.ExamplePage 示例页面组件,用于展示组件文档和代码示例。 @@ -441,7 +453,7 @@ const Entry = createEntry(children); | items | 组件列表 | array | - | | pageProps | 页面属性 | object | - | -### createEntry.ExampleContent +#### createEntry.ExampleContent 示例内容组件,渲染组件的描述、概述、代码示例和 API 文档。 @@ -449,11 +461,19 @@ const Entry = createEntry(children); |-----|----|----|-----| | data | 组件数据 | object | - | +```js +import {ExampleDriverContext} from '@kne/modules-dev/dist/index'; +``` + +#### ExampleDriverContext + +示例 `contextComponent` 包装器(`GlobalProvider` + `MemoryRouter`)。响应式上下文由 `@kne/example-driver` LiveCode 内注入,无需在此层再包 `ResponsiveProvider`。 + ```js import {FontList} from '@kne/modules-dev/dist/index'; ``` -### FontList +#### FontList 图标字体列表展示组件,用于预览和复制图标代码。 @@ -465,7 +485,7 @@ import {FontList} from '@kne/modules-dev/dist/index'; import {Example} from '@kne/modules-dev/dist/index'; ``` -### Example +#### Example 组件示例展示组件(别名,同 createEntry.Example)。 @@ -479,7 +499,7 @@ import {Example} from '@kne/modules-dev/dist/index'; import {ExamplePage} from '@kne/modules-dev/dist/index'; ``` -### ExamplePage +#### ExamplePage 示例页面组件(别名,同 createEntry.ExamplePage)。 @@ -489,4 +509,3 @@ import {ExamplePage} from '@kne/modules-dev/dist/index'; | current | 当前组件 ID | string | - | | items | 组件列表 | array | - | | pageProps | 页面属性 | object | - | - diff --git a/doc/api.md b/doc/api.md index d070dd1..fbf1672 100644 --- a/doc/api.md +++ b/doc/api.md @@ -37,6 +37,23 @@ const {CracoRemoteComponentsPlugin, CracoLibsExamplePlugin, env} = require('@kne | componentsVersion | 组件版本号 | string | | publicUrl | 公共 URL | string | +### buildComponentDocs + +生成各组件 README,并将完整文档写入项目根 `docs/{PascalName}.md`(文件名强制大驼峰,如 `button-group` → `ButtonGroup.md`);根 README 的 `` 段只写入组件目录表(名称 + 简介 + 相对链接)。production 构建时另生成**完整聚合** `build/README.md`(含全部组件文档,与根目录 README 无关);`docs/` 不进入 `build/`。 + +| 属性名 | 说明 | 类型 | 默认值 | +|-----|----|----|-----| +| moduleBaseDir | 组件目录 | string | `env.moduleBaseDir` | +| rootReadme | 根 README 路径 | string | `{appDir}/README.md` | +| docsDir | 文档输出目录 | string | `{appDir}/docs`(libs-example 为 `../docs`) | +| getModuleList | 自定义模块列表加载 | function | 内置 `getModuleList` | + +```js +const { buildComponentDocs } = require('@kne/modules-dev'); +await buildComponentDocs(); +// 或 CLI:modules-dev-build-docs / npm run build:docs +``` + ```js import createEntry from '@kne/modules-dev/dist/create-entry'; diff --git a/doc/summary.md b/doc/summary.md index 8c727df..785a756 100644 --- a/doc/summary.md +++ b/doc/summary.md @@ -4,7 +4,7 @@ modules-dev 是一个强大的组件开发辅助工具,为你的远程组件 - **零配置开箱即用**:基于 Craco 封装的构建插件,支持远程组件和组件库两种场景 - **实时预览**:开发环境支持组件示例的实时预览,所见即所得 -- **文档自动生成**:自动解析 `doc/` 目录下的文档和示例代码 +- **文档自动生成**:解析各组件 `doc/`,生成组件 README,并写入项目根 `docs/{PascalName}.md`;根 README 的 DOC_MD 段仅保留名称、简介与相对链接(GitHub 可点) - **命令行工具**:提供 `modules-dev-create` 和 `modules-dev-libs-init` 快速创建组件目录结构 - **模块联邦支持**:内置 Webpack Module Federation 支持,方便远程组件开发 - **灵活的路径配置**:通过环境变量自定义组件目录和别名 diff --git a/lib/build-component-docs.js b/lib/build-component-docs.js index 01721e6..eebba73 100644 --- a/lib/build-component-docs.js +++ b/lib/build-component-docs.js @@ -3,11 +3,13 @@ const path = require('path'); const fs = require('fs-extra'); const { stringify } = require('@kne/md-doc'); +const camelCase = require('@kne/camel-case'); const env = require('./env'); const { getModuleList } = require('./utils'); const DOC_MD_START = ''; const DOC_MD_END = ''; +const SUMMARY_MAX_LEN = 120; const upsertDocMdSection = async (rootReadmePath, body) => { const section = `${DOC_MD_START}\n\n${body}\n\n${DOC_MD_END}`; @@ -32,42 +34,145 @@ const upsertDocMdSection = async (rootReadmePath, body) => { await fs.writeFile(rootReadmePath, `${trimmed}\n\n${section}\n`); }; +const stripHtml = (html) => + String(html || '') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + +const truncateSummary = (text) => { + const s = stripHtml(text); + if (s.length <= SUMMARY_MAX_LEN) { + return s; + } + return `${s.slice(0, SUMMARY_MAX_LEN).replace(/\s+\S*$/, '')}…`; +}; + +const escapeTableCell = (text) => + String(text || '') + .replace(/\|/g, '\\|') + .replace(/\r?\n/g, ' ') + .trim(); + +/** + * 简介:doc/summary.md → package.json.description + */ +const resolveComponentSummary = async (baseDir) => { + const summaryPath = path.join(baseDir, 'doc/summary.md'); + if (await fs.exists(summaryPath)) { + const raw = await fs.readFile(summaryPath, 'utf8'); + const summary = truncateSummary(raw); + if (summary) { + return summary; + } + } + try { + const pkg = await fs.readJson(path.join(baseDir, 'package.json')); + return truncateSummary(pkg.description || ''); + } catch { + return ''; + } +}; + +const toPascalName = (name) => camelCase(String(name || '')); + +const buildDocsToc = (entries) => { + const lines = ['| 组件 | 简介 |', '|------|------|']; + entries.forEach(({ pascalName, summary }) => { + lines.push(`| [${pascalName}](docs/${pascalName}.md) | ${escapeTableCell(summary)} |`); + }); + return lines.join('\n'); +}; + /** - * 生成各组件 README.md,并聚合写入根 README 的 DOC_MD 段。 + * 生成各组件 README.md,写入 docs/{PascalName}.md,根 README 的 DOC_MD 仅保留目录表。 * @param {object} [options] * @param {string} [options.moduleBaseDir] * @param {string} [options.rootReadme] + * @param {string} [options.docsDir] * @param {Function} [options.getModuleList] */ const buildComponentDocs = async (options = {}) => { const moduleBaseDir = options.moduleBaseDir || env.moduleBaseDir; const rootReadme = options.rootReadme || path.resolve(env.appDir, 'README.md'); + const docsDir = options.docsDir || path.resolve(env.appDir, 'docs'); const listLoader = options.getModuleList || getModuleList; const list = await listLoader(moduleBaseDir); - await Promise.all(list.map(props => stringify(props))); + await Promise.all(list.map((props) => stringify(props))); const sorted = [...list].sort((a, b) => String(a.name).localeCompare(String(b.name))); - const parts = []; - for (const { baseDir } of sorted) { + await fs.ensureDir(docsDir); + + const tocEntries = []; + const writtenFiles = new Set(); + const seenPascal = new Map(); + + for (const item of sorted) { + const { name, baseDir } = item; + const pascalName = toPascalName(name); + if (!pascalName) { + continue; + } + + if (seenPascal.has(pascalName)) { + console.warn( + `[build-component-docs] PascalCase 重名: ${seenPascal.get(pascalName)} 与 ${name} → ${pascalName}.md,后者覆盖前者` + ); + } + seenPascal.set(pascalName, name); + const readmePath = path.join(baseDir, 'README.md'); - if (await fs.exists(readmePath)) { - parts.push((await fs.readFile(readmePath, 'utf8')).trim()); + if (!(await fs.exists(readmePath))) { + continue; } + + const content = (await fs.readFile(readmePath, 'utf8')).trim(); + const docsFile = `${pascalName}.md`; + await fs.writeFile(path.join(docsDir, docsFile), `${content}\n`); + writtenFiles.add(docsFile); + + const summary = await resolveComponentSummary(baseDir); + tocEntries.push({ name, pascalName, summary }); } - await upsertDocMdSection(rootReadme, parts.join('\n\n')); + // 清理 docs/ 中未再生成的旧 md(含历史非大驼峰文件名) + const existing = await fs.readdir(docsDir); + await Promise.all( + existing.map(async (file) => { + if (!file.endsWith('.md') || writtenFiles.has(file)) { + return; + } + await fs.remove(path.join(docsDir, file)); + }) + ); + + tocEntries.sort((a, b) => a.pascalName.localeCompare(b.pascalName)); + await upsertDocMdSection(rootReadme, buildDocsToc(tocEntries)); + + // 完整聚合文档(供 build/README.md,与根 README 目录表无关) + const fullParts = []; + for (const { pascalName } of tocEntries) { + const filePath = path.join(docsDir, `${pascalName}.md`); + if (await fs.exists(filePath)) { + fullParts.push((await fs.readFile(filePath, 'utf8')).trim()); + } + } + const fullReadme = `${DOC_MD_START}\n\n${fullParts.join('\n\n')}\n\n${DOC_MD_END}\n`; - return { list: sorted, rootReadme }; + return { list: sorted, rootReadme, docsDir, tocEntries, fullReadme }; }; module.exports = buildComponentDocs; module.exports.upsertDocMdSection = upsertDocMdSection; +module.exports.toPascalName = toPascalName; +module.exports.buildDocsToc = buildDocsToc; +module.exports.resolveComponentSummary = resolveComponentSummary; module.exports.DOC_MD_START = DOC_MD_START; module.exports.DOC_MD_END = DOC_MD_END; if (require.main === module) { - buildComponentDocs().catch(err => { + buildComponentDocs().catch((err) => { console.error(err); process.exit(1); }); diff --git a/lib/craco-libs-example-plugin.js b/lib/craco-libs-example-plugin.js index f91ad35..9164c51 100644 --- a/lib/craco-libs-example-plugin.js +++ b/lib/craco-libs-example-plugin.js @@ -19,8 +19,9 @@ module.exports = { packageName: context.packageJson.name }]; }, - // 组件库 README 在 example 上一级,需一并打进 build 输出 + // 组件库 README / docs 在 example 上一级,需一并打进 build 输出 readmePath: path.resolve(env.appDir, '../README.md'), + docsDir: path.resolve(env.appDir, '../docs'), loaderOptions: { description: context.packageJson.description, packageName: context.packageJson.name }, watchTarget: path.resolve(env.appDir, '../doc/**/*'), watchCallback: () => { diff --git a/lib/craco-readme-plugin.js b/lib/craco-readme-plugin.js index 82e874e..6dad6af 100644 --- a/lib/craco-readme-plugin.js +++ b/lib/craco-readme-plugin.js @@ -6,7 +6,7 @@ const {stringify} = require("@kne/md-doc"); const ReadmePlugin = { overrideWebpackConfig: ({webpackConfig, context, pluginOptions}) => { - const {loaderOptions, getModuleList, readmePath} = Object.assign({}, pluginOptions); + const {loaderOptions, getModuleList, readmePath, docsDir} = Object.assign({}, pluginOptions); webpackConfig.module.rules.push({ test: /README\.md$/, loader: require.resolve('./readme-loader'), options: loaderOptions }); @@ -45,7 +45,7 @@ const ReadmePlugin = { })(); webpackConfig.plugins.push(new ReadmeWebpackPlugin({ - env: context.env, getModuleList, readmePath + env: context.env, getModuleList, readmePath, docsDir })); return webpackConfig; diff --git a/lib/readme-webpack-plugin.js b/lib/readme-webpack-plugin.js index 433065c..755f0b5 100644 --- a/lib/readme-webpack-plugin.js +++ b/lib/readme-webpack-plugin.js @@ -9,30 +9,36 @@ const buildComponentDocs = require('./build-component-docs'); const ReadmeWebpackPlugin_MODULE_NAMES = 'ReadmeWebpackPlugin_MODULE_NAMES'; -/** 将 README.md 拷到 webpack 输出目录(与 remoteEntry.js 同级),供 CDN/文档搜索访问。 */ -const copyReadmeToOutput = async ({ readmePath, outputPath }) => { - if (!(await fs.pathExists(readmePath))) { +/** 将完整聚合文档写入 webpack 输出目录的 README.md(与 remoteEntry.js 同级)。 */ +const writeFullReadmeToOutput = async ({ content, outputPath }) => { + if (content == null || content === '') { return false; } await fs.ensureDir(outputPath); - await fs.copy(readmePath, path.join(outputPath, 'README.md')); + await fs.writeFile(path.join(outputPath, 'README.md'), content); return true; }; class ReadmeWebpackPlugin { constructor(options) { this.options = Object.assign({}, options); + this._fullReadme = ''; } apply(compiler) { virtualModules.apply(compiler); compiler.hooks.beforeRun.tapPromise('ReadmeWebpackPlugin', async (compilationParams) => { if (compilationParams.options.mode === 'production') { - await buildComponentDocs({ + const docsDir = this.options.docsDir || path.resolve(env.appDir, 'docs'); + // readmePath:仅用于更新仓库根 README 的 DOC_MD 目录表,不是 build 输出源 + const rootReadme = this.options.readmePath || path.resolve(env.appDir, 'README.md'); + const result = await buildComponentDocs({ moduleBaseDir: env.moduleBaseDir, - rootReadme: path.resolve(env.appDir, 'README.md'), + rootReadme, + docsDir, getModuleList: this.options.getModuleList || getModuleList }); + this._fullReadme = result.fullReadme || ''; } }); compiler.hooks.beforeCompile.tapPromise('ReadmeWebpackPlugin', async (params) => { @@ -51,16 +57,18 @@ class ReadmeWebpackPlugin { }))};`); virtualModules.writeModule('node_modules/readme/index.js', readmes); }); - // production 打包结束后把 README.md 写入输出目录,避免 CDN 上仅有 remoteEntry 而无文档 + // production:写入完整聚合 README.md(与根目录 TOC 版 README 无关) compiler.hooks.afterEmit.tapPromise('ReadmeWebpackPlugin', async () => { if (compiler.options.mode !== 'production') { return; } - const readmePath = this.options.readmePath || path.resolve(env.appDir, 'README.md'); - await copyReadmeToOutput({ readmePath, outputPath: compiler.outputPath }); + await writeFullReadmeToOutput({ + content: this._fullReadme, + outputPath: compiler.outputPath + }); }); } } module.exports = ReadmeWebpackPlugin; -module.exports.copyReadmeToOutput = copyReadmeToOutput; +module.exports.writeFullReadmeToOutput = writeFullReadmeToOutput; diff --git a/package.json b/package.json index d951283..0b3b36c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/modules-dev", - "version": "2.4.8", + "version": "2.4.9", "description": "用于辅助在项目内启动一个规范化组件开发的环境", "publishConfig": { "access": "public", diff --git a/test/build-component-docs.test.js b/test/build-component-docs.test.js index 10f305b..9f9388f 100644 --- a/test/build-component-docs.test.js +++ b/test/build-component-docs.test.js @@ -3,28 +3,46 @@ const os = require('os'); const fs = require('fs-extra'); const { expect } = require('chai'); const buildComponentDocs = require('../lib/build-component-docs'); -const { DOC_MD_START, DOC_MD_END } = buildComponentDocs; +const { DOC_MD_START, DOC_MD_END, toPascalName } = buildComponentDocs; + +describe('toPascalName', () => { + it('应将 kebab 转为大驼峰', () => { + expect(toPascalName('button-group')).to.equal('ButtonGroup'); + expect(toPascalName('FormInfo')).to.equal('FormInfo'); + }); +}); describe('buildComponentDocs', () => { let tempDir; + const createComponent = async (componentsDir, name, { summary, description } = {}) => { + const base = path.join(componentsDir, name); + await fs.ensureDir(path.join(base, 'doc')); + await fs.writeFile(path.join(base, 'index.js'), 'module.exports = {};\n'); + await fs.writeJson(path.join(base, 'package.json'), { + name: `@test/${String(name).toLowerCase()}`, + description: description || `${name} desc`, + version: '1.0.0' + }); + await fs.writeFile(path.join(base, 'doc/summary.md'), summary || `${name} summary`); + await fs.writeFile(path.join(base, 'doc/api.md'), `${name} api`); + await fs.writeJson(path.join(base, 'doc/example.json'), { list: [] }); + }; + beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'modules-dev-build-docs-')); const componentsDir = path.join(tempDir, 'src/components'); - for (const name of ['Beta', 'Alpha']) { - const base = path.join(componentsDir, name); - await fs.ensureDir(path.join(base, 'doc')); - await fs.writeFile(path.join(base, 'index.js'), 'module.exports = {};\n'); - await fs.writeJson(path.join(base, 'package.json'), { - name: `@test/${name.toLowerCase()}`, - description: `${name} desc`, - version: '1.0.0' - }); - await fs.writeFile(path.join(base, 'doc/summary.md'), `${name} summary`); - await fs.writeFile(path.join(base, 'doc/api.md'), `${name} api`); - await fs.writeJson(path.join(base, 'doc/example.json'), { list: [] }); - } + await createComponent(componentsDir, 'Beta'); + await createComponent(componentsDir, 'Alpha'); + await createComponent(componentsDir, 'button-group', { + summary: 'Button group summary text', + description: 'fallback desc' + }); + + // 旧的非大驼峰残留,应被清理 + await fs.ensureDir(path.join(tempDir, 'docs')); + await fs.writeFile(path.join(tempDir, 'docs', 'button-group.md'), 'stale\n'); await fs.writeFile( path.join(tempDir, 'README.md'), @@ -36,31 +54,51 @@ describe('buildComponentDocs', () => { await fs.remove(tempDir); }); - it('应生成各组件 README 并替换根 README 的 DOC_MD', async () => { + it('应生成 docs/{PascalName}.md 且根 README DOC_MD 仅为目录表', async () => { const moduleBaseDir = path.join(tempDir, 'src/components'); const rootReadme = path.join(tempDir, 'README.md'); + const docsDir = path.join(tempDir, 'docs'); - const result = await buildComponentDocs({ moduleBaseDir, rootReadme }); + const result = await buildComponentDocs({ moduleBaseDir, rootReadme, docsDir }); - expect(result.list.map(item => item.name)).to.deep.equal(['Alpha', 'Beta']); + expect(result.list.map((item) => item.name)).to.deep.equal(['Alpha', 'Beta', 'button-group']); expect(await fs.exists(path.join(moduleBaseDir, 'Alpha/README.md'))).to.equal(true); - expect(await fs.exists(path.join(moduleBaseDir, 'Beta/README.md'))).to.equal(true); + expect(await fs.exists(path.join(docsDir, 'Alpha.md'))).to.equal(true); + expect(await fs.exists(path.join(docsDir, 'Beta.md'))).to.equal(true); + expect(await fs.exists(path.join(docsDir, 'ButtonGroup.md'))).to.equal(true); + expect(await fs.exists(path.join(docsDir, 'button-group.md'))).to.equal(false); - const alphaReadme = await fs.readFile(path.join(moduleBaseDir, 'Alpha/README.md'), 'utf8'); - const betaReadme = await fs.readFile(path.join(moduleBaseDir, 'Beta/README.md'), 'utf8'); - const root = await fs.readFile(rootReadme, 'utf8'); + const alphaDocs = await fs.readFile(path.join(docsDir, 'Alpha.md'), 'utf8'); + const buttonGroupDocs = await fs.readFile(path.join(docsDir, 'ButtonGroup.md'), 'utf8'); + expect(alphaDocs).to.include('Alpha'); + expect(buttonGroupDocs.length).to.be.greaterThan(20); + const root = await fs.readFile(rootReadme, 'utf8'); expect(root).to.include('# Root'); expect(root).to.include('intro'); expect(root).to.include(DOC_MD_START); expect(root).to.include(DOC_MD_END); expect(root).to.not.include('\nold\n'); - expect(root).to.include(alphaReadme.trim()); - expect(root).to.include(betaReadme.trim()); + expect(root).to.include('[Alpha](docs/Alpha.md)'); + expect(root).to.include('[ButtonGroup](docs/ButtonGroup.md)'); + expect(root).to.include('Button group summary text'); + // 不应再嵌入组件全文 + expect(root).to.not.include(alphaDocs.trim()); const start = root.indexOf(DOC_MD_START); const end = root.indexOf(DOC_MD_END); const section = root.slice(start + DOC_MD_START.length, end); expect(section.indexOf('Alpha')).to.be.lessThan(section.indexOf('Beta')); + expect(section.indexOf('Beta')).to.be.lessThan(section.indexOf('ButtonGroup')); + + // fullReadme:完整聚合,与根 README 无关 + expect(result.fullReadme).to.include(DOC_MD_START); + expect(result.fullReadme).to.include(DOC_MD_END); + expect(result.fullReadme).to.include(alphaDocs.trim()); + expect(result.fullReadme).to.include(buttonGroupDocs.trim()); + expect(result.fullReadme.indexOf(alphaDocs.trim())).to.be.lessThan( + result.fullReadme.indexOf(buttonGroupDocs.trim()) + ); + expect(root).to.not.equal(result.fullReadme); }); }); diff --git a/test/readme-webpack-plugin.test.js b/test/readme-webpack-plugin.test.js index 9eb858a..1d99049 100644 --- a/test/readme-webpack-plugin.test.js +++ b/test/readme-webpack-plugin.test.js @@ -2,39 +2,34 @@ const path = require('path'); const os = require('os'); const fs = require('fs-extra'); const { expect } = require('chai'); -const { copyReadmeToOutput } = require('../lib/readme-webpack-plugin'); +const { writeFullReadmeToOutput } = require('../lib/readme-webpack-plugin'); -describe('copyReadmeToOutput', () => { +describe('writeFullReadmeToOutput', () => { let tempDir; beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'modules-dev-readme-copy-')); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'modules-dev-readme-write-')); }); afterEach(async () => { await fs.remove(tempDir); }); - it('应将 README.md 拷贝到输出目录(与 remoteEntry 同级)', async () => { - const readmePath = path.join(tempDir, 'README.md'); + it('应将完整文档写入输出目录 README.md', async () => { const outputPath = path.join(tempDir, 'build'); - await fs.writeFile(readmePath, '# Package docs\n'); + const content = '\n\n# FormInfo\n\nfull\n\n\n'; - const ok = await copyReadmeToOutput({ readmePath, outputPath }); + const ok = await writeFullReadmeToOutput({ content, outputPath }); expect(ok).to.equal(true); expect(await fs.exists(path.join(outputPath, 'README.md'))).to.equal(true); - expect(await fs.readFile(path.join(outputPath, 'README.md'), 'utf8')).to.equal('# Package docs\n'); + expect(await fs.readFile(path.join(outputPath, 'README.md'), 'utf8')).to.equal(content); }); - it('源 README 不存在时返回 false 且不写文件', async () => { + it('内容为空时返回 false 且不写文件', async () => { const outputPath = path.join(tempDir, 'build'); - const ok = await copyReadmeToOutput({ - readmePath: path.join(tempDir, 'missing.md'), - outputPath - }); - - expect(ok).to.equal(false); + expect(await writeFullReadmeToOutput({ content: '', outputPath })).to.equal(false); + expect(await writeFullReadmeToOutput({ content: null, outputPath })).to.equal(false); expect(await fs.exists(path.join(outputPath, 'README.md'))).to.equal(false); }); });