- Implemented astro-i18next for multi-language support, including English, Dutch, and Italian. - Configured default locale and language fallback settings. - Defined routes for localized content in the configuration. - Updated package.json and package-lock.json to include new dependencies for i18next and related plugins.
@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.
import load from '@proload/core';
await load('namespace');
@proload/corecan be used innode@12.20.1and 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 node@12.17.0, 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
.tsconfig file.@proload/coreuses a plugin system to load non-JavaScript files. See Plugins or@proload/plugin-typescriptspecifically.
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.mjsfile - any of the
js/cjs/mjsfiles inside ofconfig/directory - a
package.jsonfile 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.
namespaceis the name of your tool. As an example,donutwould search fordonut.config.[ext].optsconfigure the behavior ofload. See Options.
resolve(namespace: string, opts?: ResolveOptions);
load
The default export of @proload/core is an async function to load a configuration file.
namespaceis the name of your tool. As an example,donutwould search fordonut.config.[ext].optsconfigure the behavior ofload. See Options.
load(namespace: string, opts?: LoadOptions);
Options
cwd
load searches up the directory tree, beginning from this loaction. Defaults to process.cwd().
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.
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.
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.
// 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 interface.
Note that Plugins are able to modify similar behavior. To load non-JavaScript files, you should use a plugin instead of
accept.
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.
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 is used.
// 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.
// 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.
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.
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.
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.
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.
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.
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 and rc.
Proload would not be possible without @lukeed's amazing work on escalade and uvu.