@takazudo/zfb 2.22.0 → 3.0.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.
Files changed (85) hide show
  1. package/README.md +5 -9
  2. package/dist/config.d.ts +62 -19
  3. package/dist/config.js +27 -6
  4. package/dist/config.js.map +1 -1
  5. package/dist/content.d.ts +8 -22
  6. package/dist/content.js +6 -28
  7. package/dist/content.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/island-boundary.d.ts +4 -0
  12. package/dist/island-boundary.js +42 -0
  13. package/dist/island-boundary.js.map +1 -0
  14. package/dist/island.d.ts +2 -118
  15. package/dist/island.js +4 -284
  16. package/dist/island.js.map +1 -1
  17. package/dist/jsx-types.d.ts +2 -38
  18. package/dist/jsx-types.js +3 -10
  19. package/dist/jsx-types.js.map +1 -1
  20. package/dist/plugins.d.ts +40 -0
  21. package/dist/plugins.js.map +1 -1
  22. package/dist/runtime.d.ts +23 -77
  23. package/dist/runtime.js +149 -320
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/zudo-react/client.d.ts +3 -0
  26. package/dist/zudo-react/client.js +3 -0
  27. package/dist/zudo-react/client.js.map +1 -0
  28. package/dist/zudo-react/description.d.ts +16 -0
  29. package/dist/zudo-react/description.js +64 -0
  30. package/dist/zudo-react/description.js.map +1 -0
  31. package/dist/zudo-react/dom-bindings.d.ts +13 -0
  32. package/dist/zudo-react/dom-bindings.js +58 -0
  33. package/dist/zudo-react/dom-bindings.js.map +1 -0
  34. package/dist/zudo-react/escape.d.ts +2 -0
  35. package/dist/zudo-react/escape.js +7 -0
  36. package/dist/zudo-react/escape.js.map +1 -0
  37. package/dist/zudo-react/forms.d.ts +29 -0
  38. package/dist/zudo-react/forms.js +371 -0
  39. package/dist/zudo-react/forms.js.map +1 -0
  40. package/dist/zudo-react/hydrate.d.ts +4 -0
  41. package/dist/zudo-react/hydrate.js +909 -0
  42. package/dist/zudo-react/hydrate.js.map +1 -0
  43. package/dist/zudo-react/index.d.ts +47 -0
  44. package/dist/zudo-react/index.js +13 -0
  45. package/dist/zudo-react/index.js.map +1 -0
  46. package/dist/zudo-react/island-root-type.d.ts +1 -0
  47. package/dist/zudo-react/island-root-type.js +2 -0
  48. package/dist/zudo-react/island-root-type.js.map +1 -0
  49. package/dist/zudo-react/jsx-dev-runtime.d.ts +8 -0
  50. package/dist/zudo-react/jsx-dev-runtime.js +6 -0
  51. package/dist/zudo-react/jsx-dev-runtime.js.map +1 -0
  52. package/dist/zudo-react/jsx-runtime.d.ts +5 -0
  53. package/dist/zudo-react/jsx-runtime.js +7 -0
  54. package/dist/zudo-react/jsx-runtime.js.map +1 -0
  55. package/dist/zudo-react/jsx-types.d.ts +203 -0
  56. package/dist/zudo-react/jsx-types.js +2 -0
  57. package/dist/zudo-react/jsx-types.js.map +1 -0
  58. package/dist/zudo-react/props-transport.d.ts +2 -0
  59. package/dist/zudo-react/props-transport.js +95 -0
  60. package/dist/zudo-react/props-transport.js.map +1 -0
  61. package/dist/zudo-react/reactive-types.d.ts +8 -0
  62. package/dist/zudo-react/reactive-types.js +2 -0
  63. package/dist/zudo-react/reactive-types.js.map +1 -0
  64. package/dist/zudo-react/reactive.d.ts +20 -0
  65. package/dist/zudo-react/reactive.js +176 -0
  66. package/dist/zudo-react/reactive.js.map +1 -0
  67. package/dist/zudo-react/render-html.d.ts +3 -0
  68. package/dist/zudo-react/render-html.js +537 -0
  69. package/dist/zudo-react/render-html.js.map +1 -0
  70. package/dist/zudo-react/root.d.ts +20 -0
  71. package/dist/zudo-react/root.js +74 -0
  72. package/dist/zudo-react/root.js.map +1 -0
  73. package/dist/zudo-react/scheduler.d.ts +12 -0
  74. package/dist/zudo-react/scheduler.js +113 -0
  75. package/dist/zudo-react/scheduler.js.map +1 -0
  76. package/dist/zudo-react/scope.d.ts +42 -0
  77. package/dist/zudo-react/scope.js +220 -0
  78. package/dist/zudo-react/scope.js.map +1 -0
  79. package/dist/zudo-react/server.d.ts +15 -0
  80. package/dist/zudo-react/server.js +11 -0
  81. package/dist/zudo-react/server.js.map +1 -0
  82. package/dist/zudo-react/structure.d.ts +14 -0
  83. package/dist/zudo-react/structure.js +35 -0
  84. package/dist/zudo-react/structure.js.map +1 -0
  85. package/package.json +28 -24
