Skip to content
Moku
@moku-labs/core pages

Quick start

A framework with two plugins and an app on top of it, in four small files.

On this page

A framework in five steps

You write this page's code once per framework. An app on an existing framework, such as @moku-labs/web, never imports the core.

  1. Install. The core has no dependencies.

    install
    mkdir my-framework && cd my-framework
    bun init -y
    bun add @moku-labs/core
  2. Name the framework and its contract. createCoreConfig captures the global config and the events.

  3. Write the plugins. Each one owns its config, state, API and event handlers.

  4. Fix the defaults. createCore takes the plugin list and returns createApp.

  5. Write an app. createApp configures the plugins and returns a typed, frozen object.

The code

my-framework/config.ts
import { createCoreConfig } from "@moku-labs/core";

type Config = { siteName: string };
type Events = { "router:navigate": { from: string; to: string } };

export const coreConfig = createCoreConfig<Config, Events>("my-framework", {
  config: { siteName: "Untitled" }
});

export const { createPlugin, createCore } = coreConfig;
  1. The id starts every kernel error.
  2. This createPlugin already knows Config and Events.
my-framework/plugins/router.ts
import { createPlugin } from "../config";

export const routerPlugin = createPlugin("router", {
  config: { basePath: "/" },
  createState: ctx => ({ currentPath: ctx.config.basePath }),
  api: ctx => ({
    navigate: (path: string) => {
      const from = ctx.state.currentPath;
      ctx.state.currentPath = path;
      ctx.emit("router:navigate", { from, to: path });
    },
    current: () => ctx.state.currentPath
  })
});
  1. A wrong name or payload does not compile.
my-framework/plugins/views.ts
import { createPlugin } from "../config";
import { routerPlugin } from "./router";

export const viewsPlugin = createPlugin("views", {
  depends: [routerPlugin],
  createState: () => ({ count: 0 }),
  hooks: ctx => ({
    "router:navigate": () => {
      ctx.state.count += 1;
    }
  }),
  api: ctx => ({
    count: () => ctx.state.count
  })
});
  1. Checked in createApp: router must be earlier in the list.
my-framework/index.ts
import { coreConfig, createCore } from "./config";
import { routerPlugin } from "./plugins/router";
import { viewsPlugin } from "./plugins/views";

export const { createApp, createPlugin } = createCore(coreConfig, {
  plugins: [routerPlugin, viewsPlugin]
});
  1. An app can configure these. It cannot remove or reorder them.
app.ts
import { createApp } from "./my-framework";

const app = createApp({
  config: { siteName: "My Blog" },
  pluginConfigs: { router: { basePath: "/blog/" } },
  onReady: ctx => console.log(`ready: ${ctx.config.siteName}`),
  onStop: ctx => console.log(`views: ${ctx.views.count()}`)
});

await app.start();
app.router.navigate("/blog/about/");
console.log(`at: ${app.router.current()}`);
await app.stop();
  1. Callbacks see every plugin API.

Run

run
bun app.ts
ready: My Blog
at: /blog/about/
views: 1

createApp is synchronous, so ready prints before app.start() runs. views prints from onStop, after every plugin has stopped.

Let an app add a plugin

createApp also takes extra plugins. Export createPlugin from the framework, as above, and an app writes its own. Its API lands on app, in the same types as the defaults.

extend.ts
import { createApp, createPlugin } from "./my-framework";

const greeterPlugin = createPlugin("greeter", {
  config: { greeting: "Hello" },
  api: ctx => ({
    greet: (name: string) => `${ctx.config.greeting}, ${name}. Welcome to ${ctx.global.siteName}.`
  })
});

const app = createApp({
  plugins: [greeterPlugin],
  config: { siteName: "My Blog" },
  pluginConfigs: { greeter: { greeting: "Hi" } }
});

console.log(app.greeter.greet("Ada"));
  • Generics are all or nothing. createCoreConfig<Config, Events> is the only call that takes them. To add core plugins as well, pass the third one too: see Write a framework.
  • createApp runs onInit. Every plugin is initialized when it returns. app.start() is for what must wait: sockets, timers, listeners.
  • An app starts once. A stopped app cannot start again. Create a new one.

Next