Skip to content

Commit 7a6662d

Browse files
feat: add --components flag for external component discovery (#74)
1 parent 34806fe commit 7a6662d

20 files changed

Lines changed: 1920 additions & 257 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DESIGN.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,64 @@ pub fn get(&self, name: &str) -> Option<&Component>
386386
- Directory scanning with file matching
387387
- Cache optimization for repeated lookups
388388

389+
### External Component Discovery (webui-discovery)
390+
391+
The `webui-discovery` crate discovers components from external sources. It has **no dependency on `webui-parser`**it returns plain data structs that callers register into their component registry. This makes it reusable by CLI, FFI, and other host integrations.
392+
393+
#### Source Classification
394+
```rust
395+
enum ComponentSource {
396+
/// npm package: starts with `@` (scoped) or is a bare identifier
397+
NpmPackage(String),
398+
/// Local filesystem path: starts with `.`, `/`, `\`, or drive letter
399+
Path(PathBuf),
400+
}
401+
```
402+
403+
#### Public API
404+
```rust
405+
/// Discover components from a single source.
406+
pub fn discover_source(source: &str, search_dir: &Path) -> Result<DiscoveryResult>
407+
408+
/// Collect resolved local paths for file watching.
409+
pub fn collect_watch_paths(sources: &[String], search_dir: &Path) -> Vec<PathBuf>
410+
411+
/// A discovered component (tag name, HTML template, optional CSS).
412+
pub struct DiscoveredComponent {
413+
pub tag_name: String,
414+
pub html_content: String,
415+
pub css_content: Option<String>,
416+
pub source: String,
417+
}
418+
```
419+
420+
#### npm Package Resolution
421+
1. Walk up from the search directory to find `node_modules/` (Node.js-style resolution)
422+
2. For scoped packages (`@scope`), enumerate all sub-directories
423+
3. For each package, read `package.json`:
424+
- `exports["./template-webui.html"]` → template HTML path
425+
- `exports["./styles.css"]` → styles CSS path (optional)
426+
- `customElements` → path to Custom Elements Manifest
427+
4. Parse the Custom Elements Manifest for `modules[].declarations[].tagName`
428+
5. Return `DiscoveredComponent` structs (callers handle registration)
429+
430+
Conditional exports are resolved with deterministic priority: `default``import``require`.
431+
432+
#### Security
433+
- **Path traversal**: Export paths are validated — absolute paths and `..` components are rejected
434+
- **Symlink escape**: Resolved package paths must remain within `node_modules/` after `fs::canonicalize()`
435+
- **File size limits**: Manifests and templates are capped at 10 MB to prevent denial-of-service
436+
437+
#### Discovery Cache
438+
- Location: `~/.webui/cache/components/`
439+
- Cache key: hash of source identifier + resolved path
440+
- Invalidation: hash of `package.json` content (re-discover on change)
441+
- Atomic writes: temp file + rename to prevent corruption from concurrent builds
442+
- Corrupt cache files are silently ignored (graceful fallback)
443+
444+
#### Local Path Resolution
445+
Local paths perform a recursive WalkDir scan for HTML files with hyphenated names, pairing matching CSS files — the same convention used by the parser's `ComponentRegistry`.
446+
389447
### HTML Parser
390448
```rust
391449
pub struct HtmlParser {
@@ -574,6 +632,7 @@ pub enum ParserError {
574632
webui/
575633
├── crates/
576634
│ ├── webui-cli/ # CLI build tool (binary: "webui")
635+
│ ├── webui-discovery/ # External component discovery (npm, paths)
577636
│ ├── webui-expressions/ # Expression evaluation engine
578637
│ ├── webui-ffi/ # C-compatible FFI bindings
579638
│ ├── webui-handler/ # Protocol handler implementation

crates/webui-cli/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ path = "src/main.rs"
1515
webui-parser = { path = "../webui-parser" }
1616
webui-protocol = { path = "../webui-protocol" }
1717
webui-handler = { path = "../webui-handler" }
18+
webui-discovery = { path = "../webui-discovery" }
1819
clap = { workspace = true }
1920
anyhow = { workspace = true }
2021
console = { workspace = true }
22+
serde = { workspace = true }
2123
serde_json = { workspace = true }
2224
actix-web = { workspace = true }
2325
expand-tilde = { workspace = true }
2426
mime_guess = { workspace = true }
27+
walkdir = { workspace = true }
2528

2629
[dev-dependencies]
2730
tempfile = { workspace = true }

0 commit comments

Comments
 (0)