Skip to main content

Common utilities

API BackendTooling

A collection of common utilities, classes, and helpers.

Installation

yarn add @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 Contractis 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 and peerDependencies.

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();