Extending the VFS
The @neodx/vfs
provides a powerful plugin system that allows you to extend the VFS functionality with any custom logic.
Even the built-in features like glob or prettier are implemented as plugins and could be dropped (see createHeadlessVfs). createVfs
is literally implemented as a composition of plugins:
function createVfs(path, params) {
return createHeadlessVfs(path, params).pipe(
json(),
scan(),
glob(),
eslint(/* ... */),
prettier(/* ... */),
packageJson()
);
}
Let's overview the plugin system and see how to create a custom plugin.
Using plugins
The concept of plugins is very simple: vfs
provides a .pipe
method that accepts a list of plugins and returns a new vfs
instance with the plugin applied.
import { createHeadlessVfs, json, scan } from '@neodx/vfs';
const vfs = createHeadlessVfs('path/to/dir');
const vfsWithJson = vfs.pipe(json());
const vfsWithJsonAndScanV1 = vfs.pipe(json(), scan());
// same as
const vfsWithJsonAndScanV2 = vfsWithJson.pipe(scan());
Plugins should provide types for their extensions, so you will always know what methods are available on the vfs
instance.
Under the hood types are a bit more complex, but in the result you will see clean and safe types, for example:
export async function readManifests(vfs = defaultVfs) {
const manifests = await vfs.glob('**/manifest.json');
const entries = await Promise.all(
manifests.map(async path => [path, await vfs.readJson(path)] as const)
);
return Object.entries(entries);
}
const defaultVfs = createHeadlessVfs(process.cwd()).pipe(glob(), json());
// ^? PublicVfs<BaseVfs & GlobPluginApi & JsonPluginApi>
// OK
await readManifests(createVfs('/'));
// OK
await readManifests(createHeadlessVfs('/').pipe(json(), glob(), eslint()));
// ERROR!
await readManifests(createHeadlessVfs('/').pipe(json()));
Writing plugins
Plugins are just functions that accept vfs
instance and return a new vfs
instance with additional methods (mutable style if prefer).
Plugins should be created with createVfsPlugin
factory.
A minimal plugin looks like this:
import { createVfsPlugin, createHeadlessVfs } from '@neodx/vfs';
type MyPluginExtensions = {
myMethod(): string;
};
const plugin = createVfsPlugin<MyPluginExtensions>('my-plugin', vfs => {
vfs.myMethod = () => 'Hello, world!';
return vfs;
});
const vfs = createHeadlessVfs('/').pipe(plugin);
vfs.myMethod(); // Hello, world!
Plugin also accepts a private API that could be used to subscribe to vfs
events and access the context object.
Let's create a plugin that will install npm dependencies on package.json
change.
type NpmPluginApi = {
install(): Promise<void>;
};
const npm = createVfsPlugin<NpmPluginApi>('npm', (vfs, { context, beforeApply }) => {
const log = context.log.child('npm');
vfs.install = async () => {
log.info('Installing dependencies...');
await exec('npm install', { cwd: context.path });
};
beforeApply(async changes => {
if (!changes.some(change => change.path.endsWith('package.json'))) return;
log.info('package.json changed');
await vfs.install();
});
return vfs;
});
Now we can use it everywhere:
const vfs = createHeadlessVfs('/').pipe(json(), npm);
await vfs.install(); // install dependencies
await vfs.updateJson('package.json', json => {
json.dependencies = { ...json.dependencies, 'my-package': '1.0.0' };
});
await vfs.apply(); // install will be called automatically, because package.json changed
Built-in plugins
We have a few built-in plugins that you can use out of the box: