swc-project/swc · error
node-resolver supports only files
Error message
node-resolver supports only files
What it means
NodeResolver::resolve takes a `base` FileName and uses it to anchor relative specifier resolution. Only FileName::Real is meaningful for filesystem lookups; when the caller passes any other variant (FileName::Custom, FileName::Anon, FileName::Internal, etc.) the resolver refuses to proceed and bails 'node-resolver supports only files'. The same restriction applies to the alternative `resolve_as_import`-style entry just above, which requires an existing file path.
Source
Thrown at crates/swc_ecma_loader/src/resolvers/node.rs:441
"Resolving {} from {:#?} for {:#?}",
module_specifier, base, self.target_env
);
let path = Path::new(module_specifier);
if path.is_absolute() {
if let Ok(file) = self
.resolve_as_file(path)
.or_else(|_| self.resolve_as_directory(path, false))
{
if let Ok(file) = self.wrap(file) {
return Ok(file);
}
}
}
let base = match base {
FileName::Real(v) => v,
_ => bail!("node-resolver supports only files"),
};
let base_dir = if base.is_file() {
let cwd = &Path::new(".");
base.parent().unwrap_or(cwd)
} else {
base
};
// Handle module references for the `browser` package config
// before we map aliases.
if let TargetEnv::Browser = self.target_env {
if let Some(pkg_base) = find_package_root(base) {
if let Some(item) = BROWSER_CACHE.get(&pkg_base) {
let value = item.value();
if value.module_ignores.contains(module_specifier) {
return Ok(FileName::Custom(module_specifier.into()));
}View on GitHub (pinned to 5176682b65)
Solutions
- Convert the virtual name into a real on-disk path (write to a temp/cache dir and pass FileName::Real) before invoking NodeResolver
- If resolution against real files is impossible, implement or use a resolver that understands your virtual FS instead of NodeResolver
- Check the pipeline step that produced FileName::Custom and preserve FileName::Real through it
Example fix
// before
let resolved = node_resolver.resolve(base_file_name /* FileName::Custom("virt:main.ts") */, "./dep");
// after: map virtual files to real paths first
let real_base = materialize(&base_file_name); // writes to cache dir, returns PathBuf
let resolved = node_resolver.resolve(FileName::Real(real_base), "./dep"); Defensive patterns
Strategy: type-guard
Type guard
use swc_common::FileName;
fn realBase(f: &FileName) -> Option<&std::path::Path> {
match f {
FileName::Real(p) => Some(p),
_ => None, // Custom/Anon cannot anchor NodeResolver
}
} Try / catch
match node_resolver.resolve(base, spec) {
Ok(f) => Ok(f),
Err(e) if e.to_string().contains("node-resolver supports only files") => materialize_to_real_path(base).and_then(|b| node_resolver.resolve(FileName::Real(b), spec)),
Err(e) => Err(e),
} Prevention
- Propagate FileName::Real through your whole loader chain; tag virtual files only at the edges
- Materialize virtual sources to a real cache dir before filesystem-based resolution
- Write unit tests asserting the base filename variant before calling NodeResolver
When it happens
Trigger: Calling NodeResolver::resolve with a FileName produced by another tool that wraps sources in FileName::Custom (e.g. virtual file systems in editors, plugin pipelines, or bundler loaders that tag inputs), or passing FileName::Anon for in-memory sources.
Common situations: Embedding swc in an environment (IDE plugin, playground, custom bundler) where the current file name is a virtual/custom value; migrating from an older resolver API that accepted any FileName; wiring a loader chain where an upstream transform replaced the Real filename.
Related errors
- index not found
- file not found: {}
- `{module_specifier}` matched `{prefix}` (from tsconfig.paths
- swc-loader only accepts path. Got `{name}`
- failed to get current directory
AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17).
Data as JSON: /api/errors/9ac3e1d6b551b434.
Report an issue: GitHub.