Skip to main content

Overview

Takin is the underlying framework that drives all MorJS build tooling. It provides:
  • A plugin system based on tapable hooks.
  • Automatic configuration file loading, validation (via zod), and filtering.
  • Multi-config support: run one command across multiple configurations in parallel.
  • An extensible Runner class whose hooks and methods can be augmented by plugins.
PropertyValue
Package@morjs/takin
LicenseMIT
Default exporttakin(name: string): Takin factory function

Installation

npm install @morjs/takin

Quick start

import takin from '@morjs/takin'

const cli = takin('my-tool')

cli.use([myPlugin])

// Run with a command parsed from process.argv
await cli.run()

// Or pass a command directly (API mode)
await cli.run(
  { name: 'build', options: { watch: true } },
  [{ entry: 'src/index.ts' }]
)

Takin class

class Takin extends Config {
  readonly config: Config  // deprecated, use takin instance directly
  readonly hooks: TakinHooks

  constructor(name?: string)
  use(plugins: PluginOption[]): void
  run<R>(command?: CommandOptions, userConfigs?: UserConfig[], context?: RunnerContext | Record<string, any>): Promise<(R | undefined)[]>
  reload<R>(): Promise<(R | undefined)[]>
}

constructor

name
string
default:"DEFAULT_NAME"
The CLI name used in log prefixes and runner identification.

use

Registers plugins with the Takin instance. Plugins are applied to every Runner that is started.
use(plugins: PluginOption[]): void
plugins
PluginOption[]
required
Array of plugin instances (objects implementing the Plugin interface), plugin factories, or [plugin, options] tuples.

run

Executes a command. Internally:
  1. Fires hooks.initialize.
  2. Loads and filters user config (fires hooks.configLoaded and hooks.configFiltered).
  3. Creates one Runner per filtered config and runs it.
async run<R = any>(
  command?: CommandOptions,
  userConfigs?: UserConfig[],
  context?: RunnerContext | Record<string, any>
): Promise<(R | undefined)[]>
command
CommandOptions
Command to run. If omitted, the command is parsed from process.argv.
userConfigs
UserConfig[]
User configurations to run against. If omitted, Takin loads mor.config.ts (or equivalent) from the current working directory.
context
RunnerContext | Record<string, any>
Initial key-value context passed into every Runner. Accessible via runner.context.

reload

Stops all running Runner instances, resets configuration, and re-runs the last command with the same arguments.
async reload<R = any>(): Promise<(R | undefined)[]>

TakinHooks

Hooks available on every Takin instance.
initialize
AsyncSeriesHook<[Takin]>
Fires after takin.run() is called but before any configuration is loaded. Use this to register additional plugins or modify the Takin instance.
prepare
AsyncSeriesHook<RunnerOptions>
Preparatory stage run by an isolated Runner. Use this to modify RunnerOptions before the main runners are started.
configLoaded
AsyncSeriesHook<[Takin, CommandOptions]>
Fires after the user configuration file is loaded from disk. Not fired when userConfigs is passed directly to run(). Use this to post-process or validate the loaded config.
configFiltered
AsyncSeriesWaterfallHook<[UserConfig[], CommandOptions]>
Fires after configs are filtered by active targets / names. The hook receives the current filtered array and must return the final array to use.
extendRunner
SyncWaterfallHook<[typeof Runner, RunnerOptions]>
Allows replacing or sub-classing the Runner class used for each configuration. Tap this hook and return a Runner subclass to add custom properties or methods.

Plugin interface

interface Plugin {
  name: string
  version?: string
  enforce?: 'pre' | 'post'
  onUse?: (takin: Takin) => void
  apply: (runner: Runner) => void
}
name
string
required
Unique plugin identifier used in log output and deduplication checks.
version
string
Plugin version string. Used for debug logging.
enforce
'pre' | 'post'
Execution order relative to other plugins.
  • 'pre' — runs before all normal plugins.
  • 'post' — runs after all normal plugins.
  • Omitted — normal order.
onUse
(takin: Takin) => void
Called once when the plugin is registered with takin.use(). Use this to register additional plugins or hook into TakinHooks.
apply
(runner: Runner) => void
required
Called once per Runner execution. All runner.hooks taps should happen here.

PluginEnforceTypes

const PluginEnforceTypes = { pre: 'pre', post: 'post' } as const

Runner class

