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