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/polite-dodos-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"resolve-sync": minor
---

Add preserveSymlinks option.
5 changes: 5 additions & 0 deletions .changeset/sour-parrots-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"resolve-sync": minor
---

Add "external" option.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ The absolute path to the source _file_ (not a directory) where the resolution is

An optional root boundary directory. Module resolution won't ascend beyond this directory when traversing parent paths. Defaults to `/` if unspecified.

### `external?: (id: string) => boolean`

A function that, if provided, is called with each module id. If it returns `true`, the resolver will treat the id as external and return it as-is (without attempting to resolve it). This is useful for excluding certain modules from resolution (e.g., built-ins, peer dependencies, or virtual modules).

When not specified, the default behavior depends on the environment:
In node environments this value is set to `node:module`'s `isBuiltin` function, which treats all built-in modules as external. In other environments, it defaults to `() => false`, meaning no modules are treated as external unless specified.

### `external?: (id: string) => boolean`

Optional function to mark module ids as external. When `true` is returned, that id is returned without resolution (useful for built-ins, peer dependencies, or virtual modules).

Defaults:

- In Node.js: uses `node:module`'s `isBuiltin` (treats built-ins as external).
- Elsewhere: `() => false` (no externals).

```js
const result = resolveSync("fs", {
from: "/project/src/index.js",
external: (id) => id === "fs", // treat 'fs' as external
});
// result === "fs"
```

### `exts?: string[]`

An optional array of file extensions to try when resolving files without explicit extensions. Defaults to:
Expand Down Expand Up @@ -112,6 +136,10 @@ A partial filesystem interface used by the resolver. If running in node, and not
- `readPkg(file: string): unknown` – reads and parses a JSON file (e.g., `package.json`).
- `realpath?(file: string): string` – optionally resolves symlinks or returns the canonical path.

### `preserveSymlinks?: boolean`

For use with the `--preserve-symlinks` flag in Node.js, this option is `false` by default. If set to `true`, the resolver will return the symlinked path instead of resolving it to its real path.

## Examples

### Basic usage (in Node.js)
Expand Down
1 change: 1 addition & 0 deletions fs/node.mjs → defaults/node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fsStatOpts = { throwIfNoEntry: false };
const fsRealPath =
process.platform === "win32" ? realpathSync : realpathSync.native;

export { isBuiltin as external } from "node:module";
export const fs = {
isFile(file) {
try {
Expand Down
1 change: 1 addition & 0 deletions fs/stub.mjs → defaults/stub.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const external = () => false;
export const fs = {
isFile() {
throw new Error("isFile is not implemented by default outside of node.");
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
"license": "MIT",
"author": "Dylan Piercey <dpiercey@ebay.com>",
"imports": {
"#fs": {
"types": "./src/fs.d.ts",
"node": "./fs/node.mjs",
"default": "./fs/stub.mjs"
"#defaults": {
"types": "./src/defaults.d.ts",
"node": "./defaults/node.mjs",
"default": "./defaults/stub.mjs"
}
},
"exports": {
Expand All @@ -34,7 +34,7 @@
"main": "dist/index.js",
"module": "dist/index.mjs",
"files": [
"fs",
"defaults",
"dist",
"!**/__tests__",
"!**/*.tsbuildinfo"
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,26 @@ describe("resolve - windows path normalization", () => {
});
});

describe("resolve - external option", () => {
it("returns the id as-is when external returns true", () => {
const result = resolveSync("external-module", {
from: "/project/src/index.js",
external: (id) => id === "external-module",
fs: vfs(["/project/src/file.js"]),
});
assert.equal(result, "external-module");
});

it("resolves normally when external returns false", () => {
const result = resolveSync("./file.js", {
from: "/project/src/index.js",
external: (_) => false,
fs: vfs(["/project/src/file.js"]),
});
assert.equal(result, "/project/src/file.js");
});
});

function vfs(files: (string | [string, string])[]): ResolveOptions["fs"] {
const fileMap: Record<string, string> = {};
for (const entry of files) {
Expand Down
1 change: 1 addition & 0 deletions src/fs.d.ts → src/defaults.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import type { ResolveOptions } from "resolve-sync";
export const fs: NonNullable<ResolveOptions["fs"]>;
export const external: NonNullable<ResolveOptions["external"]>;
12 changes: 8 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { exports, imports } from "resolve.exports";

import { fs as defaultFS } from "#fs";
import { external as defaultExternal, fs as defaultFS } from "#defaults";
export interface ResolveOptions {
from: string;
root?: string;
Expand All @@ -9,6 +9,8 @@ export interface ResolveOptions {
require?: boolean;
browser?: boolean;
conditions?: string[];
external?: (id: string) => boolean;
preserveSymlinks?: boolean;
fs?: {
isFile(file: string): boolean;
readPkg(file: string): unknown;
Expand All @@ -22,6 +24,7 @@ interface ResolveContext {
fromDir: string;
exts: string[];
fields: string[];
external: (id: string) => boolean;
isFile(file: string): boolean;
readPkg(file: string): unknown;
realpath(file: string): string;
Expand Down Expand Up @@ -54,9 +57,9 @@ export function resolveSync(id: string, opts: ResolveOptions): string | false {

function toContext(opts: ResolveOptions): ResolveContext {
const fs = opts.fs || defaultFS;
const realpath = fs.realpath || identity;
const realpath = (!opts.preserveSymlinks && fs.realpath) || identity;
const root = toPosix(opts.root || "/");
const from = toPosix(realpath(opts.from));
const from = toPosix(opts.from);
const fromDir = dirname(from);
const browser = !!opts.browser;
const require = !!opts.require;
Expand All @@ -67,6 +70,7 @@ function toContext(opts: ResolveOptions): ResolveContext {
exts: opts.exts || defaultExts,
fields: opts.fields || (browser ? defaultBrowserFields : defaultFields),
realpath,
external: opts.external || defaultExternal,
readPkg: fs.readPkg,
isFile: fs.isFile,
resolve: {
Expand All @@ -84,7 +88,7 @@ function resolveId(ctx: ResolveContext, id: string) {
case "#":
return resolveSubImport(ctx, ctx.fromDir, id);
default:
return resolvePkg(ctx, id);
return ctx.external(id) ? id : resolvePkg(ctx, id);
}
}

Expand Down