Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/natemoo-re/proload

Searches for and loads your tool's JavaScript configuration files with full support for CJS, ESM, TypeScript and more.
https://github.com/natemoo-re/proload

cjs config config-loader configuration esm javascript load node typescript

Last synced: about 1 month ago
JSON representation

Searches for and loads your tool's JavaScript configuration files with full support for CJS, ESM, TypeScript and more.

Awesome Lists containing this project

README

        

# `@proload/core`

Proload searches for and loads your tool's JavaScript configuration files. Users have complex expectations when it comes to configuration files—the goal of Proload is to offer a single, straightforward and extensible API for loading them.

```js
import load from '@proload/core';

await load('namespace');
```

> `@proload/core` can be used in `[email protected]` and up. It relies on Node's native ESM semantics.

## Motivation

Configuration files are really difficult to get right. Tool authors tend to think, "Easy solve! I'll just have everyone use one `namespace.config.js`!" In most cases that should work, but since `[email protected]`, plain `.js` files can be written in either ESM or CJS—both formats are officially supported and can be configured on a per-project basis. Additionally, `node` is able to load any file using a `.cjs` or `.mjs` extension, not just `.js`.

Many popular libraries get these semantics wrong, but maintaining and testing this resolution logic in library code can be a huge maintanence burden. As a library author, you don't need to know (or care) which module format your users choose—you just need to load the contents of the config file. `@proload/core` is a well-tested solution that gets these semantics right, so you can focus on more important things.