package/dist/island.js CHANGED
@@ -1,137 +1,13 @@
1
- // Build-time `<Island when="...">` JSX wrapper.
2
- //
3
- // The wrapper is intentionally JSX-runtime-agnostic: it does not import
4
- // preact or react and never calls h() / createElement directly. Instead it
5
- // returns a plain object with a fixed shape that both Preact's and React's
6
- // jsx-runtime accept when the JSX transform turns the call site into a
7
- // jsx(Island, props) invocation.
8
- //
9
- // At build time, running through the SSR renderer (embedded V8 host):
10
- //
11
- // <Island when="visible"><Counter /></Island>
12
- //
13
- // renders as:
14
- //
15
- // <div data-zfb-island="Counter" data-when="visible">
16
- // <Counter />
17
- // </div>
18
- //
19
- // The component-name attribute (`data-zfb-island="ComponentName"`) is filled
20
- // in *here* by reading the child's JSX type identity (`displayName` first,
21
- // then `name`). Sub 3's hydration shim then `querySelectorAll`s these
22
- // markers and looks each one up in the islands manifest produced by the
23
- // scanner (see `crates/zfb-islands/src/manifest.rs` for the contract).
24
- //
25
- // SSR-skip mode: when the caller passes `ssrFallback`, the heavy child is
26
- // **not** evaluated at SSR time. Instead the wrapper emits a different
27
- // marker:
28
- //
29
- // <Island ssrFallback={<div>Loading…</div>}><HeavyClientOnly /></Island>
30
- // →
31
- // <div data-zfb-island-skip-ssr="HeavyClientOnly">
32
- // <div>Loading…</div>
33
- // </div>
34
- //
35
- // On the client the hydration runtime distinguishes hydrate vs. render by
36
- // which marker attribute is present. This is the equivalent of Astro's
37
- // `client:only="preact"`.
38
- //
39
- // When validation: in development we console.warn for unknown `when`
40
- // values and fall back to the default. In production we silently fall
41
- // back to keep the bundle path small.
42
- import { jsx } from "react/jsx-runtime";
1
+ // Build-time Island wrapper for the owned zudo-react runtime.
2
+ import { ownedIslandBoundary } from "./island-boundary.js";
43
3
  import { DEFAULT_WHEN, resolveWhen } from "./types.js";
44
- // Re-export `resolveWhen` for back-compat: tests and downstream consumers
45
- // historically imported it from `./island.js`. The implementation lives in
46
- // `./types.js` so the runtime scheduler can pull it in without dragging
47
- // the JSX wrapper along for the ride.
48
4
  export { resolveWhen } from "./types.js";
49
- /**
50
- * Marker attribute the SSR wrapper writes when the child component should
51
- * be hydrated client-side. The hydration runtime queries
52
- * `[data-${HYDRATE_MARKER_ATTR}]` to find islands.
53
- */
54
5
  export const HYDRATE_MARKER_ATTR = "data-zfb-island";
55
- /**
56
- * Marker attribute the SSR wrapper writes when SSR is being skipped (the
57
- * client:only-equivalent path). The hydration runtime queries
58
- * `[data-${SKIP_SSR_MARKER_ATTR}]` to find these placeholders and renders
59
- * the real component into them on hydration — there is no server output
60
- * to patch up.
61
- */
62
6
  export const SKIP_SSR_MARKER_ATTR = "data-zfb-island-skip-ssr";
63
- /** Fallback name surfaced when child identity cannot be determined. */
64
- export const ANONYMOUS_COMPONENT_NAME = "Anonymous";
65
- /**
66
- * Attribute the SSR wrapper writes to ferry the wrapped component's props
67
- * across the SSR → hydrate boundary. The hydration runtime parses this
68
- * with `JSON.parse` and forwards the result to the per-island `mount()`
69
- * call so the hydrated component sees the same props the SSR pass did.
70
- *
71
- * Omitted entirely when the wrapped child has no own data props (other
72
- * than `children`) — `readProps` already falls back to `{}` when the
73
- * attribute is missing, and emitting `data-props=""` would just bloat
74
- * the SSR markup.
75
- */
76
- export const PROPS_DATA_ATTR = "data-props";
77
- /**
78
- * `<Island>` JSX wrapper.
79
- *
80
- * Returns a JSX element shape compatible with both Preact and React. The
81
- * runtime tag is `"div"`. In the default (hydrate) mode the wrapper emits
82
- * `data-zfb-island="ComponentName"` and `data-when="<resolved-when>"`. In
83
- * SSR-skip mode (when `ssrFallback` is provided) it emits
84
- * `data-zfb-island-skip-ssr="ComponentName"` instead and renders the
85
- * fallback rather than the heavy child.
86
- *
87
- * The component-name string is derived from the child JSX element's type
88
- * identity (`type.displayName ?? type.name`). For string-typed children
89
- * (host elements) the tag name is used. If no usable identity can be
90
- * recovered, [`ANONYMOUS_COMPONENT_NAME`] is used so the marker still
91
- * lines up with the hydration shim's manifest lookup.
92
- *
93
- * The return type is the public [`IslandElement`] shape — the internal
94
- * VNode structure is deliberately not leaked so consumers never type-infer
95
- * through it.
96
- */
97
7
  export function Island(props) {
98
- const resolvedWhen = resolveMediaProps(props);
99
- const when = resolvedWhen.when;
100
- const media = resolvedWhen.media;
101
- const componentName = captureComponentName(props.children);
102
- const isSkipSsr = props.ssrFallback !== undefined;
103
- // Always source props from `props.children` (the heavy component VNode),
104
- // never from `ssrFallback` — the fallback is just SSR placeholder markup;
105
- // the hydrated component is the child, so its props are what `mount()`
106
- // needs at hydrate time. (Same rationale as `captureComponentName`.)
107
- const dataProps = captureSerializableProps(props.children);
108
- if (isSkipSsr) {
109
- const skipSsrProps = {
110
- [SKIP_SSR_MARKER_ATTR]: componentName,
111
- "data-when": when,
112
- };
113
- if (when === "media" && media !== undefined)
114
- skipSsrProps["data-media"] = media;
115
- if (dataProps !== undefined)
116
- skipSsrProps[PROPS_DATA_ATTR] = dataProps;
117
- return makeWrapper(skipSsrProps, props.ssrFallback ?? null);
118
- }
119
- const hydrateProps = {
120
- [HYDRATE_MARKER_ATTR]: componentName,
121
- "data-when": when,
122
- };
123
- if (when === "media" && media !== undefined)
124
- hydrateProps["data-media"] = media;
125
- if (dataProps !== undefined)
126
- hydrateProps[PROPS_DATA_ATTR] = dataProps;
127
- return makeWrapper(hydrateProps, props.children);
8
+ const { when, media } = resolveMediaProps(props);
9
+ return ownedIslandBoundary(props.children, props.ssrFallback, when, media);
128
10
  }
