@mandujs/core 0.32.0 → 0.33.1

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.
@@ -1,64 +1,297 @@
1
- /**
2
- * CLI Lifecycle Hook System
3
- *
4
- * Lightweight hook runner for build/dev/start lifecycle events.
5
- * Hooks defined in mandu.config.ts run first, then plugin hooks
6
- * in registration order. Each hook is isolated -- one failure
7
- * does not block subsequent hooks.
8
- */
9
-
10
- export interface ManduHooks {
11
- onBeforeBuild?: () => void | Promise<void>;
12
- onAfterBuild?: (result: { success: boolean; duration: number }) => void | Promise<void>;
13
- onDevStart?: (info: { port: number; hostname: string }) => void | Promise<void>;
14
- onDevStop?: () => void | Promise<void>;
15
- onRouteChange?: (info: { routeId: string; pattern: string; kind: string }) => void | Promise<void>;
16
- onBeforeStart?: () => void | Promise<void>;
17
- }
18
-
19
- export interface ManduPlugin {
20
- name: string;
21
- hooks?: Partial<ManduHooks>;
22
- setup?: (config: Record<string, unknown>) => void | Promise<void>;
23
- }
24
-
25
- /**
26
- * Run a named lifecycle hook across config-level hooks and plugins.
27
- *
28
- * Execution order:
29
- * 1. Config hook (from mandu.config.ts `hooks` field)
30
- * 2. Plugin hooks (from `plugins[].hooks`, in array order)
31
- *
32
- * Each invocation is wrapped in try/catch so a single failing hook
33
- * does not prevent the remaining hooks from executing.
34
- */
35
- export async function runHook<K extends keyof ManduHooks>(
36
- hookName: K,
37
- plugins: ManduPlugin[],
38
- configHooks: Partial<ManduHooks> | undefined,
39
- ...args: Parameters<NonNullable<ManduHooks[K]>>
40
- ): Promise<void> {
41
- const invoke = async (
42
- label: string,
43
- fn: ((...a: unknown[]) => void | Promise<void>) | undefined,
44
- ) => {
45
- if (!fn) return;
46
- try {
47
- await fn(...args);
48
- } catch (error) {
49
- const msg = error instanceof Error ? error.message : String(error);
50
- console.error(`[plugin] ${hookName} failed in ${label}: ${msg}`);
51
- }
52
- };
53
-
54
- // Config-level hook runs first
55
- await invoke("config", configHooks?.[hookName] as ((...a: unknown[]) => void | Promise<void>) | undefined);
56
-
57
- // Plugin hooks run in registration order
58
- for (const plugin of plugins) {
59
- await invoke(
60
- plugin.name,
61
- plugin.hooks?.[hookName] as ((...a: unknown[]) => void | Promise<void>) | undefined,
62
- );
63
- }
64
- }
1
+ /**
2
+ * CLI Lifecycle Hook System — Phase 18.τ expansion.
3
+ *
4
+ * The original `ManduHooks` surface covered a few coarse lifecycle points
5
+ * (`onBeforeBuild`, `onAfterBuild`, `onDevStart`, `onDevStop`,
6
+ * `onRouteChange`, `onBeforeStart`). Phase 18.τ extends it so consumers
7
+ * can extend the bundler / prerender / middleware / router pipeline
8
+ * without forking the core. Every new hook is **optional** and defaults
9
+ * to "no-op" — existing plugins remain source-compatible.
10
+ *
11
+ * Contract:
12
+ * - Each hook is isolated: one failure does not block subsequent hooks.
13
+ * - Config-level hooks run first, plugin hooks run in declaration order.
14
+ * - Hooks that "return a value" (e.g. `definePrerenderHook`,
15
+ * `onManifestBuilt`, `defineTestTransform`, `defineMiddlewareChain`,
16
+ * `defineBundlerPlugin`) compose via documented merge semantics
17
+ * implemented in `./runner.ts`.
18
+ *
19
+ * The canonical runner lives in `./runner.ts`. The legacy `runHook()` in
20
+ * this file remains for back-compat with callers that already wire the
21
+ * original coarse hooks.
22
+ *
23
+ * @see `docs/architect/plugin-api.md`
24
+ */
25
+
26
+ import type { BunPlugin } from "bun";
27
+ import type { Middleware } from "../middleware/define";
28
+ import type { BundleStats } from "../bundler/types";
29
+ import type { RouteSpec, RoutesManifest } from "../spec/schema";
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════
32
+ // Contexts exposed to plugin hooks
33
+ // ═══════════════════════════════════════════════════════════════════════
34
+
35
+ /**
36
+ * Shared context every hook receives. Kept intentionally small so
37
+ * plugins do not couple to internals that are not part of the stable API.
38
+ */
39
+ export interface PluginContext {
40
+ /** Absolute project root directory. */
41
+ rootDir: string;
42
+ /** `"development"` during `mandu dev`, `"production"` during `mandu build`. */
43
+ mode: "development" | "production";
44
+ /** Plugin-scoped logger — honours the framework's structured log format. */
45
+ logger: {
46
+ debug: (msg: string, data?: unknown) => void;
47
+ info: (msg: string, data?: unknown) => void;
48
+ warn: (msg: string, data?: unknown) => void;
49
+ error: (msg: string, data?: unknown) => void;
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Context passed to `definePrerenderHook`. A plugin may inspect the
55
+ * pathname / params / HTML being emitted and return a {@link PrerenderOverride}
56
+ * to skip, rewrite, or replace the output.
57
+ */
58
+ export interface PrerenderContext extends PluginContext {
59
+ /** The URL pathname being prerendered (e.g. `/blog/hello-world`). */
60
+ pathname: string;
61
+ /** The route pattern (e.g. `/blog/:slug`) that matched. `undefined` for free-form `routes[]` paths. */
62
+ pattern?: string;
63
+ /** Dynamic params extracted from the pattern, when available. */
64
+ params?: Record<string, string>;
65
+ /**
66
+ * The HTML the fetch handler produced. Plugins may return a mutated
67
+ * copy via {@link PrerenderOverride.html}.
68
+ */
69
+ html: string;
70
+ }
71
+
72
+ /**
73
+ * Return value of `definePrerenderHook`. All fields are optional; the
74
+ * first plugin to set a field "wins" (later plugins in the chain may
75
+ * override via the same field — last-write semantics documented in the
76
+ * runner).
77
+ */
78
+ export interface PrerenderOverride {
79
+ /**
80
+ * When `true`, the page is removed from the prerender output entirely.
81
+ * The runtime will fall back to SSR for that URL.
82
+ */
83
+ skip?: boolean;
84
+ /** Replacement HTML. Passed verbatim to `fs.writeFile`. */
85
+ html?: string;
86
+ /**
87
+ * Rewrite the output pathname. Useful for e.g. writing `/about` to
88
+ * `/about.html` for static hosts that need explicit extensions.
89
+ */
90
+ pathname?: string;
91
+ }
92
+
93
+ /**
94
+ * Pair passed to `defineTestTransform`. Plugins return the new source
95
+ * string (or a Promise thereof). Returning the original `source`
96
+ * unchanged is a legal no-op.
97
+ */
98
+ export interface TestTransformContext {
99
+ /** Absolute path of the test file being processed. */
100
+ testFile: string;
101
+ /** Current source (may already be transformed by an earlier plugin). */
102
+ source: string;
103
+ }
104
+
105
+ // ═══════════════════════════════════════════════════════════════════════
106
+ // Hook surface — the plugin authoring contract
107
+ // ═══════════════════════════════════════════════════════════════════════
108
+
109
+ /**
110
+ * Full `ManduHooks` surface. Every hook is optional; the runner gracefully
111
+ * skips undefined entries. Hooks fall into three categories:
112
+ *
113
+ * 1. **Observers** — `onBeforeBuild`, `onAfterBuild`, `onDevStart`,
114
+ * `onDevStop`, `onRouteChange`, `onBeforeStart`, `onRouteRegistered`,
115
+ * `onBundleComplete`. Return value is ignored.
116
+ *
117
+ * 2. **Contributors** — `defineBundlerPlugin`, `defineMiddlewareChain`,
118
+ * `defineTestTransform`. The runner collects return values from all
119
+ * plugins and concatenates / pipes them (see merge semantics in
120
+ * `./runner.ts`).
121
+ *
122
+ * 3. **Transformers** — `definePrerenderHook`, `onManifestBuilt`. The
123
+ * runner folds the return value back into subsequent hook invocations
124
+ * (last non-undefined return wins).
125
+ */
126
+ export interface ManduHooks {
127
+ // ───── Legacy lifecycle observers (kept for back-compat) ─────
128
+ onBeforeBuild?: () => void | Promise<void>;
129
+ onAfterBuild?: (result: {
130
+ success: boolean;
131
+ duration: number;
132
+ }) => void | Promise<void>;
133
+ onDevStart?: (info: {
134
+ port: number;
135
+ hostname: string;
136
+ }) => void | Promise<void>;
137
+ onDevStop?: () => void | Promise<void>;
138
+ onRouteChange?: (info: {
139
+ routeId: string;
140
+ pattern: string;
141
+ kind: string;
142
+ }) => void | Promise<void>;
143
+ onBeforeStart?: () => void | Promise<void>;
144
+
145
+ // ───── Phase 18.τ — router / manifest ─────
146
+
147
+ /**
148
+ * Fires once per route as the FS scanner discovers it. Pure observer —
149
+ * throwing does not abort the scan; the runner isolates the error and
150
+ * proceeds. Useful for dependency audits, custom guards, or telemetry.
151
+ */
152
+ onRouteRegistered?: (route: RouteSpec) => void | Promise<void>;
153
+
154
+ /**
155
+ * Fires after the routes manifest is built but before it is persisted
156
+ * to `.mandu/manifest.json`. A plugin may return a mutated manifest;
157
+ * subsequent plugins see the mutated form ("pipe" semantics).
158
+ */
159
+ onManifestBuilt?: (
160
+ manifest: RoutesManifest
161
+ ) => RoutesManifest | Promise<RoutesManifest | void> | void;
162
+
163
+ // ───── Phase 18.τ — bundler ─────
164
+
165
+ /**
166
+ * Contribute additional `BunPlugin` entries to every `Bun.build` call
167
+ * issued by the bundler (runtime / router / vendor / island paths).
168
+ * May return a single plugin or an array. The returned plugins run
169
+ * AFTER Mandu's default plugins so user transforms see already-resolved
170
+ * imports.
171
+ */
172
+ defineBundlerPlugin?: () =>
173
+ | BunPlugin
174
+ | BunPlugin[]
175
+ | Promise<BunPlugin | BunPlugin[]>;
176
+
177
+ /**
178
+ * Fires once per successful `buildClientBundles()` completion with the
179
+ * final `BundleStats`. Purely observational — used for analytics,
180
+ * size-budget enforcement, report emission, etc. Throwing does not
181
+ * invalidate the build; the error is logged and swallowed.
182
+ */
183
+ onBundleComplete?: (stats: BundleStats) => void | Promise<void>;
184
+
185
+ // ───── Phase 18.τ — prerender ─────
186
+
187
+ /**
188
+ * Intercept each prerender step. The runner calls every plugin's
189
+ * hook in declaration order; returns are merged field-by-field with
190
+ * last-write semantics. A plugin may short-circuit by returning
191
+ * `{ skip: true }` — subsequent plugins still see the skip flag and
192
+ * may unset it.
193
+ */
194
+ definePrerenderHook?: (
195
+ ctx: PrerenderContext
196
+ ) => PrerenderOverride | void | Promise<PrerenderOverride | void>;
197
+
198
+ // ───── Phase 18.τ — middleware / test ─────
199
+
200
+ /**
201
+ * Contribute additional request-level middleware to the global chain
202
+ * (prepended — runs BEFORE user-declared `ManduConfig.middleware`).
203
+ * Returns are concatenated across all plugins in declaration order.
204
+ *
205
+ * Bridge wrappers like `csrfMiddleware()` / `sessionMiddleware()`
206
+ * compose naturally here. See `@mandujs/core/middleware`.
207
+ */
208
+ defineMiddlewareChain?: (
209
+ ctx: PluginContext
210
+ ) => Middleware[] | Promise<Middleware[]>;
211
+
212
+ /**
213
+ * Transform test source before execution (Phase 12.1 `mandu test`).
214
+ * Returns the new source string — returning the original input is a
215
+ * legal no-op. Multiple plugins pipe through in declaration order:
216
+ * each plugin sees the source produced by the previous one.
217
+ */
218
+ defineTestTransform?: (
219
+ ctx: TestTransformContext
220
+ ) => string | Promise<string>;
221
+ }
222
+
223
+ /**
224
+ * A Mandu plugin — a named object with optional `hooks` + `setup`.
225
+ *
226
+ * Construct with {@link definePlugin} (from `./define.ts`) for validation
227
+ * at definition time, or inline as a plain object literal — both work.
228
+ */
229
+ export interface ManduPlugin {
230
+ /** Plugin identifier shown in diagnostics. Must be unique per config. */
231
+ name: string;
232
+ /** Optional hook implementations. */
233
+ hooks?: Partial<ManduHooks>;
234
+ /**
235
+ * One-shot setup hook. Called once at `loadPlugins()` time with the
236
+ * plugin's slice of the user config (if any). Useful for registering
237
+ * resources, opening files, or spinning up background workers.
238
+ */
239
+ setup?: (config: Record<string, unknown>) => void | Promise<void>;
240
+ }
241
+
242
+ // ═══════════════════════════════════════════════════════════════════════
243
+ // Legacy coarse runner — preserved for back-compat
244
+ // ═══════════════════════════════════════════════════════════════════════
245
+
246
+ /**
247
+ * Run a named lifecycle hook across config-level hooks and plugins.
248
+ *
249
+ * Execution order:
250
+ * 1. Config hook (from mandu.config.ts `hooks` field)
251
+ * 2. Plugin hooks (from `plugins[].hooks`, in array order)
252
+ *
253
+ * Each invocation is wrapped in try/catch so a single failing hook
254
+ * does not prevent the remaining hooks from executing.
255
+ *
256
+ * This is a thin observer-only runner — hook return values are ignored.
257
+ * For transforming / contributing hooks (e.g. `definePrerenderHook`,
258
+ * `onManifestBuilt`, `defineBundlerPlugin`) use the canonical runner in
259
+ * `./runner.ts`.
260
+ */
261
+ export async function runHook<K extends keyof ManduHooks>(
262
+ hookName: K,
263
+ plugins: ManduPlugin[],
264
+ configHooks: Partial<ManduHooks> | undefined,
265
+ ...args: Parameters<NonNullable<ManduHooks[K]>>
266
+ ): Promise<void> {
267
+ const invoke = async (
268
+ label: string,
269
+ fn: ((...a: unknown[]) => void | Promise<void>) | undefined,
270
+ ) => {
271
+ if (!fn) return;
272
+ try {
273
+ await fn(...args);
274
+ } catch (error) {
275
+ const msg = error instanceof Error ? error.message : String(error);
276
+ console.error(`[plugin] ${hookName} failed in ${label}: ${msg}`);
277
+ }
278
+ };
279
+
280
+ // Config-level hook runs first
281
+ await invoke(
282
+ "config",
283
+ configHooks?.[hookName] as
284
+ | ((...a: unknown[]) => void | Promise<void>)
285
+ | undefined,
286
+ );
287
+
288
+ // Plugin hooks run in registration order
289
+ for (const plugin of plugins) {
290
+ await invoke(
291
+ plugin.name,
292
+ plugin.hooks?.[hookName] as
293
+ | ((...a: unknown[]) => void | Promise<void>)
294
+ | undefined,
295
+ );
296
+ }
297
+ }
@@ -1,41 +1,80 @@
1
- /**
2
- * DNA-001: Plugin System
3
- *
4
- * Mandu 플러그인 시스템
5
- * - Guard 프리셋 플러그인
6
- * - 빌드 플러그인
7
- * - 로거 전송 플러그인
8
- * - MCP 도구 플러그인
9
- * - 미들웨어 플러그인
10
- */
11
-
12
- export {
13
- PluginRegistry,
14
- globalPluginRegistry,
15
- definePlugin,
16
- } from "./registry";
17
-
18
- export type {
19
- Plugin,
20
- PluginApi,
21
- PluginCategory,
22
- PluginMeta,
23
- PluginHooks,
24
- GuardPresetPlugin,
25
- GuardRule,
26
- GuardRuleContext,
27
- PluginGuardViolation,
28
- LayerDefinition,
29
- ImportInfo,
30
- ExportInfo,
31
- BuildPlugin,
32
- BuildContext,
33
- BuildResult,
34
- LoggerTransportPlugin,
35
- LogEntry,
36
- McpToolPlugin,
37
- MiddlewarePlugin,
38
- } from "./types";
39
-
40
- export { runHook } from "./hooks";
41
- export type { ManduPlugin, ManduHooks } from "./hooks";
1
+ /**
2
+ * DNA-001: Plugin System
3
+ *
4
+ * Mandu 플러그인 시스템
5
+ * - Guard 프리셋 플러그인
6
+ * - 빌드 플러그인
7
+ * - 로거 전송 플러그인
8
+ * - MCP 도구 플러그인
9
+ * - 미들웨어 플러그인
10
+ *
11
+ * Phase 18.τ — expanded `ManduPlugin` hook surface (bundler / prerender /
12
+ * middleware / test pipeline). The lightweight `definePlugin()` helper
13
+ * for that shape lives in `./define.ts` and is re-exported as
14
+ * `defineManduPlugin` to avoid colliding with the legacy heavy
15
+ * `Plugin`-type `definePlugin` from `./registry.ts`.
16
+ *
17
+ * @see `./hooks.ts` for the typed hook surface.
18
+ * @see `./runner.ts` for canonical dispatch + merge semantics.
19
+ * @see `docs/architect/plugin-api.md`.
20
+ */
21
+
22
+ export {
23
+ PluginRegistry,
24
+ globalPluginRegistry,
25
+ definePlugin,
26
+ } from "./registry";
27
+
28
+ export type {
29
+ Plugin,
30
+ PluginApi,
31
+ PluginCategory,
32
+ PluginMeta,
33
+ PluginHooks,
34
+ GuardPresetPlugin,
35
+ GuardRule,
36
+ GuardRuleContext,
37
+ PluginGuardViolation,
38
+ LayerDefinition,
39
+ ImportInfo,
40
+ ExportInfo,
41
+ BuildPlugin,
42
+ BuildContext,
43
+ BuildResult,
44
+ LoggerTransportPlugin,
45
+ LogEntry,
46
+ McpToolPlugin,
47
+ MiddlewarePlugin,
48
+ } from "./types";
49
+
50
+ export { runHook } from "./hooks";
51
+ export type {
52
+ ManduPlugin,
53
+ ManduHooks,
54
+ PluginContext,
55
+ PrerenderContext,
56
+ PrerenderOverride,
57
+ TestTransformContext,
58
+ } from "./hooks";
59
+
60
+ // Phase 18.τ — canonical hook runner + lightweight definePlugin helper.
61
+ export {
62
+ runOnRouteRegistered,
63
+ runOnManifestBuilt,
64
+ runOnBundleComplete,
65
+ runDefineBundlerPlugin,
66
+ runDefinePrerenderHook,
67
+ runDefineMiddlewareChain,
68
+ runDefineTestTransform,
69
+ resolvePluginMiddleware,
70
+ formatHookErrors,
71
+ type HookError,
72
+ type HookRunReport,
73
+ type RunnerArgs,
74
+ } from "./runner";
75
+
76
+ // Rename to avoid symbol collision with the legacy `definePlugin` helper
77
+ // (which takes the heavier `Plugin` type and is used by the built-in
78
+ // registry). `defineManduPlugin` is the go-to helper for the modern
79
+ // lightweight `ManduPlugin` shape introduced in Phase 18.τ.
80
+ export { definePlugin as defineManduPlugin, isManduPlugin } from "./define";