Skip to main content

mlly vs import-meta-resolve vs resolve: Module Resolution in Node.js (2026)

·PkgPulse Team

TL;DR

mlly is the UnJS module resolution utility — resolves ESM and CJS imports, finds the nearest package.json, detects module types, and provides createResolve for custom resolvers. import.meta.resolve is the built-in ESM resolution API — resolves module specifiers to file URLs, stable in Node.js 20+. resolve is the classic require.resolve algorithm as a library — synchronous path resolution, works from any directory, used by bundlers and tools. In 2026: import.meta.resolve for built-in ESM resolution, mlly for advanced resolution utilities, resolve for CJS resolution in tools.

Key Takeaways

  • mlly: ~10M weekly downloads — UnJS, ESM utilities, resolves both ESM and CJS
  • import.meta.resolve: built-in (Node.js 20+) — stable ESM resolution, returns file URLs
  • resolve: ~50M weekly downloads — implements require.resolve algorithm, works from any basedir
  • Module resolution converts specifiers ("vue") to file paths ("/node_modules/vue/dist/vue.mjs")
  • ESM resolution uses import.meta.resolve, CJS uses require.resolve
  • mlly bridges the gap with utilities that work across both module systems

The Problem

// CJS — require.resolve works great:
const vuePath = require.resolve("vue")
// → "/project/node_modules/vue/dist/vue.runtime.common.js"

// ESM — no require.resolve available:
// require.resolve("vue")  // ❌ ReferenceError: require is not defined

// import.meta.resolve is the ESM equivalent:
const vuePath = import.meta.resolve("vue")
// → "file:///project/node_modules/vue/dist/vue.runtime.esm-bundler.js"

// But what if you need to resolve from a different directory?
// Or detect if a module is ESM or CJS?
// Or resolve with custom conditions?
// → That's where mlly and resolve help

mlly

mlly — ESM module utilities:

Resolve modules

import { resolve, resolvePath, createResolve } from "mlly"

// Resolve a module specifier:
const resolved = await resolve("vue", { url: import.meta.url })
// → "file:///project/node_modules/vue/dist/vue.runtime.esm-bundler.js"

// Resolve to a file path (not URL):
const filePath = await resolvePath("vue", { url: import.meta.url })
// → "/project/node_modules/vue/dist/vue.runtime.esm-bundler.js"

// Resolve with custom conditions:
const edgePath = await resolve("vue", {
  url: import.meta.url,
  conditions: ["edge", "worker", "import"],
})

// Create a resolver with fixed base URL:
const resolveFromProject = createResolve({ url: "/path/to/project/" })
const dep = await resolveFromProject("lodash")

Detect module type

import { isValidNodeImport, detectSyntax, getProtocol } from "mlly"

// Check if a file uses ESM or CJS:
const syntax = detectSyntax(`
  import { ref } from "vue"
  export default { setup() { return { count: ref(0) } } }
`)
// → { hasESM: true, hasCJS: false, isMixed: false }

const cjsSyntax = detectSyntax(`
  const vue = require("vue")
  module.exports = { count: 0 }
`)
// → { hasESM: false, hasCJS: true, isMixed: false }

// Check if a specifier is a valid Node.js import:
await isValidNodeImport("/path/to/module.mjs")  // true
await isValidNodeImport("/path/to/module.cjs")  // true

Resolve path info

import { resolvePathSync, fileURLToPath, pathToFileURL } from "mlly"

// Convert between file URLs and paths:
const url = pathToFileURL("/Users/royce/project/src/index.ts")
// → "file:///Users/royce/project/src/index.ts"

const path = fileURLToPath("file:///Users/royce/project/src/index.ts")
// → "/Users/royce/project/src/index.ts"

Find nearest package.json

import { findPackageJSON, readPackageJSON } from "mlly"

// Find the nearest package.json:
const pkgPath = await findPackageJSON(import.meta.url)
// → "/Users/royce/project/package.json"

// Find a dependency's package.json:
const vuePkgPath = await findPackageJSON("vue", { url: import.meta.url })
// → "/Users/royce/project/node_modules/vue/package.json"

ESM interop utilities

import { interopDefault, resolveImports } from "mlly"

// Handle default export interop (CJS → ESM):
const mod = await import("cjs-package")
const defaultExport = interopDefault(mod)
// Handles: { default: { default: fn } } → fn
// Handles: { default: fn } → fn

// Resolve all import specifiers in source code:
const code = `
  import { ref } from "vue"
  import { useRouter } from "vue-router"
`
const resolved = await resolveImports(code, { url: import.meta.url })
// Replaces "vue" and "vue-router" with resolved file URLs

import.meta.resolve (Built-in)

import.meta.resolve — built-in ESM resolution:

Basic usage

