npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

locter

v4.1.0

Published

A library to locate files/modules by criteria, read them and write them back!

Readme


Features

  • 🔍 Locatelocate, locateMany, locateUp: fast-glob discovery returning structured LocatorInfo records.
  • 📖 Read — one read() for JSON, YAML, .conf, and JS/TS modules in any runtime (powered by jiti); readAsModule() for the uniform module-record shape.
  • ✍️ Writewrite() values back without pulling serializers into your app: comment-preserving YAML, indent-preserving JSON, rc9-style .conf.
  • 🔁 Sync twins — every operation ships as fn / fnSync, derived from one shared implementation.
  • 🧩 Extensible — register custom formats with independent reader/writer slots; override built-ins per extension.
  • 🚨 Typed errorsNotFoundError, LoadError, WriteError, … each carrying path and cause.

Table of Contents

Installation

npm install locter --save

Usage

The following examples are based on some shared assumptions:

  • A folder named files exists in the root directory.
  • The folder files contains the following files:
    • example.js
    • example.json
    • example.ts
    • example-long.ts

Locator

Multiple

Locating multiple files will return information about all files matching the pattern.

import { locateMany } from 'locter';

(async () => {
    let files = await locateMany(
        'files/example.{js,.ts}'
    );

    console.log(files);
    /*
    [
        { directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' },
        { directory: '/cwd/files', name: 'example', extension: '.ts', path: '/cwd/files/example.ts' }
    ]
     */

    files = await locateMany(
        'files/*.{js,ts}'
    );

    console.log(files);
    /*
    [
        { directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' },
        { directory: '/cwd/files', name: 'example', extension: '.ts', path: '/cwd/files/example.ts' },
        { directory: '/cwd/files', name: 'example-long', extension: '.ts', path: '/cwd/files/example-long.ts' }
    ]
     */
})

A synchronous variant is also available: locateManySync

Single

Locating a single file will return information about the first file matching the pattern.

import { locate } from 'locter';

(async () => {
    let file = await locate(
        'files/example.{js,.ts}'
    );

    console.log(file);
    /*
    { directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' }
     */
})

A synchronous variant is also available: locateSync

Walking up

locateUp walks from a starting directory toward the filesystem root and returns the first match (useful for discovering config files at the repo root from any sub-directory):

import { locateUp } from 'locter';

(async () => {
    const info = await locateUp('trapi.config.{ts,mts,cts,mjs,cjs,js,json}', {
        cwd: process.cwd(),
    });

    console.log(info);
    /*
    { directory: '/repo', name: 'trapi.config', extension: '.ts', path: '/repo/trapi.config.ts' }
    or `undefined` if nothing matched
     */
})

Pass stopAt: '/repo' to cap the walk at a known ceiling — inclusive, so the ceiling directory itself is still searched.

A synchronous variant locateUpSync is also available.

Options

locate / locateMany (and their sync variants) accept an options object:

| Option | Type | Default | Notes | |-------------------|-------------------------|-----------------|--------------------------------------------------------| | cwd | string \| string[] | process.cwd() | Working directory (or directories) the pattern is resolved against | | ignore | string \| string[] | [] | Patterns to exclude | | onlyFiles | boolean | true | Match files only | | onlyDirectories | boolean | false | Match directories only | | dot | boolean | false | Match dotfiles (e.g. .env, .npmrc) for wildcard patterns |

If both flags are set, onlyDirectories: true takes precedence over onlyFiles; when neither restricts the match (onlyFiles: false), files and directories are returned.

locateUp / locateUpSync take the same options except cwd must be a single string, and additionally accept stopAt?: string (inclusive ceiling for the walk).

File-name helpers

Two small helpers for working with the extensions locter routes on:

import { getFileNameExtension, removeFileNameExtension } from 'locter';

getFileNameExtension('seed.ts');                    // '.ts'
getFileNameExtension('seed.ts', ['.js', '.mjs']);   // undefined (not allowed)

removeFileNameExtension('seed.ts');                 // 'seed'
removeFileNameExtension('seed.ts', ['.js']);        // 'seed.ts' (not matched)

