- Notifications
You must be signed in to change notification settings - Fork6
natemoo-re/proload
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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.
importloadfrom'@proload/core';awaitload('namespace');
@proload/core
can be used innode@12.20.1
and up. It relies on Node's native ESM semantics.
Configuration files are really difficult to get right. Tool authors tend to think, "Easy solve! I'll just have everyone use onenamespace.config.js
!" In most cases that should work, but sincenode@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
.ts
config file.@proload/core
uses a plugin system to load non-JavaScript files. SeePlugins or@proload/plugin-typescript
specifically.
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 ofconfig/
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 nameddonut
.
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
is an additional named export of@proload/core
. It is anasync
function that resolvesbut does not load a configuration file.
namespace
is the name of your tool. As an example,donut
would search fordonut.config.[ext]
.opts
configure the behavior ofload
. SeeOptions.
resolve(namespace: string,opts?:ResolveOptions);
Thedefault
export of@proload/core
is anasync
function to load a configuration file.
namespace
is the name of your tool. As an example,donut
would search fordonut.config.[ext]
.opts
configure the behavior ofload
. SeeOptions.
load(namespace: string,opts?:LoadOptions);
load
searches up the directory tree, beginning from this loaction. Defaults toprocess.cwd()
.
importloadfrom'@proload/core';awaitload('namespace',{cwd:'/path/to/user/project'});
If you already have the exact (absolute or relative)filePath
of your user's config file, set thefilePath
option to disable Proload's search algorithm.
importloadfrom'@proload/core';awaitload('namespace',{cwd:'/path/to/user/project',filePath:'./custom-user-config.js'});
mustExist
controls whether a configurationmust be found. Defaults totrue
—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 tofalse
allows a return value ofundefined
when a configuration is not found.
importload,{ProloadError}from'@proload/core';try{awaitload('namespace',{mustExist:true});}catch(err){// Proload couldn't resolve a configuration, log a custom contextual errorif(errinstanceofProloadError&&err.code==='ERR_PROLOAD_NOT_FOUND'){console.error(`See the "namespace" docs for configuration info`);}throwerr;}
Users may want to dynamically generate a different configuration based on some contextual information passed from your tool. Any{ context }
passed to theload
function will be forwarded to configuration "factory" functions.
// Library codeimportloadfrom'@proload/core';awaitload('namespace',{context:{isDev:true}});// namespace.config.jsexportdefault({ isDev})=>{return{featureFlag:isDev}}
If you need complete control over which file to load, theaccept
handler can customize resolution behavior. A return value oftrue
marks a file to be loaded, any other return values (even truthy ones) is ignored.
See theaccept
interface.
Note thatPlugins are able to modify similar behavior. To load non-JavaScript files, you should use a plugin instead of
accept
.
importloadfrom'@proload/core';awaitload('donut',{accept(fileName){// Support alternative spelling for any European friendsreturnfileName.startsWith('doughnut.config');}})
The following example uses@proload/plugin-typescript
to add support for loading.ts
files and anaccept
handler to require all config files to use the.ts
extension.
importloadfrom'@proload/core';importtypescriptfrom'@proload/plugin-typescript';load.use([typescript]);awaitload('namespace',{accept(fileName){// Only accept `.ts` config filesreturnfileName.endsWith('.ts');}})
To customizeextends
behavior, you may pass a custommerge
function to theload
function. By default,deepmerge
is used.
// Library codeimportloadfrom'@proload/core';constshallowMerge=(a,b)=>({ ...a, ...b})awaitload('namespace',{merge:shallowMerge});// namespace.config.jsexportdefault{extends:['./a.js','./b.js']}// a.jsexportdefault{a:true}// b.jsexportdefault{b:true}// result{a:true,b:true}
Tools liketypescript
andbabel
have popularized the ability to share configuration presets through a top-levelextends
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-levelextends
clauses (string[]
) for you. It recursively resolves and merges all dependent configurations.
// namespace.config.jsexportdefault{extends:['@namespace/preset','../namespace.base.config.js']}
In many cases, particularly in monorepos, it's useful to have a base configuration file and useextends
in any sub-packages to inherit the base configuration.@proload/core
resolves paths inextends
relative to the configuration file itself.
.├── namespace.base.config.js└── packages/ ├── package-a/ │ └── namespace.config.js └── package-b/ └── namespace.config.js
@proload/core
uses the same strategy to resolve a configuration file from projectdependencies
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-levelnamespace.config.js
file can useextends
to reference the dependency.
exportdefault{extends:['@namespace/preset-env']}
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 anamespacerc.json
file, you could use both@proload/plugin-json
and@proload/plugin-rc
.
importloadfrom'@proload/core';importrcfrom'@proload/plugin-rc';importjsonfrom'@proload/plugin-json';load.use([rc,json]);awaitload('namespace');
In order to load a[namespace].config.ts
file, use@proload/plugin-typescript
.
importloadfrom'@proload/core';importtypescriptfrom'@proload/plugin-typescript';load.use([typescript]);awaitload('namespace');
In order to load a[namespace].config.json
file, use@proload/plugin-json
.
importloadfrom'@proload/core';importjsonfrom'@proload/plugin-json';load.use([json]);awaitload('namespace');
In order to load a[namespace].config.yaml
or[namespace].config.yml
file, use@proload/plugin-yaml
.
importloadfrom'@proload/core';importyamlfrom'@proload/plugin-yaml';load.use([yaml]);awaitload('namespace');
In order to load a[namespace]rc
file with any extension, use@proload/plugin-rc
.
importloadfrom'@proload/core';importrcfrom'@proload/plugin-rc';load.use([rc]);awaitload('namespace');
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
Proload is heavily inspired by tools likecosmiconfig
andrc
.
Proload would not be possible without@lukeed's amazing work onescalade
anduvu
.
About
Searches for and loads your tool's JavaScript configuration files with full support for CJS, ESM, TypeScript and more.