The command execution unit. One Runner instance is created per filtered user configuration.
class Runner<R = any> {
  readonly runnerId: number
  readonly hooks: RunnerHooks
  readonly config: Config
  readonly logger: Logger
  readonly generator: typeof Generator
  readonly context: RunnerContext
  readonly methods: MethodsContainer

  commandName?: string
  commandArgs?: string[]
  commandOptions?: Record<string, any>
  userConfig: UserConfig
  commandInvokedBy: 'cli' | 'api'

  static run(options: RunnerOptions): Promise<Runner>

  getCwd(): string
  getResult(): R | undefined
  getPlugins(): UsedPluginsMap
  getCurrentPluginName(): string | undefined
  getCommandOptions(): CommandOptions
  getRunnerName(): string
  addCommandAction(options: RunnerAddCommandActionOptions): void
  async invokeCommandAction(command?: CommandOptions): Promise<void>
}

RunnerHooks

All hooks available inside a plugin’s apply(runner) method.
HookTypeDescription
initializeSyncHook<Runner>Fires after all plugins are applied
cliSyncHook<Cli>Build the CLI command tree
matchedCommandAsyncSeriesHook<CommandOptions>A CLI command has been matched
loadConfigAsyncSeriesHook<CommandOptions>Load the user config file
modifyUserConfigAsyncSeriesWaterfallHook<[UserConfig, CommandOptions]>Modify the resolved user config
registerUserConfigAsyncSeriesWaterfallHook<[AnyZodObject, Zod]>Register config validation schema
shouldRunSyncBailHook<Runner, boolean | undefined>Return false to abort execution
shouldValidateUserConfigSyncBailHook<Runner, boolean | undefined>Return false to skip config validation
userConfigValidatedAsyncSeriesHook<UserConfig>Config validated; safe to read runner.userConfig
beforeRunAsyncSeriesHook<Runner>Just before the command action fires
runHookMap<AsyncParallelHook<CommandOptions>>Per-command action hooks (run.for('command:build'))
doneAsyncParallelHook<Runner>Runner completed successfully
failedAsyncSeriesHook<Error>Runner threw an unhandled error
shutdownAsyncSeriesHook<Runner>Runner is being shut down (used by reload())

RunnerOptions

interface RunnerOptions {
  config: Config
  userConfig: UserConfig
  command?: CommandOptions
  plugins?: Plugin[]
  context?: RunnerContext | Record<string, any>
}

Extending Runner

Use hooks.extendRunner to add typed properties and methods to the Runner class:
// 1. Declare the type augmentation
declare module '@morjs/takin' {
  interface RunnerExtendable {
    myProp: string
    myMethod(arg: number): void
  }
}

// 2. Provide the implementation
cli.hooks.extendRunner.tap('MyExtension', (RunnerBase) => {
  return class extends RunnerBase {
    myProp = 'hello'
    myMethod(arg: number) {
      console.log(arg)
    }
  }
})

Registering custom hooks

import { registerHook, registerHooks } from '@morjs/takin'

// Register a single hook factory
registerHook('myHook', () => new AsyncSeriesHook(['runner']))

// Register multiple hook factories at once
registerHooks({
  hookA: () => new SyncHook(['data']),
  hookB: () => new AsyncParallelHook(['event'])
})

Writing a plugin

import type { Plugin, Runner } from '@morjs/takin'

const myPlugin: Plugin = {
  name: 'my-plugin',
  version: '1.0.0',

  onUse(takin) {
    // Runs when takin.use([myPlugin]) is called
    console.log('Plugin registered with', takin.name)
  },

  apply(runner: Runner) {
    // Register a command
    runner.hooks.cli.tap(this.name, (cli) => {
      cli.command('greet', 'Say hello').action(() => {
        runner.addCommandAction({
          name: 'greet',
          pluginName: this.name,
          callback() {
            console.log('Hello from my-plugin!')
          }
        })
      })
    })

    // Modify user config
    runner.hooks.modifyUserConfig.tap(this.name, (config) => {
      return { ...config, myOption: config.myOption ?? 'default' }
    })

    // Register config schema
    runner.hooks.registerUserConfig.tap(this.name, (schema, z) => {
      return schema.extend({
        myOption: z.string().optional()
      })
    })

    // Do work before the command action fires
    runner.hooks.beforeRun.tapPromise(this.name, async (runner) => {
      console.log('Working directory:', runner.getCwd())
    })
  }
}

export default myPlugin

Build docs developers (and LLMs) love