Skip to content
Moku
@moku-labs/common pages

Quick start

Add log and env to a framework, read a validated secret and assert on the log trace.

On this page

Log and env in four steps

This page is for a framework author. If you build on @moku-labs/web or another framework, ctx.log and ctx.env are already there: you only configure them through pluginConfigs.

  1. Install. @moku-labs/core is a peer dependency.

    install
    bun add @moku-labs/common @moku-labs/core
  2. Register both plugins. In createCoreConfig, with their types in the third generic.

  3. Use them. ctx.log and ctx.env are on every plugin.

  4. Configure them per app. The app picks the providers, the variables and the log mode.

The code

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

type Config = { stage: "production" | "development" };
type Events = {};

export const coreConfig = createCoreConfig<Config, Events, [typeof logPlugin, typeof envPlugin]>(
  "my-framework",
  {
    config: { stage: "production" },
    plugins: [logPlugin, envPlugin]
  }
);

export const { createPlugin, createCore } = coreConfig;
  1. The tuple types ctx.log and ctx.env.
my-framework/plugins/mailer.ts
import { createPlugin } from "../config";

export const mailerPlugin = createPlugin("mailer", {
  api: ctx => ({
    send: (to: string) => {
      const from = ctx.env.require("MAIL_FROM");
      ctx.log.info("mailer:sent", { from, to });
    }
  })
});
  1. Throws when the variable has no value.
  2. The name is domain:action by convention. The data is any value.
my-framework/index.ts
import { coreConfig, createCore } from "./config";
import { mailerPlugin } from "./plugins/mailer";

export const { createApp, createPlugin } = createCore(coreConfig, { plugins: [mailerPlugin] });
app.ts
import { dotenv, processEnv } from "@moku-labs/common";
import { createApp } from "./my-framework";

const app = createApp({
  pluginConfigs: {
    log: { mode: "silent" },
    env: {
      providers: [dotenv(), processEnv()],
      schema: {
        MAIL_FROM: { public: false, required: true },
        PUBLIC_SITE_URL: { public: true, default: "https://example.com" }
      }
    }
  }
});

app.mailer.send("[email protected]");

app.log.expect().toHaveEvent("mailer:sent", { to: "[email protected]" });
console.log(app.log.trace().map(entry => entry.event));
console.log(app.env.getPublic());
  1. Records everything, prints nothing.
  2. The first provider with a value wins, so .env.local beats the shell.
  3. Missing here means createApp throws. The app never starts half-configured.
  4. Throws when the trace has no match.
.env.local

Run

run
bun app.ts
[ "mailer:sent" ]
{
  PUBLIC_SITE_URL: "https://example.com",
}

Remove MAIL_FROM from .env.local and createApp throws, before app.mailer.send ever runs.

Pick the providers for the target

The env plugin has no providers until you pass some, on every entry.

TargetProvidersImport from
Node or Bun[dotenv(), processEnv()]@moku-labs/common
Browser[browserEnv()]@moku-labs/common/browser
Cloudflare Worker[cloudflareBindings()], [workerSafeProcessEnv()]@moku-labs/common
  • ctx.log and ctx.env are not on a core plugin's context. They reach regular plugins, app, and the app callbacks.
  • Pass the tuple. With Config and Events written out, TypeScript stops inferring the third generic, and plugins no longer compiles without it.
  • production is the default log mode. It prints info and above to the console. Use test or silent in tests.

Next