use std::path::{Path, PathBuf};
use oxc_allocator::Allocator;
use oxc_ast::ast::{Expression, ObjectExpression};
use oxc_parser::Parser;
use oxc_span::SourceType;
use rustc_hash::FxHashSet;
use super::{Plugin, PluginResult, UsedExportRule, config_parser};
const ENABLERS: &[&str] = &["@docusaurus/core"];
const ENTRY_PATTERNS: &[&str] = &[
"docs/**/*.{md,mdx}",
"blog/**/*.{md,mdx}",
"src/pages/**/*.{ts,tsx,js,jsx,md,mdx}",
"sidebars.{js,ts}",
"src/theme/*.{ts,tsx,js,jsx}",
"src/theme/**/index.{ts,tsx,js,jsx}",
"versioned_docs/**/*.{md,mdx}",
"i18n/*/docusaurus-plugin-content-docs/**/*.{md,mdx}",
"i18n/*/docusaurus-plugin-content-blog/**/*.{md,mdx}",
"i18n/*/docusaurus-plugin-content-pages/**/*.{md,mdx}",
];
const CONFIG_PATTERNS: &[&str] = &["docusaurus.config.{js,ts,mjs}"];
const ALWAYS_USED: &[&str] = &["docusaurus.config.{js,ts,mjs}"];
const TOOLING_DEPENDENCIES: &[&str] = &[
"@docusaurus/core",
"@docusaurus/preset-classic",
"@docusaurus/plugin-content-docs",
"@docusaurus/plugin-content-blog",
"@docusaurus/plugin-content-pages",
"@docusaurus/theme-classic",
];
const VIRTUAL_MODULE_PREFIXES: &[&str] = &[
"@theme/",
"@theme-original/",
"@docusaurus/",
"@site/",
"@generated/",
];
const DEFAULT_EXPORTS: &[&str] = &["default"];
const DEFAULT_DOCS_PATH: &str = "docs";
const DEFAULT_BLOG_PATH: &str = "blog";
const DEFAULT_PAGES_PATH: &str = "src/pages";
const DEFAULT_I18N_PATH: &str = "i18n";
const DEFAULT_STATIC_DIRECTORIES: &[&str] = &["static"];
pub struct DocusaurusPlugin;
impl Plugin for DocusaurusPlugin {
fn name(&self) -> &'static str {
"docusaurus"
}
fn enablers(&self) -> &'static [&'static str] {
ENABLERS
}
fn entry_patterns(&self) -> &'static [&'static str] {
ENTRY_PATTERNS
}
fn config_patterns(&self) -> &'static [&'static str] {
CONFIG_PATTERNS
}
fn always_used(&self) -> &'static [&'static str] {
ALWAYS_USED
}
fn tooling_dependencies(&self) -> &'static [&'static str] {
TOOLING_DEPENDENCIES
}
fn virtual_module_prefixes(&self) -> &'static [&'static str] {
VIRTUAL_MODULE_PREFIXES
}
fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
vec![
("src/pages/**/*.{ts,tsx,js,jsx}", DEFAULT_EXPORTS),
("sidebars.{js,ts}", DEFAULT_EXPORTS),
("src/theme/*.{ts,tsx,js,jsx}", DEFAULT_EXPORTS),
("src/theme/**/index.{ts,tsx,js,jsx}", DEFAULT_EXPORTS),
]
}
fn resolve_config(&self, config_path: &Path, source: &str, root: &Path) -> PluginResult {
let mut result = PluginResult::default();
let mut referenced_dependencies = FxHashSet::default();
for import in config_parser::extract_imports(source, config_path) {
referenced_dependencies.insert(crate::resolve::extract_package_name(&import));
}
let Some(parsed) = parse_docusaurus_config(source, config_path, root) else {
result.referenced_dependencies = sort_strings(referenced_dependencies);
return result;
};
referenced_dependencies.extend(parsed.referenced_dependencies);
result.replace_entry_patterns = true;
result.replace_used_export_rules = true;
let mut entry_patterns = FxHashSet::default();
let mut used_export_rules = FxHashSet::default();
let mut always_used_files = parsed.always_used_files;
add_theme_swizzle_rules(&mut entry_patterns, &mut used_export_rules);
for docs in parsed.docs_instances {
add_docs_rules(&docs, &parsed.i18n_path, &mut entry_patterns);
if let Some(sidebar_path) = docs.sidebar_path {
used_export_rules.insert(sidebar_path.clone());
always_used_files.insert(sidebar_path);
}
}
for blog in parsed.blog_instances {
add_blog_rules(&blog, &parsed.i18n_path, &mut entry_patterns);
}
for pages in parsed.pages_instances {
add_pages_rules(
&pages,
&parsed.i18n_path,
&mut entry_patterns,
&mut used_export_rules,
);
}
result.entry_patterns = sort_strings(entry_patterns)
.into_iter()
.map(super::PathRule::new)
.collect();
result.used_exports = sort_strings(used_export_rules)
.into_iter()
.map(|pattern| UsedExportRule::new(pattern, DEFAULT_EXPORTS.iter().copied()))
.collect();
result.always_used_files = sort_strings(always_used_files);
result.referenced_dependencies = sort_strings(referenced_dependencies);
result
}
}
#[derive(Debug)]
struct ParsedDocusaurusConfig {
docs_instances: Vec<DocsInstance>,
blog_instances: Vec<BlogInstance>,
pages_instances: Vec<PagesInstance>,
i18n_path: String,
static_directories: Vec<String>,
always_used_files: FxHashSet<String>,
referenced_dependencies: FxHashSet<String>,
}
impl Default for ParsedDocusaurusConfig {
fn default() -> Self {
Self {
docs_instances: Vec::new(),
blog_instances: Vec::new(),
pages_instances: Vec::new(),
i18n_path: DEFAULT_I18N_PATH.to_string(),
static_directories: DEFAULT_STATIC_DIRECTORIES
.iter()
.map(|dir| (*dir).to_string())
.collect(),
always_used_files: FxHashSet::default(),
referenced_dependencies: FxHashSet::default(),
}
}
}
#[derive(Debug, Clone)]
struct DocsInstance {
path: String,
id: Option<String>,
sidebar_path: Option<String>,
}
impl Default for DocsInstance {
fn default() -> Self {
Self {
path: DEFAULT_DOCS_PATH.to_string(),
id: None,
sidebar_path: None,
}
}
}
#[derive(Debug, Clone)]
struct BlogInstance {
path: String,
id: Option<String>,
}
impl Default for BlogInstance {
fn default() -> Self {
Self {
path: DEFAULT_BLOG_PATH.to_string(),
id: None,
}
}
}
#[derive(Debug, Clone)]
struct PagesInstance {
path: String,
id: Option<String>,
}
impl Default for PagesInstance {
fn default() -> Self {
Self {
path: DEFAULT_PAGES_PATH.to_string(),
id: None,
}
}
}
fn parse_docusaurus_config(
source: &str,
config_path: &Path,
root: &Path,
) -> Option<ParsedDocusaurusConfig> {
let source_type = SourceType::from_path(config_path).unwrap_or_default();
let alloc = Allocator::default();
let parsed = Parser::new(&alloc, source, source_type).parse();
let config = config_parser::find_config_object_pub(&parsed.program)?;
let mut result = ParsedDocusaurusConfig {
i18n_path: parse_i18n_path(config, config_path, root),
static_directories: parse_static_directories(config, config_path, root),
..ParsedDocusaurusConfig::default()
};
let static_directories = result.static_directories.clone();
collect_top_level_assets(config, config_path, root, &static_directories, &mut result);
if let Some(plugins_expr) = config_parser::property_expr(config, "plugins") {
parse_plugin_entries(plugins_expr, config_path, root, &mut result);
}
if let Some(themes_expr) = config_parser::property_expr(config, "themes") {
parse_theme_entries(themes_expr, config_path, root, &mut result);
}
if let Some(presets_expr) = config_parser::property_expr(config, "presets") {
parse_preset_entries(presets_expr, config_path, root, &mut result);
}
Some(result)
}
fn collect_top_level_assets(
config: &ObjectExpression<'_>,
config_path: &Path,
root: &Path,
static_directories: &[String],
parsed: &mut ParsedDocusaurusConfig,
) {
for raw_path in property_array_object_paths(config, "scripts", "src") {
parsed.always_used_files.extend(resolve_site_asset_paths(
&raw_path,
config_path,
root,
static_directories,
));
}
for raw_path in property_array_object_paths(config, "stylesheets", "href") {
parsed.always_used_files.extend(resolve_site_asset_paths(
&raw_path,
config_path,
root,
static_directories,
));
}
for raw_path in property_path_values(config, "clientModules") {
if let Some(path) = normalize_project_path(&raw_path, config_path, root) {
parsed.always_used_files.insert(path);
}
}
}
fn parse_plugin_entries(
expr: &Expression<'_>,
config_path: &Path,
root: &Path,
parsed: &mut ParsedDocusaurusConfig,
) {
let Some(array) = config_parser::array_expression(expr) else {
return;
};
for element in &array.elements {
let Some(entry_expr) = element.as_expression() else {
continue;
};
let Some((module_name, options)) = tuple_name_and_options(entry_expr) else {
continue;
};
track_module_reference("plugins", &module_name, config_path, root, parsed);
match normalize_plugin_name(&module_name) {
Some(DocusaurusPluginKind::Docs) => {
parsed
.docs_instances
.push(parse_docs_instance(options, config_path, root));
}
Some(DocusaurusPluginKind::Blog) => {
parsed
.blog_instances
.push(parse_blog_instance(options, config_path, root));
}
Some(DocusaurusPluginKind::Pages) => {
parsed
.pages_instances
.push(parse_pages_instance(options, config_path, root));
}
None => {}
}
}
}
fn parse_theme_entries(
expr: &Expression<'_>,
config_path: &Path,
root: &Path,
parsed: &mut ParsedDocusaurusConfig,
) {
let Some(array) = config_parser::array_expression(expr) else {
return;
};
for element in &array.elements {
let Some(entry_expr) = element.as_expression() else {
continue;
};
let Some((module_name, options)) = tuple_name_and_options(entry_expr) else {
continue;
};
track_module_reference("themes", &module_name, config_path, root, parsed);
if normalize_theme_name(&module_name) == Some(DocusaurusThemeKind::Classic)
&& let Some(options) = options
{
parsed
.always_used_files
.extend(parse_theme_custom_css(options, config_path, root));
}
}
}
fn parse_preset_entries(
expr: &Expression<'_>,
config_path: &Path,
root: &Path,
parsed: &mut ParsedDocusaurusConfig,
) {
let Some(array) = config_parser::array_expression(expr) else {
return;
};
for element in &array.elements {
let Some(entry_expr) = element.as_expression() else {
continue;
};
let Some((module_name, options)) = tuple_name_and_options(entry_expr) else {
continue;
};
track_module_reference("presets", &module_name, config_path, root, parsed);
if normalize_preset_name(&module_name) == Some(DocusaurusPresetKind::Classic) {
parse_classic_preset(options, config_path, root, parsed);
}
}
}
fn track_module_reference(
kind: &str,
module_name: &str,
config_path: &Path,
root: &Path,
parsed: &mut ParsedDocusaurusConfig,
) {
if let Some(dep) = normalize_module_dependency(kind, module_name) {
parsed.referenced_dependencies.insert(dep);
} else if let Some(local_path) = normalize_local_module_path(module_name, config_path, root) {
parsed.always_used_files.insert(local_path);
}
}
fn parse_classic_preset(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
parsed: &mut ParsedDocusaurusConfig,
) {
parsed
.docs_instances
.extend(parse_optional_docs_preset_section(
options,
config_path,
root,
));
parsed
.blog_instances
.extend(parse_optional_blog_preset_section(
options,
config_path,
root,
));
parsed
.pages_instances
.extend(parse_optional_pages_preset_section(
options,
config_path,
root,
));
if let Some(theme_options) =
options.and_then(|obj| config_parser::property_object(obj, "theme"))
{
parsed
.always_used_files
.extend(parse_theme_custom_css(theme_options, config_path, root));
}
}
fn parse_optional_docs_preset_section(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> Vec<DocsInstance> {
match options.and_then(|obj| config_parser::property_expr(obj, "docs")) {
Some(expr) if config_parser::is_disabled_expression(expr) => Vec::new(),
Some(expr) => vec![parse_docs_instance(
config_parser::object_expression(expr),
config_path,
root,
)],
None => vec![DocsInstance::default()],
}
}
fn parse_optional_blog_preset_section(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> Vec<BlogInstance> {
match options.and_then(|obj| config_parser::property_expr(obj, "blog")) {
Some(expr) if config_parser::is_disabled_expression(expr) => Vec::new(),
Some(expr) => vec![parse_blog_instance(
config_parser::object_expression(expr),
config_path,
root,
)],
None => vec![BlogInstance::default()],
}
}
fn parse_optional_pages_preset_section(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> Vec<PagesInstance> {
match options.and_then(|obj| config_parser::property_expr(obj, "pages")) {
Some(expr) if config_parser::is_disabled_expression(expr) => Vec::new(),
Some(expr) => vec![parse_pages_instance(
config_parser::object_expression(expr),
config_path,
root,
)],
None => vec![PagesInstance::default()],
}
}
fn parse_docs_instance(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> DocsInstance {
let mut instance = DocsInstance::default();
if let Some(options) = options {
if let Some(path) = property_project_path(options, "path", config_path, root) {
instance.path = path;
}
instance.id = normalize_instance_id(config_parser::property_string(options, "id"));
instance.sidebar_path = property_project_path(options, "sidebarPath", config_path, root);
}
instance
}
fn parse_blog_instance(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> BlogInstance {
let mut instance = BlogInstance::default();
if let Some(options) = options {
if let Some(path) = property_project_path(options, "path", config_path, root) {
instance.path = path;
}
instance.id = normalize_instance_id(config_parser::property_string(options, "id"));
}
instance
}
fn parse_pages_instance(
options: Option<&ObjectExpression<'_>>,
config_path: &Path,
root: &Path,
) -> PagesInstance {
let mut instance = PagesInstance::default();
if let Some(options) = options {
if let Some(path) = property_project_path(options, "path", config_path, root) {
instance.path = path;
}
instance.id = normalize_instance_id(config_parser::property_string(options, "id"));
}
instance
}
fn parse_theme_custom_css(
options: &ObjectExpression<'_>,
config_path: &Path,
root: &Path,
) -> Vec<String> {
property_path_values(options, "customCss")
.into_iter()
.filter_map(|raw| normalize_project_path(&raw, config_path, root))
.collect()
}
fn parse_i18n_path(config: &ObjectExpression<'_>, config_path: &Path, root: &Path) -> String {
config_parser::property_object(config, "i18n")
.and_then(|i18n| property_project_path(i18n, "path", config_path, root))
.unwrap_or_else(|| DEFAULT_I18N_PATH.to_string())
}
fn parse_static_directories(
config: &ObjectExpression<'_>,
config_path: &Path,
root: &Path,
) -> Vec<String> {
let directories = config_parser::property_expr(config, "staticDirectories")
.map(config_parser::expression_to_path_values)
.unwrap_or_default()
.into_iter()
.filter_map(|raw| normalize_project_path(&raw, config_path, root))
.collect::<Vec<_>>();
if directories.is_empty() {
DEFAULT_STATIC_DIRECTORIES
.iter()
.map(|dir| (*dir).to_string())
.collect()
} else {
sort_strings(directories)
}
}
fn add_theme_swizzle_rules(
entry_patterns: &mut FxHashSet<String>,
used_exports: &mut FxHashSet<String>,
) {
entry_patterns.insert("src/theme/*.{ts,tsx,js,jsx}".to_string());
entry_patterns.insert("src/theme/**/index.{ts,tsx,js,jsx}".to_string());
used_exports.insert("src/theme/*.{ts,tsx,js,jsx}".to_string());
used_exports.insert("src/theme/**/index.{ts,tsx,js,jsx}".to_string());
}
fn add_docs_rules(
instance: &DocsInstance,
i18n_path: &str,
entry_patterns: &mut FxHashSet<String>,
) {
entry_patterns.insert(format!("{}/**/*.{{md,mdx}}", instance.path));
entry_patterns.insert(format!(
"{i18n_path}/*/{}/**/*.{{md,mdx}}",
docs_i18n_dir(instance.id.as_deref())
));
entry_patterns.insert(format!(
"{}/**/*.{{md,mdx}}",
docs_versioned_dir(instance.id.as_deref())
));
}
fn add_blog_rules(
instance: &BlogInstance,
i18n_path: &str,
entry_patterns: &mut FxHashSet<String>,
) {
entry_patterns.insert(format!("{}/**/*.{{md,mdx}}", instance.path));
entry_patterns.insert(format!(
"{i18n_path}/*/{}/**/*.{{md,mdx}}",
plugin_i18n_dir("docusaurus-plugin-content-blog", instance.id.as_deref())
));
}
fn add_pages_rules(
instance: &PagesInstance,
i18n_path: &str,
entry_patterns: &mut FxHashSet<String>,
used_exports: &mut FxHashSet<String>,
) {
entry_patterns.insert(format!("{}/**/*.{{ts,tsx,js,jsx,md,mdx}}", instance.path));
entry_patterns.insert(format!(
"{i18n_path}/*/{}/**/*.{{md,mdx}}",
plugin_i18n_dir("docusaurus-plugin-content-pages", instance.id.as_deref())
));
used_exports.insert(format!("{}/**/*.{{ts,tsx,js,jsx}}", instance.path));
}
fn docs_i18n_dir(id: Option<&str>) -> String {
plugin_i18n_dir("docusaurus-plugin-content-docs", id)
}
fn docs_versioned_dir(id: Option<&str>) -> String {
match normalize_instance_id_ref(id) {
Some(id) => format!("{id}_versioned_docs"),
None => "versioned_docs".to_string(),
}
}
fn plugin_i18n_dir(base: &str, id: Option<&str>) -> String {
match normalize_instance_id_ref(id) {
Some(id) => format!("{base}-{id}"),
None => base.to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DocusaurusPluginKind {
Docs,
Blog,
Pages,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DocusaurusThemeKind {
Classic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DocusaurusPresetKind {
Classic,
}
fn normalize_plugin_name(module_name: &str) -> Option<DocusaurusPluginKind> {
match module_name {
"@docusaurus/plugin-content-docs" | "content-docs" | "docusaurus-plugin-content-docs" => {
Some(DocusaurusPluginKind::Docs)
}
"@docusaurus/plugin-content-blog" | "content-blog" | "docusaurus-plugin-content-blog" => {
Some(DocusaurusPluginKind::Blog)
}
"@docusaurus/plugin-content-pages"
| "content-pages"
| "docusaurus-plugin-content-pages" => Some(DocusaurusPluginKind::Pages),
_ => None,
}
}
fn normalize_theme_name(module_name: &str) -> Option<DocusaurusThemeKind> {
match module_name {
"@docusaurus/theme-classic" | "classic" | "docusaurus-theme-classic" => {
Some(DocusaurusThemeKind::Classic)
}
_ => None,
}
}
fn normalize_preset_name(module_name: &str) -> Option<DocusaurusPresetKind> {
match module_name {
"@docusaurus/preset-classic" | "classic" | "docusaurus-preset-classic" => {
Some(DocusaurusPresetKind::Classic)
}
_ => None,
}
}
fn normalize_module_dependency(kind: &str, module_name: &str) -> Option<String> {
if module_name.starts_with('.') || module_name.starts_with('/') {
return None;
}
let normalized = match (kind, module_name) {
("plugins", "content-docs") => "@docusaurus/plugin-content-docs",
("plugins", "content-blog") => "@docusaurus/plugin-content-blog",
("plugins", "content-pages") => "@docusaurus/plugin-content-pages",
("themes", "classic") => "@docusaurus/theme-classic",
("presets", "classic") => "@docusaurus/preset-classic",
_ => module_name,
};
Some(crate::resolve::extract_package_name(normalized))
}
fn normalize_local_module_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
(raw.starts_with('.') || raw.starts_with('/'))
.then(|| config_parser::normalize_config_path(raw, config_path, root))
.flatten()
}
fn normalize_project_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
config_parser::normalize_config_path(raw, config_path, root)
}
fn resolve_site_asset_paths(
raw: &str,
config_path: &Path,
root: &Path,
static_directories: &[String],
) -> Vec<String> {
if is_external_resource(raw) {
return Vec::new();
}
if let Some(site_relative) = raw.strip_prefix('/') {
return static_directories
.iter()
.filter_map(|directory| {
let candidate = join_project_relative_path(directory, site_relative);
let normalized = config_parser::normalize_config_path(
&format!("/{candidate}"),
config_path,
root,
)?;
root.join(&normalized).is_file().then_some(normalized)
})
.collect();
}
normalize_project_path(raw, config_path, root)
.into_iter()
.collect()
}
fn tuple_name_and_options<'a>(
expr: &'a Expression<'a>,
) -> Option<(String, Option<&'a ObjectExpression<'a>>)> {
match expr {
Expression::ArrayExpression(tuple) => {
let name = tuple
.elements
.first()
.and_then(|item| item.as_expression())
.and_then(config_parser::expression_to_path_string)?;
let options = tuple
.elements
.get(1)
.and_then(|item| item.as_expression())
.and_then(config_parser::object_expression);
Some((name, options))
}
_ => config_parser::expression_to_path_string(expr).map(|name| (name, None)),
}
}
fn property_project_path(
obj: &ObjectExpression<'_>,
key: &str,
config_path: &Path,
root: &Path,
) -> Option<String> {
config_parser::property_expr(obj, key)
.and_then(config_parser::expression_to_path_string)
.and_then(|raw| normalize_project_path(&raw, config_path, root))
}
fn property_path_values(obj: &ObjectExpression<'_>, key: &str) -> Vec<String> {
config_parser::property_expr(obj, key)
.map(config_parser::expression_to_path_values)
.unwrap_or_default()
}
fn property_array_object_paths(obj: &ObjectExpression<'_>, key: &str, field: &str) -> Vec<String> {
let Some(expr) = config_parser::property_expr(obj, key) else {
return Vec::new();
};
let Some(array) = config_parser::array_expression(expr) else {
return Vec::new();
};
let mut paths = Vec::new();
for element in &array.elements {
let Some(item) = element.as_expression() else {
continue;
};
match item {
Expression::ObjectExpression(object) => {
if let Some(raw) = config_parser::property_expr(object, field)
.and_then(config_parser::expression_to_path_string)
{
paths.push(raw);
}
}
_ => {
if let Some(raw) = config_parser::expression_to_path_string(item) {
paths.push(raw);
}
}
}
}
paths
}
fn normalize_instance_id(id: Option<String>) -> Option<String> {
id.and_then(|id| normalize_instance_id_ref(Some(id.as_str())).map(str::to_string))
}
fn normalize_instance_id_ref(id: Option<&str>) -> Option<&str> {
match id {
Some(id) if !id.is_empty() && id != "default" => Some(id),
_ => None,
}
}
fn join_project_relative_path(base: &str, child: &str) -> String {
PathBuf::from(base)
.join(child.trim_start_matches('/'))
.to_string_lossy()
.replace('\\', "/")
}
fn is_external_resource(raw: &str) -> bool {
raw.contains("://") || raw.starts_with("//") || raw.starts_with("data:")
}
fn sort_strings(values: impl IntoIterator<Item = String>) -> Vec<String> {
let mut values: Vec<String> = values.into_iter().collect();
values.sort();
values.dedup();
values
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::*;
fn has_entry_pattern(result: &PluginResult, pattern: &str) -> bool {
result
.entry_patterns
.iter()
.any(|entry_pattern| entry_pattern.pattern == pattern)
}
fn has_used_export_rule(result: &PluginResult, pattern: &str) -> bool {
result
.used_exports
.iter()
.any(|rule| rule.path.pattern == pattern && rule.exports == vec!["default".to_string()])
}
#[test]
fn resolve_config_supports_async_config_creators_and_custom_roots() {
let temp = tempdir().expect("temp dir");
fs::create_dir_all(temp.path().join("static-assets/styles")).expect("styles dir");
fs::create_dir_all(temp.path().join("public/scripts")).expect("scripts dir");
fs::write(temp.path().join("static-assets/styles/local.css"), "").expect("style file");
fs::write(temp.path().join("public/scripts/runtime.js"), "").expect("script file");
let source = r#"
export default async function createConfigAsync() {
return {
staticDirectories: ["static-assets", "public"],
i18n: {
path: "translations",
},
scripts: [
"./src/client/custom-script.js",
"/scripts/runtime.js",
"https://cdn.example.com/script.js",
],
stylesheets: [
{ href: "./src/css/extra.css" },
"/styles/local.css",
],
clientModules: ["./src/client/global-client.js"],
presets: [
[
"classic",
{
docs: {
path: "knowledge",
sidebarPath: "./knowledge-sidebars.ts",
},
blog: {
path: "updates",
},
pages: {
path: "site-pages",
},
theme: {
customCss: ["./src/css/custom.css"],
},
},
],
],
plugins: [
[
"docusaurus-plugin-content-docs",
{
id: "community",
path: "community",
sidebarPath: "./community-sidebars.ts",
},
],
],
themes: ["docusaurus-theme-classic"],
};
}
"#;
let plugin = DocusaurusPlugin;
let result = plugin.resolve_config(
&temp.path().join("docusaurus.config.ts"),
source,
temp.path(),
);
assert!(result.replace_entry_patterns);
assert!(result.replace_used_export_rules);
assert!(has_entry_pattern(&result, "knowledge/**/*.{md,mdx}"));
assert!(has_entry_pattern(&result, "community/**/*.{md,mdx}"));
assert!(has_entry_pattern(
&result,
"site-pages/**/*.{ts,tsx,js,jsx,md,mdx}"
));
assert!(has_entry_pattern(
&result,
"community_versioned_docs/**/*.{md,mdx}"
));
assert!(has_entry_pattern(
&result,
"translations/*/docusaurus-plugin-content-docs-community/**/*.{md,mdx}"
));
assert!(has_used_export_rule(
&result,
"site-pages/**/*.{ts,tsx,js,jsx}"
));
assert!(
result
.always_used_files
.contains(&"knowledge-sidebars.ts".to_string())
);
assert!(
result
.always_used_files
.contains(&"community-sidebars.ts".to_string())
);
assert!(
result
.always_used_files
.contains(&"src/css/custom.css".to_string())
);
assert!(
result
.always_used_files
.contains(&"src/client/custom-script.js".to_string())
);
assert!(
result
.always_used_files
.contains(&"src/css/extra.css".to_string())
);
assert!(
result
.always_used_files
.contains(&"public/scripts/runtime.js".to_string())
);
assert!(
result
.always_used_files
.contains(&"static-assets/styles/local.css".to_string())
);
assert!(
result
.referenced_dependencies
.contains(&"@docusaurus/preset-classic".to_string())
);
assert!(
result
.referenced_dependencies
.contains(&"docusaurus-plugin-content-docs".to_string())
);
assert!(
result
.referenced_dependencies
.contains(&"docusaurus-theme-classic".to_string())
);
}
#[test]
fn resolve_config_respects_disabled_classic_sections() {
let source = r#"
export default {
presets: [
[
"classic",
{
docs: false,
blog: false,
pages: { path: "site-pages" },
},
],
],
};
"#;
let plugin = DocusaurusPlugin;
let result = plugin.resolve_config(
Path::new("/project/docusaurus.config.ts"),
source,
Path::new("/project"),
);
assert!(!has_entry_pattern(&result, "docs/**/*.{md,mdx}"));
assert!(!has_entry_pattern(&result, "blog/**/*.{md,mdx}"));
assert!(has_entry_pattern(
&result,
"site-pages/**/*.{ts,tsx,js,jsx,md,mdx}"
));
}
#[test]
fn resolve_config_limits_dynamic_rules_to_enabled_content_plugins() {
let source = r#"
export default {
themes: ["classic"],
};
"#;
let plugin = DocusaurusPlugin;
let result = plugin.resolve_config(
Path::new("/project/docusaurus.config.ts"),
source,
Path::new("/project"),
);
assert!(has_entry_pattern(&result, "src/theme/*.{ts,tsx,js,jsx}"));
assert!(!has_entry_pattern(&result, "docs/**/*.{md,mdx}"));
assert!(!has_entry_pattern(&result, "blog/**/*.{md,mdx}"));
assert!(!has_entry_pattern(
&result,
"src/pages/**/*.{ts,tsx,js,jsx,md,mdx}"
));
assert!(!has_used_export_rule(&result, "sidebars.{js,ts}"));
}
#[test]
fn resolve_config_does_not_assume_sidebar_file_for_classic_docs() {
let source = r#"
export default {
presets: ["classic"],
};
"#;
let plugin = DocusaurusPlugin;
let result = plugin.resolve_config(
Path::new("/project/docusaurus.config.ts"),
source,
Path::new("/project"),
);
assert!(has_entry_pattern(&result, "docs/**/*.{md,mdx}"));
assert!(
!result
.always_used_files
.iter()
.any(|path| path == "sidebars.js" || path == "sidebars.ts")
);
assert!(!has_used_export_rule(&result, "sidebars.{js,ts}"));
}
#[test]
fn resolve_config_normalizes_default_plugin_instance_ids() {
let source = r#"
export default {
plugins: [
[
"@docusaurus/plugin-content-docs",
{
id: "default",
},
],
],
};
"#;
let plugin = DocusaurusPlugin;
let result = plugin.resolve_config(
Path::new("/project/docusaurus.config.ts"),
source,
Path::new("/project"),
);
assert!(has_entry_pattern(
&result,
"i18n/*/docusaurus-plugin-content-docs/**/*.{md,mdx}"
));
assert!(has_entry_pattern(&result, "versioned_docs/**/*.{md,mdx}"));
assert!(!has_entry_pattern(
&result,
"i18n/*/docusaurus-plugin-content-docs-default/**/*.{md,mdx}"
));
assert!(!has_entry_pattern(
&result,
"default_versioned_docs/**/*.{md,mdx}"
));
}
}