@takazudo/zfb 2.3.1 → 2.4.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/dist/plugins.d.ts +7 -3
- package/dist/plugins.js.map +1 -1
- package/package.json +6 -6
package/dist/plugins.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Logger handed to every plugin hook.
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
2
|
+
* Logger handed to every plugin hook. `info`/`warn`/`error` each render on
|
|
3
|
+
* the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,
|
|
4
|
+
* attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.
|
|
5
|
+
* `console.*` is redirected the same way, but note it maps onto only two
|
|
6
|
+
* underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->
|
|
7
|
+
* error) — `console.warn` therefore renders as `zfb error:`, not
|
|
8
|
+
* `zfb warn:`. Prefer this logger over `console.*` when the level matters.
|
|
5
9
|
*/
|
|
6
10
|
export type ZfbPluginLogger = {
|
|
7
11
|
info(msg: string): void;
|
package/dist/plugins.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AA6a/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. The Rust side wraps `tracing` so\n * the same lines show up alongside the rest of the build's structured\n * logs. Hooks should prefer this over `console.log`.\n */\nexport type ZfbPluginLogger = {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAib/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. `info`/`warn`/`error` each render on\n * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,\n * attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.\n * `console.*` is redirected the same way, but note it maps onto only two\n * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->\n * error) — `console.warn` therefore renders as `zfb error:`, not\n * `zfb warn:`. Prefer this logger over `console.*` when the level matters.\n */\nexport type ZfbPluginLogger = {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zfb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. SDK with islands, content collections, pagination, and config helpers.",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"LICENSE"
|
|
75
75
|
],
|
|
76
76
|
"optionalDependencies": {
|
|
77
|
-
"@takazudo/zfb-darwin-arm64": "2.
|
|
78
|
-
"@takazudo/zfb-darwin-x64": "2.
|
|
79
|
-
"@takazudo/zfb-linux-arm64-gnu": "2.
|
|
80
|
-
"@takazudo/zfb-linux-x64-gnu": "2.
|
|
81
|
-
"@takazudo/zfb-win32-x64-msvc": "2.
|
|
77
|
+
"@takazudo/zfb-darwin-arm64": "2.4.0",
|
|
78
|
+
"@takazudo/zfb-darwin-x64": "2.4.0",
|
|
79
|
+
"@takazudo/zfb-linux-arm64-gnu": "2.4.0",
|
|
80
|
+
"@takazudo/zfb-linux-x64-gnu": "2.4.0",
|
|
81
|
+
"@takazudo/zfb-win32-x64-msvc": "2.4.0"
|
|
82
82
|
},
|
|
83
83
|
"publishConfig": {
|
|
84
84
|
"access": "public"
|