use fallow_types::extract::{ImportedName, ModuleInfo};
const USE_SERVER: &str = "use server";
const SERVER_ONLY_POISON_PACKAGE: &str = "server-only";
const SERVER_ONLY_PACKAGES: &[&str] = &[
SERVER_ONLY_POISON_PACKAGE,
"next/server",
"node:fs",
"fs",
"node:fs/promises",
"fs/promises",
"node:child_process",
"child_process",
];
const NEXT_HEADERS_SOURCE: &str = "next/headers";
const NEXT_HEADERS_SERVER_NAMES: &[&str] = &["cookies", "headers", "draftMode"];
#[must_use]
pub fn is_server_only_module(module: &ModuleInfo) -> bool {
module.directives.iter().any(|d| d == USE_SERVER) || imports_server_only_code(module)
}
#[must_use]
pub fn imports_server_only_code(module: &ModuleInfo) -> bool {
module.imports.iter().any(|import| {
if SERVER_ONLY_PACKAGES.contains(&import.source.as_str()) {
return true;
}
import.source == NEXT_HEADERS_SOURCE && is_next_headers_server_import(&import.imported_name)
})
}
fn is_next_headers_server_import(name: &ImportedName) -> bool {
match name {
ImportedName::Named(named) => NEXT_HEADERS_SERVER_NAMES.contains(&named.as_str()),
ImportedName::Namespace => true,
ImportedName::Default | ImportedName::SideEffect => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discover::FileId;
use fallow_types::extract::{ImportInfo, ModuleInfo};
fn empty_module() -> ModuleInfo {
ModuleInfo::empty(FileId(0))
}
fn module_with_import(source: &str, imported: ImportedName) -> ModuleInfo {
let mut module = empty_module();
module.imports.push(ImportInfo {
source: source.to_string(),
imported_name: imported,
local_name: "x".to_string(),
is_type_only: false,
from_style: false,
span: oxc_span::Span::new(0, 10),
source_span: oxc_span::Span::new(0, 10),
});
module
}
#[test]
fn use_server_directive_is_server_only() {
let mut module = empty_module();
module.directives.push(USE_SERVER.to_string());
assert!(is_server_only_module(&module));
}
#[test]
fn use_server_directive_alone_does_not_import_server_only_code() {
let mut module = empty_module();
module.directives.push(USE_SERVER.to_string());
assert!(!imports_server_only_code(&module));
}
#[test]
fn use_server_module_importing_node_fs_still_imports_server_only_code() {
let mut module = module_with_import("node:fs", ImportedName::Named("readFile".to_string()));
module.directives.push(USE_SERVER.to_string());
assert!(imports_server_only_code(&module));
}
#[test]
fn server_only_poison_package_is_server_only() {
let module = module_with_import(SERVER_ONLY_POISON_PACKAGE, ImportedName::SideEffect);
assert!(is_server_only_module(&module));
}
#[test]
fn node_fs_and_bare_fs_both_count() {
assert!(is_server_only_module(&module_with_import(
"node:fs",
ImportedName::Named("readFileSync".to_string()),
)));
assert!(is_server_only_module(&module_with_import(
"fs",
ImportedName::Named("readFileSync".to_string()),
)));
}
#[test]
fn next_headers_named_server_api_counts() {
assert!(is_server_only_module(&module_with_import(
NEXT_HEADERS_SOURCE,
ImportedName::Named("cookies".to_string()),
)));
}
#[test]
fn next_headers_side_effect_import_does_not_count() {
assert!(!is_server_only_module(&module_with_import(
NEXT_HEADERS_SOURCE,
ImportedName::SideEffect,
)));
}
#[test]
fn plain_utility_module_is_not_server_only() {
let module = module_with_import("./format", ImportedName::Named("formatDate".to_string()));
assert!(!is_server_only_module(&module));
}
}