Reader

Two verbs, two shapes:

  • readraw: the plain parsed value for data formats (JSON/YAML/Conf and custom readers) — mutable and round-trip-symmetric with write. For modules it returns the normalized module record: a module is a record, and the normalization only irons out the CJS/ESM interop divergence so read and readSync agree on one shape.
  • readAsModule — everything as a module record: the uniform, frozen record shape regardless of format — for export inspection, interop, and uniform handling of mixed-extension config.

Either a string or the output of the locate/locateSync method can be passed as argument; both verbs have *Sync twins.

import { locate, read, readSync } from 'locter';

(async () => {
    const config = await read('config.yml');
    // → the plain parsed value: { port: 3000, ... } — mutable,
    //   round-trip-symmetric with write()

    const settings = await read('trapi.config.ts');
    // → the module as a normalized record; unwrap explicitly:
    //   settings.default

    const file = await locate('files/example.{js,.ts}');
    const content = readSync(file);   // sync twin
})

Bare module specifiers (no file extension) are routed to the module reader, so read('yaml') behaves like import('yaml') — note that reading a module executes it.

Per-call format override

Every read/write verb accepts an options object with a format key — a registered format id (a built-in: module, conf, json, yaml, text — or the id of a rule registered via registerFormat) used instead of extension dispatch. The most useful consequence: module files become readable without being executed:

import { read } from 'locter';

(async () => {
    // the source of a module file — no evaluation, no side effects
    const source = await read('vite.config.ts', { format: 'text' });

    // extensionless files become readable, too
    const license = await read('LICENSE', { format: 'text' });
})

Unknown format ids throw LocterError.

Module records — readAsModule

Every readAsModule / readAsModuleSync result is a module record — a frozen, null-prototype object shaped like an ES module namespace: default always holds the loaded value, and when that value is an object, its top-level keys are re-exposed as named exports. For modules, read and readAsModule agree; for data formats, read returns what readAsModule(...).default holds.

import { readAsModule } from 'locter';

(async () => {
    const pkg = await readAsModule('package.json');

    console.log(pkg.default);   // the full parsed JSON value
    console.log(pkg.version);   // top-level keys are named exports
})

For JavaScript/TypeScript files the module's own namespace passes through unchanged; the output of every other reader (built-in JSON/YAML/Conf and user-registered ones) is always wrapped — even if the parsed data happens to contain an __esModule key (read of such data likewise returns it untouched — a literal __esModule key never turns data into a module). The record is read-only: to mutate the loaded data, work with .default (for data records that is the same object read returns).

Records additionally carry a private, non-forgeable brand: isModuleRecord tells records produced by readAsModule() apart from arbitrary data, and write uses the brand to unwrap .default automatically on write-back.

To pick an export by predicate, the getModuleExport helper is available:

import { getModuleExport, readAsModule } from 'locter';

(async () => {
    const record = await readAsModule('files/example.ts');

    const found = getModuleExport(record, (key) => key === 'myExport');
    // → { key: 'myExport', value: ... }, or undefined if no key matches
})

Reading a package.json field

readPackageField reads a top-level field from the nearest package.json — handy for the common pkg.<your-config-key> config-loading fallback (vite's pkg.vite, eslint's pkg.eslintConfig, etc.):

import { readPackageField } from 'locter';

(async () => {
    const cwd = process.cwd();

    const value = await readPackageField<{ entry: string }>('myapp', { cwd });
    // → the value of the `myapp` field, or undefined if package.json
    //   or the field is absent.

    // Walk parent directories until a package.json is found:
    const parentValue = await readPackageField('myapp', {
        cwd,
        walkUp: true,
        stopAt: '/repo', // optional ceiling, inclusive
    });
})

Throws LoadError if the resolved package.json is malformed. A sync variant readPackageFieldSync is also available.

Writer

The write method serializes a value to a file, dispatched by extension like read — no need to pull yaml or a .conf serializer into the consuming application:

import { read, readAsModule, write } from 'locter';

