@barefootjs/jsx 0.18.2 → 0.18.3
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/free-refs.d.ts +10 -0
- package/dist/free-refs.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/package.json +2 -2
- package/src/__tests__/render-nothing-literals.test.ts +108 -0
- package/src/free-refs.ts +13 -0
- package/src/jsx-to-ir.ts +50 -1
package/dist/free-refs.d.ts
CHANGED
|
@@ -37,5 +37,15 @@ export interface BindingEnvironment {
|
|
|
37
37
|
* through a local constant are recursively expanded so the consumer sees
|
|
38
38
|
* the constant's *kind composition*, not just the local name.
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* Whether `name` is bound by anything in the environment — an import, a
|
|
42
|
+
* local const/function, a prop param, a signal/memo, or an active loop
|
|
43
|
+
* param. Used by Phase 1's render-nothing-literal fold to distinguish
|
|
44
|
+
* the global `undefined` (renders nothing per JSX semantics) from a
|
|
45
|
+
* shadowed local binding named `undefined` (legal, if inadvisable — the
|
|
46
|
+
* shadowed VALUE must render). Cheap: the binding map is memoized per
|
|
47
|
+
* environment.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isNameBound(name: string, env: BindingEnvironment): boolean;
|
|
40
50
|
export declare function resolveFreeRefs(node: ts.Node, env: BindingEnvironment): FreeReference[];
|
|
41
51
|
//# sourceMappingURL=free-refs.d.ts.map
|
package/dist/free-refs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"free-refs.d.ts","sourceRoot":"","sources":["../src/free-refs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,KAAK,EAEV,aAAa,EAEb,UAAU,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAA;AAInB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,SAAS,EAAE,CAAA;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,cAAc,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,cAAc,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACnC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAChC,OAAO,EAAE,EAAE,CAAC,WAAW,GAAG,IAAI,CAAA;CAC/B;AA2VD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,GAAG,EAAE,kBAAkB,GACtB,aAAa,EAAE,CAEjB"}
|
|
1
|
+
{"version":3,"file":"free-refs.d.ts","sourceRoot":"","sources":["../src/free-refs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,KAAK,EAEV,aAAa,EAEb,UAAU,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAA;AAInB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,SAAS,EAAE,CAAA;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,cAAc,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,cAAc,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACnC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAChC,OAAO,EAAE,EAAE,CAAC,WAAW,GAAG,IAAI,CAAA;CAC/B;AA2VD;;;;;GAKG;AACH;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAE1E;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,GAAG,EAAE,kBAAkB,GACtB,aAAa,EAAE,CAEjB"}
|
package/dist/index.js
CHANGED
|
@@ -8026,6 +8026,9 @@ function resolveFreeRefsInternal(node, env, visited) {
|
|
|
8026
8026
|
}
|
|
8027
8027
|
return out;
|
|
8028
8028
|
}
|
|
8029
|
+
function isNameBound(name, env) {
|
|
8030
|
+
return buildBindingMap(env).has(name);
|
|
8031
|
+
}
|
|
8029
8032
|
function resolveFreeRefs(node, env) {
|
|
8030
8033
|
return resolveFreeRefsInternal(node, env, new Set);
|
|
8031
8034
|
}
|
|
@@ -8468,7 +8471,7 @@ function clientBuiltinTags(ctx) {
|
|
|
8468
8471
|
ctx._clientBuiltinTags = map;
|
|
8469
8472
|
return map;
|
|
8470
8473
|
}
|
|
8471
|
-
function
|
|
8474
|
+
function isNameBound2(ctx, name) {
|
|
8472
8475
|
const a = ctx.analyzer;
|
|
8473
8476
|
if (a.ambientGlobals.has(name))
|
|
8474
8477
|
return true;
|
|
@@ -8503,7 +8506,7 @@ function dispatchClientBuiltin(tagName, ctx, diagNode, transformAsync, transform
|
|
|
8503
8506
|
return transformAsync();
|
|
8504
8507
|
if (builtin === "Region")
|
|
8505
8508
|
return transformRegion();
|
|
8506
|
-
if (isClientBuiltinName(tagName) && !
|
|
8509
|
+
if (isClientBuiltinName(tagName) && !isNameBound2(ctx, tagName)) {
|
|
8507
8510
|
reportBuiltinNotImported(ctx, diagNode, tagName);
|
|
8508
8511
|
}
|
|
8509
8512
|
return null;
|
|
@@ -8818,6 +8821,13 @@ function transformChildren(children, ctx) {
|
|
|
8818
8821
|
}
|
|
8819
8822
|
return result;
|
|
8820
8823
|
}
|
|
8824
|
+
function isRenderNothingLiteral(expr, ctx) {
|
|
8825
|
+
let e = expr;
|
|
8826
|
+
while (ts11.isParenthesizedExpression(e) || ts11.isAsExpression(e) || ts11.isSatisfiesExpression(e) || ts11.isNonNullExpression(e)) {
|
|
8827
|
+
e = e.expression;
|
|
8828
|
+
}
|
|
8829
|
+
return e.kind === ts11.SyntaxKind.NullKeyword || e.kind === ts11.SyntaxKind.TrueKeyword || e.kind === ts11.SyntaxKind.FalseKeyword || ts11.isIdentifier(e) && e.text === "undefined" && !isNameBound("undefined", makeBindingEnv(ctx));
|
|
8830
|
+
}
|
|
8821
8831
|
function transformText(node, ctx) {
|
|
8822
8832
|
const text = node.text.replace(/\s+/g, " ");
|
|
8823
8833
|
if (text.trim() === "") {
|
|
@@ -8838,6 +8848,9 @@ function transformExpression(node, ctx) {
|
|
|
8838
8848
|
}
|
|
8839
8849
|
function transformExpressionInner(expr, ctx, node, isClientOnly) {
|
|
8840
8850
|
expr = tryDesugarInterleaveTaggedTemplate(expr, ctx);
|
|
8851
|
+
if (isRenderNothingLiteral(expr, ctx)) {
|
|
8852
|
+
return null;
|
|
8853
|
+
}
|
|
8841
8854
|
checkBareSignalOrMemoIdentifier(expr, ctx);
|
|
8842
8855
|
if (ts11.isIdentifier(expr)) {
|
|
8843
8856
|
const jsxNode = ctx.analyzer.jsxConstants.get(expr.text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jsx",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "JSX compiler for BarefootJS - transforms JSX to server HTML + client JS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"directory": "packages/jsx"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.18.
|
|
56
|
+
"@barefootjs/shared": "0.18.3"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/client": ">=0.2.0",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX render-nothing literal folding (#2171).
|
|
3
|
+
*
|
|
4
|
+
* Per JSX semantics, `{null}` / `{undefined}` / `{true}` / `{false}` in
|
|
5
|
+
* child position render NOTHING, while `{0}` renders "0" and `{''}`
|
|
6
|
+
* renders the empty string. Historically the literals fell through to
|
|
7
|
+
* the scalar IRExpression fallback and each adapter stringified them
|
|
8
|
+
* its own way (Hono emitted the text "null" for `{null}`; the template
|
|
9
|
+
* adapters emitted "false" for `{false}`) — the Priority-12 sweep's
|
|
10
|
+
* `falsy-text-values` fixture pinned that three-way divergence. Folding
|
|
11
|
+
* in Phase 1 means the IR simply carries no node for these children,
|
|
12
|
+
* so every adapter agrees by construction.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from 'bun:test'
|
|
16
|
+
import { analyzeComponent } from '../analyzer'
|
|
17
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
18
|
+
import type { IRElement, IRNode } from '../types'
|
|
19
|
+
|
|
20
|
+
function irFor(source: string): IRNode {
|
|
21
|
+
const analyzer = analyzeComponent(source, 'test.tsx')
|
|
22
|
+
const ir = jsxToIR(analyzer)
|
|
23
|
+
if (!ir) throw new Error('no IR produced')
|
|
24
|
+
return ir
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function childrenOf(ir: IRNode): IRNode[] {
|
|
28
|
+
return (ir as IRElement).children ?? []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('render-nothing literal children fold in Phase 1', () => {
|
|
32
|
+
test.each(['null', 'undefined', 'true', 'false'])(
|
|
33
|
+
'{%s} child produces no IR node',
|
|
34
|
+
(literal) => {
|
|
35
|
+
const ir = irFor(`
|
|
36
|
+
export function C() {
|
|
37
|
+
return <div><span>a</span>{${literal}}<span>b</span></div>
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
const children = childrenOf(ir)
|
|
41
|
+
expect(children).toHaveLength(2)
|
|
42
|
+
expect(children.every(c => c.type === 'element')).toBe(true)
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
test('transparently wrapped spellings fold too', () => {
|
|
47
|
+
for (const wrapped of ['(null)', 'null as unknown', 'undefined!']) {
|
|
48
|
+
const ir = irFor(`
|
|
49
|
+
export function C() {
|
|
50
|
+
return <div>{${wrapped}}</div>
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
expect(childrenOf(ir)).toHaveLength(0)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('{0} and {""} still produce expression children', () => {
|
|
58
|
+
const ir = irFor(`
|
|
59
|
+
export function C() {
|
|
60
|
+
return <div>{0}{''}</div>
|
|
61
|
+
}
|
|
62
|
+
`)
|
|
63
|
+
const children = childrenOf(ir)
|
|
64
|
+
expect(children).toHaveLength(2)
|
|
65
|
+
expect(children.every(c => c.type === 'expression')).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('a dynamic expression that may evaluate to null is NOT folded', () => {
|
|
69
|
+
const ir = irFor(`
|
|
70
|
+
export function C(props: { name?: string }) {
|
|
71
|
+
return <div>{props.name}</div>
|
|
72
|
+
}
|
|
73
|
+
`)
|
|
74
|
+
expect(childrenOf(ir)).toHaveLength(1)
|
|
75
|
+
expect(childrenOf(ir)[0].type).toBe('expression')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('a SHADOWED `undefined` binding is not folded (renders the value)', () => {
|
|
79
|
+
// `const undefined = …` is lint-hostile but legal JS — the shadowed
|
|
80
|
+
// binding's value must render, so the fold checks the binding
|
|
81
|
+
// environment before treating the identifier as the global.
|
|
82
|
+
const ir = irFor(`
|
|
83
|
+
export function C() {
|
|
84
|
+
const undefined = 'shadowed'
|
|
85
|
+
return <div>{undefined}</div>
|
|
86
|
+
}
|
|
87
|
+
`)
|
|
88
|
+
const children = childrenOf(ir)
|
|
89
|
+
expect(children).toHaveLength(1)
|
|
90
|
+
expect(children[0].type).toBe('expression')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('conditional null branches keep their existing handling', () => {
|
|
94
|
+
// `cond ? <a/> : null` routes through the conditional transformer,
|
|
95
|
+
// not the JSX-child scalar path — the fold must not disturb it.
|
|
96
|
+
const ir = irFor(`
|
|
97
|
+
'use client'
|
|
98
|
+
import { createSignal } from '@barefootjs/client'
|
|
99
|
+
export function C() {
|
|
100
|
+
const [show] = createSignal(false)
|
|
101
|
+
return <div>{show() ? <span>on</span> : null}</div>
|
|
102
|
+
}
|
|
103
|
+
`)
|
|
104
|
+
const children = childrenOf(ir)
|
|
105
|
+
expect(children).toHaveLength(1)
|
|
106
|
+
expect(children[0].type).toBe('conditional')
|
|
107
|
+
})
|
|
108
|
+
})
|
package/src/free-refs.ts
CHANGED
|
@@ -397,6 +397,19 @@ function resolveFreeRefsInternal(
|
|
|
397
397
|
* through a local constant are recursively expanded so the consumer sees
|
|
398
398
|
* the constant's *kind composition*, not just the local name.
|
|
399
399
|
*/
|
|
400
|
+
/**
|
|
401
|
+
* Whether `name` is bound by anything in the environment — an import, a
|
|
402
|
+
* local const/function, a prop param, a signal/memo, or an active loop
|
|
403
|
+
* param. Used by Phase 1's render-nothing-literal fold to distinguish
|
|
404
|
+
* the global `undefined` (renders nothing per JSX semantics) from a
|
|
405
|
+
* shadowed local binding named `undefined` (legal, if inadvisable — the
|
|
406
|
+
* shadowed VALUE must render). Cheap: the binding map is memoized per
|
|
407
|
+
* environment.
|
|
408
|
+
*/
|
|
409
|
+
export function isNameBound(name: string, env: BindingEnvironment): boolean {
|
|
410
|
+
return buildBindingMap(env).has(name)
|
|
411
|
+
}
|
|
412
|
+
|
|
400
413
|
export function resolveFreeRefs(
|
|
401
414
|
node: ts.Node,
|
|
402
415
|
env: BindingEnvironment
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
45
45
|
collectAstPropRefs,
|
|
46
46
|
} from './prop-rewrite.ts'
|
|
47
|
-
import { resolveFreeRefs, type BindingEnvironment } from './free-refs.ts'
|
|
47
|
+
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
48
48
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
49
49
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
50
50
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
@@ -1517,6 +1517,41 @@ function transformChildren(
|
|
|
1517
1517
|
// Text Transformation
|
|
1518
1518
|
// =============================================================================
|
|
1519
1519
|
|
|
1520
|
+
/**
|
|
1521
|
+
* True for the literals that render NOTHING in JSX child position per
|
|
1522
|
+
* JSX semantics: `null`, `undefined`, `true`, `false` — including
|
|
1523
|
+
* transparently wrapped spellings (`{(null)}`, `{null as any}`,
|
|
1524
|
+
* `{undefined!}`). Deliberately narrow: `0`, `NaN`, and `''` are NOT
|
|
1525
|
+
* here (`0` renders "0"; `''` renders empty by stringification), and a
|
|
1526
|
+
* dynamic expression that EVALUATES to null is a runtime concern the
|
|
1527
|
+
* client runtime / adapters own, not a compile-time fold.
|
|
1528
|
+
*
|
|
1529
|
+
* `undefined` is an Identifier, not a keyword, and CAN be legally
|
|
1530
|
+
* shadowed (`const undefined = 1` — lint-hostile but valid JS); the
|
|
1531
|
+
* shadowed binding's VALUE must render, so the identifier only folds
|
|
1532
|
+
* when nothing in the binding environment (imports, locals, props,
|
|
1533
|
+
* signals, memos, active loop params) binds the name.
|
|
1534
|
+
*/
|
|
1535
|
+
function isRenderNothingLiteral(expr: ts.Expression, ctx: TransformContext): boolean {
|
|
1536
|
+
let e = expr
|
|
1537
|
+
while (
|
|
1538
|
+
ts.isParenthesizedExpression(e) ||
|
|
1539
|
+
ts.isAsExpression(e) ||
|
|
1540
|
+
ts.isSatisfiesExpression(e) ||
|
|
1541
|
+
ts.isNonNullExpression(e)
|
|
1542
|
+
) {
|
|
1543
|
+
e = e.expression
|
|
1544
|
+
}
|
|
1545
|
+
return (
|
|
1546
|
+
e.kind === ts.SyntaxKind.NullKeyword ||
|
|
1547
|
+
e.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1548
|
+
e.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1549
|
+
(ts.isIdentifier(e) &&
|
|
1550
|
+
e.text === 'undefined' &&
|
|
1551
|
+
!isNameBoundInEnv('undefined', makeBindingEnv(ctx)))
|
|
1552
|
+
)
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1520
1555
|
function transformText(node: ts.JsxText, ctx: TransformContext): IRText | null {
|
|
1521
1556
|
// Normalize whitespace (React-like behavior)
|
|
1522
1557
|
const text = node.text.replace(/\s+/g, ' ')
|
|
@@ -1576,6 +1611,20 @@ function transformExpressionInner(
|
|
|
1576
1611
|
// scalar fallback's `ctx.getJS`) sees the untagged template literal.
|
|
1577
1612
|
expr = tryDesugarInterleaveTaggedTemplate(expr, ctx)
|
|
1578
1613
|
|
|
1614
|
+
// JSX render-nothing literals (#2171): `{null}` / `{undefined}` /
|
|
1615
|
+
// `{true}` / `{false}` in child position render NOTHING per JSX
|
|
1616
|
+
// semantics (`{0}` and `{''}` still render their stringification).
|
|
1617
|
+
// Folding here — Phase 1, before any adapter sees the node — is what
|
|
1618
|
+
// makes every backend agree: previously the literal fell through to
|
|
1619
|
+
// the scalar IRExpression fallback and each adapter stringified it
|
|
1620
|
+
// its own way (the Hono reference emitted the text "null" for
|
|
1621
|
+
// `{null}`, template adapters emitted "false" for `{false}`). The
|
|
1622
|
+
// conditional-branch path (`cond ? <a/> : null`) is NOT routed
|
|
1623
|
+
// through here and keeps its own null-branch handling.
|
|
1624
|
+
if (isRenderNothingLiteral(expr, ctx)) {
|
|
1625
|
+
return null
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1579
1628
|
// Check for bare signal/memo identifier (BF044)
|
|
1580
1629
|
checkBareSignalOrMemoIdentifier(expr, ctx)
|
|
1581
1630
|
|