Skip to content

Commit 01b18ce

Browse files
committed
修复excludePaths无效的问题
1 parent 366ab1e commit 01b18ce

1 file changed

Lines changed: 129 additions & 113 deletions

File tree

scripts/generate-docs-rss.js

Lines changed: 129 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -12,141 +12,178 @@ const SITE_CONFIG = {
1212
title: 'Gametoolkit - 文档 RSS',
1313
description: 'Gametoolkit - 文档 RSS',
1414
language: 'zh-Hans',
15-
maxItems: 15, // RSS生成的最大文章个数
16-
excludePaths: [ // 需要排除的路径(支持glob通配符、绝对/相对路径、处理后的路径)
15+
maxItems: 15,
16+
excludePaths: [
1717
'docs/01-index.md',
1818
],
1919
};
2020

21-
// Docs 源文件目录(你的 Markdown 文档目录
22-
const DOCS_SRC_DIR = path.join(__dirname, '../docs');
23-
// RSS 输出路径
24-
const OUTPUT_PATH = path.join(__dirname, '../build/docs/rss.xml');
21+
// 项目根目录(统一基准
22+
const PROJECT_ROOT = path.resolve(__dirname, '..');
23+
const DOCS_SRC_DIR = path.join(PROJECT_ROOT, 'docs');
24+
const OUTPUT_PATH = path.join(PROJECT_ROOT, 'build/docs/rss.xml');
2525

2626
/**
27-
* 工具函数:将路径转换为POSIX风格的绝对路径(跨平台统一)
28-
* @param {string} filePath 任意路径(相对/绝对、POSIX/Windows)
29-
* @param {string} baseDir 基准目录(默认当前工作目录)
30-
* @returns {string} POSIX风格的绝对路径(/分隔符)
27+
* 工具函数:转换为POSIX风格的绝对路径(跨平台统一)
3128
*/
32-
function toPosixAbsolutePath(filePath, baseDir = process.cwd()) {
33-
// 先解析为系统原生的绝对路径,再转换为POSIX风格并规范化
29+
function toPosixAbsolutePath(filePath, baseDir = PROJECT_ROOT) {
3430
const absolutePath = path.resolve(baseDir, filePath);
3531
return path.posix.normalize(absolutePath.replace(/\\/g, '/'));
3632
}
3733

3834
/**
39-
* 工具函数:移除字符串开头的数字-前缀(如 01-、123-)
40-
* @param {string} str 原始字符串(文件名或路径片段)
41-
* @returns {string} 处理后的字符串
35+
* 工具函数:判断路径是否为文件(支持存在/不存在的路径,结合glob规则)
36+
* @param {string} filePath 路径(绝对/相对)
37+
* @returns {boolean} true=文件,false=文件夹/不存在/glob规则
38+
*/
39+
function isFilePath(filePath) {
40+
// 排除glob通配符路径
41+
if (glob.hasMagic(filePath)) return false;
42+
const absolutePath = toPosixAbsolutePath(filePath);
43+
try {
44+
// 存在的路径:判断是否为文件
45+
return fs.statSync(absolutePath).isFile();
46+
} catch (err) {
47+
// 不存在的路径:根据扩展名判断(如.md/.js为文件,无扩展名为文件夹)
48+
return path.extname(absolutePath) !== '';
49+
}
50+
}
51+
52+
/**
53+
* 工具函数:移除字符串开头的数字-前缀
4254
*/
4355
function removeLeadingNumberPrefix(str) {
44-
// 正则匹配开头的一个或多个数字 + 连字符,替换为空
4556
return str.replace(/^\d+-/, '');
4657
}
4758

4859
/**
49-
* 工具函数:移除路径末尾的index(包括/index或index)
50-
* @param {string} pathStr 原始路径
51-
* @returns {string} 处理后的路径
60+
* 工具函数:移除路径末尾的index
5261
*/
5362
function removeTrailingIndex(pathStr) {
54-
// 匹配末尾的 /index 或 index(支持带/和不带/的情况)
5563
return pathStr.replace(/(\/|^)index$/, '');
5664
}
5765

5866
/**
59-
* 工具函数:判断路径是否匹配排除规则(支持原始文件路径、处理后的路径)
60-
* @param {string} pathToCheck 要检查的路径(可以是原始文件路径、处理后的相对路径)
61-
* @param {string[]} excludePaths 排除的路径规则(支持glob通配符)
62-
* @param {string} baseDir 基准目录(用于相对路径转换)
63-
* @returns {boolean} true表示需要排除,false表示保留
67+
* 工具函数:获取文件对应的处理后简洁路径(与生成RSS条目时的路径一致)
68+
*/
69+
function getProcessedCleanPath(filePath) {
70+
// 非文件路径直接返回空(避免非文件调用)
71+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
72+
return '';
73+
}
74+
const absoluteFilePath = toPosixAbsolutePath(filePath);
75+
const relativePath = path.relative(DOCS_SRC_DIR, absoluteFilePath).replace(/\\/g, '/');
76+
let cleanPath = relativePath.replace(/\.md$/, '');
77+
// 移除路径片段的数字前缀
78+
cleanPath = cleanPath.split('/').map(part => removeLeadingNumberPrefix(part)).join('/');
79+
// 移除末尾的index
80+
cleanPath = removeTrailingIndex(cleanPath);
81+
// 处理文件名与父文件夹同名的情况
82+
const originalFileName = path.basename(absoluteFilePath, '.md');
83+
const fileName = removeLeadingNumberPrefix(originalFileName);
84+
const parentFolderName = removeLeadingNumberPrefix(path.basename(path.dirname(absoluteFilePath)));
85+
const parentFolderRelativePath = path.relative(DOCS_SRC_DIR, path.dirname(absoluteFilePath))
86+
.replace(/\\/g, '/')
87+
.split('/')
88+
.map(part => removeLeadingNumberPrefix(part))
89+
.join('/');
90+
if (fileName === parentFolderName) {
91+
cleanPath = parentFolderRelativePath ? parentFolderRelativePath : fileName;
92+
}
93+
// 拼接docs前缀(与生成RSS时的路径一致)
94+
return path.posix.join('docs', cleanPath).replace(/\/$/, '');
95+
}
96+
97+
/**
98+
* 判断路径是否匹配排除规则(严格区分文件/文件夹规则)
6499
*/
65-
function isPathExcluded(pathToCheck, excludePaths, baseDir = __dirname) {
100+
function isPathExcluded(pathToCheck, excludePaths, baseDir = PROJECT_ROOT) {
66101
if (!excludePaths || excludePaths.length === 0) {
67102
return false;
68103
}
69104

70-
// 转换为POSIX风格的绝对路径(跨平台统一)
71105
const normalizedPath = toPosixAbsolutePath(pathToCheck, baseDir);
72-
// 系统原生的基准目录(用于glob的cwd参数,确保IO操作正确)
73106
const systemBaseDir = path.resolve(baseDir);
107+
// 仅当pathToCheck是文件时,才生成处理后路径(避免文件夹路径的误匹配)
108+
const isCheckFile = fs.existsSync(normalizedPath) ? fs.statSync(normalizedPath).isFile() : isFilePath(pathToCheck);
109+
const processedCleanPath = isCheckFile ? getProcessedCleanPath(normalizedPath) : '';
74110

75-
// 遍历排除规则,判断是否匹配
76111
for (const excludePath of excludePaths) {
77-
// 转换排除规则为POSIX风格的绝对路径
78112
const normalizedExcludePath = toPosixAbsolutePath(excludePath, baseDir);
79-
113+
// 判断排除规则的类型:文件/文件夹
114+
const isExcludeFile = isFilePath(excludePath);
115+
// 排除规则对应的处理后路径(仅文件规则需要)
116+
const excludeProcessedCleanPath = isExcludeFile ? getProcessedCleanPath(normalizedExcludePath) : '';
117+
80118
// 情况1:排除规则是glob通配符
81119
if (glob.hasMagic(excludePath)) {
82-
// glob使用系统原生路径作为cwd,匹配后转换为POSIX绝对路径
83-
const matchedPaths = glob.sync(excludePath, {
84-
cwd: systemBaseDir, // 系统原生路径,确保Linux下glob能正确查找
120+
const matchedPaths = glob.sync(excludePath, {
121+
cwd: systemBaseDir,
85122
absolute: true,
86-
nodir: true // 只匹配文件(避免文件夹匹配)
87-
}).map(p => toPosixAbsolutePath(p)); // 统一转换为POSIX风格路径
88-
89-
// 检查当前路径是否在匹配结果中,或是否是匹配路径的父路径
90-
if (matchedPaths.includes(normalizedPath) ||
91-
matchedPaths.some(p => normalizedPath.startsWith(p.replace(/[^/]+$/, '') + '/'))) {
123+
nodir: true // 只匹配文件,避免文件夹匹配
124+
}).map(p => toPosixAbsolutePath(p));
125+
// glob规则仅精准匹配文件路径,不触发子路径
126+
if (matchedPaths.includes(normalizedPath)) {
92127
return true;
93128
}
94-
}
95-
// 情况2:排除规则是普通路径(文件或文件夹
129+
}
130+
// 情况2:排除规则是普通路径(文件/文件夹
96131
else {
97-
// 检查当前路径是否是排除文件、在排除文件夹下,或路径完全匹配
98-
if (normalizedPath === normalizedExcludePath ||
99-
normalizedPath.startsWith(normalizedExcludePath + '/')) {
100-
return true;
132+
// 子逻辑1:文件规则 → 仅允许精准匹配(原始路径 或 处理后路径)
133+
if (isExcludeFile) {
134+
const isMatched = (
135+
normalizedPath === normalizedExcludePath || // 原始文件路径精准匹配
136+
(processedCleanPath && processedCleanPath === excludeProcessedCleanPath) // 处理后路径精准匹配
137+
);
138+
if (isMatched) {
139+
return true;
140+
}
141+
}
142+
// 子逻辑2:文件夹规则 → 精准匹配 + 子路径匹配(仅文件夹规则触发)
143+
else {
144+
const isMatched = (
145+
normalizedPath === normalizedExcludePath || // 文件夹路径精准匹配
146+
normalizedPath.startsWith(normalizedExcludePath + '/') // 子路径匹配
147+
);
148+
if (isMatched) {
149+
return true;
150+
}
101151
}
102152
}
103153
}
104154
return false;
105155
}
106156

107157
/**
108-
* 工具函数:处理文件名为直接父文件夹同名的情况,仅移除末尾的文件名部分(保留父文件夹路径)
109-
* @param {string} filePath Markdown文件的绝对路径
110-
* @param {string} cleanRelativePath 处理后的文档相对路径(已移除.md后缀、统一分隔符、数字前缀)
111-
* @returns {string} 处理后的路径
158+
* 工具函数:处理文件名为直接父文件夹同名的情况
112159
*/
113160
function removeSameNameFileNamePart(filePath, cleanRelativePath) {
114-
// 获取处理后的文件名(不含后缀,已移除数字前缀)
115161
const originalFileName = path.basename(filePath, '.md');
116162
const fileName = removeLeadingNumberPrefix(originalFileName);
117-
// 获取文件的直接父文件夹的名称(处理数字前缀)
118163
const parentFolderName = removeLeadingNumberPrefix(path.basename(path.dirname(filePath)));
119-
// 获取文件的直接父文件夹的相对路径(相对于DOCS_SRC_DIR,处理数字前缀)
120164
const parentFolderRelativePath = path.relative(DOCS_SRC_DIR, path.dirname(filePath))
121165
.replace(/\\/g, '/')
122166
.split('/')
123167
.map(part => removeLeadingNumberPrefix(part))
124168
.join('/');
125169

126-
// 仅当文件名与直接父文件夹名相同时处理
127170
if (fileName === parentFolderName) {
128-
// 情况1:父文件夹是docs根目录(parentFolderRelativePath为空),则保留父文件夹名(即fileName)
129-
if (!parentFolderRelativePath) {
130-
return fileName;
131-
}
132-
// 情况2:父文件夹是子目录,返回父文件夹的相对路径(移除末尾的文件名)
133-
return parentFolderRelativePath;
171+
return parentFolderRelativePath ? parentFolderRelativePath : fileName;
134172
}
135-
136-
// 不满足条件则返回原路径
137173
return cleanRelativePath;
138174
}
139175

140176
/**
141-
* 获取文件的最后更新时间(通过 git log,若没有 git 则用文件修改时间)
177+
* 获取文件的最后更新时间
142178
*/
143179
function getFileLastUpdatedTime(filePath) {
144180
try {
145-
// 执行 git log 获取最后提交时间
146-
const log = execSync(`git log -1 --format=%cd --date=iso "${filePath}"`, { encoding: 'utf8' });
181+
const log = execSync(`git log -1 --format=%cd --date=iso "${filePath.replace(/"/g, '\\"')}"`, {
182+
encoding: 'utf8',
183+
cwd: PROJECT_ROOT
184+
});
147185
return new Date(log.trim());
148186
} catch (err) {
149-
// 没有 git 则用文件的修改时间
150187
const stats = fs.statSync(filePath);
151188
return stats.mtime;
152189
}
@@ -157,90 +194,74 @@ function getFileLastUpdatedTime(filePath) {
157194
*/
158195
async function generateDocsRSS() {
159196
try {
160-
// 1. 检查 Docs 目录是否存在
161197
if (!fs.existsSync(DOCS_SRC_DIR)) {
162198
throw new Error(`Docs 源目录不存在:${DOCS_SRC_DIR}`);
163199
}
164200

165-
// 2. 确保 build/docs 目录存在
166201
await fs.ensureDir(path.dirname(OUTPUT_PATH));
167202

168-
// 3. 遍历 Docs 目录下的所有 Markdown 文件(排除 node_modules、.git 等)
169-
const mdFiles = glob.sync(`${DOCS_SRC_DIR}/**/*.md`, {
170-
ignore: ['**/node_modules/**', '**/.git/**', '**/_*.md'], // 基础排除项
203+
// 遍历docs下的所有md文件(基于项目根目录)
204+
const mdFiles = glob.sync('docs/**/*.md', {
205+
cwd: PROJECT_ROOT,
206+
ignore: ['**/node_modules/**', '**/.git/**', '**/_*.md'],
207+
absolute: true
171208
});
172209

173210
if (mdFiles.length === 0) {
174211
throw new Error('Docs 目录下未找到 Markdown 文档');
175212
}
176213

177-
// 4. 初始化 RSS 实例(使用new URL处理路径,避免重复//)
214+
// 初始化RSS实例
178215
const siteUrl = new URL(SITE_CONFIG.baseUrl, SITE_CONFIG.url).href;
179216
const feedUrl = new URL('docs/rss.xml', siteUrl).href;
180-
181217
const feed = new RSS({
182218
title: SITE_CONFIG.title,
183219
description: SITE_CONFIG.description,
184-
site_url: siteUrl, // 处理后的完整站点URL
185-
feed_url: feedUrl, // 处理后的RSS文件URL
220+
site_url: siteUrl,
221+
feed_url: feedUrl,
186222
language: SITE_CONFIG.language,
187223
pubDate: new Date(),
188224
});
189225

190-
// 用于记录已处理的路径,避免重复
191226
const processedPaths = new Set();
192-
// 5. 处理每个 Markdown 文件
193227
const docsData = [];
228+
194229
for (const file of mdFiles) {
195230
// 第一步:过滤排除路径的文件(原始文件路径)
196-
if (isPathExcluded(file, SITE_CONFIG.excludePaths, __dirname)) {
231+
if (isPathExcluded(file, SITE_CONFIG.excludePaths)) {
197232
continue;
198233
}
199234

200-
// 读取文件内容
201235
const content = fs.readFileSync(file, 'utf8');
202-
// 解析前端matter(--- 之间的内容)
203236
const { data: frontMatter } = matter(content);
204237

205-
// 过滤:排除草稿、隐藏文档
238+
// 过滤草稿/隐藏文档
206239
if (frontMatter.draft || frontMatter.hide) {
207240
continue;
208241
}
209242

210-
// 获取原始文件名(不含后缀)
243+
// 处理文档标题
211244
const originalFileName = path.basename(file, '.md');
212-
// 处理文件名:移除开头的数字-前缀
213245
const processedFileName = removeLeadingNumberPrefix(originalFileName);
214-
// 获取文档标题(优先 frontMatter.title,其次处理后的文件名)
215246
const title = frontMatter.title || processedFileName;
216247

217-
// 计算文档的相对路径(适配 Docusaurus 的路由规则)
218-
const relativePath = path.relative(DOCS_SRC_DIR, file);
219-
// 步骤1:替换路径分隔符为/,并移除.md后缀
220-
let cleanRelativePath = relativePath.replace(/\\/g, '/').replace(/\.md$/, '');
221-
// 步骤2:拆分路径片段,逐个移除数字-前缀,再拼接(处理路径中的数字前缀)
222-
cleanRelativePath = cleanRelativePath
223-
.split('/')
224-
.map(part => removeLeadingNumberPrefix(part))
225-
.join('/');
226-
// 步骤3:处理文件名与直接父文件夹同名的情况(仅移除文件名,保留父文件夹路径)
248+
// 处理文档相对路径(适配Docusaurus路由)
249+
const relativePath = path.relative(DOCS_SRC_DIR, file).replace(/\\/g, '/');
250+
let cleanRelativePath = relativePath.replace(/\.md$/, '');
251+
cleanRelativePath = cleanRelativePath.split('/').map(part => removeLeadingNumberPrefix(part)).join('/');
227252
cleanRelativePath = removeSameNameFileNamePart(file, cleanRelativePath);
228-
// 步骤4:移除末尾的index(最后执行,避免影响路径层级)
229253
cleanRelativePath = removeTrailingIndex(cleanRelativePath);
230254

231-
// 第二步:过滤处理后的路径(包括根路径如docs)
232-
// 使用path.posix.join拼接路径,避免手动拼接的分隔符问题
255+
// 拼接处理后的路径
233256
const processedRelativePath = path.posix.join('docs', cleanRelativePath).replace(/\/$/, '');
234-
if (isPathExcluded(processedRelativePath, SITE_CONFIG.excludePaths, DOCS_SRC_DIR) || processedPaths.has(processedRelativePath)) {
257+
// 第二步:过滤已处理的路径或排除的路径
258+
if (isPathExcluded(processedRelativePath, SITE_CONFIG.excludePaths) || processedPaths.has(processedRelativePath)) {
235259
continue;
236260
}
237-
// 记录已处理的路径,避免重复
238261
processedPaths.add(processedRelativePath);
239262

240-
// 步骤5:拼接baseUrl和文档路径,形成完整的permalink(解决baseUrl缺失问题)
241-
// 注意:使用posix.join确保路径分隔符为/,且不会出现重复//
263+
// 生成永久链接和更新时间
242264
const permalink = path.posix.join(SITE_CONFIG.baseUrl, processedRelativePath);
243-
// 步骤6:获取最后更新时间
244265
const lastUpdatedAt = getFileLastUpdatedTime(file);
245266

246267
docsData.push({
@@ -251,29 +272,24 @@ async function generateDocsRSS() {
251272
});
252273
}
253274

254-
// 6. 按最后更新时间排序(最新的在前)
275+
// 按更新时间排序并截取最大数量
255276
const sortedDocs = docsData.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
256-
257-
// 7. 截取指定数量的文章(支持设置最大个数)
258277
const limitedDocs = sortedDocs.slice(0, SITE_CONFIG.maxItems);
259278

260-
// 8. 添加 RSS 条目
279+
// 添加RSS条目
261280
limitedDocs.forEach((doc) => {
262-
// 拼接完整 URL(使用new URL确保路径正确,自动处理重复//)
263281
const docUrl = new URL(doc.permalink, SITE_CONFIG.url).href;
264-
// 处理guid的路径分隔符,确保统一为/
265282
const guid = doc.permalink.replace(/\\/g, '/');
266-
267283
feed.item({
268284
title: doc.title,
269-
url: docUrl, // link标签对应的值
270-
guid: guid, // guid标签对应的值(融入baseUrl,解决缺失问题)
271-
guid_isPermaLink: false, // 显式设置isPermaLink属性
285+
url: docUrl,
286+
guid: guid,
287+
guid_isPermaLink: false,
272288
date: doc.lastUpdatedAt,
273289
});
274290
});
275291

276-
// 9. 生成 RSS XML 并写入文件
292+
// 写入RSS文件
277293
const xml = feed.xml({ indent: true });
278294
await fs.writeFile(OUTPUT_PATH, xml, 'utf8');
279295

0 commit comments

Comments
 (0)