(async () => {
    // read → modify → write, fully symmetric
    const config = await read('config.yml');
    config.port = 8080;
    await write('config.yml', config);

    // records produced by readAsModule() are unwrapped automatically —
    // the file receives the plain value, not the record wrapper
    const record = await readAsModule('config.yml');
    record.default.port = 8080;
    await write('config.yml', record);

    // plain values work just as well
    await write('generated/settings.json', { port: 8080 });
})

A synchronous variant is also available: writeSync.

Write semantics:

  • Missing parent directories are created (mkdir -p).
  • Output always ends with a single trailing newline.
  • Values that are records produced by readAsModule() (detected via the private brand — an __esModule key in plain data does not count) are unwrapped to their .default value before serialization.
  • Module formats (.js, .ts, …) are read-only: write('config.ts', …) throws WriteError, as does writing to a bare module specifier.

An explicit format id lifts both restrictions of the last bullet — naming the format resolves the ambiguity those guards exist for:

import { write } from 'locter';

(async () => {
    // scaffold a module file from raw source ('text' bypasses read-only)
    await write('generated/config.ts', 'export default {};', { format: 'text' });

    // extensionless target, explicit format
    await write('.myapprc', { port: 3000 }, { format: 'json' });
})

Per-format behavior:

  • JSON — 4-space indent by default; configurable per instance: new JSONWriter({ indent }) accepts a number of spaces, a literal indent string, or 'auto' (detect and keep the indentation of the existing file).

  • YAMLcomment-preserving write-back: when the target file exists, the value is grafted into the parsed document, so comments and anchors attached to surviving keys survive. Arrays and type-changed nodes are replaced wholesale. A corrupt existing file throws instead of being overwritten.

    # port used by the dev server
    port: 3000
    await write('config.yml', { port: 8080 });
    # port used by the dev server
    port: 8080
  • Conf — the inverse of the reader: nested objects become dot-separated key=value lines, arrays repeated key[]= lines. Round-trips are structural, not textual: comments and line order are dropped, and a string that parses as another type ('123') reads back as that type (123).

Writing a package.json field

writePackageField is the write-side companion of readPackageField (same cwd / walkUp / stopAt semantics): it sets a top-level field of the nearest package.json and writes it back preserving the file's existing indentation.

import { writePackageField } from 'locter';

(async () => {
    await writePackageField('myapp', { entry: 'src' }, { cwd: process.cwd() });

    // passing undefined removes the field
    await writePackageField('myapp', undefined);
})

Throws NotFoundError when no package.json could be located. A sync variant writePackageFieldSync is also available.

Formats

A format couples an extension set with a reader and an optional writer. The built-in formats are pre-registered:

  • conf (.conf): ConfReader + ConfWriter
  • json (.json): JSONReader + JSONWriter
  • yaml (.yml, .yaml): YAMLReader + YAMLWriter
  • text (.txt): TextReader + TextWriter — raw string content, no value coercion (read('notes.txt') is the file content; writes accept strings only).
  • module (.js, .mjs, .mts, .cjs, .cts, .ts): ModuleReader (read-only) — loads modules independent of the environment (cjs or esm).

To register a format for other file types, use registerFormat. Rules are matched in registration order, before the built-ins — registering an extension a built-in claims (e.g. .json) overrides it. The reader and writer slots are independent: a reader-only rule overrides how .json is read without shadowing the built-in JSON writer (and vice versa).

import { registerFormat } from 'locter';

registerFormat({
    test: ['.ext'],
    reader: {
        async read(input: string) {
            // ...
        },
        readSync(input: string) {
            // ...
        },
    },
    writer: {
        async write(path: string, value: unknown) {
            // ...
        },
        writeSync(path: string, value: unknown) {
            // ...
        },
    },
});

For text-based formats, extending the abstract TextFileReader / TextFileWriter base classes is simpler than implementing the IReader / IWriter ports by hand: the bases own UTF-8 I/O, typed error wrapping, and derive the sync and async surface from a single implementation — only parse(content) respectively stringify(value) must be provided. The built-in JSON/YAML/Conf formats are implemented this way.

import { TextFileReader, TextFileWriter, registerFormat } from 'locter';
import { parse, stringify } from 'smol-toml';

