zengen 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.github/workflows/bump-version.yml +112 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/pages.yml +1 -7
  4. package/.zen/meta.json +57 -0
  5. package/.zen/translations.json +51 -0
  6. package/dist/ai-client.d.ts +34 -0
  7. package/dist/ai-client.d.ts.map +1 -0
  8. package/dist/ai-client.js +180 -0
  9. package/dist/ai-client.js.map +1 -0
  10. package/dist/ai-processor.d.ts +51 -0
  11. package/dist/ai-processor.d.ts.map +1 -0
  12. package/dist/ai-processor.js +215 -0
  13. package/dist/ai-processor.js.map +1 -0
  14. package/dist/ai-service.d.ts +79 -0
  15. package/dist/ai-service.d.ts.map +1 -0
  16. package/dist/ai-service.js +257 -0
  17. package/dist/ai-service.js.map +1 -0
  18. package/dist/builder.d.ts +26 -2
  19. package/dist/builder.d.ts.map +1 -1
  20. package/dist/builder.js +420 -9
  21. package/dist/builder.js.map +1 -1
  22. package/dist/cli.js +45 -3
  23. package/dist/cli.js.map +1 -1
  24. package/dist/gitignore.d.ts +2 -1
  25. package/dist/gitignore.d.ts.map +1 -1
  26. package/dist/gitignore.js +21 -3
  27. package/dist/gitignore.js.map +1 -1
  28. package/dist/gitignore.test.js +82 -17
  29. package/dist/gitignore.test.js.map +1 -1
  30. package/dist/markdown.d.ts +6 -1
  31. package/dist/markdown.d.ts.map +1 -1
  32. package/dist/markdown.js +31 -9
  33. package/dist/markdown.js.map +1 -1
  34. package/dist/navigation.js +5 -5
  35. package/dist/navigation.js.map +1 -1
  36. package/dist/scanner.d.ts +26 -0
  37. package/dist/scanner.d.ts.map +1 -0
  38. package/dist/scanner.js +190 -0
  39. package/dist/scanner.js.map +1 -0
  40. package/dist/template.d.ts +6 -2
  41. package/dist/template.d.ts.map +1 -1
  42. package/dist/template.js +57 -8
  43. package/dist/template.js.map +1 -1
  44. package/dist/translation-service.d.ts +72 -0
  45. package/dist/translation-service.d.ts.map +1 -0
  46. package/dist/translation-service.js +291 -0
  47. package/dist/translation-service.js.map +1 -0
  48. package/dist/types.d.ts +35 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/docs/advanced-usage.md +39 -0
  51. package/docs/getting-started.md +26 -0
  52. package/docs/guides/best-practices.md +0 -113
  53. package/docs/guides/config.md +0 -233
  54. package/package.json +2 -1
  55. package/src/ai-client.ts +227 -0
  56. package/src/ai-processor.ts +243 -0
  57. package/src/ai-service.ts +281 -0
  58. package/src/builder.ts +543 -10
  59. package/src/cli.ts +49 -3
  60. package/src/gitignore.test.ts +82 -17
  61. package/src/gitignore.ts +23 -3
  62. package/src/markdown.ts +39 -11
  63. package/src/navigation.ts +5 -5
  64. package/src/scanner.ts +180 -0
  65. package/src/template.ts +68 -8
  66. package/src/translation-service.ts +350 -0
  67. package/src/types.ts +39 -3
  68. package/test-multilang.js +44 -0
  69. package/docs/ci/github-ci-cd.md +0 -127
  70. package/docs/guides/api.md +0 -277
package/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { config } from 'dotenv';
3
4
  import { Cli, Command, Option } from 'clipanion';
4
5
  import { ZenBuilder } from './builder';
5
6
  import { ZenConfig } from './types';
@@ -8,6 +9,9 @@ import * as fs from 'fs/promises';
8
9
  import * as fsSync from 'fs';
9
10
  import * as url from 'url';
10
11
 
12
+ // 加载 .env 文件中的环境变量
13
+ config();
14
+
11
15
  // 获取版本号 - 从 package.json 读取
