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
. TheBlueprint
andSchemas
(formerlyPredicates
) 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 everyPath
instantiation.
While this change was beneficial for file system paths, it had the unfortunate side-effect of
breaking all Node.js module Path
s, 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.