class TomlReader extends TextFileReader {
    parse(content: string) {
        return parse(content);
    }
}

class TomlWriter extends TextFileWriter {
    stringify(value: unknown) {
        return stringify(value);
    }
}

registerFormat({
    test: ['.toml'],
    reader: () => new TomlReader(),
    writer: () => new TomlWriter(),
});

Instead of instances, the slots accept factory functions (as above) — a factory is invoked lazily on the first matching input and the first successfully constructed instance is cached (a factory that throws is retried on the next match).

Every rule has a stable id (auto-generated, or set explicitly). Registering an existing id replaces that rule in place; built-in ids (module, conf, json, yaml) are reserved. The registry can be inspected and unwound:

import { registerFormat, unregisterFormat, useFormatRegistry } from 'locter';

const registration = registerFormat({ test: ['.json5'], reader: json5Reader });
unregisterFormat(registration.id);          // remove it again

registerFormat({ id: 'json5', test: ['.json5'], reader: otherReader }); // replace by id

const registry = useFormatRegistry();       // the process-global FormatRegistry
registry.entries();                         // [{ id, test, builtIn }, ...] in match order
registry.has('json');                       // true (built-in)
registry.reset();                           // drop all user rules + cached instances

The global registry belongs to the application. Libraries that need custom formats should create their own isolated instance instead of mutating the singleton: new FormatRegistry({ rules: [...] }).

Value codec

For env-var / key-value style contexts, the lenient value codec the .conf format uses per value is exported directly — handy for building lenient custom formats (or serializing to stores that hold bare strings):

import { deserializeValue, serializeValue } from 'locter';

serializeValue(true);          // 'true'
serializeValue(/^a+$/i);       // '/^a+$/i'
serializeValue({ a: 1 });      // '{"a":1}'

deserializeValue('123');       // 123
deserializeValue('{ broken');  // '{ broken' — falls back to the raw string, never throws

Errors

read / write (and every built-in format) throw a typed subclass of LocterError, so callers can distinguish failure modes without substring matching on .message:

| Class | When it's thrown | |--------------------------------|---------------------------------------------------------| | NotFoundError | File or module does not exist (ENOENT, MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND) | | LoadError | A reader threw — parse error, runtime/eval error, etc. | | WriteError | A write failed — serialization error, filesystem error, read-only format, corrupt existing YAML target | | UnknownExtensionError | No rule matched the file's extension | | LocterError | Base class; matches all of the above |

Each error exposes the offending path and preserves the underlying error on cause:

import { read, NotFoundError, LoadError } from 'locter';

try {
    await read('config.json');
} catch (err) {
    if (err instanceof NotFoundError) {
        console.error(`Config file not found: ${err.path}`);
    } else if (err instanceof LoadError) {
        console.error(`Failed to parse config: ${err.cause}`);
    } else {
        throw err;
    }
}

Note that on the write side ENOENT stays a WriteError — a missing parent directory is a write failure, not a lookup miss (and write creates missing parents anyway).

LocterError extends BaseError from @ebec/core, so each instance also carries a code (auto-derived from the class name unless overridden — e.g. 'ENOENT' for NotFoundErrors thrown by the JSON reader) and a toJSON() for structured logging.

Each error class also publishes its Symbol.for(...) marker (LOCTER_ERROR_MARKER, LOCTER_NOT_FOUND_ERROR_MARKER, …), which makes instanceof work across realms — useful when consumers end up with duplicate copies of locter in their dependency tree.

Runtime environments

