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 {
if module.directives.iter().any(|d| d == USE_SERVER) {
return true;
}
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 {
file_id: FileId(0),
exports: vec![],
imports: vec![],
re_exports: vec![],
dynamic_imports: vec![],
dynamic_import_patterns: vec![],
require_calls: vec![],
package_path_references: vec![],
member_accesses: vec![],
whole_object_uses: vec![],
has_cjs_exports: false,
has_angular_component_template_url: false,
content_hash: 0,
suppressions: vec![],
unknown_suppression_kinds: vec![],
unused_import_bindings: vec![],
type_referenced_import_bindings: vec![],
value_referenced_import_bindings: vec![],
line_offsets: vec![],
complexity: vec![],
flag_uses: vec![],
class_heritage: vec![],
injection_tokens: vec![],
local_type_declarations: vec![],
public_signature_type_references: vec![],
namespace_object_aliases: vec![],
iconify_prefixes: vec![],
iconify_icon_names: vec![],
auto_import_candidates: vec![],
directives: vec![],
client_only_dynamic_import_spans: vec![],
security_sinks: vec![],
security_sinks_skipped: 0,
security_unresolved_callee_sites: Vec::new(),
tainted_bindings: vec![],
sanitized_sink_args: vec![],
security_control_sites: vec![],
callee_uses: vec![],
misplaced_directives: vec![],
inline_server_action_exports: Vec::new(),
di_key_sites: Vec::new(),
has_dynamic_provide: false,
referenced_import_bindings: Vec::new(),
component_props: Vec::new(),
has_props_attrs_fallthrough: false,
has_define_expose: false,
has_define_model: false,
has_unharvestable_props: false,
component_emits: Vec::new(),
angular_inputs: Vec::new(),
angular_outputs: Vec::new(),
has_unharvestable_emits: false,
has_dynamic_emit: false,
has_emit_whole_object_use: false,
load_return_keys: Vec::new(),
has_unharvestable_load: false,
has_load_data_whole_use: false,
has_page_data_store_whole_use: false,
component_functions: Vec::new(),
react_props: Vec::new(),
hook_uses: Vec::new(),
render_edges: Vec::new(),
svelte_dispatched_events: Vec::new(),
svelte_listened_events: Vec::new(),
angular_component_selectors: Vec::new(),
angular_used_selectors: Vec::new(),
angular_entry_component_refs: Vec::new(),
has_dynamic_component_render: false,
has_dynamic_dispatch: false,
}
}
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 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));
}
}