1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! rspress documentation framework plugin.
//!
//! rspress (Rsbuild / Rspack ecosystem) exposes its theme layer through the
//! `@theme` build-time virtual module, mirroring Docusaurus. Importing
//! `@theme` or a `@theme/<component>` subpath from docs/source resolves at
//! build time and is not an npm package, so the plugin contributes the prefix
//! to suppress false `unlisted-dependency` / `unresolved-import` findings.
use super::Plugin;
const ENABLERS: &[&str] = &["rspress", "@rspress/"];
/// Virtual module prefixes provided by rspress at build time.
/// `@theme/` exposes the active theme's components; `@theme-original/` is the
/// swizzle escape hatch for extending the default theme (same convention as
/// Docusaurus). The trailing slash also covers the bare `@theme` import via the
/// exact-bare branch of `matches_virtual_prefix`.
const VIRTUAL_MODULE_PREFIXES: &[&str] = &["@theme/", "@theme-original/"];
define_plugin! {
struct RspressPlugin => "rspress",
enablers: ENABLERS,
virtual_module_prefixes: VIRTUAL_MODULE_PREFIXES,
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use crate::analyze::matches_virtual_prefix;
#[test]
fn activates_on_rspress_and_rspress_scope() {
let plugin = RspressPlugin;
assert!(plugin.is_enabled_with_deps(&["rspress".to_string()], Path::new("/p")));
assert!(plugin.is_enabled_with_deps(&["@rspress/core".to_string()], Path::new("/p")));
assert!(
plugin.is_enabled_with_deps(&["@rspress/theme-default".to_string()], Path::new("/p"))
);
}
#[test]
fn inactive_without_rspress() {
let plugin = RspressPlugin;
assert!(!plugin.is_enabled_with_deps(&["react".to_string()], Path::new("/p")));
// A near-miss package that merely starts with `rspress` but is not the
// scope prefix must not activate the plugin.
assert!(!plugin.is_enabled_with_deps(&["rspress-plugin-foo".to_string()], Path::new("/p")));
}
#[test]
fn contributes_theme_virtual_prefixes() {
let prefixes = RspressPlugin.virtual_module_prefixes();
assert!(prefixes.contains(&"@theme/"));
assert!(prefixes.contains(&"@theme-original/"));
}
#[test]
fn theme_prefix_covers_bare_and_subpath_imports() {
let prefixes = RspressPlugin.virtual_module_prefixes();
// Bare `@theme` (the reported repro) matches the `@theme/` entry via the
// exact-bare branch; `@theme/Layout` matches as an ordinary prefix.
assert!(
prefixes
.iter()
.any(|prefix| matches_virtual_prefix(prefix, "@theme"))
);
assert!(
prefixes
.iter()
.any(|prefix| matches_virtual_prefix(prefix, "@theme/Layout"))
);
assert!(
prefixes
.iter()
.any(|prefix| matches_virtual_prefix(prefix, "@theme-original/Layout"))
);
// A real scoped package sharing the `@theme` lexical start must NOT be
// swallowed.
assert!(
!prefixes
.iter()
.any(|prefix| matches_virtual_prefix(prefix, "@theme-ui/core"))
);
}
}