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 crate::engine::collection::Collection;
13use crate::engine::schema_ts::schema_to_ts_object;
14
15use crate::engine::utils::{pascal_case, relative_from};
16
17/// Pick the right `.d.ts` mode and write `index.js` + `index.d.ts`.
18pub fn write_index(
19  out_dir: &Path,
20  collections: &[Collection],
21  format: &str,
22  config_path: Option<&Path>,
23) -> std::io::Result<()> {
24  let names: Vec<&str> = collections.iter().map(|c| c.name.as_str()).collect();
25  write_index_js(out_dir, &names, format)?;
26
27  let ts_cfg =
28    config_path.filter(|p| matches!(p.extension().and_then(|s| s.to_str()), Some("ts") | Some("js") | Some("mjs"),));
29
30  match ts_cfg {
31    Some(p) => {
32      let rel = relative_from(out_dir, p);
33      write_dts_velite_style(out_dir, &names, &rel)?;
34    },
35    None => write_dts_self_contained(out_dir, collections)?,
36  }
37  Ok(())
38}
39
40fn write_index_js(out_dir: &Path, names: &[&str], format: &str) -> std::io::Result<()> {
41  let mut js = String::from("// This file is generated by dmc. DO NOT EDIT.\n\n");
42  if format == "cjs" {
43    for name in names {
44      js.push_str(&format!("exports.{name} = require('./{name}.json')\n"));
45    }
46  } else {
47    for name in names {
48      js.push_str(&format!("export {{ default as {name} }} from './{name}.json' with {{ type: 'json' }}\n",));
49    }
50  }
51  std::fs::write(out_dir.join("index.js"), js)
52}
53
54fn write_dts_self_contained(out_dir: &Path, collections: &[Collection]) -> std::io::Result<()> {
55  let mut dts = String::from(
56    "// This file is generated by dmc. DO NOT EDIT.
57
58export interface TocItem { title: string; url: string; items: TocItem[] }
59export interface Metadata { readingTime: number; wordCount: number }
60export interface BaseCollection {
61  body: string
62  content: string
63  excerpt: string
64  metadata: Metadata
65  toc: TocItem[]
66  contentType: string
67  flattenedPath: string
68  permalink: string
69  slug: string
70  sourceFileDir: string
71  sourceFileName: string
72  sourceFilePath: string
73  html?: string
74}
75export interface DocCollection extends BaseCollection { [frontmatterField: string]: unknown }
76
77",
78  );
79
80  for c in collections {
81    let iface = pascal_case(&c.name) + "Collection";
82    match &c.schema {
83      Some(schema) => {
84        dts.push_str(&format!("export interface {iface} extends BaseCollection "));
85        dts.push_str(&schema_to_ts_object(schema));
86        dts.push('\n');
87        dts.push_str(&format!("export declare const {}: {iface}[]\n\n", c.name));
88      },
89      None => {
90        dts.push_str(&format!("export declare const {}: DocCollection[]\n\n", c.name));
91      },
92    }
93  }
94
95  std::fs::write(out_dir.join("index.d.ts"), dts)
96}
97
98fn write_dts_velite_style(out_dir: &Path, names: &[&str], cfg_rel: &str) -> std::io::Result<()> {
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  std::fs::write(out_dir.join("index.d.ts"), dts)
109}