@moku-labs/common 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 moku-labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ <div align="center">
2
+
3
+ # @moku-labs/common
4
+
5
+ **Shared, framework-agnostic plugins for the Moku family.**
6
+
7
+ Author a plugin once, reuse it across every Moku framework. `@moku-labs/common` is
8
+ a *catalog* — it exports plugin objects built on the
9
+ [@moku-labs/core](https://github.com/moku-labs/core) micro-kernel, ready to drop
10
+ into any framework's `createCoreConfig`. No framework of its own, no lock-in.
11
+
12
+ <br/>
13
+
14
+ [![npm](https://img.shields.io/npm/v/@moku-labs/common?logo=npm&color=cb3837&label=npm)](https://www.npmjs.com/package/@moku-labs/common)
15
+ [![types](https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white)](#requirements)
16
+ [![browser entry](https://img.shields.io/badge/browser%20entry-node--free-2da44e)](#entry-points)
17
+ [![node](https://img.shields.io/badge/node-%3E%3D24-339933?logo=node.js&logoColor=white)](#requirements)
18
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
19
+
20
+ <br/>
21
+
22
+ [Install](#install) ·
23
+ [Catalog](#catalog) ·
24
+ [Usage](#usage) ·
25
+ [Entry points](#entry-points) ·
26
+ [Scripts](#scripts)
27
+
28
+ </div>
29
+
30
+ ---
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ bun add @moku-labs/common @moku-labs/core
36
+ ```
37
+
38
+ > [!NOTE]
39
+ > **Status: `0.x` — early.** The catalog is small and growing. The plugins are built on `@moku-labs/core`; install it alongside (your framework already depends on it).
40
+
41
+ ## Why @moku-labs/common
42
+
43
+ - **Write once, reuse everywhere.** Cross-cutting plugins (logging, env, …) live here instead of being copy-pasted into every Moku framework.
44
+ - **A catalog, not a framework.** No `createApp`, no defaults of its own. You import plugin objects and register them in *your* framework's `createCoreConfig`.
45
+ - **Core plugins.** Everything here is built with `createCorePlugin`, so a plugin's API is injected onto every plugin's context (`ctx.log`, `ctx.env`) — framework-agnostic by construction.
46
+ - **The `/browser` entry is guaranteed node-free.** A client-safe subset whose static import graph references zero node modules, enforced by a CI gate.
47
+
48
+ ## Catalog
49
+
50
+ | Export | Kind | Responsibility |
51
+ |---|---|---|
52
+ | [`logPlugin`](src/plugins/log/README.md) | core plugin | Always-on in-memory trace + an `expect()` event-trace DSL for testable workflows. Injected as `ctx.log`. |
53
+ | [`envPlugin`](src/plugins/env/README.md) | core plugin | Multi-provider environment / secret injection, validated and frozen at `onInit`, with `PUBLIC_` cross-validation. Exposed as `ctx.env`. |
54
+ | `dotenv` · `processEnv` · `cloudflareBindings` | env providers (Node) | Resolve env from `.env` files / `process.env` / Cloudflare bindings. Import `node:fs`. |
55
+ | `browserEnv` | env provider (browser) | Reads `import.meta.env` + `globalThis.__ENV__`. Zero `node:*`. |
56
+ | `Log` · `Env` | type namespaces | `Log.LogApi`, `Env.EnvConfig`, … |
57
+
58
+ ## Usage
59
+
60
+ Register the plugins in your framework's `createCoreConfig` — their APIs are then injected onto every plugin's context:
61
+
62
+ ```ts
63
+ // my-framework/config.ts
64
+ import { createCoreConfig } from "@moku-labs/core";
65
+ import { envPlugin, logPlugin } from "@moku-labs/common";
66
+
67
+ type Config = { /* … */ };
68
+ type Events = { /* … */ };
69
+
70
+ export const coreConfig = createCoreConfig<Config, Events, [typeof logPlugin, typeof envPlugin]>(
71
+ "my-framework",
72
+ {
73
+ config: { /* … */ },
74
+ plugins: [logPlugin, envPlugin] // ctx.log + ctx.env on every plugin
75
+ }
76
+ );
77
+ ```
78
+
79
+ Supply the `env` provider that matches each target:
80
+
81
+ ```ts
82
+ import { dotenv, processEnv } from "@moku-labs/common"; // Node
83
+ import { browserEnv } from "@moku-labs/common/browser"; // browser (node-free)
84
+ ```
85
+
86
+ ## Entry points
87
+
88
+ | Entry | Format | For | Includes |
89
+ |---|---|---|---|
90
+ | **`@moku-labs/common`** | dual ESM + CJS | Node | the full catalog, incl. the Node env providers (`dotenv` / `processEnv` / `cloudflareBindings`) |
91
+ | **`@moku-labs/common/browser`** | ESM-only | client bundles | `logPlugin`, `envPlugin`, `browserEnv` and the `Log` / `Env` types — **with all node-only code excluded** |
92
+
93
+ Importing `@moku-labs/common/browser` can **never** drag `node:*` code into a client bundle, regardless of bundler or tree-shaking — its static import graph references zero node-only modules. CI proves it:
94
+
95
+ ```sh
96
+ bun run check:bundle # asserts: zero static node imports + under the gzip budget
97
+ ```
98
+
99
+ ## Scripts
100
+
101
+ ```sh
102
+ bun run build # build with tsdown (dual ESM+CJS + ESM-only browser entry)
103
+ bun run test # all tests (vitest)
104
+ bun run test:unit # unit tests only
105
+ bun run test:integration # integration tests only
106
+ bun run test:coverage # tests with coverage (90% threshold)
107
+ bun run lint # biome check + eslint
108
+ bun run lint:fix # auto-fix lint issues
109
+ bun run format # format with biome
110
+ bun run validate # publint + attw — verify the package export map
111
+ bun run check:bundle # assert the browser bundle is node-free + under the gzip budget
112
+ ```
113
+
114
+ ## Requirements
115
+
116
+ - **Node `>= 24`** and **Bun `>= 1.3.14`** — use `bun` exclusively (never npm/yarn/pnpm).
117
+ - **TypeScript** in strict mode, with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
118
+ - **[`@moku-labs/core`](https://github.com/moku-labs/core)** — the micro-kernel the plugins are built on.
119
+
120
+ ## License
121
+
122
+ [MIT](./LICENSE) © [moku-labs](https://github.com/moku-labs)
@@ -0,0 +1,344 @@
1
+ declare namespace types_d_exports$1 {
2
+ export { ExpectChain, LogApi, LogConfig, LogEntry, LogLevel, LogSink, LogState };
3
+ }
4
+ /**
5
+ * @file log plugin — type definitions skeleton.
6
+ *
7
+ * Core-plugin type surface: config, state, public API, and the supporting
8
+ * value/sink/assertion-chain types. These types are inferred onto the plugin
9
+ * via state.ts / api.ts; index.ts passes NO explicit generics.
10
+ */
11
+ /**
12
+ * Runtime mode for the log plugin. Selects which default sinks are installed at
13
+ * onInit. The in-memory trace sink is ALWAYS installed regardless of mode.
14
+ *
15
+ * - "test" — no console sink (keeps test output clean); trace only.
16
+ * - "silent" — no console sink (explicit quiet); trace only.
17
+ * - "dev" — console sink + trace.
18
+ * - "production" — console sink + trace.
19
+ */
20
+ type LogConfig = {
21
+ /** Sink-selection mode. Defaults to `production`. */mode: "test" | "dev" | "production" | "silent";
22
+ };
23
+ /** Severity level for a log entry. */
24
+ type LogLevel = "debug" | "info" | "warn" | "error";
25
+ /**
26
+ * A single recorded log entry.
27
+ */
28
+ type LogEntry = {
29
+ /** Severity level. */level: LogLevel; /** Event identifier (free-form string; convention: `domain:action`). */
30
+ event: string; /** Optional structured payload associated with the event. */
31
+ data?: unknown; /** Capture timestamp in epoch milliseconds (`Date.now()` at append time). */
32
+ ts: number; /** Optional originating plugin name. Reserved for future enrichment. */
33
+ plugin?: string;
34
+ };
35
+ /**
36
+ * Pluggable output target. Implement this to add console/file/JSON/etc. sinks
37
+ * WITHOUT changing the log API. Each logged entry is passed to `write` once,
38
+ * in registration order.
39
+ */
40
+ type LogSink = {
41
+ /**
42
+ * Write a single entry to this sink.
43
+ *
44
+ * @param entry - The entry to emit.
45
+ */
46
+ write(entry: LogEntry): void;
47
+ };
48
+ /**
49
+ * Fluent event-trace assertion chain. Reads the live entries array on each call,
50
+ * so assertions reflect the trace state at call time (not chain-creation time).
51
+ * Every method returns the same chain for fluent chaining; assertion failures throw.
52
+ */
53
+ type ExpectChain = {
54
+ /**
55
+ * Assert at least one entry has `event`, optionally matching `partial` (subset match).
56
+ *
57
+ * @param event - Event name to find.
58
+ * @param partial - Optional partial data shape (subset-matched against `entry.data`).
59
+ * @returns The same chain for chaining.
60
+ * @throws {Error} `LogExpectAssertionError` when no matching entry exists.
61
+ */
62
+ toHaveEvent(event: string, partial?: Record<string, unknown>): ExpectChain;
63
+ /**
64
+ * Assert all of `events` appear in the trace in the given relative order
65
+ * (gaps allowed; later events must occur after earlier ones).
66
+ *
67
+ * @param events - Ordered list of event names.
68
+ * @returns The same chain for chaining.
69
+ * @throws {Error} `LogExpectAssertionError` when the ordering cannot be satisfied.
70
+ */
71
+ toHaveEventInOrder(events: string[]): ExpectChain;
72
+ /**
73
+ * Assert NO entry has `event` (optionally narrowed by `partial`).
74
+ *
75
+ * @param event - Event name that must be absent.
76
+ * @param partial - Optional partial data shape; only matching entries violate the assertion.
77
+ * @returns The same chain for chaining.
78
+ * @throws {Error} `LogExpectAssertionError` when a matching entry exists.
79
+ */
80
+ toNotHaveEvent(event: string, partial?: Record<string, unknown>): ExpectChain;
81
+ };
82
+ /**
83
+ * Internal mutable state for the log plugin. Created fresh per createApp construction.
84
+ */
85
+ type LogState = {
86
+ /** Append-only ordered trace of every logged entry (the in-memory trace sink's backing store). */entries: LogEntry[]; /** Registered output sinks. Each entry is written to every sink in order. */
87
+ sinks: LogSink[];
88
+ };
89
+ /** Public log API injected as `ctx.log` on every regular plugin and exposed as `app.log`. */
90
+ type LogApi = {
91
+ /**
92
+ * Append an `info` entry and fan it out to every sink.
93
+ *
94
+ * @param event - Event identifier (convention: `domain:action`).
95
+ * @param data - Optional structured payload.
96
+ */
97
+ info(event: string, data?: unknown): void;
98
+ /**
99
+ * Append a `debug` entry and fan it out to every sink.
100
+ *
101
+ * @param event - Event identifier (convention: `domain:action`).
102
+ * @param data - Optional structured payload.
103
+ */
104
+ debug(event: string, data?: unknown): void;
105
+ /**
106
+ * Append a `warn` entry and fan it out to every sink.
107
+ *
108
+ * @param event - Event identifier (convention: `domain:action`).
109
+ * @param data - Optional structured payload.
110
+ */
111
+ warn(event: string, data?: unknown): void;
112
+ /**
113
+ * Append an `error` entry. When `error` is provided, its `message`/`stack` are
114
+ * merged into `data` under an `error` key; otherwise `data` is recorded as-is.
115
+ *
116
+ * @param event - Event identifier (convention: `domain:action`).
117
+ * @param data - Optional structured payload.
118
+ * @param error - Optional originating Error to merge into `data`.
119
+ */
120
+ error(event: string, data?: unknown, error?: Error): void;
121
+ /**
122
+ * Return a frozen snapshot of the entries recorded so far (a fresh copy).
123
+ *
124
+ * @returns A readonly, frozen copy of the recorded entries.
125
+ */
126
+ trace(): readonly LogEntry[];
127
+ /**
128
+ * Return a fluent assertion chain bound to the live entries array.
129
+ *
130
+ * @returns A fresh {@link ExpectChain}.
131
+ */
132
+ expect(): ExpectChain;
133
+ /**
134
+ * Register an additional output sink at runtime.
135
+ *
136
+ * @param sink - The sink to add to the fan-out list.
137
+ */
138
+ addSink(sink: LogSink): void; /** Clear all recorded entries while keeping registered sinks. */
139
+ reset(): void;
140
+ };
141
+ //#endregion
142
+ //#region src/plugins/log/index.d.ts
143
+ /**
144
+ * Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
145
+ * API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
146
+ * No depends / events / hooks (core plugin per spec/03 §5).
147
+ *
148
+ * @see README.md
149
+ */
150
+ declare const logPlugin: import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>;
151
+ declare namespace types_d_exports {
152
+ export { EnvApi, EnvConfig, EnvProvider, EnvState, EnvVarSpec };
153
+ }
154
+ /**
155
+ * @file env plugin — public + boundary type definitions.
156
+ */
157
+ /**
158
+ * A source of raw environment values.
159
+ *
160
+ * Providers are walked in array order during resolution; the first provider to
161
+ * return a non-`undefined` (and non-empty-string) value for a key wins. `load()`
162
+ * is called exactly once per resolution at `onInit` time, after which both env
163
+ * maps are frozen. A provider like {@link cloudflareBindings} reads `globalThis`
164
+ * at that single `onInit` call (not per request).
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const custom: EnvProvider = {
169
+ * name: "vault",
170
+ * load: () => ({ DB_URL: readVaultSecret("db") })
171
+ * };
172
+ * ```
173
+ */
174
+ interface EnvProvider {
175
+ /** Human-readable provider name, used in diagnostics and error messages. */
176
+ name: string;
177
+ /**
178
+ * Reads this provider's current view of the environment.
179
+ *
180
+ * @returns A flat record of variable names to string values. Keys the
181
+ * provider cannot supply must be omitted or set to `undefined`.
182
+ */
183
+ load(): Record<string, string | undefined>;
184
+ }
185
+ /**
186
+ * Declares how a single environment variable is validated and exposed.
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * const port: EnvVarSpec = { public: false, required: false, default: "3000" };
191
+ * const apiBase: EnvVarSpec = { public: true }; // key must start with PUBLIC_
192
+ * const token: EnvVarSpec = { public: false, required: true, secret: true };
193
+ * ```
194
+ */
195
+ interface EnvVarSpec {
196
+ /**
197
+ * Whether the variable is safe to ship to the browser. When `true`, the key
198
+ * **must** start with {@link EnvConfig.publicPrefix} (cross-checked at
199
+ * `onInit`), and the variable is included in {@link EnvApi.getPublicMap}.
200
+ */
201
+ public: boolean;
202
+ /** Whether resolution fails if the variable is still undefined after defaults. */
203
+ required?: boolean;
204
+ /** Value applied when no provider supplies the variable. */
205
+ default?: string;
206
+ /**
207
+ * Marks the variable as a secret for documentation / tooling. Has no runtime
208
+ * effect on resolution, but secrets are never permitted to be `public`.
209
+ */
210
+ secret?: boolean;
211
+ }
212
+ /**
213
+ * Configuration for the {@link envPlugin} core plugin.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * createCoreConfig("web", {
218
+ * plugins: [envPlugin],
219
+ * pluginConfigs: {
220
+ * env: {
221
+ * schema: {
222
+ * PUBLIC_API_URL: { public: true, default: "/api" },
223
+ * SESSION_SECRET: { public: false, required: true, secret: true }
224
+ * }
225
+ * }
226
+ * }
227
+ * });
228
+ * ```
229
+ */
230
+ type EnvConfig = {
231
+ /** Per-variable validation + exposure rules, keyed by variable name. */schema: Record<string, EnvVarSpec>;
232
+ /**
233
+ * Ordered list of value sources. The first provider yielding a non-`undefined`
234
+ * (and non-empty-string) value for a key wins. The plugin's own spec default is
235
+ * `[]`; the consumer supplies the providers per target (`[dotenv(), processEnv()]`
236
+ * on Node) — only the `/browser` entry pre-wires `browserEnv()` out of the box.
237
+ */
238
+ providers: EnvProvider[];
239
+ /**
240
+ * Prefix that public variable names must carry. Bidirectionally enforced at
241
+ * `onInit`. Framework default is `"PUBLIC_"`.
242
+ */
243
+ publicPrefix: string;
244
+ };
245
+ /**
246
+ * Internal env plugin state: the resolved variable table and its public subset.
247
+ * Both maps are populated and frozen (via `freezeMap`) during `onInit`.
248
+ *
249
+ * Exported only to type the `createState` / `api` / `validate` boundary —
250
+ * consumers use {@link EnvApi}, never `EnvState`.
251
+ */
252
+ interface EnvState {
253
+ /** All validated variables that resolved to a defined value (incl. defaults). */
254
+ resolved: Map<string, string>;
255
+ /** Subset of `resolved` where `schema[key].public === true`. */
256
+ publicMap: Map<string, string>;
257
+ }
258
+ /**
259
+ * The resolved-environment accessor mounted at `ctx.env`. Built by the plugin's
260
+ * `api` factory over `ctx.state` ({@link EnvState}).
261
+ *
262
+ * Available after `onInit` (i.e. inside any plugin's lifecycle and in consumer
263
+ * code). All accessors read from the frozen `resolved` / `publicMap` maps;
264
+ * mutation is impossible.
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * const url = ctx.env.get("PUBLIC_API_URL"); // string | undefined
269
+ * const token = ctx.env.require("DEPLOY_TOKEN"); // string, or throws
270
+ * ```
271
+ */
272
+ type EnvApi = {
273
+ /**
274
+ * Reads a resolved variable.
275
+ *
276
+ * @param key - Variable name.
277
+ * @returns The value, or `undefined` if not present / not in schema.
278
+ */
279
+ get(key: string): string | undefined;
280
+ /**
281
+ * Reads a variable that must exist.
282
+ *
283
+ * @param key - Variable name.
284
+ * @returns The value.
285
+ * @throws {Error} If the variable is undefined.
286
+ */
287
+ require(key: string): string;
288
+ /**
289
+ * Tests presence of a resolved variable.
290
+ *
291
+ * @param key - Variable name.
292
+ * @returns `true` if a value is present.
293
+ */
294
+ has(key: string): boolean;
295
+ /**
296
+ * Returns all public variables as a frozen plain object — convenient for
297
+ * spreading into a serializable payload.
298
+ *
299
+ * @returns A frozen `Record` of public variable names to values.
300
+ */
301
+ getPublic(): Readonly<Record<string, string>>;
302
+ /**
303
+ * Returns the frozen map of public variables. This is the **sole** intended
304
+ * input to a build-time `define` injection: every entry is safe to inline
305
+ * into the browser bundle.
306
+ *
307
+ * @returns The frozen public map.
308
+ */
309
+ getPublicMap(): ReadonlyMap<string, string>;
310
+ };
311
+ //#endregion
312
+ //#region src/plugins/env/providers.browser.d.ts
313
+ /**
314
+ * A browser-safe {@link EnvProvider} that reads `import.meta.env` and an optional
315
+ * `globalThis[globalKey]` snapshot, merging them with the runtime global winning.
316
+ * Contains zero `node:*` imports, so it is safe to include in the client bundle.
317
+ * Never throws on missing sources — each absent source resolves to `{}`.
318
+ *
319
+ * @param options - Optional settings.
320
+ * @param options.globalKey - `globalThis` key to read a public-env snapshot from. Defaults to `"__ENV__"`.
321
+ * @returns An {@link EnvProvider} named `browser-env`.
322
+ * @example
323
+ * ```ts
324
+ * const provider = browserEnv();
325
+ * provider.load(); // { PUBLIC_API_URL: "/api", ... }
326
+ * ```
327
+ */
328
+ declare function browserEnv(options?: {
329
+ globalKey?: string;
330
+ }): EnvProvider;
331
+ //#endregion
332
+ //#region src/plugins/env/index.d.ts
333
+ /**
334
+ * Core plugin that resolves, validates, and freezes the environment at `onInit`,
335
+ * exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * createApp({ pluginConfigs: { env: { schema: { PUBLIC_API_URL: { public: true } } } } });
340
+ * ```
341
+ */
342
+ declare const envPlugin: import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>;
343
+ //#endregion
344
+ export { types_d_exports as Env, types_d_exports$1 as Log, browserEnv, envPlugin, logPlugin };