mlly vs import-meta-resolve vs resolve: Module Resolution in Node.js (2026)
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 usesrequire.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
| Feature | mlly | import.meta.resolve | resolve |
|---|---|---|---|
| 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 by | Nuxt, UnJS | Node.js core | Webpack, Browserify |
| Weekly downloads | ~10M | Built-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 →