129
- /**
130
- * Validate and resolve the `when` / `media` props together.
131
- *
132
- * - `when="media"` without `media` → warn + fall back to `DEFAULT_WHEN`.
133
- * - `media` without `when="media"` → warn-and-ignore; `media` is dropped.
134
- */
135
11
  function resolveMediaProps(props) {
136
12
  const when = resolveWhen(props.when);
137
13
  const media = props.media;
@@ -153,160 +29,4 @@ function resolveMediaProps(props) {
153
29
  }
154
30
  return { when, media };
155
31
  }
156
- /**
157
- * Construct the wrapper element through the **automatic JSX runtime
158
- * factory**, called explicitly so this file stays plain `.ts` (no JSX
159
- * syntax).
160
- *
161
- * Why the explicit `jsx(...)` call rather than a hand-rolled
162
- * `{ type, props, key }` object literal: a plain literal is only valid for
163
- * Preact (whose diff path / `preact-render-to-string` recognise a VNode by
164
- * `vnode.constructor === undefined`). React's renderer rejects such an
165
- * object as a child with "Objects are not valid as a React child"
166
- * (minified error #31) because a real React element carries
167
- * `$$typeof: Symbol.for("react.element")`, which a literal cannot fake
168
- * portably. Calling `jsx` from `react/jsx-runtime` mints a real element:
169
- * in React mode it resolves natively; in Preact mode the engine rewrites
170
- * `react/jsx-runtime` → `preact/jsx-runtime` (bundler.rs ~2886), so the
171
- * Preact runtime mints the element. Same result the JSX `<div>` delegation
172
- * produced — but as plain `.ts`.
173
- *
174
- * Why `.ts` and NOT `.tsx`: esbuild rewrites a `.js` import specifier to
175
- * `.ts` but NOT to `.tsx`. The barrel `index.ts` imports `./island.js`;
176
- * when this file was `island.tsx`, source consumers (the dev `exports`
177
- * point at `src/*.ts`, e.g. the node-free template / smoke path) failed
178
- * with `Could not resolve "./island.js"`. The published `dist` worked
179
- * (tsc emits a real `island.js`), which masked the regression in
180
- * pnpm-pack/dist probes. Keeping this a `.ts` file makes the barrel resolve
181
- * `./island.js` → `./island.ts` from source everywhere, while tsc still
182
- * emits `dist/island.js` for the published package.
183
- *
184
- * The cast back to `IslandElement` keeps the public type tight; the
185
- * concrete element shape produced by the runtime is invisible to type
186
- * consumers.
187
- */
188
- function makeWrapper(props, children) {
189
- return jsx("div", { ...props, children });
190
- }
191
- /**
192
- * Pull a component-name string out of a JSX child.
193
- *
194
- * Both Preact and React store rendered VNodes as plain objects whose
195
- * `.type` field is either:
196
- * - the component function (look at `displayName ?? name`),
197
- * - or the host element tag name as a string.
198
- *
199
- * If `children` is an array (multiple children), the first child with a
200
- * usable identity wins. This is intentional: the typical island shape is
201
- * `<Island><Foo /></Island>` (single child); when the caller wraps a
202
- * fragment-like list we still want a deterministic, debuggable name.
203
- *
204
- * Exported for tests; not re-exported from `index.ts`.
205
- */
206
- export function captureComponentName(children) {
207
- if (Array.isArray(children)) {
208
- for (const child of children) {
209
- const name = nameFromSingle(child);
210
- if (name)
211
- return name;
212
- }
213
- return ANONYMOUS_COMPONENT_NAME;
214
- }
215
- return nameFromSingle(children) || ANONYMOUS_COMPONENT_NAME;
216
- }
217
- function nameFromSingle(child) {
218
- if (!child || typeof child !== "object")
219
- return "";
220
- const c = child;
221
- const t = c.type;
222
- if (typeof t === "function") {
223
- const fn = t;
224
- if (typeof fn.displayName === "string" && fn.displayName)
225
- return fn.displayName;
226
- if (typeof fn.name === "string" && fn.name)
227
- return fn.name;
228
- return "";
229
- }
230
- if (typeof t === "string" && t)
231
- return t;
232
- return "";
233
- }
234
- /**
235
- * Serialize the wrapped child's data props as a JSON string the runtime
236
- * can parse out of the `data-props` attribute on the Island marker div.
237
- *
238
- * Mirrors [`captureComponentName`]'s array handling: when `children` is
239
- * an array (multiple JSX siblings), the first child whose own props
240
- * yield a non-empty serialization wins. This keeps the "first
241
- * identifiable child" contract consistent across both attributes —
242
- * whatever the marker name points at is what the data-props payload
243
- * describes.
244
- *
245
- * Returns `undefined` (not `"{}"` and not `""`) when no usable props
246
- * exist. The runtime's `readProps` already maps a missing attribute to
247
- * `{}`, so omitting the attribute keeps the SSR markup smaller and
248
- * preserves the invariant that the attribute, when present, always
249
- * parses to a non-empty record.
250
- *
251
- * Exported for tests; not re-exported from `index.ts`.
252
- */
253
- export function captureSerializableProps(children) {
254
- if (Array.isArray(children)) {
255
- for (const child of children) {
256
- const json = propsFromSingle(child);
257
- if (json !== undefined)
258
- return json;
259
- }
260
- return undefined;
261
- }
262
- return propsFromSingle(children);
263
- }
264
- function propsFromSingle(child) {
265
- if (!child || typeof child !== "object")
266
- return undefined;
267
- const c = child;
268
- const raw = c.props;
269
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
270
- return undefined;
271
- // Exclude `children` from the serialized payload: the SSR pass already
272
- // emitted the rendered children into the DOM (the hydration target),
273
- // and JSX child nodes are typically VNode trees with non-serializable
274
- // shapes (functions, circular refs) that would either bloat the
275
- // payload or throw inside JSON.stringify. The hydration runtime
276
- // re-renders into the existing DOM, so it does not need the JSX
277
- // children replayed through props.
278
- //
279
- // Note: JSON.stringify already silently drops function / symbol /
280
- // undefined values from the output, so those don't need explicit
281
- // pre-filtering here.
282
- const propsRecord = raw;
283
- let hasOwn = false;
284
- const filtered = {};
285
- for (const key of Object.keys(propsRecord)) {
286
- if (key === "children")
287
- continue;
288
- filtered[key] = propsRecord[key];
289
- hasOwn = true;
290
- }
291
- if (!hasOwn)
292
- return undefined;
293
- let json;
294
- try {
295
- json = JSON.stringify(filtered);
296
- }
297
- catch {
298
- // Circular references, BigInt, or any other non-serializable input
299
- // — silently fall through (no `data-props`) rather than ship a
300
- // partial payload. The runtime already handles the missing-attribute
301
- // case by returning `{}` from `readProps`.
302
- return undefined;
303
- }
304
- // JSON.stringify can also return `undefined` (when the top-level value
305
- // serializes to nothing) or `"{}"` (when every key was a function /
306
- // symbol / undefined and got dropped). Treat both as "nothing useful
307
- // to ship" so the marker stays clean.
308
- if (json === undefined || json === "{}")
309
- return undefined;
310
- return json;
311
- }
312
32
  //# sourceMappingURL=island.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"island.js","sourceRoot":"","sources":["../src/island.ts"],"names":[],"mappings":"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","sourcesContent":["// Build-time `<Island when=\"...\">` JSX wrapper.\n//\n// The wrapper is intentionally JSX-runtime-agnostic: it does not import\n// preact or react and never calls h() / createElement directly. Instead it\n// returns a plain object with a fixed shape that both Preact's and React's\n// jsx-runtime accept when the JSX transform turns the call site into a\n// jsx(Island, props) invocation.\n//\n// At build time, running through the SSR renderer (embedded V8 host):\n//\n// <Island when=\"visible\"><Counter /></Island>\n//\n// renders as:\n//\n// <div data-zfb-island=\"Counter\" data-when=\"visible\">\n// <Counter />\n// </div>\n//\n// The component-name attribute (`data-zfb-island=\"ComponentName\"`) is filled\n// in *here* by reading the child's JSX type identity (`displayName` first,\n// then `name`). Sub 3's hydration shim then `querySelectorAll`s these\n// markers and looks each one up in the islands manifest produced by the\n// scanner (see `crates/zfb-islands/src/manifest.rs` for the contract).\n//\n// SSR-skip mode: when the caller passes `ssrFallback`, the heavy child is\n// **not** evaluated at SSR time. Instead the wrapper emits a different\n// marker:\n//\n// <Island ssrFallback={<div>Loading…</div>}><HeavyClientOnly /></Island>\n// →\n// <div data-zfb-island-skip-ssr=\"HeavyClientOnly\">\n// <div>Loading…</div>\n// </div>\n//\n// On the client the hydration runtime distinguishes hydrate vs. render by\n// which marker attribute is present. This is the equivalent of Astro's\n// `client:only=\"preact\"`.\n//\n// When validation: in development we console.warn for unknown `when`\n// values and fall back to the default. In production we silently fall\n// back to keep the bundle path small.\n\nimport { jsx } from \"react/jsx-runtime\";\n\nimport type { VNode } from \"./jsx-types.js\";\nimport { DEFAULT_WHEN, resolveWhen, type When } from \"./types.js\";\n\n// Re-export `resolveWhen` for back-compat: tests and downstream consumers\n// historically imported it from `./island.js`. The implementation lives in\n// `./types.js` so the runtime scheduler can pull it in without dragging\n// the JSX wrapper along for the ride.\nexport { resolveWhen } from \"./types.js\";\n\n/**\n * Marker attribute the SSR wrapper writes when the child component should\n * be hydrated client-side. The hydration runtime queries\n * `[data-${HYDRATE_MARKER_ATTR}]` to find islands.\n */\nexport const HYDRATE_MARKER_ATTR = \"data-zfb-island\";\n\n/**\n * Marker attribute the SSR wrapper writes when SSR is being skipped (the\n * client:only-equivalent path). The hydration runtime queries\n * `[data-${SKIP_SSR_MARKER_ATTR}]` to find these placeholders and renders\n * the real component into them on hydration — there is no server output\n * to patch up.\n */\nexport const SKIP_SSR_MARKER_ATTR = \"data-zfb-island-skip-ssr\";\n\n/** Fallback name surfaced when child identity cannot be determined. */\nexport const ANONYMOUS_COMPONENT_NAME = \"Anonymous\";\n\n/**\n * Attribute the SSR wrapper writes to ferry the wrapped component's props\n * across the SSR → hydrate boundary. The hydration runtime parses this\n * with `JSON.parse` and forwards the result to the per-island `mount()`\n * call so the hydrated component sees the same props the SSR pass did.\n *\n * Omitted entirely when the wrapped child has no own data props (other\n * than `children`) — `readProps` already falls back to `{}` when the\n * attribute is missing, and emitting `data-props=\"\"` would just bloat\n * the SSR markup.\n */\nexport const PROPS_DATA_ATTR = \"data-props\";\n\n/** Props for `<Island>`. */\nexport interface IslandProps {\n /** Hydration scheduling strategy. Defaults to `\"load\"`. */\n when?: When;\n /**\n * CSS media query string for `when=\"media\"` islands. The island hydrates\n * when this query first matches (including later viewport changes). The\n * runtime uses `window.matchMedia(media)` to register a listener.\n *\n * Ignored when `when` is not `\"media\"` (a dev warning is emitted).\n * Required when `when === \"media\"` (omitting it falls back to DEFAULT_WHEN\n * with a dev warning).\n */\n media?: string;\n /**\n * If supplied, switches the island into SSR-skip mode (Astro's\n * `client:only` equivalent). The wrapper emits the\n * `data-zfb-island-skip-ssr` marker, the heavy `children` are **not**\n * evaluated server-side, and `ssrFallback` is rendered in their place.\n * On hydration the client runtime swaps in the real component.\n */\n ssrFallback?: VNode;\n /**\n * Server-rendered children, hydrated client-side once `when` fires.\n *\n * Typed as `VNode` (structural union) rather than `ReactNode` so\n * non-React frameworks can implement `IslandProps` without a React\n * type dependency (BCI-4).\n */\n children?: VNode;\n}\n\n/**\n * Public JSX-element shape returned by [`Island`]. Intentionally widened\n * to a structural type so consumers don't infer through the internal\n * `{ type, props, key }` VNode shape of either Preact or React. Both\n * jsx-runtimes accept this object on either side of the boundary.\n */\nexport type IslandElement = {\n readonly type: string;\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/**\n * `<Island>` JSX wrapper.\n *\n * Returns a JSX element shape compatible with both Preact and React. The\n * runtime tag is `\"div\"`. In the default (hydrate) mode the wrapper emits\n * `data-zfb-island=\"ComponentName\"` and `data-when=\"<resolved-when>\"`. In\n * SSR-skip mode (when `ssrFallback` is provided) it emits\n * `data-zfb-island-skip-ssr=\"ComponentName\"` instead and renders the\n * fallback rather than the heavy child.\n *\n * The component-name string is derived from the child JSX element's type\n * identity (`type.displayName ?? type.name`). For string-typed children\n * (host elements) the tag name is used. If no usable identity can be\n * recovered, [`ANONYMOUS_COMPONENT_NAME`] is used so the marker still\n * lines up with the hydration shim's manifest lookup.\n *\n * The return type is the public [`IslandElement`] shape — the internal\n * VNode structure is deliberately not leaked so consumers never type-infer\n * through it.\n */\nexport function Island(props: IslandProps): IslandElement {\n const resolvedWhen = resolveMediaProps(props);\n const when = resolvedWhen.when;\n const media = resolvedWhen.media;\n const componentName = captureComponentName(props.children);\n const isSkipSsr = props.ssrFallback !== undefined;\n // Always source props from `props.children` (the heavy component VNode),\n // never from `ssrFallback` — the fallback is just SSR placeholder markup;\n // the hydrated component is the child, so its props are what `mount()`\n // needs at hydrate time. (Same rationale as `captureComponentName`.)\n const dataProps = captureSerializableProps(props.children);\n\n if (isSkipSsr) {\n const skipSsrProps: Record<string, unknown> = {\n [SKIP_SSR_MARKER_ATTR]: componentName,\n \"data-when\": when,\n };\n if (when === \"media\" && media !== undefined) skipSsrProps[\"data-media\"] = media;\n if (dataProps !== undefined) skipSsrProps[PROPS_DATA_ATTR] = dataProps;\n return makeWrapper(skipSsrProps, props.ssrFallback ?? null);\n }\n\n const hydrateProps: Record<string, unknown> = {\n [HYDRATE_MARKER_ATTR]: componentName,\n \"data-when\": when,\n };\n if (when === \"media\" && media !== undefined) hydrateProps[\"data-media\"] = media;\n if (dataProps !== undefined) hydrateProps[PROPS_DATA_ATTR] = dataProps;\n return makeWrapper(hydrateProps, props.children);\n}\n\n/**\n * Validate and resolve the `when` / `media` props together.\n *\n * - `when=\"media\"` without `media` → warn + fall back to `DEFAULT_WHEN`.\n * - `media` without `when=\"media\"` → warn-and-ignore; `media` is dropped.\n */\nfunction resolveMediaProps(props: IslandProps): { when: When; media: string | undefined } {\n const when = resolveWhen(props.when);\n const media = props.media;\n\n if (when === \"media\" && !media) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] <Island when=\"media\"> requires the \\`media\\` prop (a CSS media query string). ` +\n `Falling back to \"${DEFAULT_WHEN}\".`,\n );\n }\n return { when: DEFAULT_WHEN, media: undefined };\n }\n\n if (media && when !== \"media\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] The \\`media\\` prop is only used when \\`when=\"media\"\\`. ` +\n `Current when=\"${when}\" — \\`media\\` will be ignored.`,\n );\n }\n return { when, media: undefined };\n }\n\n return { when, media };\n}\n\n/**\n * Construct the wrapper element through the **automatic JSX runtime\n * factory**, called explicitly so this file stays plain `.ts` (no JSX\n * syntax).\n *\n * Why the explicit `jsx(...)` call rather than a hand-rolled\n * `{ type, props, key }` object literal: a plain literal is only valid for\n * Preact (whose diff path / `preact-render-to-string` recognise a VNode by\n * `vnode.constructor === undefined`). React's renderer rejects such an\n * object as a child with \"Objects are not valid as a React child\"\n * (minified error #31) because a real React element carries\n * `$$typeof: Symbol.for(\"react.element\")`, which a literal cannot fake\n * portably. Calling `jsx` from `react/jsx-runtime` mints a real element:\n * in React mode it resolves natively; in Preact mode the engine rewrites\n * `react/jsx-runtime` → `preact/jsx-runtime` (bundler.rs ~2886), so the\n * Preact runtime mints the element. Same result the JSX `<div>` delegation\n * produced — but as plain `.ts`.\n *\n * Why `.ts` and NOT `.tsx`: esbuild rewrites a `.js` import specifier to\n * `.ts` but NOT to `.tsx`. The barrel `index.ts` imports `./island.js`;\n * when this file was `island.tsx`, source consumers (the dev `exports`\n * point at `src/*.ts`, e.g. the node-free template / smoke path) failed\n * with `Could not resolve \"./island.js\"`. The published `dist` worked\n * (tsc emits a real `island.js`), which masked the regression in\n * pnpm-pack/dist probes. Keeping this a `.ts` file makes the barrel resolve\n * `./island.js` → `./island.ts` from source everywhere, while tsc still\n * emits `dist/island.js` for the published package.\n *\n * The cast back to `IslandElement` keeps the public type tight; the\n * concrete element shape produced by the runtime is invisible to type\n * consumers.\n */\nfunction makeWrapper(props: Record<string, unknown>, children: unknown): IslandElement {\n return jsx(\"div\", { ...props, children }) as unknown as IslandElement;\n}\n\n/**\n * Pull a component-name string out of a JSX child.\n *\n * Both Preact and React store rendered VNodes as plain objects whose\n * `.type` field is either:\n * - the component function (look at `displayName ?? name`),\n * - or the host element tag name as a string.\n *\n * If `children` is an array (multiple children), the first child with a\n * usable identity wins. This is intentional: the typical island shape is\n * `<Island><Foo /></Island>` (single child); when the caller wraps a\n * fragment-like list we still want a deterministic, debuggable name.\n *\n * Exported for tests; not re-exported from `index.ts`.\n */\nexport function captureComponentName(children: unknown): string {\n if (Array.isArray(children)) {\n for (const child of children) {\n const name = nameFromSingle(child);\n if (name) return name;\n }\n return ANONYMOUS_COMPONENT_NAME;\n }\n return nameFromSingle(children) || ANONYMOUS_COMPONENT_NAME;\n}\n\nfunction nameFromSingle(child: unknown): string {\n if (!child || typeof child !== \"object\") return \"\";\n const c = child as { type?: unknown };\n const t = c.type;\n if (typeof t === \"function\") {\n const fn = t as { displayName?: unknown; name?: unknown };\n if (typeof fn.displayName === \"string\" && fn.displayName) return fn.displayName;\n if (typeof fn.name === \"string\" && fn.name) return fn.name;\n return \"\";\n }\n if (typeof t === \"string\" && t) return t;\n return \"\";\n}\n\n/**\n * Serialize the wrapped child's data props as a JSON string the runtime\n * can parse out of the `data-props` attribute on the Island marker div.\n *\n * Mirrors [`captureComponentName`]'s array handling: when `children` is\n * an array (multiple JSX siblings), the first child whose own props\n * yield a non-empty serialization wins. This keeps the \"first\n * identifiable child\" contract consistent across both attributes —\n * whatever the marker name points at is what the data-props payload\n * describes.\n *\n * Returns `undefined` (not `\"{}\"` and not `\"\"`) when no usable props\n * exist. The runtime's `readProps` already maps a missing attribute to\n * `{}`, so omitting the attribute keeps the SSR markup smaller and\n * preserves the invariant that the attribute, when present, always\n * parses to a non-empty record.\n *\n * Exported for tests; not re-exported from `index.ts`.\n */\nexport function captureSerializableProps(children: unknown): string | undefined {\n if (Array.isArray(children)) {\n for (const child of children) {\n const json = propsFromSingle(child);\n if (json !== undefined) return json;\n }\n return undefined;\n }\n return propsFromSingle(children);\n}\n\nfunction propsFromSingle(child: unknown): string | undefined {\n if (!child || typeof child !== \"object\") return undefined;\n const c = child as { props?: unknown };\n const raw = c.props;\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n\n // Exclude `children` from the serialized payload: the SSR pass already\n // emitted the rendered children into the DOM (the hydration target),\n // and JSX child nodes are typically VNode trees with non-serializable\n // shapes (functions, circular refs) that would either bloat the\n // payload or throw inside JSON.stringify. The hydration runtime\n // re-renders into the existing DOM, so it does not need the JSX\n // children replayed through props.\n //\n // Note: JSON.stringify already silently drops function / symbol /\n // undefined values from the output, so those don't need explicit\n // pre-filtering here.\n const propsRecord = raw as Record<string, unknown>;\n let hasOwn = false;\n const filtered: Record<string, unknown> = {};\n for (const key of Object.keys(propsRecord)) {\n if (key === \"children\") continue;\n filtered[key] = propsRecord[key];\n hasOwn = true;\n }\n if (!hasOwn) return undefined;\n\n let json: string | undefined;\n try {\n json = JSON.stringify(filtered);\n } catch {\n // Circular references, BigInt, or any other non-serializable input\n // — silently fall through (no `data-props`) rather than ship a\n // partial payload. The runtime already handles the missing-attribute\n // case by returning `{}` from `readProps`.\n return undefined;\n }\n\n // JSON.stringify can also return `undefined` (when the top-level value\n // serializes to nothing) or `\"{}\"` (when every key was a function /\n // symbol / undefined and got dropped). Treat both as \"nothing useful\n // to ship\" so the marker stays clean.\n if (json === undefined || json === \"{}\") return undefined;\n return json;\n}\n"]}
