@barefootjs/jsx 0.32.0 → 0.33.1
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/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +22 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +29 -6
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -80
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +12 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/dist/static-literal.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +2 -4
- package/src/__tests__/child-components-in-map.test.ts +11 -3
- package/src/__tests__/client-js-generation.test.ts +37 -1
- package/src/__tests__/expression-parser.test.ts +43 -6
- package/src/__tests__/inline-jsx-callback.test.ts +55 -0
- package/src/__tests__/ir-jsx-props.test.ts +148 -0
- package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
- package/src/__tests__/markup-prop-brand.test.ts +49 -0
- package/src/__tests__/nested-loop-conditional.test.ts +20 -11
- package/src/__tests__/return-through-local-var.test.ts +269 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +17 -3
- package/src/__tests__/ssr-defaults.test.ts +41 -0
- package/src/__tests__/ssr-seed-plan.test.ts +12 -2
- package/src/adapters/dangerous-inner-html.ts +1 -0
- package/src/adapters/parsed-expr-emitter.ts +49 -3
- package/src/analyzer.ts +71 -0
- package/src/errors.ts +17 -1
- package/src/expression-parser.ts +200 -89
- package/src/index.ts +2 -2
- package/src/ir-to-client-js/collect-elements.ts +31 -20
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
- package/src/ir-to-client-js/element-refs.ts +8 -0
- package/src/ir-to-client-js/emit-reactive.ts +91 -23
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +4 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +161 -9
- package/src/query-href-lowering.ts +4 -0
- package/src/rich-type-refusal.ts +1 -1
- package/src/ssr-defaults.ts +51 -2
- package/src/ssr-seed-plan.ts +20 -3
- package/src/static-literal.ts +15 -1
- package/src/to-locale-date-lowering.ts +4 -0
|
@@ -458,6 +458,47 @@ describe('extractSsrDefaults', () => {
|
|
|
458
458
|
// space mirrors Hono's empty-className interpolation).
|
|
459
459
|
expect(defaults?.classes).toEqual({ value: 'a b c d tail' })
|
|
460
460
|
})
|
|
461
|
+
|
|
462
|
+
// #2698 review: a property access on a non-plain-object base (an array
|
|
463
|
+
// element bound by `.map()`) must refuse (UNRESOLVED), not silently read
|
|
464
|
+
// `undefined` via `hasOwnProperty` — an array exposes prototype members
|
|
465
|
+
// (`.map`, `.filter`, …) that aren't own properties, so the old guard
|
|
466
|
+
// (`typeof !== 'object'`) let arrays through and misreported them as a
|
|
467
|
+
// real `undefined` value instead of an unrepresentable read.
|
|
468
|
+
test('.map() body property-access on an ARRAY element is UNRESOLVED, not undefined', () => {
|
|
469
|
+
const metadata = metadataFor(`
|
|
470
|
+
'use client'
|
|
471
|
+
import { createSignal } from '@barefootjs/client'
|
|
472
|
+
function C() {
|
|
473
|
+
const [x] = createSignal([[1, 2], [3, 4]].map(t => t.foo))
|
|
474
|
+
return <p>{x()}</p>
|
|
475
|
+
}
|
|
476
|
+
`)
|
|
477
|
+
|
|
478
|
+
const defaults = extractSsrDefaults(metadata)
|
|
479
|
+
// UNRESOLVED aborts the whole `.map()` (not a per-element `null`) —
|
|
480
|
+
// resultToJsonable renders the abort as the same `null` a genuinely
|
|
481
|
+
// undefined value would, but the two paths reach it differently: this
|
|
482
|
+
// pin exists to keep the array case going through the abort path.
|
|
483
|
+
expect(defaults?.x).toEqual({ value: null })
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
test('.map() body property-access on a PLAIN-OBJECT element still resolves a missing key to undefined', () => {
|
|
487
|
+
const metadata = metadataFor(`
|
|
488
|
+
'use client'
|
|
489
|
+
import { createSignal } from '@barefootjs/client'
|
|
490
|
+
function C() {
|
|
491
|
+
const [x] = createSignal([{ a: 1 }, {}].map(t => t.a))
|
|
492
|
+
return <p>{x()}</p>
|
|
493
|
+
}
|
|
494
|
+
`)
|
|
495
|
+
|
|
496
|
+
const defaults = extractSsrDefaults(metadata)
|
|
497
|
+
// Unlike the array case, a missing key on a plain object resolves that
|
|
498
|
+
// ONE element to `undefined` (→ `null`) and the `.map()` keeps going —
|
|
499
|
+
// it does not abort the whole computation.
|
|
500
|
+
expect(defaults?.x).toEqual({ value: [1, null] })
|
|
501
|
+
})
|
|
461
502
|
})
|
|
462
503
|
|
|
463
504
|
// TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
|
|
@@ -158,7 +158,12 @@ describe('computeSsrSeedPlan', () => {
|
|
|
158
158
|
if (label.kind === 'opaque') expect(label.origin).toBe('memo')
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
test('
|
|
161
|
+
test('object-literal body → derived (#2696: value-position object literal, isSupportedValue)', () => {
|
|
162
|
+
// Was `opaque` before `checkSupport` gained its `pos` parameter — a
|
|
163
|
+
// memo's WHOLE body is a value position (an assignment, never a
|
|
164
|
+
// render), so an object literal there is exactly the shape
|
|
165
|
+
// `isSupportedValue` now admits (every property value — here `n()` —
|
|
166
|
+
// is itself supported).
|
|
162
167
|
const plan = planFor(`
|
|
163
168
|
'use client'
|
|
164
169
|
import { createMemo, createSignal } from '@barefootjs/client'
|
|
@@ -169,7 +174,12 @@ describe('computeSsrSeedPlan', () => {
|
|
|
169
174
|
}
|
|
170
175
|
`)
|
|
171
176
|
|
|
172
|
-
|
|
177
|
+
const obj = step(plan, 'obj')
|
|
178
|
+
expect(obj.kind).toBe('derived')
|
|
179
|
+
if (obj.kind === 'derived') {
|
|
180
|
+
expect(obj.origin).toBe('memo')
|
|
181
|
+
expect(obj.frees).toEqual(['n'])
|
|
182
|
+
}
|
|
173
183
|
})
|
|
174
184
|
|
|
175
185
|
test('literal signal init → derived with empty frees (constant-skip is emit-side)', () => {
|
|
@@ -122,6 +122,7 @@ function htmlPropValue(parsed: ParsedExpr): ParsedExpr | null {
|
|
|
122
122
|
if (parsed.kind !== 'object-literal') return null
|
|
123
123
|
if (parsed.properties.length !== 1) return null
|
|
124
124
|
const [prop] = parsed.properties
|
|
125
|
+
if (prop.kind === 'spread') return null
|
|
125
126
|
if (prop.key !== '__html') return null
|
|
126
127
|
return prop.value
|
|
127
128
|
}
|
|
@@ -163,9 +163,11 @@ export interface ParsedExprEmitter {
|
|
|
163
163
|
arrow(params: string[], body: ParsedExpr, emit: (e: ParsedExpr) => string): string
|
|
164
164
|
regex(raw: string): string
|
|
165
165
|
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
166
|
-
// Emit an object literal `{ a: 1, b: x }
|
|
167
|
-
//
|
|
168
|
-
//
|
|
166
|
+
// Emit an object literal `{ a: 1, b: x, ...rest }` (#2696 Step 2 adds
|
|
167
|
+
// spread entries — `properties` is ORDER-PRESERVING, so an adapter must
|
|
168
|
+
// merge left-to-right, later entries winning, to match JS). `raw` is the
|
|
169
|
+
// original expression string so an adapter that doesn't lower object
|
|
170
|
+
// values yet can delegate to `unsupported(raw, …)` and stay byte-identical.
|
|
169
171
|
objectLiteral(
|
|
170
172
|
properties: ObjectLiteralProperty[],
|
|
171
173
|
raw: string,
|
|
@@ -277,6 +279,50 @@ export function groupBinaryOperand(operand: ParsedExpr, emitted: string): string
|
|
|
277
279
|
: emitted
|
|
278
280
|
}
|
|
279
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Split an `object-literal`'s (order-preserving) `properties` into rendered
|
|
284
|
+
* SEGMENTS an adapter's `objectLiteral()` folds with its own merge idiom
|
|
285
|
+
* (#2696 Step 2): each maximal run of consecutive `prop` entries collapses
|
|
286
|
+
* into ONE literal (via `literalOf`), and each `spread` entry becomes its
|
|
287
|
+
* own segment (its emitted source expression) — so `{ a: 1, b: 2, ...t, c: 3
|
|
288
|
+
* }` yields `[literal({a,b}), emit(t), literal({c})]`, in source order.
|
|
289
|
+
*
|
|
290
|
+
* A caller with NO spread entries at all should keep its pre-#2696 single
|
|
291
|
+
* `literalOf(properties)` call instead of this (this always returns at
|
|
292
|
+
* least one segment, but a caller folding 1-segment output through a merge
|
|
293
|
+
* call — `array_merge(x)`, `{}.merge(x)` — would be needlessly indirect
|
|
294
|
+
* for the common spread-free case).
|
|
295
|
+
*
|
|
296
|
+
* Shared here (not duplicated per adapter) because the grouping itself is
|
|
297
|
+
* backend-neutral structure — only `literalOf` (the target language's
|
|
298
|
+
* literal syntax) and the fold (the target language's merge idiom) are
|
|
299
|
+
* adapter-specific.
|
|
300
|
+
*/
|
|
301
|
+
export function groupObjectLiteralSegments(
|
|
302
|
+
properties: readonly ObjectLiteralProperty[],
|
|
303
|
+
literalOf: (run: ReadonlyArray<Extract<ObjectLiteralProperty, { kind: 'prop' }>>) => string,
|
|
304
|
+
emit: (e: ParsedExpr) => string,
|
|
305
|
+
): string[] {
|
|
306
|
+
const segments: string[] = []
|
|
307
|
+
let run: Extract<ObjectLiteralProperty, { kind: 'prop' }>[] = []
|
|
308
|
+
const flushRun = () => {
|
|
309
|
+
if (run.length > 0) {
|
|
310
|
+
segments.push(literalOf(run))
|
|
311
|
+
run = []
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const p of properties) {
|
|
315
|
+
if (p.kind === 'spread') {
|
|
316
|
+
flushRun()
|
|
317
|
+
segments.push(emit(p.expr))
|
|
318
|
+
} else {
|
|
319
|
+
run.push(p)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
flushRun()
|
|
323
|
+
return segments
|
|
324
|
+
}
|
|
325
|
+
|
|
280
326
|
/**
|
|
281
327
|
* Single point of dispatch from `ParsedExpr.kind` to the adapter's
|
|
282
328
|
* method. Adapters call this once at their entry point; the recursion
|
package/src/analyzer.ts
CHANGED
|
@@ -796,6 +796,29 @@ function visitComponentBody(node: ts.Node, ctx: AnalyzerContext): void {
|
|
|
796
796
|
(ts.isBlock(node) && node.parent === ctx.componentBodyBlock)
|
|
797
797
|
)
|
|
798
798
|
) {
|
|
799
|
+
// #2720: a bare top-level block whose ONLY job is naming the render
|
|
800
|
+
// value before returning it (`{ const __root = <jsx/>; return __root
|
|
801
|
+
// }`) would otherwise be swallowed whole by the opaque-block
|
|
802
|
+
// preservation above — this walk never recurses into it, so neither
|
|
803
|
+
// `jsxConstants` nor `jsxReturn` ever get set and the component
|
|
804
|
+
// silently produces zero files, zero diagnostics. Detect the shape
|
|
805
|
+
// before preserving it and report loudly instead.
|
|
806
|
+
if (ts.isBlock(node)) {
|
|
807
|
+
const returnedLocal = findBlockBodyReturnedJsxLocalName(node)
|
|
808
|
+
if (returnedLocal) {
|
|
809
|
+
ctx.errors.push(createError(
|
|
810
|
+
ErrorCodes.RETURN_VALUE_NOT_JSX,
|
|
811
|
+
getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
812
|
+
{
|
|
813
|
+
message:
|
|
814
|
+
`Component '${ctx.componentName ?? '(unknown)'}' return value is not recognized ` +
|
|
815
|
+
`as JSX — return the JSX expression directly instead of binding it to a local ` +
|
|
816
|
+
`variable first (\`return ${returnedLocal}\` after \`const ${returnedLocal} = ` +
|
|
817
|
+
`<jsx/>\` is not resolved at return position).`,
|
|
818
|
+
},
|
|
819
|
+
))
|
|
820
|
+
}
|
|
821
|
+
}
|
|
799
822
|
collectInitStatement(node, ctx)
|
|
800
823
|
return
|
|
801
824
|
}
|
|
@@ -886,6 +909,54 @@ export function unwrapJsxTransparent(expr: ts.Expression): ts.Expression {
|
|
|
886
909
|
return current
|
|
887
910
|
}
|
|
888
911
|
|
|
912
|
+
/**
|
|
913
|
+
* BF027 (#2720) shape detector: a block whose last statement returns a
|
|
914
|
+
* bare identifier, where some earlier statement in the SAME block declares
|
|
915
|
+
* that identifier as a `const`/`let` initialized to JSX (root JSX, or JSX
|
|
916
|
+
* nested in a ternary/`&&`/`||`/`??`) — `{ const __root = <jsx/>; return
|
|
917
|
+
* __root }`. Mirrors the same two "does this initializer hold JSX" checks
|
|
918
|
+
* `collectConstant` uses to populate `jsxConstants` / `inlineableJsxConsts`
|
|
919
|
+
* for ordinary top-level locals, applied here to a nested block that would
|
|
920
|
+
* otherwise never be walked (it is preserved whole as an opaque init
|
|
921
|
+
* statement, see #930). Returns the identifier's name on a match, else
|
|
922
|
+
* null — deliberately narrow (exact "name, then return that name" shape)
|
|
923
|
+
* so an ordinary block scoping unrelated imperative logic is untouched.
|
|
924
|
+
*/
|
|
925
|
+
function findBlockBodyReturnedJsxLocalName(block: ts.Block): string | null {
|
|
926
|
+
const stmts = block.statements
|
|
927
|
+
const last = stmts[stmts.length - 1]
|
|
928
|
+
if (!last || !ts.isReturnStatement(last) || !last.expression) return null
|
|
929
|
+
const returned = unwrapJsxTransparent(last.expression)
|
|
930
|
+
if (!ts.isIdentifier(returned)) return null
|
|
931
|
+
const name = returned.text
|
|
932
|
+
|
|
933
|
+
for (const stmt of stmts) {
|
|
934
|
+
if (!ts.isVariableStatement(stmt)) continue
|
|
935
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
936
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== name || !decl.initializer) continue
|
|
937
|
+
let init: ts.Expression = decl.initializer
|
|
938
|
+
while (ts.isParenthesizedExpression(init)) init = init.expression
|
|
939
|
+
if (
|
|
940
|
+
ts.isJsxElement(init) ||
|
|
941
|
+
ts.isJsxSelfClosingElement(init) ||
|
|
942
|
+
ts.isJsxFragment(init) ||
|
|
943
|
+
initializerShapeContainsJsx(init) ||
|
|
944
|
+
// `initializerShapeContainsJsx` deliberately stops at arrow
|
|
945
|
+
// boundaries, so a `.map()`/`.flatMap()` whose CALLBACK returns JSX
|
|
946
|
+
// needs the same dedicated check `collectConstant` uses to admit
|
|
947
|
+
// that shape into `inlineableJsxConsts` (#1554) — without it,
|
|
948
|
+
// `{ const __root = items.map(i => <div/>); return __root }` slips
|
|
949
|
+
// past BF027 back into the silent-drop path (Copilot review on
|
|
950
|
+
// #2726).
|
|
951
|
+
isMapLikeCallWithJsx(init)
|
|
952
|
+
) {
|
|
953
|
+
return name
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return null
|
|
958
|
+
}
|
|
959
|
+
|
|
889
960
|
/**
|
|
890
961
|
* Extract JSX element from an expression, handling parenthesized
|
|
891
962
|
* expressions and TS type-only wrappers (`as`, `satisfies`, `!`,
|
package/src/errors.ts
CHANGED
|
@@ -22,11 +22,24 @@ export const ErrorCodes = {
|
|
|
22
22
|
// Signal/Memo errors (BF011-BF019)
|
|
23
23
|
SIGNAL_OUTSIDE_COMPONENT: 'BF011',
|
|
24
24
|
|
|
25
|
-
// JSX errors (BF021-BF029)
|
|
25
|
+
// JSX errors (BF021-BF029). BF022 was retired (see
|
|
26
|
+
// `invalid-jsx-attribute.audit.test.ts`) and BF026 is reserved by
|
|
27
|
+
// `spec/callback-fidelity.md` for a future `.map()`-callback-shape
|
|
28
|
+
// diagnostic — BF027 is the next free slot.
|
|
26
29
|
UNSUPPORTED_JSX_PATTERN: 'BF021',
|
|
27
30
|
MISSING_KEY_IN_LIST: 'BF023',
|
|
28
31
|
MISSING_KEY_IN_NESTED_LIST: 'BF024',
|
|
29
32
|
UNSUPPORTED_DESTRUCTURE_REST: 'BF025',
|
|
33
|
+
// The component's return statement resolves to a bare identifier that
|
|
34
|
+
// refers to a local `const`/`let` whose initializer IS JSX (or a
|
|
35
|
+
// JSX-shaped ternary/`&&`/`||`/`??`), e.g. `const __root = <div/>; return
|
|
36
|
+
// __root`. JSX-child position resolves such identifiers through
|
|
37
|
+
// `jsxConstants` / `inlineableJsxConsts` (#547 / #1409), but return
|
|
38
|
+
// position deliberately does not (see `transformExpressionInner`'s
|
|
39
|
+
// docstring) — so the dispatcher's scalar-leaf fallback silently produces
|
|
40
|
+
// no IR and no diagnostic (#2720). Loud stopgap until the analyzer learns
|
|
41
|
+
// to resolve the identifier at return position too.
|
|
42
|
+
RETURN_VALUE_NOT_JSX: 'BF027',
|
|
30
43
|
|
|
31
44
|
// Component errors (BF043-BF049)
|
|
32
45
|
PROPS_DESTRUCTURING: 'BF043',
|
|
@@ -152,6 +165,9 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
152
165
|
// stable.
|
|
153
166
|
'Computed property key in .map() callback destructure is not supported. Rewrite the callback to destructure explicit bindings (e.g., `({ a, b }) => ...`) so the compiler can rewrite references to per-item signal accessors.',
|
|
154
167
|
|
|
168
|
+
[ErrorCodes.RETURN_VALUE_NOT_JSX]:
|
|
169
|
+
"Component's return value is not recognized as JSX — return the JSX expression directly instead of binding it to a local variable first.",
|
|
170
|
+
|
|
155
171
|
[ErrorCodes.PROPS_DESTRUCTURING]:
|
|
156
172
|
'Props destructuring in function parameters breaks reactivity. Use props object directly.',
|
|
157
173
|
[ErrorCodes.SIGNAL_GETTER_NOT_CALLED]:
|