@barefootjs/jsx 0.17.1 → 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/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 +1 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +815 -251
- 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/types.d.ts +68 -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 +63 -2
- 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__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +70 -3
- package/src/__tests__/ssr-defaults.test.ts +20 -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/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 +4 -0
- package/src/expression-parser.ts +237 -56
- package/src/index.ts +23 -1
- 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/types.ts +65 -0
|
@@ -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)
|
|
@@ -1700,14 +1739,30 @@ describe('expression-parser — .flat(depth?) lowering (#1448 Tier C)', () => {
|
|
|
1700
1739
|
})
|
|
1701
1740
|
}
|
|
1702
1741
|
|
|
1703
|
-
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.
|
|
1704
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)')
|
|
1705
1760
|
expect(result.kind).toBe('unsupported')
|
|
1706
1761
|
if (result.kind === 'unsupported') {
|
|
1707
1762
|
// Wrong remedy `@client` must not be suggested (doesn't work in
|
|
1708
1763
|
// attribute / condition position).
|
|
1709
1764
|
expect(result.reason).not.toContain('@client')
|
|
1710
|
-
expect(result.reason).toContain('
|
|
1765
|
+
expect(result.reason).toContain('resolved')
|
|
1711
1766
|
}
|
|
1712
1767
|
})
|
|
1713
1768
|
|
|
@@ -1719,6 +1774,12 @@ describe('expression-parser — .flat(depth?) lowering (#1448 Tier C)', () => {
|
|
|
1719
1774
|
expect(exprToString(parseExpression('arr.flat(Infinity)'))).toBe('arr.flat(Infinity)')
|
|
1720
1775
|
expect(exprToString(parseExpression('arr.flat(0)'))).toBe('arr.flat(0)')
|
|
1721
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
|
+
})
|
|
1722
1783
|
})
|
|
1723
1784
|
|
|
1724
1785
|
describe('expression-parser — .flatMap(fn) projection (#1448 Tier C)', () => {
|
|
@@ -382,3 +382,264 @@ describe('sort().map() / toSorted().map()', () => {
|
|
|
382
382
|
}
|
|
383
383
|
})
|
|
384
384
|
})
|
|
385
|
+
|
|
386
|
+
// #2090: `.sort(cb)` / `.toSorted(cb)` where `cb` is a bare identifier
|
|
387
|
+
// reference to a same-file arrow/function-declaration comparator, resolved
|
|
388
|
+
// through the analyzer's scope machinery (`findLocalConst` /
|
|
389
|
+
// `findLocalFunction` in jsx-to-ir.ts) one hop, then fed through the SAME
|
|
390
|
+
// `sortComparatorFromArrow` catalogue as an inline comparator — no new
|
|
391
|
+
// comparator shapes, no IR schema change.
|
|
392
|
+
describe('function-reference sort comparator (#2090)', () => {
|
|
393
|
+
test('const arrow reference, ascending', () => {
|
|
394
|
+
const source = `
|
|
395
|
+
'use client'
|
|
396
|
+
import { createSignal } from '@barefootjs/client'
|
|
397
|
+
|
|
398
|
+
export function ProductList() {
|
|
399
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
400
|
+
const byPrice = (a, b) => a.price - b.price
|
|
401
|
+
return (
|
|
402
|
+
<ul>
|
|
403
|
+
{products().sort(byPrice).map(p => (
|
|
404
|
+
<li key={p.name}>{p.name}</li>
|
|
405
|
+
))}
|
|
406
|
+
</ul>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
`
|
|
410
|
+
|
|
411
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
412
|
+
const ir = jsxToIR(ctx)
|
|
413
|
+
|
|
414
|
+
expect(ir).not.toBeNull()
|
|
415
|
+
if (ir!.type === 'element') {
|
|
416
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
417
|
+
expect(loop).toBeDefined()
|
|
418
|
+
if (loop?.type === 'loop') {
|
|
419
|
+
expect(loop.sortComparator).toBeDefined()
|
|
420
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
421
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
422
|
+
])
|
|
423
|
+
expect(loop.sortComparator!.paramA).toBe('a')
|
|
424
|
+
expect(loop.sortComparator!.paramB).toBe('b')
|
|
425
|
+
expect(loop.sortComparator!.raw).toBe('a.price - b.price')
|
|
426
|
+
expect(loop.array).toBe('products()')
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('const arrow reference, descending multi-key (||-chain)', () => {
|
|
432
|
+
const source = `
|
|
433
|
+
'use client'
|
|
434
|
+
import { createSignal } from '@barefootjs/client'
|
|
435
|
+
|
|
436
|
+
export function ProductList() {
|
|
437
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
438
|
+
const byPriceThenName = (a, b) => b.price - a.price || a.name.localeCompare(b.name)
|
|
439
|
+
return (
|
|
440
|
+
<ul>
|
|
441
|
+
{products().sort(byPriceThenName).map(p => (
|
|
442
|
+
<li key={p.name}>{p.name}</li>
|
|
443
|
+
))}
|
|
444
|
+
</ul>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
`
|
|
448
|
+
|
|
449
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
450
|
+
const ir = jsxToIR(ctx)
|
|
451
|
+
|
|
452
|
+
expect(ir).not.toBeNull()
|
|
453
|
+
if (ir!.type === 'element') {
|
|
454
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
455
|
+
if (loop?.type === 'loop') {
|
|
456
|
+
expect(loop.sortComparator).toBeDefined()
|
|
457
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
458
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
|
|
459
|
+
{ key: { kind: 'field', field: 'name' }, type: 'string', direction: 'asc' },
|
|
460
|
+
])
|
|
461
|
+
expect(loop.sortComparator!.raw).toBe('b.price - a.price || a.name.localeCompare(b.name)')
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('function-declaration reference with return-block body', () => {
|
|
467
|
+
const source = `
|
|
468
|
+
'use client'
|
|
469
|
+
import { createSignal } from '@barefootjs/client'
|
|
470
|
+
|
|
471
|
+
function byPrice(a, b) {
|
|
472
|
+
return a.price - b.price
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function ProductList() {
|
|
476
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
477
|
+
return (
|
|
478
|
+
<ul>
|
|
479
|
+
{products().sort(byPrice).map(p => (
|
|
480
|
+
<li key={p.name}>{p.name}</li>
|
|
481
|
+
))}
|
|
482
|
+
</ul>
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
`
|
|
486
|
+
|
|
487
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
488
|
+
const ir = jsxToIR(ctx)
|
|
489
|
+
|
|
490
|
+
expect(ir).not.toBeNull()
|
|
491
|
+
if (ir!.type === 'element') {
|
|
492
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
493
|
+
if (loop?.type === 'loop') {
|
|
494
|
+
expect(loop.sortComparator).toBeDefined()
|
|
495
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
496
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
497
|
+
])
|
|
498
|
+
expect(loop.sortComparator!.paramA).toBe('a')
|
|
499
|
+
expect(loop.sortComparator!.paramB).toBe('b')
|
|
500
|
+
expect(loop.sortComparator!.raw).toBe('a.price - b.price')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('.toSorted(ref) resolves the same way as .sort(ref)', () => {
|
|
506
|
+
const source = `
|
|
507
|
+
'use client'
|
|
508
|
+
import { createSignal } from '@barefootjs/client'
|
|
509
|
+
|
|
510
|
+
export function ProductList() {
|
|
511
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
512
|
+
const byPrice = (a, b) => a.price - b.price
|
|
513
|
+
return (
|
|
514
|
+
<ul>
|
|
515
|
+
{products().toSorted(byPrice).map(p => (
|
|
516
|
+
<li key={p.name}>{p.name}</li>
|
|
517
|
+
))}
|
|
518
|
+
</ul>
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
`
|
|
522
|
+
|
|
523
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
524
|
+
const ir = jsxToIR(ctx)
|
|
525
|
+
|
|
526
|
+
expect(ir).not.toBeNull()
|
|
527
|
+
if (ir!.type === 'element') {
|
|
528
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
529
|
+
if (loop?.type === 'loop') {
|
|
530
|
+
expect(loop.sortComparator).toBeDefined()
|
|
531
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
532
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
|
|
533
|
+
])
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
test('component-scope const shadows a module-scope const of the same name', () => {
|
|
539
|
+
const source = `
|
|
540
|
+
'use client'
|
|
541
|
+
import { createSignal } from '@barefootjs/client'
|
|
542
|
+
|
|
543
|
+
const byPrice = (a, b) => a.price - b.price
|
|
544
|
+
|
|
545
|
+
export function ProductList() {
|
|
546
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
547
|
+
const byPrice = (a, b) => b.price - a.price
|
|
548
|
+
return (
|
|
549
|
+
<ul>
|
|
550
|
+
{products().sort(byPrice).map(p => (
|
|
551
|
+
<li key={p.name}>{p.name}</li>
|
|
552
|
+
))}
|
|
553
|
+
</ul>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
`
|
|
557
|
+
|
|
558
|
+
const ctx = analyzeComponent(source, 'ProductList.tsx')
|
|
559
|
+
const ir = jsxToIR(ctx)
|
|
560
|
+
|
|
561
|
+
expect(ir).not.toBeNull()
|
|
562
|
+
if (ir!.type === 'element') {
|
|
563
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
564
|
+
if (loop?.type === 'loop') {
|
|
565
|
+
expect(loop.sortComparator).toBeDefined()
|
|
566
|
+
// The component-scope binding (descending) wins over the
|
|
567
|
+
// module-scope one (ascending) — distinguishable via `direction`.
|
|
568
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
569
|
+
{ key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
|
|
570
|
+
])
|
|
571
|
+
expect(loop.sortComparator!.raw).toBe('b.price - a.price')
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('filter().sort(ref).map() resolves the reference in a chain', () => {
|
|
577
|
+
const source = `
|
|
578
|
+
'use client'
|
|
579
|
+
import { createSignal } from '@barefootjs/client'
|
|
580
|
+
|
|
581
|
+
export function TodoList() {
|
|
582
|
+
const [todos, setTodos] = createSignal<any[]>([])
|
|
583
|
+
const byPriority = (a, b) => a.priority - b.priority
|
|
584
|
+
return (
|
|
585
|
+
<ul>
|
|
586
|
+
{todos().filter(t => !t.done).sort(byPriority).map(t => (
|
|
587
|
+
<li key={t.text}>{t.text}</li>
|
|
588
|
+
))}
|
|
589
|
+
</ul>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
`
|
|
593
|
+
|
|
594
|
+
const ctx = analyzeComponent(source, 'TodoList.tsx')
|
|
595
|
+
const ir = jsxToIR(ctx)
|
|
596
|
+
|
|
597
|
+
expect(ir).not.toBeNull()
|
|
598
|
+
if (ir!.type === 'element') {
|
|
599
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
600
|
+
if (loop?.type === 'loop') {
|
|
601
|
+
expect(loop.filterPredicate).toBeDefined()
|
|
602
|
+
expect(loop.sortComparator).toBeDefined()
|
|
603
|
+
expect(loop.chainOrder).toBe('filter-sort')
|
|
604
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
605
|
+
{ key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
|
|
606
|
+
])
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('sort(ref).filter().map() resolves the reference in a chain', () => {
|
|
612
|
+
const source = `
|
|
613
|
+
'use client'
|
|
614
|
+
import { createSignal } from '@barefootjs/client'
|
|
615
|
+
|
|
616
|
+
export function TodoList() {
|
|
617
|
+
const [todos, setTodos] = createSignal<any[]>([])
|
|
618
|
+
const byPriority = (a, b) => a.priority - b.priority
|
|
619
|
+
return (
|
|
620
|
+
<ul>
|
|
621
|
+
{todos().sort(byPriority).filter(t => !t.done).map(t => (
|
|
622
|
+
<li key={t.text}>{t.text}</li>
|
|
623
|
+
))}
|
|
624
|
+
</ul>
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
`
|
|
628
|
+
|
|
629
|
+
const ctx = analyzeComponent(source, 'TodoList.tsx')
|
|
630
|
+
const ir = jsxToIR(ctx)
|
|
631
|
+
|
|
632
|
+
expect(ir).not.toBeNull()
|
|
633
|
+
if (ir!.type === 'element') {
|
|
634
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
635
|
+
if (loop?.type === 'loop') {
|
|
636
|
+
expect(loop.filterPredicate).toBeDefined()
|
|
637
|
+
expect(loop.sortComparator).toBeDefined()
|
|
638
|
+
expect(loop.chainOrder).toBe('sort-filter')
|
|
639
|
+
expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
|
|
640
|
+
{ key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
|
|
641
|
+
])
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
})
|