1
+ {"version":3,"file":"island.js","sourceRoot":"","sources":["../src/island.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACrD,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAW/D,MAAM,UAAU,MAAM,CAAC,KAAkB;IACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,OAAO,mBAAmB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAkB;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAE1B,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,sFAAsF;gBACpF,oBAAoB,YAAY,IAAI,CACvC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAClD,CAAC;IAED,IAAI,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC9B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,+DAA+D;gBAC7D,iBAAiB,IAAI,gCAAgC,CACxD,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC","sourcesContent":["// Build-time Island wrapper for the owned zudo-react runtime.\nimport { ownedIslandBoundary } from \"./island-boundary.js\";\nimport type { VNode } from \"./jsx-types.js\";\nimport type { Description } from \"./zudo-react/description.js\";\nimport { DEFAULT_WHEN, resolveWhen, type When } from \"./types.js\";\n\nexport { resolveWhen } from \"./types.js\";\nexport const HYDRATE_MARKER_ATTR = \"data-zfb-island\";\nexport const SKIP_SSR_MARKER_ATTR = \"data-zfb-island-skip-ssr\";\n\nexport interface IslandProps {\n when?: When;\n media?: string;\n ssrFallback?: VNode;\n children?: VNode;\n}\n\nexport type IslandElement = Description;\n\nexport function Island(props: IslandProps): IslandElement {\n const { when, media } = resolveMediaProps(props);\n return ownedIslandBoundary(props.children, props.ssrFallback, when, media);\n}\n\nfunction resolveMediaProps(props: IslandProps): { when: When; media: string | undefined } {\n const when = resolveWhen(props.when);\n const media = props.media;\n\n if (when === \"media\" && !media) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] <Island when=\"media\"> requires the \\`media\\` prop (a CSS media query string). ` +\n `Falling back to \"${DEFAULT_WHEN}\".`,\n );\n }\n return { when: DEFAULT_WHEN, media: undefined };\n }\n\n if (media && when !== \"media\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] The \\`media\\` prop is only used when \\`when=\"media\"\\`. ` +\n `Current when=\"${when}\" — \\`media\\` will be ignored.`,\n );\n }\n return { when, media: undefined };\n }\n\n return { when, media };\n}\n"]}
@@ -1,46 +1,10 @@
1
- /**
2
- * Structural JSX element (VNode). Framework-agnostic: both Preact and React
3
- * produce objects that satisfy this shape when a JSX element is rendered.
4
- *
5
- * The `props` bag may itself contain `children` (nested VNodes) as well as
6
- * HTML attribute values (strings, numbers, booleans). All fields are widened
7
- * to `unknown` so the type stays opaque to callers that don't know which
8
- * framework produced the value.
9
- *
10
- * `type` accepts BOTH function components (call signature) AND class
11
- * components (construct signature). Preact and React both expose
12
- * `ComponentType = FunctionComponent | ComponentClass`, and the JSX runtime
13
- * stores either on `.type`. Without the construct signature, valid Preact
14
- * `<MyComponent />` expressions fail to assign to `VNode` at the framework
15
- * boundary (e.g. inside `<Island>` children) — even though class components
16
- * are rare in modern Preact, the `ComponentType` union includes them.
17
- */
1
+ /** Opaque description-shaped object accepted at the SDK boundary. */
18
2
  export type VNodeObject = {
19
3
  readonly type: string | ((...args: unknown[]) => unknown) | (new (...args: unknown[]) => unknown);
20
4
  readonly props: Readonly<Record<string, unknown>>;
21
5
  readonly key: unknown;
22
6
  };
23
- /**
24
- * A renderable JSX node: a primitive, `null`, `undefined`, an array, a
25
- * structured VNode object, or a bare `object`.
26
- *
27
- * This union covers every value both Preact and React treat as valid
28
- * `children`. The bare `object` member is required to make Preact's
29
- * `ComponentChildren` / `VNode<{}>` assignable without casts — Preact's own
30
- * `ComponentChild` uses the same widening pattern. The loosening is safe
31
- * because all SDK runtime code duck-types `unknown` at the boundary
32
- * (island.ts, content.ts) and the Rust pipeline is type-erased.
33
- *
34
- * Importantly, the union does **not** reference any framework-specific type,
35
- * so packages that depend on `zfb` need not install a React / Preact `@types`
36
- * package to satisfy this type.
37
- *
38
- * **Name-collision note for Preact consumers:** if your file already has
39
- * `import { VNode } from "preact"`, use a qualifier on the zfb import:
40
- * `import type { VNode as ZfbVNode } from "@takazudo/zfb"`.
41
- */
7
+ /** Broad authored child input; the owned renderer validates actual children. */
42
8
  export type VNode = string | number | boolean | null | undefined | bigint | VNodeArray | VNodeObject | object;
43
9
  export interface VNodeArray extends ReadonlyArray<VNode> {
44
10
  }
45
- /** @deprecated Use `VNode` instead. */
46
- export type ReactNode = VNode;
package/dist/jsx-types.js CHANGED
@@ -1,12 +1,5 @@
1
- // Lightweight ambient JSX types so the public Island wrapper does not
2
- // hard-depend on either preact or react as a runtime/types dependency.
3
- //
4
- // BCI-4: `IslandProps.children` is now typed as `VNode` (a structural union)
5
- // rather than the old `ReactNode` alias, so non-React frameworks can
6
- // implement `IslandProps` without any React type dependency at the type level.
7
- //
8
- // Both Preact and React's `children` accept this structural union; the build
9
- // is type-erased so the actual rendering happens in the user's app under
10
- // whichever framework adapter they chose.
1
+ // Structural input types used by SDK helpers that inspect authored children.
2
+ // The owned Island boundary validates descriptions and component identity
3
+ // at runtime before rendering.
11
4
  export {};
12
5
  //# sourceMappingURL=jsx-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-types.js","sourceRoot":"","sources":["../src/jsx-types.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,uEAAuE;AACvE,EAAE;AACF,6EAA6E;AAC7E,qEAAqE;AACrE,+EAA+E;AAC/E,EAAE;AACF,6EAA6E;AAC7E,yEAAyE;AACzE,0CAA0C","sourcesContent":["// Lightweight ambient JSX types so the public Island wrapper does not\n// hard-depend on either preact or react as a runtime/types dependency.\n//\n// BCI-4: `IslandProps.children` is now typed as `VNode` (a structural union)\n// rather than the old `ReactNode` alias, so non-React frameworks can\n// implement `IslandProps` without any React type dependency at the type level.\n//\n// Both Preact and React's `children` accept this structural union; the build\n// is type-erased so the actual rendering happens in the user's app under\n// whichever framework adapter they chose.\n\n/**\n * Structural JSX element (VNode). Framework-agnostic: both Preact and React\n * produce objects that satisfy this shape when a JSX element is rendered.\n *\n * The `props` bag may itself contain `children` (nested VNodes) as well as\n * HTML attribute values (strings, numbers, booleans). All fields are widened\n * to `unknown` so the type stays opaque to callers that don't know which\n * framework produced the value.\n *\n * `type` accepts BOTH function components (call signature) AND class\n * components (construct signature). Preact and React both expose\n * `ComponentType = FunctionComponent | ComponentClass`, and the JSX runtime\n * stores either on `.type`. Without the construct signature, valid Preact\n * `<MyComponent />` expressions fail to assign to `VNode` at the framework\n * boundary (e.g. inside `<Island>` children) — even though class components\n * are rare in modern Preact, the `ComponentType` union includes them.\n */\nexport type VNodeObject = {\n readonly type: string | ((...args: unknown[]) => unknown) | (new (...args: unknown[]) => unknown);\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/**\n * A renderable JSX node: a primitive, `null`, `undefined`, an array, a\n * structured VNode object, or a bare `object`.\n *\n * This union covers every value both Preact and React treat as valid\n * `children`. The bare `object` member is required to make Preact's\n * `ComponentChildren` / `VNode<{}>` assignable without casts — Preact's own\n * `ComponentChild` uses the same widening pattern. The loosening is safe\n * because all SDK runtime code duck-types `unknown` at the boundary\n * (island.ts, content.ts) and the Rust pipeline is type-erased.\n *\n * Importantly, the union does **not** reference any framework-specific type,\n * so packages that depend on `zfb` need not install a React / Preact `@types`\n * package to satisfy this type.\n *\n * **Name-collision note for Preact consumers:** if your file already has\n * `import { VNode } from \"preact\"`, use a qualifier on the zfb import:\n * `import type { VNode as ZfbVNode } from \"@takazudo/zfb\"`.\n */\nexport type VNode =\n | string\n | number\n | boolean\n | null\n | undefined\n | bigint\n | VNodeArray\n | VNodeObject\n | object;\n\nexport interface VNodeArray extends ReadonlyArray<VNode> {}\n\n// Back-compat alias — `ReactNode` was the previous name; code inside this\n// package that has not been migrated yet can still refer to it. New code\n// should use `VNode` directly.\n/** @deprecated Use `VNode` instead. */\nexport type ReactNode = VNode;\n"]}
1
+ {"version":3,"file":"jsx-types.js","sourceRoot":"","sources":["../src/jsx-types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,0EAA0E;AAC1E,+BAA+B","sourcesContent":["// Structural input types used by SDK helpers that inspect authored children.\n// The owned Island boundary validates descriptions and component identity\n// at runtime before rendering.\n\n/** Opaque description-shaped object accepted at the SDK boundary. */\nexport type VNodeObject = {\n readonly type: string | ((...args: unknown[]) => unknown) | (new (...args: unknown[]) => unknown);\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/** Broad authored child input; the owned renderer validates actual children. */\nexport type VNode =\n | string\n | number\n | boolean\n | null\n | undefined\n | bigint\n | VNodeArray\n | VNodeObject\n | object;\n\nexport interface VNodeArray extends ReadonlyArray<VNode> {}\n"]}
package/dist/plugins.d.ts CHANGED
@@ -63,6 +63,16 @@ export type ZfbRouteManifest = {
63
63
  export type ZfbBuildHookContext = {
64
64
  /** Project root — the directory containing `zfb.config.ts`. */
65
65
  projectRoot: string;
66
+ /**
67
+ * Absolute directory for the plugin's opaque intermediates
68
+ * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by
69
+ * default). It is not an importable route location: generated route
70
+ * modules placed here are not staged into the bundle. zfb does not
71
+ * create it, so `mkdir(scratchDir, { recursive: true })` and use a
72
+ * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,
73
+ * plugin isolation only holds if the plugin adopts this directory.
74
+ */
75
+ scratchDir: string;
66
76
  /** Resolved absolute path of the build output directory. */
67
77
  outDir: string;
68
78
  /** The full loaded `ZfbConfig` (data-only view). */
@@ -118,6 +128,16 @@ export type ZfbDevMiddlewareHandler = (req: ZfbDevMiddlewareRequest) => Promise<
118
128
  */
119
129
  export type ZfbDevMiddlewareContext = {
120
130
  projectRoot: string;
131
+ /**
132
+ * Absolute directory for the plugin's opaque intermediates
133
+ * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by
134
+ * default). It is not an importable route location: generated route
135
+ * modules placed here are not staged into the bundle. zfb does not
136
+ * create it, so `mkdir(scratchDir, { recursive: true })` and use a
137
+ * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,
138
+ * plugin isolation only holds if the plugin adopts this directory.
139
+ */
140
+ scratchDir: string;
121
141
  config: import("./config.js").ZfbConfig;
122
142
  options: Record<string, unknown>;
123
143
  logger: ZfbPluginLogger;
@@ -150,6 +170,16 @@ export type ZfbPreviewMiddlewareHandler = (req: ZfbDevMiddlewareRequest) => Prom
150
170
  */
151
171
  export type ZfbPreviewMiddlewareContext = {
152
172
  projectRoot: string;
173
+ /**
174
+ * Absolute directory for the plugin's opaque intermediates
175
+ * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by
176
+ * default). It is not an importable route location: generated route
177
+ * modules placed here are not staged into the bundle. zfb does not
178
+ * create it, so `mkdir(scratchDir, { recursive: true })` and use a
179
+ * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,
180
+ * plugin isolation only holds if the plugin adopts this directory.
181
+ */
182
+ scratchDir: string;
153
183
  config: import("./config.js").ZfbConfig;
154
184
  options: Record<string, unknown>;
155
185
  logger: ZfbPluginLogger;
@@ -258,6 +288,16 @@ export type ZfbSetupContext = {
258
288
  command: "build" | "dev" | "preview";
259
289
  /** Project root — the directory containing `zfb.config.ts`. */
260
290
  projectRoot: string;
291
+ /**
292
+ * Absolute directory for the plugin's opaque intermediates
293
+ * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by
294
+ * default). It is not an importable route location: generated route
295
+ * modules placed here are not staged into the bundle. zfb does not
296
+ * create it, so `mkdir(scratchDir, { recursive: true })` and use a
297
+ * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,
298
+ * plugin isolation only holds if the plugin adopts this directory.
299
+ */
300
+ scratchDir: string;
261
301
  /** The full loaded `ZfbConfig` (data-only view). */
262
302
  config: import("./config.js").ZfbConfig;
263
303
  /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */
@@ -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;AA6b/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 * A `to` that resolves to a single existing file is also applied\n * to importers inside `node_modules`, via esbuild `--alias`. A\n * directory-shaped or missing `to` is applied through tsconfig\n * `paths` only, which esbuild does not honor for `node_modules`\n * importers — packages that need an alias to work from inside\n * `node_modules` should register one alias per file, or use\n * `addVirtualModule`. A `from` that is a slash-prefix of another\n * alias, of a virtual module specifier, or of a reserved name\n * such as `zfb` gets no `--alias` flag.\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 * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`\n * endpoint and raise `ReservedRoutePrefix` with this plugin's name.\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;AAqe/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 /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: 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 /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: 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 /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: 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 /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: 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 * A `to` that resolves to a single existing file is also applied\n * to importers inside `node_modules`, via esbuild `--alias`. A\n * directory-shaped or missing `to` is applied through tsconfig\n * `paths` only, which esbuild does not honor for `node_modules`\n * importers — packages that need an alias to work from inside\n * `node_modules` should register one alias per file, or use\n * `addVirtualModule`. A `from` that is a slash-prefix of another\n * alias, of a virtual module specifier, or of a reserved name\n * such as `zfb` gets no `--alias` flag.\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 * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`\n * endpoint and raise `ReservedRoutePrefix` with this plugin's name.\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"]}