// Resolve a bare specifier:
const vuePath = import.meta.resolve("vue")
// → "file:///project/node_modules/vue/dist/vue.runtime.esm-bundler.js"

// Resolve a relative path:
const utilsPath = import.meta.resolve("./utils.js")
// → "file:///project/src/utils.js"

// Resolve a subpath export:
const vueReactivity = import.meta.resolve("vue/reactivity")
// → "file:///project/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js"

Checking if a package exists

// Check if a package is installed:
function isPackageInstalled(name: string): boolean {
  try {
    import.meta.resolve(name)
    return true
  } catch {
    return false
  }
}

isPackageInstalled("vue")       // true (if installed)
isPackageInstalled("nonexist")  // false

Limitations

import.meta.resolve:
  ✅ Built-in (Node.js 20+ stable)
  ✅ Zero dependencies
  ✅ Follows Node.js ESM resolution algorithm
  ✅ Respects package.json "exports" field

  ❌ Only resolves from current file's location
  ❌ Cannot resolve from arbitrary directories
  ❌ Returns file:// URLs (not paths)
  ❌ No CJS support (ESM only)
  ❌ No custom conditions parameter
  ❌ Synchronous (can't customize async hooks)

resolve

resolve — require.resolve as a library:

Basic usage

import resolve from "resolve"

// Synchronous resolution:
const vuePath = resolve.sync("vue", {
  basedir: "/path/to/project",
})
// → "/path/to/project/node_modules/vue/dist/vue.runtime.common.js"

// Async resolution:
resolve("vue", { basedir: "/path/to/project" }, (err, resolved) => {
  console.log(resolved)
})

// Promise wrapper:
import { promisify } from "node:util"
const resolveAsync = promisify(resolve)
const path = await resolveAsync("vue", { basedir: process.cwd() })

Custom resolution

import resolve from "resolve"

// Resolve from a specific directory:
const path = resolve.sync("./utils", {
  basedir: "/path/to/project/src",
  extensions: [".ts", ".tsx", ".js", ".jsx"],
})

// Custom module directories:
const path2 = resolve.sync("my-package", {
  basedir: process.cwd(),
  moduleDirectory: ["node_modules", "custom_modules"],
})

// Package filter:
const path3 = resolve.sync("some-package", {
  basedir: process.cwd(),
  packageFilter: (pkg) => {
    // Use "module" field instead of "main":
    if (pkg.module) pkg.main = pkg.module
    return pkg
  },
})

Path filter and resolution hooks

import resolve from "resolve"

// Path filter — transform resolved paths:
const path = resolve.sync("my-lib", {
  basedir: process.cwd(),
  pathFilter: (pkg, path, relativePath) => {
    // Redirect imports:
    if (relativePath === "./lib/index.js") {
      return "./src/index.ts"
    }
    return relativePath
  },
})

// Check if a path is a core module:
resolve.isCore("fs")     // true
resolve.isCore("vue")    // false
resolve.isCore("path")   // true

How bundlers use resolve

// Webpack, Browserify, and other bundlers use resolve internally:

// Simplified bundler resolution:
import resolve from "resolve"

function resolveModule(specifier: string, fromFile: string) {
  return resolve.sync(specifier, {
    basedir: dirname(fromFile),
    extensions: [".js", ".ts", ".tsx", ".jsx", ".json"],
    packageFilter: (pkg) => {
      // Prefer "module" or "browser" fields:
      pkg.main = pkg.module || pkg.browser || pkg.main
      return pkg
    },
  })
}

// This is why bundlers can resolve TypeScript files,
// use package.json "module" field, etc.

Feature Comparison

Featuremllyimport.meta.resolveresolve
ESM resolution❌ (CJS)
CJS resolution
Custom basedir
Custom conditions
Package.json exports⚠️ (limited)
Syntax detection
URL ↔ path conversion
Sync API
Built-in✅ (Node.js 20+)
Used byNuxt, UnJSNode.js coreWebpack, Browserify
Weekly downloads~10MBuilt-in~50M

When to Use Each

Use mlly if:

  • Need ESM resolution utilities beyond import.meta.resolve
  • Want to detect ESM vs CJS syntax in files
  • Building tools that need to resolve from arbitrary directories
  • In the UnJS ecosystem (Nuxt, Nitro, unbuild)

Use import.meta.resolve if:

  • Resolving modules from the current file's location
  • Want zero dependencies (built-in Node.js 20+)
  • Checking if a package is installed
  • Simple specifier-to-URL resolution

Use resolve if:

  • Building a bundler or CJS-based tool
  • Need to resolve from arbitrary base directories
  • Want custom package.json field resolution (module, browser)
  • Need the require.resolve algorithm as a library

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on mlly v1.x, Node.js 22 import.meta.resolve, and resolve v1.x.

Compare module resolution and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.