Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-ideas-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"resolve-sync": minor
---

Add "silent" config option to gracefully handle missing resolved paths.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ const result = resolveSync("fs", {
// result === "fs"
```

### `silent?: boolean`

If `silent` is set to `true`, the resolver will suppress errors and return `undefined` when a module cannot be resolved, instead of throwing an exception. This is useful for optional dependencies or when you want to handle missing modules gracefully.

### `exts?: string[]`

An optional array of file extensions to try when resolving files without explicit extensions. Defaults to:
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,17 @@ describe("resolve - external option", () => {
});
});

describe("resolve - silent option", () => {
it("returns undefined instead of throwing when module is missing and silent is true", () => {
const result = resolveSync("./missing.js", {
from: "/project/src/index.js",
fs: vfs(["/project/src/file.js"]),
silent: true,
});
assert.equal(result, undefined);
});
});

function vfs(files: (string | [string, string])[]): ResolveOptions["fs"] {
const fileMap: Record<string, string> = {};
for (const entry of files) {
Expand Down
44 changes: 43 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,49 @@ import { exports, imports } from "resolve.exports";

import { external as defaultExternal, fs as defaultFS } from "#defaults";
export interface ResolveOptions {
/**
* The absolute path to the file from which to resolve the module.
*/
from: string;
/**
* The root directory for resolution. Defaults to "/".
*/
root?: string;
/**
* File extensions to consider. Defaults to [".js", ".json"].
*/
exts?: string[];
/**
* Package.json fields to check for entry points. Defaults to ["module", "main"] or ["browser", "module", "main"] if browser option is true.
*/
fields?: string[];
/**
* If true, suppresses errors and returns undefined when a module cannot be resolved. Defaults to false.
*/
silent?: boolean;
/**
* If true, resolves using CommonJS (require) semantics.
*/
require?: boolean;
/**
* If true, resolves using browser field mappings.
*/
browser?: boolean;
/**
* Additional conditions for conditional exports/imports.
*/
conditions?: string[];
/**
* Function to determine if a module id should be treated as external.
*/
external?: (id: string) => boolean;
/**
* If true, preserves symbolic links instead of resolving to real paths.
*/
preserveSymlinks?: boolean;
/**
* Custom file system interface.
*/
fs?: {
isFile(file: string): boolean;
readPkg(file: string): unknown;
Expand All @@ -24,6 +58,7 @@ interface ResolveContext {
fromDir: string;
exts: string[];
fields: string[];
silent: boolean;
external: (id: string) => boolean;
isFile(file: string): boolean;
readPkg(file: string): unknown;
Expand All @@ -40,7 +75,10 @@ const defaultFields = ["module", "main"];
const defaultBrowserFields = ["browser", "module", "main"];
const identity = (file: string) => file;

export function resolveSync(id: string, opts: ResolveOptions): string | false {
export function resolveSync(
id: string,
opts: ResolveOptions,
): string | false | undefined {
const ctx = toContext(opts);
const resolved = resolveId(ctx, id);

Expand All @@ -49,6 +87,7 @@ export function resolveSync(id: string, opts: ResolveOptions): string | false {
}

if (!resolved) {
if (ctx.silent) return;
throw new Error(`Cannot find module '${id}' from '${ctx.from}'`);
}

Expand All @@ -67,6 +106,7 @@ function toContext(opts: ResolveOptions): ResolveContext {
root,
from,
fromDir,
silent: !!opts.silent,
exts: opts.exts || defaultExts,
fields: opts.fields || (browser ? defaultBrowserFields : defaultFields),
realpath,
Expand Down Expand Up @@ -178,6 +218,7 @@ function resolvePkgPart(ctx: ResolveContext, pkgDir: string, part: string) {
const pkg = ctx.readPkg(pkgFile);

if (!pkg || typeof pkg !== "object") {
if (ctx.silent) return;
throw new Error(`Invalid package '${pkgFile}' loaded by '${ctx.from}'.`);
}

Expand All @@ -187,6 +228,7 @@ function resolvePkgPart(ctx: ResolveContext, pkgDir: string, part: string) {
: resolvePkgField(ctx, pkg, pkgDir, part);

if (resolved === undefined) {
if (ctx.silent) return;
throw new Error(
`Could not resolve entry for package '${pkgFile}' loaded by '${ctx.from}'.`,
);
Expand Down