@barefootjs/cli 0.31.5 → 0.31.7

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.
@@ -24,6 +24,8 @@ error[BF001]: 'use client' directive required for components with createSignal
24
24
 
25
25
  ## Directive Errors (BF001–BF003)
26
26
 
27
+ <a id="bf001"></a>
28
+
27
29
  ### BF001 — Missing `"use client"` Directive
28
30
 
29
31
  **Trigger:** Reactive APIs used without `"use client"`.
@@ -46,6 +48,8 @@ import { createSignal } from '@barefootjs/client'
46
48
  export function Counter() { ... }
47
49
  ```
48
50
 
51
+ <a id="bf003"></a>
52
+
49
53
  ### BF003 — Client Component Importing Server Component
50
54
 
51
55
  **Trigger:** Client component imports from a file without `"use client"`.
@@ -56,6 +60,8 @@ export function Counter() { ... }
56
60
 
57
61
  ## Signal Errors (BF011)
58
62
 
63
+ <a id="bf011"></a>
64
+
59
65
  ### BF011 — Module-Level Reactive Declaration
60
66
 
61
67
  **Trigger:** A `createSignal` or `createMemo` call at module scope without a leading `/* @client */` directive.
@@ -100,6 +106,8 @@ export function Counter() {
100
106
 
101
107
  ## JSX Errors (BF021–BF023)
102
108
 
109
+ <a id="bf021"></a>
110
+
103
111
  ### BF021 — Unsupported JSX Pattern
104
112
 
105
113
  **Trigger:** Array method chain before `.map()` cannot compile to SSR template.
@@ -185,13 +193,13 @@ where `d` is a signal) has no evidence and is not flagged.
185
193
  #### Workaround
186
194
 
187
195
  ```tsx
188
- // ✅ Defer to the client
189
- {/* @client */ createdAt.toISOString()}
190
-
191
- // ✅ Or format in the backend and pass a string prop
196
+ // ✅ Format in the backend and pass a string prop
192
197
  function Post({ createdAt }: { createdAt: string }) {
193
198
  return <div>{createdAt}</div>
194
199
  }
200
+
201
+ // ✅ Or defer to the client — but revive the receiver first
202
+ {/* @client */ new Date(createdAt).toISOString()}
195
203
  ```
196
204
 
197
205
  The string-prop variant moves the formatting to where full language power
@@ -201,6 +209,18 @@ that a component-body local (`const iso = createdAt.toISOString()`) is NOT a
201
209
  workaround: it lowers to a template variable whose value the template
202
210
  backend cannot compute, and dies at render time the same way.
203
211
 
212
+ The `/* @client */` block must wrap the receiver in `new Date(...)` — a
213
+ BARE `{/* @client */ createdAt.toISOString()}` compiles clean but crashes
214
+ at real hydrate with a `TypeError`. Props cross the hydration boundary as
215
+ JSON with no type-aware revival, so `createdAt` arrives at hydrate as its
216
+ `toJSON()` ISO string, not a `Date` instance; wrapping it in `new Date(...)`
217
+ revives it first, since `Date`'s `toJSON()` output round-trips through its
218
+ own constructor (#2636). This revival trick only works for `Date` and
219
+ `URL` — every other host rich type (`Map`, `Set`, …) has no safe
220
+ `/* @client */` escape at all; pre-compute server-side instead.
221
+
222
+ <a id="bf023"></a>
223
+
204
224
  ### BF023 — Missing Key in List
205
225
 
206
226
  **Trigger:** `.map()` loop without `key` prop.
@@ -219,7 +239,55 @@ backend cannot compute, and dies at render time the same way.
219
239
 
220
240
  ---
221
241
 
222
- ## Component Errors (BF043–BF044)
242
+ ## Template Adapter Errors (BF101)
243
+
244
+ <a id="bf101"></a>
245
+
246
+ ### BF101 — No Template-Language Lowering
247
+
248
+ **Trigger:** An expression that a JS-runtime adapter (Hono, CSR) executes verbatim has no lowering on a non-JS template adapter (Go, Mojo, Xslate, Twig, ERB, Blade, Jinja, MiniJinja). Two shapes are permanent known limitations rather than subset widenings:
249
+
250
+ **A nested `.some()` / `.find()` inside a filter predicate** ([#2320](https://github.com/piconic-ai/barefootjs/issues/2320)) — `find`-family methods return an element, not a boolean, so degrading them to their receiver would silently change predicate semantics:
251
+
252
+ ```tsx
253
+ // ❌ BF101 on Go/Mojo/Xslate/Twig/ERB/Blade/Jinja/MiniJinja
254
+ {items().filter(t => picked().some(p => p.id === t.id)).map(t => <li key={t.id}>{t.name}</li>)}
255
+ ```
256
+
257
+ **A `.map()` loop array bound to a component-scope `const` with a computed initializer** ([#2321](https://github.com/piconic-ai/barefootjs/issues/2321)) — no template adapter binds an arbitrary computed local, only a prop/param it passes straight through:
258
+
259
+ ```tsx
260
+ // ❌ BF101 on Go/Mojo/Xslate/Twig/ERB/Blade/Jinja/MiniJinja
261
+ function ReactionBar(props: { reactions: Record<string, string[]> }) {
262
+ const entries = Object.entries(props.reactions).filter(([, users]) => users.length > 0)
263
+ return <div>{entries.map(([emoji, users]) => <span key={emoji}>{emoji}</span>)}</div>
264
+ }
265
+ ```
266
+
267
+ **Escapes** — each verified by a conformance twin that compiles clean on the refusing adapter, listed best-SSR-first:
268
+
269
+ - **Pass the computed result as a prop** (`prop-precompute`) — available for the loop-source shape, wherever the array is already computable server-side. **Full server render**: the rendered result is present in the server HTML.
270
+ - **`/* @client */`** (`client-directive`) — available for both shapes, and compiles clean on every adapter. **Client-render**: the region is *empty in server HTML until hydration*. That trade is the cost of the escape, not a bug — the twin fixtures pin the empty region in their own committed `expectedHtml`.
271
+
272
+ ```tsx
273
+ // ✅ Best for the loop-source shape: pass the computed array as a prop
274
+ function ReactionBar({ entries }: { entries: [string, string[]][] }) {
275
+ return <div>{entries.map(([emoji, users]) => <span key={emoji}>{emoji}</span>)}</div>
276
+ }
277
+
278
+ // ✅ Either shape: defer to the client
279
+ {/* @client */ items().filter(t => picked().some(p => p.id === t.id)).map(t => (
280
+ <li key={t.id}>{t.name}</li>
281
+ ))}
282
+ ```
283
+
284
+ See [JSX Compatibility](../rendering/jsx-compatibility.md) for the full worked examples.
285
+
286
+ ---
287
+
288
+ ## Component Errors (BF043–BF049)
289
+
290
+ <a id="bf043"></a>
223
291
 
224
292
  ### BF043 — Props Destructuring (Warning)
225
293
 
@@ -257,6 +325,8 @@ function Child({ initialCount }: Props) {
257
325
  }
258
326
  ```