> You probably have TypeScript users, too! They would definitely appreciate being able to write a `.ts` config file. `@proload/core` uses a plugin system to load non-JavaScript files. See [Plugins](https://github.com/natemoo-re/proload/tree/main/packages/core#plugins) or [`@proload/plugin-typescript`](https://github.com/natemoo-re/proload/tree/main/packages/core#typescript) specifically.

## Resolution

Out of the box, `@proload/core` searches up the directory tree for the following files:

- a `[namespace].config.js`, `[namespace].config.cjs`, or `[namespace].config.mjs` file
- any of the `js/cjs/mjs` files inside of `config/` directory
- a `package.json` file with a top-level `[namespace]` key

Here's an overview of all the files supported by default for a tool named `donut`.

```
await load('donut');

.
├── donut.config.js // Either ESM or CJS supported
├── donut.config.cjs
├── donut.config.mjs
├── config/ // Great for organizing many configs
│ ├── donut.config.js
│ ├── donut.config.cjs
│ └── donut.config.mjs
└── package.json // with top-level "donut" property
```

## `resolve`

`resolve` is an additional named export of `@proload/core`. It is an `async` function that resolves **but does not load** a configuration file.

- `namespace` is the name of your tool. As an example, `donut` would search for `donut.config.[ext]`.
- `opts` configure the behavior of `load`. See [Options](https://github.com/natemoo-re/proload/tree/main/packages/core#options).

```ts
resolve(namespace: string, opts?: ResolveOptions);
```

## `load`

The `default` export of `@proload/core` is an `async` function to load a configuration file.

- `namespace` is the name of your tool. As an example, `donut` would search for `donut.config.[ext]`.
- `opts` configure the behavior of `load`. See [Options](https://github.com/natemoo-re/proload/tree/main/packages/core#options).

```ts
load(namespace: string, opts?: LoadOptions);
```

## Options

### cwd
`load` searches up the directory tree, beginning from this loaction. Defaults to `process.cwd()`.

```js
import load from '@proload/core';
await load('namespace', { cwd: '/path/to/user/project' });
```

### filePath
If you already have the exact (absolute or relative) `filePath` of your user's config file, set the `filePath` option to disable Proload's search algorithm.

```js
import load from '@proload/core';
await load('namespace', { cwd: '/path/to/user/project', filePath: './custom-user-config.js' });
```

### mustExist
`mustExist` controls whether a configuration _must_ be found. Defaults to `true`—Proload will throw an error when a configuration is not found. To customize error handling, you may check the shape of the thrown error.

Setting this option to `false` allows a return value of `undefined` when a configuration is not found.

```js
import load, { ProloadError } from '@proload/core';

try {
await load('namespace', { mustExist: true });
} catch (err) {
// Proload couldn't resolve a configuration, log a custom contextual error
if (err instanceof ProloadError && err.code === 'ERR_PROLOAD_NOT_FOUND') {
console.error(`See the "namespace" docs for configuration info`);
}
throw err;
}
```

### context

Users may want to dynamically generate a different configuration based on some contextual information passed from your tool. Any `{ context }` passed to the `load` function will be forwarded to configuration "factory" functions.

```js
// Library code
import load from '@proload/core';
await load('namespace', { context: { isDev: true }});

// namespace.config.js
export default ({ isDev }) => {
return { featureFlag: isDev }
}
```

### accept
If you need complete control over which file to load, the `accept` handler can customize resolution behavior. A return value of `true` marks a file to be loaded, any other return values (even truthy ones) is ignored.

See the [`accept`](https://github.com/natemoo-re/proload/blob/34413acf87d98d3ef310ce2873103455cb2eb379/packages/core/lib/index.d.ts#L11) interface.

> Note that [Plugins](https://github.com/natemoo-re/proload/tree/main/packages/core#plugins) are able to modify similar behavior. To load non-JavaScript files, you should use a plugin instead of `accept`.

```js
import load from '@proload/core';

await load('donut', {
accept(fileName) {
// Support alternative spelling for any European friends
return fileName.startsWith('doughnut.config');
}
})
```

The following example uses `@proload/plugin-typescript` to add support for loading `.ts` files and an `accept` handler to require all config files to use the `.ts` extension.
```js
import load from '@proload/core';
import typescript from '@proload/plugin-typescript';

load.use([typescript]);
await load('namespace', {
accept(fileName) {
// Only accept `.ts` config files
return fileName.endsWith('.ts');
}
})
```

### merge

To customize `extends` behavior, you may pass a custom `merge` function to the `load` function. By default, [`deepmerge`](https://github.com/TehShrike/deepmerge) is used.

```js
// Library code
import load from '@proload/core';

const shallowMerge = (a, b) => ({ ...a, ...b })
await load('namespace', { merge: shallowMerge });

// namespace.config.js
export default {
extends: ['./a.js', './b.js']
}

// a.js
export default {
a: true
}

// b.js
export default {
b: true
}

// result
{
a: true,
b: true
}
```

## Automatic `extends`

Tools like `typescript` and `babel` have popularized the ability to share configuration presets through a top-level `extends` clause. `extends` also allows you to share a local base configuration with other packages, which is extremely useful for monorepo users.

Custom implementation of this behavior can be difficult, so `@proload/core` automatically recognizes top-level `extends` clauses (`string[]`) for you. It recursively resolves and merges all dependent configurations.

```js
// namespace.config.js
export default {
extends: ['@namespace/preset', '../namespace.base.config.js']
}
```

### Extending local configuration files
In many cases, particularly in monorepos, it's useful to have a base configuration file and use `extends` in any sub-packages to inherit the base configuration. `@proload/core` resolves paths in `extends` relative to the configuration file itself.

```
.
├── namespace.base.config.js
└── packages/
├── package-a/
│ └── namespace.config.js
└── package-b/
└── namespace.config.js
```

### Extending configuration presets
`@proload/core` uses the same strategy to resolve a configuration file from project `dependencies` as it does for user configurations. When publishing a configuration preset, use the same file naming strategy as you would for local configuration.

```
.
├── node_modules/
│ └── @namespace/
│ └── preset-env/
│ ├── package.json
│ └── namespace.config.js
├── package.json
└── namespace.config.js
```

Assuming `@namespace/preset-env` is a project dependency, the top-level `namespace.config.js` file can use `extends` to reference the dependency.

```js
export default {
extends: ['@namespace/preset-env']
}
```

## Plugins

In order to support as many use cases as possible, `@proload/core` uses a plugin system. Plugins build on each other and are designed to be combined. For example, to support a `namespacerc.json` file, you could use both `@proload/plugin-json` and `@proload/plugin-rc`.

```js
import load from '@proload/core';
import rc from '@proload/plugin-rc';
import json from '@proload/plugin-json';

load.use([rc, json]);
await load('namespace');
```

### TypeScript
In order to load a `[namespace].config.ts` file, use `@proload/plugin-typescript`.

```js
import load from '@proload/core';
import typescript from '@proload/plugin-typescript';

load.use([typescript]);
await load('namespace');
```

### JSON
In order to load a `[namespace].config.json` file, use `@proload/plugin-json`.

```js
import load from '@proload/core';
import json from '@proload/plugin-json';

load.use([json]);
await load('namespace');
```

### YAML
In order to load a `[namespace].config.yaml` or `[namespace].config.yml` file, use `@proload/plugin-yaml`.

```js
import load from '@proload/core';
import yaml from '@proload/plugin-yaml';

load.use([yaml]);
await load('namespace');
```

### RC files
In order to load a `[namespace]rc` file with any extension, use `@proload/plugin-rc`.

```js
import load from '@proload/core';
import rc from '@proload/plugin-rc';

load.use([rc]);
await load('namespace');
```

### All Plugins
For illustrative purposes (please don't do this), combining all of these plugins would support the following resolution logic:

```
.
├── namespace.config.js
├── namespace.config.cjs
├── namespace.config.mjs
├── namespace.config.ts
├── namespace.config.json
├── namespace.config.yaml
├── namespace.config.yml
├── namespacerc.js
├── namespacerc.cjs
├── namespacerc.mjs
├── namespacerc.ts
├── namespacerc.json
├── namespacerc.yaml
├── namespacerc.yml
├── config/
│ ├── namespace.config.js
│ ├── namespace.config.cjs
│ ├── namespace.config.mjs
│ ├── namespace.config.ts
│ ├── namespace.config.json
│ ├── namespace.config.yaml
│ ├── namespace.config.yml
│ ├── namespacerc.js
│ ├── namespacerc.cjs
│ ├── namespacerc.mjs
│ ├── namespacerc.ts
│ ├── namespacerc.json
│ ├── namespacerc.yaml
│ └── namespacerc.yml
└── package.json
```

## Credits

Proload is heavily inspired by tools like [`cosmiconfig`](https://github.com/davidtheclark/cosmiconfig#readme) and [`rc`](https://github.com/dominictarr/rc).

Proload would not be possible without [@lukeed](https://github.com/lukeed)'s amazing work on [`escalade`](https://github.com/lukeed/escalade) and [`uvu`](https://github.com/lukeed/uvu).