webui_discovery/plugin/mod.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Discovery contracts and the default filename-based implementation.
5
6use crate::catalog::PackageCatalog;
7use crate::npm::PackageContext;
8use crate::{DiscoveredComponent, PreparedPackage};
9use anyhow::{bail, Result};
10use std::path::Path;
11
12mod fast;
13pub use fast::FastDiscoveryPlugin;
14
15/// Maps a resolved local or npm package layout to WebUI component registrations.
16///
17/// Package resolution supplies parsed metadata through [`PackageContext::manifest`]
18/// and includes `package.json` in cache invalidation for every plugin.
19pub trait DiscoveryPlugin {
20 /// Stable cache namespace for this discovery layout.
21 fn cache_namespace(&self) -> &'static str;
22
23 /// Discover components below a local source root.
24 ///
25 /// # Errors
26 ///
27 /// Returns an error when a claimed component source cannot be read.
28 fn discover_local(&self, root: &Path) -> Result<Vec<DiscoveredComponent>>;
29
30 /// Whether a package declares components using this discovery layout.
31 ///
32 /// Scope searches skip packages returning `false`, but propagate failures
33 /// from packages that declare components. Explicit package requests still
34 /// report invalid or missing component inputs. Custom plugins default to
35 /// accepting every package.
36 ///
37 /// # Errors
38 ///
39 /// Returns an error when component-source presence cannot be determined.
40 fn supports_package(&self, _package: PackageContext<'_>) -> Result<bool> {
41 Ok(true)
42 }
43
44 /// Resolve a package's component inventory and source choices once.
45 ///
46 /// File-backed plans are loaded by the shared cache layer without repeating
47 /// discovery. Return [`PreparedPackage::Uncached`] for computed components
48 /// whose input observations are not represented by a file-backed plan.
49 ///
50 /// # Errors
51 ///
52 /// Returns an error when required package metadata or component sources are
53 /// missing or invalid.
54 fn prepare_package(&self, package: PackageContext<'_>) -> Result<PreparedPackage>;
55}
56
57/// Default discovery using component filenames and matching sibling files.
58#[derive(Debug, Default, Clone, Copy)]
59pub struct WebUIDiscoveryPlugin;
60
61impl WebUIDiscoveryPlugin {
62 /// Create default filename-based discovery.
63 #[must_use]
64 pub const fn new() -> Self {
65 Self
66 }
67}
68
69impl DiscoveryPlugin for WebUIDiscoveryPlugin {
70 fn cache_namespace(&self) -> &'static str {
71 "webui-prepared-v1"
72 }
73
74 fn discover_local(&self, root: &Path) -> Result<Vec<DiscoveredComponent>> {
75 crate::catalog::discover(&root.to_string_lossy(), root)
76 }
77
78 fn supports_package(&self, package: PackageContext<'_>) -> Result<bool> {
79 PackageCatalog::new(package)?.has_templates(|_| true)
80 }
81
82 fn prepare_package(&self, package: PackageContext<'_>) -> Result<PreparedPackage> {
83 let catalog = PackageCatalog::new(package)?;
84 let components = catalog.prepare(|_| true)?;
85 if components.is_empty() && !catalog.is_disabled() {
86 bail!(
87 "No component templates in {}. Add <component-name>.html files under \
88 components/ or declare webui.components in package.json; \
89 the filename is the custom element name.",
90 package.root.display()
91 );
92 }
93 Ok(PreparedPackage::Files(components))
94 }
95}