use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct RuntimeApiPattern {
pub file_pattern: String,
pub export_names: Vec<String>,
pub description: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct CustomRuntimeApi {
pub framework: String,
pub exports: Vec<String>,
pub file_patterns: Vec<String>,
}
pub struct RuntimeApiRegistry {
patterns: Vec<RuntimeApiPattern>,
}
impl RuntimeApiRegistry {
pub fn new() -> Self {
let mut patterns = Vec::new();
patterns.push(RuntimeApiPattern {
file_pattern: "**/loader.{js,mjs,cjs}".to_string(),
export_names: vec![
"resolve".to_string(),
"load".to_string(),
"globalPreload".to_string(),
"initialize".to_string(),
],
description: "Node.js ES Module loader hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/loaders/*.{js,mjs,cjs}".to_string(),
export_names: vec![
"resolve".to_string(),
"load".to_string(),
"globalPreload".to_string(),
"initialize".to_string(),
],
description: "Node.js ES Module loader hooks (loaders directory)".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/lib/internal/modules/esm/*.js".to_string(),
export_names: vec![
"resolve".to_string(),
"load".to_string(),
"getFormat".to_string(),
"getSource".to_string(),
"transformSource".to_string(),
],
description: "Node.js internal ES Module hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/*.test.{js,mjs,cjs,ts,mts,cts}".to_string(),
export_names: vec![
"before".to_string(),
"after".to_string(),
"beforeEach".to_string(),
"afterEach".to_string(),
],
description: "Node.js test runner hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/*.worker.{js,ts}".to_string(),
export_names: vec![
"onmessage".to_string(),
"onmessageerror".to_string(),
"onerror".to_string(),
],
description: "Web Workers event handlers".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/service-worker.{js,ts}".to_string(),
export_names: vec![
"install".to_string(),
"activate".to_string(),
"fetch".to_string(),
"message".to_string(),
"sync".to_string(),
"push".to_string(),
],
description: "Service Worker lifecycle hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/sw.{js,ts}".to_string(),
export_names: vec![
"install".to_string(),
"activate".to_string(),
"fetch".to_string(),
"message".to_string(),
"sync".to_string(),
"push".to_string(),
],
description: "Service Worker lifecycle hooks (sw.js)".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/vite.config.{js,ts}".to_string(),
export_names: vec![
"config".to_string(),
"configResolved".to_string(),
"buildStart".to_string(),
"buildEnd".to_string(),
],
description: "Vite plugin hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/webpack.config.{js,ts}".to_string(),
export_names: vec![
"apply".to_string(),
],
description: "Webpack plugin hooks".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/middleware.{js,ts}".to_string(),
export_names: vec![
"middleware".to_string(),
"config".to_string(),
],
description: "Next.js middleware".to_string(),
});
patterns.push(RuntimeApiPattern {
file_pattern: "**/*.astro".to_string(),
export_names: vec![
"getStaticPaths".to_string(),
"prerender".to_string(),
],
description: "Astro component static generation".to_string(),
});
Self { patterns }
}
pub fn add_custom_pattern(&mut self, pattern: RuntimeApiPattern) {
self.patterns.push(pattern);
}
pub fn is_runtime_api(&self, file_path: &str, export_name: &str) -> Option<String> {
let normalized_path = file_path.replace('\\', "/");
for pattern in &self.patterns {
if let Ok(glob) = globset::Glob::new(&pattern.file_pattern) {
let matcher = glob.compile_matcher();
if matcher.is_match(&normalized_path)
&& pattern.export_names.iter().any(|name| name == export_name)
{
return Some(pattern.description.clone());
}
}
}
None
}
pub fn is_runtime_api_with_custom(
&self,
file_path: &str,
export_name: &str,
custom_apis: &[CustomRuntimeApi],
) -> Option<String> {
if let Some(desc) = self.is_runtime_api(file_path, export_name) {
return Some(desc);
}
let normalized_path = file_path.replace('\\', "/");
for custom in custom_apis {
if !custom.exports.iter().any(|name| name == export_name) {
continue;
}
for pattern_str in &custom.file_patterns {
if let Ok(glob) = globset::Glob::new(pattern_str) {
let matcher = glob.compile_matcher();
if matcher.is_match(&normalized_path) {
return Some(format!("{} runtime API", custom.framework));
}
}
}
}
None
}
pub fn patterns(&self) -> &[RuntimeApiPattern] {
&self.patterns
}
}
impl Default for RuntimeApiRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nodejs_loader_hooks() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("lib/internal/modules/esm/hooks.js", "resolve")
.is_some());
assert!(registry
.is_runtime_api("lib/internal/modules/esm/hooks.js", "load")
.is_some());
assert!(registry
.is_runtime_api("custom-loader.js", "resolve")
.is_some());
assert!(registry
.is_runtime_api("loaders/typescript-loader.mjs", "load")
.is_some());
assert!(registry
.is_runtime_api("src/utils.js", "resolve")
.is_none());
}
#[test]
fn test_test_runner_hooks() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("src/app.test.js", "beforeEach")
.is_some());
assert!(registry
.is_runtime_api("tests/integration.test.ts", "afterEach")
.is_some());
assert!(registry
.is_runtime_api("src/app.js", "beforeEach")
.is_none());
}
#[test]
fn test_web_workers() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("workers/data-processor.worker.js", "onmessage")
.is_some());
assert!(registry
.is_runtime_api("src/background.worker.ts", "onerror")
.is_some());
assert!(registry
.is_runtime_api("src/app.js", "onmessage")
.is_none());
}
#[test]
fn test_service_workers() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("public/service-worker.js", "install")
.is_some());
assert!(registry
.is_runtime_api("src/sw.js", "activate")
.is_some());
assert!(registry
.is_runtime_api("service-worker.ts", "fetch")
.is_some());
}
#[test]
fn test_vite_plugins() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("vite.config.js", "config")
.is_some());
assert!(registry
.is_runtime_api("config/vite.config.ts", "buildStart")
.is_some());
}
#[test]
fn test_nextjs_middleware() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("middleware.ts", "middleware")
.is_some());
assert!(registry
.is_runtime_api("src/middleware.js", "config")
.is_some());
}
#[test]
fn test_custom_patterns() {
let registry = RuntimeApiRegistry::new();
let custom_apis = vec![CustomRuntimeApi {
framework: "Remix".to_string(),
exports: vec!["loader".to_string(), "action".to_string()],
file_patterns: vec!["**/routes/*.{jsx,tsx}".to_string()],
}];
assert!(registry
.is_runtime_api_with_custom("app/routes/index.tsx", "loader", &custom_apis)
.is_some());
assert!(registry
.is_runtime_api_with_custom("app/routes/about.jsx", "action", &custom_apis)
.is_some());
}
#[test]
fn test_cross_platform_paths() {
let registry = RuntimeApiRegistry::new();
assert!(registry
.is_runtime_api("lib\\internal\\modules\\esm\\hooks.js", "resolve")
.is_some());
assert!(registry
.is_runtime_api("lib/internal/modules/esm/hooks.js", "load")
.is_some());
}
}