@chronoter/main 0.1.10 → 0.2.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 CHANGED
@@ -8,6 +8,10 @@ import { visit } from 'unist-util-visit';
8
8
  import { compile } from '@mdx-js/mdx';
9
9
  import matter from 'gray-matter';
10
10
  import remarkGfm from 'remark-gfm';
11
+ import rehypeSlug from 'rehype-slug';
12
+ import { unified } from 'unified';
13
+ import remarkParse from 'remark-parse';
14
+ import GithubSlugger from 'github-slugger';
11
15
  import { fileURLToPath } from 'url';
12
16
  import { Command } from 'commander';
13
17
  import chalk2 from 'chalk';
@@ -237,12 +241,14 @@ var init_processor = __esm({
237
241
  throw new MDXError(`Failed to parse frontmatter: ${message}`, filePath);
238
242
  }
239
243
  const code = await this.compile(content, filePath, options?.baseUrl);
244
+ const headings = this.extractHeadings(content);
240
245
  const slug = this.generateSlug(filePath);
241
246
  const frontmatter = this.parseFrontmatter(data);
242
247
  return {
243
248
  code,
244
249
  frontmatter,
245
- slug
250
+ slug,
251
+ headings
246
252
  };
247
253
  } catch (error) {
248
254
  if (error instanceof MDXError) {
@@ -295,7 +301,8 @@ var init_processor = __esm({
295
301
  const result = await compile(source, {
296
302
  outputFormat: "function-body",
297
303
  development: false,
298
- remarkPlugins
304
+ remarkPlugins,
305
+ rehypePlugins: [rehypeSlug]
299
306
  });
300
307
  return String(result);
301
308
  } catch (error) {
@@ -320,6 +327,52 @@ var init_processor = __esm({
320
327
  const fileName = basename(filePath, extname(filePath));
321
328
  return fileName;
322
329
  }
330
+ /**
331
+ * Markdownコンテンツから見出しを抽出する
332
+ *
333
+ * @param content - Markdownコンテンツ
334
+ * @returns 見出しの配列(h2, h3のみ)
335
+ * @private
336
+ */
337
+ extractHeadings(content) {
338
+ const headings = [];
339
+ const slugger = new GithubSlugger();
340
+ try {
341
+ const tree = unified().use(remarkParse).parse(content);
342
+ visit(tree, "heading", (node) => {
343
+ if (node.depth === 2 || node.depth === 3) {
344
+ const text = this.extractTextFromNode(node.children);
345
+ const id = slugger.slug(text);
346
+ headings.push({
347
+ id,
348
+ text,
349
+ level: node.depth
350
+ });
351
+ }
352
+ });
353
+ } catch (error) {
354
+ console.warn("Failed to extract headings:", error);
355
+ }
356
+ return headings;
357
+ }
358
+ /**
359
+ * 見出しノードの子要素からテキストを抽出する
360
+ *
361
+ * @param children - 見出しノードの子要素
362
+ * @returns テキスト
363
+ * @private
364
+ */
365
+ extractTextFromNode(children) {
366
+ return children.map((child) => {
367
+ if (child.type === "text") {
368
+ return child.value;
369
+ }
370
+ if ("children" in child && Array.isArray(child.children)) {
371
+ return this.extractTextFromNode(child.children);
372
+ }
373
+ return "";
374
+ }).join("");
375
+ }
323
376
  };
324
377
  }
325
378
  });
@@ -446,16 +499,17 @@ var init_router = __esm({
446
499
  /**
447
500
  * ファイルパスからルートを作成
448
501
  *
449
- * @param filePath - MDXまたはMDファイルのパス
450
- * @param baseDir - ベースディレクトリパス
502
+ * @param filePath - MDXまたはMDファイルの絶対パス
503
+ * @param baseDir - ベースディレクトリパス(docsDir)
451
504
  * @returns 生成されたルート
452
505
  */
453
506
  async createRoute(filePath, baseDir) {
454
507
  const frontmatter = await this.extractFrontmatter(filePath);
455
508
  const urlPath = this.filePathToUrlPath(filePath, baseDir);
509
+ const relativeFilePath = path.relative(baseDir, filePath);
456
510
  return {
457
511
  path: urlPath,
458
- filePath,
512
+ filePath: relativeFilePath,
459
513
  frontmatter
460
514
  };
461
515
  }
@@ -513,12 +567,17 @@ var routing_middleware_exports = {};
513
567
  __export(routing_middleware_exports, {
514
568
  createRoutingMiddleware: () => createRoutingMiddleware
515
569
  });
516
- var __filename$1, __dirname$1, createRoutingMiddleware, handleAllRoutesRequest, handleRouteRequest, handleMDXRequest, handle500;
570
+ var __filename$1, __dirname$1, hasFileExtension, createRoutingMiddleware, handleAllRoutesRequest, handleRouteRequest, handleMDXRequest, handle500;
517
571
  var init_routing_middleware = __esm({
518
572
  "src/server/routing-middleware.ts"() {
519
573
  init_router();
520
574
  __filename$1 = fileURLToPath(import.meta.url);
521
575
  __dirname$1 = dirname(__filename$1);
576
+ hasFileExtension = (urlPath) => {
577
+ const lastSegment = urlPath.split("/").pop() || "";
578
+ const dotIndex = lastSegment.lastIndexOf(".");
579
+ return dotIndex > 0;
580
+ };
522
581
  createRoutingMiddleware = (server, options) => {
523
582
  const { config, cwd } = options;
524
583
  const routeGenerator = new RouteGenerator();
@@ -569,7 +628,8 @@ var init_routing_middleware = __esm({
569
628
  }
570
629
  const urlPath = url.split("?")[0];
571
630
  if (urlPath === "/__chronoter_mdx__") {
572
- return handleMDXRequest(req, res, cwd);
631
+ const docsDir = config.docsDir ? resolve(cwd, config.docsDir) : cwd;
632
+ return handleMDXRequest(req, res, docsDir);
573
633
  }
574
634
  if (urlPath === "/__chronoter_routes__") {
575
635
  return handleRouteRequest(req, res, getRoutes, routeGenerator);
@@ -579,7 +639,7 @@ var init_routing_middleware = __esm({
579
639
  }
580
640
  if (urlPath.startsWith("/@") || // Vite内部リクエスト
581
641
  urlPath.startsWith("/node_modules") || urlPath.startsWith("/src/") || // Viteが処理するソースファイル
582
- urlPath.includes(".") && !urlPath.endsWith("/") && !urlPath.startsWith("/src/")) {
642
+ hasFileExtension(urlPath) && !urlPath.endsWith("/") && !urlPath.startsWith("/src/")) {
583
643
  return next();
584
644
  }
585
645
  try {
@@ -689,7 +749,7 @@ var init_routing_middleware = __esm({
689
749
  );
690
750
  }
691
751
  };
692
- handleMDXRequest = async (req, res, cwd) => {
752
+ handleMDXRequest = async (req, res, docsDir) => {
693
753
  try {
694
754
  const url = new URL(req.url || "", `http://${req.headers.host}`);
695
755
  const filePath = url.searchParams.get("path");
@@ -699,8 +759,8 @@ var init_routing_middleware = __esm({
699
759
  res.end(JSON.stringify({ error: "Missing 'path' query parameter" }));
700
760
  return;
701
761
  }
702
- const absolutePath = resolve(cwd, filePath);
703
- if (!absolutePath.startsWith(cwd)) {
762
+ const absolutePath = resolve(docsDir, filePath);
763
+ if (!absolutePath.startsWith(docsDir)) {
704
764
  res.statusCode = 403;
705
765
  res.setHeader("Content-Type", "application/json");
706
766
  res.end(JSON.stringify({ error: "Access denied" }));
@@ -715,7 +775,8 @@ var init_routing_middleware = __esm({
715
775
  JSON.stringify({
716
776
  code: processedMDX.code,
717
777
  frontmatter: processedMDX.frontmatter,
718
- slug: processedMDX.slug
778
+ slug: processedMDX.slug,
779
+ headings: processedMDX.headings
719
780
  })
720
781
  );
721
782
  } catch (error) {
@@ -824,7 +885,7 @@ var ConfigError = class _ConfigError extends Error {
824
885
  }
825
886
  };
826
887
  var siteConfigSchema = z.object({
827
- title: z.string().min(1, "site.title is required"),
888
+ title: z.string().min(1, "site.title must not be empty").optional(),
828
889
  description: z.string().optional(),
829
890
  baseUrl: z.string().url("site.baseUrl must be a valid URL").optional()
830
891
  });
@@ -840,7 +901,7 @@ var themeConfigSchema = z.object({
840
901
  logo: z.string().optional()
841
902
  });
842
903
  var chronoterConfigSchema = z.object({
843
- site: siteConfigSchema,
904
+ site: siteConfigSchema.optional(),
844
905
  navigation: z.array(navigationItemSchema).optional(),
845
906
  docsDir: z.string().min(1, "docsDir must not be empty").optional(),
846
907
  outDir: z.string().optional(),
@@ -850,6 +911,10 @@ var chronoterConfigSchema = z.object({
850
911
 
851
912
  // src/core/config/defaults.ts
852
913
  var defaultConfig = {
914
+ site: {
915
+ title: "Chronoter Documentation"
916
+ // デフォルトタイトル
917
+ },
853
918
  docsDir: ".",
854
919
  // デフォルトはルートディレクトリ
855
920
  theme: {
@@ -858,9 +923,12 @@ var defaultConfig = {
858
923
  }
859
924
  };
860
925
  var mergeWithDefaults = (userConfig) => {
861
- const site = userConfig.site;
862
926
  return {
863
- site,
927
+ site: {
928
+ title: userConfig.site?.title ?? defaultConfig.site.title,
929
+ description: userConfig.site?.description,
930
+ baseUrl: userConfig.site?.baseUrl
931
+ },
864
932
  docsDir: userConfig.docsDir ?? defaultConfig.docsDir,
865
933
  navigation: userConfig.navigation,
866
934
  theme: {
@@ -896,10 +964,7 @@ var ConfigLoader = class {
896
964
  return this.validate(parsedConfig);
897
965
  } catch (error) {
898
966
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
899
- throw new ConfigError(
900
- `Config file not found: ${configPath}
901
- Please create a chronoter.config.json file.`
902
- );
967
+ return defaultConfig;
903
968
  }
904
969
  if (error instanceof ConfigError) {
905
970
  throw error;
@@ -1015,10 +1080,22 @@ var createViteConfig = (config, options = {}) => {
1015
1080
  );
1016
1081
  }
1017
1082
  },
1018
- // ビルド済みクライアントファイル配信ミドルウェアを登録するプラグイン
1083
+ // ビルド済みクライアントファイル配信プラグイン
1019
1084
  // npmパッケージとして使用時、/server/client/ へのリクエストをdist内のファイルから配信
1020
1085
  {
1021
1086
  name: "chronoter-built-client",
1087
+ // Viteのモジュール解決フック: /server/client/main.js を解決
1088
+ // transformIndexHtml がHTMLを解析する際にモジュール解決を試みるため必要
1089
+ resolveId(id) {
1090
+ if (id === "/server/client/main.js" || id === "/server/client/main.css") {
1091
+ const fileName = id.replace("/server/client/", "");
1092
+ const clientFilePath = resolve(__dirname2, "server/client", fileName);
1093
+ if (existsSync(clientFilePath)) {
1094
+ return clientFilePath;
1095
+ }
1096
+ }
1097
+ return null;
1098
+ },
1022
1099
  configureServer(server) {
1023
1100
  server.middlewares.use((req, res, next) => {
1024
1101
  const url = req.url || "";
@@ -1649,7 +1726,7 @@ var runVersionCheck = async (packageName, currentVersion) => {
1649
1726
 
1650
1727
  // src/cli/index.ts
1651
1728
  var PACKAGE_NAME = "chronoter";
1652
- var CURRENT_VERSION = "0.1.10";
1729
+ var CURRENT_VERSION = "0.2.0";
1653
1730
  var createCliProgram = () => {
1654
1731
  const program = new Command();
1655
1732
  program.name("chronoter").description("Chronoter - MDX-based documentation site generator").version(CURRENT_VERSION);
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ import { visit } from 'unist-util-visit';
7
7
  import { compile } from '@mdx-js/mdx';
8
8
  import matter from 'gray-matter';
9
9
  import remarkGfm from 'remark-gfm';
10
+ import rehypeSlug from 'rehype-slug';
11
+ import { unified } from 'unified';
12
+ import remarkParse from 'remark-parse';
13
+ import GithubSlugger from 'github-slugger';
10
14
  import { fileURLToPath } from 'url';
11
15
  import chalk2 from 'chalk';
12
16
  import { defineConfig, createServer } from 'vite';
@@ -232,12 +236,14 @@ var init_processor = __esm({
232
236
  throw new MDXError(`Failed to parse frontmatter: ${message}`, filePath);
233
237
  }
234
238
  const code = await this.compile(content, filePath, options?.baseUrl);
239
+ const headings = this.extractHeadings(content);
235
240
  const slug = this.generateSlug(filePath);
236
241
  const frontmatter = this.parseFrontmatter(data);
237
242
  return {
238
243
  code,
239
244
  frontmatter,
240
- slug
245
+ slug,
246
+ headings
241
247
  };
242
248
  } catch (error) {
243
249
  if (error instanceof MDXError) {
@@ -290,7 +296,8 @@ var init_processor = __esm({
290
296
  const result = await compile(source, {
291
297
  outputFormat: "function-body",
292
298
  development: false,
293
- remarkPlugins
299
+ remarkPlugins,
300
+ rehypePlugins: [rehypeSlug]
294
301
  });
295
302
  return String(result);
296
303
  } catch (error) {
@@ -315,6 +322,52 @@ var init_processor = __esm({
315
322
  const fileName = basename(filePath, extname(filePath));
316
323
  return fileName;
317
324
  }
325
+ /**
326
+ * Markdownコンテンツから見出しを抽出する
327
+ *
328
+ * @param content - Markdownコンテンツ
329
+ * @returns 見出しの配列(h2, h3のみ)
330
+ * @private
331
+ */
332
+ extractHeadings(content) {
333
+ const headings = [];
334
+ const slugger = new GithubSlugger();
335
+ try {
336
+ const tree = unified().use(remarkParse).parse(content);
337
+ visit(tree, "heading", (node) => {
338
+ if (node.depth === 2 || node.depth === 3) {
339
+ const text = this.extractTextFromNode(node.children);
340
+ const id = slugger.slug(text);
341
+ headings.push({
342
+ id,
343
+ text,
344
+ level: node.depth
345
+ });
346
+ }
347
+ });
348
+ } catch (error) {
349
+ console.warn("Failed to extract headings:", error);
350
+ }
351
+ return headings;
352
+ }
353
+ /**
354
+ * 見出しノードの子要素からテキストを抽出する
355
+ *
356
+ * @param children - 見出しノードの子要素
357
+ * @returns テキスト
358
+ * @private
359
+ */
360
+ extractTextFromNode(children) {
361
+ return children.map((child) => {
362
+ if (child.type === "text") {
363
+ return child.value;
364
+ }
365
+ if ("children" in child && Array.isArray(child.children)) {
366
+ return this.extractTextFromNode(child.children);
367
+ }
368
+ return "";
369
+ }).join("");
370
+ }
318
371
  };
319
372
  }
320
373
  });
@@ -441,16 +494,17 @@ var init_router = __esm({
441
494
  /**
442
495
  * ファイルパスからルートを作成
443
496
  *
444
- * @param filePath - MDXまたはMDファイルのパス
445
- * @param baseDir - ベースディレクトリパス
497
+ * @param filePath - MDXまたはMDファイルの絶対パス
498
+ * @param baseDir - ベースディレクトリパス(docsDir)
446
499
  * @returns 生成されたルート
447
500
  */
448
501
  async createRoute(filePath, baseDir) {
449
502
  const frontmatter = await this.extractFrontmatter(filePath);
450
503
  const urlPath = this.filePathToUrlPath(filePath, baseDir);
504
+ const relativeFilePath = path.relative(baseDir, filePath);
451
505
  return {
452
506
  path: urlPath,
453
- filePath,
507
+ filePath: relativeFilePath,
454
508
  frontmatter
455
509
  };
456
510
  }
@@ -508,12 +562,17 @@ var routing_middleware_exports = {};
508
562
  __export(routing_middleware_exports, {
509
563
  createRoutingMiddleware: () => createRoutingMiddleware
510
564
  });
511
- var __filename$1, __dirname$1, createRoutingMiddleware, handleAllRoutesRequest, handleRouteRequest, handleMDXRequest, handle500;
565
+ var __filename$1, __dirname$1, hasFileExtension, createRoutingMiddleware, handleAllRoutesRequest, handleRouteRequest, handleMDXRequest, handle500;
512
566
  var init_routing_middleware = __esm({
513
567
  "src/server/routing-middleware.ts"() {
514
568
  init_router();
515
569
  __filename$1 = fileURLToPath(import.meta.url);
516
570
  __dirname$1 = dirname(__filename$1);
571
+ hasFileExtension = (urlPath) => {
572
+ const lastSegment = urlPath.split("/").pop() || "";
573
+ const dotIndex = lastSegment.lastIndexOf(".");
574
+ return dotIndex > 0;
575
+ };
517
576
  createRoutingMiddleware = (server, options) => {
518
577
  const { config, cwd } = options;
519
578
  const routeGenerator = new RouteGenerator();
@@ -564,7 +623,8 @@ var init_routing_middleware = __esm({
564
623
  }
565
624
  const urlPath = url.split("?")[0];
566
625
  if (urlPath === "/__chronoter_mdx__") {
567
- return handleMDXRequest(req, res, cwd);
626
+ const docsDir = config.docsDir ? resolve(cwd, config.docsDir) : cwd;
627
+ return handleMDXRequest(req, res, docsDir);
568
628
  }
569
629
  if (urlPath === "/__chronoter_routes__") {
570
630
  return handleRouteRequest(req, res, getRoutes, routeGenerator);
@@ -574,7 +634,7 @@ var init_routing_middleware = __esm({
574
634
  }
575
635
  if (urlPath.startsWith("/@") || // Vite内部リクエスト
576
636
  urlPath.startsWith("/node_modules") || urlPath.startsWith("/src/") || // Viteが処理するソースファイル
577
- urlPath.includes(".") && !urlPath.endsWith("/") && !urlPath.startsWith("/src/")) {
637
+ hasFileExtension(urlPath) && !urlPath.endsWith("/") && !urlPath.startsWith("/src/")) {
578
638
  return next();
579
639
  }
580
640
  try {
@@ -684,7 +744,7 @@ var init_routing_middleware = __esm({
684
744
  );
685
745
  }
686
746
  };
687
- handleMDXRequest = async (req, res, cwd) => {
747
+ handleMDXRequest = async (req, res, docsDir) => {
688
748
  try {
689
749
  const url = new URL(req.url || "", `http://${req.headers.host}`);
690
750
  const filePath = url.searchParams.get("path");
@@ -694,8 +754,8 @@ var init_routing_middleware = __esm({
694
754
  res.end(JSON.stringify({ error: "Missing 'path' query parameter" }));
695
755
  return;
696
756
  }
697
- const absolutePath = resolve(cwd, filePath);
698
- if (!absolutePath.startsWith(cwd)) {
757
+ const absolutePath = resolve(docsDir, filePath);
758
+ if (!absolutePath.startsWith(docsDir)) {
699
759
  res.statusCode = 403;
700
760
  res.setHeader("Content-Type", "application/json");
701
761
  res.end(JSON.stringify({ error: "Access denied" }));
@@ -710,7 +770,8 @@ var init_routing_middleware = __esm({
710
770
  JSON.stringify({
711
771
  code: processedMDX.code,
712
772
  frontmatter: processedMDX.frontmatter,
713
- slug: processedMDX.slug
773
+ slug: processedMDX.slug,
774
+ headings: processedMDX.headings
714
775
  })
715
776
  );
716
777
  } catch (error) {
@@ -819,7 +880,7 @@ var ConfigError = class _ConfigError extends Error {
819
880
  }
820
881
  };
821
882
  var siteConfigSchema = z.object({
822
- title: z.string().min(1, "site.title is required"),
883
+ title: z.string().min(1, "site.title must not be empty").optional(),
823
884
  description: z.string().optional(),
824
885
  baseUrl: z.string().url("site.baseUrl must be a valid URL").optional()
825
886
  });
@@ -835,7 +896,7 @@ var themeConfigSchema = z.object({
835
896
  logo: z.string().optional()
836
897
  });
837
898
  var chronoterConfigSchema = z.object({
838
- site: siteConfigSchema,
899
+ site: siteConfigSchema.optional(),
839
900
  navigation: z.array(navigationItemSchema).optional(),
840
901
  docsDir: z.string().min(1, "docsDir must not be empty").optional(),
841
902
  outDir: z.string().optional(),
@@ -845,6 +906,10 @@ var chronoterConfigSchema = z.object({
845
906
 
846
907
  // src/core/config/defaults.ts
847
908
  var defaultConfig = {
909
+ site: {
910
+ title: "Chronoter Documentation"
911
+ // デフォルトタイトル
912
+ },
848
913
  docsDir: ".",
849
914
  // デフォルトはルートディレクトリ
850
915
  theme: {
@@ -853,9 +918,12 @@ var defaultConfig = {
853
918
  }
854
919
  };
855
920
  var mergeWithDefaults = (userConfig) => {
856
- const site = userConfig.site;
857
921
  return {
858
- site,
922
+ site: {
923
+ title: userConfig.site?.title ?? defaultConfig.site.title,
924
+ description: userConfig.site?.description,
925
+ baseUrl: userConfig.site?.baseUrl
926
+ },
859
927
  docsDir: userConfig.docsDir ?? defaultConfig.docsDir,
860
928
  navigation: userConfig.navigation,
861
929
  theme: {
@@ -891,10 +959,7 @@ var ConfigLoader = class {
891
959
  return this.validate(parsedConfig);
892
960
  } catch (error) {
893
961
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
894
- throw new ConfigError(
895
- `Config file not found: ${configPath}
896
- Please create a chronoter.config.json file.`
897
- );
962
+ return defaultConfig;
898
963
  }
899
964
  if (error instanceof ConfigError) {
900
965
  throw error;
@@ -1010,10 +1075,22 @@ var createViteConfig = (config, options = {}) => {
1010
1075
  );
1011
1076
  }
1012
1077
  },
1013
- // ビルド済みクライアントファイル配信ミドルウェアを登録するプラグイン
1078
+ // ビルド済みクライアントファイル配信プラグイン
1014
1079
  // npmパッケージとして使用時、/server/client/ へのリクエストをdist内のファイルから配信
1015
1080
  {
1016
1081
  name: "chronoter-built-client",
1082
+ // Viteのモジュール解決フック: /server/client/main.js を解決
1083
+ // transformIndexHtml がHTMLを解析する際にモジュール解決を試みるため必要
1084
+ resolveId(id) {
1085
+ if (id === "/server/client/main.js" || id === "/server/client/main.css") {
1086
+ const fileName = id.replace("/server/client/", "");
1087
+ const clientFilePath = resolve(__dirname2, "server/client", fileName);
1088
+ if (existsSync(clientFilePath)) {
1089
+ return clientFilePath;
1090
+ }
1091
+ }
1092
+ return null;
1093
+ },
1017
1094
  configureServer(server) {
1018
1095
  server.middlewares.use((req, res, next) => {
1019
1096
  const url = req.url || "";