259
327
 
328
+ <a id="bf044"></a>
329
+
260
330
  ### BF044 — Signal/Memo Getter Not Called
261
331
 
262
332
  **Trigger:** Signal/memo getter passed without calling it.
@@ -273,6 +343,57 @@ function Child({ initialCount }: Props) {
273
343
  <Child count={count()} />
274
344
  ```
275
345
 
346
+ <a id="bf049"></a>
347
+
348
+ ### BF049 — Rich-Typed Prop Not Hydratable
349
+
350
+ **Trigger:** A prop typed as a JSON-unsafe host rich type — `Map`, `Set`,
351
+ `WeakMap`, `WeakSet`, `URLSearchParams`, `RegExp`, `Promise`, `Error`,
352
+ `Symbol`, `BigInt`, `Function` — is used anywhere in this component's own
353
+ client code (an event handler, an effect), regardless of whether a method is
354
+ called on it. This is the sibling of [BF021](#bf021)'s host-rich-type
355
+ refusal for a different shape: BF021 only walks expression positions
356
+ reachable through template lowering (JSX text/attribute positions rendered
357
+ at SSR); a handler or effect body is a different code path BF021 never
358
+ analyzes, so even a method call there (like `data.get(...)` below) is just
359
+ as invisible to it as a bare read. Either way the prop crosses the `bf-p`
360
+ hydration boundary as JSON, where a `Map`/`Set` arrives de-riched (`{}`,
361
+ every entry silently dropped) and a `BigInt` fails to serialize at all
362
+ (`TypeError` at SSR render, failing the whole page).
363
+
364
+ ```tsx
365
+ // ❌ BF049 — a Map prop used by client code cannot survive hydration
366
+ 'use client'
367
+ export function Foo({ data }: { data: Map<string, number> }) {
368
+ return <button onClick={() => console.log(data.get('x'))}>go</button>
369
+ }
370
+ ```
371
+
372
+ **Fix:** Pre-compute a JSON-serializable value server-side and rebuild the
373
+ rich value client-side where it's actually needed.
374
+
375
+ ```tsx
376
+ // ✅ Fixed
377
+ 'use client'
378
+ export function Foo({ entries }: { entries: [string, number][] }) {
379
+ return <button onClick={() => console.log(new Map(entries).get('x'))}>go</button>
380
+ }
381
+ ```
382
+
383
+ > `Date` and `URL` props are exempt — their `toJSON()` output round-trips
384
+ > through their own constructor, so they're not JSON-unsafe (see BF021's
385
+ > host-rich-type section above).
386
+ >
387
+ > This is a compile-time check: it only fires when the prop's type is
388
+ > provable from the component's own props type (same evidence
389
+ > `checkRichTypeMethodCalls` uses). An imported/aliased type alias, or a
390
+ > prop typed too loosely to resolve statically, isn't caught here — on the
391
+ > Hono adapter, an unsound value reaching hydration serialization throws a
392
+ > clear runtime error naming the prop and this code instead of failing
393
+ > silently or with an opaque `JSON.stringify` error.
394
+
395
+ <a id="bf054"></a>
396
+
276
397
  ### BF054 — Built-in `<Async>` / `<Region>` Used Without Import
277
398
 
278
399
  **Trigger:** A bare `<Async>` or `<Region>` tag is used without importing it
@@ -334,4 +455,5 @@ function Component({ checked }: Props) {
334
455
  | BF023 | Error | Missing key in list |
335
456
  | BF043 | Warning | Props destructuring breaks reactivity |
336
457
  | BF044 | Error | Signal/memo getter passed without calling it |
458
+ | BF049 | Error | Rich-typed prop read by client code cannot survive hydration |
337
459
  | BF054 | Error | Built-in `<Async>` / `<Region>` used without `@barefootjs/client` import |
@@ -125,6 +125,7 @@ Some JavaScript expressions cannot be translated into marked template syntax. Wh
125
125
  | JSX-returning `.flatMap()` with a statement body (early `return`, `const` before the projection) | works (runs as JS) | **BF021** |
126
126
  | Nested `.filter()` / `.map()` in a filter predicate (`x => x.tags.filter(...).length > 0`) | works | works |
127
127
  | Nested `.some()` / `.find()` / `.reduce()` in a filter predicate | works | **BF101** |
128
+ | `.map()` loop array bound to a component-scope `const` with a computed initializer (e.g. `Object.entries(props.x).filter(...)`) | works | **BF101** |
128
129
  | Sort comparator that's a multi-statement block body or `localeCompare(b, locale, opts)` | works (runs as JS) | **BF021** |
129
130
  | Sort comparator that's a function reference to an imported/prop identifier, or an alias chain (`const c2 = c1`) | works (runs as JS) | **BF021** |
130
131
  | `typeof` in a filter predicate | works (runs as JS) | **BF021** |
@@ -182,6 +183,39 @@ A nested `.some()` / `.find()` / `.reduce()` still has no faithful Go/Mojo lower
182
183
  {items().filter(x => x.done)}
183
184
  ```
184
185
 
186
+ **Computed loop array (`const` with a runtime initializer):**
187
+
188
+ A `.map()` loop whose array is a bare identifier works when that identifier is a prop or a signal read, but not when it's a component-scope `const` computed from one at render time (`Object.entries(...)`, `.filter(...)`, …) — no template adapter has a binding for an arbitrary computed local, only for a prop/param it can pass straight through:
189
+
190
+ ```tsx
191
+ // ❌ BF101 on Go/Mojo/Xslate/Twig/ERB/Blade/Jinja/MiniJinja; works on Hono
192
+ type Props = { reactions: Record<string, string[]> }
193
+ function ReactionBar(props: Props) {
194
+ const entries = Object.entries(props.reactions).filter(([, users]) => users.length > 0)
195
+ return <div>{entries.map(([emoji, users]) => (
196
+ <span key={emoji}>{emoji}: {String(users.length)}</span>
197
+ ))}</div>
198
+ }
199
+
200
+ // ✅ Best: precompute the array in the parent/route handler and pass it as a prop
201
+ type Entry = [string, string[]]
202
+ function ReactionBarByProp({ entries }: { entries: Entry[] }) {
203
+ return <div>{entries.map(([emoji, users]) => (
204
+ <span key={emoji}>{emoji}: {String(users.length)}</span>
205
+ ))}</div>
206
+ }
207
+
208
+ // ✅ Or defer to the client with /* @client */
209
+ function ReactionBarClientOnly(props: Props) {
210
+ const entries = Object.entries(props.reactions).filter(([, users]) => users.length > 0)
211
+ return <div>{/* @client */ entries.map(([emoji, users]) => (
212
+ <span key={emoji}>{emoji}: {String(users.length)}</span>
213
+ ))}</div>
214
+ }
215
+ ```
216
+
217
+ The prop-passing form is the better fix — it keeps full SSR output, since the template adapters bind a plain prop array directly. `/* @client */` compiles clean too, but renders **nothing** for that region at SSR — no content until hydration/mount runs the loop client-side.
218
+
185
219
  ### Sort comparators that error on Go / Mojo
186
220
 
187
221
  **Unsupported sort comparators** (imperative block bodies, unresolved function references) — a JS-runtime adapter (Hono, CSR) runs any of these verbatim; only non-JS template backends refuse them:
package/dist/index.js CHANGED
@@ -4663,6 +4663,10 @@ function irToComponentTemplateWithOpts(node, opts) {
4663
4663
  return escapeHtml(node.value);
4664
4664
  case "expression": {
4665
4665
  if (node.expr === "null" || node.expr === "undefined") return "";
4666
+ if (node.clientOnly && node.slotId) {
4667
+ if (node.markerless) return "";
4668
+ return `<!--bf:${node.slotId}--><!--/-->`;
4669
+ }
4666
4670
  const wrapped = transformExpr(node.expr, node.templateExpr);
4667
4671
  const value2 = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
4668
4672
  if (node.slotId) {
@@ -4963,6 +4967,7 @@ function generateCsrTemplateWithOpts(node, opts) {
4963
4967
  case "expression":
4964
4968
  if (node.expr === "null" || node.expr === "undefined") return "";
4965
4969
  if (node.clientOnly && node.slotId) {
4970
+ if (node.markerless) return "";
4966
4971
  return `<!--bf:${node.slotId}--><!--/-->`;
4967
4972
  }
4968
4973
  {
@@ -5906,6 +5911,16 @@ var init_errors = __esm({
5906
5911
  // helper verbatim instead of compiling it as a component — so this code
5907
5912
  // fires only for the client-component compilation path.
5908
5913
  SIBLING_COMPONENT_NOT_COMPILED: "BF048",
5914
+ // A prop typed as a host rich type whose `JSON.stringify` output is not
5915
+ // revivable (`Map`, `Set`, `BigInt`, …) is used by this component's own
5916
+ // client code (a handler, an effect) — regardless of whether a method is
5917
+ // called on it, since `checkRichTypeMethodCalls`'s BF021 only walks
5918
+ // template-lowered expression positions and never sees a handler/effect
5919
+ // body either way. The prop still crosses the `bf-p` hydration boundary as
5920
+ // JSON and arrives de-riched (or, for `BigInt`, fails to serialize at all,
5921
+ // throwing at SSR render). Sibling of BF021 for the "client-side use"
5922
+ // shape, which BF021's template-only walk can never reach (#2643).
5923
+ RICH_TYPE_PROP_NOT_HYDRATABLE: "BF049",
5909
5924
  // Import errors (BF050-BF059)
5910
5925
  SHARED_PROGRAM_REQUIRED: "BF050",
5911
5926
  WRONG_PACKAGE_IMPORT: "BF051",
@@ -5978,6 +5993,7 @@ var init_errors = __esm({
5978
5993
  [ErrorCodes.COMPONENT_REQUIRED_PROP_MISSING]: "Built-in component is missing a required prop.",
5979
5994
  [ErrorCodes.JSX_BRANCH_LOCAL_IN_CALLBACK]: "JSX-typed local declared inside an `if`-block cannot be referenced from a callback body (ref / event handler). Render it as a child instead: `<div ref={...}>{local}</div>`.",
5980
5995
  [ErrorCodes.SIBLING_COMPONENT_NOT_COMPILED]: "Referenced component did not compile to a template, so this reference would throw `ReferenceError` at render time. Multi-return JSX dispatch (a `switch` or `if`/`else` chain across multiple JSX-returning branches) cannot compile as a component in a 'use client' file. Extract it to a separate non-\"use client\" file (where it is preserved verbatim, see #932), or rewrite it as a single-return ternary/conditional chain so the component pipeline can compile it.",
5996
+ [ErrorCodes.RICH_TYPE_PROP_NOT_HYDRATABLE]: "Rich-typed prop cannot cross the bf-p hydration boundary as JSON.",
5981
5997
  [ErrorCodes.SHARED_PROGRAM_REQUIRED]: "Shared ts.Program required for type-based reactivity classification. This source imports a Reactive<T>-branded library (e.g. @barefootjs/form) whose getters cannot be classified by regex alone. Pass `options.program` (built via `createProgramForCorpus`) so the analyzer can resolve the brand through the TypeChecker.",
5982
5998
  [ErrorCodes.WRONG_PACKAGE_IMPORT]: "Import from wrong package.",
5983
5999
  [ErrorCodes.BUILTIN_REQUIRES_IMPORT]: "Built-in <Async> / <Region> must be imported from '@barefootjs/client'. The compiler recognises these tags by their import (not by tag name), so an unimported tag with this name is treated as an undeclared component.",
@@ -6031,6 +6047,20 @@ function lookupProperty(objType, propName, meta) {
6031
6047
  const prop = deref.properties?.find((p) => p.name === propName);
6032
6048
  return prop ? stripUnion(prop.type) : null;
6033
6049
  }
6050
+ function resolvePropDeclaredType(propName, meta) {
6051
+ return lookupProperty(meta.propsType, propName, meta);
6052
+ }
6053
+ function jsonUnsafeTypeName(type2) {
6054
+ if (!type2) return null;
6055
+ if (type2.kind === "interface") {
6056
+ const name2 = baseTypeName(type2.raw);
6057
+ return JSON_UNSAFE_RICH_TYPE_NAMES.has(name2) ? name2 : null;
6058
+ }
6059
+ if (type2.kind === "unknown" && (type2.raw === "bigint" || type2.raw === "symbol")) {
6060
+ return type2.raw;
6061
+ }
6062
+ return null;
6063
+ }
6034
6064
  function resolveReceiverType(expr, meta, bindings) {
6035
6065
  if (expr.kind === "identifier") {
6036
6066
  if (bindings.has(expr.name)) return stripUnion(bindings.get(expr.name) ?? null);
@@ -6047,7 +6077,7 @@ function resolveReceiverType(expr, meta, bindings) {
6047
6077
  }
6048
6078
  return null;
6049
6079
  }
6050
- var HOST_RICH_TYPE_NAMES;
6080
+ var HOST_RICH_TYPE_NAMES, JSON_REVIVABLE_RICH_TYPE_NAMES, JSON_UNSAFE_RICH_TYPE_NAMES;
6051
6081
  var init_rich_type_evidence = __esm({
6052
6082
  "../jsx/src/rich-type-evidence.ts"() {
6053
6083
  "use strict";
@@ -6066,6 +6096,10 @@ var init_rich_type_evidence = __esm({
6066
6096
  "BigInt",
6067
6097
  "Function"
6068
6098
  ]);
6099
+ JSON_REVIVABLE_RICH_TYPE_NAMES = /* @__PURE__ */ new Set(["Date", "URL"]);
6100
+ JSON_UNSAFE_RICH_TYPE_NAMES = new Set(
6101
+ [...HOST_RICH_TYPE_NAMES].filter((n) => !JSON_REVIVABLE_RICH_TYPE_NAMES.has(n))
6102
+ );
6069
6103
  }
6070
6104
  });
6071
6105
 
@@ -9328,7 +9362,7 @@ function pickAttrMetaFromIR(src) {
9328
9362
  ...src.freeIdentifiers !== void 0 && { freeIdentifiers: src.freeIdentifiers }
9329
9363
  };
9330
9364
  }
9331
- var SCOPE_FORBIDDEN, REACTIVE_BINDING_KINDS, AttrValueOf;
9365
+ var SCOPE_FORBIDDEN, REACTIVE_BINDING_KINDS, AttrValueOf, ESCAPE_SSR_COST;
9332
9366
  var init_types = __esm({
9333
9367
  "../jsx/src/types.ts"() {
9334
9368
  "use strict";
@@ -9400,6 +9434,14 @@ var init_types = __esm({
9400
9434
  return { kind: "jsx-children", children: children2 };
9401
9435
  }
9402
9436
  };
9437
+ ESCAPE_SSR_COST = {
9438
+ // `/* @client */` — compiles and hydrates correctly, renders nothing at SSR.
9439
+ "client-directive": "client-render",
9440
+ // The refused computation moves to an already-computed prop.
9441
+ "prop-precompute": "none",
9442
+ // The source is restructured into an equivalent in-subset shape.
9443
+ rewrite: "none"
9444
+ };
9403
9445
  }
9404
9446
  });
9405
9447
 
@@ -15685,6 +15727,9 @@ function buildReferencesGraph(ctx2, irRoot) {
15685
15727
  addExprEdges(ROOT_SOURCE, event.handler, "init-body");
15686
15728
  }
15687
15729
  }
15730
+ for (const elem of ctx2.clientOnlyElements) {
15731
+ addExprEdges(ROOT_SOURCE, elem.expression, "init-body");
15732
+ }
15688
15733
  for (const elem of ctx2.loopElements) {
15689
15734
  addExprEdges(ROOT_SOURCE, elem.array, "template-closure");
15690
15735
  addTemplateEdges(ROOT_SOURCE, elem.template, "template-closure");
@@ -20339,9 +20384,14 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
20339
20384
  }
20340
20385
  return restore(result2);
20341
20386
  }
20342
- function emitDynamicTextUpdates(lines, ctx2) {
20343
- const dateLoweringMatcher = getReactiveDateLoweringMatcher(ctx2);
20387
+ function makeCataloguedCallLowerer(ctx2) {
20388
+ const dateMatcher = getReactiveDateLoweringMatcher(ctx2);
20344
20389
  const toLocaleMatcher = getReactiveToLocaleMatcher(ctx2);
20390
+ if (!dateMatcher && !toLocaleMatcher) return (expr) => expr;
20391
+ return (expr) => lowerToLocaleCallsInReactiveExpr(lowerDateCallsInReactiveExpr(expr, dateMatcher), toLocaleMatcher);
20392
+ }
20393
+ function emitDynamicTextUpdates(lines, ctx2) {
20394
+ const lower = makeCataloguedCallLowerer(ctx2);
20345
20395
  const byExpression = /* @__PURE__ */ new Map();
20346
20396
  for (const elem of ctx2.dynamicElements) {
20347
20397
  const key = elem.expression;
@@ -20351,10 +20401,7 @@ function emitDynamicTextUpdates(lines, ctx2) {
20351
20401
  byExpression.get(key).push(elem);
20352
20402
  }
20353
20403
  for (const [rawExpr, elems] of byExpression) {
20354
- const expr = lowerToLocaleCallsInReactiveExpr(
20355
- lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher),
20356
- toLocaleMatcher
20357
- );
20404
+ const expr = lower(rawExpr);
20358
20405
  const conditionalElems = elems.filter((e) => e.insideConditional);
20359
20406
  const normalElems = elems.filter((e) => !e.insideConditional);
20360
20407
  if (normalElems.length > 0 || conditionalElems.length > 0) {
@@ -20391,19 +20438,21 @@ function emitDynamicTextUpdates(lines, ctx2) {
20391
20438
  }
20392
20439
  }
20393
20440
  function emitClientOnlyExpressions(lines, ctx2) {
20441
+ const lower = makeCataloguedCallLowerer(ctx2);
20394
20442
  for (const elem of ctx2.clientOnlyElements) {
20395
20443
  const slots = elem.elidedPath ? [{ id: elem.slotId, kind: "text", path: elem.elidedPath, markerless: true }] : [{ id: elem.slotId, kind: "text", path: [] }];
20396
20444
  const writer = claimWriterVarName(slots, varSlotId);
20397
20445
  lines.push(` // @client: ${elem.slotId}`);
20398
20446
  lines.push(` { const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
20399
20447
  lines.push(` createEffect(() => {`);
20400
- lines.push(` ${writer}('${elem.slotId}', ${elem.expression})`);
20448
+ lines.push(` ${writer}('${elem.slotId}', ${lower(elem.expression)})`);
20401
20449
  lines.push(` }${bindingIdArg(ctx2, elem.slotId)}) }`);
20402
20450
  lines.push("");
20403
20451
  }
20404
20452
  }
20405
20453
  function emitReactiveAttributeUpdates(lines, ctx2) {
20406
20454
  if (ctx2.reactiveAttrs.length > 0) {
20455
+ const lower = makeCataloguedCallLowerer(ctx2);
20407
20456
  const attrsBySlot = /* @__PURE__ */ new Map();
20408
20457
  for (const attr of ctx2.reactiveAttrs) {
20409
20458
  if (!attrsBySlot.has(attr.slotId)) {
@@ -20416,7 +20465,7 @@ function emitReactiveAttributeUpdates(lines, ctx2) {
20416
20465
  lines.push(` createEffect(() => {`);
20417
20466
  lines.push(` if (_${v}) {`);
20418
20467
  for (const attr of attrs) {
20419
- const expression = rewriteDestructuredPropsInExpr(attr.expression, ctx2);
20468
+ const expression = rewriteDestructuredPropsInExpr(lower(attr.expression), ctx2);
20420
20469
  for (const stmt of emitAttrUpdate(`_${v}`, attr.attrName, expression, attr)) {
20421
20470
  lines.push(` ${stmt}`);
20422
20471
  }
@@ -24299,6 +24348,33 @@ function checkRichTypeMethodCalls(root2, metadata, errors) {
24299
24348
  const seen = /* @__PURE__ */ new Set();
24300
24349
  walkNode2(root2, metadata, EMPTY_BINDINGS2, matchers, errors, seen);
24301
24350
  }
24351
+ function checkRichTypePropSerialization(root2, metadata, errors, declLoc) {
24352
+ if (!metadata.propsType || !metadata.clientAnalysis?.needsInit) return;
24353
+ const usedProps = new Set(metadata.clientAnalysis.usedProps);
24354
+ const loc = declLoc ?? root2.loc;
24355
+ for (const param of metadata.propsParams) {
24356
+ if (param.isRest || param.name.startsWith("on") || param.name.startsWith("__")) continue;
24357
+ if (!usedProps.has(param.name)) continue;
24358
+ const declared = resolvePropDeclaredType(param.sourceName ?? param.name, metadata);
24359
+ const typeName = jsonUnsafeTypeName(declared);
24360
+ if (!typeName) continue;
24361
+ if (metadata.typeDefinitions.some((d) => d.name === typeName)) continue;
24362
+ pushPropSerializationDiagnostic(errors, loc, param.name, typeName, declared.raw);
24363
+ }
24364
+ }
24365
+ function pushPropSerializationDiagnostic(errors, loc, propName, typeName, declaredRaw) {
24366
+ const consequence = typeName === "bigint" || typeName === "BigInt" ? "JSON.stringify throws at SSR render ('Do not know how to serialize a BigInt'), failing the whole page" : typeName === "symbol" || typeName === "Symbol" || typeName === "Function" ? "JSON.stringify drops the value entirely, so the client reads undefined at hydrate" : "it serializes de-riched (e.g. a Map or Set becomes {} with every entry silently dropped), so the client hydrates against corrupt data";
24367
+ errors.push({
24368
+ code: ErrorCodes.RICH_TYPE_PROP_NOT_HYDRATABLE,
24369
+ severity: "error",
24370
+ message: `Prop '${propName}' is typed '${declaredRaw}' and is read by this component's own client code, so it must cross the bf-p hydration boundary as JSON \u2014 and ${typeName} cannot: ${consequence}.`,
24371
+ loc,
24372
+ suggestion: {
24373
+ message: "Pre-compute a JSON-serializable value server-side \u2014 a string, number, boolean, array, or plain object (e.g. pass [...map.entries()] and rebuild the Map client-side where needed) \u2014 and pass that as the prop instead. /* @client */ is NOT an escape here: the prop still crosses the bf-p boundary as JSON and arrives de-riched (#2636).",
24374
+ escape: [{ kind: "prop-precompute" }]
24375
+ }
24376
+ });
24377
+ }
24302
24378
  function isLoweringClaimed(matchers, callee, args2) {
24303
24379
  return matchers.some((m) => m(callee, args2) !== null);
24304
24380
  }
@@ -24312,20 +24388,38 @@ function receiverRootIsProp(expr, bindings) {
24312
24388
  while (root2.kind === "member" && !root2.computed) root2 = root2.object;
24313
24389
  return root2.kind === "identifier" && !bindings.has(root2.name);
24314
24390
  }
24391
+ function buildSuggestion(method2, receiverPath, receiver, typeName) {
24392
+ const revivalExpr = receiverPath === "<expression>" ? `wrapping the receiver in new ${typeName}(...) before calling .${method2}()` : `{/* @client */ new ${typeName}(${receiverPath}).${method2}(...)}`;
24393
+ const revivalReason = `a bare /* @client */ crashes at hydrate because ${receiver} crosses the bf-p boundary as JSON and arrives as a plain string, not a ${typeName} instance (#2636)`;
24394
+ if (method2 === "toLocaleDateString" && typeName === "Date") {
24395
+ return {
24396
+ message: `Pass a literal locale and an explicit literal timeZone \u2014 .toLocaleDateString('ja-JP', { timeZone: 'UTC' }), a fixed '\xB1HH:MM' offset, or a canonical IANA zone ID like 'Asia/Tokyo' (exact case; #2344) \u2014 to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. Alternatively pre-compute server-side, or evaluate client-only by ${revivalExpr} \u2014 ${revivalReason}.`,
24397
+ escape: [{ kind: "rewrite" }, { kind: "prop-precompute" }, { kind: "client-directive" }]
24398
+ };
24399
+ }
24400
+ if (JSON_REVIVABLE_RICH_TYPE_NAMES.has(typeName)) {
24401
+ return {
24402
+ message: `Pre-compute the value server-side and pass it as a prop. Alternatively, evaluate client-only by ${revivalExpr} \u2014 ${revivalReason}.`,
24403
+ escape: [{ kind: "prop-precompute" }, { kind: "client-directive" }]
24404
+ };
24405
+ }
24406
+ return {
24407
+ message: `Pre-compute the value server-side and pass the result \u2014 a string, number, array, or plain object \u2014 as a prop. /* @client */ is NOT a safe escape here: ${receiver} cannot cross the bf-p hydration boundary as JSON \u2014 it arrives de-riched (e.g. a Map or Set serializes to {}), so the call throws or silently returns the wrong result at hydrate (#2636).`,
24408
+ escape: [{ kind: "prop-precompute" }]
24409
+ };
24410
+ }
24315
24411
  function pushDiagnostic(errors, seen, loc, method2, receiverPath, isProp, typeName) {
24316
24412
  const key = `${loc.start.line}:${loc.start.column}:${receiverPath}.${method2}`;
24317
24413
  if (seen.has(key)) return;
24318
24414
  seen.add(key);
24319
24415
  const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`;
24320
- const suggestion = method2 === "toLocaleDateString" && typeName === "Date" ? "Pass a literal locale and an explicit literal timeZone \u2014 .toLocaleDateString('ja-JP', { timeZone: 'UTC' }), a fixed '\xB1HH:MM' offset, or a canonical IANA zone ID like 'Asia/Tokyo' (exact case; #2344) \u2014 to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. Alternatively add /* @client */ or pre-compute server-side." : "Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side.";
24416
+ const suggestion = buildSuggestion(method2, receiverPath, receiver, typeName);
24321
24417
  errors.push({
24322
24418
  code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
24323
24419
  severity: "error",
24324
24420
  message: `Expression cannot be compiled to marked template: method '.${method2}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
24325
24421
  loc,
24326
- suggestion: {
24327
- message: suggestion
24328
- }
24422
+ suggestion
24329
24423
  });
24330
24424
  }
24331
24425
  function checkExpr(expr, loc, meta, bindings, matchers, errors, seen) {
@@ -24556,6 +24650,7 @@ function compileMultipleComponents(source, filePath, componentNames, options2) {
24556
24650
  };
24557
24651
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
24558
24652
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
24653
+ checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx2.propsDestructuring?.loc);
24559
24654
  decideClientOnlyElision(componentIR.root);
24560
24655
  entries2.push({ componentIR, ctx: ctx2 });
24561
24656
  }
@@ -24962,6 +25057,7 @@ function compileJSX(source, filePath, options2) {
24962
25057
  };
24963
25058
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
24964
25059
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
25060
+ checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx2.propsDestructuring?.loc);
24965
25061
  decideClientOnlyElision(componentIR.root);
24966
25062
  if (ctx2.importedClientSignalNames.size > 0) {
24967
25063
  const sources = /* @__PURE__ */ new Set();
@@ -29240,6 +29336,7 @@ __export(src_exports, {
29240
29336
  BindingScope: () => BindingScope,
29241
29337
  CALLBACK_METHODS: () => CALLBACK_METHODS,
29242
29338
  ENV_SIGNAL_READERS: () => ENV_SIGNAL_READERS,
29339
+ ESCAPE_SSR_COST: () => ESCAPE_SSR_COST,
29243
29340
  ErrorCodes: () => ErrorCodes,
29244
29341
  JsxAdapter: () => JsxAdapter,
29245
29342
  PARSED_EXPR_KINDS: () => PARSED_EXPR_KINDS,
@@ -29430,6 +29527,7 @@ var init_src2 = __esm({
29430
29527
  init_source_map();
29431
29528
  init_combine_client_js();
29432
29529
  init_types();
29530
+ init_types();
29433
29531
  init_css_layer_prefixer();
29434
29532
  init_instrumentation();
29435
29533
  init_errors();
package/dist/tokens.json CHANGED
@@ -49,6 +49,7 @@
49
49
  { "name": "card-foreground", "value": "oklch(0.145 0 0)", "dark": "oklch(0.985 0 0)" },
50
50
  { "name": "primary", "value": "oklch(0.205 0 0)", "dark": "oklch(0.35 0 0)" },
51
51
  { "name": "primary-foreground", "value": "oklch(0.985 0 0)", "dark": "oklch(0.985 0 0)" },
52
+ { "name": "link", "value": "oklch(0.205 0 0)", "dark": "oklch(0.75 0 0)" },
52
53
  { "name": "secondary", "value": "oklch(0.97 0 0)", "dark": "oklch(0.269 0 0)" },
53
54
  { "name": "secondary-foreground", "value": "oklch(0.205 0 0)", "dark": "oklch(0.985 0 0)" },
54
55
  { "name": "muted", "value": "oklch(0.97 0 0)", "dark": "oklch(0.269 0 0)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/cli",
3
- "version": "0.31.5",
3
+ "version": "0.31.7",
4
4
  "description": "CLI for agent-driven UI component discovery and scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,12 +30,12 @@
30
30
  "esbuild": "^0.25.0",
31
31
  "typescript": "^5.0.0",
32
32
  "vite": "^6.0.0",
33
- "@barefootjs/client": "0.31.5",
34
- "@barefootjs/shared": "0.31.5"
33
+ "@barefootjs/client": "0.31.7",
34
+ "@barefootjs/shared": "0.31.7"
35
35
  },
36
36
  "devDependencies": {
37
- "@barefootjs/jsx": "0.31.5",
38
- "@barefootjs/vite": "0.31.5",
37
+ "@barefootjs/jsx": "0.31.7",
38
+ "@barefootjs/vite": "0.31.7",
39
39
  "@happy-dom/global-registrator": "^20.0.11",
40
40
  "@types/node": "^22.0.0",
41
41
  "happy-dom": "^20.0.11"