1use std::collections::{BTreeMap, HashMap};
2use std::fs;
3use std::path::{Component, Path, PathBuf};
4
5use crate::discovery::read_source;
6use crate::json::json_string;
7
8pub fn is_unpinned_range(range: &str) -> bool {
13 let r = range.trim();
14 if r.is_empty() || r == "*" || r.eq_ignore_ascii_case("x") || r.eq_ignore_ascii_case("latest") {
15 return true;
16 }
17 !r.chars().any(|c| c.is_ascii_digit())
18}
19
20pub fn render_package_json(deps: &std::collections::BTreeMap<String, String>) -> String {
29 let mut out = String::from("{\n \"type\": \"module\"");
30 if !deps.is_empty() {
31 out.push_str(",\n \"dependencies\": {\n");
32 let entries: Vec<String> = deps
33 .iter()
34 .map(|(pkg, range)| format!(" {}: {}", json_string(pkg), json_string(range)))
35 .collect();
36 out.push_str(&entries.join(",\n"));
37 out.push_str("\n }");
38 }
39 out.push_str("\n}\n");
40 out
41}
42
43pub fn normalize_rel(p: &Path) -> PathBuf {
47 let mut out: Vec<std::ffi::OsString> = Vec::new();
48 for c in p.components() {
49 match c {
50 Component::CurDir => {}
51 Component::ParentDir => {
52 out.pop();
53 }
54 Component::Normal(s) => out.push(s.to_os_string()),
55 Component::RootDir | Component::Prefix(_) => {}
56 }
57 }
58 out.iter().collect()
59}
60
61#[derive(Debug, Clone)]
68pub struct ProjectPaths {
69 pub include: Vec<PathBuf>,
73 pub exclude: Vec<PathBuf>,
76}
77
78impl ProjectPaths {
79 pub fn conventional(project_root: &Path) -> Self {
85 let mut include = Vec::new();
86 for role in ["src", "tests"] {
87 if project_root.join(role).is_dir() {
88 include.push(PathBuf::from(role));
89 }
90 }
91 if include.is_empty() {
92 include.push(PathBuf::from("."));
93 }
94 ProjectPaths {
95 include,
96 exclude: Vec::new(),
97 }
98 }
99}
100
101#[cfg(test)]
110pub(crate) fn read_project_paths_with(
111 project_root: &Path,
112 overlay: &HashMap<PathBuf, String>,
113) -> ProjectPaths {
114 try_read_project_paths_with(project_root, overlay)
115 .unwrap_or_else(|_| ProjectPaths::conventional(project_root))
116}
117
118#[derive(Debug)]
121pub enum ProjectPathsError {
122 Malformed,
124 UnknownKey(String),
127 UnknownTable(String),
131 TopLevelKey(String),
133 UnknownTableKey { table: &'static str, key: String },
137 NotAStringList {
141 table: &'static str,
142 key: &'static str,
143 },
144 BadTestTarget { target: String, reason: String },
146 ProjectAndPackage,
149 BadPackage { key: &'static str, reason: String },
151 BadDependency {
154 table: &'static str,
155 alias: String,
156 reason: String,
157 },
158 UnknownEntryKey {
160 table: &'static str,
161 entry: String,
162 key: String,
163 },
164 DependenciesUnresolved { table: &'static str },
167 BadRegistry { name: String, reason: String },
169 RegistryCredential { name: String },
171 BadWorkspace { reason: String },
174 WorkspaceRootTable(String),
177 WorkspaceRootSources,
179 WorkspaceRoot,
182 MemberRegistries { workspace: PathBuf },
185 NestedWorkspace { workspace: PathBuf },
187}
188
189pub const MANIFEST_TABLES: &[(&str, &[&str])] = &[
193 ("project", &["name", "version"]),
194 (
195 "package",
196 &[
197 "name",
198 "version",
199 "description",
200 "license",
201 "repository",
202 "exports",
203 ],
204 ),
205 ("paths", &["include", "exclude"]),
206 ("fmt", &[]),
207 ("lsp", &["diagnostics_mode", "diagnostics_debounce_ms"]),
208 ("workers", &["compatibility_flags"]),
209 ("unstable", &["allow"]),
210 ("test", &["targets"]),
211 ("workspace", &["members", "exclude"]),
212];
213
214pub const MANIFEST_MAP_TABLES: &[(&str, &[&str])] = &[
220 ("dependencies", crate::manifest::DEPENDENCY_KEYS),
221 ("dev-dependencies", crate::manifest::DEPENDENCY_KEYS),
222 ("registries", crate::manifest::REGISTRY_KEYS),
223];
224
225const WORKSPACE_ROOT_TABLES: &[&str] = &["workspace", "registries"];
227
228const PLANNED_TABLES: &[(&str, &str)] = &[("deploy", "#551")];
231
232fn all_tables() -> impl Iterator<Item = &'static str> {
235 MANIFEST_TABLES
236 .iter()
237 .chain(MANIFEST_MAP_TABLES)
238 .map(|(t, _)| *t)
239}
240
241fn table_list(conj: &str) -> String {
245 let names: Vec<String> = all_tables().map(|t| format!("`[{t}]`")).collect();
246 match names.split_last() {
247 Some((last, rest)) if !rest.is_empty() => format!("{} {conj} {last}", rest.join(", ")),
248 _ => names.join(""),
249 }
250}
251
252pub(crate) fn did_you_mean<'a>(
254 name: &str,
255 candidates: impl IntoIterator<Item = &'a str>,
256) -> Option<&'a str> {
257 candidates
258 .into_iter()
259 .map(|c| (edit_distance(name, c), c))
260 .filter(|(d, _)| *d <= 2)
261 .min_by_key(|(d, _)| *d)
262 .map(|(_, c)| c)
263}
264
265fn edit_distance(a: &str, b: &str) -> usize {
267 let b: Vec<char> = b.chars().collect();
268 let mut row: Vec<usize> = (0..=b.len()).collect();
269 for (i, ca) in a.chars().enumerate() {
270 let mut prev = row[0];
271 row[0] = i + 1;
272 for (j, cb) in b.iter().enumerate() {
273 let cur = row[j + 1];
274 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
275 prev = cur;
276 }
277 }
278 row[b.len()]
279}
280
281impl std::fmt::Display for ProjectPathsError {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 ProjectPathsError::Malformed => write!(f, "`bynk.toml` is not valid TOML"),
285 ProjectPathsError::UnknownKey(k) => {
286 write!(
287 f,
288 "`[paths]` has no key named `{k}` — did you mean `include` or `exclude`?"
289 )
290 }
291 ProjectPathsError::UnknownTable(name) => {
292 write!(f, "`bynk.toml` has no table named `[{name}]` — ")?;
293 if let Some((_, issue)) = PLANNED_TABLES.iter().find(|(t, _)| *t == name) {
294 write!(f, "it is planned but not yet supported ({issue})")
295 } else if let Some(t) = did_you_mean(name, all_tables()) {
296 write!(f, "did you mean `[{t}]`?")
297 } else {
298 write!(f, "the tables are {}", table_list("and"))
299 }
300 }
301 ProjectPathsError::TopLevelKey(key) => {
302 if all_tables().any(|t| t == key) {
305 return write!(
306 f,
307 "`{key}` in `bynk.toml` must be a table — write it as `[{key}]`"
308 );
309 }
310 write!(f, "`bynk.toml` has a key `{key}` outside any table — ")?;
311 match MANIFEST_TABLES
312 .iter()
313 .find(|(_, keys)| keys.contains(&key.as_str()))
314 {
315 Some((table, _)) => write!(f, "did you mean it under `[{table}]`?"),
316 None => write!(f, "keys belong in {}", table_list("or")),
317 }
318 }
319 ProjectPathsError::UnknownTableKey { table, key } => {
320 let keys = MANIFEST_TABLES
321 .iter()
322 .find(|(t, _)| t == table)
323 .map(|(_, k)| *k)
324 .unwrap_or_default();
325 write!(f, "`[{table}]` has no key named `{key}` — ")?;
326 match did_you_mean(key, keys.iter().copied()) {
327 Some(k) => write!(f, "did you mean `{k}`?"),
328 None => {
329 let list: Vec<String> = keys.iter().map(|k| format!("`{k}`")).collect();
330 write!(f, "its keys are {}", list.join(", "))
331 }
332 }
333 }
334 ProjectPathsError::NotAStringList { table, key } => write!(
335 f,
336 "`[{table}]` `{key}` must be a list of strings, like `{key} = [\"nodejs_compat\"]`"
337 ),
338 ProjectPathsError::BadTestTarget { target, reason } => {
339 write!(f, "`[test.targets.{target}]` {reason}")
340 }
341 ProjectPathsError::ProjectAndPackage => write!(
342 f,
343 "`bynk.toml` has both `[project]` and `[package]` — a package is named by `[package] name`, so remove `[project]`"
344 ),
345 ProjectPathsError::BadPackage { key, reason } => {
346 write!(f, "`[package]` `{key}` {reason}")
347 }
348 ProjectPathsError::BadDependency {
349 table,
350 alias,
351 reason,
352 } => write!(f, "`[{table}]` `{alias}` {reason}"),
353 ProjectPathsError::UnknownEntryKey { table, entry, key } => {
354 let keys = MANIFEST_MAP_TABLES
355 .iter()
356 .find(|(t, _)| t == table)
357 .map(|(_, k)| *k)
358 .unwrap_or_default();
359 let place = if *table == "registries" {
360 format!("`[registries.{entry}]`")
361 } else {
362 format!("`[{table}]` `{entry}`")
363 };
364 write!(f, "{place} has no key named `{key}` — ")?;
365 match did_you_mean(key, keys.iter().copied()) {
366 Some(k) => write!(f, "did you mean `{k}`?"),
367 None => {
368 let list: Vec<String> = keys.iter().map(|k| format!("`{k}`")).collect();
369 write!(f, "its keys are {}", list.join(", "))
370 }
371 }
372 }
373 ProjectPathsError::DependenciesUnresolved { table } => write!(
374 f,
375 "`[{table}]` declares dependencies, but they can't be resolved until packaging slice 2b (#843) — remove them for now"
376 ),
377 ProjectPathsError::BadRegistry { name, reason } => {
378 write!(f, "`[registries.{name}]` {reason}")
379 }
380 ProjectPathsError::RegistryCredential { name } => write!(
381 f,
382 "`[registries.{name}]` has a `token` — credentials belong in `{}` or the user credentials file, never in `bynk.toml`",
383 crate::manifest::registry_token_var(name)
384 ),
385 ProjectPathsError::BadWorkspace { reason } => write!(f, "`[workspace]` {reason}"),
386 ProjectPathsError::WorkspaceRootTable(table) => write!(
387 f,
388 "a workspace root's `bynk.toml` holds only `[workspace]` and `[registries]` — `[{table}]` belongs in a member's `bynk.toml`"
389 ),
390 ProjectPathsError::WorkspaceRootSources => write!(
391 f,
392 "a workspace root holds no sources of its own — move them into a member"
393 ),
394 ProjectPathsError::WorkspaceRoot => write!(
395 f,
396 "this is a workspace root, not a project — run the command inside a member (`bynk check`, `fmt` and `test` run across every member from here)"
397 ),
398 ProjectPathsError::MemberRegistries { workspace } => write!(
399 f,
400 "`[registries]` belongs in the workspace root's `bynk.toml` (`{}`), not a member's",
401 workspace.join("bynk.toml").display()
402 ),
403 ProjectPathsError::NestedWorkspace { workspace } => write!(
404 f,
405 "this project is a member of the workspace at `{}`, so it can't declare a `[workspace]` of its own",
406 workspace.display()
407 ),
408 }
409 }
410}
411
412pub fn check_manifest(
423 project_root: &Path,
424 overlay: &HashMap<PathBuf, String>,
425) -> Result<(), ProjectPathsError> {
426 let toml_path = project_root.join("bynk.toml");
427 let Ok(content) = read_source(&toml_path, overlay) else {
428 return Ok(());
429 };
430 check_manifest_str(&content)?;
431 let doc = content
435 .parse::<toml::Table>()
436 .map_err(|_| ProjectPathsError::Malformed)?;
437 if (doc.contains_key("registries") || doc.contains_key("workspace"))
438 && let Some(workspace) = crate::workspace::find_parent_workspace(project_root)
439 {
440 return Err(if doc.contains_key("workspace") {
441 ProjectPathsError::NestedWorkspace { workspace }
442 } else {
443 ProjectPathsError::MemberRegistries { workspace }
444 });
445 }
446 Ok(())
447}
448
449pub fn check_manifest_str(content: &str) -> Result<(), ProjectPathsError> {
452 let doc = content
453 .parse::<toml::Table>()
454 .map_err(|_| ProjectPathsError::Malformed)?;
455 let mut declared: Vec<&'static str> = Vec::new();
456 for (name, value) in &doc {
457 if PLANNED_TABLES.iter().any(|(t, _)| t == name) {
460 return Err(ProjectPathsError::UnknownTable(name.clone()));
461 }
462 let known = MANIFEST_TABLES
463 .iter()
464 .map(|(t, keys)| (*t, *keys, false))
465 .chain(
466 MANIFEST_MAP_TABLES
467 .iter()
468 .map(|(t, keys)| (*t, *keys, true)),
469 )
470 .find(|(t, _, _)| t == name);
471 let Some((table, keys, is_map)) = known else {
472 return Err(if value.is_table() {
473 ProjectPathsError::UnknownTable(name.clone())
474 } else {
475 ProjectPathsError::TopLevelKey(name.clone())
476 });
477 };
478 let Some(entries) = value.as_table() else {
482 return Err(ProjectPathsError::TopLevelKey(name.clone()));
483 };
484 declared.push(table);
485 if is_map {
488 match table {
489 "dependencies" | "dev-dependencies" => {
490 crate::manifest::dependencies_from(table, entries)?;
491 }
492 _ => {
493 crate::manifest::registries_from(entries)?;
494 }
495 }
496 continue;
497 }
498 if keys.is_empty() {
499 continue;
500 }
501 for key in entries.keys() {
502 if !keys.contains(&key.as_str()) {
503 return Err(match table {
504 "paths" => ProjectPathsError::UnknownKey(key.clone()),
505 _ => ProjectPathsError::UnknownTableKey {
506 table,
507 key: key.clone(),
508 },
509 });
510 }
511 }
512 match table {
513 "workers" => drop(workers_config_from(entries)?),
514 "unstable" => drop(unstable_config_from(entries)?),
515 "test" => drop(test_targets_from(entries)?),
516 "package" => drop(crate::manifest::package_config_from(entries)?),
517 "workspace" => drop(crate::manifest::workspace_config_from(entries)?),
518 _ => {}
519 }
520 }
521 if declared.contains(&"project") && declared.contains(&"package") {
523 return Err(ProjectPathsError::ProjectAndPackage);
524 }
525 if declared.contains(&"workspace")
526 && let Some(other) = declared.iter().find(|t| !WORKSPACE_ROOT_TABLES.contains(t))
527 {
528 return Err(ProjectPathsError::WorkspaceRootTable((*other).to_string()));
529 }
530 if doc
534 .get("dev-dependencies")
535 .and_then(|v| v.as_table())
536 .is_some_and(|t| !t.is_empty())
537 {
538 return Err(ProjectPathsError::DependenciesUnresolved {
539 table: "dev-dependencies",
540 });
541 }
542 Ok(())
543}
544
545pub fn try_read_project_paths(project_root: &Path) -> Result<ProjectPaths, ProjectPathsError> {
555 let toml_path = project_root.join("bynk.toml");
556 let overlay = match fs::read_to_string(&toml_path) {
557 Ok(text) => HashMap::from([(toml_path, text)]),
558 Err(_) => HashMap::new(),
559 };
560 try_read_project_paths_with(project_root, &overlay)
561}
562
563pub fn try_read_project_paths_with(
566 project_root: &Path,
567 overlay: &HashMap<PathBuf, String>,
568) -> Result<ProjectPaths, ProjectPathsError> {
569 let toml_path = project_root.join("bynk.toml");
570 let Ok(content) = read_source(&toml_path, overlay) else {
571 return Ok(ProjectPaths::conventional(project_root));
572 };
573 let Ok(doc) = content.parse::<toml::Table>() else {
574 return Err(ProjectPathsError::Malformed);
575 };
576 let paths = doc.get("paths").and_then(|v| v.as_table());
577 if let Some(t) = paths {
578 for k in t.keys() {
579 if k != "include" && k != "exclude" {
580 return Err(ProjectPathsError::UnknownKey(k.clone()));
581 }
582 }
583 }
584 let list = |key: &str| -> Vec<PathBuf> {
585 match paths.and_then(|t| t.get(key)) {
586 Some(toml::Value::Array(items)) => items
587 .iter()
588 .filter_map(|v| v.as_str())
589 .map(PathBuf::from)
590 .collect(),
591 Some(toml::Value::String(s)) => vec![PathBuf::from(s)],
592 _ => Vec::new(),
593 }
594 };
595 let mut include = list("include");
596 let exclude = list("exclude");
597 if include.is_empty() {
598 include = ProjectPaths::conventional(project_root).include;
599 }
600 Ok(ProjectPaths { include, exclude })
601}
602
603#[derive(Debug, Clone, Default, PartialEq, Eq)]
606pub struct WorkersConfig {
607 pub compatibility_flags: Vec<String>,
613}
614
615pub fn try_read_workers_config_with(
623 project_root: &Path,
624 overlay: &HashMap<PathBuf, String>,
625) -> Result<WorkersConfig, ProjectPathsError> {
626 let toml_path = project_root.join("bynk.toml");
627 let Ok(content) = read_source(&toml_path, overlay) else {
628 return Ok(WorkersConfig::default());
629 };
630 let doc = content
631 .parse::<toml::Table>()
632 .map_err(|_| ProjectPathsError::Malformed)?;
633 match doc.get("workers").and_then(|v| v.as_table()) {
634 Some(t) => workers_config_from(t),
635 None => Ok(WorkersConfig::default()),
636 }
637}
638
639pub fn try_read_workers_config(project_root: &Path) -> Result<WorkersConfig, ProjectPathsError> {
641 let toml_path = project_root.join("bynk.toml");
642 let overlay = match fs::read_to_string(&toml_path) {
643 Ok(text) => HashMap::from([(toml_path, text)]),
644 Err(_) => HashMap::new(),
645 };
646 try_read_workers_config_with(project_root, &overlay)
647}
648
649#[derive(Debug, Clone, Default, PartialEq, Eq)]
652pub struct TestTarget {
653 pub urls: BTreeMap<String, String>,
656 pub secrets: Vec<String>,
659 pub production: bool,
661 pub vars: BTreeMap<String, String>,
664}
665
666pub fn try_read_test_targets(
670 project_root: &Path,
671) -> Result<BTreeMap<String, TestTarget>, ProjectPathsError> {
672 let Ok(content) = fs::read_to_string(project_root.join("bynk.toml")) else {
673 return Ok(BTreeMap::new());
674 };
675 let doc = content
676 .parse::<toml::Table>()
677 .map_err(|_| ProjectPathsError::Malformed)?;
678 match doc.get("test").and_then(|v| v.as_table()) {
679 Some(t) => test_targets_from(t),
680 None => Ok(BTreeMap::new()),
681 }
682}
683
684fn test_targets_from(
688 table: &toml::Table,
689) -> Result<BTreeMap<String, TestTarget>, ProjectPathsError> {
690 let mut out = BTreeMap::new();
691 let Some(targets) = table.get("targets") else {
692 return Ok(out);
693 };
694 let Some(targets) = targets.as_table() else {
695 return Err(ProjectPathsError::BadTestTarget {
696 target: "…".to_string(),
697 reason: "must be a table of named targets, like `[test.targets.preview]`".to_string(),
698 });
699 };
700 for (name, entry) in targets {
701 let bad = |reason: &str| ProjectPathsError::BadTestTarget {
702 target: name.clone(),
703 reason: reason.to_string(),
704 };
705 let Some(entry) = entry.as_table() else {
706 return Err(bad("must be a table"));
707 };
708 let strings =
709 |v: &toml::Value, what: &str| -> Result<BTreeMap<String, String>, ProjectPathsError> {
710 v.as_table()
711 .ok_or_else(|| bad(&format!("`{what}` must be a table of strings")))?
712 .iter()
713 .map(|(k, v)| {
714 v.as_str()
715 .map(|s| (k.clone(), s.to_string()))
716 .ok_or_else(|| bad(&format!("`{what}.{k}` must be a string")))
717 })
718 .collect()
719 };
720 let mut target = TestTarget::default();
721 for (key, value) in entry {
722 match key.as_str() {
723 "url" => target.urls = strings(value, "url")?,
724 "vars" => target.vars = strings(value, "vars")?,
725 "secrets" => {
726 target.secrets = value
727 .as_array()
728 .and_then(|a| a.iter().map(|v| v.as_str().map(str::to_string)).collect())
729 .ok_or_else(|| bad("`secrets` must be a list of names"))?;
730 }
731 "production" => {
732 target.production = value
733 .as_bool()
734 .ok_or_else(|| bad("`production` must be `true` or `false`"))?;
735 }
736 other => {
737 return Err(bad(&format!(
738 "has no key named `{other}` — its keys are `url`, `secrets`, `vars` and `production`"
739 )));
740 }
741 }
742 }
743 if target.urls.is_empty() {
744 return Err(bad(
745 "names no Worker URL — add `url.<worker> = \"https://…\"`",
746 ));
747 }
748 out.insert(name.clone(), target);
749 }
750 Ok(out)
751}
752
753pub fn duplicate_compatibility_flag_warning(
761 flag: &str,
762 default: bool,
763 manifest: PathBuf,
764) -> crate::AttributedError {
765 let message = if default {
766 format!(
767 "`bynk.toml`'s `[workers] compatibility_flags` lists `{flag}`, which every Bynk Worker already has"
768 )
769 } else {
770 format!("`bynk.toml`'s `[workers] compatibility_flags` lists `{flag}` more than once")
771 };
772 let mut error = bynk_syntax::CompileError::new(
773 "bynk.project.duplicate_compatibility_flag",
774 bynk_syntax::span::Span::default(),
775 message,
776 );
777 error
778 .notes
779 .push("the duplicate is dropped; remove it from `bynk.toml`".to_string());
780 crate::AttributedError::new(Some(manifest), error)
781}
782
783fn workers_config_from(table: &toml::Table) -> Result<WorkersConfig, ProjectPathsError> {
786 let not_a_list = || ProjectPathsError::NotAStringList {
787 table: "workers",
788 key: "compatibility_flags",
789 };
790 let compatibility_flags = match table.get("compatibility_flags") {
791 None => Vec::new(),
792 Some(toml::Value::Array(items)) => items
793 .iter()
794 .map(|v| v.as_str().map(str::to_owned).ok_or_else(not_a_list))
795 .collect::<Result<_, _>>()?,
796 Some(_) => return Err(not_a_list()),
797 };
798 Ok(WorkersConfig {
799 compatibility_flags,
800 })
801}
802
803#[derive(Debug, Clone, Default, PartialEq, Eq)]
806pub struct UnstableConfig {
807 pub allow: Vec<String>,
812}
813
814pub fn try_read_unstable_config_with(
819 project_root: &Path,
820 overlay: &HashMap<PathBuf, String>,
821) -> Result<UnstableConfig, ProjectPathsError> {
822 let toml_path = project_root.join("bynk.toml");
823 let Ok(content) = read_source(&toml_path, overlay) else {
824 return Ok(UnstableConfig::default());
825 };
826 let doc = content
827 .parse::<toml::Table>()
828 .map_err(|_| ProjectPathsError::Malformed)?;
829 match doc.get("unstable").and_then(|v| v.as_table()) {
830 Some(t) => unstable_config_from(t),
831 None => Ok(UnstableConfig::default()),
832 }
833}
834
835pub fn try_read_unstable_config(project_root: &Path) -> Result<UnstableConfig, ProjectPathsError> {
837 let toml_path = project_root.join("bynk.toml");
838 let overlay = match fs::read_to_string(&toml_path) {
839 Ok(text) => HashMap::from([(toml_path, text)]),
840 Err(_) => HashMap::new(),
841 };
842 try_read_unstable_config_with(project_root, &overlay)
843}
844
845pub fn read_unstable_config(
851 project_root: &Path,
852 overlay: &HashMap<PathBuf, String>,
853) -> UnstableConfig {
854 let manifest = project_root.join("bynk.toml");
855 let read = if overlay.contains_key(&manifest) {
856 try_read_unstable_config_with(project_root, overlay)
857 } else {
858 try_read_unstable_config(project_root)
859 };
860 read.unwrap_or_default()
861}
862
863fn unstable_config_from(table: &toml::Table) -> Result<UnstableConfig, ProjectPathsError> {
866 let not_a_list = || ProjectPathsError::NotAStringList {
867 table: "unstable",
868 key: "allow",
869 };
870 let allow = match table.get("allow") {
871 None => Vec::new(),
872 Some(toml::Value::Array(items)) => items
873 .iter()
874 .map(|v| v.as_str().map(str::to_owned).ok_or_else(not_a_list))
875 .collect::<Result<_, _>>()?,
876 Some(_) => return Err(not_a_list()),
877 };
878 Ok(UnstableConfig { allow })
879}
880
881pub fn commons_dir_for(name: &str) -> PathBuf {
887 let mut p = PathBuf::new();
888 let mut parts = name.split('.');
889 let first = parts.next().unwrap_or_default();
890 match crate::packages::PackageId::decode(first) {
891 Some(id) => p.push(id.out_dir()),
892 None => p.push(first),
893 }
894 for part in parts {
895 p.push(part);
896 }
897 p
898}
899
900pub fn ts_output_path(source: &Path) -> PathBuf {
901 let mut out = source.to_path_buf();
902 out.set_extension("ts");
903 out
904}
905
906pub fn worker_dir_name(context: &str) -> String {
909 context.replace('.', "-")
910}
911
912pub fn worker_handlers_source_path(context: &str) -> PathBuf {
916 PathBuf::from(format!(
917 "workers/{}/handlers.bynk",
918 worker_dir_name(context)
919 ))
920}
921
922pub fn worker_handlers_output_path(context: &str) -> PathBuf {
924 PathBuf::from(format!("workers/{}/handlers.ts", worker_dir_name(context)))
925}
926
927pub fn worker_file_source_path(context: &str, source: &Path) -> PathBuf {
939 let stem = source.file_stem().unwrap_or_default().to_string_lossy();
940 PathBuf::from(format!(
941 "workers/{}/handlers/{stem}.bynk",
942 worker_dir_name(context)
943 ))
944}
945
946fn stem_parts(rel_path: &Path) -> Vec<String> {
949 rel_path
950 .with_extension("")
951 .components()
952 .filter_map(|c| match c {
953 Component::Normal(s) => Some(s.to_string_lossy().to_string()),
954 _ => None,
955 })
956 .collect()
957}
958
959pub fn unit_path_matches(rel_path: &Path, qualified_name: &str) -> bool {
968 let dir = commons_dir_for(qualified_name);
971 let owned: Vec<String> = dir
972 .components()
973 .map(|c| c.as_os_str().to_string_lossy().into_owned())
974 .collect();
975 let name_parts: Vec<&str> = owned.iter().map(String::as_str).collect();
976 let stem_parts = stem_parts(rel_path);
977 let single_file_match = stem_parts.len() == name_parts.len()
978 && stem_parts
979 .iter()
980 .zip(name_parts.iter())
981 .all(|(a, b)| a == b);
982 single_file_match || is_multi_file_parts(&stem_parts, &name_parts)
983}
984
985fn is_multi_file_parts(stem_parts: &[String], name_parts: &[&str]) -> bool {
988 if stem_parts.is_empty() {
989 return false;
990 }
991 let parent_parts = &stem_parts[..stem_parts.len() - 1];
992 parent_parts.len() == name_parts.len()
993 && parent_parts
994 .iter()
995 .zip(name_parts.iter())
996 .all(|(a, b)| a == b)
997}
998
999pub fn is_multi_file_layout(rel_path: &Path, qualified_name: &str) -> bool {
1009 let name_parts: Vec<&str> = qualified_name.split('.').collect();
1010 is_multi_file_parts(&stem_parts(rel_path), &name_parts)
1011}
1012
1013pub fn renamed_unit_name(old_rel: &Path, old_name: &str, new_rel: &Path) -> Option<String> {
1028 let name_parts: Vec<&str> = old_name.split('.').collect();
1029 let old_stem = stem_parts(old_rel);
1030 let new_stem = stem_parts(new_rel);
1031
1032 let suffix_matches = |haystack: &[String]| {
1033 haystack.len() >= name_parts.len() && {
1034 let prefix_len = haystack.len() - name_parts.len();
1035 haystack[prefix_len..]
1036 .iter()
1037 .zip(name_parts.iter())
1038 .all(|(a, b)| a == b)
1039 }
1040 };
1041
1042 if suffix_matches(&old_stem) {
1043 let prefix_len = old_stem.len() - name_parts.len();
1044 return (new_stem.len() >= prefix_len).then(|| new_stem[prefix_len..].join("."));
1045 }
1046 if !old_stem.is_empty() {
1047 let old_parent = &old_stem[..old_stem.len() - 1];
1048 if suffix_matches(old_parent) {
1049 let prefix_len = old_parent.len() - name_parts.len();
1050 if new_stem.is_empty() {
1051 return None;
1052 }
1053 let new_parent = &new_stem[..new_stem.len() - 1];
1054 return (new_parent.len() >= prefix_len).then(|| new_parent[prefix_len..].join("."));
1055 }
1056 }
1057 None
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063 use std::path::{Path, PathBuf};
1064
1065 #[test]
1067 fn is_unpinned_range_true_for_wildcards_and_digitless() {
1068 assert!(is_unpinned_range(""));
1069 assert!(is_unpinned_range("*"));
1070 assert!(is_unpinned_range("x"));
1071 assert!(is_unpinned_range("X"));
1072 assert!(is_unpinned_range("latest"));
1073 assert!(is_unpinned_range("LATEST"));
1074 assert!(is_unpinned_range(" * ")); assert!(is_unpinned_range("workspace:*")); assert!(is_unpinned_range("beta"));
1077 }
1078
1079 #[test]
1080 fn is_unpinned_range_false_when_a_digit_is_present() {
1081 assert!(!is_unpinned_range("1.0.0"));
1082 assert!(!is_unpinned_range("^1.2"));
1083 assert!(!is_unpinned_range("~0.1"));
1084 assert!(!is_unpinned_range(">=2"));
1085 assert!(!is_unpinned_range("18"));
1086 }
1087
1088 #[test]
1090 fn read_project_paths_with_honours_a_virtual_bynk_toml() {
1091 let root = PathBuf::from("/nonexistent-bynk-test-root-57");
1092 let mut overlay = HashMap::new();
1093 overlay.insert(
1094 root.join("bynk.toml"),
1095 "[paths]\ninclude = [\"app\"]\nexclude = [\"vendor\"]\n".to_string(),
1096 );
1097 let paths = read_project_paths_with(&root, &overlay);
1098 assert_eq!(paths.include, vec![PathBuf::from("app")]);
1099 assert_eq!(paths.exclude, vec![PathBuf::from("vendor")]);
1100 }
1101
1102 #[test]
1103 fn read_project_paths_with_falls_back_to_conventional_with_no_overlay_entry() {
1104 let root = PathBuf::from("/nonexistent-bynk-test-root-57-empty");
1107 let paths = read_project_paths_with(&root, &HashMap::new());
1108 let conventional = ProjectPaths::conventional(&root);
1109 assert_eq!(paths.include, conventional.include);
1110 assert_eq!(paths.exclude, conventional.exclude);
1111 }
1112
1113 #[test]
1116 fn read_project_paths_with_honours_three_or_more_include_entries() {
1117 let root = PathBuf::from("/nonexistent-bynk-test-root-1113-many-includes");
1118 let mut overlay = HashMap::new();
1119 overlay.insert(
1120 root.join("bynk.toml"),
1121 "[paths]\ninclude = [\"src\", \"tests\", \"examples\"]\n".to_string(),
1122 );
1123 let paths = try_read_project_paths_with(&root, &overlay).expect("must parse");
1124 assert_eq!(
1125 paths.include,
1126 vec![
1127 PathBuf::from("src"),
1128 PathBuf::from("tests"),
1129 PathBuf::from("examples"),
1130 ]
1131 );
1132 }
1133
1134 #[test]
1136 fn render_package_json_declares_es_modules() {
1137 let bare = render_package_json(&std::collections::BTreeMap::new());
1139 assert_eq!(bare, "{\n \"type\": \"module\"\n}\n");
1140 let mut deps = std::collections::BTreeMap::new();
1141 deps.insert("jose".to_string(), "^5.0.0".to_string());
1142 let with_deps = render_package_json(&deps);
1143 assert_eq!(
1144 with_deps,
1145 "{\n \"type\": \"module\",\n \"dependencies\": {\n \"jose\": \"^5.0.0\"\n }\n}\n"
1146 );
1147 }
1148
1149 #[test]
1150 fn render_package_json_renders_sorted_dependencies() {
1151 let mut deps = std::collections::BTreeMap::new();
1152 deps.insert("zod".to_string(), "^3.22.4".to_string());
1153 deps.insert("hono".to_string(), "^4.0.0".to_string());
1154 let out = render_package_json(&deps);
1155 assert!(
1157 out.find("\"hono\"").unwrap() < out.find("\"zod\"").unwrap(),
1158 "dependencies render in sorted order:\n{out}"
1159 );
1160 assert!(out.contains("\"hono\": \"^4.0.0\""), "{out}");
1161 }
1162
1163 #[test]
1169 fn render_package_json_escapes_the_control_range() {
1170 let mut deps = std::collections::BTreeMap::new();
1171 deps.insert("pkg\nname".to_string(), "^1.0\u{1}0".to_string());
1172 let out = render_package_json(&deps);
1173 assert!(out.contains("\"pkg\\nname\""), "{out}");
1174 assert!(out.contains("\"^1.0\\u00010\""), "{out}");
1175 assert!(
1178 !out.lines().any(|l| l.chars().any(|c| (c as u32) < 0x20)),
1179 "a raw control character reached the output:\n{out:?}"
1180 );
1181 }
1182
1183 #[test]
1184 fn render_package_json_escapes_structural_characters() {
1185 let mut deps = std::collections::BTreeMap::new();
1186 deps.insert("a\"b".to_string(), "c\\d".to_string());
1187 let out = render_package_json(&deps);
1188 assert!(out.contains(r#""a\"b": "c\\d""#), "{out}");
1189 }
1190
1191 #[test]
1193 fn normalize_rel_resolves_dot_and_parent() {
1194 assert_eq!(
1195 normalize_rel(Path::new("./tokens.binding.ts")),
1196 PathBuf::from("tokens.binding.ts")
1197 );
1198 assert_eq!(normalize_rel(Path::new("a/./b")), PathBuf::from("a/b"));
1199 assert_eq!(normalize_rel(Path::new("a/../b")), PathBuf::from("b"));
1200 assert_eq!(normalize_rel(Path::new("a/b/../../c")), PathBuf::from("c"));
1201 assert_eq!(normalize_rel(Path::new("a/b")), PathBuf::from("a/b"));
1202 }
1203
1204 #[test]
1205 fn normalize_rel_drops_root_and_pops_through_empty() {
1206 assert_eq!(normalize_rel(Path::new("/a/b")), PathBuf::from("a/b"));
1208 assert_eq!(normalize_rel(Path::new("../a")), PathBuf::from("a"));
1210 }
1211
1212 #[test]
1214 fn commons_dir_for_splits_dotted_name_into_dirs() {
1215 assert_eq!(commons_dir_for("a.b.c"), PathBuf::from("a/b/c"));
1216 assert_eq!(commons_dir_for("foo"), PathBuf::from("foo"));
1217 }
1218
1219 #[test]
1220 fn ts_output_path_sets_ts_extension() {
1221 assert_eq!(
1222 ts_output_path(Path::new("foo.bynk")),
1223 PathBuf::from("foo.ts")
1224 );
1225 assert_eq!(
1226 ts_output_path(Path::new("a/b.bynk")),
1227 PathBuf::from("a/b.ts")
1228 );
1229 assert_eq!(ts_output_path(Path::new("foo")), PathBuf::from("foo.ts"));
1230 }
1231
1232 #[test]
1234 fn worker_paths_dasherise_and_root_under_workers() {
1235 assert_eq!(worker_dir_name("commerce.payment"), "commerce-payment");
1236 assert_eq!(worker_dir_name("plain"), "plain");
1237 assert_eq!(
1238 worker_handlers_source_path("commerce.payment"),
1239 PathBuf::from("workers/commerce-payment/handlers.bynk")
1240 );
1241 assert_eq!(
1242 worker_handlers_output_path("commerce.payment"),
1243 PathBuf::from("workers/commerce-payment/handlers.ts")
1244 );
1245 }
1246
1247 #[test]
1248 fn a_split_context_file_has_its_own_module_under_handlers() {
1249 assert_eq!(
1250 worker_file_source_path("shop.orders", Path::new("shop/orders/place.bynk")),
1251 PathBuf::from("workers/shop-orders/handlers/place.bynk")
1252 );
1253 }
1254
1255 #[test]
1257 fn unit_path_matches_single_file_layout() {
1258 assert!(unit_path_matches(Path::new("a/b/c.bynk"), "a.b.c"));
1259 assert!(unit_path_matches(Path::new("foo.bynk"), "foo"));
1260 }
1261
1262 #[test]
1263 fn unit_path_matches_multi_file_layout() {
1264 assert!(unit_path_matches(Path::new("a/b/c/handlers.bynk"), "a.b.c"));
1266 assert!(unit_path_matches(Path::new("a/b/c/anything.bynk"), "a.b.c"));
1267 }
1268
1269 #[test]
1270 fn unit_path_matches_rejects_misalignment() {
1271 assert!(!unit_path_matches(Path::new("a/b.bynk"), "a.b.c"));
1272 assert!(!unit_path_matches(Path::new("x/y/z.bynk"), "a.b.c"));
1273 }
1274
1275 #[test]
1277 fn is_multi_file_layout_true_only_for_directory_layout() {
1278 assert!(is_multi_file_layout(Path::new("thing/a.bynk"), "thing"));
1280 assert!(is_multi_file_layout(Path::new("thing/b.bynk"), "thing"));
1281 assert!(is_multi_file_layout(Path::new("a/b/one.bynk"), "a.b"));
1283 }
1284
1285 #[test]
1286 fn is_multi_file_layout_false_for_single_file_and_misalignment() {
1287 assert!(!is_multi_file_layout(Path::new("thing.bynk"), "thing"));
1289 assert!(!is_multi_file_layout(Path::new("a/b.bynk"), "a.b"));
1291 assert!(!is_multi_file_layout(Path::new("other/a.bynk"), "thing"));
1293 }
1294
1295 #[test]
1297 fn renamed_unit_name_single_file() {
1298 assert_eq!(
1299 renamed_unit_name(
1300 Path::new("a/b/c.bynk"),
1301 "a.b.c",
1302 Path::new("a/b/renamed.bynk")
1303 ),
1304 Some("a.b.renamed".to_string())
1305 );
1306 assert_eq!(
1307 renamed_unit_name(Path::new("foo.bynk"), "foo", Path::new("bar.bynk")),
1308 Some("bar".to_string())
1309 );
1310 }
1311
1312 #[test]
1313 fn renamed_unit_name_multi_file_member_rename_is_a_no_op() {
1314 assert_eq!(
1318 renamed_unit_name(
1319 Path::new("a/b/c/old.bynk"),
1320 "a.b.c",
1321 Path::new("a/b/c/new.bynk")
1322 ),
1323 Some("a.b.c".to_string())
1324 );
1325 }
1326
1327 #[test]
1328 fn renamed_unit_name_multi_file_directory_move() {
1329 assert_eq!(
1330 renamed_unit_name(
1331 Path::new("a/b/c/handlers.bynk"),
1332 "a.b.c",
1333 Path::new("a/b/renamed/handlers.bynk")
1334 ),
1335 Some("a.b.renamed".to_string())
1336 );
1337 }
1338
1339 #[test]
1340 fn renamed_unit_name_none_on_preexisting_misalignment() {
1341 assert_eq!(
1342 renamed_unit_name(Path::new("x/y/z.bynk"), "a.b.c", Path::new("x/y/w.bynk")),
1343 None
1344 );
1345 }
1346
1347 #[test]
1348 fn renamed_unit_name_tolerates_a_shared_include_root_prefix() {
1349 assert_eq!(
1355 renamed_unit_name(
1356 Path::new("src/billing/charge.bynk"),
1357 "billing.charge",
1358 Path::new("src/billing/pay.bynk")
1359 ),
1360 Some("billing.pay".to_string())
1361 );
1362 assert_eq!(
1364 renamed_unit_name(
1365 Path::new("src/a/b/c/handlers.bynk"),
1366 "a.b.c",
1367 Path::new("src/a/b/renamed/handlers.bynk")
1368 ),
1369 Some("a.b.renamed".to_string())
1370 );
1371 }
1372}
1373
1374#[cfg(test)]
1375mod manifest_tests {
1376 use super::*;
1377
1378 fn err(manifest: &str) -> String {
1379 check_manifest_str(manifest)
1380 .expect_err("the manifest is refused")
1381 .to_string()
1382 }
1383
1384 #[test]
1386 fn the_documented_tables_are_accepted() {
1387 check_manifest_str(
1388 "[project]\nname = \"p\"\nversion = \"0.1.0\"\n\n[paths]\ninclude = [\"src\"]\nexclude = []\n\n\
1389 [fmt]\nindent = \"tab\"\nmax_line_width = 100\n\n[lsp]\ndiagnostics_mode = \"live\"\n\
1390 diagnostics_debounce_ms = 300\n\n[workers]\ncompatibility_flags = [\"nodejs_compat\"]\n",
1391 )
1392 .expect("accepted");
1393 check_manifest_str("").expect("an empty manifest is accepted");
1394 }
1395
1396 #[test]
1397 fn a_planned_table_names_its_issue() {
1398 assert!(err("[deploy]\ngroups = []\n").contains("not yet supported (#551)"));
1399 }
1400
1401 #[test]
1402 fn a_typod_table_suggests_the_nearest() {
1403 assert!(err("[pahts]\n").contains("did you mean `[paths]`?"));
1404 assert!(err("[fnt]\n").contains("did you mean `[fmt]`?"));
1405 assert!(err("[zzzzzz]\n").contains("the tables are"));
1406 }
1407
1408 #[test]
1409 fn an_unknown_key_in_project_or_lsp_suggests_the_nearest() {
1410 assert!(err("[project]\nnmae = \"p\"\n").contains("did you mean `name`?"));
1411 assert!(
1412 err("[lsp]\ndiagnostics_mod = \"live\"\n").contains("did you mean `diagnostics_mode`?")
1413 );
1414 assert!(err("[project]\nlicense = \"MIT\"\n").contains("its keys are `name`, `version`"));
1415 }
1416
1417 #[test]
1418 fn a_paths_key_keeps_its_own_error() {
1419 assert!(matches!(
1420 check_manifest_str("[paths]\nout = \"out\"\n"),
1421 Err(ProjectPathsError::UnknownKey(k)) if k == "out"
1422 ));
1423 }
1424
1425 #[test]
1426 fn a_top_level_key_points_at_its_table() {
1427 assert!(err("name = \"p\"\n").contains("did you mean it under `[project]`?"));
1428 }
1429
1430 #[test]
1433 fn a_table_name_with_a_plain_value_must_be_a_table() {
1434 assert!(err("paths = \"src\"\n").contains("must be a table — write it as `[paths]`"));
1435 assert!(err("lsp = 3\n").contains("`[lsp]`"));
1436 assert!(err("fmt = true\n").contains("`[fmt]`"));
1437 check_manifest_str("paths = { include = [\"src\"] }\n").expect("accepted");
1439 }
1440
1441 #[test]
1443 fn a_planned_table_in_any_shape_names_its_issue() {
1444 assert!(err("[[deploy]]\nname = \"acme\"\n").contains("(#551)"));
1445 assert!(err("deploy = [\"acme\"]\n").contains("(#551)"));
1446 }
1447
1448 #[test]
1450 fn a_map_table_in_another_shape_must_be_a_table() {
1451 assert_eq!(
1452 err("[[dependencies]]\nname = \"acme\"\n"),
1453 "`dependencies` in `bynk.toml` must be a table — write it as `[dependencies]`"
1454 );
1455 assert_eq!(
1456 err("registries = [\"acme\"]\n"),
1457 "`registries` in `bynk.toml` must be a table — write it as `[registries]`"
1458 );
1459 assert_eq!(
1460 err("[dependencis]\n"),
1461 "`bynk.toml` has no table named `[dependencis]` — did you mean `[dependencies]`?"
1462 );
1463 }
1464
1465 #[test]
1467 fn fmt_keys_are_left_to_the_formatter() {
1468 check_manifest_str("[fmt]\nanything = 1\n").expect("not this check's concern");
1469 }
1470
1471 #[test]
1474 fn workers_compatibility_flags_must_be_a_list_of_strings() {
1475 let msg = "`[workers]` `compatibility_flags` must be a list of strings";
1476 assert!(err("[workers]\ncompatibility_flags = \"nodejs_compat\"\n").contains(msg));
1477 assert!(err("[workers]\ncompatibility_flags = [\"a\", 1]\n").contains(msg));
1478 assert!(err("[workers]\ncompatibility_flags = [[\"a\"]]\n").contains(msg));
1479 assert!(
1480 err("[workers]\ncompatibility_flag = []\n")
1481 .contains("did you mean `compatibility_flags`?")
1482 );
1483 check_manifest_str("[workers]\ncompatibility_flags = [\"no_such_flag_yet\"]\n")
1484 .expect("flag names are not validated");
1485 check_manifest_str("[workers]\n").expect("an empty `[workers]` is accepted");
1486 }
1487
1488 #[test]
1491 fn unstable_allow_must_be_a_list_of_strings() {
1492 let msg = "`[unstable]` `allow` must be a list of strings";
1493 assert!(err("[unstable]\nallow = \"bynk.cloudflare.D1\"\n").contains(msg));
1494 assert!(err("[unstable]\nallow = [\"a\", 1]\n").contains(msg));
1495 assert!(err("[unstable]\nallows = []\n").contains("did you mean `allow`?"));
1496 check_manifest_str("[unstable]\nallow = [\"bynk.anything\"]\n")
1497 .expect("item paths are the checker's to validate");
1498 check_manifest_str("[unstable]\n").expect("an empty `[unstable]` is accepted");
1499 }
1500
1501 #[test]
1502 fn unstable_config_reads_the_entries_in_order() {
1503 let root = PathBuf::from("/nonexistent-bynk-test-root-1963");
1504 let read = |manifest: &str| {
1505 let overlay = HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1506 try_read_unstable_config_with(&root, &overlay)
1507 };
1508 assert_eq!(
1509 read("[unstable]\nallow = [\"b.x\", \"a.y\"]\n")
1510 .unwrap()
1511 .allow,
1512 vec!["b.x", "a.y"]
1513 );
1514 assert_eq!(
1515 read("[project]\nname = \"p\"\n").unwrap(),
1516 UnstableConfig::default()
1517 );
1518 assert!(matches!(
1519 read("[unstable]\nallow = 3\n"),
1520 Err(ProjectPathsError::NotAStringList {
1521 table: "unstable",
1522 key: "allow"
1523 })
1524 ));
1525 }
1526
1527 #[test]
1528 fn workers_config_reads_the_flags_in_order() {
1529 let root = PathBuf::from("/nonexistent-bynk-test-root-1890");
1530 let read = |manifest: &str| {
1531 let overlay = HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1532 try_read_workers_config_with(&root, &overlay)
1533 };
1534 assert_eq!(
1535 read("[workers]\ncompatibility_flags = [\"b\", \"a\", \"b\"]\n")
1536 .unwrap()
1537 .compatibility_flags,
1538 vec!["b", "a", "b"],
1539 "order and duplicates are kept; deduplication is the emitter's"
1540 );
1541 assert_eq!(
1542 read("[project]\nname = \"p\"\n").unwrap(),
1543 WorkersConfig::default()
1544 );
1545 assert_eq!(
1546 try_read_workers_config_with(&root, &HashMap::new()).unwrap(),
1547 WorkersConfig::default(),
1548 "no manifest at all"
1549 );
1550 assert!(matches!(
1551 read("[workers]\ncompatibility_flags = [true]\n"),
1552 Err(ProjectPathsError::NotAStringList { .. })
1553 ));
1554 assert!(matches!(
1555 read("[workers\n"),
1556 Err(ProjectPathsError::Malformed)
1557 ));
1558 }
1559
1560 #[test]
1561 fn a_malformed_manifest_is_malformed() {
1562 assert!(matches!(
1563 check_manifest_str("[project\n"),
1564 Err(ProjectPathsError::Malformed)
1565 ));
1566 }
1567
1568 #[test]
1573 fn the_readers_accept_the_shapes_the_manifest_schema_offers() {
1574 let root = PathBuf::from("/nonexistent-bynk-test-root-2096");
1575 let overlay =
1576 |manifest: &str| HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1577
1578 for manifest in [
1580 "[paths]\ninclude = \"src\"\n",
1581 "[paths]\ninclude = [\"src\"]\n",
1582 ] {
1583 check_manifest_str(manifest).expect("the manifest check accepts it");
1584 let paths = try_read_project_paths_with(&root, &overlay(manifest))
1585 .unwrap_or_else(|e| panic!("{manifest:?}: {e}"));
1586 assert_eq!(paths.include, vec![PathBuf::from("src")], "{manifest:?}");
1587 }
1588
1589 let manifest = "[workers]\ncompatibility_flags = [\"x\"]\n";
1591 check_manifest_str(manifest).expect("the manifest check accepts it");
1592 assert_eq!(
1593 try_read_workers_config_with(&root, &overlay(manifest))
1594 .unwrap()
1595 .compatibility_flags,
1596 vec!["x"]
1597 );
1598 let manifest = "[unstable]\nallow = [\"x\"]\n";
1599 check_manifest_str(manifest).expect("the manifest check accepts it");
1600 assert_eq!(
1601 try_read_unstable_config_with(&root, &overlay(manifest))
1602 .unwrap()
1603 .allow,
1604 vec!["x"]
1605 );
1606
1607 let manifest = "[workers]\ncompatibility_flags = \"x\"\n";
1610 assert!(matches!(
1611 try_read_workers_config_with(&root, &overlay(manifest)),
1612 Err(ProjectPathsError::NotAStringList { .. })
1613 ));
1614 assert!(check_manifest_str(manifest).is_err());
1615 let manifest = "[unstable]\nallow = \"x\"\n";
1616 assert!(matches!(
1617 try_read_unstable_config_with(&root, &overlay(manifest)),
1618 Err(ProjectPathsError::NotAStringList { .. })
1619 ));
1620 assert!(check_manifest_str(manifest).is_err());
1621
1622 let manifest = "[test.targets.preview]\nurl.shop-orders = \"https://x\"\nsecrets = [\"S\"]\nvars = { P = \"fake\" }\nproduction = false\n";
1624 check_manifest_str(manifest).expect("a well-formed target is accepted");
1625 let targets = test_targets_from(
1626 manifest.parse::<toml::Table>().unwrap()["test"]
1627 .as_table()
1628 .unwrap(),
1629 )
1630 .unwrap();
1631 let preview = &targets["preview"];
1632 assert_eq!(preview.urls["shop-orders"], "https://x");
1633 assert_eq!(preview.secrets, ["S"]);
1634 assert_eq!(preview.vars["P"], "fake");
1635 assert!(!preview.production);
1636 for (manifest, needle) in [
1637 ("[test.targets.p]\nsecrets = []\n", "names no Worker URL"),
1638 (
1639 "[test.targets.p]\nurl.a = \"x\"\nsecret = \"S\"\n",
1640 "no key named `secret`",
1641 ),
1642 (
1643 "[test.targets.p]\nurl.a = \"x\"\nproduction = \"yes\"\n",
1644 "`production` must be",
1645 ),
1646 ("[test.targets.p]\nurl.a = 1\n", "`url.a` must be a string"),
1647 ("[test]\ntarget = 1\n", "no key named `target`"),
1648 ] {
1649 let err = check_manifest_str(manifest)
1650 .expect_err(manifest)
1651 .to_string();
1652 assert!(err.contains(needle), "{manifest:?}: {err}");
1653 }
1654 }
1655}