@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.
- package/dist/docs/core/advanced/error-codes.md +65 -5
- package/dist/index.js +109 -15
- package/package.json +5 -5
|
@@ -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
|
-
// ✅
|
|
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–
|
|
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(
|
|
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(
|
|
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
|
|
20352
|
-
const
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
34
|
-
"@barefootjs/shared": "0.31.
|
|
33
|
+
"@barefootjs/client": "0.31.8",
|
|
34
|
+
"@barefootjs/shared": "0.31.8"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@barefootjs/jsx": "0.31.
|
|
38
|
-
"@barefootjs/vite": "0.31.
|
|
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"
|