1use crate::add_template;
6use crate::content::add_obj;
7use anyhow::bail;
8use fs_err::File;
9use ordinary_config::TemplateRefFieldBind::Segment;
10use ordinary_config::{
11 AssetsConfig, Content, ContentDefinition, ContentObjectLifecycle, FragmentsConfig, Global,
12 LifecycleBeforeAfterScripts, OrdinaryConfig, TemplateConfig, TemplateFfi,
13 TemplateFfiSerialization, TemplateFfiVersion, TemplateRef, TemplateRefField, TopLevelLifecycle,
14};
15use ordinary_types::{ContentField, ContentObject, Field, Kind, TimeUnit};
16use serde_json::Value;
17use std::io::Write;
18use std::path::Path;
19use std::time::SystemTime;
20use tracing::instrument;
21
22const GITIGNORE: &[u8] = include_bytes!(concat!(
23 env!("CARGO_MANIFEST_DIR"),
24 "/static/project/.gitignore"
25));
26
27const README: &[u8] = include_bytes!(concat!(
28 env!("CARGO_MANIFEST_DIR"),
29 "/static/project/README.md"
30));
31
32const HOOKS_POSTS_SRC_MAIN_RS: &[u8] = include_bytes!(concat!(
33 env!("CARGO_MANIFEST_DIR"),
34 "/static/project/hooks/posts/src/main.rs"
35));
36
37const HOOKS_POSTS_CARGO_TOML: &[u8] = include_bytes!(concat!(
38 env!("CARGO_MANIFEST_DIR"),
39 "/static/project/hooks/posts/Freight.toml"
40));
41
42const TMP_CSS: &[u8] = include_bytes!(concat!(
43 env!("CARGO_MANIFEST_DIR"),
44 "/static/project/assets/themes/tmp.css"
45));
46
47const FAVICON: &[u8] = include_bytes!(concat!(
48 env!("CARGO_MANIFEST_DIR"),
49 "/static/project/assets/favicon.svg"
50));
51
52const INDEX: &[u8] = include_bytes!(concat!(
53 env!("CARGO_MANIFEST_DIR"),
54 "/static/project/templates/index.html"
55));
56
57const POST: &[u8] = include_bytes!(concat!(
58 env!("CARGO_MANIFEST_DIR"),
59 "/static/project/templates/post.html"
60));
61
62const SITEMAP: &[u8] = include_bytes!(concat!(
63 env!("CARGO_MANIFEST_DIR"),
64 "/static/project/templates/sitemap.xml"
65));
66
67const ROBOTS: &[u8] = include_bytes!(concat!(
68 env!("CARGO_MANIFEST_DIR"),
69 "/static/project/templates/robots.txt"
70));
71
72const RSS: &[u8] = include_bytes!(concat!(
73 env!("CARGO_MANIFEST_DIR"),
74 "/static/project/templates/rss.xml"
75));
76
77const HEADER: &[u8] = include_bytes!(concat!(
78 env!("CARGO_MANIFEST_DIR"),
79 "/static/project/fragments/header.html"
80));
81
82const FOOTER: &[u8] = include_bytes!(concat!(
83 env!("CARGO_MANIFEST_DIR"),
84 "/static/project/fragments/footer.html"
85));
86
87#[allow(clippy::too_many_lines)]
88#[instrument(skip_all, err)]
89pub fn new(path: &str, domain: &str) -> anyhow::Result<()> {
90 let path = Path::new(path).join(domain);
91
92 if path.exists() {
93 bail!("project already exists");
94 }
95
96 tracing::info!("creating project at {:?}", path);
97 fs_err::create_dir_all(&path)?;
98
99 let hooks_posts_path = path.join("hooks").join("posts");
100 fs_err::create_dir_all(hooks_posts_path.join("src"))?;
101
102 let mut file = File::create(hooks_posts_path.join("src").join("main.rs"))?;
103 file.write_all(HOOKS_POSTS_SRC_MAIN_RS)?;
104 file.flush()?;
105
106 let mut file = File::create(hooks_posts_path.join("Cargo.toml"))?;
107 file.write_all(HOOKS_POSTS_CARGO_TOML)?;
108 file.flush()?;
109
110 let app_config = OrdinaryConfig {
111 lifecycle: Some(TopLevelLifecycle {
112 before_all: Some(vec![
113 [
114 "cargo",
115 "build",
116 "--release",
117 "--manifest-path",
118 "hooks/posts/Cargo.toml",
119 ]
120 .iter()
121 .map(ToString::to_string)
122 .collect::<Vec<_>>(),
123 ]),
124 build: Some(LifecycleBeforeAfterScripts {
125 before: Some(vec![
126 [
127 "hooks/posts/target/release/posts",
128 "before_build",
129 "content/posts",
130 ]
131 .iter()
132 .map(ToString::to_string)
133 .collect::<Vec<_>>(),
134 ]),
135 after: None,
136 }),
137 }),
138 domain: domain.to_string(),
139
140 version: "0.1.0".into(),
141 contacts: Some(vec![]),
142
143 storage_size: Some(5_000_000),
144
145 port: Some(4433),
146 redirect_port: Some(8080),
147 globals: Some(vec![Global {
148 name: "author".to_string(),
149 kind: Kind::String,
150 value: "Your Name".into(),
151 }]),
152
153 fragments: Some(FragmentsConfig {
154 dir_path: "fragments".to_string(),
155 }),
156 error: None,
157 assets: Some(AssetsConfig {
158 dir_path: Some("assets".to_string()),
159 ..Default::default()
160 }),
161
162 content: Some(Content {
163 file_path: "./content.json".to_string(),
164 definitions: vec![ContentDefinition {
165 idx: 0,
166 name: "post".to_string(),
167 fields: vec![
168 Field {
169 idx: 0,
170 name: "slug".to_string(),
171 kind: Kind::String,
172 indexed: Some(true),
173 ..Default::default()
174 },
175 Field {
176 idx: 1,
177 name: "title".to_string(),
178 kind: Kind::String,
179 ..Default::default()
180 },
181 Field {
182 idx: 2,
183 name: "date".to_string(),
184 kind: Kind::Timestamp {
185 unit: TimeUnit::Seconds,
186 },
187 ..Default::default()
188 },
189 Field {
190 idx: 3,
191 name: "body".to_string(),
192 kind: Kind::Markdown,
193 ..Default::default()
194 },
195 ],
196 lifecycle: Some(ContentObjectLifecycle {
197 before_all: None,
198 on_add: Some(LifecycleBeforeAfterScripts {
199 after: Some(vec![
200 [
201 "hooks/posts/target/release/posts",
202 "after_add",
203 "content/posts",
204 ]
205 .iter()
206 .map(ToString::to_string)
207 .collect::<Vec<_>>(),
208 ]),
209 before: None,
210 }),
211 on_edit: Some(LifecycleBeforeAfterScripts {
212 after: Some(vec![
213 [
214 "hooks/posts/target/release/posts",
215 "after_edit",
216 "content/posts",
217 ]
218 .iter()
219 .map(ToString::to_string)
220 .collect::<Vec<_>>(),
221 ]),
222 before: None,
223 }),
224 on_delete: Some(LifecycleBeforeAfterScripts {
225 after: Some(vec![
226 [
227 "hooks/posts/target/release/posts",
228 "after_delete",
229 "content/posts",
230 ]
231 .iter()
232 .map(ToString::to_string)
233 .collect::<Vec<_>>(),
234 ]),
235 before: None,
236 }),
237 }),
238 }],
239 ..Default::default()
240 }),
241
242 templates: Some(vec![
243 TemplateConfig {
244 ffi: TemplateFfi {
245 version: TemplateFfiVersion::V1,
246 serialization: TemplateFfiSerialization::FlexBufferVector,
247 },
248 idx: 0,
249 name: "index".to_string(),
250 mime: "text/html".to_string(),
251 path: Some("./templates/index.html".to_string()),
252 route: "/".to_string(),
253 globals: Some(vec!["author".to_string()]),
254 content: Some(vec![TemplateRef {
255 idx: 0,
256 name: "post".to_string(),
257 fields: vec![
258 TemplateRefField {
259 idx: 0,
260 name: "slug".to_string(),
261 ..Default::default()
262 },
263 TemplateRefField {
264 idx: 1,
265 name: "title".to_string(),
266 ..Default::default()
267 },
268 TemplateRefField {
269 idx: 2,
270 name: "date".to_string(),
271 ..Default::default()
272 },
273 ],
274 all: Some("posts".to_string()),
275 }]),
276 ..Default::default()
277 },
278 TemplateConfig {
279 ffi: TemplateFfi {
280 version: TemplateFfiVersion::V1,
281 serialization: TemplateFfiSerialization::FlexBufferVector,
282 },
283 idx: 1,
284 name: "post".to_string(),
285 mime: "text/html".to_string(),
286 path: Some("./templates/post.html".to_string()),
287 route: "/posts/{slug}".to_string(),
288 globals: Some(vec!["author".to_string()]),
289 content: Some(vec![TemplateRef {
290 idx: 0,
291 name: "post".to_string(),
292 fields: vec![
293 TemplateRefField {
294 idx: 0,
295 name: "slug".to_string(),
296 bind: Some(Segment {
297 name: "slug".to_string(),
298 expression: None,
299 }),
300 ..Default::default()
301 },
302 TemplateRefField {
303 idx: 1,
304 name: "title".to_string(),
305 ..Default::default()
306 },
307 TemplateRefField {
308 idx: 2,
309 name: "date".to_string(),
310 ..Default::default()
311 },
312 TemplateRefField {
313 idx: 3,
314 name: "body".to_string(),
315 ..Default::default()
316 },
317 ],
318 all: None,
319 }]),
320 ..Default::default()
321 },
322 ]),
323 ..Default::default()
324 };
325
326 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
327
328 let mut file = File::create(path.join("ordinary.json"))?;
329 file.write_all(ordinary_json.as_bytes())?;
330 file.flush()?;
331
332 let mut file = File::create(path.join("content.json"))?;
333 file.write_all(b"[]")?;
334 file.flush()?;
335
336 let first_post_json = serde_json::to_string_pretty(&ContentObject {
337 uuid: uuid::Uuid::new_v4().to_string(),
338 instance_of: "post".to_string(),
339 fields: vec.".to_string(),
361 ),
362 },
363 ],
364 })?;
365
366 if let Some(path_str) = path.to_str() {
367 add_obj(path_str, &first_post_json)?;
368 }
369
370 let mut file = File::create(path.join(".gitignore"))?;
371 file.write_all(GITIGNORE)?;
372 file.flush()?;
373
374 let mut file = File::create(path.join("README.md"))?;
375 file.write_all(README)?;
376 file.flush()?;
377
378 let assets_dir = path.join("assets");
379 let themes_dir = assets_dir.join("themes");
380
381 fs_err::create_dir_all(&themes_dir)?;
382
383 let mut file = File::create(themes_dir.join("tmp.css"))?;
384 file.write_all(TMP_CSS)?;
385 file.flush()?;
386
387 let mut file = File::create(assets_dir.join("favicon.svg"))?;
388 file.write_all(FAVICON)?;
389 file.flush()?;
390
391 let fragments_dir = path.join("fragments");
392 fs_err::create_dir_all(&fragments_dir)?;
393
394 let mut header_file = File::create(fragments_dir.join("header.html"))?;
395 header_file.write_all(HEADER)?;
396 header_file.flush()?;
397
398 let mut footer_file = File::create(fragments_dir.join("footer.html"))?;
399 footer_file.write_all(FOOTER)?;
400 footer_file.flush()?;
401
402 let html_globals = Some(vec!["author".to_string()]);
403
404 if let Some(path_str) = path.to_str() {
405 let templates_dir = path.join("templates");
406 fs_err::create_dir_all(&templates_dir)?;
407
408 let mut index_file = File::create(templates_dir.join("index.html"))?;
409 index_file.write_all(INDEX)?;
410 index_file.flush()?;
411
412 let mut post_file = File::create(templates_dir.join("post.html"))?;
413 post_file.write_all(POST)?;
414 post_file.flush()?;
415
416 add_template(
417 path_str,
418 "error",
419 "/error",
420 "text/html",
421 r#"
422 <link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
423 <link rel="stylesheet" href="/assets/themes/tmp.css">
424"#,
425 r#"{% include "fragments/header.html" %}"#,
426 r#"{% include "fragments/footer.html" %}"#,
427 true,
428 html_globals,
429 None,
430 )?;
431
432 add_template(
433 path_str,
434 "sitemap",
435 "/sitemap.xml",
436 "text/xml",
437 "",
438 "",
439 "",
440 false,
441 None,
442 Some(vec![TemplateRef {
443 idx: 0,
444 name: "post".to_string(),
445 fields: vec![
446 TemplateRefField {
447 idx: 0,
448 name: "slug".to_string(),
449 ..Default::default()
450 },
451 TemplateRefField {
452 idx: 1,
453 name: "date".to_string(),
454 ..Default::default()
455 },
456 ],
457 all: Some("posts".to_string()),
458 }]),
459 )?;
460
461 let mut sitemap_file = File::create(templates_dir.join("sitemap.xml"))?;
462 sitemap_file.write_all(SITEMAP)?;
463 sitemap_file.flush()?;
464
465 add_template(
466 path_str,
467 "robots",
468 "/robots.txt",
469 "text/plain",
470 "",
471 "",
472 "",
473 false,
474 None,
475 None,
476 )?;
477
478 let mut robots_file = File::create(templates_dir.join("robots.txt"))?;
479 robots_file.write_all(ROBOTS)?;
480 robots_file.flush()?;
481
482 add_template(
483 path_str,
484 "rss",
485 "/rss",
486 "application/rss+xml",
487 "",
488 "",
489 "",
490 false,
491 Some(vec!["author".to_string()]),
492 Some(vec![TemplateRef {
493 idx: 0,
494 name: "post".to_string(),
495 fields: vec![
496 TemplateRefField {
497 idx: 0,
498 name: "slug".to_string(),
499 ..Default::default()
500 },
501 TemplateRefField {
502 idx: 1,
503 name: "title".to_string(),
504 ..Default::default()
505 },
506 TemplateRefField {
507 idx: 2,
508 name: "date".to_string(),
509 ..Default::default()
510 },
511 ],
512 all: Some("posts".to_string()),
513 }]),
514 )?;
515
516 let mut robots_file = File::create(templates_dir.join("rss.xml"))?;
517 robots_file.write_all(RSS)?;
518 robots_file.flush()?;
519 }
520
521 Ok(())
522}