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);
});
});