Skip to main content

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}