Common utilities
A collection of common utilities, classes, and helpers.
Installation
- Yarn
- pnpm
- npm
yarn add @boost/common
pnpm add @boost/common
npm install @boost/common
Helpers and serializers
Boost provides many functions for common scenarios and patterns, like
isObject
for verifying a value is an object, or
toArray
for converting a value to an array.
View the API for a full list of functions with examples.
JSON
Powered by the JSON5 package, the
json
serializer can be used to parse and stringify JSON data.
import { json } from '@boost/common';
json.parse(data);
json.stringify(data);
YAML
Powered by the YAML package, the
yaml
serializer can be used to parse and stringify YAML data.
import { yaml } from '@boost/common';
yaml.parse(data);
yaml.stringify(data);
Class contracts
A Contract
is an abstract class that implements the
Optionable
interface, which provides an options object layer,
and is meant to be inherited from as a super class. All classes that extend Contract
accept an options object through the constructor, which is validated and built using
optimal.
To start, extend Contract
with a generic interface that represents the shape of the
options object. Next, implement the abstract
Contract#blueprint()
method, which is passed
optimal schemas as an argument, and
must return an
optimal blueprint that
matches the generic interface.
import { Contract, Blueprint, Schemas } from '@boost/common';
export interface AdapterOptions {
name?: string;
priority?: number;
}
export default class Adapter extends Contract<AdapterOptions> {
blueprint({ number, string }: Schemas): Blueprint<AdapterOptions> {
return {
name: string().notEmpty(),
priority: number().gte(0),
};
}
}
When the class is instantiated, the provided values will be checked and validated using the
blueprint. If invalid, an error will be thrown. Furthermore, the
Contract#options
property is readonly
, and will error when
mutated.
const adapter = new Adapter({
name: 'Boost',
});
const { name } = adapter.options; // => Boost
Required options
By default, the options argument in the constructor is optional, and if your interface has a required property, it will not be bubbled up in TypeScript. To support this, the constructor will need to be overridden so that the argument can be marked as non-optional.
export interface AdapterOptions {
name: string;
priority?: number;
}
export default class Adapter extends Contract<AdapterOptions> {
constructor(options: AdapterOptions) {
super(options);
}
// ...
}
Project management
The Project
class provides workspace and package metadata for a project. A project is
denoted by a root package.json
file and abides the npm and Node.js module pattern. To begin,
import and instantiate the Project
class with a path to the project's root.
import { Project } from '@boost/common';
const project = new Project();
Root defaults to
process.cwd()
if not provided.
Workspaces
The primary feature of this class is to extract metadata about a project's workspaces. Workspaces are used to support multi-package architectures known as monorepos, typically through Yarn, PNPM, or Lerna. In Boost, our implementation of workspaces aligns with:
- Project - Typically a repository with a root
package.json
. Can either be a collection of packages, or a package itself. - Package - A folder with a
package.json
file that represents an npm package. Contains source and test files specific to the package. - Workspace - A folder that houses one or many packages.
Package graph
The PackageGraph
class can be used to generate a dependency graph for a list of
packages, based on their defined dependencies
and peerDependencies
. To begin, import and
instantiate the class, which accepts a list of optional package.json
objects as the 1st argument.
import { PackageGraph } from '@boost/common';
const graph = new PackageGraph([
{
name: '@boost/common',
version: '1.2.3',
},
{
name: '@boost/cli',
version: '1.0.0',
dependencies: {
'@boost/common': '^1.0.0',
},
},
]);
Once all packages have been defined, we can resolve our graph into 1 of 3 formats, using the following methods. View the API for more information on these methods.
const batch = graph.resolveBatchList();
const list = graph.resolveList();
const tree = graph.resolveTree();
Will only resolve and return packages that have been defined. Will not return non-defined packages found in
dependencies
andpeerDependencies
.
Path management
The Path
class is an immutable abstraction around file/module paths and the Node.js fs
and path
modules. It aims to solve cross platform and operating system related issues in a
straight forward way. To begin, import and instantiate the Path
class, with either a
single path, or a list of path parts that will be joined.
import { Path } from '@boost/common';
const absPath = new Path('/root/some/path');
const relPath = new Path('some/path', '../move/around', 'again');
By default, the class operates on the defined path parts as-is. If you would prefer to operate
against real or resolved paths, use the Path#realPath()
and
Path#resolve()
methods respectively. The current path is
resolved against the defined current
working directory (process.cwd()
).
path.path(); // Possibly inaccurate
path.resolve().path(); // Resolved accurately
With that being said, the class supports many convenient methods. View the API for a full list.
Static factories
The static Path.create()
and
Path.resolve()
methods can be used to factory a Path
instance from a string or an existing instance. Especially useful when used in combination with the
PortablePath
type.
Path.create('some/file/path'); // Path
Resolving lookup paths
The PathResolver
class can be used to find a real path amongst a list of possible
lookups. A lookup is either a file system path or a Node.js module. If a path is found, an absolute
resolved Path
instance is returned, otherwise an error is thrown.
A perfect scenario for this mechanism would be finding a valid configuration file, which we'll demonstrate below. Import and instantiate the class to begin.
import { PathResolver } from '@boost/common';
const resolver = new PathResolver();
// With a custom module resolver (can be async!)
const resolver = new PathResolver(customResolver);
To add a file system lookup, use the
PathResolver#lookupFilePath()
method, which
requires a path and an optional current working directory (defaults to process.cwd()
).
// Look in current directory
resolver
.lookupFilePath('tool.config.js')
.lookupFilePath('tool.config.json')
.lookupFilePath('tool.config.yaml');
// Look in a folder
resolver.lookupFilePath('configs/tool.js');
// Look in user's home directory
resolver.lookupFilePath('tool.config.js', os.homedir());
And to add a Node.js module lookup, use the
PathResolver#lookupNodeModule()
method, which
accepts a module name or path.
// Look in module (assuming index export)
resolver.lookupNodeModule('tool-config-module');
// Look in module with sub-path
resolver.lookupNodeModule('tool-config-module/lib/configs/tool.js');
Once all the lookup paths have been defined, the
PathResolver#resolve()
method will iterate through them
in order until one is found. If a file system path, fs.existsSync()
will be used to check for
existence, while the resolve
npm package will be used for
Node.js modules. If found, a result object will be returned with the resolved Path
and
original lookup parts.
const { originalSource, resolvedPath, type } = await resolver.resolve();
If you'd prefer to only have the resolved path returned, the
PathResolver#resolvePath()
method can be used
instead.
const resolvedPath = await resolver.resolvePath();