Skip to main content

dmc/engine/
index.rs

1//! Top-level entry emission: `index.js` + `index.d.ts` that re-export
2//! every collection's `<name>.json`.
3//!
4//! Two `.d.ts` modes:
5//! - **Velite-style** (config is `.ts`/`.js`/`.mjs`): re-import the user's
6//!   config, infer record types via `typeof import(..)['collections']`.
7//! - **Self-contained** (TOML / no config): per-collection TS interface
8//!   from `schema = { ... }`, else the generic `DocRecord` shape.
9
10use std::path::Path;
11
12use dmc_diagnostic::{Code, DiagResult};
13use duck_diagnostic::diag;
14
15use crate::engine::collection::Collection;
16use crate::engine::schema_ts::schema_to_ts_object;
17
18use crate::engine::utils::{pascal_case, relative_from};
19
20/// Pick the right `.d.ts` mode and write `index.js` + `index.d.ts`.
21pub fn write_index(out_dir: &Path, collections: &[Collection], format: &str, config_path: Option<&Path>) -> DiagResult {
22  let names: Vec<&str> = collections.iter().map(|c| c.name.as_str()).collect();
23  write_index_js(out_dir, &names, format)?;
24
25  let ts_cfg =
26    config_path.filter(|p| matches!(p.extension().and_then(|s| s.to_str()), Some("ts") | Some("js") | Some("mjs"),));
27
28  match ts_cfg {
29    Some(p) => {
30      let rel = relative_from(out_dir, p);
31      write_dts_velite_style(out_dir, &names, &rel)?;
32    },
33    None => write_dts_self_contained(out_dir, collections)?,
34  }
35  Ok(())
36}
37
38fn write_index_js(out_dir: &Path, names: &[&str], format: &str) -> DiagResult {
39  let mut js = String::from("// This file is generated by dmc. DO NOT EDIT.\n\n");
40  if format == "cjs" {
41    for name in names {
42      js.push_str(&format!("exports.{name} = require('./{name}.json')\n"));
43    }
44  } else {
45    for name in names {
46      js.push_str(&format!("export {{ default as {name} }} from './{name}.json' with {{ type: 'json' }}\n",));
47    }
48  }
49  let path = out_dir.join("index.js");
50  std::fs::write(&path, js).map_err(|e| diag!(Code::IoWrite, format!("write {}: {}", path.display(), e)))
51}
52
53fn write_dts_self_contained(out_dir: &Path, collections: &[Collection]) -> DiagResult {
54  let mut dts = String::from(
55    "// This file is generated by dmc. DO NOT EDIT.
56
57export interface TocItem { title: string; url: string; items: TocItem[] }
58export interface Metadata { readingTime: number; wordCount: number }
59export interface BaseCollection {
60  body: string
61  content: string
62  excerpt: string
63  metadata: Metadata
64  toc: TocItem[]
65  contentType: string
66  flattenedPath: string
67  permalink: string
68  slug: string
69  sourceFileDir: string
70  sourceFileName: string
71  sourceFilePath: string
72  html?: string
73}
74export interface DocCollection extends BaseCollection { [frontmatterField: string]: unknown }
75
76",
77  );
78
79  for c in collections {
80    let iface = pascal_case(&c.name) + "Collection";
81    match &c.schema {
82      Some(schema) => {
83        dts.push_str(&format!("export interface {iface} extends BaseCollection "));
84        dts.push_str(&schema_to_ts_object(schema));
85        dts.push('\n');
86        dts.push_str(&format!("export declare const {}: {iface}[]\n\n", c.name));
87      },
88      None => {
89        dts.push_str(&format!("export declare const {}: DocCollection[]\n\n", c.name));
90      },
91    }
92  }
93
94  let path = out_dir.join("index.d.ts");
95  std::fs::write(&path, dts).map_err(|e| diag!(Code::IoWrite, format!("write {}: {}", path.display(), e)))
96}
97
98fn write_dts_velite_style(out_dir: &Path, names: &[&str], cfg_rel: &str) -> DiagResult {
99  let mut dts = String::from("// This file is generated by dmc. DO NOT EDIT.\n\n");
100  dts.push_str(&format!("import type __dmc from '{cfg_rel}'\n\n"));
101  dts.push_str("type Collections = typeof __dmc['collections']\n");
102  dts.push_str("type CollectionName = keyof Collections\n");
103  dts.push_str("type RecordOf<K extends CollectionName> =\n");
104  dts.push_str("  Collections[K] extends { schema: { _output: infer T } } ? T : unknown\n\n");
105  for name in names {
106    dts.push_str(&format!("export declare const {name}: RecordOf<'{name}'>[]\n"));
107  }
108  let path = out_dir.join("index.d.ts");
109  std::fs::write(&path, dts).map_err(|e| diag!(Code::IoWrite, format!("write {}: {}", path.display(), e)))
110}