Skip to content

Create a plugin

A plugin is a bundle of files that ships with your novel. It usually includes a script that runs when the novel starts. That script can add new features, or extend and override the behavior of other plugins, including the bundled ones. Anything Inkweaver’s own plugins can do, yours can too.

The quickest way to start is to let the CLI create a plugin for you. Make a folder for your plugin, open a terminal there, and run:

Terminal
ink init-plugin
npm install
my-plugin/
├── .build/
├── .release/
├── assets/
│ └── plugin.json
├── src/
│ ├── commands/
│ │ ├── greet.ts
│ │ └── index.ts
│ ├── selectors/
│ │ └── index.ts
│ ├── index.ts
│ └── plugin-types.ts
├── package.json
└── tsconfig.json

The files that ship with your plugin, other than scripts.

This folder must include plugin.json, and it can hold anything else too, like .psd artwork, .fountain or .fdx screenplays, and audio. Everything in it is available to your novel.

Your plugin’s name, version, and entry point:

{
"name": "my-plugin",
"version": "1.0.0",
"main": "index.js"
}

main is a path relative to the .build folder, not your source folder. See the plugin.json reference for every field.

Your plugin’s entry point. It must export a createPlugin function:

import {
bindContext,
ContextKey,
onInitState,
Plugin,
} from "@inkweaverdev/inkweaver-sdk";
import * as commands from "./commands";
import * as selectors from "./selectors";
export function createPlugin(key: ContextKey): Plugin {
bindContext(key);
onInitState({
default: { greetingCount: 0 },
});
return {
commands,
selectors,
};
}

createPlugin runs once, when the novel starts. It returns the commands and selectors your plugin provides.

bindContext connects your plugin to its own private state and services. It’s required, and it must run before anything else in your plugin uses the SDK.

onInitState sets your plugin’s starting state. See State and settings for more.

The TypeScript types for your plugin’s public API. Your plugin’s commands and selectors are declared here by extending the SDK’s Commands and Selectors interfaces:

import "@inkweaverdev/inkweaver-sdk";
declare module "@inkweaverdev/inkweaver-sdk" {
interface Commands {
greet: (ctx: GreetArgs) => Promise<GreetResult>;
}
interface Selectors {
getGreetingCount: () => GetGreetingCountResult;
}
}
export interface GreetArgs {
name: string;
}
export interface GreetResult {
greeting: string;
}
export interface GetGreetingCountResult {
count: number;
}
export {};

decorateCommand and decorateSelector are typed against these interfaces. Until you declare a command or selector here, no code can decorate it, including your own.

Your command and selector implementations. Keeping them in separate folders is a convention, and it keeps createPlugin tidy as your plugin grows.

A standard npm manifest for your plugin.

The TypeScript configuration. It compiles everything in src into the JavaScript your plugin ships.

A temporary folder with your compiled scripts and a copy of assets. Look here to see what your plugin will contain.

Where your finished .inkb file goes. It’s a zip archive of the .build folder.

Before you can use a plugin in a novel, you need to bottle it. Run:

Terminal
npm run bottle

This compiles your TypeScript and packages your plugin as an .inkb file in the .release folder.

You can try a plugin in a novel without copying it into the novel’s workspace. Bottle your plugin, then add its path to the externals entry in the novel workspace’s .env file:

externals=../../plugins/my-plugin/.release/my-plugin.inkb,../../plugins/my-plugin/.release/another-plugin.inkb

externals is a comma-separated list of paths, relative to the novel’s workspace. Inkweaver includes each one whenever it builds the novel.