@chronoter/main 0.1.0
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.
- package/README.md +118 -0
- package/README.npm.md +118 -0
- package/dist/cli.js +1708 -0
- package/dist/core/media/logos/Chronoter-dark.png +0 -0
- package/dist/core/media/logos/Chronoter.png +0 -0
- package/dist/core/media/logos/Chronoter_favicon-dark.svg +1 -0
- package/dist/core/media/logos/Chronoter_favicon.svg +1 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +1418 -0
- package/dist/server/templates/index.html +12 -0
- package/package.json +92 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1708 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import ignore from 'ignore';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { dirname, resolve, join, relative, extname, basename } from 'path';
|
|
7
|
+
import { visit } from 'unist-util-visit';
|
|
8
|
+
import { compile } from '@mdx-js/mdx';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
import remarkGfm from 'remark-gfm';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import chalk2 from 'chalk';
|
|
14
|
+
import { createServer, defineConfig } from 'vite';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import react from '@vitejs/plugin-react';
|
|
17
|
+
import mdx from '@mdx-js/rollup';
|
|
18
|
+
import { realpathSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
19
|
+
import { watch } from 'chokidar';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { createInterface } from 'readline';
|
|
22
|
+
import { spawn } from 'child_process';
|
|
23
|
+
|
|
24
|
+
var __defProp = Object.defineProperty;
|
|
25
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
26
|
+
var __esm = (fn, res) => function __init() {
|
|
27
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
28
|
+
};
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
32
|
+
};
|
|
33
|
+
var DEFAULT_IGNORE_PATTERNS, IgnorePatterns;
|
|
34
|
+
var init_ignore_patterns = __esm({
|
|
35
|
+
"src/core/routing/ignore-patterns.ts"() {
|
|
36
|
+
DEFAULT_IGNORE_PATTERNS = [
|
|
37
|
+
// 依存関係
|
|
38
|
+
"node_modules/**",
|
|
39
|
+
"bower_components/**",
|
|
40
|
+
// ビルド出力
|
|
41
|
+
"dist/**",
|
|
42
|
+
"build/**",
|
|
43
|
+
"out/**",
|
|
44
|
+
".next/**",
|
|
45
|
+
".nuxt/**",
|
|
46
|
+
".output/**",
|
|
47
|
+
// バージョン管理
|
|
48
|
+
".git/**",
|
|
49
|
+
".svn/**",
|
|
50
|
+
".hg/**",
|
|
51
|
+
// IDE/エディタ
|
|
52
|
+
".vscode/**",
|
|
53
|
+
".idea/**",
|
|
54
|
+
"*.swp",
|
|
55
|
+
"*.swo",
|
|
56
|
+
"*~",
|
|
57
|
+
// OS
|
|
58
|
+
".DS_Store",
|
|
59
|
+
"Thumbs.db",
|
|
60
|
+
// テスト/カバレッジ
|
|
61
|
+
"coverage/**",
|
|
62
|
+
".nyc_output/**",
|
|
63
|
+
// キャッシュ
|
|
64
|
+
".cache/**",
|
|
65
|
+
".temp/**",
|
|
66
|
+
".tmp/**",
|
|
67
|
+
// ログ
|
|
68
|
+
"*.log",
|
|
69
|
+
"logs/**"
|
|
70
|
+
];
|
|
71
|
+
IgnorePatterns = class {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.ig = ignore();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 除外パターンを初期化
|
|
77
|
+
*
|
|
78
|
+
* 優先順位:
|
|
79
|
+
* 1. デフォルト除外パターン(常に適用)
|
|
80
|
+
* 2. .gitignoreの内容(baseDirのルートに存在する場合)
|
|
81
|
+
* 3. chronoter.config.jsonのignoreフィールド(存在する場合)
|
|
82
|
+
*
|
|
83
|
+
* .gitignoreの読み込み:
|
|
84
|
+
* - baseDirは通常docsDirと同じです
|
|
85
|
+
* - docsDir: "docs" の場合 → docs/.gitignoreを読み込む
|
|
86
|
+
* - docsDir: "." の場合(デフォルト) → プロジェクトルートの.gitignoreを読み込む
|
|
87
|
+
*
|
|
88
|
+
* 注意:
|
|
89
|
+
* - サブディレクトリの.gitignoreは現在サポートされていません
|
|
90
|
+
* - 必要な場合はchronoter.config.jsonのignoreフィールドで指定してください
|
|
91
|
+
*
|
|
92
|
+
* ネガティブパターン:
|
|
93
|
+
* デフォルト除外を解除したい場合は、`!` プレフィックスを使用できます。
|
|
94
|
+
* 例: `["!dist/**", "!build/**"]` でdistとbuildの除外を解除
|
|
95
|
+
*
|
|
96
|
+
* @param baseDir ベースディレクトリ(通常はdocsDir)
|
|
97
|
+
* @param customIgnore カスタム除外パターン(chronoter.config.jsonから)
|
|
98
|
+
*/
|
|
99
|
+
async initialize(baseDir, customIgnore) {
|
|
100
|
+
this.ig.add(DEFAULT_IGNORE_PATTERNS);
|
|
101
|
+
try {
|
|
102
|
+
const gitignorePath = join(baseDir, ".gitignore");
|
|
103
|
+
const gitignoreContent = await readFile(gitignorePath, "utf-8");
|
|
104
|
+
this.ig.add(gitignoreContent);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
}
|
|
107
|
+
if (customIgnore && customIgnore.length > 0) {
|
|
108
|
+
this.ig.add(customIgnore);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 指定されたパスが除外対象かどうかを判定
|
|
113
|
+
*
|
|
114
|
+
* @param relativePath ベースディレクトリからの相対パス
|
|
115
|
+
* @returns 除外対象の場合true
|
|
116
|
+
*/
|
|
117
|
+
shouldIgnore(relativePath) {
|
|
118
|
+
return this.ig.ignores(relativePath);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// src/core/mdx/errors.ts
|
|
125
|
+
var MDXError;
|
|
126
|
+
var init_errors = __esm({
|
|
127
|
+
"src/core/mdx/errors.ts"() {
|
|
128
|
+
MDXError = class _MDXError extends Error {
|
|
129
|
+
/**
|
|
130
|
+
* MDXErrorのコンストラクタ
|
|
131
|
+
* @param message エラーメッセージ
|
|
132
|
+
* @param filePath エラーが発生したファイルのパス
|
|
133
|
+
* @param line エラーが発生した行番号(オプション)
|
|
134
|
+
*/
|
|
135
|
+
constructor(message, filePath, line) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.filePath = filePath;
|
|
138
|
+
this.line = line;
|
|
139
|
+
this.name = "MDXError";
|
|
140
|
+
Object.setPrototypeOf(this, _MDXError.prototype);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* エラーメッセージを整形して返す
|
|
144
|
+
* ファイルパスと行番号を含む詳細なエラーメッセージを生成します
|
|
145
|
+
* @returns 整形されたエラーメッセージ
|
|
146
|
+
*/
|
|
147
|
+
toString() {
|
|
148
|
+
const location = this.line !== void 0 ? `${this.filePath}:${this.line}` : this.filePath;
|
|
149
|
+
return `${this.name}: ${this.message}
|
|
150
|
+
at ${location}`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
function isRelativePath(url) {
|
|
156
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (url.startsWith("/")) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function convertToAbsolutePath(relativePath, baseUrl) {
|
|
165
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
166
|
+
const cleanedPath = relativePath.startsWith("./") ? relativePath.slice(2) : relativePath;
|
|
167
|
+
return `${normalizedBaseUrl}${cleanedPath}`;
|
|
168
|
+
}
|
|
169
|
+
var remarkAbsolutePath;
|
|
170
|
+
var init_remark_absolute_path = __esm({
|
|
171
|
+
"src/core/mdx/remark-absolute-path.ts"() {
|
|
172
|
+
remarkAbsolutePath = (options) => {
|
|
173
|
+
const { baseUrl } = options;
|
|
174
|
+
return (tree) => {
|
|
175
|
+
visit(tree, (node) => {
|
|
176
|
+
const typedNode = node;
|
|
177
|
+
if (typedNode.type === "image") {
|
|
178
|
+
const imageNode = node;
|
|
179
|
+
if (isRelativePath(imageNode.url)) {
|
|
180
|
+
imageNode.url = convertToAbsolutePath(imageNode.url, baseUrl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (typedNode.type === "mdxJsxFlowElement" || typedNode.type === "mdxJsxTextElement") {
|
|
184
|
+
const jsxNode = node;
|
|
185
|
+
const tagName = jsxNode.name;
|
|
186
|
+
if (tagName === "img" || tagName === "video" || tagName === "source") {
|
|
187
|
+
jsxNode.attributes?.forEach((attr) => {
|
|
188
|
+
const typedAttr = attr;
|
|
189
|
+
if (typedAttr.type === "mdxJsxAttribute" && typedAttr.name === "src") {
|
|
190
|
+
if (typeof typedAttr.value === "string" && isRelativePath(typedAttr.value)) {
|
|
191
|
+
typedAttr.value = convertToAbsolutePath(typedAttr.value, baseUrl);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// src/core/mdx/processor.ts
|
|
204
|
+
var processor_exports = {};
|
|
205
|
+
__export(processor_exports, {
|
|
206
|
+
MDXProcessor: () => MDXProcessor
|
|
207
|
+
});
|
|
208
|
+
var MDXProcessor;
|
|
209
|
+
var init_processor = __esm({
|
|
210
|
+
"src/core/mdx/processor.ts"() {
|
|
211
|
+
init_errors();
|
|
212
|
+
init_remark_absolute_path();
|
|
213
|
+
MDXProcessor = class {
|
|
214
|
+
/**
|
|
215
|
+
* MarkdownまたはMDXファイルを処理する
|
|
216
|
+
*
|
|
217
|
+
* ファイルを読み込み、フロントマターを抽出し、MDXとしてコンパイルします。
|
|
218
|
+
* .mdと.mdxの両方のファイル形式に対応しています。
|
|
219
|
+
*
|
|
220
|
+
* @param filePath - 処理するMarkdownまたはMDXファイルのパス(.mdまたは.mdx)
|
|
221
|
+
* @param options - 処理オプション
|
|
222
|
+
* @param options.baseUrl - 画像パスを絶対パスに変換する際のベースURL(例: /cli/configuration)
|
|
223
|
+
* @returns 処理済みファイルの情報
|
|
224
|
+
* @throws {MDXError} ファイルの読み込みまたはコンパイルに失敗した場合
|
|
225
|
+
*/
|
|
226
|
+
async process(filePath, options) {
|
|
227
|
+
try {
|
|
228
|
+
const source = await readFile(filePath, "utf-8");
|
|
229
|
+
let data;
|
|
230
|
+
let content;
|
|
231
|
+
try {
|
|
232
|
+
const result = matter(source);
|
|
233
|
+
data = result.data;
|
|
234
|
+
content = result.content;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
throw new MDXError(`Failed to parse frontmatter: ${message}`, filePath);
|
|
238
|
+
}
|
|
239
|
+
const code = await this.compile(content, filePath, options?.baseUrl);
|
|
240
|
+
const slug = this.generateSlug(filePath);
|
|
241
|
+
const frontmatter = this.parseFrontmatter(data);
|
|
242
|
+
return {
|
|
243
|
+
code,
|
|
244
|
+
frontmatter,
|
|
245
|
+
slug
|
|
246
|
+
};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof MDXError) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
throw new MDXError(`Failed to process MDX file: ${message}`, filePath);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* gray-matterから取得したdataをMDXFrontmatter型に変換する
|
|
257
|
+
*
|
|
258
|
+
* asを使わずに型安全にフロントマターを処理します。
|
|
259
|
+
* gray-matterのdataは{ [key: string]: any }型なので、
|
|
260
|
+
* そのまま使用しても実行時の動作は同じですが、型の明示性を高めます。
|
|
261
|
+
*
|
|
262
|
+
* フロントマターにtitleやdescriptionが存在しない場合、
|
|
263
|
+
* デフォルト値は設定せず、undefinedを返します。
|
|
264
|
+
* デフォルト値の適用は、このデータを使用する上位層(Layout、HTMLジェネレーター等)で行います。
|
|
265
|
+
*
|
|
266
|
+
* @param data - gray-matterから取得した生データ
|
|
267
|
+
* @returns MDXFrontmatter型のオブジェクト
|
|
268
|
+
* @private
|
|
269
|
+
*/
|
|
270
|
+
parseFrontmatter(data) {
|
|
271
|
+
return {
|
|
272
|
+
title: typeof data.title === "string" ? data.title : void 0,
|
|
273
|
+
description: typeof data.description === "string" ? data.description : void 0,
|
|
274
|
+
...data
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* MarkdownまたはMDXソースコードをコンパイルする
|
|
279
|
+
*
|
|
280
|
+
* @mdx-js/mdxを使用してMarkdown/MDXをReactコンポーネントにコンパイルします。
|
|
281
|
+
* 通常のMarkdownもMDXとして処理されるため、JSXを含めることができます。
|
|
282
|
+
*
|
|
283
|
+
* @param source - MarkdownまたはMDXソースコード
|
|
284
|
+
* @param filePath - ファイルパス(エラーメッセージ用、オプション)
|
|
285
|
+
* @param baseUrl - 画像パスを絶対パスに変換する際のベースURL(例: /cli/configuration)
|
|
286
|
+
* @returns コンパイル済みのJavaScriptコード
|
|
287
|
+
* @throws {MDXError} コンパイルに失敗した場合
|
|
288
|
+
*/
|
|
289
|
+
async compile(source, filePath, baseUrl) {
|
|
290
|
+
try {
|
|
291
|
+
const remarkPlugins = [remarkGfm];
|
|
292
|
+
if (baseUrl) {
|
|
293
|
+
remarkPlugins.push([remarkAbsolutePath, { baseUrl }]);
|
|
294
|
+
}
|
|
295
|
+
const result = await compile(source, {
|
|
296
|
+
outputFormat: "function-body",
|
|
297
|
+
development: false,
|
|
298
|
+
remarkPlugins
|
|
299
|
+
});
|
|
300
|
+
return String(result);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
const lineMatch = message.match(/:(\d+):/);
|
|
304
|
+
const line = lineMatch ? parseInt(lineMatch[1], 10) : void 0;
|
|
305
|
+
throw new MDXError(
|
|
306
|
+
`Failed to compile MDX: ${message}`,
|
|
307
|
+
filePath || "unknown",
|
|
308
|
+
line
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* ファイルパスからスラッグを生成する
|
|
314
|
+
*
|
|
315
|
+
* @param filePath - ファイルパス
|
|
316
|
+
* @returns スラッグ
|
|
317
|
+
* @private
|
|
318
|
+
*/
|
|
319
|
+
generateSlug(filePath) {
|
|
320
|
+
const fileName = basename(filePath, extname(filePath));
|
|
321
|
+
return fileName;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
var RouteGenerator;
|
|
327
|
+
var init_router = __esm({
|
|
328
|
+
"src/core/routing/router.ts"() {
|
|
329
|
+
init_processor();
|
|
330
|
+
init_ignore_patterns();
|
|
331
|
+
RouteGenerator = class {
|
|
332
|
+
constructor() {
|
|
333
|
+
this.mdxProcessor = new MDXProcessor();
|
|
334
|
+
this.ignorePatterns = new IgnorePatterns();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* docsディレクトリを再帰的にスキャンしてルートを生成
|
|
338
|
+
*
|
|
339
|
+
* .mdxと.mdの両方のファイルを検出してルートを生成します。
|
|
340
|
+
* node_modulesなどの除外パターンに一致するファイル/ディレクトリは自動的にスキップされます。
|
|
341
|
+
*
|
|
342
|
+
* @param docsDir - ドキュメントディレクトリのパス
|
|
343
|
+
* @param customIgnore - カスタム除外パターン(オプション)
|
|
344
|
+
* @returns 生成されたルートの配列
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* const generator = new RouteGenerator();
|
|
349
|
+
* const routes = await generator.generateRoutes('docs');
|
|
350
|
+
* // routes = [
|
|
351
|
+
* // { path: '/', filePath: 'docs/index.mdx', frontmatter: {...} },
|
|
352
|
+
* // { path: '/guide/intro', filePath: 'docs/guide/intro.md', frontmatter: {...} }
|
|
353
|
+
* // ]
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
async generateRoutes(docsDir, customIgnore) {
|
|
357
|
+
await this.ignorePatterns.initialize(docsDir, customIgnore);
|
|
358
|
+
const routes = [];
|
|
359
|
+
await this.scanDirectory(docsDir, docsDir, routes);
|
|
360
|
+
return this.resolveDuplicatePaths(routes);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 重複するパスを解決する
|
|
364
|
+
*
|
|
365
|
+
* 同じURLパスに対して複数のファイル(.mdと.mdx)が存在する場合、
|
|
366
|
+
* .mdxファイルを優先します。
|
|
367
|
+
*
|
|
368
|
+
* @param routes - ルート配列
|
|
369
|
+
* @returns 重複を解決したルート配列
|
|
370
|
+
*/
|
|
371
|
+
resolveDuplicatePaths(routes) {
|
|
372
|
+
const pathMap = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const route of routes) {
|
|
374
|
+
const existingRoute = pathMap.get(route.path);
|
|
375
|
+
if (!existingRoute) {
|
|
376
|
+
pathMap.set(route.path, route);
|
|
377
|
+
} else {
|
|
378
|
+
const currentIsMdx = route.filePath.endsWith(".mdx");
|
|
379
|
+
const existingIsMdx = existingRoute.filePath.endsWith(".mdx");
|
|
380
|
+
if (currentIsMdx && !existingIsMdx) {
|
|
381
|
+
console.warn(
|
|
382
|
+
`Duplicate path detected: ${route.path}. Prioritizing ${route.filePath} over ${existingRoute.filePath}`
|
|
383
|
+
);
|
|
384
|
+
pathMap.set(route.path, route);
|
|
385
|
+
} else if (!currentIsMdx && existingIsMdx) {
|
|
386
|
+
console.warn(
|
|
387
|
+
`Duplicate path detected: ${route.path}. Prioritizing ${existingRoute.filePath} over ${route.filePath}`
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
console.warn(
|
|
391
|
+
`Duplicate path detected: ${route.path}. Using ${existingRoute.filePath}, ignoring ${route.filePath}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return Array.from(pathMap.values());
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* URLパスから対応するルートを検索
|
|
400
|
+
*
|
|
401
|
+
* @param urlPath - URLパス(例: /guide/intro)
|
|
402
|
+
* @param routes - 検索対象のルート配列
|
|
403
|
+
* @returns 対応するルート、見つからない場合はnull
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```typescript
|
|
407
|
+
* const generator = new RouteGenerator();
|
|
408
|
+
* const routes = await generator.generateRoutes('docs');
|
|
409
|
+
* const route = generator.resolveRoute('/guide/intro', routes);
|
|
410
|
+
* // route = { path: '/guide/intro', filePath: 'docs/guide/intro.mdx', frontmatter: {...} }
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
resolveRoute(urlPath, routes) {
|
|
414
|
+
const normalizedPath = urlPath === "/" ? "/" : urlPath.replace(/\/$/, "");
|
|
415
|
+
return routes.find((route) => route.path === normalizedPath) || null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* ディレクトリを再帰的にスキャンしてMDX/MDファイルを検出
|
|
419
|
+
*
|
|
420
|
+
* @param currentDir - 現在のディレクトリパス
|
|
421
|
+
* @param baseDir - ベースディレクトリパス(docsDir)
|
|
422
|
+
* @param routes - ルートを追加する配列
|
|
423
|
+
*/
|
|
424
|
+
async scanDirectory(currentDir, baseDir, routes) {
|
|
425
|
+
try {
|
|
426
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
429
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
430
|
+
if (this.ignorePatterns.shouldIgnore(relativePath)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (entry.isDirectory()) {
|
|
434
|
+
await this.scanDirectory(fullPath, baseDir, routes);
|
|
435
|
+
} else if (entry.isFile() && (entry.name.endsWith(".mdx") || entry.name.endsWith(".md"))) {
|
|
436
|
+
const route = await this.createRoute(fullPath, baseDir);
|
|
437
|
+
routes.push(route);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Failed to scan directory ${currentDir}: ${error instanceof Error ? error.message : String(error)}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* ファイルパスからルートを作成
|
|
448
|
+
*
|
|
449
|
+
* @param filePath - MDXまたはMDファイルのパス
|
|
450
|
+
* @param baseDir - ベースディレクトリパス
|
|
451
|
+
* @returns 生成されたルート
|
|
452
|
+
*/
|
|
453
|
+
async createRoute(filePath, baseDir) {
|
|
454
|
+
const frontmatter = await this.extractFrontmatter(filePath);
|
|
455
|
+
const urlPath = this.filePathToUrlPath(filePath, baseDir);
|
|
456
|
+
return {
|
|
457
|
+
path: urlPath,
|
|
458
|
+
filePath,
|
|
459
|
+
frontmatter
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* ファイルパスからURLパスへ変換
|
|
464
|
+
*
|
|
465
|
+
* @param filePath - MDXまたはMDファイルのパス(例: docs/guide/intro.mdx, docs/guide/intro.md)
|
|
466
|
+
* @param baseDir - ベースディレクトリパス(例: docs)
|
|
467
|
+
* @returns URLパス(例: /guide/intro)
|
|
468
|
+
*
|
|
469
|
+
* 変換ルール:
|
|
470
|
+
* - docs/index.mdx → /
|
|
471
|
+
* - docs/index.md → /
|
|
472
|
+
* - docs/guide/index.mdx → /guide
|
|
473
|
+
* - docs/guide/index.md → /guide
|
|
474
|
+
* - docs/guide/intro.mdx → /guide/intro
|
|
475
|
+
* - docs/guide/intro.md → /guide/intro
|
|
476
|
+
*/
|
|
477
|
+
filePathToUrlPath(filePath, baseDir) {
|
|
478
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
479
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, "");
|
|
480
|
+
const normalized = withoutExt.split(path.sep).join("/");
|
|
481
|
+
if (normalized === "index") {
|
|
482
|
+
return "/";
|
|
483
|
+
}
|
|
484
|
+
if (normalized.endsWith("/index")) {
|
|
485
|
+
const dirPath = normalized.replace(/\/index$/, "");
|
|
486
|
+
return `/${dirPath}`;
|
|
487
|
+
}
|
|
488
|
+
return `/${normalized}`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* MDXまたはMDファイルからフロントマターを抽出
|
|
492
|
+
*
|
|
493
|
+
* @param filePath - MDXまたはMDファイルのパス
|
|
494
|
+
* @returns 抽出されたフロントマター
|
|
495
|
+
*/
|
|
496
|
+
async extractFrontmatter(filePath) {
|
|
497
|
+
try {
|
|
498
|
+
const processed = await this.mdxProcessor.process(filePath);
|
|
499
|
+
return processed.frontmatter;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.warn(
|
|
502
|
+
`Failed to extract frontmatter from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
503
|
+
);
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// src/server/routing-middleware.ts
|
|
512
|
+
var routing_middleware_exports = {};
|
|
513
|
+
__export(routing_middleware_exports, {
|
|
514
|
+
createRoutingMiddleware: () => createRoutingMiddleware
|
|
515
|
+
});
|
|
516
|
+
var __filename$1, __dirname$1, createRoutingMiddleware, handleAllRoutesRequest, handleRouteRequest, handleMDXRequest, handle500;
|
|
517
|
+
var init_routing_middleware = __esm({
|
|
518
|
+
"src/server/routing-middleware.ts"() {
|
|
519
|
+
init_router();
|
|
520
|
+
__filename$1 = fileURLToPath(import.meta.url);
|
|
521
|
+
__dirname$1 = dirname(__filename$1);
|
|
522
|
+
createRoutingMiddleware = (server, options) => {
|
|
523
|
+
const { config, cwd } = options;
|
|
524
|
+
const routeGenerator = new RouteGenerator();
|
|
525
|
+
let routes = null;
|
|
526
|
+
let routesPromise = null;
|
|
527
|
+
const getRoutes = async () => {
|
|
528
|
+
if (routes !== null) {
|
|
529
|
+
return routes;
|
|
530
|
+
}
|
|
531
|
+
if (routesPromise !== null) {
|
|
532
|
+
return routesPromise;
|
|
533
|
+
}
|
|
534
|
+
routesPromise = (async () => {
|
|
535
|
+
const docsDir = config.docsDir ? resolve(cwd, config.docsDir) : cwd;
|
|
536
|
+
const generatedRoutes = await routeGenerator.generateRoutes(
|
|
537
|
+
docsDir,
|
|
538
|
+
config.ignore
|
|
539
|
+
);
|
|
540
|
+
routes = generatedRoutes;
|
|
541
|
+
routesPromise = null;
|
|
542
|
+
return generatedRoutes;
|
|
543
|
+
})();
|
|
544
|
+
return routesPromise;
|
|
545
|
+
};
|
|
546
|
+
const clearRoutesCache = () => {
|
|
547
|
+
routes = null;
|
|
548
|
+
routesPromise = null;
|
|
549
|
+
};
|
|
550
|
+
server.watcher.on("add", (path2) => {
|
|
551
|
+
if (path2.endsWith(".mdx") || path2.endsWith(".md")) {
|
|
552
|
+
clearRoutesCache();
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
server.watcher.on("unlink", (path2) => {
|
|
556
|
+
if (path2.endsWith(".mdx") || path2.endsWith(".md")) {
|
|
557
|
+
clearRoutesCache();
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
server.watcher.on("change", (path2) => {
|
|
561
|
+
if (path2.endsWith(".mdx") || path2.endsWith(".md")) {
|
|
562
|
+
clearRoutesCache();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
return async (req, res, next) => {
|
|
566
|
+
const url = req.url;
|
|
567
|
+
if (!url) {
|
|
568
|
+
return next();
|
|
569
|
+
}
|
|
570
|
+
const urlPath = url.split("?")[0];
|
|
571
|
+
if (urlPath === "/__chronoter_mdx__") {
|
|
572
|
+
return handleMDXRequest(req, res, cwd);
|
|
573
|
+
}
|
|
574
|
+
if (urlPath === "/__chronoter_routes__") {
|
|
575
|
+
return handleRouteRequest(req, res, getRoutes, routeGenerator);
|
|
576
|
+
}
|
|
577
|
+
if (urlPath === "/__chronoter_all_routes__") {
|
|
578
|
+
return handleAllRoutesRequest(req, res, getRoutes);
|
|
579
|
+
}
|
|
580
|
+
if (urlPath.startsWith("/@") || // Vite内部リクエスト
|
|
581
|
+
urlPath.startsWith("/node_modules") || urlPath.startsWith("/src/") || // Viteが処理するソースファイル
|
|
582
|
+
urlPath.includes(".") && !urlPath.endsWith("/") && !urlPath.startsWith("/src/")) {
|
|
583
|
+
return next();
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const allRoutes = await getRoutes();
|
|
587
|
+
const route = routeGenerator.resolveRoute(urlPath, allRoutes);
|
|
588
|
+
if (route) {
|
|
589
|
+
req.chronoterRoute = route;
|
|
590
|
+
}
|
|
591
|
+
const templatePath = __dirname$1.endsWith("dist") ? resolve(__dirname$1, "server/templates/index.html") : resolve(__dirname$1, "templates/index.html");
|
|
592
|
+
const html = await readFile(templatePath, "utf-8");
|
|
593
|
+
const transformedHtml = await server.transformIndexHtml(url, html);
|
|
594
|
+
res.statusCode = route ? 200 : 404;
|
|
595
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
596
|
+
res.end(transformedHtml);
|
|
597
|
+
return;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error("Routing middleware error:", error);
|
|
600
|
+
return handle500(res, error);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
handleAllRoutesRequest = async (req, res, getRoutes) => {
|
|
605
|
+
try {
|
|
606
|
+
const allRoutes = await getRoutes();
|
|
607
|
+
res.statusCode = 200;
|
|
608
|
+
res.setHeader("Content-Type", "application/json");
|
|
609
|
+
res.end(
|
|
610
|
+
JSON.stringify({
|
|
611
|
+
routes: allRoutes.map((route) => ({
|
|
612
|
+
path: route.path,
|
|
613
|
+
filePath: route.filePath,
|
|
614
|
+
frontmatter: route.frontmatter
|
|
615
|
+
}))
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.error("[All Routes API] Error:", error);
|
|
620
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
621
|
+
res.statusCode = 500;
|
|
622
|
+
res.setHeader("Content-Type", "application/json");
|
|
623
|
+
res.end(
|
|
624
|
+
JSON.stringify({
|
|
625
|
+
error: "Failed to fetch all routes",
|
|
626
|
+
message: errorMessage
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
handleRouteRequest = async (req, res, getRoutes, routeGenerator) => {
|
|
632
|
+
try {
|
|
633
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
634
|
+
const urlPath = url.searchParams.get("path");
|
|
635
|
+
if (!urlPath) {
|
|
636
|
+
res.statusCode = 400;
|
|
637
|
+
res.setHeader("Content-Type", "application/json");
|
|
638
|
+
res.end(JSON.stringify({ error: "Missing 'path' query parameter" }));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const allRoutes = await getRoutes();
|
|
642
|
+
const route = routeGenerator.resolveRoute(urlPath, allRoutes);
|
|
643
|
+
if (!route) {
|
|
644
|
+
res.statusCode = 404;
|
|
645
|
+
res.setHeader("Content-Type", "application/json");
|
|
646
|
+
res.end(JSON.stringify({ error: "Route not found" }));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
res.statusCode = 200;
|
|
650
|
+
res.setHeader("Content-Type", "application/json");
|
|
651
|
+
res.end(
|
|
652
|
+
JSON.stringify({
|
|
653
|
+
path: route.path,
|
|
654
|
+
filePath: route.filePath,
|
|
655
|
+
frontmatter: route.frontmatter
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.error("[Route API] Error:", error);
|
|
660
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
661
|
+
res.statusCode = 500;
|
|
662
|
+
res.setHeader("Content-Type", "application/json");
|
|
663
|
+
res.end(
|
|
664
|
+
JSON.stringify({
|
|
665
|
+
error: "Failed to fetch route",
|
|
666
|
+
message: errorMessage
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
handleMDXRequest = async (req, res, cwd) => {
|
|
672
|
+
try {
|
|
673
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
674
|
+
const filePath = url.searchParams.get("path");
|
|
675
|
+
if (!filePath) {
|
|
676
|
+
res.statusCode = 400;
|
|
677
|
+
res.setHeader("Content-Type", "application/json");
|
|
678
|
+
res.end(JSON.stringify({ error: "Missing 'path' query parameter" }));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const absolutePath = resolve(cwd, filePath);
|
|
682
|
+
if (!absolutePath.startsWith(cwd)) {
|
|
683
|
+
res.statusCode = 403;
|
|
684
|
+
res.setHeader("Content-Type", "application/json");
|
|
685
|
+
res.end(JSON.stringify({ error: "Access denied" }));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const { MDXProcessor: MDXProcessor2 } = await Promise.resolve().then(() => (init_processor(), processor_exports));
|
|
689
|
+
const processor = new MDXProcessor2();
|
|
690
|
+
const processedMDX = await processor.process(absolutePath);
|
|
691
|
+
res.statusCode = 200;
|
|
692
|
+
res.setHeader("Content-Type", "application/json");
|
|
693
|
+
res.end(
|
|
694
|
+
JSON.stringify({
|
|
695
|
+
code: processedMDX.code,
|
|
696
|
+
frontmatter: processedMDX.frontmatter,
|
|
697
|
+
slug: processedMDX.slug
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.error("[MDX API] Error:", error);
|
|
702
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
703
|
+
res.statusCode = 500;
|
|
704
|
+
res.setHeader("Content-Type", "application/json");
|
|
705
|
+
res.end(
|
|
706
|
+
JSON.stringify({
|
|
707
|
+
error: "Failed to process MDX file",
|
|
708
|
+
message: errorMessage
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
handle500 = (res, error) => {
|
|
714
|
+
res.statusCode = 500;
|
|
715
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
716
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
717
|
+
const errorStack = error instanceof Error ? error.stack : "";
|
|
718
|
+
const html = `
|
|
719
|
+
<!DOCTYPE html>
|
|
720
|
+
<html lang="ja">
|
|
721
|
+
<head>
|
|
722
|
+
<meta charset="UTF-8">
|
|
723
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
724
|
+
<title>500 - Internal Server Error</title>
|
|
725
|
+
<style>
|
|
726
|
+
body {
|
|
727
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
728
|
+
max-width: 800px;
|
|
729
|
+
margin: 0 auto;
|
|
730
|
+
padding: 2rem;
|
|
731
|
+
line-height: 1.6;
|
|
732
|
+
color: #333;
|
|
733
|
+
}
|
|
734
|
+
h1 {
|
|
735
|
+
color: #e74c3c;
|
|
736
|
+
font-size: 2.5rem;
|
|
737
|
+
margin-bottom: 0.5rem;
|
|
738
|
+
}
|
|
739
|
+
.error-code {
|
|
740
|
+
font-size: 4rem;
|
|
741
|
+
font-weight: bold;
|
|
742
|
+
color: #e74c3c;
|
|
743
|
+
margin: 0;
|
|
744
|
+
}
|
|
745
|
+
.message {
|
|
746
|
+
font-size: 1.2rem;
|
|
747
|
+
color: #666;
|
|
748
|
+
margin-bottom: 2rem;
|
|
749
|
+
}
|
|
750
|
+
.error-details {
|
|
751
|
+
background: #f5f5f5;
|
|
752
|
+
padding: 1rem;
|
|
753
|
+
border-radius: 4px;
|
|
754
|
+
border-left: 4px solid #e74c3c;
|
|
755
|
+
margin: 1rem 0;
|
|
756
|
+
}
|
|
757
|
+
.error-message {
|
|
758
|
+
color: #e74c3c;
|
|
759
|
+
font-weight: bold;
|
|
760
|
+
margin-bottom: 1rem;
|
|
761
|
+
}
|
|
762
|
+
.error-stack {
|
|
763
|
+
background: #2c3e50;
|
|
764
|
+
color: #ecf0f1;
|
|
765
|
+
padding: 1rem;
|
|
766
|
+
border-radius: 4px;
|
|
767
|
+
overflow-x: auto;
|
|
768
|
+
font-family: 'Courier New', monospace;
|
|
769
|
+
font-size: 0.9rem;
|
|
770
|
+
white-space: pre-wrap;
|
|
771
|
+
}
|
|
772
|
+
</style>
|
|
773
|
+
</head>
|
|
774
|
+
<body>
|
|
775
|
+
<p class="error-code">500</p>
|
|
776
|
+
<h1>Internal Server Error</h1>
|
|
777
|
+
<p class="message">An error occurred while processing your request.</p>
|
|
778
|
+
|
|
779
|
+
<div class="error-details">
|
|
780
|
+
<div class="error-message">${errorMessage}</div>
|
|
781
|
+
${errorStack ? `<div class="error-stack">${errorStack}</div>` : ""}
|
|
782
|
+
</div>
|
|
783
|
+
</body>
|
|
784
|
+
</html>
|
|
785
|
+
`;
|
|
786
|
+
res.end(html);
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// src/core/errors/config-error.ts
|
|
792
|
+
var ConfigError = class _ConfigError extends Error {
|
|
793
|
+
/**
|
|
794
|
+
* ConfigErrorのコンストラクタ
|
|
795
|
+
* @param message エラーメッセージ
|
|
796
|
+
* @param details エラーの詳細情報(オプション)
|
|
797
|
+
*/
|
|
798
|
+
constructor(message, details) {
|
|
799
|
+
super(message);
|
|
800
|
+
this.details = details;
|
|
801
|
+
this.name = "ConfigError";
|
|
802
|
+
Object.setPrototypeOf(this, _ConfigError.prototype);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
var siteConfigSchema = z.object({
|
|
806
|
+
title: z.string().min(1, "site.title is required"),
|
|
807
|
+
description: z.string().optional(),
|
|
808
|
+
baseUrl: z.string().url("site.baseUrl must be a valid URL").optional()
|
|
809
|
+
});
|
|
810
|
+
var navigationItemSchema = z.lazy(
|
|
811
|
+
() => z.object({
|
|
812
|
+
title: z.string().min(1, "navigation item title is required"),
|
|
813
|
+
path: z.string().optional(),
|
|
814
|
+
items: z.array(navigationItemSchema).optional()
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
var themeConfigSchema = z.object({
|
|
818
|
+
variant: z.enum(["blue", "slate", "violet", "green", "orange", "red", "rose", "zinc"]).optional(),
|
|
819
|
+
logo: z.string().optional()
|
|
820
|
+
});
|
|
821
|
+
var chronoterConfigSchema = z.object({
|
|
822
|
+
site: siteConfigSchema,
|
|
823
|
+
navigation: z.array(navigationItemSchema).optional(),
|
|
824
|
+
docsDir: z.string().min(1, "docsDir must not be empty").optional(),
|
|
825
|
+
outDir: z.string().optional(),
|
|
826
|
+
theme: themeConfigSchema.optional(),
|
|
827
|
+
ignore: z.array(z.string()).optional()
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// src/core/config/defaults.ts
|
|
831
|
+
var defaultConfig = {
|
|
832
|
+
docsDir: ".",
|
|
833
|
+
// デフォルトはルートディレクトリ
|
|
834
|
+
theme: {
|
|
835
|
+
variant: "blue"
|
|
836
|
+
// デフォルトテーマ
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
var mergeWithDefaults = (userConfig) => {
|
|
840
|
+
const site = userConfig.site;
|
|
841
|
+
return {
|
|
842
|
+
site,
|
|
843
|
+
docsDir: userConfig.docsDir ?? defaultConfig.docsDir,
|
|
844
|
+
navigation: userConfig.navigation,
|
|
845
|
+
theme: {
|
|
846
|
+
variant: userConfig.theme?.variant ?? defaultConfig.theme?.variant,
|
|
847
|
+
logo: userConfig.theme?.logo
|
|
848
|
+
},
|
|
849
|
+
ignore: userConfig.ignore
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/core/config/loader.ts
|
|
854
|
+
var ConfigLoader = class {
|
|
855
|
+
/**
|
|
856
|
+
* 指定されたディレクトリからchronoter.config.jsonを読み込む
|
|
857
|
+
*
|
|
858
|
+
* @param cwd 作業ディレクトリ(デフォルト: process.cwd())
|
|
859
|
+
* @returns 検証済みの設定オブジェクト
|
|
860
|
+
* @throws {ConfigError} 設定ファイルが見つからない、または無効な場合
|
|
861
|
+
*/
|
|
862
|
+
static async load(cwd = process.cwd()) {
|
|
863
|
+
const configPath = resolve(cwd, "chronoter.config.json");
|
|
864
|
+
try {
|
|
865
|
+
const fileContent = await readFile(configPath, "utf-8");
|
|
866
|
+
let parsedConfig;
|
|
867
|
+
try {
|
|
868
|
+
parsedConfig = JSON.parse(fileContent);
|
|
869
|
+
} catch (parseError) {
|
|
870
|
+
throw new ConfigError(
|
|
871
|
+
`Failed to parse JSON in config file: ${configPath}`,
|
|
872
|
+
parseError instanceof Error ? parseError.message : parseError
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
return this.validate(parsedConfig);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
878
|
+
throw new ConfigError(
|
|
879
|
+
`Config file not found: ${configPath}
|
|
880
|
+
Please create a chronoter.config.json file.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (error instanceof ConfigError) {
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
throw new ConfigError(
|
|
887
|
+
`Error occurred while loading config file: ${configPath}`,
|
|
888
|
+
error
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* 設定オブジェクトを検証する
|
|
894
|
+
*
|
|
895
|
+
* @param config 検証する設定オブジェクト
|
|
896
|
+
* @returns 検証済みの設定オブジェクト(デフォルト値がマージ済み)
|
|
897
|
+
* @throws {ConfigError} 設定が無効な場合
|
|
898
|
+
*/
|
|
899
|
+
static validate(config) {
|
|
900
|
+
try {
|
|
901
|
+
const validatedConfig = chronoterConfigSchema.parse(config);
|
|
902
|
+
return mergeWithDefaults(validatedConfig);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
if (error instanceof z.ZodError) {
|
|
905
|
+
const errorMessages = error.errors.map((err) => {
|
|
906
|
+
const path2 = err.path.join(".");
|
|
907
|
+
return ` - ${path2}: ${err.message}`;
|
|
908
|
+
}).join("\n");
|
|
909
|
+
throw new ConfigError(
|
|
910
|
+
"Config validation failed:\n" + errorMessages,
|
|
911
|
+
error.errors
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
throw new ConfigError(
|
|
915
|
+
"Unexpected error occurred during config validation",
|
|
916
|
+
error
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/server/vite-config.ts
|
|
923
|
+
init_ignore_patterns();
|
|
924
|
+
|
|
925
|
+
// src/server/plugins/config-virtual-module.ts
|
|
926
|
+
var VIRTUAL_CONFIG_MODULE_ID = "virtual:chronoter-config";
|
|
927
|
+
var RESOLVED_VIRTUAL_CONFIG_MODULE_ID = "\0" + VIRTUAL_CONFIG_MODULE_ID;
|
|
928
|
+
var createConfigVirtualModulePlugin = (config) => {
|
|
929
|
+
return {
|
|
930
|
+
name: "chronoter-config-virtual-module",
|
|
931
|
+
/**
|
|
932
|
+
* Virtual moduleのIDを解決
|
|
933
|
+
*/
|
|
934
|
+
resolveId(id) {
|
|
935
|
+
if (id === VIRTUAL_CONFIG_MODULE_ID) {
|
|
936
|
+
return RESOLVED_VIRTUAL_CONFIG_MODULE_ID;
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
},
|
|
940
|
+
/**
|
|
941
|
+
* Virtual moduleのコードを生成
|
|
942
|
+
*/
|
|
943
|
+
load(id) {
|
|
944
|
+
if (id === RESOLVED_VIRTUAL_CONFIG_MODULE_ID) {
|
|
945
|
+
const configCode = `
|
|
946
|
+
// Auto-generated by chronoter-config-virtual-module plugin
|
|
947
|
+
// This module provides access to the Chronoter configuration
|
|
948
|
+
|
|
949
|
+
export const config = ${JSON.stringify(config, null, 2)};
|
|
950
|
+
|
|
951
|
+
// HMR\u5BFE\u5FDC
|
|
952
|
+
if (import.meta.hot) {
|
|
953
|
+
import.meta.hot.accept((newModule) => {
|
|
954
|
+
if (newModule) {
|
|
955
|
+
console.log('[chronoter] Configuration updated');
|
|
956
|
+
// \u8A2D\u5B9A\u304C\u5909\u66F4\u3055\u308C\u305F\u5834\u5408\u306F\u30DA\u30FC\u30B8\u5168\u4F53\u3092\u30EA\u30ED\u30FC\u30C9
|
|
957
|
+
window.location.reload();
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
`;
|
|
962
|
+
return configCode;
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
};
|
|
968
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
969
|
+
var __dirname2 = dirname(__filename2);
|
|
970
|
+
var isMediaFileRequest = (url) => {
|
|
971
|
+
const mediaExtensions = [
|
|
972
|
+
".png",
|
|
973
|
+
".jpg",
|
|
974
|
+
".jpeg",
|
|
975
|
+
".gif",
|
|
976
|
+
".svg",
|
|
977
|
+
".webp",
|
|
978
|
+
".mp4",
|
|
979
|
+
".webm",
|
|
980
|
+
".ogg",
|
|
981
|
+
".mp3",
|
|
982
|
+
".wav"
|
|
983
|
+
];
|
|
984
|
+
const ext = extname(url.split("?")[0]);
|
|
985
|
+
return mediaExtensions.includes(ext.toLowerCase());
|
|
986
|
+
};
|
|
987
|
+
var resolveMediaFilePath = (url, cwd) => {
|
|
988
|
+
const cleanUrl = url.split("?")[0];
|
|
989
|
+
const relativePath = cleanUrl.startsWith("/") ? cleanUrl.slice(1) : cleanUrl;
|
|
990
|
+
const fullPath = join(cwd, relativePath);
|
|
991
|
+
return fullPath;
|
|
992
|
+
};
|
|
993
|
+
var getMimeType = (filePath) => {
|
|
994
|
+
const ext = extname(filePath).toLowerCase();
|
|
995
|
+
const mimeTypes = {
|
|
996
|
+
".png": "image/png",
|
|
997
|
+
".jpg": "image/jpeg",
|
|
998
|
+
".jpeg": "image/jpeg",
|
|
999
|
+
".gif": "image/gif",
|
|
1000
|
+
".svg": "image/svg+xml",
|
|
1001
|
+
".webp": "image/webp",
|
|
1002
|
+
".mp4": "video/mp4",
|
|
1003
|
+
".webm": "video/webm",
|
|
1004
|
+
".ogg": "video/ogg",
|
|
1005
|
+
".mp3": "audio/mpeg",
|
|
1006
|
+
".wav": "audio/wav"
|
|
1007
|
+
};
|
|
1008
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
1009
|
+
};
|
|
1010
|
+
var createViteConfig = (config, options = {}) => {
|
|
1011
|
+
const { port = 3e3, host = "localhost", cwd = process.cwd() } = options;
|
|
1012
|
+
return defineConfig({
|
|
1013
|
+
// プラグイン設定
|
|
1014
|
+
plugins: [
|
|
1015
|
+
// MDXプラグイン(GitHub Flavored Markdownサポート)
|
|
1016
|
+
mdx({
|
|
1017
|
+
remarkPlugins: [remarkGfm],
|
|
1018
|
+
rehypePlugins: []
|
|
1019
|
+
}),
|
|
1020
|
+
// React Fast Refresh対応
|
|
1021
|
+
react({
|
|
1022
|
+
// MDXファイルもReactコンポーネントとして扱う
|
|
1023
|
+
include: /\.(jsx|tsx|mdx)$/
|
|
1024
|
+
}),
|
|
1025
|
+
// 設定情報をクライアントに注入するVirtual Moduleプラグイン
|
|
1026
|
+
createConfigVirtualModulePlugin(config),
|
|
1027
|
+
// ルーティングミドルウェアを登録するプラグイン
|
|
1028
|
+
{
|
|
1029
|
+
name: "chronoter-routing",
|
|
1030
|
+
configureServer(server) {
|
|
1031
|
+
Promise.resolve().then(() => (init_routing_middleware(), routing_middleware_exports)).then(
|
|
1032
|
+
({ createRoutingMiddleware: createRoutingMiddleware2 }) => {
|
|
1033
|
+
console.log("\u{1F500} Setting up routing middleware...");
|
|
1034
|
+
const routingMiddleware = createRoutingMiddleware2(server, {
|
|
1035
|
+
config,
|
|
1036
|
+
cwd
|
|
1037
|
+
});
|
|
1038
|
+
server.middlewares.use(routingMiddleware);
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
// ロゴアセット配信ミドルウェアを登録するプラグイン(メディアファイルミドルウェアより前に配置)
|
|
1044
|
+
{
|
|
1045
|
+
name: "chronoter-logo-assets",
|
|
1046
|
+
configureServer(server) {
|
|
1047
|
+
server.middlewares.use((req, res, next) => {
|
|
1048
|
+
const url = req.url || "";
|
|
1049
|
+
if (url.startsWith("/chronoter-assets/media/")) {
|
|
1050
|
+
const fileName = url.replace("/chronoter-assets/media/", "");
|
|
1051
|
+
const logoPath = resolve(
|
|
1052
|
+
__dirname2,
|
|
1053
|
+
"core/media/logos",
|
|
1054
|
+
fileName
|
|
1055
|
+
);
|
|
1056
|
+
if (!existsSync(logoPath)) {
|
|
1057
|
+
res.statusCode = 404;
|
|
1058
|
+
res.end("Logo file not found");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const content = readFileSync(logoPath);
|
|
1063
|
+
const mimeType = getMimeType(logoPath);
|
|
1064
|
+
res.setHeader("Content-Type", mimeType);
|
|
1065
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
1066
|
+
res.end(content);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.error("Error reading logo file:", error);
|
|
1069
|
+
res.statusCode = 500;
|
|
1070
|
+
res.end("Error reading logo file");
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (url.startsWith("/chronoter-assets/project/")) {
|
|
1075
|
+
const fileName = url.replace("/chronoter-assets/project/", "");
|
|
1076
|
+
const logoPath = join(cwd, fileName);
|
|
1077
|
+
if (!existsSync(logoPath)) {
|
|
1078
|
+
res.statusCode = 404;
|
|
1079
|
+
res.end("Custom logo file not found");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const content = readFileSync(logoPath);
|
|
1084
|
+
const mimeType = getMimeType(logoPath);
|
|
1085
|
+
res.setHeader("Content-Type", mimeType);
|
|
1086
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
1087
|
+
res.end(content);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.error("Error reading custom logo file:", error);
|
|
1090
|
+
res.statusCode = 500;
|
|
1091
|
+
res.end("Error reading custom logo file");
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
next();
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
// メディアファイル配信ミドルウェアを登録するプラグイン(ロゴアセットミドルウェアの後に配置)
|
|
1100
|
+
{
|
|
1101
|
+
name: "chronoter-media-files",
|
|
1102
|
+
configureServer(server) {
|
|
1103
|
+
server.middlewares.use((req, res, next) => {
|
|
1104
|
+
if (!isMediaFileRequest(req.url || "")) {
|
|
1105
|
+
return next();
|
|
1106
|
+
}
|
|
1107
|
+
const mediaFilePath = resolveMediaFilePath(req.url || "", cwd);
|
|
1108
|
+
if (!existsSync(mediaFilePath)) {
|
|
1109
|
+
res.statusCode = 404;
|
|
1110
|
+
res.end("Media file not found");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
const content = readFileSync(mediaFilePath);
|
|
1115
|
+
const mimeType = getMimeType(mediaFilePath);
|
|
1116
|
+
res.setHeader("Content-Type", mimeType);
|
|
1117
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
1118
|
+
res.end(content);
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
console.error("Error reading media file:", error);
|
|
1121
|
+
res.statusCode = 500;
|
|
1122
|
+
res.end("Error reading media file");
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
],
|
|
1128
|
+
// エイリアス設定
|
|
1129
|
+
resolve: {
|
|
1130
|
+
alias: {
|
|
1131
|
+
// @/をchronoter-dev内のcoreモジュールにマップ
|
|
1132
|
+
// __dirnameはビルド後: dist/server、開発時: src/server
|
|
1133
|
+
"@": resolve(__dirname2, "../core")
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
// 開発サーバー設定
|
|
1137
|
+
server: {
|
|
1138
|
+
port,
|
|
1139
|
+
host,
|
|
1140
|
+
// ブラウザを自動的に開く(CLIオプションで制御)
|
|
1141
|
+
open: false,
|
|
1142
|
+
// CORS設定
|
|
1143
|
+
cors: true,
|
|
1144
|
+
// HMR設定
|
|
1145
|
+
hmr: {
|
|
1146
|
+
overlay: true,
|
|
1147
|
+
// エラーオーバーレイを表示
|
|
1148
|
+
protocol: "ws",
|
|
1149
|
+
// WebSocketプロトコル
|
|
1150
|
+
host,
|
|
1151
|
+
// HMRのホスト
|
|
1152
|
+
port,
|
|
1153
|
+
// HMRのポート
|
|
1154
|
+
clientPort: port
|
|
1155
|
+
// クライアント側のポート
|
|
1156
|
+
},
|
|
1157
|
+
// ファイル監視設定
|
|
1158
|
+
watch: {
|
|
1159
|
+
// chronoter-coreのデフォルト除外パターン + カスタム除外パターンを使用
|
|
1160
|
+
ignored: [...DEFAULT_IGNORE_PATTERNS, ...config.ignore || []]
|
|
1161
|
+
// ポーリング間隔(ミリ秒)- ファイルシステムイベントが信頼できない環境用
|
|
1162
|
+
// usePolling: false,
|
|
1163
|
+
// interval: 100,
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
// ビルド設定(開発サーバーでは使用しないが、将来的な拡張のため定義)
|
|
1167
|
+
build: {
|
|
1168
|
+
// ソースマップを生成
|
|
1169
|
+
sourcemap: true,
|
|
1170
|
+
// チャンク分割戦略
|
|
1171
|
+
rollupOptions: {
|
|
1172
|
+
output: {
|
|
1173
|
+
manualChunks: {
|
|
1174
|
+
// Reactを別チャンクに分離
|
|
1175
|
+
react: ["react", "react-dom"]
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
// 最適化設定
|
|
1181
|
+
optimizeDeps: {
|
|
1182
|
+
// 事前バンドルする依存関係
|
|
1183
|
+
include: ["react", "react-dom"],
|
|
1184
|
+
// 除外する依存関係(Node.js専用モジュール)
|
|
1185
|
+
exclude: ["fsevents", "chokidar"]
|
|
1186
|
+
},
|
|
1187
|
+
// ルートディレクトリ(chronoter-devパッケージのルートに設定)
|
|
1188
|
+
// ビルド後: __dirname = dist, resolve(__dirname, "..") = packages/chronoter-dev
|
|
1189
|
+
// 開発時: __dirname = src/server, resolve(__dirname, "../..") = packages/chronoter-dev
|
|
1190
|
+
// ビルド後のパスを使用(dist -> packages/chronoter-dev)
|
|
1191
|
+
root: resolve(__dirname2, ".."),
|
|
1192
|
+
// 公開ディレクトリ(静的アセット用)
|
|
1193
|
+
publicDir: resolve(cwd, "public"),
|
|
1194
|
+
// サーバー設定でベースパスを指定
|
|
1195
|
+
base: "/",
|
|
1196
|
+
// ログレベル
|
|
1197
|
+
logLevel: "info"
|
|
1198
|
+
});
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/hmr/index.ts
|
|
1202
|
+
init_ignore_patterns();
|
|
1203
|
+
var HMRManager = class {
|
|
1204
|
+
constructor(server, options) {
|
|
1205
|
+
this.watcher = null;
|
|
1206
|
+
this.configWatcher = null;
|
|
1207
|
+
this.server = server;
|
|
1208
|
+
this.options = options;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* ファイル監視を開始する
|
|
1212
|
+
*/
|
|
1213
|
+
start() {
|
|
1214
|
+
this.startMDXWatcher();
|
|
1215
|
+
this.startConfigWatcher();
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* MDXファイルの監視を開始
|
|
1219
|
+
*/
|
|
1220
|
+
startMDXWatcher() {
|
|
1221
|
+
const watchPath = resolve(this.options.cwd, this.options.watchDir);
|
|
1222
|
+
console.log(chalk2.cyan("\u{1F440} Watching for MDX file changes..."));
|
|
1223
|
+
console.log(chalk2.gray(` Path: ${watchPath}`));
|
|
1224
|
+
this.watcher = watch(["**/*.mdx", "**/*.md"], {
|
|
1225
|
+
cwd: watchPath,
|
|
1226
|
+
// chronoter-coreのデフォルト除外パターン + カスタム除外パターンを使用
|
|
1227
|
+
ignored: [
|
|
1228
|
+
...DEFAULT_IGNORE_PATTERNS,
|
|
1229
|
+
...this.options.customIgnore || []
|
|
1230
|
+
],
|
|
1231
|
+
ignoreInitial: true,
|
|
1232
|
+
persistent: true
|
|
1233
|
+
});
|
|
1234
|
+
this.watcher.on("add", (path2) => {
|
|
1235
|
+
this.handleMDXChange("added", path2);
|
|
1236
|
+
});
|
|
1237
|
+
this.watcher.on("change", (path2) => {
|
|
1238
|
+
this.handleMDXChange("changed", path2);
|
|
1239
|
+
});
|
|
1240
|
+
this.watcher.on("unlink", (path2) => {
|
|
1241
|
+
this.handleMDXChange("removed", path2);
|
|
1242
|
+
});
|
|
1243
|
+
this.watcher.on("error", (error) => {
|
|
1244
|
+
console.error(chalk2.red("\u2717 File watcher error:"), error);
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* 設定ファイルの監視を開始
|
|
1249
|
+
*
|
|
1250
|
+
* chronoter.config.json は固定ファイル名
|
|
1251
|
+
*/
|
|
1252
|
+
startConfigWatcher() {
|
|
1253
|
+
const configPath = resolve(this.options.cwd, "chronoter.config.json");
|
|
1254
|
+
console.log(chalk2.cyan("\u{1F440} Watching for config file changes..."));
|
|
1255
|
+
console.log(chalk2.gray(` Path: ${configPath}`));
|
|
1256
|
+
this.configWatcher = watch(configPath, {
|
|
1257
|
+
ignoreInitial: true,
|
|
1258
|
+
persistent: true
|
|
1259
|
+
});
|
|
1260
|
+
this.configWatcher.on("change", () => {
|
|
1261
|
+
this.handleConfigChange();
|
|
1262
|
+
});
|
|
1263
|
+
this.configWatcher.on("error", (error) => {
|
|
1264
|
+
console.error(chalk2.red("\u2717 Config watcher error:"), error);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* MDXファイルの変更を処理
|
|
1269
|
+
*
|
|
1270
|
+
* @param event 変更イベントの種類
|
|
1271
|
+
* @param path 変更されたファイルのパス
|
|
1272
|
+
*/
|
|
1273
|
+
handleMDXChange(event, path2) {
|
|
1274
|
+
const relativePath = relative(this.options.cwd, path2);
|
|
1275
|
+
const eventLabel = event === "added" ? "\u2795" : event === "changed" ? "\u{1F4DD}" : "\u{1F5D1}\uFE0F";
|
|
1276
|
+
const eventColor = event === "added" ? chalk2.green : event === "changed" ? chalk2.yellow : chalk2.red;
|
|
1277
|
+
console.log();
|
|
1278
|
+
console.log(eventColor(`${eventLabel} MDX file ${event}: ${relativePath}`));
|
|
1279
|
+
const moduleId = `/${path2}`;
|
|
1280
|
+
const module = this.server.moduleGraph.getModuleById(moduleId);
|
|
1281
|
+
if (module) {
|
|
1282
|
+
this.server.moduleGraph.invalidateModule(module);
|
|
1283
|
+
console.log(chalk2.gray(` Module invalidated: ${moduleId}`));
|
|
1284
|
+
}
|
|
1285
|
+
this.server.ws.send({
|
|
1286
|
+
type: "full-reload",
|
|
1287
|
+
path: "*"
|
|
1288
|
+
});
|
|
1289
|
+
console.log(chalk2.green("\u2713 Browser reloaded"));
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 設定ファイルの変更を処理
|
|
1293
|
+
*/
|
|
1294
|
+
handleConfigChange() {
|
|
1295
|
+
console.log();
|
|
1296
|
+
console.log(chalk2.yellow("\u2699\uFE0F Config file changed"));
|
|
1297
|
+
this.server.ws.send({
|
|
1298
|
+
type: "full-reload",
|
|
1299
|
+
path: "*"
|
|
1300
|
+
});
|
|
1301
|
+
console.log(chalk2.green("\u2713 Configuration reloaded via HMR"));
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* ファイル監視を停止する
|
|
1305
|
+
*/
|
|
1306
|
+
async stop() {
|
|
1307
|
+
console.log(chalk2.yellow("\u23F9 Stopping file watchers..."));
|
|
1308
|
+
if (this.watcher) {
|
|
1309
|
+
await this.watcher.close();
|
|
1310
|
+
this.watcher = null;
|
|
1311
|
+
}
|
|
1312
|
+
if (this.configWatcher) {
|
|
1313
|
+
await this.configWatcher.close();
|
|
1314
|
+
this.configWatcher = null;
|
|
1315
|
+
}
|
|
1316
|
+
console.log(chalk2.green("\u2713 File watchers stopped"));
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// src/server/dev-server.ts
|
|
1321
|
+
var startDevServer = async (options = {}) => {
|
|
1322
|
+
const {
|
|
1323
|
+
port = 3e3,
|
|
1324
|
+
host = "localhost",
|
|
1325
|
+
open = true,
|
|
1326
|
+
cwd = process.cwd()
|
|
1327
|
+
} = options;
|
|
1328
|
+
try {
|
|
1329
|
+
console.log(chalk2.cyan("\u{1F4D6} Loading configuration..."));
|
|
1330
|
+
const config = await loadConfig(cwd);
|
|
1331
|
+
console.log(chalk2.green("\u2713 Configuration loaded successfully"));
|
|
1332
|
+
console.log(chalk2.gray(` Site: ${config.site.title}`));
|
|
1333
|
+
if (config.docsDir) {
|
|
1334
|
+
console.log(chalk2.gray(` Docs directory: ${config.docsDir}`));
|
|
1335
|
+
}
|
|
1336
|
+
console.log();
|
|
1337
|
+
console.log(chalk2.cyan("\u2699\uFE0F Creating Vite configuration..."));
|
|
1338
|
+
const viteConfig = createViteConfig(config, { port, host, cwd });
|
|
1339
|
+
console.log(chalk2.cyan("\u{1F527} Creating Vite server..."));
|
|
1340
|
+
const server = await createServer(viteConfig);
|
|
1341
|
+
console.log(chalk2.cyan("\u{1F680} Starting server..."));
|
|
1342
|
+
await server.listen();
|
|
1343
|
+
console.log(chalk2.cyan("\u{1F525} Setting up Hot Module Replacement..."));
|
|
1344
|
+
const hmrManager = new HMRManager(server, {
|
|
1345
|
+
watchDir: config.docsDir || ".",
|
|
1346
|
+
cwd,
|
|
1347
|
+
customIgnore: config.ignore
|
|
1348
|
+
// カスタム除外パターンを渡す
|
|
1349
|
+
});
|
|
1350
|
+
hmrManager.start();
|
|
1351
|
+
const serverInfo = server.httpServer?.address();
|
|
1352
|
+
const actualPort = typeof serverInfo === "object" && serverInfo !== null ? serverInfo.port : port;
|
|
1353
|
+
const url = `http://${host}:${actualPort}`;
|
|
1354
|
+
console.log(chalk2.green.bold("\u2713 Development server is ready!"));
|
|
1355
|
+
console.log();
|
|
1356
|
+
console.log(
|
|
1357
|
+
chalk2.cyan(" \u279C") + chalk2.bold(" Local: ") + chalk2.cyan(url)
|
|
1358
|
+
);
|
|
1359
|
+
console.log();
|
|
1360
|
+
console.log(chalk2.gray(" Press Ctrl+C to stop the server"));
|
|
1361
|
+
console.log();
|
|
1362
|
+
if (open) {
|
|
1363
|
+
console.log(chalk2.cyan("\u{1F310} Opening browser..."));
|
|
1364
|
+
await server.openBrowser();
|
|
1365
|
+
}
|
|
1366
|
+
process.on("SIGINT", async () => {
|
|
1367
|
+
console.log();
|
|
1368
|
+
console.log(chalk2.yellow("\u23F9 Stopping server..."));
|
|
1369
|
+
await hmrManager.stop();
|
|
1370
|
+
await server.close();
|
|
1371
|
+
console.log(chalk2.green("\u2713 Server stopped"));
|
|
1372
|
+
process.exit(0);
|
|
1373
|
+
});
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
handleServerError(error);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var loadConfig = async (cwd) => {
|
|
1379
|
+
try {
|
|
1380
|
+
return await ConfigLoader.load(cwd);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
if (error instanceof ConfigError) {
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
throw new ConfigError(
|
|
1386
|
+
"An unexpected error occurred while loading configuration",
|
|
1387
|
+
error
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
var handleServerError = (error) => {
|
|
1392
|
+
console.log();
|
|
1393
|
+
if (error instanceof ConfigError) {
|
|
1394
|
+
console.error(chalk2.red.bold("\u2717 Configuration Error"));
|
|
1395
|
+
console.error();
|
|
1396
|
+
console.error(chalk2.red(error.message));
|
|
1397
|
+
if (error.details) {
|
|
1398
|
+
console.error();
|
|
1399
|
+
console.error(chalk2.yellow("Details:"));
|
|
1400
|
+
console.error(chalk2.gray(JSON.stringify(error.details, null, 2)));
|
|
1401
|
+
}
|
|
1402
|
+
console.error();
|
|
1403
|
+
console.error(
|
|
1404
|
+
chalk2.cyan("\u{1F4A1} Tip: Make sure chronoter.config.json exists and is valid")
|
|
1405
|
+
);
|
|
1406
|
+
} else if (error instanceof Error) {
|
|
1407
|
+
console.error(chalk2.red.bold("\u2717 Server Error"));
|
|
1408
|
+
console.error();
|
|
1409
|
+
console.error(chalk2.red(error.message));
|
|
1410
|
+
if (error.stack) {
|
|
1411
|
+
console.error();
|
|
1412
|
+
console.error(chalk2.gray(error.stack));
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
console.error(chalk2.red.bold("\u2717 Unknown Error"));
|
|
1416
|
+
console.error();
|
|
1417
|
+
console.error(chalk2.red(String(error)));
|
|
1418
|
+
}
|
|
1419
|
+
console.error();
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
// src/cli/version-checker/npm-registry.ts
|
|
1424
|
+
var fetchLatestVersion = async (packageName, timeout = 5e3) => {
|
|
1425
|
+
const registryUrl = `https://registry.npmjs.org/${packageName}`;
|
|
1426
|
+
try {
|
|
1427
|
+
const controller = new AbortController();
|
|
1428
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1429
|
+
const response = await fetch(registryUrl, {
|
|
1430
|
+
headers: {
|
|
1431
|
+
Accept: "application/json"
|
|
1432
|
+
},
|
|
1433
|
+
signal: controller.signal
|
|
1434
|
+
});
|
|
1435
|
+
clearTimeout(timeoutId);
|
|
1436
|
+
if (!response.ok) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
const data = await response.json();
|
|
1440
|
+
return {
|
|
1441
|
+
latestVersion: data["dist-tags"].latest,
|
|
1442
|
+
name: data.name
|
|
1443
|
+
};
|
|
1444
|
+
} catch {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1449
|
+
var getCacheDir = () => {
|
|
1450
|
+
return join(homedir(), ".chronoter");
|
|
1451
|
+
};
|
|
1452
|
+
var getCacheFilePath = () => {
|
|
1453
|
+
return join(getCacheDir(), "version-check-cache.json");
|
|
1454
|
+
};
|
|
1455
|
+
var ensureCacheDir = () => {
|
|
1456
|
+
const cacheDir = getCacheDir();
|
|
1457
|
+
if (!existsSync(cacheDir)) {
|
|
1458
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
var isCacheValid = (cache, ttlMs) => {
|
|
1462
|
+
const now = Date.now();
|
|
1463
|
+
return now - cache.timestamp < ttlMs;
|
|
1464
|
+
};
|
|
1465
|
+
var getVersionCache = (packageName, ttlMs = DEFAULT_CACHE_TTL_MS) => {
|
|
1466
|
+
try {
|
|
1467
|
+
const cachePath = getCacheFilePath();
|
|
1468
|
+
if (!existsSync(cachePath)) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
1472
|
+
const cache = JSON.parse(content);
|
|
1473
|
+
if (cache.packageName === packageName && isCacheValid(cache, ttlMs)) {
|
|
1474
|
+
return cache;
|
|
1475
|
+
}
|
|
1476
|
+
return null;
|
|
1477
|
+
} catch {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
var setVersionCache = (packageName, latestVersion) => {
|
|
1482
|
+
try {
|
|
1483
|
+
ensureCacheDir();
|
|
1484
|
+
const cache = {
|
|
1485
|
+
packageName,
|
|
1486
|
+
latestVersion,
|
|
1487
|
+
timestamp: Date.now()
|
|
1488
|
+
};
|
|
1489
|
+
writeFileSync(getCacheFilePath(), JSON.stringify(cache, null, 2), "utf-8");
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
var isInteractiveTerminal = () => {
|
|
1494
|
+
return Boolean(process.stdin.isTTY);
|
|
1495
|
+
};
|
|
1496
|
+
var promptForUpdate = async (currentVersion, latestVersion) => {
|
|
1497
|
+
console.log();
|
|
1498
|
+
console.log(
|
|
1499
|
+
chalk2.yellow(
|
|
1500
|
+
`A new version of Chronoter is available: ${chalk2.bold(latestVersion)} (current: ${currentVersion})`
|
|
1501
|
+
)
|
|
1502
|
+
);
|
|
1503
|
+
if (!isInteractiveTerminal()) {
|
|
1504
|
+
console.log(
|
|
1505
|
+
chalk2.gray("Non-interactive environment detected. Skipping update prompt.")
|
|
1506
|
+
);
|
|
1507
|
+
console.log(
|
|
1508
|
+
chalk2.gray(`Run 'npm install -g ${process.env.npm_package_name || "chronoter"}@latest' to update manually.`)
|
|
1509
|
+
);
|
|
1510
|
+
console.log();
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
const rl = createInterface({
|
|
1514
|
+
input: process.stdin,
|
|
1515
|
+
output: process.stdout
|
|
1516
|
+
});
|
|
1517
|
+
return new Promise((resolve5) => {
|
|
1518
|
+
rl.question(
|
|
1519
|
+
"A new version of Chronoter is available. Would you like to update? (yes/no) ",
|
|
1520
|
+
(answer) => {
|
|
1521
|
+
rl.close();
|
|
1522
|
+
const normalizedAnswer = answer.trim().toLowerCase();
|
|
1523
|
+
resolve5(normalizedAnswer === "yes" || normalizedAnswer === "y");
|
|
1524
|
+
}
|
|
1525
|
+
);
|
|
1526
|
+
});
|
|
1527
|
+
};
|
|
1528
|
+
var showUpdateStartMessage = () => {
|
|
1529
|
+
console.log();
|
|
1530
|
+
console.log(chalk2.cyan("Updating Chronoter..."));
|
|
1531
|
+
};
|
|
1532
|
+
var showUpdateSuccessMessage = (version) => {
|
|
1533
|
+
console.log(chalk2.green(`Successfully updated to version ${version}`));
|
|
1534
|
+
console.log();
|
|
1535
|
+
};
|
|
1536
|
+
var showUpdateFailedMessage = (error) => {
|
|
1537
|
+
console.log(chalk2.red(`Failed to update Chronoter: ${error}`));
|
|
1538
|
+
console.log(
|
|
1539
|
+
chalk2.yellow("Continuing with the current version...")
|
|
1540
|
+
);
|
|
1541
|
+
console.log();
|
|
1542
|
+
};
|
|
1543
|
+
var showUpdateSkippedMessage = () => {
|
|
1544
|
+
console.log(chalk2.gray("Update skipped. Continuing with the current version..."));
|
|
1545
|
+
console.log();
|
|
1546
|
+
};
|
|
1547
|
+
var installLatestVersion = async (packageName) => {
|
|
1548
|
+
return new Promise((resolve5) => {
|
|
1549
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1550
|
+
const child = spawn(npmCommand, ["install", "-g", `${packageName}@latest`], {
|
|
1551
|
+
stdio: "inherit"
|
|
1552
|
+
});
|
|
1553
|
+
child.on("error", (err) => {
|
|
1554
|
+
resolve5({
|
|
1555
|
+
success: false,
|
|
1556
|
+
error: err.message
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
child.on("close", (code) => {
|
|
1560
|
+
if (code === 0) {
|
|
1561
|
+
resolve5({ success: true });
|
|
1562
|
+
} else {
|
|
1563
|
+
resolve5({
|
|
1564
|
+
success: false,
|
|
1565
|
+
error: `npm install exited with code ${code}`
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
// src/cli/version-checker/index.ts
|
|
1573
|
+
var compareVersions = (v1, v2) => {
|
|
1574
|
+
const parts1 = v1.replace(/^v/, "").split(".").map(Number);
|
|
1575
|
+
const parts2 = v2.replace(/^v/, "").split(".").map(Number);
|
|
1576
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
1577
|
+
const p1 = parts1[i] ?? 0;
|
|
1578
|
+
const p2 = parts2[i] ?? 0;
|
|
1579
|
+
if (p1 < p2) return -1;
|
|
1580
|
+
if (p1 > p2) return 1;
|
|
1581
|
+
}
|
|
1582
|
+
return 0;
|
|
1583
|
+
};
|
|
1584
|
+
var getLatestVersion = async (packageName) => {
|
|
1585
|
+
const cached = getVersionCache(packageName);
|
|
1586
|
+
if (cached) {
|
|
1587
|
+
return cached.latestVersion;
|
|
1588
|
+
}
|
|
1589
|
+
const npmInfo = await fetchLatestVersion(packageName);
|
|
1590
|
+
if (npmInfo) {
|
|
1591
|
+
setVersionCache(packageName, npmInfo.latestVersion);
|
|
1592
|
+
return npmInfo.latestVersion;
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
};
|
|
1596
|
+
var checkVersion = async (packageName, currentVersion) => {
|
|
1597
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
1598
|
+
if (!latestVersion) {
|
|
1599
|
+
return {
|
|
1600
|
+
updateAvailable: false,
|
|
1601
|
+
currentVersion
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1605
|
+
return {
|
|
1606
|
+
updateAvailable: comparison < 0,
|
|
1607
|
+
currentVersion,
|
|
1608
|
+
latestVersion
|
|
1609
|
+
};
|
|
1610
|
+
};
|
|
1611
|
+
var runVersionCheck = async (packageName, currentVersion) => {
|
|
1612
|
+
try {
|
|
1613
|
+
const result = await checkVersion(packageName, currentVersion);
|
|
1614
|
+
if (!result.updateAvailable || !result.latestVersion) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const shouldUpdate = await promptForUpdate(
|
|
1618
|
+
result.currentVersion,
|
|
1619
|
+
result.latestVersion
|
|
1620
|
+
);
|
|
1621
|
+
if (shouldUpdate) {
|
|
1622
|
+
showUpdateStartMessage();
|
|
1623
|
+
const updateResult = await installLatestVersion(packageName);
|
|
1624
|
+
if (updateResult.success) {
|
|
1625
|
+
showUpdateSuccessMessage(result.latestVersion);
|
|
1626
|
+
} else {
|
|
1627
|
+
showUpdateFailedMessage(updateResult.error ?? "Unknown error");
|
|
1628
|
+
}
|
|
1629
|
+
} else {
|
|
1630
|
+
showUpdateSkippedMessage();
|
|
1631
|
+
}
|
|
1632
|
+
} catch {
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
var PACKAGE_NAME = "chronoter";
|
|
1636
|
+
var CURRENT_VERSION = "0.1.0";
|
|
1637
|
+
var createCliProgram = () => {
|
|
1638
|
+
const program = new Command();
|
|
1639
|
+
program.name("chronoter").description("Chronoter - MDX-based documentation site generator").version(CURRENT_VERSION);
|
|
1640
|
+
program.command("dev").description("Start the development server with HMR support").option("-p, --port <port>", "Server port number", "3000").option("-H, --host <host>", "Server host", "localhost").option(
|
|
1641
|
+
"--no-open",
|
|
1642
|
+
"Disable automatic browser opening (browser opens by default)"
|
|
1643
|
+
).option("--cwd <path>", "Working directory", process.cwd()).action(async (options) => {
|
|
1644
|
+
try {
|
|
1645
|
+
const port = typeof options.port === "string" ? parseInt(options.port, 10) : options.port;
|
|
1646
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1647
|
+
console.error(
|
|
1648
|
+
chalk2.red(`Error: Invalid port number: ${options.port}`)
|
|
1649
|
+
);
|
|
1650
|
+
console.error(
|
|
1651
|
+
chalk2.yellow("Port number must be between 1 and 65535.")
|
|
1652
|
+
);
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
const cwd = options.cwd || process.cwd();
|
|
1656
|
+
console.log(chalk2.cyan("\u{1F680} Starting Chronoter development server..."));
|
|
1657
|
+
console.log(chalk2.gray(` Port: ${port}`));
|
|
1658
|
+
console.log(chalk2.gray(` Host: ${options.host}`));
|
|
1659
|
+
console.log(
|
|
1660
|
+
chalk2.gray(
|
|
1661
|
+
` Open browser: ${options.open !== false ? "enabled" : "disabled"}`
|
|
1662
|
+
)
|
|
1663
|
+
);
|
|
1664
|
+
console.log(chalk2.gray(` Working directory: ${cwd}`));
|
|
1665
|
+
console.log();
|
|
1666
|
+
await startDevServer({
|
|
1667
|
+
port,
|
|
1668
|
+
host: options.host,
|
|
1669
|
+
open: options.open,
|
|
1670
|
+
cwd
|
|
1671
|
+
});
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
console.error(chalk2.red("An error occurred:"));
|
|
1674
|
+
console.error(error);
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
return program;
|
|
1679
|
+
};
|
|
1680
|
+
var main = async () => {
|
|
1681
|
+
const args = process.argv.slice(2);
|
|
1682
|
+
const isVersionOrHelp = args.some(
|
|
1683
|
+
(arg) => arg === "--version" || arg === "-V" || arg === "--help" || arg === "-h"
|
|
1684
|
+
);
|
|
1685
|
+
if (!isVersionOrHelp) {
|
|
1686
|
+
await runVersionCheck(PACKAGE_NAME, CURRENT_VERSION);
|
|
1687
|
+
}
|
|
1688
|
+
const program = createCliProgram();
|
|
1689
|
+
await program.parseAsync(process.argv);
|
|
1690
|
+
};
|
|
1691
|
+
var isMainModule = (() => {
|
|
1692
|
+
try {
|
|
1693
|
+
const realArgv1 = realpathSync(process.argv[1]);
|
|
1694
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1695
|
+
return currentFile === realArgv1;
|
|
1696
|
+
} catch {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
})();
|
|
1700
|
+
if (isMainModule) {
|
|
1701
|
+
main().catch((error) => {
|
|
1702
|
+
console.error(chalk2.red("An unexpected error occurred:"));
|
|
1703
|
+
console.error(error);
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
export { CURRENT_VERSION, PACKAGE_NAME, createCliProgram };
|