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

  1. Convert the virtual name into a real on-disk path (write to a temp/cache dir and pass FileName::Real) before invoking NodeResolver
  2. If resolution against real files is impossible, implement or use a resolver that understands your virtual FS instead of NodeResolver
  3. 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

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


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/9ac3e1d6b551b434. Report an issue: GitHub.