12
16
  function getVersion(): string {
13
17
  try {
@@ -55,6 +59,10 @@ class BuildCommand extends BaseCommand {
55
59
  config = Option.String('-c,--config');
56
60
  baseUrl = Option.String('--base-url');
57
61
  clean = Option.Boolean('--clean');
62
+ ai = Option.Boolean('--ai', { description: 'Enable AI metadata extraction' });
63
+ lang = Option.Array('--lang', {
64
+ description: 'Target languages for translation (e.g., en-US, ja-JP)',
65
+ });
58
66
 
59
67
  static usage = Command.Usage({
60
68
  description: 'Build documentation site from Markdown files in current directory',
@@ -69,6 +77,8 @@ class BuildCommand extends BaseCommand {
69
77
  $ zengen build --watch --serve --port 8080
70
78
  $ zengen build --config zen.config.json
71
79
  $ zengen build --clean
80
+ $ zengen build --ai (requires OPENAI_API_KEY environment variable)
81
+ $ zengen build --lang en-US --lang ja-JP (translate to English and Japanese)
72
82
  `,
73
83
  });
74
84
 
@@ -81,6 +91,23 @@ class BuildCommand extends BaseCommand {
81
91
  const currentDir = process.cwd();
82
92
  const outDir = this.getOutDir();
83
93
 
94
+ // 处理 AI 配置:如果指定了 --ai 参数,启用 AI;否则使用配置中的设置
95
+ const aiConfig = {
96
+ ...config.ai,
97
+ enabled: this.ai ? true : config.ai?.enabled,
98
+ };
99
+
100
+ // 处理语言配置:命令行参数优先于配置文件
101
+ const targetLangs = this.lang && this.lang.length > 0 ? this.lang : config.i18n?.targetLangs;
102
+ const i18nConfig =
103
+ targetLangs && targetLangs.length > 0
104
+ ? {
105
+ ...config.i18n,
106
+ sourceLang: config.i18n?.sourceLang || 'zh-Hans', // 默认源语言为中文
107
+ targetLangs,
108
+ }
109
+ : undefined;
110
+
84
111
  // 合并命令行参数和配置
85
112
  const buildOptions = {
86
113
  srcDir: currentDir,
@@ -92,12 +119,20 @@ class BuildCommand extends BaseCommand {
92
119
  host: this.host,
93
120
  verbose: this.verbose,
94
121
  baseUrl: this.baseUrl || config.baseUrl,
122
+ langs: this.lang,
123
+ };
124
+
125
+ // 创建最终的配置,包含 AI 和 i18n 设置
126
+ const finalConfig = {
127
+ ...config,
128
+ ai: aiConfig.enabled ? aiConfig : undefined,
129
+ i18n: i18nConfig,
95
130
  };
96
131
 
97
- const builder = new ZenBuilder(config);
132
+ const builder = new ZenBuilder(finalConfig);
98
133
 
99
134
  // 验证配置
100
- const errors = builder.validateConfig(config);
135
+ const errors = builder.validateConfig(finalConfig);
101
136
  if (errors.length > 0) {
102
137
  this.context.stderr.write('❌ Configuration errors:\n');
103
138
  errors.forEach(error => this.context.stderr.write(` - ${error}\n`));
@@ -121,7 +156,18 @@ class BuildCommand extends BaseCommand {
121
156
  if (this.watch) {
122
157
  await builder.watch(buildOptions);
123
158
  } else {
124
- await builder.build(buildOptions);
159
+ // 如果指定了语言参数,使用多语言构建
160
+ if (this.lang && this.lang.length > 0) {
161
+ const multiLangOptions = {
162
+ ...buildOptions,
163
+ langs: this.lang,
164
+ useMetaData: true,
165
+ filterOrphans: true,
166
+ };
167
+ await builder.buildMultiLang(multiLangOptions);
168
+ } else {
169
+ await builder.build(buildOptions);
170
+ }
125
171
  }
126
172
 
127
173
  return 0;
@@ -35,10 +35,14 @@ describe('GitIgnoreProcessor', () => {
35
35
  assert.strictEqual(processor['baseDir'], '/test/dir');
36
36
  });
37
37
 
38
- it('应该初始化空模式数组', () => {
38
+ it('应该初始化包含硬编码模式的数组', () => {
39
39
  const processor = new GitIgnoreProcessor('/test/dir');
40
40
  const patterns = processor.getPatterns();
41
- assert.deepStrictEqual(patterns, []);
41
+ // 检查是否包含硬编码模式
42
+ assert.ok(patterns.includes('node_modules'));
43
+ assert.ok(patterns.includes('**/node_modules'));
44
+ assert.ok(patterns.includes('.git'));
45
+ assert.ok(patterns.includes('.zen'));
42
46
  });
43
47
  });
44
48
 
@@ -57,15 +61,28 @@ dist/
57
61
  await processor.loadFromFile('.gitignore');
58
62
 
59
63
  const patterns = processor.getPatterns();
60
- assert.deepStrictEqual(patterns, ['**/node_modules', '*.log', '**/dist', '**/.DS_Store']);
64
+ // 应该包含硬编码模式和从.gitignore加载的模式
65
+ assert.ok(patterns.includes('node_modules')); // 硬编码
66
+ assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
67
+ assert.ok(patterns.includes('*.log')); // 从.gitignore加载
68
+ assert.ok(patterns.includes('**/dist')); // 从.gitignore加载
69
+ assert.ok(patterns.includes('**/.DS_Store')); // 从.gitignore加载
70
+ assert.ok(patterns.includes('.git')); // 硬编码
71
+ assert.ok(patterns.includes('.zen')); // 硬编码
61
72
  });
62
73
 
63
- it('当.gitignore文件不存在时应该使用空模式', async () => {
74
+ it('当.gitignore文件不存在时应该使用硬编码模式', async () => {
64
75
  const processor = new GitIgnoreProcessor(testDir);
65
76
  await processor.loadFromFile('non-existent-file');
66
77
 
67
78
  const patterns = processor.getPatterns();
68
- assert.deepStrictEqual(patterns, []);
79
+ // 应该只包含硬编码模式
80
+ assert.ok(patterns.includes('node_modules'));
81
+ assert.ok(patterns.includes('**/node_modules'));
82
+ assert.ok(patterns.includes('.git'));
83
+ assert.ok(patterns.includes('.zen'));
84
+ // 不应该有用户定义的模式
85
+ assert.ok(!patterns.includes('*.log'));
69
86
  });
70
87
 
71
88
  it('应该跳过空行和注释', async () => {
@@ -83,7 +100,12 @@ node_modules/
83
100
  await processor.loadFromFile('.gitignore');
84
101
 
85
102
  const patterns = processor.getPatterns();
86
- assert.deepStrictEqual(patterns, ['**/node_modules', '*.log']);
103
+ // 应该包含硬编码模式和从.gitignore加载的模式
104
+ assert.ok(patterns.includes('node_modules')); // 硬编码
105
+ assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
106
+ assert.ok(patterns.includes('*.log')); // 从.gitignore加载
107
+ assert.ok(patterns.includes('.git')); // 硬编码
108
+ assert.ok(patterns.includes('.zen')); // 硬编码
87
109
  });
88
110
 
89
111
  it('应该跳过否定模式', async () => {
@@ -98,7 +120,12 @@ node_modules/
98
120
  await processor.loadFromFile('.gitignore');
99
121
 
100
122
  const patterns = processor.getPatterns();
101
- assert.deepStrictEqual(patterns, ['**/node_modules', '*.log']);
123
+ // 应该包含硬编码模式和从.gitignore加载的模式(不包括否定模式)
124
+ assert.ok(patterns.includes('node_modules')); // 硬编码
125
+ assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
126
+ assert.ok(patterns.includes('*.log')); // 从.gitignore加载
127
+ assert.ok(patterns.includes('.git')); // 硬编码
128
+ assert.ok(patterns.includes('.zen')); // 硬编码
102
129
  });
103
130
  });
104
131
 
@@ -112,7 +139,12 @@ dist/
112
139
  processor['parsePatterns'](content);
113
140
 
114
141
  const patterns = processor.getPatterns();
115
- assert.deepStrictEqual(patterns, ['**/node_modules', '**/dist']);
142
+ // 应该包含硬编码模式和解析的模式
143
+ assert.ok(patterns.includes('node_modules')); // 硬编码
144
+ assert.ok(patterns.includes('**/node_modules')); // 从内容解析
145
+ assert.ok(patterns.includes('**/dist')); // 从内容解析
146
+ assert.ok(patterns.includes('.git')); // 硬编码
147
+ assert.ok(patterns.includes('.zen')); // 硬编码
116
148
  });
117
149
 
118
150
  it('应该正确处理文件扩展名模式', () => {
@@ -124,7 +156,12 @@ dist/
124
156
  processor['parsePatterns'](content);
125
157
 
126
158
  const patterns = processor.getPatterns();
127
- assert.deepStrictEqual(patterns, ['*.log', '*.tmp']);
159
+ // 应该包含硬编码模式和解析的模式
160
+ assert.ok(patterns.includes('node_modules')); // 硬编码
161
+ assert.ok(patterns.includes('*.log')); // 从内容解析
162
+ assert.ok(patterns.includes('*.tmp')); // 从内容解析
163
+ assert.ok(patterns.includes('.git')); // 硬编码
164
+ assert.ok(patterns.includes('.zen')); // 硬编码
128
165
  });
129
166
 
130
167
  it('应该处理以/开头的模式', () => {
@@ -136,7 +173,11 @@ dist/
136
173
  processor['parsePatterns'](content);
137
174
 
138
175
  const patterns = processor.getPatterns();
139
- assert.deepStrictEqual(patterns, ['node_modules', 'dist']);
176
+ // 应该包含硬编码模式和解析的模式
177
+ assert.ok(patterns.includes('node_modules')); // 硬编码
178
+ assert.ok(patterns.includes('dist')); // 从内容解析
179
+ assert.ok(patterns.includes('.git')); // 硬编码
180
+ assert.ok(patterns.includes('.zen')); // 硬编码
140
181
  });
141
182
 
142
183
  it('应该处理包含通配符的模式', () => {
@@ -148,7 +189,12 @@ dist/
148
189
  processor['parsePatterns'](content);
149
190
 
150
191
  const patterns = processor.getPatterns();
151
- assert.deepStrictEqual(patterns, ['**/node_modules', '**/.vscode']);
192
+ // 应该包含硬编码模式和解析的模式
193
+ assert.ok(patterns.includes('node_modules')); // 硬编码
194
+ assert.ok(patterns.includes('**/node_modules')); // 从内容解析
195
+ assert.ok(patterns.includes('**/.vscode')); // 从内容解析
196
+ assert.ok(patterns.includes('.git')); // 硬编码
197
+ assert.ok(patterns.includes('.zen')); // 硬编码
152
198
  });
153
199
  });
154
200
 
@@ -218,21 +264,35 @@ dist/
218
264
  const processor = new GitIgnoreProcessor(testDir);
219
265
 
220
266
  processor.addPattern('custom-pattern');
221
- assert.deepStrictEqual(processor.getPatterns(), ['custom-pattern']);
267
+ const patterns = processor.getPatterns();
268
+ // 应该包含硬编码模式和自定义模式
269
+ assert.ok(patterns.includes('node_modules')); // 硬编码
270
+ assert.ok(patterns.includes('custom-pattern')); // 自定义
222
271
 
223
272
  processor.addPattern('another-pattern');
224
- assert.deepStrictEqual(processor.getPatterns(), ['custom-pattern', 'another-pattern']);
273
+ const updatedPatterns = processor.getPatterns();
274
+ assert.ok(updatedPatterns.includes('node_modules')); // 硬编码
275
+ assert.ok(updatedPatterns.includes('custom-pattern')); // 自定义
276
+ assert.ok(updatedPatterns.includes('another-pattern')); // 自定义
225
277
  });
226
278
 
227
- it('应该能够清除所有模式', () => {
279
+ it('应该能够清除所有用户模式(但保留硬编码模式)', () => {
228
280
  const processor = new GitIgnoreProcessor(testDir);
229
281
 
230
282
  processor.addPattern('pattern1');
231
283
  processor.addPattern('pattern2');
232
- assert.strictEqual(processor.getPatterns().length, 2);
284
+ // 总模式数 = 硬编码模式数 + 用户模式数
285
+ const totalPatterns = processor.getPatterns().length;
286
+ assert.ok(totalPatterns >= 2); // 至少包含硬编码模式
233
287
 
234
288
  processor.clearPatterns();
235
- assert.deepStrictEqual(processor.getPatterns(), []);
289
+ const patternsAfterClear = processor.getPatterns();
290
+ // 清除后应该只包含硬编码模式
291
+ assert.ok(patternsAfterClear.includes('node_modules'));
292
+ assert.ok(patternsAfterClear.includes('.git'));
293
+ assert.ok(patternsAfterClear.includes('.zen'));
294
+ assert.ok(!patternsAfterClear.includes('pattern1'));
295
+ assert.ok(!patternsAfterClear.includes('pattern2'));
236
296
  });
237
297
  });
238
298
 
@@ -247,7 +307,12 @@ node_modules/
247
307
  const processor = await createGitIgnoreProcessor(testDir);
248
308
  const patterns = processor.getPatterns();
249
309
 
250
- assert.deepStrictEqual(patterns, ['**/node_modules', '*.log']);
310
+ // 应该包含硬编码模式和从.gitignore加载的模式
311
+ assert.ok(patterns.includes('node_modules')); // 硬编码
312
+ assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
313
+ assert.ok(patterns.includes('*.log')); // 从.gitignore加载
314
+ assert.ok(patterns.includes('.git')); // 硬编码
315
+ assert.ok(patterns.includes('.zen')); // 硬编码
251
316
  });
252
317
  });
253
318
  });
package/src/gitignore.ts CHANGED
@@ -10,6 +10,19 @@ export class GitIgnoreProcessor {
10
10
  private patterns: string[] = [];
11
11
  private baseDir: string;
12
12
 
13
+ // 始终忽略的硬编码模式列表
14
+ private readonly hardcodedPatterns: string[] = [
15
+ 'node_modules', // Node.js 依赖目录
16
+ '**/node_modules', // 任意深度的 node_modules
17
+ '**/node_modules/**', // node_modules 中的所有内容
18
+ '.git', // Git 目录
19
+ '**/.git', // 任意深度的 .git 目录
20
+ '**/.git/**', // .git 目录中的所有内容
21
+ '.zen', // ZEN 构建输出目录
22
+ '**/.zen', // 任意深度的 .zen 目录
23
+ '**/.zen/**', // .zen 目录中的所有内容
24
+ ];
25
+
13
26
  constructor(baseDir: string) {
14
27
  this.baseDir = baseDir;
15
28
  }
@@ -90,7 +103,14 @@ export class GitIgnoreProcessor {
90
103
  // 标准化路径分隔符为 /
91
104
  const normalizedPath = relativePath.replace(/\\/g, '/');
92
105
 
93
- // 检查每个模式
106
+ // 首先检查硬编码模式(始终忽略)
107
+ for (const pattern of this.hardcodedPatterns) {
108
+ if (minimatch(normalizedPath, pattern, { dot: true })) {
109
+ return true;
110
+ }
111
+ }
112
+
113
+ // 然后检查用户定义的 .gitignore 模式
94
114
  for (const pattern of this.patterns) {
95
115
  // 首先尝试直接匹配
96
116
  if (minimatch(normalizedPath, pattern, { dot: true })) {
@@ -142,10 +162,10 @@ export class GitIgnoreProcessor {
142
162
  }
143
163
 
144
164
  /**
145
- * 获取所有忽略模式
165
+ * 获取所有忽略模式(包括硬编码模式)
146
166
  */
147
167
  getPatterns(): string[] {
148
- return [...this.patterns];
168
+ return [...this.hardcodedPatterns, ...this.patterns];
149
169
  }
150
170
 
151
171
  /**
package/src/markdown.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { marked } from 'marked';
2
2
  import hljs from 'highlight.js';
3
- import { FileInfo, MarkdownProcessor } from './types';
3
+ import { FileInfo, MarkdownProcessor, ScannedFile } from './types';
4
4
  import * as fs from 'fs/promises';
5
5
  import * as path from 'path';
6
6
  import { GitIgnoreProcessor } from './gitignore';
@@ -123,8 +123,7 @@ export class MarkdownConverter {
123
123
  const name = path.basename(filePath, ext);
124
124
 
125
125
  const fileInfo: FileInfo = {
126
- path: filePath,
127
- relativePath,
126
+ path: relativePath,
128
127
  name,
129
128
  ext,
130
129
  content,
@@ -135,14 +134,15 @@ export class MarkdownConverter {
135
134
 
136
135
  /**
137
136
  * 从目录读取所有 Markdown 文件并转换
137
+ * 保持向后兼容,但内部使用扫描逻辑
138
138
  */
139
139
  async convertDirectory(dirPath: string): Promise<FileInfo[]> {
140
- const files: FileInfo[] = [];
141
-
142
- // 创建 GitIgnoreProcessor 并加载 .gitignore 文件
140
+ // 使用扫描逻辑获取文件列表
143
141
  const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
144
142
  await gitignoreProcessor.loadFromFile();
145
143
 
144
+ const scannedFiles: ScannedFile[] = [];
145
+
146
146
  async function scanDirectory(currentPath: string) {
147
147
  const entries = await fs.readdir(currentPath, { withFileTypes: true });
148
148
 
@@ -162,23 +162,51 @@ export class MarkdownConverter {
162
162
  if (entry.isDirectory()) {
163
163
  await scanDirectory(fullPath);
164
164
  } else if (entry.isFile() && entry.name.endsWith('.md')) {
165
- const content = await fs.readFile(fullPath, 'utf-8');
166
165
  const relativePath = path.relative(dirPath, fullPath);
167
166
  const ext = path.extname(entry.name);
168
167
  const name = path.basename(entry.name, ext);
169
168
 
170
- files.push({
171
- path: fullPath,
172
- relativePath,
169
+ scannedFiles.push({
170
+ path: relativePath,
173
171
  name,
174
172
  ext,
175
- content,
176
173
  });
177
174
  }
178
175
  }
179
176
  }
180
177
 
181
178
  await scanDirectory(dirPath);
179
+
180
+ // 使用新的方法转换扫描的文件
181
+ return this.convertScannedFiles(scannedFiles, dirPath);
182
+ }
183
+
184
+ /**
185
+ * 从扫描的文件列表读取内容并转换
186
+ */
187
+ async convertScannedFiles(
188
+ scannedFiles: ScannedFile[],
189
+ baseDir: string = ''
190
+ ): Promise<FileInfo[]> {
191
+ const files: FileInfo[] = [];
192
+
193
+ for (const scannedFile of scannedFiles) {
194
+ try {
195
+ // 构建绝对路径
196
+ const absolutePath = baseDir ? path.join(baseDir, scannedFile.path) : scannedFile.path;
197
+ const content = await fs.readFile(absolutePath, 'utf-8');
198
+ files.push({
199
+ path: scannedFile.path,
200
+ name: scannedFile.name,
201
+ ext: scannedFile.ext,
202
+ content,
203
+ hash: scannedFile.hash, // 复制 hash 字段
204
+ });
205
+ } catch (error) {
206
+ console.warn(`⚠️ Failed to read file ${scannedFile.path}:`, error);
207
+ }
208
+ }
209
+
182
210
  return this.convertFiles(files);
183
211
  }
184
212
  }
package/src/navigation.ts CHANGED
@@ -20,7 +20,7 @@ export class NavigationGenerator {
20
20
  */
21
21
  generate(files: FileInfo[]): NavigationItem[] {
22
22
  // 按路径排序
23
- const sortedFiles = [...files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
23
+ const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
24
24
 
25
25
  // 构建树形结构
26
26
  const root: NavigationItem[] = [];
@@ -36,7 +36,7 @@ export class NavigationGenerator {
36
36
  * 将文件添加到导航树中
37
37
  */
38
38
  private addFileToNavigation(navigation: NavigationItem[], file: FileInfo): void {
39
- const parts = file.relativePath.split('/');
39
+ const parts = file.path.split('/');
40
40
  let currentLevel = navigation;
41
41
 
42
42
  for (let i = 0; i < parts.length; i++) {
@@ -55,7 +55,7 @@ export class NavigationGenerator {
55
55
 
56
56
  // 生成路径
57
57
  const rawPath = isMarkdownFile
58
- ? `/${file.relativePath.replace(/\.md$/, '.html')}`
58
+ ? `/${file.path.replace(/\.md$/, '.html')}`
59
59
  : `/${parts.slice(0, i + 1).join('/')}`;
60
60
  const itemPath = this.generatePath(rawPath);
61
61
 
@@ -137,7 +137,7 @@ export class NavigationGenerator {
137
137
  return files
138
138
  .map(file => {
139
139
  const title = file.metadata?.title || this.formatTitle(file.name); // 优先使用提取的标题
140
- const rawPath = `/${file.relativePath.replace(/\.md$/, '.html')}`;
140
+ const rawPath = `/${file.path.replace(/\.md$/, '.html')}`;
141
141
  const itemPath = this.generatePath(rawPath);
142
142
 
143
143
  return {
@@ -209,7 +209,7 @@ export class NavigationGenerator {
209
209
  const effectiveBaseUrl = baseUrl || this.baseUrl || 'https://example.com';
210
210
  const urls = files
211
211
  .map(file => {
212
- const path = `/${file.relativePath.replace(/\.md$/, '.html')}`;
212
+ const path = `/${file.path.replace(/\.md$/, '.html')}`;
213
213
  const lastmod = new Date().toISOString().split('T')[0];
214
214
 
215
215
  return ` <url>
package/src/scanner.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { ScannedFile, ZenConfig } from './types';
2
+ import { GitIgnoreProcessor } from './gitignore';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import * as minimatch from 'minimatch';
6
+ import * as crypto from 'crypto';
7
+
8
+ export class Scanner {
9
+ private config: ZenConfig;
10
+
11
+ constructor(config: ZenConfig = {}) {
12
+ this.config = config;
13
+ }
14
+
15
+ /**
16
+ * 计算文件内容的 sha256 hash
17
+ */
18
+ private async calculateFileHash(filePath: string): Promise<string> {
19
+ try {
20
+ const content = await fs.readFile(filePath, 'utf-8');
21
+ return crypto.createHash('sha256').update(content).digest('hex');
22
+ } catch (error) {
23
+ console.warn(`⚠️ Failed to calculate hash for ${filePath}:`, error);
24
+ return '';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 扫描目录并生成文件列表
30
+ */
31
+ async scanDirectory(dirPath: string): Promise<ScannedFile[]> {
32
+ const files: ScannedFile[] = [];
33
+
34
+ // 创建 GitIgnoreProcessor 并加载 .gitignore 文件
35
+ const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
36
+ await gitignoreProcessor.loadFromFile();
37
+
38
+ // 获取 include/exclude 模式
39
+ const includePattern = this.config.includePattern || '**/*.md';
40
+ const excludePattern = this.config.excludePattern;
41
+
42
+ const scanDirectory = async (currentPath: string) => {
43
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
44
+
45
+ for (const entry of entries) {
46
+ const fullPath = path.join(currentPath, entry.name);
47
+
48
+ // 检查是否应该被 .gitignore 忽略
49
+ if (gitignoreProcessor.shouldIgnore(fullPath)) {
50
+ continue;
51
+ }
52
+
53
+ // 忽略 .zen 目录(保持向后兼容)
54
+ if (entry.name === '.zen') {
55
+ continue;
56
+ }
57
+
58
+ if (entry.isDirectory()) {
59
+ await scanDirectory(fullPath);
60
+ } else if (entry.isFile()) {
61
+ const relativePath = path.relative(dirPath, fullPath);
62
+
63
+ // 应用 include 模式
64
+ if (!minimatch.match([relativePath], includePattern).length) {
65
+ continue;
66
+ }
67
+
68
+ // 应用 exclude 模式
69
+ if (excludePattern && minimatch.match([relativePath], excludePattern).length > 0) {
70
+ continue;
71
+ }
72
+
73
+ const ext = path.extname(entry.name);
74
+ const name = path.basename(entry.name, ext);
75
+ const hash = await this.calculateFileHash(fullPath);
76
+
77
+ files.push({
78
+ path: relativePath, // 只保存相对路径
79
+ name,
80
+ ext,
81
+ hash,
82
+ });
83
+ }
84
+ }
85
+ };
86
+
87
+ await scanDirectory(dirPath);
88
+ return files;
89
+ }
90
+
91
+ /**
92
+ * 保存扫描结果到文件
93
+ */
94
+ async saveScanResult(files: ScannedFile[], outputPath: string): Promise<void> {
95
+ // 确保输出目录存在
96
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
97
+
98
+ // 将扫描结果保存为 JSON
99
+ const scanResult = {
100
+ timestamp: new Date().toISOString(),
101
+ files,
102
+ };
103
+
104
+ await fs.writeFile(outputPath, JSON.stringify(scanResult, null, 2), 'utf-8');
105
+ }
106
+
107
+ /**
108
+ * 从文件加载扫描结果
109
+ */
110
+ async loadScanResult(inputPath: string): Promise<ScannedFile[]> {
111
+ try {
112
+ const content = await fs.readFile(inputPath, 'utf-8');
113
+ const scanResult = JSON.parse(content);
114
+ return scanResult.files || [];
115
+ } catch (error) {
116
+ console.warn(`⚠️ Failed to load scan result from ${inputPath}:`, error);
117
+ return [];
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 检查扫描结果是否过期
123
+ */
124
+ async isScanResultOutdated(scanResultPath: string, dirPath: string): Promise<boolean> {
125
+ try {
126
+ // 检查扫描结果文件是否存在
127
+ await fs.access(scanResultPath);
128
+
129
+ // 读取扫描结果的时间戳
130
+ const content = await fs.readFile(scanResultPath, 'utf-8');
131
+ const scanResult = JSON.parse(content);
132
+ const scanTime = new Date(scanResult.timestamp).getTime();
133
+
134
+ // 检查源目录中是否有文件比扫描时间更新
135
+ const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
136
+ await gitignoreProcessor.loadFromFile();
137
+
138
+ let isOutdated = false;
139
+
140
+ async function checkDirectory(currentPath: string) {
141
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
142
+
143
+ for (const entry of entries) {
144
+ const fullPath = path.join(currentPath, entry.name);
145
+
146
+ // 检查是否应该被 .gitignore 忽略
147
+ if (gitignoreProcessor.shouldIgnore(fullPath)) {
148
+ continue;
149
+ }
150
+
151
+ // 忽略 .zen 目录
152
+ if (entry.name === '.zen') {
153
+ continue;
154
+ }
155
+
156
+ if (entry.isDirectory()) {
157
+ await checkDirectory(fullPath);
158
+ } else if (entry.isFile()) {
159
+ // 检查文件修改时间
160
+ const stats = await fs.stat(fullPath);
161
+ if (stats.mtime.getTime() > scanTime) {
162
+ isOutdated = true;
163
+ return; // 提前退出
164
+ }
165
+ }
166
+
167
+ if (isOutdated) {
168
+ break;
169
+ }
170
+ }
171
+ }
172
+
173
+ await checkDirectory(dirPath);
174
+ return isOutdated;
175
+ } catch (error) {
176
+ // 如果扫描结果文件不存在或读取失败,视为过期
177
+ return true;
178
+ }
179
+ }
180
+ }