@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/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 };