const BUILTINS: &[&str] = &[
"assert",
"async_hooks",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"diagnostics_channel",
"dns",
"domain",
"events",
"fs",
"http",
"http2",
"https",
"inspector",
"module",
"net",
"os",
"path",
"perf_hooks",
"process",
"punycode",
"querystring",
"readline",
"repl",
"stream",
"string_decoder",
"sys",
"timers",
"tls",
"trace_events",
"tty",
"url",
"util",
"v8",
"vm",
"wasi",
"worker_threads",
"zlib",
];
const BUILTIN_SUBPATHS: &[&str] = &[
"assert/strict",
"dns/promises",
"fs/promises",
"inspector/promises",
"path/posix",
"path/win32",
"readline/promises",
"stream/consumers",
"stream/promises",
"stream/web",
"timers/promises",
"util/types",
];
pub(crate) const RUNTIME_PACKAGES: &[&str] = &["node", "bun", "deno"];
pub(crate) fn is_builtin(specifier: &str) -> bool {
if specifier.starts_with("node:") {
return true;
}
BUILTINS.contains(&specifier) || BUILTIN_SUBPATHS.contains(&specifier)
}
pub(crate) fn has_foreign_scheme(specifier: &str) -> bool {
let Some((scheme, _)) = specifier.split_once(':') else {
return false;
};
scheme.len() > 1 && scheme.bytes().all(|b| b.is_ascii_lowercase())
}
pub(crate) fn starts_like_package_name(specifier: &str) -> bool {
let bytes = specifier.as_bytes();
let name_start = |b: u8| b.is_ascii_alphanumeric();
match (bytes.first().copied(), bytes.get(1).copied()) {
(Some(b'@'), Some(second)) => name_start(second) || matches!(second, b'.' | b'_' | b'~'),
(Some(first), second) => name_start(first) && second != Some(b':'),
_ => false,
}
}
pub(crate) fn assumed_package(specifier: &str) -> Option<String> {
let sanitized = specifier.split(['?', '#']).next()?;
if !starts_like_package_name(sanitized) {
return None;
}
package_name(sanitized).or_else(|| {
let mut parts = sanitized.split('/');
let first = parts.next()?;
if first.starts_with('@') {
Some(format!("{first}/{}", parts.next()?))
} else {
Some(first.to_owned())
}
})
}
pub(crate) fn package_name(specifier: &str) -> Option<String> {
let specifier = specifier.split(['?', '#']).next()?;
if specifier.is_empty() || specifier.starts_with(['.', '/', '#', '~']) {
return None;
}
let name = if let Some(rest) = specifier.strip_prefix('@') {
let mut parts = rest.split('/');
let scope = parts.next().filter(|part| !part.is_empty())?;
let name = parts.next().filter(|part| !part.is_empty())?;
format!("@{scope}/{name}")
} else {
specifier.split('/').next().filter(|part| !part.is_empty())?.to_owned()
};
let valid = name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'@' | b'/'));
valid.then_some(name)
}
pub(crate) fn node_modules_specifier_package(specifier: &str) -> Option<String> {
let specifier = specifier.split(['?', '#']).next()?;
let segments: Vec<&str> = specifier.split(['/', '\\']).collect();
let position = segments.iter().rposition(|segment| *segment == "node_modules")?;
let rest = segments.get(position + 1..)?;
let first = rest.first().copied().filter(|part| !part.is_empty())?;
let joined = if first.starts_with('@') {
let second = rest.get(1).copied().filter(|part| !part.is_empty())?;
format!("{first}/{second}")
} else {
first.to_owned()
};
package_name(&joined)
}
pub(crate) fn bin_link(specifier: &str) -> Option<&str> {
let name = specifier
.strip_prefix("./")
.unwrap_or(specifier)
.strip_prefix("node_modules/")
.unwrap_or(specifier)
.strip_prefix(".bin/")?;
(!name.is_empty() && !name.contains(['/', '\\', '?', '#'])).then_some(name)
}
pub(crate) fn subpath<'a>(specifier: &'a str, package: &str) -> Option<&'a str> {
let rest = specifier.strip_prefix(package)?.strip_prefix('/')?;
let rest = rest.split(['?', '#']).next().unwrap_or(rest);
(!rest.is_empty()).then_some(rest)
}
pub(crate) fn types_package(name: &str) -> Option<String> {
if name.starts_with("@types/") {
return None;
}
Some(match name.strip_prefix('@').and_then(|rest| rest.split_once('/')) {
Some((scope, rest)) => format!("@types/{scope}__{rest}"),
None => format!("@types/{name}"),
})
}
pub(crate) fn typed_package(types: &str) -> Option<String> {
let rest = types.strip_prefix("@types/")?;
Some(match rest.split_once("__") {
Some((scope, name)) => format!("@{scope}/{name}"),
None => rest.to_owned(),
})
}
pub(crate) fn node_modules_package(path: &std::path::Path) -> Option<String> {
const SEGMENT: &[u8] = b"node_modules";
if !path
.as_os_str()
.as_encoded_bytes()
.windows(SEGMENT.len())
.any(|window| window == SEGMENT)
{
return None;
}
let components: Vec<_> = path.components().map(|c| c.as_os_str().to_string_lossy()).collect();
let position = components.iter().rposition(|c| c == "node_modules")?;
let first = components.get(position + 1)?;
if first.starts_with('.') {
return None;
}
if first.starts_with('@') {
let second = components.get(position + 2)?;
Some(format!("{first}/{second}"))
} else {
Some(first.to_string())
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn package_names() {
let cases = [
("lodash", Some("lodash")),
("lodash/fp", Some("lodash")),
("@scope/pkg", Some("@scope/pkg")),
("@scope/pkg/deep/file.js", Some("@scope/pkg")),
("@scope", None),
("@/alias/x", None),
("./local", None),
("../up", None),
("/abs", None),
("#internal", None),
("~/x", None),
("pkg?raw", Some("pkg")),
("", None),
];
for (specifier, expected) in cases {
assert_eq!(package_name(specifier).as_deref(), expected, "{specifier}");
}
}
#[test]
fn node_modules_specifiers() {
let cases = [
("mylib/node_modules/express", Some("express")),
("../node_modules/lodash/fp", Some("lodash")),
("/abs/node_modules/@scope/pkg/x", Some("@scope/pkg")),
("./a/node_modules/b/node_modules/c/d.js", Some("c")),
("node_modules/@scope/pkg", Some("@scope/pkg")),
("/abs/node_modules/@scope", None),
("/abs/node_modules/", None),
("my_node_modules/x", None),
("lodash", None),
("..\\node_modules\\lodash\\fp", Some("lodash")),
];
for (specifier, expected) in cases {
assert_eq!(
node_modules_specifier_package(specifier).as_deref(),
expected,
"{specifier}"
);
}
}
#[test]
fn bin_link_specifiers() {
let cases = [
(".bin/jscodeshift", Some("jscodeshift")),
("node_modules/.bin/tsc", Some("tsc")),
("./node_modules/.bin/tsc", Some("tsc")),
(".bin/n8n-node", Some("n8n-node")),
(".bin/", None),
(".bin/a/b", None),
(".bin/tool?x", None),
("./.bin/tool", None),
("../node_modules/.bin/tool", None),
(".binary/tool", None),
("tool", None),
];
for (specifier, expected) in cases {
assert_eq!(bin_link(specifier), expected, "{specifier}");
}
}
#[test]
fn builtins_and_schemes() {
assert!(is_builtin("node:fs"));
assert!(is_builtin("fs"));
assert!(is_builtin("fs/promises"));
assert!(!is_builtin("fs-extra"));
assert!(!is_builtin("buffer/"));
assert!(!is_builtin("events/"));
assert!(!is_builtin("fs/extra"));
assert!(is_builtin("stream/web"));
assert!(has_foreign_scheme("https://x.dev/a.js"));
assert!(has_foreign_scheme("virtual:pwa"));
assert!(!has_foreign_scheme("lodash"));
}
#[test]
fn types_pairing() {
assert_eq!(types_package("react").as_deref(), Some("@types/react"));
assert_eq!(types_package("@a/b").as_deref(), Some("@types/a__b"));
assert_eq!(types_package("@types/react"), None);
assert_eq!(typed_package("@types/a__b").as_deref(), Some("@a/b"));
assert_eq!(typed_package("@types/node").as_deref(), Some("node"));
assert_eq!(typed_package("react"), None);
}
#[test]
fn subpaths() {
assert_eq!(subpath("@a/b/c/d", "@a/b"), Some("c/d"));
assert_eq!(subpath("@a/b", "@a/b"), None);
}
#[test]
fn node_modules_packages() {
assert_eq!(
node_modules_package(Path::new("/r/node_modules/.pnpm/x/node_modules/@a/b/index.js")).as_deref(),
Some("@a/b")
);
assert_eq!(node_modules_package(Path::new("/r/src/a.ts")), None);
assert_eq!(node_modules_package(Path::new("/r/node_modules/.bin/tool")), None);
assert_eq!(
node_modules_package(Path::new("/r/node_modules/.vite/deps/react.js")),
None
);
}
}