@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
|
-
// ✅
|
|
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
|
-
##
|
|
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
|
|
20343
|
-
const
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
34
|
-
"@barefootjs/shared": "0.31.
|
|
33
|
+
"@barefootjs/client": "0.31.7",
|
|
34
|
+
"@barefootjs/shared": "0.31.7"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@barefootjs/jsx": "0.31.
|
|
38
|
-
"@barefootjs/vite": "0.31.
|
|
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"
|