@barefootjs/jsx 0.17.0 → 0.18.0
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/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/interface.d.ts +20 -4
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +19 -0
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1728 -969
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +41 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +22 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +55 -18
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +13 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/relocate.d.ts +28 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
- package/src/__tests__/augment-inherited-props.test.ts +96 -0
- package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
- package/src/__tests__/compiler-stress-1244.test.ts +13 -4
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
- package/src/__tests__/destructured-map-params.test.ts +11 -1
- package/src/__tests__/expression-parser.test.ts +74 -3
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/ir-sort-comparator.test.ts +261 -0
- package/src/__tests__/loop-destructure.test.ts +313 -0
- package/src/__tests__/loop-hoisted-template.test.ts +235 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
- package/src/__tests__/tagged-template-interleave.test.ts +268 -0
- package/src/__tests__/unsupported-expression.test.ts +194 -7
- package/src/adapters/env-signal.ts +57 -9
- package/src/adapters/interface.ts +20 -4
- package/src/adapters/parsed-expr-emitter.ts +19 -2
- package/src/analyzer-context.ts +20 -0
- package/src/analyzer.ts +74 -1
- package/src/augment-inherited-props.ts +139 -9
- package/src/compiler.ts +10 -1
- package/src/expression-parser.ts +421 -50
- package/src/index.ts +30 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -1
- package/src/ir-to-client-js/compute-inlinability.ts +6 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
- package/src/ir-to-client-js/emit-registration.ts +4 -2
- package/src/ir-to-client-js/html-template.ts +198 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/types.ts +22 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/loop-destructure.ts +89 -36
- package/src/lowering-registry.ts +16 -0
- package/src/relocate.ts +201 -14
- package/src/ssr-defaults.ts +34 -32
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +76 -0
|
@@ -76,6 +76,102 @@ describe('augmentInheritedPropAccesses', () => {
|
|
|
76
76
|
}
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
test('sees text-expression, condition, loop-array, and component-prop reads (#2126)', () => {
|
|
80
|
+
// These carriers all lower `props.X` to a bare template scalar, so a
|
|
81
|
+
// missed read means the emitted template references a var the props
|
|
82
|
+
// type / ssrDefaults never declared — a strict-mode 500 on the
|
|
83
|
+
// Perl-family adapters, a missing struct field on Go.
|
|
84
|
+
const ir = makeIR({
|
|
85
|
+
propsObjectName: 'props',
|
|
86
|
+
propsParams: [],
|
|
87
|
+
root: {
|
|
88
|
+
type: 'element',
|
|
89
|
+
tag: 'div',
|
|
90
|
+
attrs: [],
|
|
91
|
+
children: [
|
|
92
|
+
// dynamic text child → string (default classification)
|
|
93
|
+
{ type: 'expression', expr: 'props.label' },
|
|
94
|
+
// conditional condition → nillable (truth-tested only)
|
|
95
|
+
{
|
|
96
|
+
type: 'conditional',
|
|
97
|
+
condition: 'props.show',
|
|
98
|
+
whenTrue: { type: 'element', tag: 'span', attrs: [], children: [] },
|
|
99
|
+
whenFalse: { type: 'text', value: '' },
|
|
100
|
+
},
|
|
101
|
+
// loop array → nillable (iterated only)
|
|
102
|
+
{
|
|
103
|
+
type: 'loop',
|
|
104
|
+
array: '(props.items ?? [])',
|
|
105
|
+
param: 'it',
|
|
106
|
+
children: [{ type: 'expression', expr: 'it' }],
|
|
107
|
+
},
|
|
108
|
+
// component prop values → scanned, but NOT as HTML attributes:
|
|
109
|
+
// no boolean-attr-by-name inference; nillable unless a
|
|
110
|
+
// `?? <literal>` pins a concrete type
|
|
111
|
+
{
|
|
112
|
+
type: 'component',
|
|
113
|
+
name: 'Child',
|
|
114
|
+
props: [
|
|
115
|
+
{ name: 'value', value: { kind: 'expression', expr: 'props.childValue ?? 1' } },
|
|
116
|
+
{ name: 'ref', value: { kind: 'expression', expr: 'props.childRef' } },
|
|
117
|
+
// `disabled` is a boolean HTML attr name, but on a component
|
|
118
|
+
// prop that inference must not fire — stays nillable.
|
|
119
|
+
{ name: 'disabled', value: { kind: 'expression', expr: 'props.childFlag' } },
|
|
120
|
+
],
|
|
121
|
+
children: [],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
augmentInheritedPropAccesses(ir)
|
|
128
|
+
|
|
129
|
+
const byName = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
130
|
+
expect(byName.get('label')?.type).toMatchObject({ kind: 'primitive', primitive: 'string' })
|
|
131
|
+
expect(byName.get('show')?.type).toMatchObject({ kind: 'unknown' })
|
|
132
|
+
expect(byName.get('items')?.type).toMatchObject({ kind: 'unknown' })
|
|
133
|
+
// Component-prop reads: `?? 1` pins number; bare reads stay nillable,
|
|
134
|
+
// including on boolean-HTML-attr names (`disabled`).
|
|
135
|
+
expect(byName.get('childValue')?.type).toMatchObject({ kind: 'primitive', primitive: 'number' })
|
|
136
|
+
expect(byName.get('childRef')?.type).toMatchObject({ kind: 'unknown' })
|
|
137
|
+
expect(byName.get('childFlag')?.type).toMatchObject({ kind: 'unknown' })
|
|
138
|
+
for (const name of ['label', 'show', 'items', 'childValue', 'childRef', 'childFlag']) {
|
|
139
|
+
expect(byName.get(name)?.optional).toBe(true)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('pins a synthetic param to the `?? <literal>` fallback type', () => {
|
|
144
|
+
// The Go adapter seeds a prop-derived signal from the synthetic field
|
|
145
|
+
// (`Count: in.Initial` where `Count int` came from evaluating
|
|
146
|
+
// `props.initial ?? 0`), so the field type must match the literal —
|
|
147
|
+
// the old string default produced `cannot use in.Initial (variable
|
|
148
|
+
// of type string) as int value`.
|
|
149
|
+
const ir = makeIR({
|
|
150
|
+
propsObjectName: 'props',
|
|
151
|
+
propsParams: [],
|
|
152
|
+
memos: [{ computation: 'props.step ?? 1' }],
|
|
153
|
+
root: {
|
|
154
|
+
type: 'element',
|
|
155
|
+
tag: 'div',
|
|
156
|
+
attrs: [],
|
|
157
|
+
children: [{ type: 'expression', expr: 'props.title ?? "untitled"' }],
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
ir.metadata.signals = [
|
|
161
|
+
{ getter: 'count', initialValue: 'props.initial ?? 0' },
|
|
162
|
+
{ getter: 'on', initialValue: 'props.defaultOn ?? false' },
|
|
163
|
+
] as never
|
|
164
|
+
|
|
165
|
+
augmentInheritedPropAccesses(ir)
|
|
166
|
+
|
|
167
|
+
const byName = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
168
|
+
expect(byName.get('initial')?.type).toMatchObject({ kind: 'primitive', primitive: 'number' })
|
|
169
|
+
expect(byName.get('defaultOn')?.type).toMatchObject({ kind: 'primitive', primitive: 'boolean' })
|
|
170
|
+
expect(byName.get('step')?.type).toMatchObject({ kind: 'primitive', primitive: 'number' })
|
|
171
|
+
// Text-expression coalesce reads keep their string classification.
|
|
172
|
+
expect(byName.get('title')?.type).toMatchObject({ kind: 'primitive', primitive: 'string' })
|
|
173
|
+
})
|
|
174
|
+
|
|
79
175
|
test('is idempotent — re-running adds nothing', () => {
|
|
80
176
|
const ir = makeIR({
|
|
81
177
|
propsObjectName: 'props',
|
|
@@ -261,11 +261,21 @@ describe('Compiler-Runtime Contract', () => {
|
|
|
261
261
|
})
|
|
262
262
|
|
|
263
263
|
test('issue #1321: rest spread on typed module-level function param survives emit', () => {
|
|
264
|
+
// The tag body joins with a `-` separator between `p` and the
|
|
265
|
+
// interleaved arg — deliberately OFF the #2092 interleave-tag
|
|
266
|
+
// catalogue (which requires the exact `(acc + p) + X` two-term
|
|
267
|
+
// chain), so `cn` stays an opaque tagged-template callee that must
|
|
268
|
+
// still be shipped to the client. This keeps the #1321 regression
|
|
269
|
+
// (rest token survives type-strip + module-level emission) pinned
|
|
270
|
+
// against a real "helper still needed" case; #2092's own desugar
|
|
271
|
+
// coverage lives in `tagged-template-interleave.test.ts` and the
|
|
272
|
+
// `compiler-stress-1244.test.ts` "tagged template literal for
|
|
273
|
+
// className" describe block.
|
|
264
274
|
const js = compileClient(`
|
|
265
275
|
"use client"
|
|
266
276
|
import { createSignal } from '@barefootjs/client'
|
|
267
277
|
function cn(parts: TemplateStringsArray, ...args: unknown[]): string {
|
|
268
|
-
return parts.reduce<string>((acc, p, i) => acc + p + (args[i] ?? ''), '')
|
|
278
|
+
return parts.reduce<string>((acc, p, i) => acc + p + '-' + (args[i] ?? ''), '')
|
|
269
279
|
}
|
|
270
280
|
export function TagDemo() {
|
|
271
281
|
const [tone, setTone] = createSignal('primary')
|
|
@@ -1554,9 +1554,11 @@ describe('JSX spread of a reactive object', () => {
|
|
|
1554
1554
|
|
|
1555
1555
|
describe('tagged template literal for className', () => {
|
|
1556
1556
|
// `cn\`base ${signal()}\`` is the most common alternative to `cva` in
|
|
1557
|
-
// small codebases.
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1557
|
+
// small codebases. Resolved by #2092: `cn`'s body structurally matches
|
|
1558
|
+
// the interleave-tag catalogue (`parts.reduce((acc, p, i) => acc + p +
|
|
1559
|
+
// (args[i] ?? ''), '')`), so the tagged template desugars to the
|
|
1560
|
+
// equivalent untagged template literal — the signal dependency inside
|
|
1561
|
+
// the placeholder IS analysed, producing a reactive className binding.
|
|
1560
1562
|
test('cn`base ${signal()}` produces the className binding', () => {
|
|
1561
1563
|
const src = `
|
|
1562
1564
|
'use client'
|
|
@@ -1569,7 +1571,14 @@ describe('tagged template literal for className', () => {
|
|
|
1569
1571
|
return <div onClick={() => setTone('secondary')} className={cn\`base \${tone()}\`} />
|
|
1570
1572
|
}
|
|
1571
1573
|
`
|
|
1572
|
-
|
|
1574
|
+
const c = compile(src)
|
|
1575
|
+
expectNoFatalErrors(c)
|
|
1576
|
+
// The tag call is gone from the emitted client JS — replaced by the
|
|
1577
|
+
// desugared untagged template literal — and the className update
|
|
1578
|
+
// effect reads the signal getter reactively.
|
|
1579
|
+
expect(c.clientJs).not.toMatch(/\bcn`/)
|
|
1580
|
+
expect(c.clientJs).toContain('`base ${(tone()) ?? \'\'}`')
|
|
1581
|
+
expect(c.clientJs).toMatch(/setAttribute\('class',\s*String\(__v\)\)/)
|
|
1573
1582
|
})
|
|
1574
1583
|
})
|
|
1575
1584
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #2106: the templatePrimitives V2 widening (#2069)
|
|
3
|
+
* let a call whose RAW callee is an identifier path
|
|
4
|
+
* (`someModuleArray.includes(prop)`) pass `computeInlinability`'s Stage-2
|
|
5
|
+
* classification (`classifyConstantInitial`, compute-inlinability.ts) —
|
|
6
|
+
* e.g. via an adapter with a broad `acceptsTemplateCall` (Hono's SSR
|
|
7
|
+
* runtime is a full JS engine, so it accepts any identifier-path callee).
|
|
8
|
+
* That check runs on the RAW initializer text so it can still see
|
|
9
|
+
* bridged prop args for the #1138 rejection.
|
|
10
|
+
*
|
|
11
|
+
* `populateCsrInlinable` (compute-inlinability.ts) re-runs the same
|
|
12
|
+
* `isInlinableInTemplate` check on the CSR-*substituted* form, where the
|
|
13
|
+
* module-scope array has already been literal-inlined
|
|
14
|
+
* (`['plus','minus'].includes(name)`). The callee is no longer an
|
|
15
|
+
* identifier path at all, so the adapter can't vouch for it, and the
|
|
16
|
+
* bridged-arg rejection correctly fires — recorded as
|
|
17
|
+
* `ctx.csrInlinable.get(name) === null`.
|
|
18
|
+
*
|
|
19
|
+
* Pre-fix, `unsafeLocalNames` (Stage-2-derived) didn't know about this
|
|
20
|
+
* second, stricter refusal, so the CSR template emitter
|
|
21
|
+
* (`generateCsrTemplate` / `transformExpr` in html-template.ts) found
|
|
22
|
+
* neither a substitution (excluded by `csrInlinableConstantsFromCtx`,
|
|
23
|
+
* which skips null entries) NOR an unsafe flag — the bare,
|
|
24
|
+
* module-scope-invisible identifier leaked straight into the emitted
|
|
25
|
+
* template text, throwing `ReferenceError` when the template function
|
|
26
|
+
* actually ran. This is the same bug class (and same shape) as the real
|
|
27
|
+
* repro in `ui/components/ui/icon`'s `linecap` constant.
|
|
28
|
+
*
|
|
29
|
+
* Fix: `generateCsrTemplate` (html-template.ts) folds `ctx.csrInlinable`'s
|
|
30
|
+
* null verdicts into the unsafe-name set it uses internally
|
|
31
|
+
* (`mergeCsrNullUnsafe`), so a name `populateCsrInlinable` refused is
|
|
32
|
+
* always treated as unsafe at the CSR template layer, regardless of what
|
|
33
|
+
* the looser Stage-2 check concluded elsewhere.
|
|
34
|
+
*/
|
|
35
|
+
import { describe, test, expect } from 'bun:test'
|
|
36
|
+
import { compileJSX } from '../compiler'
|
|
37
|
+
import { HonoAdapter } from '../../../adapter-hono/src/adapter/hono-adapter'
|
|
38
|
+
import { extractTemplateBody } from './staged-ir/helpers'
|
|
39
|
+
import { RUNTIME_IMPORT_CANDIDATES } from '../ir-to-client-js/imports'
|
|
40
|
+
|
|
41
|
+
const onlyErrors = (errors: { severity?: string }[]) =>
|
|
42
|
+
errors.filter((e) => e.severity === 'error')
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compile `source` with `HonoAdapter` (broad `acceptsTemplateCall`, the
|
|
46
|
+
* ingredient the real bug needs) and extract the CSR template's
|
|
47
|
+
* back-tick body.
|
|
48
|
+
*/
|
|
49
|
+
function compileTemplateBody(source: string, fileName: string): { templateBody: string; clientJs: string; errors: string[] } {
|
|
50
|
+
const result = compileJSX(source, fileName, { adapter: new HonoAdapter() })
|
|
51
|
+
expect(onlyErrors(result.errors)).toHaveLength(0)
|
|
52
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
53
|
+
return { templateBody: extractTemplateBody(clientJs), clientJs, errors: result.errors.map((e) => e.code) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Evaluate an extracted CSR template body as a real template-literal
|
|
58
|
+
* function, with every runtime helper the compiler might reference
|
|
59
|
+
* stubbed out. Mirrors what `hydrate()` does at runtime, minus the DOM.
|
|
60
|
+
*/
|
|
61
|
+
function evalTemplate(templateBody: string, props: Record<string, unknown>): string {
|
|
62
|
+
const helperNames = [...RUNTIME_IMPORT_CANDIDATES]
|
|
63
|
+
const stubs = helperNames.map((name) => {
|
|
64
|
+
if (name === 'escapeAttr' || name === 'escapeText' || name === 'styleToCss') {
|
|
65
|
+
return (v: unknown) => String(v)
|
|
66
|
+
}
|
|
67
|
+
return (..._args: unknown[]) => ''
|
|
68
|
+
})
|
|
69
|
+
// eslint-disable-next-line no-new-func
|
|
70
|
+
const fn = new Function('_p', ...helperNames, `return \`${templateBody}\``)
|
|
71
|
+
return fn(props, ...stubs)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('CSR template: Stage-2 / post-substitution inline-safety divergence (#2106)', () => {
|
|
75
|
+
test('a const whose RAW form is adapter-accepted but whose CSR-substituted form is refused falls back to undefined, not a bare identifier', () => {
|
|
76
|
+
const source = `
|
|
77
|
+
'use client'
|
|
78
|
+
import { createSignal } from '@barefootjs/client'
|
|
79
|
+
|
|
80
|
+
const buttLinecapIcons = ['plus', 'minus']
|
|
81
|
+
|
|
82
|
+
export function CsrNullDivergence({ name }: { name: string }) {
|
|
83
|
+
const [count] = createSignal(0)
|
|
84
|
+
const linecap = (buttLinecapIcons).includes(name) ? 'butt' : 'round'
|
|
85
|
+
return <div data-linecap={linecap}>{count()}</div>
|
|
86
|
+
}
|
|
87
|
+
`
|
|
88
|
+
const { templateBody } = compileTemplateBody(source, 'CsrNullDivergence.tsx')
|
|
89
|
+
|
|
90
|
+
// (a) No bare `linecap` identifier anywhere in the template body.
|
|
91
|
+
// Excludes the HTML attribute name text `data-linecap="` (preceded by
|
|
92
|
+
// `-`, not a real identifier reference) from the check.
|
|
93
|
+
expect(templateBody).not.toMatch(/(?<![-.\w$])linecap(?![\w$])/)
|
|
94
|
+
|
|
95
|
+
// (c) The fallback is the spec'd null-guarded `undefined` shape
|
|
96
|
+
// (`(undefined) != null ? '...' : ''`), the same fallback every other
|
|
97
|
+
// unsafe-template-scope reference degrades to (html-template.ts's
|
|
98
|
+
// `templateAttrExpr` generic path).
|
|
99
|
+
expect(templateBody).toMatch(/\(undefined\)\s*!=\s*null/)
|
|
100
|
+
|
|
101
|
+
// (b) Evaluating the template with stub runtime helpers must not
|
|
102
|
+
// throw `ReferenceError: linecap is not defined` — the pre-fix
|
|
103
|
+
// manifestation of this bug.
|
|
104
|
+
let rendered = ''
|
|
105
|
+
expect(() => { rendered = evalTemplate(templateBody, { name: 'plus', size: 'md', className: '' }) }).not.toThrow()
|
|
106
|
+
|
|
107
|
+
// The dropped-attribute fallback: SSR/CSR-initial HTML omits the
|
|
108
|
+
// attribute entirely rather than emitting a bogus value.
|
|
109
|
+
expect(rendered).not.toContain('linecap=')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('control: a plain object-literal lookup (no call involved) still inlines through the same CSR path', () => {
|
|
113
|
+
// Guards against over-suppression: the fix must only affect names
|
|
114
|
+
// `ctx.csrInlinable` actually refused (`=== null`), not every const
|
|
115
|
+
// whose value happens to reference a module-scope constant.
|
|
116
|
+
const source = `
|
|
117
|
+
'use client'
|
|
118
|
+
import { createSignal } from '@barefootjs/client'
|
|
119
|
+
|
|
120
|
+
const sizeMap: Record<string, string> = { sm: 'h-4', md: 'h-6' }
|
|
121
|
+
|
|
122
|
+
export function CsrStillInlines({ size }: { size: string }) {
|
|
123
|
+
const [count] = createSignal(0)
|
|
124
|
+
const cls = sizeMap[size]
|
|
125
|
+
return <div data-cls={cls}>{count()}</div>
|
|
126
|
+
}
|
|
127
|
+
`
|
|
128
|
+
const { templateBody } = compileTemplateBody(source, 'CsrStillInlines.tsx')
|
|
129
|
+
|
|
130
|
+
// No bare `cls` identifier left in the template — it was inlined.
|
|
131
|
+
expect(templateBody).not.toMatch(/(?<![-.\w$])cls(?![\w$])/)
|
|
132
|
+
// The module-scope object literal was substituted in place of `sizeMap`,
|
|
133
|
+
// and the prop was correctly bridged to `_p.size` — not marked unsafe.
|
|
134
|
+
expect(templateBody).toContain(`{ sm: 'h-4', md: 'h-6' })[_p.size]`)
|
|
135
|
+
expect(templateBody).not.toContain('undefined')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -51,7 +51,17 @@ describe('destructured .map() param rewriting (#951)', () => {
|
|
|
51
51
|
// Binding references rewritten to accessor paths
|
|
52
52
|
expect(js).toContain('__bfItem().label')
|
|
53
53
|
expect(js).toContain('__bfItem().done')
|
|
54
|
-
|
|
54
|
+
// `id` is referenced ONLY via `key={id}` in this fixture. The loop's
|
|
55
|
+
// `keyFn` argument to `mapArray` evaluates against the raw destructured
|
|
56
|
+
// item (before per-item signal wrapping) — `({ id, label, done }) =>
|
|
57
|
+
// String(id)` — so `id` never needs the `__bfItem()` accessor rewrite at
|
|
58
|
+
// all; `mapArray` stamps the real `data-key` from that keyFn onto every
|
|
59
|
+
// freshly created element itself (see `map-array.ts`). The hoisted
|
|
60
|
+
// shared-template fast path (perf) therefore omits the key interpolation
|
|
61
|
+
// from the once-per-loop skeleton (`data-key=""` placeholder) rather than
|
|
62
|
+
// baking a `__bfItem().id` reference into it — no `__bfItem().id`
|
|
63
|
+
// occurrence anywhere is the CORRECT, not incomplete, output here.
|
|
64
|
+
expect(js).toContain('({ id, label, done }) => String(id)')
|
|
55
65
|
})
|
|
56
66
|
|
|
57
67
|
test('tuple destructure with hole: references become __bfItem()[index]', () => {
|
|
@@ -1325,6 +1325,45 @@ describe('expression-parser', () => {
|
|
|
1325
1325
|
expect(result.level).toBe('L4')
|
|
1326
1326
|
})
|
|
1327
1327
|
|
|
1328
|
+
// #2087: `x ?? {}` — the chart component's `<Ctx.Provider value={{
|
|
1329
|
+
// config: props.config ?? {} }}>` needs an EMPTY object-literal fallback
|
|
1330
|
+
// operand admitted, narrowly, as the right side of `??` only.
|
|
1331
|
+
test('L4: nullish coalescing with an EMPTY object-literal fallback is supported (#2087)', () => {
|
|
1332
|
+
const expr = parseExpression('props.config ?? {}')
|
|
1333
|
+
expect(expr.kind).toBe('logical')
|
|
1334
|
+
const result = isSupported(expr)
|
|
1335
|
+
expect(result.supported).toBe(true)
|
|
1336
|
+
expect(result.level).toBe('L4')
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
test('a NON-EMPTY object-literal fallback of `??` is still refused (#2087 scope)', () => {
|
|
1340
|
+
const expr = parseExpression('props.config ?? ({ a: 1 })')
|
|
1341
|
+
expect(expr.kind).toBe('logical')
|
|
1342
|
+
const result = isSupported(expr)
|
|
1343
|
+
expect(result.supported).toBe(false)
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
test('an empty object-literal fallback of `&&` / `||` is still refused — #2087 scopes the relaxation to `??` only', () => {
|
|
1347
|
+
expect(isSupported(parseExpression('props.config && ({})')).supported).toBe(false)
|
|
1348
|
+
expect(isSupported(parseExpression('props.config || ({})')).supported).toBe(false)
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
test('a standalone (non-fallback) object literal is still refused', () => {
|
|
1352
|
+
const expr = parseExpression('({})')
|
|
1353
|
+
const result = isSupported(expr)
|
|
1354
|
+
expect(result.supported).toBe(false)
|
|
1355
|
+
expect(result.reason).toBe('Unsupported syntax: ObjectLiteralExpression')
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
test('an empty object-literal on the LEFT of `??` does not trigger the fallback relaxation', () => {
|
|
1359
|
+
// The relaxation only inspects `expr.right`; a left-hand object
|
|
1360
|
+
// literal has no fallback role and stays refused via the general
|
|
1361
|
+
// `checkSupport(expr.left)` refusal.
|
|
1362
|
+
const expr = parseExpression('({}) ?? props.config')
|
|
1363
|
+
const result = isSupported(expr)
|
|
1364
|
+
expect(result.supported).toBe(false)
|
|
1365
|
+
})
|
|
1366
|
+
|
|
1328
1367
|
test('L5: filter() with simple predicate IS supported', () => {
|
|
1329
1368
|
const expr = parseExpression('items().filter(x => x.done)')
|
|
1330
1369
|
const result = isSupported(expr)
|
|
@@ -1383,9 +1422,19 @@ describe('expression-parser', () => {
|
|
|
1383
1422
|
expect(result.level).toBe('L2')
|
|
1384
1423
|
})
|
|
1385
1424
|
|
|
1386
|
-
test('L5: map()
|
|
1425
|
+
test('L5: value-producing map() with an arrow IS supported (evaluator lowering, #2073)', () => {
|
|
1387
1426
|
const expr = parseExpression('items().map(x => x.name)')
|
|
1388
1427
|
const result = isSupported(expr)
|
|
1428
|
+
expect(result.supported).toBe(true)
|
|
1429
|
+
expect(result.level).toBe('L5')
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
test('L5: map() with a function-reference callback is NOT supported', () => {
|
|
1433
|
+
// No arrow argument → not a CALLBACK_METHODS shape; falls through to
|
|
1434
|
+
// the UNSUPPORTED_METHODS gate and refuses loudly (like reduce's
|
|
1435
|
+
// no-init fall-through).
|
|
1436
|
+
const expr = parseExpression('items().map(format)')
|
|
1437
|
+
const result = isSupported(expr)
|
|
1389
1438
|
expect(result.supported).toBe(false)
|
|
1390
1439
|
expect(result.level).toBe('L5_UNSUPPORTED')
|
|
1391
1440
|
expect(result.reason).toContain('map')
|
|
@@ -1690,14 +1739,30 @@ describe('expression-parser — .flat(depth?) lowering (#1448 Tier C)', () => {
|
|
|
1690
1739
|
})
|
|
1691
1740
|
}
|
|
1692
1741
|
|
|
1693
|
-
test('non-literal depth
|
|
1742
|
+
test('a non-literal depth that itself resolves becomes a DYNAMIC depth (#2094)', () => {
|
|
1743
|
+
// `n` parses to a supported `identifier` — the depth isn't known until
|
|
1744
|
+
// render time, but it's still an EXPRESSIBLE one, so it's accepted as a
|
|
1745
|
+
// dynamic depth (`depthExpr`) instead of refusing.
|
|
1694
1746
|
const result = parseExpression('arr.flat(n)')
|
|
1747
|
+
expect(result.kind).toBe('array-method')
|
|
1748
|
+
if (result.kind === 'array-method' && result.method === 'flat') {
|
|
1749
|
+
expect(result.depthExpr).toEqual({ kind: 'identifier', name: 'n' })
|
|
1750
|
+
} else {
|
|
1751
|
+
throw new Error(`expected a flat array-method, got ${result.kind}`)
|
|
1752
|
+
}
|
|
1753
|
+
// An arithmetic / member-access depth resolves too.
|
|
1754
|
+
expect(parseExpression('arr.flat(depth + 1)').kind).toBe('array-method')
|
|
1755
|
+
expect(parseExpression('arr.flat(props.depth)').kind).toBe('array-method')
|
|
1756
|
+
})
|
|
1757
|
+
|
|
1758
|
+
test('a non-literal depth that itself does NOT resolve still refuses', () => {
|
|
1759
|
+
const result = parseExpression('arr.flat(a instanceof B)')
|
|
1695
1760
|
expect(result.kind).toBe('unsupported')
|
|
1696
1761
|
if (result.kind === 'unsupported') {
|
|
1697
1762
|
// Wrong remedy `@client` must not be suggested (doesn't work in
|
|
1698
1763
|
// attribute / condition position).
|
|
1699
1764
|
expect(result.reason).not.toContain('@client')
|
|
1700
|
-
expect(result.reason).toContain('
|
|
1765
|
+
expect(result.reason).toContain('resolved')
|
|
1701
1766
|
}
|
|
1702
1767
|
})
|
|
1703
1768
|
|
|
@@ -1709,6 +1774,12 @@ describe('expression-parser — .flat(depth?) lowering (#1448 Tier C)', () => {
|
|
|
1709
1774
|
expect(exprToString(parseExpression('arr.flat(Infinity)'))).toBe('arr.flat(Infinity)')
|
|
1710
1775
|
expect(exprToString(parseExpression('arr.flat(0)'))).toBe('arr.flat(0)')
|
|
1711
1776
|
})
|
|
1777
|
+
|
|
1778
|
+
test('exprToString / stringifyParsedExpr round-trip a dynamic depth (#2094)', () => {
|
|
1779
|
+
expect(exprToString(parseExpression('arr.flat(n)'))).toBe('arr.flat(n)')
|
|
1780
|
+
expect(stringifyParsedExpr(parseExpression('arr.flat(n)'))).toBe('arr.flat(n)')
|
|
1781
|
+
expect(stringifyParsedExpr(parseExpression('arr.flat(depth + 1)'))).toBe('arr.flat(depth + 1)')
|
|
1782
|
+
})
|
|
1712
1783
|
})
|
|
1713
1784
|
|
|
1714
1785
|
describe('expression-parser — .flatMap(fn) projection (#1448 Tier C)', () => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseExpression, freeIdentifiers } from '../expression-parser'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `freeIdentifiers` pins the LEXICALLY-SCOPED free-identifier walk used by the
|
|
5
|
+
* Perl-family seed-scope guard (Mojo / Xslate `tryLowerToPerl` / `tryLowerToKolon`,
|
|
6
|
+
*): a name bound by an arrow's own params is free
|
|
7
|
+
* OUTSIDE that arrow's body but bound WITHIN it, and an `unsupported` node
|
|
8
|
+
* makes the whole result unanalyzable (`null`) so callers fail safe.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe('freeIdentifiers', () => {
|
|
12
|
+
test('simple identifiers are free', () => {
|
|
13
|
+
const expr = parseExpression('a + b.c')
|
|
14
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['a', 'b']))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('an arrow param is bound inside its own body but free outside it', () => {
|
|
18
|
+
// The `filter((p) => p.ok) && p` shape : the outer `p` is a
|
|
19
|
+
// DIFFERENT (unbound) reference from the callback's own `p` param.
|
|
20
|
+
const expr = parseExpression('props.items.filter((p) => p.ok) && p')
|
|
21
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['props', 'p']))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('an arrow param fully shadowed inside its body reports no free callback ref', () => {
|
|
25
|
+
const expr = parseExpression('items.filter((p) => p.ok)')
|
|
26
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['items']))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('nested arrows accumulate params from every enclosing scope', () => {
|
|
30
|
+
// `a` and `b` are both bound within the innermost body; only `outer` is free.
|
|
31
|
+
const expr = parseExpression('items.filter((a) => other.filter((b) => a + b + outer))')
|
|
32
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['items', 'other', 'outer']))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('member property names are not references; object-literal keys are not either', () => {
|
|
36
|
+
const expr = parseExpression('({ total: item.price, tax: rate })')
|
|
37
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['item', 'rate']))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a builtin callee (Math.floor) is not reported as free; its args are', () => {
|
|
41
|
+
const expr = parseExpression('Math.floor(n)')
|
|
42
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['n']))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('a non-builtin call callee IS a free reference', () => {
|
|
46
|
+
const expr = parseExpression('someFn(x)')
|
|
47
|
+
expect(freeIdentifiers(expr)).toEqual(new Set(['someFn', 'x']))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('an unsupported node makes the whole tree unanalyzable', () => {
|
|
51
|
+
// A block-bodied arrow with imperative statements parses as `unsupported`.
|
|
52
|
+
const expr = parseExpression('(() => { for (;;) {} })()')
|
|
53
|
+
expect(freeIdentifiers(expr)).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|