@barefootjs/jsx 0.31.6 → 0.31.8
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/compiler.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +105 -11
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts +86 -0
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/rich-type-refusal.d.ts +59 -11
- package/dist/rich-type-refusal.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/client-only-date-lowering.test.ts +182 -0
- package/src/__tests__/env-signal-template-prelude.test.ts +87 -0
- package/src/__tests__/prop-references.test.ts +72 -0
- package/src/__tests__/rich-type-method-refusal.test.ts +79 -2
- package/src/__tests__/rich-type-prop-serialization.test.ts +238 -0
- package/src/compiler.ts +3 -1
- package/src/errors.ts +13 -0
- package/src/ir-to-client-js/build-references.ts +17 -0
- package/src/ir-to-client-js/csr-substitute.ts +3 -2
- package/src/ir-to-client-js/emit-reactive.ts +29 -8
- package/src/ir-to-client-js/emit-registration.ts +51 -3
- package/src/ir-to-client-js/html-template.ts +22 -4
- package/src/rich-type-evidence.ts +104 -0
- package/src/rich-type-refusal.ts +177 -23
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalogued Date/URL calls inside `/* @client */` expressions and reactive
|
|
3
|
+
* attribute bindings (#2640, #2641).
|
|
4
|
+
*
|
|
5
|
+
* `emitClientOnlyExpressions` and `emitReactiveAttributeUpdates`
|
|
6
|
+
* (`ir-to-client-js/emit-reactive.ts`) used to splice a reactive expression's
|
|
7
|
+
* source text VERBATIM into the emitted `createEffect` body — correct for an
|
|
8
|
+
* un-catalogued method (BF021 already refused anything else, and the
|
|
9
|
+
* `/* @client */` escape is genuinely just "run this raw"), but wrong for a
|
|
10
|
+
* CATALOGUED method (`.toISOString()`, an explicit-locale
|
|
11
|
+
* `.toLocaleDateString(...)`, #2292/#2324): those never fire BF021 (a
|
|
12
|
+
* registered lowering plugin claims them), yet the receiver still crosses
|
|
13
|
+
* the `bf-p` hydration boundary as a de-riched JSON value. The sibling
|
|
14
|
+
* non-`@client` TEXT path (`emitDynamicTextUpdates`) already routed a
|
|
15
|
+
* catalogued call through the `date`/`formatDate` runtime helper before
|
|
16
|
+
* hydrate re-evaluates it (#2292) — these two sites now share that same
|
|
17
|
+
* rewrite via `makeCataloguedCallLowerer`.
|
|
18
|
+
*
|
|
19
|
+
* This suite compiles through `../index` (not the bare `../compiler`) so the
|
|
20
|
+
* built-in lowering plugins (`datePlugin`, `toLocaleDatePlugin`) are
|
|
21
|
+
* registered as the real package does — a call the registry doesn't know
|
|
22
|
+
* about would fire BF021 and never reach the reactive-emission sites this
|
|
23
|
+
* suite is about.
|
|
24
|
+
*/
|
|
25
|
+
import { describe, test, expect } from 'bun:test'
|
|
26
|
+
import { compileJSX } from '../index'
|
|
27
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
28
|
+
import { ErrorCodes } from '../errors'
|
|
29
|
+
|
|
30
|
+
function compile(src: string) {
|
|
31
|
+
const result = compileJSX(src.trimStart(), 'T.tsx', { adapter: new TestAdapter() })
|
|
32
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')!.content
|
|
33
|
+
const bf021 = result.errors.filter((e) => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
34
|
+
return { clientJs, bf021 }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('#2640 — /* @client */ text expressions route catalogued calls through the runtime helper', () => {
|
|
38
|
+
test('a catalogued zero-arg accessor lowers to date(...), not the raw call', () => {
|
|
39
|
+
const { clientJs, bf021 } = compile(`
|
|
40
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
41
|
+
return <div>{/* @client */ createdAt.toISOString()}</div>
|
|
42
|
+
}
|
|
43
|
+
`)
|
|
44
|
+
expect(bf021).toHaveLength(0)
|
|
45
|
+
expect(clientJs).toContain('date(createdAt, "toISOString")')
|
|
46
|
+
expect(clientJs).not.toContain('createdAt.toISOString()')
|
|
47
|
+
expect(clientJs).toMatch(/import\s*\{[^}]*\bdate\b[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('an explicit-locale toLocaleDateString lowers to formatDate(...)', () => {
|
|
51
|
+
const { clientJs, bf021 } = compile(`
|
|
52
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
53
|
+
return <div>{/* @client */ createdAt.toLocaleDateString('ja-JP', { timeZone: 'UTC' })}</div>
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
expect(bf021).toHaveLength(0)
|
|
57
|
+
expect(clientJs).toContain('formatDate(createdAt, "YYYY/M/D", "UTC")')
|
|
58
|
+
expect(clientJs).not.toContain(".toLocaleDateString('ja-JP'")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('an UNCATALOGUED method call stays verbatim (only what the matcher claims is rewritten)', () => {
|
|
62
|
+
const { clientJs, bf021 } = compile(`
|
|
63
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
64
|
+
return <div>{/* @client */ createdAt.getDay()}</div>
|
|
65
|
+
}
|
|
66
|
+
`)
|
|
67
|
+
expect(bf021).toHaveLength(0)
|
|
68
|
+
expect(clientJs).toContain('createdAt.getDay()')
|
|
69
|
+
expect(clientJs).not.toContain('date(')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('a zero-arg toLocaleDateString() (implicit locale) stays verbatim — the deliberate revival-escape case (#2639)', () => {
|
|
73
|
+
// BF021 refuses this shape outside /* @client */ and recommends the
|
|
74
|
+
// explicit `new Date(...)` revival wrapper as the sound escape; inside
|
|
75
|
+
// /* @client */ it never fires (the directive already opts out), and
|
|
76
|
+
// this suite pins that the compiler does NOT special-case it further —
|
|
77
|
+
// an implicit-locale call is genuinely the user's own responsibility.
|
|
78
|
+
const { clientJs, bf021 } = compile(`
|
|
79
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
80
|
+
return <div>{/* @client */ createdAt.toLocaleDateString()}</div>
|
|
81
|
+
}
|
|
82
|
+
`)
|
|
83
|
+
expect(bf021).toHaveLength(0)
|
|
84
|
+
expect(clientJs).toContain('createdAt.toLocaleDateString()')
|
|
85
|
+
expect(clientJs).not.toContain('formatDate(')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('#2641 — reactive attribute bindings route catalogued calls through the runtime helper', () => {
|
|
90
|
+
test('/* @client */ attribute, destructured-prop style', () => {
|
|
91
|
+
const { clientJs, bf021 } = compile(`
|
|
92
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
93
|
+
return <div data-x={/* @client */ createdAt.toISOString()} />
|
|
94
|
+
}
|
|
95
|
+
`)
|
|
96
|
+
expect(bf021).toHaveLength(0)
|
|
97
|
+
expect(clientJs).toContain('date(_p.createdAt, "toISOString")')
|
|
98
|
+
expect(clientJs).not.toContain('createdAt.toISOString()')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('/* @client */ attribute, props-object style', () => {
|
|
102
|
+
const { clientJs, bf021 } = compile(`
|
|
103
|
+
export function Foo(props: { createdAt: Date }) {
|
|
104
|
+
return <div data-x={/* @client */ props.createdAt.toISOString()} />
|
|
105
|
+
}
|
|
106
|
+
`)
|
|
107
|
+
expect(bf021).toHaveLength(0)
|
|
108
|
+
expect(clientJs).toContain('date(_p.createdAt, "toISOString")')
|
|
109
|
+
expect(clientJs).not.toContain('props.createdAt.toISOString()')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('non-@client reactive attribute, a catalogued call — the broader gap #2641 found: this site has no directive at all, and previously spliced the raw call verbatim too', () => {
|
|
113
|
+
const { clientJs, bf021 } = compile(`
|
|
114
|
+
export function Foo({ createdAt, label }: { createdAt: Date; label: string }) {
|
|
115
|
+
return <time data-iso={createdAt.toISOString()}>{label}</time>
|
|
116
|
+
}
|
|
117
|
+
`)
|
|
118
|
+
expect(bf021).toHaveLength(0)
|
|
119
|
+
expect(clientJs).toContain('date(_p.createdAt, "toISOString")')
|
|
120
|
+
expect(clientJs).not.toContain('_p.createdAt.toISOString()')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('an explicit-locale toLocaleDateString in attribute position lowers to formatDate(...)', () => {
|
|
124
|
+
const { clientJs, bf021 } = compile(`
|
|
125
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
126
|
+
return <div data-x={/* @client */ createdAt.toLocaleDateString('ja-JP', { timeZone: 'UTC' })} />
|
|
127
|
+
}
|
|
128
|
+
`)
|
|
129
|
+
expect(bf021).toHaveLength(0)
|
|
130
|
+
expect(clientJs).toContain('formatDate(_p.createdAt, "YYYY/M/D", "UTC")')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('an uncatalogued attribute method call stays verbatim', () => {
|
|
134
|
+
const { clientJs, bf021 } = compile(`
|
|
135
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
136
|
+
return <div data-x={/* @client */ createdAt.getDay()} />
|
|
137
|
+
}
|
|
138
|
+
`)
|
|
139
|
+
expect(bf021).toHaveLength(0)
|
|
140
|
+
expect(clientJs).toContain('createdAt.getDay()')
|
|
141
|
+
expect(clientJs).not.toContain('date(')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a non-Date reactive attribute is untouched', () => {
|
|
145
|
+
const { clientJs, bf021 } = compile(`
|
|
146
|
+
export function Foo({ label }: { label: string }) {
|
|
147
|
+
return <div data-x={/* @client */ label.toUpperCase()} />
|
|
148
|
+
}
|
|
149
|
+
`)
|
|
150
|
+
expect(bf021).toHaveLength(0)
|
|
151
|
+
expect(clientJs).toContain('label.toUpperCase()')
|
|
152
|
+
expect(clientJs).not.toContain('date(')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('#2645 — /* @client */ text expressions elide in the static/CSR template like SSR', () => {
|
|
157
|
+
// `irToComponentTemplateWithOpts`'s 'expression' case never checked
|
|
158
|
+
// `node.clientOnly` — a bare-destructured-prop receiver (`isSimplePropExpression`
|
|
159
|
+
// treats `createdAt.toISOString()` as "simple") routed to this static
|
|
160
|
+
// template builder, which had no clientOnly awareness and inlined the
|
|
161
|
+
// (possibly lowered) value directly, breaking SSR/CSR byte parity: SSR
|
|
162
|
+
// renders the @client region empty, this builder rendered it populated.
|
|
163
|
+
test('markerless: the region is fully empty, matching SSR', () => {
|
|
164
|
+
const { clientJs, bf021 } = compile(`
|
|
165
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
166
|
+
return <div>{/* @client */ createdAt.toISOString()}</div>
|
|
167
|
+
}
|
|
168
|
+
`)
|
|
169
|
+
expect(bf021).toHaveLength(0)
|
|
170
|
+
expect(clientJs).toContain('template: (_p) => `<div bf="s1"></div>`')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('marker pair (adjacent static text defeats markerless elision): empty marker pair, no inlined value', () => {
|
|
174
|
+
const { clientJs, bf021 } = compile(`
|
|
175
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
176
|
+
return <div>ISO: {/* @client */ createdAt.toISOString()}</div>
|
|
177
|
+
}
|
|
178
|
+
`)
|
|
179
|
+
expect(bf021).toHaveLength(0)
|
|
180
|
+
expect(clientJs).toContain('template: (_p) => `<div bf="s1">ISO: <!--bf:s0--><!--/--></div>`')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2654 — the CSR `template:` lambda must declare its own env-signal
|
|
3
|
+
* getter (`createSearchParams()`, #2057) before referencing it, instead
|
|
4
|
+
* of relying on `init`'s `const [<getter>] = createSearchParams()`
|
|
5
|
+
* closure — the template lambda runs at module scope and has no access
|
|
6
|
+
* to that init-local binding, so the bare call ReferenceErrors at
|
|
7
|
+
* template-evaluation time.
|
|
8
|
+
*
|
|
9
|
+
* See `buildTemplateDefPart` in `../ir-to-client-js/emit-registration.ts`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect } from 'bun:test'
|
|
13
|
+
import { compileJSX } from '../compiler'
|
|
14
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
15
|
+
|
|
16
|
+
const adapter = new TestAdapter()
|
|
17
|
+
|
|
18
|
+
function compileClient(source: string, fileName: string): string {
|
|
19
|
+
const result = compileJSX(source, fileName, { adapter })
|
|
20
|
+
expect(result.errors).toHaveLength(0)
|
|
21
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
22
|
+
return clientJs?.content ?? ''
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('#2654 — env-signal getter self-declared in the template lambda', () => {
|
|
26
|
+
test('default getter name (`searchParams`): template lambda declares its own copy', () => {
|
|
27
|
+
const source = `
|
|
28
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
29
|
+
export function SortLabel() {
|
|
30
|
+
const [searchParams] = createSearchParams()
|
|
31
|
+
return <p>{searchParams().get('sort') ?? 'none'}</p>
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
const clientJs = compileClient(source, 'SortLabel.tsx')
|
|
35
|
+
|
|
36
|
+
// The template lambda gets a block body with its own prelude
|
|
37
|
+
// declaration ahead of the returned template literal.
|
|
38
|
+
expect(clientJs).toMatch(
|
|
39
|
+
/template:\s*\(_p\)\s*=>\s*\{\s*const \[searchParams\] = createSearchParams\(\);\s*return\s*`/,
|
|
40
|
+
)
|
|
41
|
+
// The getter call inside the template body is untouched (still a
|
|
42
|
+
// real call — csr-substitute.ts deliberately leaves env-signal
|
|
43
|
+
// getters as live calls, not baked initial values).
|
|
44
|
+
expect(clientJs).toMatch(/\$\{escapeText\(searchParams\(\)\.get\('sort'\) \?\? 'none'\)\}/)
|
|
45
|
+
// `init` keeps its own independent destructure — unaffected by the
|
|
46
|
+
// template-lambda prelude.
|
|
47
|
+
expect(clientJs).toMatch(/export function initSortLabel[\s\S]*const \[searchParams\] = createSearchParams\(\)/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('aliased getter name (`sp`): prelude uses the destructured alias, not the canonical name', () => {
|
|
51
|
+
const source = `
|
|
52
|
+
'use client'
|
|
53
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
54
|
+
export function SortStatus() {
|
|
55
|
+
const [sp] = createSearchParams()
|
|
56
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
57
|
+
return <p>sort: {sort()}</p>
|
|
58
|
+
}
|
|
59
|
+
`
|
|
60
|
+
const clientJs = compileClient(source, 'SortStatus.tsx')
|
|
61
|
+
|
|
62
|
+
expect(clientJs).toMatch(
|
|
63
|
+
/template:\s*\(_p\)\s*=>\s*\{\s*const \[sp\] = createSearchParams\(\);\s*return\s*`/,
|
|
64
|
+
)
|
|
65
|
+
// The memo body substitution inlines the memo's computation, still
|
|
66
|
+
// calling the self-declared `sp()` getter — no bare, undeclared
|
|
67
|
+
// reference reaches the module-scope lambda.
|
|
68
|
+
expect(clientJs).toMatch(/sp\(\)\.get\('sort'\) \?\? 'date'/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('component with no env signal keeps the plain expression-body template (no prelude leak)', () => {
|
|
72
|
+
const source = `
|
|
73
|
+
'use client'
|
|
74
|
+
import { createSignal } from '@barefootjs/client'
|
|
75
|
+
export function Counter() {
|
|
76
|
+
const [count] = createSignal(0)
|
|
77
|
+
return <p>{count()}</p>
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
const clientJs = compileClient(source, 'Counter.tsx')
|
|
81
|
+
|
|
82
|
+
// Plain expression-body form — unconditional prelude emission must
|
|
83
|
+
// not leak into components that hold no env signal.
|
|
84
|
+
expect(clientJs).toMatch(/template:\s*\(_p\)\s*=>\s*`/)
|
|
85
|
+
expect(clientJs).not.toMatch(/template:\s*\(_p\)\s*=>\s*\{/)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -311,6 +311,78 @@ describe('ClientJS generation with semantic prop refs', () => {
|
|
|
311
311
|
})
|
|
312
312
|
})
|
|
313
313
|
|
|
314
|
+
// Issue #2634 regression test: a prop used ONLY inside a `/* @client */`
|
|
315
|
+
// expression was never extracted from `_p` — the identifier reached
|
|
316
|
+
// `createEffect`'s body unbound, throwing `ReferenceError` at hydrate.
|
|
317
|
+
// `buildReferencesGraph` (build-references.ts) deliberately skips adding a
|
|
318
|
+
// `template-closure` edge for a clientOnly expression (it isn't template-
|
|
319
|
+
// reachable), but was never wired to `ctx.clientOnlyElements` either, so no
|
|
320
|
+
// edge of ANY kind reached `neededProps` and `emitPropsExtraction` had no
|
|
321
|
+
// evidence the prop was used at all.
|
|
322
|
+
describe('Issue #2634 regression', () => {
|
|
323
|
+
test('a destructured prop used only inside /* @client */ is extracted from _p', () => {
|
|
324
|
+
const source = `
|
|
325
|
+
interface Props {
|
|
326
|
+
label: string
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function LabelClientOnly({ label }: Props) {
|
|
330
|
+
return <div>{/* @client */ label.toUpperCase()}</div>
|
|
331
|
+
}
|
|
332
|
+
`
|
|
333
|
+
|
|
334
|
+
const result = compileJSX(source, 'LabelClientOnly.tsx', { adapter })
|
|
335
|
+
|
|
336
|
+
expect(result.errors).toHaveLength(0)
|
|
337
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
338
|
+
expect(clientJs).toBeDefined()
|
|
339
|
+
// The prop must be bound before the createEffect body reads it.
|
|
340
|
+
expect(clientJs?.content).toContain('const label = _p.label')
|
|
341
|
+
// A bare `label` reference with no preceding binding is exactly the
|
|
342
|
+
// unbound-identifier shape that threw ReferenceError at hydrate.
|
|
343
|
+
expect(clientJs?.content).toMatch(/const label = _p\.label[\s\S]*label\.toUpperCase\(\)/)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('a props-object member used only inside /* @client */ needs no extraction (already _p.x)', () => {
|
|
347
|
+
const source = `
|
|
348
|
+
interface Props {
|
|
349
|
+
label: string
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function LabelClientOnly(props: Props) {
|
|
353
|
+
return <div>{/* @client */ props.label.toUpperCase()}</div>
|
|
354
|
+
}
|
|
355
|
+
`
|
|
356
|
+
|
|
357
|
+
const result = compileJSX(source, 'LabelClientOnly.tsx', { adapter })
|
|
358
|
+
|
|
359
|
+
expect(result.errors).toHaveLength(0)
|
|
360
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
361
|
+
expect(clientJs).toBeDefined()
|
|
362
|
+
expect(clientJs?.content).toContain('_p.label.toUpperCase()')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('a prop used both in a normal reactive position AND inside /* @client */ is still extracted once', () => {
|
|
366
|
+
const source = `
|
|
367
|
+
interface Props {
|
|
368
|
+
label: string
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function LabelBoth({ label }: Props) {
|
|
372
|
+
return <div><span>{label}</span><span>{/* @client */ label.toUpperCase()}</span></div>
|
|
373
|
+
}
|
|
374
|
+
`
|
|
375
|
+
|
|
376
|
+
const result = compileJSX(source, 'LabelBoth.tsx', { adapter })
|
|
377
|
+
|
|
378
|
+
expect(result.errors).toHaveLength(0)
|
|
379
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
380
|
+
expect(clientJs).toBeDefined()
|
|
381
|
+
const matches = clientJs?.content.match(/const label = _p\.label/g) ?? []
|
|
382
|
+
expect(matches).toHaveLength(1)
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
314
386
|
// Issue #257 regression test: Double props prefix in template literals
|
|
315
387
|
describe('Issue #257 regression', () => {
|
|
316
388
|
test('does NOT double-wrap props.xxx when already prefixed', () => {
|
|
@@ -173,14 +173,78 @@ describe('rich-type method-call refusal — fires (BF021)', () => {
|
|
|
173
173
|
expect(errors[0].message).toContain("'Map'")
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
// #2636: bare /* @client */ crashes at hydrate for every rich-type
|
|
177
|
+
// receiver here (the prop arrives de-riched over the JSON bf-p boundary),
|
|
178
|
+
// so the suggestion must never recommend it bare. Date/URL (JSON-revivable
|
|
179
|
+
// via their own one-arg constructor) get an explicit-revival @client form
|
|
180
|
+
// instead; every other host rich type gets pre-compute only.
|
|
181
|
+
test('Date generic method: pre-compute lead + explicit-revival @client escape, never a bare @client', () => {
|
|
177
182
|
const errors = bf021(`
|
|
178
183
|
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
179
184
|
return <div>{createdAt.toISOString()}</div>
|
|
180
185
|
}
|
|
181
186
|
`)
|
|
182
187
|
expect(errors[0].severity).toBe('error')
|
|
183
|
-
|
|
188
|
+
const message = errors[0].suggestion?.message ?? ''
|
|
189
|
+
expect(message).toContain('Pre-compute')
|
|
190
|
+
expect(message).toContain('new Date(createdAt)')
|
|
191
|
+
expect(message).not.toMatch(/Add \/\* @client \*\//)
|
|
192
|
+
expect(errors[0].suggestion?.escape).toEqual([{ kind: 'prop-precompute' }, { kind: 'client-directive' }])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('Map method: pre-compute only, explicit no-safe-@client-escape warning', () => {
|
|
196
|
+
const errors = bf021(`
|
|
197
|
+
export function Foo({ m }: { m: Map<string, string> }) {
|
|
198
|
+
return <div>{m.get('x')}</div>
|
|
199
|
+
}
|
|
200
|
+
`)
|
|
201
|
+
const message = errors[0].suggestion?.message ?? ''
|
|
202
|
+
expect(message).toContain('Pre-compute')
|
|
203
|
+
expect(message).toContain('NOT a safe escape')
|
|
204
|
+
expect(message).not.toContain('new Map(')
|
|
205
|
+
expect(errors[0].suggestion?.escape).toEqual([{ kind: 'prop-precompute' }])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Zero-arg toLocaleDateString() is BF021's one case where the refusal is
|
|
209
|
+
// deliberately ADAPTER-UNIFORM rather than a DSL-adapter-only gap (unlike
|
|
210
|
+
// BF101's "JS-runtime executes verbatim" carve-out): `TestAdapter extends
|
|
211
|
+
// JsxAdapter` (adapter-hono's own base class) here, and it still refuses.
|
|
212
|
+
// See #2356's decision comment and `date-method-uncatalogued.ts`'s
|
|
213
|
+
// docstring — a JS-runtime adapter's hydrate leg re-evaluates a
|
|
214
|
+
// prop-derived expression against a JSON-de-riched receiver (a `Date`
|
|
215
|
+
// prop arrives as its ISO string), so "the SSR runtime can evaluate the
|
|
216
|
+
// call" does not make the compiled artifact sound; the compiler doesn't
|
|
217
|
+
// special-case this adapter class here on purpose.
|
|
218
|
+
test('zero-arg toLocaleDateString() refuses even on a JsxAdapter (#2356 — adapter-uniform by design)', () => {
|
|
219
|
+
const errors = bf021(`
|
|
220
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
221
|
+
return <div>{createdAt.toLocaleDateString()}</div>
|
|
222
|
+
}
|
|
223
|
+
`)
|
|
224
|
+
expect(errors).toHaveLength(1)
|
|
225
|
+
expect(errors[0].message).toContain("'.toLocaleDateString()'")
|
|
226
|
+
expect(errors[0].message).toContain("'createdAt'")
|
|
227
|
+
// The suggestion for this specific method+type points at the
|
|
228
|
+
// literal-locale/timeZone escape (#2324) ahead of the generic
|
|
229
|
+
// pre-compute-or-revival fallback every other BF021 carries.
|
|
230
|
+
const message = errors[0].suggestion?.message ?? ''
|
|
231
|
+
expect(message).toContain('literal locale')
|
|
232
|
+
// #2636: even this branch's @client escape must be the explicit-revival
|
|
233
|
+
// form, never a bare /* @client */.
|
|
234
|
+
expect(message).toContain('new Date(createdAt)')
|
|
235
|
+
expect(errors[0].suggestion?.escape).toEqual([{ kind: 'rewrite' }, { kind: 'prop-precompute' }, { kind: 'client-directive' }])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('URL generic method: revivable tier applies past Date (#2636)', () => {
|
|
239
|
+
const errors = bf021(`
|
|
240
|
+
export function Foo({ href }: { href: URL }) {
|
|
241
|
+
return <div>{href.toString()}</div>
|
|
242
|
+
}
|
|
243
|
+
`)
|
|
244
|
+
expect(errors).toHaveLength(1)
|
|
245
|
+
const message = errors[0].suggestion?.message ?? ''
|
|
246
|
+
expect(message).toContain('new URL(href)')
|
|
247
|
+
expect(errors[0].suggestion?.escape).toEqual([{ kind: 'prop-precompute' }, { kind: 'client-directive' }])
|
|
184
248
|
})
|
|
185
249
|
})
|
|
186
250
|
|
|
@@ -194,6 +258,19 @@ describe('rich-type method-call refusal — silent (no BF021)', () => {
|
|
|
194
258
|
expect(errors).toHaveLength(0)
|
|
195
259
|
})
|
|
196
260
|
|
|
261
|
+
// Pins the premise the revivable-tier suggestion (#2636) depends on: a
|
|
262
|
+
// /* @client */ block that wraps the receiver in `new Date(...)` first is
|
|
263
|
+
// itself a call-result receiver (resolveReceiverType → null), so it never
|
|
264
|
+
// fires BF021 — the same reason a bare @client call never fires above.
|
|
265
|
+
test('/* @client */ with explicit new Date(...) revival wrapper', () => {
|
|
266
|
+
const errors = bf021(`
|
|
267
|
+
export function Foo({ createdAt }: { createdAt: Date }) {
|
|
268
|
+
return <div>{/* @client */ new Date(createdAt).getUTCFullYear()}</div>
|
|
269
|
+
}
|
|
270
|
+
`)
|
|
271
|
+
expect(errors).toHaveLength(0)
|
|
272
|
+
})
|
|
273
|
+
|
|
197
274
|
test('/* @client */-wrapped conditional branch', () => {
|
|
198
275
|
const errors = bf021(`
|
|
199
276
|
export function Foo({ d }: { d: Date | null }) {
|