@barefootjs/cli 0.31.6 → 0.31.8

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.
@@ -193,13 +193,13 @@ where `d` is a signal) has no evidence and is not flagged.
193
193
  #### Workaround
194
194
 
195
195
  ```tsx
196
- // ✅ Defer to the client
197
- {/* @client */ createdAt.toISOString()}
198
-
199
- // ✅ Or format in the backend and pass a string prop
196
+ // ✅ Format in the backend and pass a string prop
200
197
  function Post({ createdAt }: { createdAt: string }) {
201
198
  return <div>{createdAt}</div>
202
199
  }
200
+
201
+ // ✅ Or defer to the client — but revive the receiver first
202
+ {/* @client */ new Date(createdAt).toISOString()}
203
203
  ```
204
204
 
205
205
  The string-prop variant moves the formatting to where full language power
@@ -209,6 +209,16 @@ that a component-body local (`const iso = createdAt.toISOString()`) is NOT a
209
209
  workaround: it lowers to a template variable whose value the template
210
210
  backend cannot compute, and dies at render time the same way.
211
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
+
212
222
  <a id="bf023"></a>
213
223
 
214
224
  ### BF023 — Missing Key in List
@@ -275,7 +285,7 @@ See [JSX Compatibility](../rendering/jsx-compatibility.md) for the full worked e
275
285
 
276
286
  ---
277
287
 
278
- ## Component Errors (BF043–BF044)
288
+ ## Component Errors (BF043–BF049)
279
289
 
280
290
  <a id="bf043"></a>
281
291
 
@@ -333,6 +343,55 @@ function Child({ initialCount }: Props) {
333
343
  <Child count={count()} />
334
344
  ```
335
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
+
336
395
  <a id="bf054"></a>
337
396
 
338
397
  ### BF054 — Built-in `<Async>` / `<Region>` Used Without Import
@@ -396,4 +455,5 @@ function Component({ checked }: Props) {
396
455
  | BF023 | Error | Missing key in list |
397
456
  | BF043 | Warning | Props destructuring breaks reactivity |
398
457
  | BF044 | Error | Signal/memo getter passed without calling it |
458
+ | BF049 | Error | Rich-typed prop read by client code cannot survive hydration |
399
459
  | BF054 | Error | Built-in `<Async>` / `<Region>` used without `@barefootjs/client` import |
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) {
@@ -5907,6 +5911,16 @@ var init_errors = __esm({
5907
5911
  // helper verbatim instead of compiling it as a component — so this code
5908
5912
  // fires only for the client-component compilation path.
5909
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",
5910
5924
  // Import errors (BF050-BF059)
5911
5925
  SHARED_PROGRAM_REQUIRED: "BF050",
5912
5926
  WRONG_PACKAGE_IMPORT: "BF051",
@@ -5979,6 +5993,7 @@ var init_errors = __esm({
5979
5993
  [ErrorCodes.COMPONENT_REQUIRED_PROP_MISSING]: "Built-in component is missing a required prop.",
5980
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>`.",
5981
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.",
5982
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.",
5983
5998
  [ErrorCodes.WRONG_PACKAGE_IMPORT]: "Import from wrong package.",
5984
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.",
@@ -6032,6 +6047,20 @@ function lookupProperty(objType, propName, meta) {
6032
6047
  const prop = deref.properties?.find((p) => p.name === propName);
6033
6048
  return prop ? stripUnion(prop.type) : null;
6034
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
+ }
6035
6064
  function resolveReceiverType(expr, meta, bindings) {
6036
6065
  if (expr.kind === "identifier") {
6037
6066
  if (bindings.has(expr.name)) return stripUnion(bindings.get(expr.name) ?? null);
@@ -6048,7 +6077,7 @@ function resolveReceiverType(expr, meta, bindings) {
6048
6077
  }
6049
6078
  return null;
6050
6079
  }
6051
- var HOST_RICH_TYPE_NAMES;
6080
+ var HOST_RICH_TYPE_NAMES, JSON_REVIVABLE_RICH_TYPE_NAMES, JSON_UNSAFE_RICH_TYPE_NAMES;
6052
6081
  var init_rich_type_evidence = __esm({
6053
6082
  "../jsx/src/rich-type-evidence.ts"() {
6054
6083
  "use strict";
@@ -6067,6 +6096,10 @@ var init_rich_type_evidence = __esm({
6067
6096
  "BigInt",
6068
6097
  "Function"
6069
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
+ );
6070
6103
  }
6071
6104
  });
6072
6105
 
@@ -15694,6 +15727,9 @@ function buildReferencesGraph(ctx2, irRoot) {
15694
15727
  addExprEdges(ROOT_SOURCE, event.handler, "init-body");
15695
15728
  }
15696
15729
  }
15730
+ for (const elem of ctx2.clientOnlyElements) {
15731
+ addExprEdges(ROOT_SOURCE, elem.expression, "init-body");
15732
+ }
15697
15733
  for (const elem of ctx2.loopElements) {
15698
15734
  addExprEdges(ROOT_SOURCE, elem.array, "template-closure");
15699
15735
  addTemplateEdges(ROOT_SOURCE, elem.template, "template-closure");
@@ -17099,6 +17135,13 @@ function csrInlinableConstantsFromCtx(ctx2) {
17099
17135
  }
17100
17136
  return out;
17101
17137
  }
17138
+ function buildTemplateDefPart(ctx2, templateHtml) {
17139
+ const envDecls = ctx2.signals.filter((s) => Boolean(s.envReader) && Boolean(s.envFactory)).map((s) => `const [${s.getter}] = ${s.envFactory}()`);
17140
+ if (envDecls.length === 0) {
17141
+ return `template: (${PROPS_PARAM}) => \`${templateHtml}\``;
17142
+ }
17143
+ return `template: (${PROPS_PARAM}) => { ${envDecls.join("; ")}; return \`${templateHtml}\` }`;
17144
+ }
17102
17145
  function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
17103
17146
  const name2 = ctx2.componentName;
17104
17147
  lines.push(`}`);
@@ -17113,7 +17156,7 @@ function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
17113
17156
  if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
17114
17157
  const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx2.propsObjectName);
17115
17158
  if (templateHtml) {
17116
- defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``);
17159
+ defParts.push(buildTemplateDefPart(ctx2, templateHtml));
17117
17160
  }
