Skip to main content

dmc/engine/
index.rs

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