Locter ships with runtime detection helpers:

  • isJestRuntimeEnvironment() — true when running under Jest. Built-in: the ModuleReader falls back to require() under Jest to avoid a known segmentation fault (nodejs/node#35889).
  • isVitestRuntimeEnvironment() — true when running under Vitest (checks process.env.VITEST === 'true').
  • isTsNodeRuntimeEnvironment() — true when a ts-node register instance is active. Built-in: the ModuleReader's async path falls back to its sync loading under ts-node.
  • isTsxRuntimeEnvironment() — true when the process was started through tsx (tsx script.ts, node --import tsx, or a resolved .../node_modules/tsx/... loader path). Useful for callers that need to know whether TypeScript sources can be executed directly.

Using locter with Vitest

Locter's built-in await import(id) runs inside node_modules/locter and therefore bypasses Vitest's vite-node module graph by default. For most use cases this is fine, but it produces duplicate module instances when test code statically imports a module and locter dynamically loads the same module (or a transitive dependency of it). This breaks class identity for libraries that rely on it (e.g. TypeORM entity metadata).

Use setModuleReader to inject an import call from user space so Vitest can rewrite it:

// vitest setup file
import { setModuleReader } from 'locter';

setModuleReader({
    load: (id) => import(id),
});

setModuleReader returns a restore function that re-applies the previous configuration — useful for scoped overrides:

const restore = setModuleReader({ load: (id) => import(id) });
// ...
restore();

Alternatively, the same effect can be achieved by inlining locter in vitest.config.ts:

export default defineConfig({
    test: {
        server: {
            deps: {
                inline: [/locter/],
            },
        },
    },
});

Migration

Migrating from 3.x

The 4.0 release renames the loader subsystem around the format concept (read + write), reworks registration and dispatch, and normalizes results. The list below is exhaustive — no other public APIs changed.

Renames (clean break, no deprecated aliases):

| 3.x | 4.x | |--------------------------------------|--------------------------------------------| | load / loadSync | read / readSync (raw: plain data values / module records) or readAsModule / readAsModuleSync (everything as a module record) | | registerLoader / unregisterLoader| registerFormat / unregisterFormat | | LoaderManager | FormatRegistry | | useLoader | useFormatRegistry | | Loader type (execute/executeSync) | IReader (read/readSync) + IWriter (write/writeSync) | | JSONLoader / YAMLLoader / ConfLoader | JSONReader+JSONWriter / YAMLReader+YAMLWriter / ConfReader+ConfWriter | | ModuleLoader | ModuleReader | | setModuleLoader | setModuleReader | | loadPackageField / loadPackageFieldSync | readPackageField / readPackageFieldSync | | LocterLoadError / LocterNotFoundError / LocterUnknownExtensionError | LoadError / NotFoundError / UnknownExtensionError — the LocterError base class keeps its name |

Behavioral changes:

  // 1. User rules now override built-ins. Registering an extension a
  //    built-in claims (e.g. `.json`) previously had NO effect — the
  //    built-in rule always matched first. It now takes precedence.
  registerFormat({ test: ['.json'], reader: myJson5Reader }); // 4.x: overrides the built-in

  // 2. Plugin-string loaders are removed, and rules use the object form
  //    with independent reader/writer slots. Import the implementation
  //    yourself and register it (optionally lazily, via a factory).
- registerLoader({ test: ['.toml'], loader: 'toml' }); // resolved to @locter/toml, sync require()
+ import { TomlReader, TomlWriter } from '@locter/toml';
+ registerFormat({ test: ['.toml'], reader: () => new TomlReader(), writer: () => new TomlWriter() });

  // 3. The registry no longer implements the loader interface: execute() /
  //    executeSync() become read() / readSync() (accepting LocatorInfo |
  //    string), and findLoader() / resolve() are replaced by
  //    findReader() / findWriter() / builtInReader() / builtInWriter().
- const manager = new LoaderManager();
- await manager.execute('file.json');
+ const registry = new FormatRegistry();
+ await registry.read('file.json');       // mirrors the global read() helper
+ registry.findReader('file.json');       // IReader | undefined
+ registry.builtInReader('module');       // statically typed as ModuleReader

  // 4. Two verbs replace 3.x load(): read() is RAW (the plain parsed
  //    value for data — 3.x data loads map to read() nearly 1:1 — and
  //    the normalized record for modules), while readAsModule() returns
  //    a frozen, null-prototype module record for EVERY format (see
  //    "Module records" above).
  const value = await read('package.json');
  value.version;                         // the plain parsed JSON

  const pkg = await readAsModule('package.json');
  pkg.version;                           // top-level keys as named exports
+ pkg.default;                           // the full parsed value
+ // The record is frozen and null-prototype: code that mutates the
+ // result, relies on Object.prototype, or enumerates/serializes it
+ // must use pkg.default (or read()) instead.

  // 5. ModuleReader.read() / readSync() return the raw module value;
  //    normalization happens exactly once, at the registry boundary. Only
  //    relevant when invoking the reader class directly.
- await new ModuleLoader().execute('file.ts');  // normalized record
+ await new ModuleReader().read('file.ts');     // raw module value
+ await registry.read('file.ts');               // normalized record

Removed from the public surface (internal plumbing that leaked through the old export * barrels; the package now exports a curated, snapshot-tested list):

  • Locator internals: buildLocatorPatterns, buildLocatorOptions, pathToLocatorInfo, buildFilePathWithoutExtension (deleted — it had no callers). buildFilePath and isLocatorInfo stay public.
  • Record internals: isESModule, toModuleRecord, createModuleRecord. Use readAsModule() (which normalizes) / read() (raw) and the public getModuleExport / isModuleRecord.
  • Generic helpers: isObject, isSafeObjectKey, hasOwnProperty, hasStringProperty, toArray, isFilePath, isTypeScriptError. The domain helpers stay public: getFileNameExtension / removeFileNameExtension (see File-name helpers) and the runtime detection helpers (isJestRuntimeEnvironment, isVitestRuntimeEnvironment, isTsNodeRuntimeEnvironment, isTsxRuntimeEnvironment).

Locator option precedence{ onlyFiles: true, onlyDirectories: true } used to resolve to neither restriction (everything matched); onlyDirectories: true now wins. { onlyFiles: false } used to mean directories-only; it now means "no restriction" (files and directories).

New in 4.0:

  • write / writeSync with per-format writers (JSONWriter, YAMLWriter, ConfWriter), including comment-preserving YAML write-back — see Writer.
  • writePackageField / writePackageFieldSync.
  • WriteError + wrapWriteError.
  • isModuleRecord — brand-based detection of records produced by read().
  • The ports are exported as IReader / IWriter, alongside Rule, ReaderFactory, WriterFactory — so custom formats can be typed against the package.
  • The extensions handled by the module reader are exported as MODULE_FILE_EXTENSIONS.
  • readPackageField / readPackageFieldSync read package.json via the built-in JSON reader directly: user-registered .json rules no longer intercept the read, and synthetic record keys (default, __esModule) no longer resolve as package fields.

Migrating from 2.x

The 3.0 release reshapes a few public surfaces. The list below is exhaustive — no other public APIs changed. (Symbols are shown under their 3.x names; see Migrating from 3.x for the current ones.)

- import { load, locate } from 'locter';
+ import { load, locate, LocterNotFoundError, LocterLoadError } from 'locter';

  // 1. LocatorOptions.path → cwd
- await locate('config.json', { path: process.cwd() });
+ await locate('config.json', { cwd: process.cwd() });

  // 2. LocatorInfo.path now holds the FULL file path (it was the
  //    containing directory in 2.x). buildFilePath(info) still works in
  //    both versions but is now a near-identity for LocatorInfo inputs.
- const filePath = buildFilePath(info);  // 2.x: composed from .path + .name + .extension
+ const filePath = info.path;            // 3.x: just the path field
+ const dir      = info.directory;       // 3.x: containing directory (the old `info.path`)

  // 3. load() / loadSync() (and every built-in loader) now throw
  //    LocterError subclasses instead of generic Error / ebec BaseError.
  try {
      await load('config.json');
- } catch (err) {
-     if (err.message.includes('ENOENT')) { /* … */ }
+ } catch (err) {
+     if (err instanceof LocterNotFoundError) { /* … */ }
+     else if (err instanceof LocterLoadError) { /* parse error: err.cause */ }
+     else throw err;
  }

Additionally:

  • The ebec runtime dependency is replaced by @ebec/core. If you used to import BaseError from ebec, switch to @ebec/core.
  • The package is now ESM-only (no require('locter')). This change shipped in the 2.x→3.x toolchain modernization commit; mentioned here for completeness.

License

Made with 💚

Published under MIT License.