@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
|
@@ -136,12 +136,14 @@ describe('Unsupported Expression Error (BF021)', () => {
|
|
|
136
136
|
})
|
|
137
137
|
|
|
138
138
|
describe('Unsupported Sort Comparator (BF021)', () => {
|
|
139
|
-
test('
|
|
139
|
+
test('function-reference comparator resolves through scope — no BF021 (#2090)', () => {
|
|
140
140
|
// #1448 Tier B follow-up widened the catalogue to include
|
|
141
141
|
// multi-key (`a.x - b.x || a.y - b.y`), relational ternary, and
|
|
142
|
-
// single-`return` block bodies.
|
|
143
|
-
// (`arr.sort(cmp)`
|
|
144
|
-
//
|
|
142
|
+
// single-`return` block bodies. #2090 closes the remaining gap:
|
|
143
|
+
// a bare identifier callback (`arr.sort(cmp)`) is now resolved
|
|
144
|
+
// through the analyzer's scope machinery (one hop, same-file
|
|
145
|
+
// only) to the const-bound arrow, then fed through the same
|
|
146
|
+
// catalogue as an inline comparator.
|
|
145
147
|
const source = `
|
|
146
148
|
'use client'
|
|
147
149
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -152,7 +154,73 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
152
154
|
return (
|
|
153
155
|
<ul>
|
|
154
156
|
{items().sort(cmp).map(t => (
|
|
155
|
-
<li>{t.name}</li>
|
|
157
|
+
<li key={t.name}>{t.name}</li>
|
|
158
|
+
))}
|
|
159
|
+
</ul>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
|
|
164
|
+
const { ir, errors } = compileToIR(source)
|
|
165
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
166
|
+
|
|
167
|
+
expect(bf021).toHaveLength(0)
|
|
168
|
+
expect(ir).not.toBeNull()
|
|
169
|
+
if (ir!.type === 'element') {
|
|
170
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
171
|
+
expect(loop).toBeDefined()
|
|
172
|
+
if (loop?.type === 'loop') {
|
|
173
|
+
expect(loop.sortComparator).toBeDefined()
|
|
174
|
+
expect(loop.sortComparator!.raw).toBe('a.priority - b.priority')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('function-declaration comparator reference resolves — no BF021 (#2090)', () => {
|
|
180
|
+
const source = `
|
|
181
|
+
'use client'
|
|
182
|
+
import { createSignal } from '@barefootjs/client'
|
|
183
|
+
|
|
184
|
+
function cmp(a, b) { return a.priority - b.priority }
|
|
185
|
+
|
|
186
|
+
export function TodoList() {
|
|
187
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
188
|
+
return (
|
|
189
|
+
<ul>
|
|
190
|
+
{items().toSorted(cmp).map(t => (
|
|
191
|
+
<li key={t.name}>{t.name}</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
`
|
|
197
|
+
|
|
198
|
+
const { ir, errors } = compileToIR(source)
|
|
199
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
200
|
+
|
|
201
|
+
expect(bf021).toHaveLength(0)
|
|
202
|
+
expect(ir).not.toBeNull()
|
|
203
|
+
if (ir!.type === 'element') {
|
|
204
|
+
const loop = ir!.children.find(c => c.type === 'loop')
|
|
205
|
+
if (loop?.type === 'loop') {
|
|
206
|
+
expect(loop.sortComparator).toBeDefined()
|
|
207
|
+
expect(loop.sortComparator!.raw).toBe('a.priority - b.priority')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('identifier resolving to a non-function const emits BF021 (#2090)', () => {
|
|
213
|
+
const source = `
|
|
214
|
+
'use client'
|
|
215
|
+
import { createSignal } from '@barefootjs/client'
|
|
216
|
+
|
|
217
|
+
export function TodoList() {
|
|
218
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
219
|
+
const cmp = 5
|
|
220
|
+
return (
|
|
221
|
+
<ul>
|
|
222
|
+
{items().sort(cmp).map(t => (
|
|
223
|
+
<li key={t.name}>{t.name}</li>
|
|
156
224
|
))}
|
|
157
225
|
</ul>
|
|
158
226
|
)
|
|
@@ -163,16 +231,135 @@ describe('Unsupported Sort Comparator (BF021)', () => {
|
|
|
163
231
|
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
164
232
|
|
|
165
233
|
expect(bf021).toHaveLength(1)
|
|
234
|
+
expect(bf021[0].message).toContain('could not be resolved')
|
|
166
235
|
})
|
|
167
236
|
|
|
168
|
-
test('
|
|
237
|
+
test('cross-kind shadowing (component const over module function) emits BF021, not the shadowed function (#2090)', () => {
|
|
238
|
+
// A name bound both as a const and as a `function` declaration can
|
|
239
|
+
// only happen across scopes, and FunctionInfo does not carry lexical
|
|
240
|
+
// scope (component-body functions are hoisted for client emission).
|
|
241
|
+
// Resolution refuses the ambiguity rather than guessing — pre-fix
|
|
242
|
+
// this wrongly compiled against the shadowed module function
|
|
243
|
+
// (Copilot review on #2091).
|
|
169
244
|
const source = `
|
|
170
245
|
'use client'
|
|
171
246
|
import { createSignal } from '@barefootjs/client'
|
|
172
247
|
|
|
248
|
+
function cmp(a, b) { return a.priority - b.priority }
|
|
249
|
+
|
|
250
|
+
export function TodoList() {
|
|
251
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
252
|
+
const cmp = 5
|
|
253
|
+
return (
|
|
254
|
+
<ul>
|
|
255
|
+
{items().sort(cmp).map(t => (
|
|
256
|
+
<li key={t.name}>{t.name}</li>
|
|
257
|
+
))}
|
|
258
|
+
</ul>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
`
|
|
262
|
+
|
|
263
|
+
const { errors } = compileToIR(source)
|
|
264
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
265
|
+
|
|
266
|
+
expect(bf021).toHaveLength(1)
|
|
267
|
+
expect(bf021[0].message).toContain('could not be resolved')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('cross-kind shadowing (component function over module const) also refuses with BF021 (#2090)', () => {
|
|
271
|
+
// The safe half of the same ambiguity: JS would use the component
|
|
272
|
+
// function here, but resolution cannot prove which binding the call
|
|
273
|
+
// site sees, so it refuses loudly instead of risking the WRONG
|
|
274
|
+
// (opposite-direction) comparator.
|
|
275
|
+
const source = `
|
|
276
|
+
'use client'
|
|
277
|
+
import { createSignal } from '@barefootjs/client'
|
|
278
|
+
|
|
279
|
+
const byPrice = (a, b) => a.price - b.price
|
|
280
|
+
|
|
281
|
+
export function ProductList() {
|
|
282
|
+
const [products, setProducts] = createSignal<any[]>([])
|
|
283
|
+
function byPrice(a, b) { return b.price - a.price }
|
|
284
|
+
return (
|
|
285
|
+
<ul>
|
|
286
|
+
{products().sort(byPrice).map(p => (
|
|
287
|
+
<li key={p.name}>{p.name}</li>
|
|
288
|
+
))}
|
|
289
|
+
</ul>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
`
|
|
293
|
+
|
|
294
|
+
const { errors } = compileToIR(source)
|
|
295
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
296
|
+
|
|
297
|
+
expect(bf021).toHaveLength(1)
|
|
298
|
+
expect(bf021[0].message).toContain('could not be resolved')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('unresolved (imported) identifier comparator emits BF021 (#2090)', () => {
|
|
302
|
+
const source = `
|
|
303
|
+
'use client'
|
|
304
|
+
import { createSignal } from '@barefootjs/client'
|
|
305
|
+
import { cmp } from './cmp'
|
|
306
|
+
|
|
307
|
+
export function TodoList() {
|
|
308
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
309
|
+
return (
|
|
310
|
+
<ul>
|
|
311
|
+
{items().sort(cmp).map(t => (
|
|
312
|
+
<li key={t.name}>{t.name}</li>
|
|
313
|
+
))}
|
|
314
|
+
</ul>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
`
|
|
318
|
+
|
|
319
|
+
const { errors } = compileToIR(source)
|
|
320
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
321
|
+
|
|
322
|
+
expect(bf021).toHaveLength(1)
|
|
323
|
+
expect(bf021[0].message).toContain('could not be resolved')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('resolved-but-off-catalogue comparator body emits BF021 naming the comparator (#2090)', () => {
|
|
327
|
+
// `a.deep.x - b.deep.x` has operand depth > 1 — `classifySortOperand`
|
|
328
|
+
// only accepts the param itself or a single-level field access, so this
|
|
329
|
+
// stays refused even once the identifier resolves to the arrow.
|
|
330
|
+
const source = `
|
|
331
|
+
'use client'
|
|
332
|
+
import { createSignal } from '@barefootjs/client'
|
|
333
|
+
|
|
334
|
+
export function TodoList() {
|
|
335
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
336
|
+
const cmp = (a, b) => a.deep.x - b.deep.x
|
|
337
|
+
return (
|
|
338
|
+
<ul>
|
|
339
|
+
{items().sort(cmp).map(t => (
|
|
340
|
+
<li key={t.name}>{t.name}</li>
|
|
341
|
+
))}
|
|
342
|
+
</ul>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
`
|
|
346
|
+
|
|
347
|
+
const { errors } = compileToIR(source)
|
|
348
|
+
const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
|
|
349
|
+
|
|
350
|
+
expect(bf021).toHaveLength(1)
|
|
351
|
+
expect(bf021[0].message).toContain("'cmp'")
|
|
352
|
+
expect(bf021[0].message).toContain('not a supported shape')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('@client suppresses BF021 for an unresolved sort comparator identifier', () => {
|
|
356
|
+
const source = `
|
|
357
|
+
'use client'
|
|
358
|
+
import { createSignal } from '@barefootjs/client'
|
|
359
|
+
import { cmp } from './cmp'
|
|
360
|
+
|
|
173
361
|
export function TodoList() {
|
|
174
362
|
const [items, setItems] = createSignal<any[]>([])
|
|
175
|
-
const cmp = (a, b) => a.priority - b.priority
|
|
176
363
|
return (
|
|
177
364
|
<ul>
|
|
178
365
|
{/* @client */ items().sort(cmp).map(t => (
|
|
@@ -96,9 +96,23 @@ export type TemplatePrimitiveEmit = (args: string[]) => string
|
|
|
96
96
|
* Keys are the textual callee path as it appears in the JSX expression
|
|
97
97
|
* (`JSON.stringify`, `Math.floor`, `String`).
|
|
98
98
|
*
|
|
99
|
-
* V1 scope: identifier-path callees only
|
|
100
|
-
*
|
|
101
|
-
*
|
|
99
|
+
* V1 scope (#1187): identifier-path callees only, and only names the adapter
|
|
100
|
+
* knows about AHEAD OF TIME — this map is fixed at adapter-construction
|
|
101
|
+
* time, so it can never contain a name from a component's own (unknown in
|
|
102
|
+
* advance) imports. Method calls on values whose type the analyzer must
|
|
103
|
+
* resolve (`props.name.toUpperCase()`) are ALSO out of scope — see #1187 R1.
|
|
104
|
+
* Users can fall back to `/* @client *\/` for either limitation.
|
|
105
|
+
*
|
|
106
|
+
* V2 (#2069) does not widen this map — it adds a separate, orthogonal
|
|
107
|
+
* acceptance path instead: `RelocateEnv.loweringMatchers`, bound once per
|
|
108
|
+
* component from the global `LoweringPlugin` registry
|
|
109
|
+
* (`prepareLoweringMatchers`, `packages/jsx/src/lowering-registry.ts`). A
|
|
110
|
+
* plugin's `prepare(metadata)` resolves the component's ACTUAL import list,
|
|
111
|
+
* so it can recognise a call this string-keyed map structurally never could
|
|
112
|
+
* (a bespoke user import, unknown until compile time). `templatePrimitives`
|
|
113
|
+
* remains the right home for well-known JS builtins (`JSON.stringify`,
|
|
114
|
+
* `Math.floor`) that every component might reasonably use, with no import
|
|
115
|
+
* to key a plugin against.
|
|
102
116
|
*/
|
|
103
117
|
export type TemplatePrimitiveRegistry = Record<string, TemplatePrimitiveEmit>
|
|
104
118
|
|
|
@@ -110,7 +124,9 @@ export type TemplatePrimitiveRegistry = Record<string, TemplatePrimitiveEmit>
|
|
|
110
124
|
*
|
|
111
125
|
* Adapters whose template runtime can't execute arbitrary JS (Go, Perl,
|
|
112
126
|
* other server-side template languages) should leave this undefined and
|
|
113
|
-
* rely on the explicit `templatePrimitives` map alone
|
|
127
|
+
* rely on the explicit `templatePrimitives` map alone (plus, since #2069,
|
|
128
|
+
* whatever `LoweringPlugin`s are registered — see `TemplatePrimitiveRegistry`
|
|
129
|
+
* above).
|
|
114
130
|
*/
|
|
115
131
|
export type TemplateCallAcceptor = (calleeName: string) => boolean
|
|
116
132
|
|
|
@@ -177,9 +177,22 @@ export interface ParsedExprEmitter {
|
|
|
177
177
|
// a structured `FlatDepth` (the validated literal / `'infinity'`) rather
|
|
178
178
|
// than a `ParsedExpr[]` args list. Non-callback, so it is NOT routed
|
|
179
179
|
// through `callbackMethod`.
|
|
180
|
+
//
|
|
181
|
+
// `depth` is `FlatDepth` for the literal path (unchanged: adapters keep
|
|
182
|
+
// their existing `bf_flat`-family emit exactly as before) or
|
|
183
|
+
// `{ expr: ParsedExpr }` for a DYNAMIC depth (#2094) — an adapter renders
|
|
184
|
+
// `expr` with the `emit` callback and passes the result to a SEPARATE
|
|
185
|
+
// runtime helper that coerces it at render time (JS `ToIntegerOrInfinity`;
|
|
186
|
+
// see the `depthExpr` doc on `ParsedExpr`'s `array-method`/`flat` variant
|
|
187
|
+
// for why this must NOT be the same helper as the literal path — the
|
|
188
|
+
// literal path's `-1` sentinel means "flatten fully", but a genuinely
|
|
189
|
+
// dynamic `-1` means the opposite per JS). Every adapter emits its
|
|
190
|
+
// `flat_dynamic`-family helper for the dynamic form, and every runtime
|
|
191
|
+
// implements the coercion; parity is pinned by the `flat_dynamic`
|
|
192
|
+
// golden helper vectors.
|
|
180
193
|
flatMethod(
|
|
181
194
|
object: ParsedExpr,
|
|
182
|
-
depth: FlatDepth,
|
|
195
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
183
196
|
emit: (e: ParsedExpr) => string,
|
|
184
197
|
): string
|
|
185
198
|
unsupported(raw: string, reason: string): string
|
|
@@ -232,7 +245,11 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
232
245
|
return emitter.objectLiteral(expr.properties, expr.raw, emit)
|
|
233
246
|
case 'array-method':
|
|
234
247
|
if (expr.method === 'flat') {
|
|
235
|
-
return emitter.flatMethod(
|
|
248
|
+
return emitter.flatMethod(
|
|
249
|
+
expr.object,
|
|
250
|
+
expr.depthExpr ? { expr: expr.depthExpr } : expr.flatDepth,
|
|
251
|
+
emit,
|
|
252
|
+
)
|
|
236
253
|
}
|
|
237
254
|
return emitter.arrayMethod(expr.method, expr.object, expr.args, emit)
|
|
238
255
|
case 'unsupported':
|
package/src/analyzer-context.ts
CHANGED
|
@@ -268,6 +268,26 @@ export function createAnalyzerContext(
|
|
|
268
268
|
checker: null,
|
|
269
269
|
componentBodyBlock: null,
|
|
270
270
|
getJS(node: ts.Node): string {
|
|
271
|
+
// A node whose owning source file differs from this component's
|
|
272
|
+
// `sourceFile` comes from a synthetic re-parse — e.g. #2090's
|
|
273
|
+
// resolved sort-comparator functions, or #2092's desugared
|
|
274
|
+
// tagged-template-literal callee — rather than a live position in
|
|
275
|
+
// this file. `reconstructWithoutTypes` slices `sourceFile.text` by
|
|
276
|
+
// the node's start/end, so feeding it a foreign node's positions
|
|
277
|
+
// would silently slice the WRONG bytes (not throw). These synthetic
|
|
278
|
+
// subtrees are already built from plain re-parsed source text with
|
|
279
|
+
// no straggling TS type syntax to strip, so the node's own text is
|
|
280
|
+
// correct as-is — mirrors the `astText()` workaround already used
|
|
281
|
+
// for synthetic const/function re-parses elsewhere in the compiler.
|
|
282
|
+
let ownSourceFile: ts.SourceFile | undefined
|
|
283
|
+
try {
|
|
284
|
+
ownSourceFile = node.getSourceFile()
|
|
285
|
+
} catch {
|
|
286
|
+
ownSourceFile = undefined
|
|
287
|
+
}
|
|
288
|
+
if (ownSourceFile && ownSourceFile !== sourceFile) {
|
|
289
|
+
return node.getText(ownSourceFile)
|
|
290
|
+
}
|
|
271
291
|
return reconstructWithoutTypes(node, sourceFile, this.typeExcludeRanges)
|
|
272
292
|
},
|
|
273
293
|
}
|
package/src/analyzer.ts
CHANGED
|
@@ -3011,6 +3011,12 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3011
3011
|
hasIgnoreDirective: ignored,
|
|
3012
3012
|
}
|
|
3013
3013
|
|
|
3014
|
+
// Resolve each destructured binding's declared type from the param's type
|
|
3015
|
+
// annotation, so `{ value }: Props` keeps the same per-prop TypeInfo as the
|
|
3016
|
+
// `props: Props` path instead of degrading to `unknown` (which typed
|
|
3017
|
+
// adapters emit as `interface{}` + an unchecked assertion). See issue #2150.
|
|
3018
|
+
const memberTypes = param.type ? collectMemberTypes(param.type, ctx) : null
|
|
3019
|
+
|
|
3014
3020
|
for (const element of param.name.elements) {
|
|
3015
3021
|
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
3016
3022
|
const localName = element.name.text
|
|
@@ -3022,10 +3028,19 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3022
3028
|
continue
|
|
3023
3029
|
}
|
|
3024
3030
|
|
|
3031
|
+
// Aliased bindings (`{ value: v }`) take their type from the source
|
|
3032
|
+
// property name, not the local alias.
|
|
3033
|
+
const sourcePropName =
|
|
3034
|
+
element.propertyName && ts.isIdentifier(element.propertyName)
|
|
3035
|
+
? element.propertyName.text
|
|
3036
|
+
: localName
|
|
3037
|
+
const resolvedType: TypeInfo =
|
|
3038
|
+
memberTypes?.get(sourcePropName) ?? { kind: 'unknown', raw: 'unknown' }
|
|
3039
|
+
|
|
3025
3040
|
const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false
|
|
3026
3041
|
ctx.propsParams.push({
|
|
3027
3042
|
name: localName,
|
|
3028
|
-
type:
|
|
3043
|
+
type: resolvedType,
|
|
3029
3044
|
optional: !!element.initializer,
|
|
3030
3045
|
defaultValue,
|
|
3031
3046
|
defaultContainsArrow: defaultContainsArrow || undefined,
|
|
@@ -3108,6 +3123,64 @@ function collectKeysFromMembers(
|
|
|
3108
3123
|
return keys
|
|
3109
3124
|
}
|
|
3110
3125
|
|
|
3126
|
+
/**
|
|
3127
|
+
* Build a property-name -> resolved TypeInfo map from a props type annotation,
|
|
3128
|
+
* for destructured params (`{ value }: Props`) so they carry the same per-prop
|
|
3129
|
+
* types the props-object path (`props: Props`) already resolves. Returns null
|
|
3130
|
+
* for external/unresolvable types.
|
|
3131
|
+
*
|
|
3132
|
+
* Scope (deliberately narrow — targets the panic in issue #2150):
|
|
3133
|
+
* - Only REQUIRED members are resolved. Optional members (`value?: T`) are left
|
|
3134
|
+
* out so they keep degrading to `unknown` -> `interface{}`: typed adapters
|
|
3135
|
+
* rely on that nillable field to omit an unset optional attribute
|
|
3136
|
+
* (`{{if ne .X nil}}` in Go), which a concrete zero value could not express.
|
|
3137
|
+
* - Only PRIMITIVE members (string/number/boolean) are resolved. Those are the
|
|
3138
|
+
* types that otherwise produce an unchecked scalar assertion (`in.X.(int)`)
|
|
3139
|
+
* that panics. Arrays/objects/functions are left as `unknown` because typed
|
|
3140
|
+
* adapters lower them through interface{}-based helpers (`bf_flat`, spread,
|
|
3141
|
+
* `bf_json`); giving them concrete types would break that lowering and is a
|
|
3142
|
+
* larger, separate change.
|
|
3143
|
+
*/
|
|
3144
|
+
function collectMemberTypes(
|
|
3145
|
+
typeNode: ts.TypeNode,
|
|
3146
|
+
ctx: AnalyzerContext
|
|
3147
|
+
): Map<string, TypeInfo> | null {
|
|
3148
|
+
const isResolvablePrimitive = (info: TypeInfo): boolean =>
|
|
3149
|
+
info.kind === 'primitive' &&
|
|
3150
|
+
(info.primitive === 'string' || info.primitive === 'number' || info.primitive === 'boolean')
|
|
3151
|
+
|
|
3152
|
+
const fromMembers = (members: ts.NodeArray<ts.TypeElement>): Map<string, TypeInfo> => {
|
|
3153
|
+
const map = new Map<string, TypeInfo>()
|
|
3154
|
+
for (const member of members) {
|
|
3155
|
+
if (ts.isPropertySignature(member) && member.name && member.type && !member.questionToken) {
|
|
3156
|
+
const info = typeNodeToTypeInfo(member.type, ctx.sourceFile)
|
|
3157
|
+
if (info && isResolvablePrimitive(info)) {
|
|
3158
|
+
map.set(member.name.getText(ctx.sourceFile), info)
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
return map
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
3166
|
+
return fromMembers(typeNode.members)
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
3170
|
+
const typeName = typeNode.typeName.getText(ctx.sourceFile)
|
|
3171
|
+
const typeDecl = findTypeDeclaration(typeName, ctx.sourceFile)
|
|
3172
|
+
if (!typeDecl) return null
|
|
3173
|
+
if (ts.isInterfaceDeclaration(typeDecl)) {
|
|
3174
|
+
return fromMembers(typeDecl.members)
|
|
3175
|
+
}
|
|
3176
|
+
if (ts.isTypeAliasDeclaration(typeDecl) && ts.isTypeLiteralNode(typeDecl.type)) {
|
|
3177
|
+
return fromMembers(typeDecl.type.members)
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
return null
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3111
3184
|
/**
|
|
3112
3185
|
* Extract props parameters from a type annotation.
|
|
3113
3186
|
* Supports type literals and type references to local interfaces.
|
|
@@ -32,6 +32,25 @@ export interface ContextConsumer {
|
|
|
32
32
|
* absent / not a literal (the consumer then defaults to the empty value).
|
|
33
33
|
*/
|
|
34
34
|
defaultValue: string | number | boolean | null
|
|
35
|
+
/**
|
|
36
|
+
* Set to `'object'` when the `createContext(<default>)` argument is an
|
|
37
|
+
* OBJECT LITERAL (`createContext<{ config: X }>({ config: {} })`) — a shape
|
|
38
|
+
* `defaultValue` alone can't distinguish from "no default at all" (both
|
|
39
|
+
* collapse to `null`). Adapters with a live SSR context stack (twig, jinja,
|
|
40
|
+
* erb, minijinja, xslate, mojolicious) never read this: their consumer seed
|
|
41
|
+
* (`bf.use_context(name, default)`) only matters when NO enclosing Provider
|
|
42
|
+
* ran, and every existing fixture exercising an object-shaped default keeps
|
|
43
|
+
* its consumer nested inside a Provider that supplies a live value at
|
|
44
|
+
* render time — see each adapter's `memo/seed.ts` (or equivalent). Go has no
|
|
45
|
+
* such runtime stack: it bakes the Provider's value into the descendant's
|
|
46
|
+
* constructor call at COMPILE time (`extendProviderContext` /
|
|
47
|
+
* `emitStaticChildInstances`, go-template-adapter.ts), so an object-shaped
|
|
48
|
+
* default needs its own Go type (`map[string]interface{}`, not the scalar
|
|
49
|
+
* `string` fallback `contextConsumerGoType` used to pick for every
|
|
50
|
+
* non-literal default) — `defaultKind` is what lets it tell "object" apart
|
|
51
|
+
* from "no default" (#2087).
|
|
52
|
+
*/
|
|
53
|
+
defaultKind?: 'object'
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/**
|
|
@@ -44,9 +63,15 @@ export function collectContextConsumers(metadata: IRMetadata): ContextConsumer[]
|
|
|
44
63
|
const constants = metadata.localConstants ?? []
|
|
45
64
|
// Map each createContext const name → its default-arg literal value.
|
|
46
65
|
const contextDefaults = new Map<string, string | number | boolean | null>()
|
|
66
|
+
// Map each createContext const name → 'object' when its default is an
|
|
67
|
+
// object-literal (a shape `contextDefaults` alone can't distinguish from
|
|
68
|
+
// "no default" — both parse to `null`). Only Go consults this (see
|
|
69
|
+
// `ContextConsumer.defaultKind`'s docstring).
|
|
70
|
+
const contextDefaultKinds = new Map<string, 'object'>()
|
|
47
71
|
for (const c of constants) {
|
|
48
72
|
if (c.systemConstructKind !== 'createContext' || c.value === undefined) continue
|
|
49
73
|
contextDefaults.set(c.name, parseCreateContextDefault(c.value))
|
|
74
|
+
if (isObjectLiteralCreateContextDefault(c.value)) contextDefaultKinds.set(c.name, 'object')
|
|
50
75
|
}
|
|
51
76
|
if (contextDefaults.size === 0) return []
|
|
52
77
|
|
|
@@ -59,6 +84,7 @@ export function collectContextConsumers(metadata: IRMetadata): ContextConsumer[]
|
|
|
59
84
|
localName: c.name,
|
|
60
85
|
contextName: ctxName,
|
|
61
86
|
defaultValue: contextDefaults.get(ctxName) ?? null,
|
|
87
|
+
defaultKind: contextDefaultKinds.get(ctxName),
|
|
62
88
|
})
|
|
63
89
|
}
|
|
64
90
|
return consumers
|
|
@@ -87,6 +113,14 @@ function parseCreateContextDefault(source: string): string | number | boolean |
|
|
|
87
113
|
return null
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
/** True when `createContext(<arg>)`'s argument is an object-literal (`{ config: {} }`). */
|
|
117
|
+
function isObjectLiteralCreateContextDefault(source: string): boolean {
|
|
118
|
+
const expr = parseSingleExpression(source)
|
|
119
|
+
if (!expr || !ts.isCallExpression(expr)) return false
|
|
120
|
+
if (expr.arguments.length === 0) return false
|
|
121
|
+
return ts.isObjectLiteralExpression(expr.arguments[0])
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
function parseSingleExpression(source: string): ts.Expression | null {
|
|
91
125
|
const sf = ts.createSourceFile('__ctx.ts', `(${source})`, ts.ScriptTarget.Latest, false)
|
|
92
126
|
const stmt = sf.statements[0]
|
|
@@ -139,14 +173,65 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
139
173
|
const booleanAttrProps = new Set<string>()
|
|
140
174
|
const accessed = new Set<string>()
|
|
141
175
|
const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, 'g')
|
|
142
|
-
const scan = (s: string | undefined): void => {
|
|
176
|
+
const scan = (s: string | undefined, also?: Set<string>): void => {
|
|
143
177
|
if (!s) return
|
|
144
|
-
for (const m of s.matchAll(accessRe))
|
|
178
|
+
for (const m of s.matchAll(accessRe)) {
|
|
179
|
+
accessed.add(m[1])
|
|
180
|
+
also?.add(m[1])
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Coalesce-literal type pins: a read of the form `props.X ?? <literal>`
|
|
185
|
+
// (or `|| <literal>`) fixes X's type to the literal's. The Go adapter
|
|
186
|
+
// seeds a prop-derived signal/memo constructor from the synthetic field
|
|
187
|
+
// (`Count: in.Initial` where `Count int` came from evaluating
|
|
188
|
+
// `props.initial ?? 0`), so classifying `initial` with the string
|
|
189
|
+
// default produces a Go type mismatch the compiler rejects.
|
|
190
|
+
const coalesceLiteralTypes = new Map<string, 'number' | 'boolean' | 'string'>()
|
|
191
|
+
const pinCoalesceLiterals = (s: string | undefined): void => {
|
|
192
|
+
if (!s || !s.includes(propsObj)) return
|
|
193
|
+
const sf = ts.createSourceFile('__aug.ts', `(${s})`, ts.ScriptTarget.Latest, false)
|
|
194
|
+
const visit = (n: ts.Node): void => {
|
|
195
|
+
if (
|
|
196
|
+
ts.isBinaryExpression(n) &&
|
|
197
|
+
(n.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
|
198
|
+
n.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
199
|
+
) {
|
|
200
|
+
let left: ts.Expression = n.left
|
|
201
|
+
while (ts.isParenthesizedExpression(left)) left = left.expression
|
|
202
|
+
if (
|
|
203
|
+
ts.isPropertyAccessExpression(left) &&
|
|
204
|
+
ts.isIdentifier(left.expression) &&
|
|
205
|
+
left.expression.text === propsObj
|
|
206
|
+
) {
|
|
207
|
+
const name = left.name.text
|
|
208
|
+
let right: ts.Expression = n.right
|
|
209
|
+
while (ts.isParenthesizedExpression(right)) right = right.expression
|
|
210
|
+
if (ts.isPrefixUnaryExpression(right)) right = right.operand
|
|
211
|
+
const kind = ts.isNumericLiteral(right)
|
|
212
|
+
? 'number'
|
|
213
|
+
: right.kind === ts.SyntaxKind.TrueKeyword || right.kind === ts.SyntaxKind.FalseKeyword
|
|
214
|
+
? 'boolean'
|
|
215
|
+
: ts.isStringLiteralLike(right)
|
|
216
|
+
? 'string'
|
|
217
|
+
: null
|
|
218
|
+
if (kind && !coalesceLiteralTypes.has(name)) coalesceLiteralTypes.set(name, kind)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
ts.forEachChild(n, visit)
|
|
222
|
+
}
|
|
223
|
+
visit(sf)
|
|
145
224
|
}
|
|
146
225
|
|
|
147
226
|
// Memos, signals, init statements, effects: any `props.X` read.
|
|
148
|
-
for (const memo of ir.metadata.memos)
|
|
149
|
-
|
|
227
|
+
for (const memo of ir.metadata.memos) {
|
|
228
|
+
scan(memo.computation)
|
|
229
|
+
pinCoalesceLiterals(memo.computation)
|
|
230
|
+
}
|
|
231
|
+
for (const signal of ir.metadata.signals) {
|
|
232
|
+
scan(signal.initialValue)
|
|
233
|
+
pinCoalesceLiterals(signal.initialValue)
|
|
234
|
+
}
|
|
150
235
|
for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body)
|
|
151
236
|
for (const eff of ir.metadata.effects ?? []) scan((eff as { body?: string }).body)
|
|
152
237
|
|
|
@@ -157,12 +242,58 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
157
242
|
for (const c of ir.metadata.localConstants ?? []) {
|
|
158
243
|
if (c.isModule) continue
|
|
159
244
|
scan(c.value)
|
|
245
|
+
pinCoalesceLiterals(c.value)
|
|
160
246
|
}
|
|
161
247
|
|
|
162
248
|
// Template attribute exprs — also note bare-ref / boolean usage.
|
|
163
249
|
const walk = (node: IRNode | undefined): void => {
|
|
164
250
|
if (!node) return
|
|
251
|
+
// Non-attribute expression carriers (#2126 follow-up): a `props.X`
|
|
252
|
+
// read can reach the template as a bare scalar through a dynamic
|
|
253
|
+
// text child (`<p>{props.label}</p>`), a conditional / if-statement
|
|
254
|
+
// condition (`{props.show && …}`), or a loop's array expression
|
|
255
|
+
// (`(props.items ?? []).map(…)`). With a typed props object those
|
|
256
|
+
// names are already in `propsParams`; with an untyped `props`
|
|
257
|
+
// parameter these reads were invisible to this pass, so the emitted
|
|
258
|
+
// template referenced a var the props type / ssrDefaults never
|
|
259
|
+
// declared (a strict-mode 500 on Perl-family adapters, a missing
|
|
260
|
+
// struct field on Go). Text reads keep the default `string`
|
|
261
|
+
// classification; condition / loop-array reads land in the nillable
|
|
262
|
+
// bucket (`unknown` → omittable/interface-typed) since their values
|
|
263
|
+
// are only ever truth-tested or iterated.
|
|
264
|
+
const carrier = node as unknown as { type?: string; expr?: string; condition?: string; array?: string }
|
|
265
|
+
if (carrier.type === 'expression') {
|
|
266
|
+
scan(carrier.expr)
|
|
267
|
+
pinCoalesceLiterals(carrier.expr)
|
|
268
|
+
}
|
|
269
|
+
scan(carrier.condition, bareRefProps)
|
|
270
|
+
pinCoalesceLiterals(carrier.condition)
|
|
271
|
+
scan(carrier.array, bareRefProps)
|
|
165
272
|
const el = node as unknown as IRElement
|
|
273
|
+
// Component nodes carry the same AttrValue shapes under `props`
|
|
274
|
+
// (`<Child value={props.foo} />` reads `$foo` at child-props
|
|
275
|
+
// construction time in the SSR template) — but they are NOT HTML
|
|
276
|
+
// attributes: boolean-attr inference by name doesn't apply, and the
|
|
277
|
+
// value's type belongs to the child's contract, not to how this
|
|
278
|
+
// component renders it. Classify these reads as nillable (`unknown`)
|
|
279
|
+
// unless a `?? <literal>` pins a concrete type.
|
|
280
|
+
for (const prop of (node as unknown as { props?: IRElement['attrs'] }).props ?? []) {
|
|
281
|
+
const v = prop.value as { kind?: string; expr?: string; parts?: Array<{ type: string } & Record<string, string>> }
|
|
282
|
+
if (v?.parts) {
|
|
283
|
+
for (const part of v.parts) {
|
|
284
|
+
if (part.type === 'string') scan(part.value)
|
|
285
|
+
else if (part.type === 'ternary') {
|
|
286
|
+
scan(part.condition)
|
|
287
|
+
scan(part.whenTrue)
|
|
288
|
+
scan(part.whenFalse)
|
|
289
|
+
} else if (part.type === 'lookup') scan(part.key)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (v?.kind === 'expression' && typeof v.expr === 'string') {
|
|
293
|
+
scan(v.expr, bareRefProps)
|
|
294
|
+
pinCoalesceLiterals(v.expr)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
166
297
|
for (const attr of el.attrs ?? []) {
|
|
167
298
|
const v = attr.value as {
|
|
168
299
|
kind?: string
|
|
@@ -231,14 +362,13 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
231
362
|
if (existing.has(name)) continue
|
|
232
363
|
let raw: string
|
|
233
364
|
if (booleanAttrProps.has(name)) raw = 'boolean'
|
|
365
|
+
else if (coalesceLiteralTypes.has(name)) raw = coalesceLiteralTypes.get(name)!
|
|
234
366
|
else if (bareRefProps.has(name)) raw = 'unknown' // → interface{} (nillable, omittable)
|
|
235
367
|
else raw = 'string' // read in a memo / string context (e.g. className)
|
|
236
368
|
const type: TypeInfo =
|
|
237
|
-
raw === '
|
|
238
|
-
? { kind: '
|
|
239
|
-
: raw
|
|
240
|
-
? { kind: 'primitive', raw: 'string', primitive: 'string' }
|
|
241
|
-
: { kind: 'unknown', raw: 'unknown' }
|
|
369
|
+
raw === 'unknown'
|
|
370
|
+
? { kind: 'unknown', raw: 'unknown' }
|
|
371
|
+
: { kind: 'primitive', raw, primitive: raw as 'string' | 'number' | 'boolean' }
|
|
242
372
|
ir.metadata.propsParams.push({ name, type, optional: true })
|
|
243
373
|
existing.add(name)
|
|
244
374
|
}
|
package/src/compiler.ts
CHANGED
|
@@ -276,6 +276,7 @@ function compileMultipleComponents(
|
|
|
276
276
|
path: dir + output.componentName + adapter.extension,
|
|
277
277
|
content: output.rawTemplate,
|
|
278
278
|
type: 'markedTemplate',
|
|
279
|
+
componentName: output.componentName,
|
|
279
280
|
})
|
|
280
281
|
// SSR defaults, paired with the per-component template file via
|
|
281
282
|
// the matching basename (the build pipeline pairs them in
|
|
@@ -287,6 +288,7 @@ function compileMultipleComponents(
|
|
|
287
288
|
path: dir + output.componentName + '.ssr-defaults.json',
|
|
288
289
|
content: JSON.stringify(ssrDefaults),
|
|
289
290
|
type: 'ssrDefaults',
|
|
291
|
+
componentName: output.componentName,
|
|
290
292
|
})
|
|
291
293
|
}
|
|
292
294
|
}
|
|
@@ -672,6 +674,7 @@ export function compileJSX(
|
|
|
672
674
|
path: filePath.replace(/\.tsx?$/, adapter.extension),
|
|
673
675
|
content,
|
|
674
676
|
type: 'markedTemplate',
|
|
677
|
+
componentName: componentIR.metadata.componentName,
|
|
675
678
|
})
|
|
676
679
|
|
|
677
680
|
// SSR defaults — JSON-encoded seed values for the template's
|
|
@@ -686,6 +689,7 @@ export function compileJSX(
|
|
|
686
689
|
path: filePath.replace(/\.tsx?$/, '.ssr-defaults.json'),
|
|
687
690
|
content: JSON.stringify(ssrDefaults),
|
|
688
691
|
type: 'ssrDefaults',
|
|
692
|
+
componentName: componentIR.metadata.componentName,
|
|
689
693
|
})
|
|
690
694
|
}
|
|
691
695
|
}
|