17118
17161
  } else {
17119
17162
  const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx2);
@@ -17127,7 +17170,7 @@ function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
17127
17170
  ctx2.deferredChildSlots
17128
17171
  );
17129
17172
  if (templateHtml) {
17130
- defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``);
17173
+ defParts.push(buildTemplateDefPart(ctx2, templateHtml));
17131
17174
  }
17132
17175
  }
17133
17176
  if (isCommentScope) {
@@ -20348,9 +20391,14 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
20348
20391
  }
20349
20392
  return restore(result2);
20350
20393
  }
20351
- function emitDynamicTextUpdates(lines, ctx2) {
20352
- const dateLoweringMatcher = getReactiveDateLoweringMatcher(ctx2);
20394
+ function makeCataloguedCallLowerer(ctx2) {
20395
+ const dateMatcher = getReactiveDateLoweringMatcher(ctx2);
20353
20396
  const toLocaleMatcher = getReactiveToLocaleMatcher(ctx2);
20397
+ if (!dateMatcher && !toLocaleMatcher) return (expr) => expr;
20398
+ return (expr) => lowerToLocaleCallsInReactiveExpr(lowerDateCallsInReactiveExpr(expr, dateMatcher), toLocaleMatcher);
20399
+ }
20400
+ function emitDynamicTextUpdates(lines, ctx2) {
20401
+ const lower = makeCataloguedCallLowerer(ctx2);
20354
20402
  const byExpression = /* @__PURE__ */ new Map();
20355
20403
  for (const elem of ctx2.dynamicElements) {
20356
20404
  const key = elem.expression;
@@ -20360,10 +20408,7 @@ function emitDynamicTextUpdates(lines, ctx2) {
20360
20408
  byExpression.get(key).push(elem);
20361
20409
  }
20362
20410
  for (const [rawExpr, elems] of byExpression) {
20363
- const expr = lowerToLocaleCallsInReactiveExpr(
20364
- lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher),
20365
- toLocaleMatcher
20366
- );
20411
+ const expr = lower(rawExpr);
20367
20412
  const conditionalElems = elems.filter((e) => e.insideConditional);
20368
20413
  const normalElems = elems.filter((e) => !e.insideConditional);
20369
20414
  if (normalElems.length > 0 || conditionalElems.length > 0) {
@@ -20400,19 +20445,21 @@ function emitDynamicTextUpdates(lines, ctx2) {
20400
20445
  }
20401
20446
  }
20402
20447
  function emitClientOnlyExpressions(lines, ctx2) {
20448
+ const lower = makeCataloguedCallLowerer(ctx2);
20403
20449
  for (const elem of ctx2.clientOnlyElements) {
20404
20450
  const slots = elem.elidedPath ? [{ id: elem.slotId, kind: "text", path: elem.elidedPath, markerless: true }] : [{ id: elem.slotId, kind: "text", path: [] }];
20405
20451
  const writer = claimWriterVarName(slots, varSlotId);
20406
20452
  lines.push(` // @client: ${elem.slotId}`);
20407
20453
  lines.push(` { const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
20408
20454
  lines.push(` createEffect(() => {`);
20409
- lines.push(` ${writer}('${elem.slotId}', ${elem.expression})`);
20455
+ lines.push(` ${writer}('${elem.slotId}', ${lower(elem.expression)})`);
20410
20456
  lines.push(` }${bindingIdArg(ctx2, elem.slotId)}) }`);
20411
20457
  lines.push("");
20412
20458
  }
20413
20459
  }
20414
20460
  function emitReactiveAttributeUpdates(lines, ctx2) {
20415
20461
  if (ctx2.reactiveAttrs.length > 0) {
20462
+ const lower = makeCataloguedCallLowerer(ctx2);
20416
20463
  const attrsBySlot = /* @__PURE__ */ new Map();
20417
20464
  for (const attr of ctx2.reactiveAttrs) {
20418
20465
  if (!attrsBySlot.has(attr.slotId)) {
@@ -20425,7 +20472,7 @@ function emitReactiveAttributeUpdates(lines, ctx2) {
20425
20472
  lines.push(` createEffect(() => {`);
20426
20473
  lines.push(` if (_${v}) {`);
20427
20474
  for (const attr of attrs) {
20428
- const expression = rewriteDestructuredPropsInExpr(attr.expression, ctx2);
20475
+ const expression = rewriteDestructuredPropsInExpr(lower(attr.expression), ctx2);
20429
20476
  for (const stmt of emitAttrUpdate(`_${v}`, attr.attrName, expression, attr)) {
20430
20477
  lines.push(` ${stmt}`);
20431
20478
  }
@@ -24308,6 +24355,33 @@ function checkRichTypeMethodCalls(root2, metadata, errors) {
24308
24355
  const seen = /* @__PURE__ */ new Set();
24309
24356
  walkNode2(root2, metadata, EMPTY_BINDINGS2, matchers, errors, seen);
24310
24357
  }
24358
+ function checkRichTypePropSerialization(root2, metadata, errors, declLoc) {
24359
+ if (!metadata.propsType || !metadata.clientAnalysis?.needsInit) return;
24360
+ const usedProps = new Set(metadata.clientAnalysis.usedProps);
24361
+ const loc = declLoc ?? root2.loc;
24362
+ for (const param of metadata.propsParams) {
24363
+ if (param.isRest || param.name.startsWith("on") || param.name.startsWith("__")) continue;
24364
+ if (!usedProps.has(param.name)) continue;
24365
+ const declared = resolvePropDeclaredType(param.sourceName ?? param.name, metadata);
24366
+ const typeName = jsonUnsafeTypeName(declared);
24367
+ if (!typeName) continue;
24368
+ if (metadata.typeDefinitions.some((d) => d.name === typeName)) continue;
24369
+ pushPropSerializationDiagnostic(errors, loc, param.name, typeName, declared.raw);
24370
+ }
24371
+ }
24372
+ function pushPropSerializationDiagnostic(errors, loc, propName, typeName, declaredRaw) {
24373
+ 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";
24374
+ errors.push({
24375
+ code: ErrorCodes.RICH_TYPE_PROP_NOT_HYDRATABLE,
24376
+ severity: "error",
24377
+ 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}.`,
24378
+ loc,
24379
+ suggestion: {
24380
+ 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).",
24381
+ escape: [{ kind: "prop-precompute" }]
24382
+ }
24383
+ });
24384
+ }
24311
24385
  function isLoweringClaimed(matchers, callee, args2) {
24312
24386
  return matchers.some((m) => m(callee, args2) !== null);
24313
24387
  }
@@ -24321,20 +24395,38 @@ function receiverRootIsProp(expr, bindings) {
24321
24395
  while (root2.kind === "member" && !root2.computed) root2 = root2.object;
24322
24396
  return root2.kind === "identifier" && !bindings.has(root2.name);
24323
24397
  }
24398
+ function buildSuggestion(method2, receiverPath, receiver, typeName) {
24399
+ const revivalExpr = receiverPath === "<expression>" ? `wrapping the receiver in new ${typeName}(...) before calling .${method2}()` : `{/* @client */ new ${typeName}(${receiverPath}).${method2}(...)}`;
24400
+ 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)`;
24401
+ if (method2 === "toLocaleDateString" && typeName === "Date") {
24402
+ return {
24403
+ 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}.`,
24404
+ escape: [{ kind: "rewrite" }, { kind: "prop-precompute" }, { kind: "client-directive" }]
24405
+ };
24406
+ }
24407
+ if (JSON_REVIVABLE_RICH_TYPE_NAMES.has(typeName)) {
24408
+ return {
24409
+ message: `Pre-compute the value server-side and pass it as a prop. Alternatively, evaluate client-only by ${revivalExpr} \u2014 ${revivalReason}.`,
24410
+ escape: [{ kind: "prop-precompute" }, { kind: "client-directive" }]
24411
+ };
24412
+ }
24413
+ return {
24414
+ 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).`,
24415
+ escape: [{ kind: "prop-precompute" }]
24416
+ };
24417
+ }
24324
24418
  function pushDiagnostic(errors, seen, loc, method2, receiverPath, isProp, typeName) {
24325
24419
  const key = `${loc.start.line}:${loc.start.column}:${receiverPath}.${method2}`;
24326
24420
  if (seen.has(key)) return;
24327
24421
  seen.add(key);
24328
24422
  const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`;
24329
- 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.";
24423
+ const suggestion = buildSuggestion(method2, receiverPath, receiver, typeName);
24330
24424
  errors.push({
24331
24425
  code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
24332
24426
  severity: "error",
24333
24427
  message: `Expression cannot be compiled to marked template: method '.${method2}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
24334
24428
  loc,
24335
- suggestion: {
24336
- message: suggestion
24337
- }
24429
+ suggestion
24338
24430
  });
24339
24431
  }
24340
24432
  function checkExpr(expr, loc, meta, bindings, matchers, errors, seen) {
@@ -24565,6 +24657,7 @@ function compileMultipleComponents(source, filePath, componentNames, options2) {
24565
24657
  };
24566
24658
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
24567
24659
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
24660
+ checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx2.propsDestructuring?.loc);
24568
24661
  decideClientOnlyElision(componentIR.root);
24569
24662
  entries2.push({ componentIR, ctx: ctx2 });
24570
24663
  }
@@ -24971,6 +25064,7 @@ function compileJSX(source, filePath, options2) {
24971
25064
  };
24972
25065
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
24973
25066
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
25067
+ checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx2.propsDestructuring?.loc);
24974
25068
  decideClientOnlyElision(componentIR.root);
24975
25069
  if (ctx2.importedClientSignalNames.size > 0) {
24976
25070
  const sources = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/cli",
3
- "version": "0.31.6",
3
+ "version": "0.31.8",
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.6",
34
- "@barefootjs/shared": "0.31.6"
33
+ "@barefootjs/client": "0.31.8",
34
+ "@barefootjs/shared": "0.31.8"
35
35
  },
36
36
  "devDependencies": {
37
- "@barefootjs/jsx": "0.31.6",
38
- "@barefootjs/vite": "0.31.6",
37
+ "@barefootjs/jsx": "0.31.8",
38
+ "@barefootjs/vite": "0.31.8",
39
39
  "@happy-dom/global-registrator": "^20.0.11",
40
40
  "@types/node": "^22.0.0",
41
41
  "happy-dom": "^20.0.11"