Skip to main content

High-level overview

MorJS is built in layers. @morjs/takin is the lowest-level engine; @morjs/cli registers commands and compiler plugins on top of it; your mor.config.ts configures what runs.
┌─────────────────────────────────────────────────────┐
│                    @morjs/cli                       │
│  defineConfig · mor compile · mor create · …        │
├─────────────────────────────────────────────────────┤
│               Plugin ecosystem                      │
│  plugin-compiler · plugin-composer · plugin-mocker  │
├─────────────────────────────────────────────────────┤
│                @morjs/takin                         │
│     Takin (engine)  →  Runner  →  Hook system       │
└─────────────────────────────────────────────────────┘

Takin engine

Takin is the main entry class (packages/takin/src/takin.ts). It extends Config, owns the global lifecycle hooks, and orchestrates one or more Runner instances — one per user config entry when multi-config mode is active.

Takin lifecycle hooks

All hooks are tapable instances.
HookTypeDescription
initializeAsyncSeriesHook<[Takin]>Fires after construction, before run() processes any config.
prepareAsyncSeriesHook<RunnerOptions>Pre-run preparation phase run through a dedicated inner Runner. Use it for early feature interception.
configLoadedAsyncSeriesHook<[Takin, CommandOptions]>Fires after the config file is loaded from disk. Not called when config is passed directly via API.
configFilteredAsyncSeriesWaterfallHook<[UserConfig[], CommandOptions]>Fires after multi-config filtering. Modify which configs get executed.
extendRunnerSyncWaterfallHook<[typeof Runner, RunnerOptions]>Replace or extend the Runner class itself before it is instantiated.

Run sequence

1

initialize

takin.hooks.initialize fires. Plugins registered via takin.use() have already run their onUse callbacks at this point.
2

prepare

A dedicated inner Runner executes ChangeCwdPlugin, LoadEnvPlugin, MultiConfigPlugin, and PluginConfigPlugin to load .env files, discover the config file, and compute multi-config filters.
3

configLoaded / configFiltered

After the config file is read, configLoaded fires. Then configFiltered trims the config array based on --name filters or index.
4

extendRunner

extendRunner allows swapping the Runner class. The result is used for all subsequent Runner instantiations.
5

Runner.run (per config)

A Runner is created for each filtered user config and runs independently through its own hook chain.

Runner

Each Runner processes a single user config object. It parses the CLI, validates config, then invokes the run hook where plugins do their actual work (compilation, mocking, etc.).

Runner lifecycle hooks

HookTypeDescription
initializeSyncHook<Runner>Runner is constructed and plugins are loaded.
cliSyncHook<Cli>Register commands and options with the CLI parser.
matchedCommandAsyncSeriesHook<CommandOptions>A CLI command was matched.
loadConfigAsyncSeriesHook<CommandOptions>Load user config from disk.
modifyUserConfigAsyncSeriesWaterfallHook<[UserConfig, CommandOptions]>Transform the resolved user config before validation.
registerUserConfigAsyncSeriesWaterfallHook<[AnyZodObject, Zod]>Register additional zod schema fields for config validation.
shouldRunSyncBailHook<Runner, boolean>Return false to abort execution (e.g., for --help or --version).
shouldValidateUserConfigSyncBailHook<Runner, boolean>Return false to skip config validation.
userConfigValidatedAsyncSeriesHook<UserConfig>Config has passed validation.
beforeRunAsyncSeriesHook<Runner>Pre-run hook for preparing data needed by commands.
runHookMap<AsyncParallelHook<CommandOptions>>The main execution hook, keyed by command name.
doneAsyncParallelHook<Runner>Runner completed successfully.
failedAsyncSeriesHook<Error>Runner encountered an error.
shutdownAsyncSeriesHook<Runner>Runner is being shut down (e.g., during reload()).

Plugin system

All extensibility in MorJS goes through the Plugin interface defined in packages/takin/src/plugin.ts:
export interface Plugin {
  name: string
  version?: string
  enforce?: 'pre' | 'post'
  onUse?: (takin: Takin) => void
  apply: (runner: Runner) => void
}

Plugin execution order

Plugins are sorted into three buckets before the Runner executes:
OrderConditionDescription
1enforce: 'pre'Runs before all other plugins.
2(no enforce)Normal plugins in registration order.
3enforce: 'post'Runs after all other plugins.
Within each bucket the original registration order is preserved.

Plugin registration sources

Plugins can enter the system from multiple sources (PluginTypes enum in packages/takin/src/config.ts):
SourceDescription
useRegistered via takin.use([...])
configListed in mor.config.ts under the plugins field
cliPassed via CLI options
runnerPassed directly to Runner.run()
autoAuto-loaded from package.json dependencies by pattern matching

Writing a plugin

import { Plugin, Runner } from '@morjs/utils'

const myPlugin: Plugin = {
  name: 'MyPlugin',
  enforce: 'pre', // optional: 'pre' | 'post'

  onUse(takin) {
    // Called once when registered via takin.use()
    takin.hooks.configFiltered.tap('MyPlugin', (configs, command) => {
      return configs
    })
  },

  apply(runner: Runner) {
    // Called for each Runner instance
    runner.hooks.userConfigValidated.tapPromise('MyPlugin', async (userConfig) => {
      // React to validated config
    })

    runner.hooks.run.for('compile').tapPromise('MyPlugin', async (command) => {
      // Execute during the 'compile' command
    })
  }
}

Compilation pipeline

The @morjs/plugin-compiler package drives the build. It hooks into runner.hooks.run.for('compile') and runs this pipeline:
Source files (WeChat or Alipay DSL)


  Entry Builder
  (discovers app.json → pages → components)


  webpack + custom loaders
  ┌─────────────────────────────────┐
  │  preprocessLoader               │
  │  scriptLoader  (TS → JS)        │
  │  templateLoader (WXML/AXML →    │
  │    target template format)      │
  │  styleLoader  (Less/SCSS → CSS) │
  │  configLoader (JSON/JSONC/JSON5)│
  │  sjsLoader    (WXS/SJS)         │
  │  postprocessLoader              │
  └─────────────────────────────────┘


  Platform compiler plugin
  (@morjs/plugin-compiler-wechat,
   @morjs/plugin-compiler-alipay, …)


  Platform output
  (dist/wechat/, dist/alipay/, …)

Compile modes

The compileMode user config field controls how modules are resolved:
ModeDescription
bundleFull webpack bundling with npm resolution. Recommended for projects using npm packages.
transformSource-only transformation without bundling. Faster for projects where dependencies are pre-bundled.
default and transfer are legacy aliases for transform. They produce a warning and are normalized automatically.

Source types

The sourceType field in user config tells the compiler which DSL the source is written in:
  • wechat — source written in WeChat mini-program syntax
  • alipay — source written in Alipay mini-program syntax

Internal plugins in @morjs/cli

When you install @morjs/cli, these plugins are always registered:
PluginRole
CliPluginRegisters version/help CLI options, verbose mode, and the default command.
CleanPluginCleans the output directory before compilation.
WebpackPluginWraps webpack configuration and exposes webpackWrapper hook.
CompilerPluginEntry point for the full compilation pipeline.
ComposerPluginHandles multi-module composition (integrated mini-programs).
GeneratorPluginBacks the mor create scaffold command.
AnalyzerPluginProvides bundle analysis output.
AutoReloadPluginWatches for config changes and triggers takin.reload().
MockerPluginProvides local API mocking during development.
PrettyErrorPluginFormats runtime errors for readable terminal output.

Build docs developers (and LLMs) love