@barefootjs/jsx 0.31.1 → 0.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interface.d.ts +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1531 -1155
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +295 -5
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +70 -11
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +11 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +245 -107
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +26 -6
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/scope/binding-scope.ts +238 -0
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +18 -0
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
+
AttrValue,
|
|
9
10
|
ComponentIR,
|
|
10
11
|
IRNode,
|
|
12
|
+
IRTemplatePart,
|
|
11
13
|
ImportSpecifier,
|
|
12
14
|
} from '../types.ts'
|
|
15
|
+
import { templatePartsToJsExpr } from '../template-parts.ts'
|
|
13
16
|
import { BF_SCOPE, BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
14
17
|
import { BaseAdapter } from './interface.ts'
|
|
15
18
|
import type { CallbackBodyAcceptor } from './interface.ts'
|
|
16
19
|
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
17
20
|
import { formatParamWithType, findReachableNames } from '../module-exports.ts'
|
|
21
|
+
import { extractFreeIdentifiersFromText } from '../ir-to-client-js/csr-substitute.ts'
|
|
18
22
|
|
|
19
23
|
export interface JsxAdapterConfig {
|
|
20
24
|
/** Use typed versions (typedInitialValue, etc.) for type-safe .tsx output */
|
|
@@ -144,11 +148,15 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
144
148
|
const initialValue = rawInitialValue.trim().startsWith('{') ? `(${rawInitialValue})` : rawInitialValue
|
|
145
149
|
|
|
146
150
|
// When preserveTypes and typedInitialValue is absent but signal.type has a meaningful
|
|
147
|
-
// type from a generic parameter, add a type assertion to prevent TS inference issues
|
|
151
|
+
// type from a generic parameter, add a type assertion to prevent TS inference issues.
|
|
152
|
+
// A bare `object` raw is the analyzer's coarse KIND, not a real type — asserting
|
|
153
|
+
// `as object` only destroys the initializer's inferred literal type (a
|
|
154
|
+
// `{ x: 0, y: 0 }` signal getter stops matching `() => { x: number; y: number }`).
|
|
148
155
|
const needsTypeAssertion = preserveTypes
|
|
149
156
|
&& !signal.typedInitialValue
|
|
150
157
|
&& signal.type.kind !== 'unknown'
|
|
151
158
|
&& signal.type.kind !== 'primitive'
|
|
159
|
+
&& signal.type.raw !== 'object'
|
|
152
160
|
if (needsTypeAssertion) {
|
|
153
161
|
lines.push(` const ${signal.getter} = () => ${initialValue} as ${signal.type.raw}`)
|
|
154
162
|
} else {
|
|
@@ -173,12 +181,27 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
173
181
|
lines.push(` const ${memo.name} = ${computation}`)
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
// Include local constants — skip unreachable ones (only used in event
|
|
184
|
+
// Include local constants — skip unreachable ones (only used in event
|
|
185
|
+
// handlers). Genuinely module-scope constants are NOT localised here:
|
|
186
|
+
// they're emitted at module scope by `generateModuleScopeDeclarations`
|
|
187
|
+
// so module-scope types that reference them (via `typeof`) keep
|
|
188
|
+
// resolving, matching the client bundle's `emitModuleLevelDeclarations`
|
|
189
|
+
// (#2570). `moduleScopeDeclarationNames` — not the raw `isModule` flag
|
|
190
|
+
// — decides the split; see its docstring.
|
|
191
|
+
const moduleScopeNames = this.moduleScopeDeclarationNames(ir)
|
|
177
192
|
for (const constant of ir.metadata.localConstants) {
|
|
178
193
|
if (constant.isExported) continue
|
|
194
|
+
if (moduleScopeNames.has(constant.name)) continue
|
|
179
195
|
const keyword = constant.declarationKind ?? 'const'
|
|
180
196
|
if (!constant.value) {
|
|
181
|
-
|
|
197
|
+
// No initializer (e.g. `let emblaApi: EmblaCarouselType | undefined`)
|
|
198
|
+
// — carry the declared type annotation through so `.tsx` output
|
|
199
|
+
// doesn't fall back to implicit `any` (TS7034/TS7005, #2573).
|
|
200
|
+
const typeAnnotation =
|
|
201
|
+
preserveTypes && (constant.typeAnnotation ?? constant.type)
|
|
202
|
+
? `: ${constant.typeAnnotation ?? constant.type?.raw}`
|
|
203
|
+
: ''
|
|
204
|
+
lines.push(` ${keyword} ${constant.name}${typeAnnotation}`)
|
|
182
205
|
continue
|
|
183
206
|
}
|
|
184
207
|
const value = constant.value.trim()
|
|
@@ -193,11 +216,24 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
193
216
|
const constValue = preserveTypes
|
|
194
217
|
? (constant.typedValue ?? constant.value)
|
|
195
218
|
: constant.value
|
|
196
|
-
|
|
219
|
+
// Preserve an explicit `let` type annotation from source (#2589) —
|
|
220
|
+
// without it, TS infers the initializer's (often narrower) type and
|
|
221
|
+
// later reassignments/reads fail under strict (TS7034/TS7005, and
|
|
222
|
+
// TS2339 via `never` narrowing). `const` is left alone: its type
|
|
223
|
+
// always infers correctly from the (immutable) initializer, so
|
|
224
|
+
// adding annotations there would only churn snapshots.
|
|
225
|
+
const letTypeAnnotation =
|
|
226
|
+
preserveTypes && keyword === 'let' && constant.typeAnnotation
|
|
227
|
+
? `: ${constant.typeAnnotation}`
|
|
228
|
+
: ''
|
|
229
|
+
lines.push(` ${keyword} ${constant.name}${letTypeAnnotation} = ${constValue}`)
|
|
197
230
|
}
|
|
198
231
|
|
|
199
|
-
// Include local functions — skip unreachable ones (only used in event
|
|
232
|
+
// Include local functions — skip unreachable ones (only used in event
|
|
233
|
+
// handlers). Genuinely module-scope functions stay at module scope,
|
|
234
|
+
// same as constants above.
|
|
200
235
|
for (const func of localFunctions) {
|
|
236
|
+
if (moduleScopeNames.has(func.name)) continue
|
|
201
237
|
if (!reachable.has(func.name)) continue
|
|
202
238
|
// Prefer the source-verbatim signature when types are preserved so
|
|
203
239
|
// type-predicate annotations (`element is { tag: unknown; … }`) and
|
|
@@ -219,6 +255,222 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
219
255
|
return lines.join('\n')
|
|
220
256
|
}
|
|
221
257
|
|
|
258
|
+
// ===========================================================================
|
|
259
|
+
// Module-Scope Declarations
|
|
260
|
+
// ===========================================================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Names that genuinely live at module scope in the emitted template.
|
|
264
|
+
*
|
|
265
|
+
* The analyzer's `isModule` flag is NOT a lexical-scope oracle: the
|
|
266
|
+
* module walker recurses into component bodies, so a component-body
|
|
267
|
+
* helper (calendar's `renderMonthGrid`, xyflow's edge handlers) can
|
|
268
|
+
* carry `isModule: true` while closing over component state. Hoisting
|
|
269
|
+
* such a helper breaks every reference (TS2304/TS2552 — and worse,
|
|
270
|
+
* wrong runtime scope). So candidates are demoted by a forward-
|
|
271
|
+
* reachability fixpoint, mirroring the client bundle's
|
|
272
|
+
* `computeDeclarationScopes` (`ir-to-client-js/compute-scope.ts`): a
|
|
273
|
+
* candidate that references component scope — a signal getter/setter, a
|
|
274
|
+
* memo, a prop, a body const/function, or an already-demoted candidate
|
|
275
|
+
* — is component-scoped, transitively. References are REAL identifier
|
|
276
|
+
* nodes from a TS AST walk (`extractFreeIdentifiersFromText`), not text
|
|
277
|
+
* matches — a component-scope name occurring inside a candidate's
|
|
278
|
+
* string literal (`[data-state="open"]` vs a body const `open`) is not
|
|
279
|
+
* a reference and must not demote it.
|
|
280
|
+
*
|
|
281
|
+
* EXPORTED candidates are never demoted: `export` is only legal at
|
|
282
|
+
* module top level, so an exported declaration is lexically module-
|
|
283
|
+
* scoped by construction — and a spurious demotion would make it
|
|
284
|
+
* vanish entirely (the body loop skips exported declarations and
|
|
285
|
+
* `generateModuleExports` is told to skip value declarations).
|
|
286
|
+
*
|
|
287
|
+
* Memoized per IR: `generateModuleScopeDeclarations` (module emission)
|
|
288
|
+
* and `generateSignalInitializers` (body emission) must agree on the
|
|
289
|
+
* split or a declaration is emitted twice or not at all.
|
|
290
|
+
*/
|
|
291
|
+
private readonly moduleScopeNamesCache = new WeakMap<ComponentIR, Set<string>>()
|
|
292
|
+
|
|
293
|
+
protected moduleScopeDeclarationNames(ir: ComponentIR): Set<string> {
|
|
294
|
+
const cached = this.moduleScopeNamesCache.get(ir)
|
|
295
|
+
if (cached) return cached
|
|
296
|
+
|
|
297
|
+
const componentScope = new Set<string>()
|
|
298
|
+
for (const sig of ir.metadata.signals) {
|
|
299
|
+
if (sig.isModule) continue
|
|
300
|
+
componentScope.add(sig.getter)
|
|
301
|
+
if (sig.setter) componentScope.add(sig.setter)
|
|
302
|
+
}
|
|
303
|
+
for (const memo of ir.metadata.memos) {
|
|
304
|
+
if (!memo.isModule) componentScope.add(memo.name)
|
|
305
|
+
}
|
|
306
|
+
for (const p of ir.metadata.propsParams) componentScope.add(p.name)
|
|
307
|
+
if (ir.metadata.propsObjectName) componentScope.add(ir.metadata.propsObjectName)
|
|
308
|
+
if (ir.metadata.restPropsName) componentScope.add(ir.metadata.restPropsName)
|
|
309
|
+
for (const c of ir.metadata.localConstants) {
|
|
310
|
+
if (!c.isModule) componentScope.add(c.name)
|
|
311
|
+
}
|
|
312
|
+
for (const f of ir.metadata.localFunctions) {
|
|
313
|
+
if (!f.isModule) componentScope.add(f.name)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Exported declarations join the emit set unconditionally (see the
|
|
317
|
+
// docstring); only non-exported candidates enter the demotion
|
|
318
|
+
// fixpoint, each carrying its REAL free-identifier set.
|
|
319
|
+
const exported = new Set<string>()
|
|
320
|
+
const candidates = new Map<string, ReadonlySet<string>>()
|
|
321
|
+
for (const c of ir.metadata.localConstants) {
|
|
322
|
+
if (!c.isModule) continue
|
|
323
|
+
// JSX-valued consts are inlined at their usage sites at IR level
|
|
324
|
+
// (#547/#569) — same skip as the client's `classifyConstant`.
|
|
325
|
+
if (c.isJsx || c.isJsxFunction) continue
|
|
326
|
+
if (c.isExported) {
|
|
327
|
+
exported.add(c.name)
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
// The analyzer precomputes the value's free identifiers; fall back
|
|
331
|
+
// to the same AST walk for values it didn't cover.
|
|
332
|
+
candidates.set(c.name, c.freeIdentifiers ?? extractFreeIdentifiersFromText(c.value ?? ''))
|
|
333
|
+
}
|
|
334
|
+
for (const f of ir.metadata.localFunctions) {
|
|
335
|
+
if (!f.isModule) continue
|
|
336
|
+
// JSX-returning helpers (single- and multi-return) are inlined at
|
|
337
|
+
// their call sites (#569/#932); emitting them too would resurrect
|
|
338
|
+
// them as dead module-scope declarations — and the multi-return
|
|
339
|
+
// bodies carry raw source JSX that must not reach the template
|
|
340
|
+
// verbatim. Same skips as the client's `computeDeclarationScopes`.
|
|
341
|
+
if (f.isJsxFunction || f.isMultiReturnJsxHelper) continue
|
|
342
|
+
if (f.isExported) {
|
|
343
|
+
exported.add(f.name)
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
// Wrap as an ARROW so the extractor's parameter shadowing applies
|
|
347
|
+
// (it tracks arrow params only) — param references then don't count
|
|
348
|
+
// as free, matching the client fixpoint's `refs.delete(p.name)`.
|
|
349
|
+
const params = f.typedParams !== undefined
|
|
350
|
+
? f.typedParams
|
|
351
|
+
: f.params.map(formatParamWithType).join(', ')
|
|
352
|
+
candidates.set(f.name, extractFreeIdentifiersFromText(`(${params}) => ${f.body}`))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const referencesAny = (refs: ReadonlySet<string>, names: ReadonlySet<string>): boolean => {
|
|
356
|
+
for (const ref of refs) {
|
|
357
|
+
if (names.has(ref)) return true
|
|
358
|
+
}
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
let changed = true
|
|
362
|
+
while (changed) {
|
|
363
|
+
changed = false
|
|
364
|
+
for (const [name, refs] of candidates) {
|
|
365
|
+
if (referencesAny(refs, componentScope)) {
|
|
366
|
+
candidates.delete(name)
|
|
367
|
+
componentScope.add(name)
|
|
368
|
+
changed = true
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = new Set([...exported, ...candidates.keys()])
|
|
374
|
+
this.moduleScopeNamesCache.set(ir, result)
|
|
375
|
+
return result
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Module-scope type, constant, and function declarations for the
|
|
380
|
+
* emitted template, kept at MODULE scope in SOURCE ORDER — the emitted
|
|
381
|
+
* module preserves the source module's shape, as the client bundle
|
|
382
|
+
* already does (`emitModuleLevelDeclarations` in `ir-to-client-js`).
|
|
383
|
+
* Root cure for the #2570 family: type declarations are re-emitted
|
|
384
|
+
* verbatim at module scope, so any value they reference through
|
|
385
|
+
* `typeof` (a type alias's `keyof typeof strokePaths`, a props
|
|
386
|
+
* annotation's `keyof typeof modes`) must be declared there too.
|
|
387
|
+
* Localising those values into each component body — the previous
|
|
388
|
+
* shape — failed the query with TS2304, and an unresolved
|
|
389
|
+
* `keyof typeof` degrades to `keyof any`, silently widening the type
|
|
390
|
+
* to `string | number | symbol` and taking every downstream check
|
|
391
|
+
* with it.
|
|
392
|
+
*
|
|
393
|
+
* EXPORTED module declarations are emitted here too (with their
|
|
394
|
+
* `export` keyword), interleaved with the non-exported ones in source
|
|
395
|
+
* order — not split off to `generateModuleExports`' section, which is
|
|
396
|
+
* emitted after this one and so would put an exported const AFTER a
|
|
397
|
+
* non-exported const that reads it (input-otp's `patternPresets`
|
|
398
|
+
* reading `REGEXP_ONLY_DIGITS`): a module-load TDZ crash, not just
|
|
399
|
+
* TS2448. The compiler skips value declarations in
|
|
400
|
+
* `generateModuleExports` when `moduleConstantsIncludeExports` is set
|
|
401
|
+
* on the sections.
|
|
402
|
+
*
|
|
403
|
+
* Emission is deliberately UNFILTERED by per-component reachability: an
|
|
404
|
+
* unused module declaration in the emitted template is harmless and
|
|
405
|
+
* matches the source module. In a multi-component file each component's
|
|
406
|
+
* adapter output carries its own block (the analyzer collects the module
|
|
407
|
+
* declarations lexically preceding the component, so blocks can be
|
|
408
|
+
* unequal prefixes); the compiler merges them with top-level-STATEMENT
|
|
409
|
+
* dedup, so shared declarations land exactly once in source order.
|
|
410
|
+
*
|
|
411
|
+
* `new WeakMap()` bindings stay client-only, and exported
|
|
412
|
+
* `createContext()` bindings stay unemitted, exactly as before.
|
|
413
|
+
*/
|
|
414
|
+
protected generateModuleScopeDeclarations(ir: ComponentIR): string {
|
|
415
|
+
const { preserveTypes } = this.jsxConfig
|
|
416
|
+
const moduleNames = this.moduleScopeDeclarationNames(ir)
|
|
417
|
+
const entries: Array<{ line: number, text: string }> = []
|
|
418
|
+
|
|
419
|
+
for (const t of ir.metadata.typeDefinitions) {
|
|
420
|
+
entries.push({ line: t.loc.start.line, text: t.definition })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const c of ir.metadata.localConstants) {
|
|
424
|
+
if (!c.isModule || !moduleNames.has(c.name)) continue
|
|
425
|
+
const keyword = c.declarationKind ?? 'const'
|
|
426
|
+
const exportKw = c.isExported ? 'export ' : ''
|
|
427
|
+
if (!c.value) {
|
|
428
|
+
// No initializer (e.g. module-scope `let pending: number`) — carry
|
|
429
|
+
// the declared type annotation through, mirroring the function-scope
|
|
430
|
+
// fix above (#2573 / #2589).
|
|
431
|
+
const typeAnnotation =
|
|
432
|
+
preserveTypes && (c.typeAnnotation ?? c.type)
|
|
433
|
+
? `: ${c.typeAnnotation ?? c.type?.raw}`
|
|
434
|
+
: ''
|
|
435
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name}${typeAnnotation}` })
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
const trimmed = c.value.trim()
|
|
439
|
+
if (/^new WeakMap\b/.test(trimmed)) continue
|
|
440
|
+
if (c.isExported && /^createContext\b/.test(trimmed)) continue
|
|
441
|
+
const value = preserveTypes ? (c.typedValue ?? c.value) : c.value
|
|
442
|
+
// Preserve an explicit module-scope `let` type annotation from source
|
|
443
|
+
// (#2589) — see the function-scope sibling above for rationale. `const`
|
|
444
|
+
// is left alone: its type always infers correctly from the (immutable)
|
|
445
|
+
// initializer.
|
|
446
|
+
const letTypeAnnotation =
|
|
447
|
+
preserveTypes && keyword === 'let' && c.typeAnnotation
|
|
448
|
+
? `: ${c.typeAnnotation}`
|
|
449
|
+
: ''
|
|
450
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name}${letTypeAnnotation} = ${value}` })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const f of ir.metadata.localFunctions) {
|
|
454
|
+
if (!f.isModule || !moduleNames.has(f.name)) continue
|
|
455
|
+
const params = preserveTypes && f.typedParams !== undefined
|
|
456
|
+
? f.typedParams
|
|
457
|
+
: f.params.map(formatParamWithType).join(', ')
|
|
458
|
+
const returnAnnotation = preserveTypes && f.typedReturnType
|
|
459
|
+
? `: ${f.typedReturnType}`
|
|
460
|
+
: ''
|
|
461
|
+
const body = preserveTypes ? (f.typedBody ?? f.body) : f.body
|
|
462
|
+
const asyncKw = f.isAsync ? 'async ' : ''
|
|
463
|
+
const exportKw = f.isExported ? 'export ' : ''
|
|
464
|
+
entries.push({
|
|
465
|
+
line: f.loc.start.line,
|
|
466
|
+
text: `${exportKw}${asyncKw}function ${f.name}(${params})${returnAnnotation} ${body}`,
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
entries.sort((a, b) => a.line - b.line)
|
|
471
|
+
return entries.map(e => e.text).join('\n')
|
|
472
|
+
}
|
|
473
|
+
|
|
222
474
|
// ===========================================================================
|
|
223
475
|
// Raw Node Rendering
|
|
224
476
|
// ===========================================================================
|
|
@@ -233,6 +485,44 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
233
485
|
return this.renderNode(node)
|
|
234
486
|
}
|
|
235
487
|
|
|
488
|
+
// ===========================================================================
|
|
489
|
+
// Template Part Rendering
|
|
490
|
+
// ===========================================================================
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Render a structured `template` variant's parts as JS template-literal
|
|
494
|
+
* source for this adapter's .tsx output, carrying the `preserveTypes`
|
|
495
|
+
* index annotation on inlined `lookup` records (#2565 — see
|
|
496
|
+
* `lookupPartToJsExpr`).
|
|
497
|
+
*/
|
|
498
|
+
protected renderTemplatePartsAsJs(parts: readonly IRTemplatePart[]): string {
|
|
499
|
+
return templatePartsToJsExpr(parts, { typed: this.jsxConfig.preserveTypes })
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The JS source for an `expression` attribute / component-prop value.
|
|
504
|
+
*
|
|
505
|
+
* A component-prop `template` is collapsed into an `expression` at IR
|
|
506
|
+
* construction time (component props are runtime values, not HTML
|
|
507
|
+
* attribute bodies), which drops it out of `renderTemplatePartsAsJs`'s
|
|
508
|
+
* reach — the same inlined-record index, minus the type annotation
|
|
509
|
+
* (#2565). The collapse keeps its `parts`, so re-render from those when
|
|
510
|
+
* `expr` is still EXACTLY the neutral collapse. Any later rewrite of
|
|
511
|
+
* `expr` (a presence peel, a prop-ref rewrite) fails the identity check
|
|
512
|
+
* and wins, so this can only ever add the annotation, never undo a
|
|
513
|
+
* downstream edit.
|
|
514
|
+
*/
|
|
515
|
+
protected expressionValueToJs(value: Extract<AttrValue, { kind: 'expression' }>): string {
|
|
516
|
+
if (
|
|
517
|
+
this.jsxConfig.preserveTypes &&
|
|
518
|
+
value.parts &&
|
|
519
|
+
value.expr === templatePartsToJsExpr(value.parts)
|
|
520
|
+
) {
|
|
521
|
+
return this.renderTemplatePartsAsJs(value.parts)
|
|
522
|
+
}
|
|
523
|
+
return value.expr
|
|
524
|
+
}
|
|
525
|
+
|
|
236
526
|
// ===========================================================================
|
|
237
527
|
// Hydration Markers
|
|
238
528
|
// ===========================================================================
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Generates simple JSX output without framework-specific features.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import ts from 'typescript'
|
|
8
9
|
import type {
|
|
9
10
|
ComponentIR,
|
|
10
11
|
IRNode,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
import type { AdapterOutput, TemplateSections } from './interface.ts'
|
|
21
22
|
import { type JsxAdapterConfig, JsxAdapter } from './jsx-adapter.ts'
|
|
22
23
|
import { rewriteImportsForTemplate } from './template-imports.ts'
|
|
24
|
+
import { propsDestructureBinding } from '../props-binding.ts'
|
|
23
25
|
|
|
24
26
|
export class TestAdapter extends JsxAdapter {
|
|
25
27
|
name = 'test'
|
|
@@ -31,6 +33,7 @@ export class TestAdapter extends JsxAdapter {
|
|
|
31
33
|
this.componentName = ir.metadata.componentName
|
|
32
34
|
|
|
33
35
|
const imports = this.generateImports(ir)
|
|
36
|
+
const moduleConstants = this.generateModuleScopeDeclarations(ir)
|
|
34
37
|
const types = this.generateTypes(ir)
|
|
35
38
|
const component = this.generateComponent(ir)
|
|
36
39
|
|
|
@@ -43,10 +46,12 @@ export class TestAdapter extends JsxAdapter {
|
|
|
43
46
|
types: types || '',
|
|
44
47
|
component,
|
|
45
48
|
defaultExport,
|
|
49
|
+
moduleConstants,
|
|
50
|
+
moduleConstantsIncludeExports: true,
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// Assemble template for backward compat
|
|
49
|
-
const template = [imports, types, component].filter(Boolean).join('\n\n') + defaultExport
|
|
54
|
+
const template = [imports, moduleConstants, types, component].filter(Boolean).join('\n\n') + defaultExport
|
|
50
55
|
|
|
51
56
|
return {
|
|
52
57
|
template,
|
|
@@ -84,9 +89,9 @@ export class TestAdapter extends JsxAdapter {
|
|
|
84
89
|
generateTypes(ir: ComponentIR): string | null {
|
|
85
90
|
const lines: string[] = []
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
// Source type declarations are emitted once at module scope by
|
|
93
|
+
// `generateModuleScopeDeclarations` (#2570) — only the synthesized
|
|
94
|
+
// hydration alias is per-component.
|
|
90
95
|
|
|
91
96
|
// Only generate PropsWithHydration when destructured-props pattern uses it
|
|
92
97
|
const propsTypeName = ir.metadata.propsType?.raw
|
|
@@ -122,8 +127,10 @@ export class TestAdapter extends JsxAdapter {
|
|
|
122
127
|
const bodyRefText = [jsxBody, signalInits, scopeIdLine].join('\n')
|
|
123
128
|
const bfScopeAlias = /\b__bfScope\b/.test(bodyRefText) ? '__bfScope' : '__bfScope: _bfScope'
|
|
124
129
|
|
|
130
|
+
// `key: local` rename-aware bindings, shared with the Hono SSR
|
|
131
|
+
// destructure — one renderer, zero drift (`propsDestructureBinding`).
|
|
125
132
|
const propsParams = ir.metadata.propsParams
|
|
126
|
-
.map((p: ParamInfo) => (p
|
|
133
|
+
.map((p: ParamInfo) => propsDestructureBinding(p))
|
|
127
134
|
.join(', ')
|
|
128
135
|
|
|
129
136
|
const restPropsName = ir.metadata.restPropsName
|
|
@@ -325,11 +332,7 @@ export class TestAdapter extends JsxAdapter {
|
|
|
325
332
|
// Simple stringifier for `template`-kind values; tests only need a
|
|
326
333
|
// recognisable JSX shape, not byte-exact reproduction.
|
|
327
334
|
const v = value as { kind: 'template'; parts: import('../types.ts').IRTemplatePart[] }
|
|
328
|
-
return
|
|
329
|
-
if (p.type === 'string') return p.value
|
|
330
|
-
if (p.type === 'ternary') return `\${${p.condition} ? '${p.whenTrue}' : '${p.whenFalse}'}`
|
|
331
|
-
return `\${(${JSON.stringify(p.cases)})[${p.key}]}`
|
|
332
|
-
}).join('') + '`'
|
|
335
|
+
return this.renderTemplatePartsAsJs(v.parts)
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
private renderComponentProps(comp: IRComponent): string {
|
package/src/analyzer.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
10
|
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, FactoryRenameSite, SourceLocation } from './types.ts'
|
|
11
|
-
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
11
|
+
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr, tsNodeToParsedExpr } from './expression-parser.ts'
|
|
12
12
|
import type { CallbackBodyAcceptor } from './adapters/interface.ts'
|
|
13
13
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
14
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
14
15
|
import { incrementCounter } from './instrumentation.ts'
|
|
15
16
|
import {
|
|
16
17
|
type AnalyzerContext,
|
|
@@ -503,8 +504,18 @@ function visit(
|
|
|
503
504
|
collectAmbientGlobals(node, ctx)
|
|
504
505
|
}
|
|
505
506
|
|
|
506
|
-
// Module-level constants (outside component)
|
|
507
|
-
|
|
507
|
+
// Module-level constants (outside component). Ambient statements
|
|
508
|
+
// (`declare let X: T`) are type-only contracts with no runtime binding —
|
|
509
|
+
// collectAmbientGlobals above already tracks them for BF052, and
|
|
510
|
+
// re-emitting one as a runtime `let` would shadow the real global, so
|
|
511
|
+
// they must not reach collectConstant. (Previously excluded only by
|
|
512
|
+
// accident: this path required an initializer, which `declare`
|
|
513
|
+
// statements never have — the #2589 uninitialized-`let` fix removed
|
|
514
|
+
// that gate, so the exclusion is now explicit.)
|
|
515
|
+
const isDeclareStatement =
|
|
516
|
+
ts.isVariableStatement(node) &&
|
|
517
|
+
(node.modifiers?.some(m => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false)
|
|
518
|
+
if (ts.isVariableStatement(node) && !ctx.componentNode && !isDeclareStatement) {
|
|
508
519
|
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
509
520
|
const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0
|
|
510
521
|
const isModuleClientDirective = hasLeadingClientDirectiveOnStatement(node, ctx.sourceFile)
|
|
@@ -524,9 +535,15 @@ function visit(
|
|
|
524
535
|
}
|
|
525
536
|
continue
|
|
526
537
|
}
|
|
538
|
+
// An initializer is required for `const` (TS grammar enforces this),
|
|
539
|
+
// but an uninitialized module-scope `let` (e.g. `let pending: number`)
|
|
540
|
+
// is legal source and must still be collected — mirroring the
|
|
541
|
+
// component-scope path below (line ~687), which has never gated on
|
|
542
|
+
// `decl.initializer` — so its declared type carries into the emitted
|
|
543
|
+
// template instead of the declaration vanishing outright (#2589).
|
|
527
544
|
if (
|
|
528
545
|
ts.isIdentifier(decl.name) &&
|
|
529
|
-
decl.initializer &&
|
|
546
|
+
(decl.initializer || isLet) &&
|
|
530
547
|
!isArrowComponentFunction(decl)
|
|
531
548
|
) {
|
|
532
549
|
collectConstant(decl, ctx, true, isLet ? 'let' : 'const', isExported)
|
|
@@ -1273,7 +1290,8 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1273
1290
|
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1274
1291
|
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1275
1292
|
if (propNames.size > 0) {
|
|
1276
|
-
|
|
1293
|
+
const propAliases = buildPropAliasMap(ctx.propsParams)
|
|
1294
|
+
templateInitialValue = rewriteBarePropRefs(initialValue, callExpr.arguments[0], propNames, undefined, propAliases)
|
|
1277
1295
|
}
|
|
1278
1296
|
}
|
|
1279
1297
|
|
|
@@ -1613,7 +1631,8 @@ function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
|
1613
1631
|
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1614
1632
|
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1615
1633
|
if (propNames.size > 0) {
|
|
1616
|
-
|
|
1634
|
+
const propAliases = buildPropAliasMap(ctx.propsParams)
|
|
1635
|
+
templateComputation = rewriteBarePropRefs(computation, callExpr.arguments[0], propNames, undefined, propAliases)
|
|
1617
1636
|
}
|
|
1618
1637
|
}
|
|
1619
1638
|
|
|
@@ -3129,9 +3148,15 @@ function collectConstant(
|
|
|
3129
3148
|
let templateValue: string | undefined
|
|
3130
3149
|
if (value && ctx.propsParams.length > 0 && node.initializer) {
|
|
3131
3150
|
let propNames: Set<string>
|
|
3151
|
+
// Local → caller-facing key (#2524 CSR half) — only Case 1 propsParams
|
|
3152
|
+
// entries can carry `sourceName`; Case 2's alias set below is keyed by
|
|
3153
|
+
// the destructured-from-`props.X` local name, which by its own
|
|
3154
|
+
// `isPureAlias` construction always equals the source key.
|
|
3155
|
+
let propAliases: Map<string, string> | undefined
|
|
3132
3156
|
if (!ctx.propsObjectName) {
|
|
3133
3157
|
// Case 1: destructured-arg — all propsParams are bare locals.
|
|
3134
3158
|
propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
3159
|
+
propAliases = buildPropAliasMap(ctx.propsParams)
|
|
3135
3160
|
} else {
|
|
3136
3161
|
// Case 2: `(props)`-arg — restrict to body-destructured pure aliases
|
|
3137
3162
|
// of `props.X` (entries pushed by the body-destructure expansion at
|
|
@@ -3157,7 +3182,7 @@ function collectConstant(
|
|
|
3157
3182
|
)
|
|
3158
3183
|
}
|
|
3159
3184
|
if (propNames.size > 0) {
|
|
3160
|
-
const rewritten = rewriteBarePropRefs(value, node.initializer, propNames)
|
|
3185
|
+
const rewritten = rewriteBarePropRefs(value, node.initializer, propNames, undefined, propAliases)
|
|
3161
3186
|
if (rewritten !== undefined) {
|
|
3162
3187
|
templateValue = rewritten
|
|
3163
3188
|
// For the body-destructure case, also rewrite the init-body
|
|
@@ -3194,6 +3219,7 @@ function collectConstant(
|
|
|
3194
3219
|
value,
|
|
3195
3220
|
parsed,
|
|
3196
3221
|
typedValue: typedValue !== value ? typedValue : undefined,
|
|
3222
|
+
typeAnnotation: node.type ? node.type.getText(ctx.sourceFile) : undefined,
|
|
3197
3223
|
valueBranches,
|
|
3198
3224
|
declarationKind,
|
|
3199
3225
|
isExported,
|
|
@@ -3307,6 +3333,12 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3307
3333
|
const resolvedType: TypeInfo = member?.type ?? { kind: 'unknown', raw: 'unknown' }
|
|
3308
3334
|
|
|
3309
3335
|
const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false
|
|
3336
|
+
// Structured mirror of `defaultValue`, from the binding element's OWN
|
|
3337
|
+
// `initializer` node — `tsNodeToParsedExpr` converts the already-parsed
|
|
3338
|
+
// AST directly, no re-parse of the `defaultValue` text (same convention
|
|
3339
|
+
// as `SignalInfo.parsed`). An unsupported shape leaves `parsed` unset;
|
|
3340
|
+
// consumers fall back to `defaultValue` text.
|
|
3341
|
+
const parsedDefault = element.initializer ? tsNodeToParsedExpr(element.initializer) : undefined
|
|
3310
3342
|
ctx.propsParams.push({
|
|
3311
3343
|
name: localName,
|
|
3312
3344
|
type: resolvedType,
|
|
@@ -3314,6 +3346,7 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3314
3346
|
// the caller may omit the prop.
|
|
3315
3347
|
optional: !!member?.optional || !!element.initializer,
|
|
3316
3348
|
defaultValue,
|
|
3349
|
+
...(parsedDefault && parsedDefault.kind !== 'unsupported' && { parsed: parsedDefault }),
|
|
3317
3350
|
defaultContainsArrow: defaultContainsArrow || undefined,
|
|
3318
3351
|
// Only aliased bindings carry the source key — see ParamInfo.sourceName.
|
|
3319
3352
|
...(sourcePropName !== localName && { sourceName: sourcePropName }),
|
|
@@ -3631,21 +3664,47 @@ function inferTypeFromValue(value: string): TypeInfo {
|
|
|
3631
3664
|
return { kind: 'unknown', raw: 'unknown' }
|
|
3632
3665
|
}
|
|
3633
3666
|
|
|
3667
|
+
/**
|
|
3668
|
+
* Identifiers that appear as a direct call (`name(...)`) in `code`,
|
|
3669
|
+
* collected off a real TS parse — never a regex over the raw text, which
|
|
3670
|
+
* false-matches inside comments and string literals (the repo-wide
|
|
3671
|
+
* "no regex over JS syntax" rule). A doc comment mentioning
|
|
3672
|
+
* `otherSignal()` inside a memo body must NOT become a dependency
|
|
3673
|
+
* (phantom `internalValue`/`controlledValue` deps on slider's
|
|
3674
|
+
* `percentage`, #2581 review). A raw token scan is not enough either:
|
|
3675
|
+
* without a parser driving `reScanTemplateToken`, everything between a
|
|
3676
|
+
* substitution's closing `}` and the next backtick is mis-lexed as code
|
|
3677
|
+
* (and the template tail swallows real code after it), so a read
|
|
3678
|
+
* following a `${...}` template — xyflow's `visibleClass` reading
|
|
3679
|
+
* `animated()` after the `${BF_FLOW_EDGE_SELECTED}` line — would vanish.
|
|
3680
|
+
*/
|
|
3681
|
+
function collectCalledIdentifiers(code: string): Set<string> {
|
|
3682
|
+
const called = new Set<string>()
|
|
3683
|
+
const sf = ts.createSourceFile('__deps__.tsx', code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
|
|
3684
|
+
const visit = (node: ts.Node): void => {
|
|
3685
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
3686
|
+
called.add(node.expression.text)
|
|
3687
|
+
}
|
|
3688
|
+
ts.forEachChild(node, visit)
|
|
3689
|
+
}
|
|
3690
|
+
visit(sf)
|
|
3691
|
+
return called
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3634
3694
|
function extractDependencies(code: string, ctx: AnalyzerContext): string[] {
|
|
3695
|
+
const called = collectCalledIdentifiers(code)
|
|
3635
3696
|
const deps: string[] = []
|
|
3636
3697
|
|
|
3637
3698
|
// Find signal getter calls: signalName()
|
|
3638
3699
|
for (const signal of ctx.signals) {
|
|
3639
|
-
|
|
3640
|
-
if (pattern.test(code)) {
|
|
3700
|
+
if (called.has(signal.getter)) {
|
|
3641
3701
|
deps.push(signal.getter)
|
|
3642
3702
|
}
|
|
3643
3703
|
}
|
|
3644
3704
|
|
|
3645
3705
|
// Find memo calls: memoName()
|
|
3646
3706
|
for (const memo of ctx.memos) {
|
|
3647
|
-
|
|
3648
|
-
if (pattern.test(code)) {
|
|
3707
|
+
if (called.has(memo.name)) {
|
|
3649
3708
|
deps.push(memo.name)
|
|
3650
3709
|
}
|
|
3651
3710
|
}
|