Skip to main content

3.0 migration

All packages

  • Requires TypeScript v4.4 or greater, as we rely on new syntax and features.
  • Dropped Node.js v10 support. Now requires v12.17 and above.
  • Dropped Internet Explorer 11 support (for packages with browser code). Now requires the latest versions of Edge, Chrome, or Firefox.

Updated optimal to v5

We utilize optimal for building and validating objects based on schemas, and this dependency has been updated to the next major, version 5 (view the official changelog). This major includes a complete rewrite, resulting in very different TypeScript types, and a slightly different consumable public API.

Now why is this important for Boost? Because it's used internally by Contract.blueprint(), which in turn is used by many other downstream packages. The biggest changes that need to be made are as follows:

  • Predicates (v4) are now know as schemas (v5).
  • Array, instance, object, and union schemas now define children types with a chainable of() method, instead of through the constructor.
  • All optimal schemas and types are now exported through a new module import @boost/common/optimal. The Blueprint and Schemas (formerly Predicates) types are still exported from the index for convenience.
// Before
import { Contract, Blueprint, Predicates } from '@boost/common';

export interface AdapterOptions {
name?: string;
env?: number;
}

export default class Adapter extends Contract<AdapterOptions> {
blueprint({ number, object }: Predicates): Blueprint<AdapterOptions> {
return {
name: string().notEmpty(),
env: object(string()),
};
}
}
// After
import { Contract, Blueprint, Schemas } from '@boost/common';
// OR
import { Contract } from '@boost/common';
import { Blueprint, Schemas } from '@boost/common/optimal';

export interface AdapterOptions {
name?: string;
env?: number;
}

export default class Adapter extends Contract<AdapterOptions> {
blueprint({ number, object }: Schemas): Blueprint<AdapterOptions> {
return {
name: string().notEmpty(),
env: object().of(string()),
};
}
}

@boost/common

Updated Path to be more performant and OS compliant

The Path class was designed as an abstraction around file system and Node.js module paths to provide seamless interoperability between different operating systems. While it achieved this, it did so by replacing all path separators with /, which works on both POSIX and Windows, but wasn't exactly correct for Windows. It also incurred a performance cost by constantly normalizing and replacing the path parts. We wanted to remedy this, so the following changes have been made:

  • Path separators are no longer forced to / and instead will be OS native: / on POSIX, \ on Windows.
  • Path normalization is now deferred until the Path#path() method is called, instead of being called on every Path instantiation.

While this change was beneficial for file system paths, it had the unfortunate side-effect of breaking all Node.js module Paths, as they must always use forward slashes /. To remedy this, a new ModulePath class has been added specifially for Node.js modules, and any reference to the Path type has been replaced with a new Pathable interface.

This is most noticeable with PathResolver, as it may return either a Path or ModulePath instance. To utilize methods on these instances, they must now be type cast.

const path = new PathResolver().resolvePath();

// When a file system path is found
(path as Path).isFile();

// When a node module is found
(path as ModulePath).hasScope();

Furthermore, this change was also detrimental to unit tests that run in both POSIX and Windows environments. Typically tests are written in POSIX styled paths (Boost was), which worked before on Windows, but will now fail since we no longer force the path separators to be the same. To remedy this, we now provide test utilities that are operating system aware, which can be imported from @boost/common/test.

// Before
expect(somePathInstance).toEqual(new Path('some/file/system/path'));
expect(somePathInstance.path()).toBe('some/file/system/path');
import { mockFilePath, normalizeSeparators } from '@boost/common/test';

// After
expect(somePathInstance).toEqual(mockFilePath('some/file/system/path'));
expect(somePathInstance.path()).toBe(normalizeSeparators('some/file/system/path'));

Updated PathResolver to be async

The PathResolver class and its resolve methods were synchronous by design (only because require.resolve() was). Since we're moving to an "ESM first and only" approach, we removed the require.resolve() compatibility and updated the resolver signature to be async. The resolver also accepts a "starting directory" in which to resolve from.

This change will support the future import.meta.resolve() API, but until that lands, the class will use the resolve npm package internally.

// Before
import { PathResolver } from '@boost/common';

const resolver = new PathResolver();
const path = resolver.resolve();
// After
import { PathResolver } from '@boost/common';

const resolver = new PathResolver();
const path = await resolver.resolve(__dirname);

If you require a synchronous API, unfortunately, you will need to implement that functionality yourself.

Updated Project to use path instances

All methods on Project that returned file system paths, will now return a Path instance instead of a string.

// Before
project.getWorkspacePackagePaths().map((path) => new Path(path, 'src/index.ts'));
// After
project.getWorkspacePackagePaths().map((path) => path.append('src/index.ts'));

The exception to this is Project#getWorkspaceGlobs(), which returns a list of strings, since these are glob patterns and not file paths (even though they look similar). Furthermore, glob patterns will always use forward slashes, regardless of operating system.

Removed parseFile function

The parseFile() function has been removed as it partially relied on requireModule(), which has also been removed (below).

However, similar functionality can be achieved with the json and yaml serializers, or simply native require().

// Before
import { parseFile } from '@boost/common';

const contents = parseFile('file.js');
const contents = parseFile('file.json');
const contents = parseFile('file.yaml');
// After
import { json, yaml } from '@boost/common';

const contents = require('file.js');
const contents = json.load('file.json');
const contents = yaml.load('file.yaml');

Removed requireModule and requireTypedModule functions

These functions have moved to the new @boost/module package.

// Before
import { requireModule } from '@boost/common';

const result = requireModule('foo');
// After
import { requireModule } from '@boost/module';

const result = requireModule('foo').default;

@boost/cli

React components and hooks must be imported from new module path

In an effort to reduce startup time and evaluation cost, all React components and hooks provided by Boost must now be imported from @boost/cli/react.

// Before
import { Help, Style } from '@boost/cli';
// After
import { Help, Style } from '@boost/cli/react';

Updated useRenderLoop argument to accept seconds

Previously, the useRenderLoop() hook required the FPS interval in milliseconds, which is a bit confusing. This has been changed to seconds, as we do the calculation internally.

// Before
import { useRenderLoop } from '@boost/cli/react';

useRenderLoop(30 / 1000); // 30 FPS
// After
import { useRenderLoop } from '@boost/cli/react';

useRenderLoop(30); // 30 FPS

Removed shorthand commands

Instead of using Command classes, Boost supported a feature known as shorthand commands, where an object of options, params, config, etc, could be passed during registration (below).

program.register<BuildOptions, BuildParams>(
'build',
{
description: 'Build a project',
options: {
minify: { description: 'Minify source files', type: 'boolean' },
},
params: [
{ description: 'Name of project', label: 'name', type: 'string' }
]
},
function build(this: TaskContext, options: BuildOptions, params: BuildParams, rest: string[]) => {
// ...
},
);

While this feature is nice for its simplicity, it was rather complicated to support internally as we had multiple layers of abstractions and proxies to get it working correctly. Shorthand commands were forcing us into a more complex implementation, so we opted to remove them entirely.