@barefootjs/jsx 0.25.0 → 0.26.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/dangerous-inner-html.d.ts +52 -24
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.js +248 -110
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +24 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +13 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +31 -10
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
- package/src/__tests__/nested-loop-conditional.test.ts +133 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
- package/src/__tests__/reactive-factory-cross-file.test.ts +252 -1
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +222 -58
- package/src/ir-to-client-js/collect-elements.ts +28 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
- package/src/ir-to-client-js/reactivity.ts +61 -7
- package/src/ir-to-client-js/types.ts +13 -0
- package/src/rich-type-refusal.ts +3 -2
- package/src/to-locale-date-lowering.ts +50 -13
- package/src/types.ts +8 -4
package/src/analyzer.ts
CHANGED
|
@@ -2542,6 +2542,59 @@ export function extractFreeIdentifiersFromNode(node: ts.Node): Set<string> {
|
|
|
2542
2542
|
return ids
|
|
2543
2543
|
}
|
|
2544
2544
|
|
|
2545
|
+
/**
|
|
2546
|
+
* Identifiers referenced in TYPE position anywhere under `node` (#2350) —
|
|
2547
|
+
* the mirror image of `extractFreeIdentifiersFromNode`, which explicitly
|
|
2548
|
+
* skips type nodes since they carry no runtime reference. Walks the whole
|
|
2549
|
+
* tree without stopping at type nodes; every `TypeReferenceNode`'s root
|
|
2550
|
+
* name is collected, which by plain `forEachChild` recursion also reaches
|
|
2551
|
+
* nested references (generic type arguments, array/union/tuple members,
|
|
2552
|
+
* function-type params/returns) without needing to special-case each shape.
|
|
2553
|
+
* Also collects `TypeQueryNode` (`typeof Foo`) root names — `Foo` there is
|
|
2554
|
+
* a VALUE reference in type position, so `moduleCaptureCheck`'s fallback to
|
|
2555
|
+
* `moduleBindings.imported`/`local` (added for the plain-value-import case,
|
|
2556
|
+
* Copilot review, PR #2351) resolves it the same way (Copilot review, PR
|
|
2557
|
+
* #2351).
|
|
2558
|
+
* A function-like node's own type parameters (`<T>`) are tracked and
|
|
2559
|
+
* excluded within its body/signature, same idea as the value walker's
|
|
2560
|
+
* arrow-parameter tracking — otherwise a generic named the same as a
|
|
2561
|
+
* module-scope type (`function useThing<SavedList>(x: SavedList)`) would
|
|
2562
|
+
* be misclassified as a reference to the import (Copilot review, PR
|
|
2563
|
+
* #2351). Doesn't exclude body-local type/interface declarations that
|
|
2564
|
+
* shadow a module-scope name — rare enough here that, per this file's
|
|
2565
|
+
* accepted-limitation policy, a redundant-but-harmless re-provisioned
|
|
2566
|
+
* import is an acceptable outcome (never a silent dangling reference).
|
|
2567
|
+
*/
|
|
2568
|
+
function extractFreeTypeIdentifiersFromNode(node: ts.Node): Set<string> {
|
|
2569
|
+
const ids = new Set<string>()
|
|
2570
|
+
const boundTypeParams = new Set<string>()
|
|
2571
|
+
function rootName(name: ts.EntityName): ts.Identifier {
|
|
2572
|
+
return ts.isQualifiedName(name) ? rootName(name.left) : name
|
|
2573
|
+
}
|
|
2574
|
+
function visit(n: ts.Node): void {
|
|
2575
|
+
if (ts.isTypeReferenceNode(n)) {
|
|
2576
|
+
const name = rootName(n.typeName).text
|
|
2577
|
+
if (!boundTypeParams.has(name)) ids.add(name)
|
|
2578
|
+
// Keep descending — nested references (`Promise<SavedList>`,
|
|
2579
|
+
// `Record<string, SavedList>`) live in this same node's type arguments.
|
|
2580
|
+
}
|
|
2581
|
+
if (ts.isTypeQueryNode(n)) {
|
|
2582
|
+
const name = rootName(n.exprName).text
|
|
2583
|
+
if (!boundTypeParams.has(name)) ids.add(name)
|
|
2584
|
+
}
|
|
2585
|
+
if (ts.isFunctionLike(n) && n.typeParameters && n.typeParameters.length > 0) {
|
|
2586
|
+
const names = n.typeParameters.map((p) => p.name.text)
|
|
2587
|
+
for (const p of names) boundTypeParams.add(p)
|
|
2588
|
+
ts.forEachChild(n, visit)
|
|
2589
|
+
for (const p of names) boundTypeParams.delete(p)
|
|
2590
|
+
return
|
|
2591
|
+
}
|
|
2592
|
+
ts.forEachChild(n, visit)
|
|
2593
|
+
}
|
|
2594
|
+
visit(node)
|
|
2595
|
+
return ids
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2545
2598
|
/**
|
|
2546
2599
|
* Check if a const initializer expression contains JSX at a non-root
|
|
2547
2600
|
* position — ternary with JSX on either side, logical-AND / OR /
|
|
@@ -3965,32 +4018,40 @@ function toComponentRelativeSpecifier(resolvedAbs: string, componentFilePath: st
|
|
|
3965
4018
|
}
|
|
3966
4019
|
|
|
3967
4020
|
/**
|
|
3968
|
-
* localName -> identity of what the entry file's own top-level named
|
|
3969
|
-
* imports bind, for the satisfied-import dedupe check (#2332
|
|
3970
|
-
* component file already imports the exact
|
|
3971
|
-
* re-provision, injecting it again would be a
|
|
3972
|
-
* than a shadow, so that case is skipped
|
|
3973
|
-
* the resolved absolute path for
|
|
3974
|
-
* when probing fails) and the
|
|
4021
|
+
* localName -> identity of what the entry file's own top-level named
|
|
4022
|
+
* imports bind, for the satisfied-import dedupe check (#2332, type-only
|
|
4023
|
+
* imports included #2350): if the component file already imports the exact
|
|
4024
|
+
* binding a factory needs to re-provision, injecting it again would be a
|
|
4025
|
+
* duplicate declaration rather than a shadow, so that case is skipped
|
|
4026
|
+
* instead of injected. `targetKey` is the resolved absolute path for
|
|
4027
|
+
* relative sources (or `unresolved:<source>` when probing fails) and the
|
|
4028
|
+
* raw specifier for bare sources. `isTypeOnly` matters at the call site: an
|
|
4029
|
+
* existing type-only import satisfies a factory's type-only need but NOT a
|
|
4030
|
+
* value need (it has no runtime binding) — treating it as satisfying both
|
|
4031
|
+
* would silently skip re-provisioning a value the inlined body actually
|
|
4032
|
+
* calls, the same dangling-reference failure #2341 BUG-2 already covers.
|
|
3975
4033
|
*/
|
|
3976
4034
|
function buildEntryImportIndex(
|
|
3977
4035
|
sf: ts.SourceFile,
|
|
3978
4036
|
filePath: string
|
|
3979
|
-
): Map<string, { targetKey: string; exportedName: string }> {
|
|
3980
|
-
const index = new Map<string, { targetKey: string; exportedName: string }>()
|
|
4037
|
+
): Map<string, { targetKey: string; exportedName: string; isTypeOnly: boolean }> {
|
|
4038
|
+
const index = new Map<string, { targetKey: string; exportedName: string; isTypeOnly: boolean }>()
|
|
3981
4039
|
for (const stmt of sf.statements) {
|
|
3982
4040
|
if (!ts.isImportDeclaration(stmt)) continue
|
|
3983
4041
|
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
3984
|
-
if (stmt.importClause?.isTypeOnly) continue
|
|
3985
4042
|
const src = stmt.moduleSpecifier.text
|
|
3986
4043
|
const targetKey = src.startsWith('./') || src.startsWith('../')
|
|
3987
4044
|
? (resolveRelativeImportToFile(src, filePath) ?? 'unresolved:' + src)
|
|
3988
4045
|
: src
|
|
4046
|
+
const wholeTypeOnly = stmt.importClause?.isTypeOnly === true
|
|
3989
4047
|
const namedBindings = stmt.importClause?.namedBindings
|
|
3990
4048
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
3991
4049
|
for (const el of namedBindings.elements) {
|
|
3992
|
-
|
|
3993
|
-
|
|
4050
|
+
index.set(el.name.text, {
|
|
4051
|
+
targetKey,
|
|
4052
|
+
exportedName: (el.propertyName ?? el.name).text,
|
|
4053
|
+
isTypeOnly: wholeTypeOnly || el.isTypeOnly,
|
|
4054
|
+
})
|
|
3994
4055
|
}
|
|
3995
4056
|
}
|
|
3996
4057
|
}
|
|
@@ -4034,6 +4095,9 @@ function collectEntryBindingNames(sf: ts.SourceFile): Set<string> {
|
|
|
4034
4095
|
) {
|
|
4035
4096
|
names.add(node.name.text)
|
|
4036
4097
|
}
|
|
4098
|
+
if ((ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name) {
|
|
4099
|
+
names.add(node.name.text)
|
|
4100
|
+
}
|
|
4037
4101
|
if (ts.isFunctionLike(node)) {
|
|
4038
4102
|
for (const p of node.parameters) {
|
|
4039
4103
|
const out: string[] = []
|
|
@@ -4165,6 +4229,10 @@ function prescanImportedReactiveFactories(
|
|
|
4165
4229
|
const entryImportIndex = buildEntryImportIndex(entrySourceFile, filePath)
|
|
4166
4230
|
// localName -> planned injection identity; a later factory requiring the
|
|
4167
4231
|
// same name from a DIFFERENT (targetKey, exportedName) is a collision.
|
|
4232
|
+
// Deliberately isTypeOnly-agnostic: a value need and a type need for the
|
|
4233
|
+
// SAME (targetKey, exportedName) aren't a real collision (the eventual
|
|
4234
|
+
// value import satisfies both) — the final line-generation step below
|
|
4235
|
+
// drops the redundant type-only line rather than declining here.
|
|
4168
4236
|
const plannedInjections = new Map<string, { targetKey: string; exportedName: string }>()
|
|
4169
4237
|
|
|
4170
4238
|
// #2341 BUG-2 — one read+parse per helper file per entry file, memoized
|
|
@@ -4346,7 +4414,9 @@ function prescanImportedReactiveFactories(
|
|
|
4346
4414
|
break
|
|
4347
4415
|
}
|
|
4348
4416
|
// #2332 — re-provision the helper file's own named value imports
|
|
4349
|
-
// that the factory body references, instead of declining
|
|
4417
|
+
// that the factory body references, instead of declining (type-
|
|
4418
|
+
// only refs included #2350 — same treatment, tagged `isTypeOnly`
|
|
4419
|
+
// so the line-generator below emits `import type { ... }`). Each
|
|
4350
4420
|
// ref resolves to a component-relative specifier (or passes
|
|
4351
4421
|
// through unchanged for bare/npm specifiers); a ref already
|
|
4352
4422
|
// satisfied by an identical top-level import in the component
|
|
@@ -4358,7 +4428,11 @@ function prescanImportedReactiveFactories(
|
|
|
4358
4428
|
const required: RequiredFactoryImport[] = []
|
|
4359
4429
|
const pending: Array<[string, { targetKey: string; exportedName: string }]> = []
|
|
4360
4430
|
let declinedEntry: DeclinedReactiveFactory | null = null
|
|
4361
|
-
|
|
4431
|
+
const allRefs = [
|
|
4432
|
+
...capture.importedRefs.map((r) => ({ ...r, isTypeOnly: false })),
|
|
4433
|
+
...capture.importedTypeRefs.map((r) => ({ ...r, isTypeOnly: true })),
|
|
4434
|
+
]
|
|
4435
|
+
for (const ref of allRefs) {
|
|
4362
4436
|
let specifier: string
|
|
4363
4437
|
let targetKey: string
|
|
4364
4438
|
if (ref.source.startsWith('./') || ref.source.startsWith('../')) {
|
|
@@ -4384,8 +4458,13 @@ function prescanImportedReactiveFactories(
|
|
|
4384
4458
|
}
|
|
4385
4459
|
// Already satisfied by an identical top-level import in the
|
|
4386
4460
|
// component file — injecting again would redeclare the binding.
|
|
4461
|
+
// A type-only need is satisfiable by ANY matching import (value
|
|
4462
|
+
// or type — a value import brings its type into scope too); a
|
|
4463
|
+
// value need can only be satisfied by an existing value import,
|
|
4464
|
+
// since a type-only import has no runtime binding (#2350).
|
|
4387
4465
|
const existing = entryImportIndex.get(ref.localName)
|
|
4388
|
-
if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName
|
|
4466
|
+
if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName &&
|
|
4467
|
+
(ref.isTypeOnly || !existing.isTypeOnly)) {
|
|
4389
4468
|
continue
|
|
4390
4469
|
}
|
|
4391
4470
|
const planned = plannedInjections.get(ref.localName)
|
|
@@ -4402,7 +4481,7 @@ function prescanImportedReactiveFactories(
|
|
|
4402
4481
|
break
|
|
4403
4482
|
}
|
|
4404
4483
|
pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }])
|
|
4405
|
-
required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier })
|
|
4484
|
+
required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier, isTypeOnly: ref.isTypeOnly || undefined })
|
|
4406
4485
|
}
|
|
4407
4486
|
if (declinedEntry) {
|
|
4408
4487
|
result.declined.set(spec.local, declinedEntry)
|
|
@@ -4445,8 +4524,17 @@ function prescanImportedReactiveFactories(
|
|
|
4445
4524
|
* came from (`resolveFinalImports` / `detectUsedImports` regex-scan the
|
|
4446
4525
|
* *generated* code, not the consumer's source imports — see #2325 spec C1),
|
|
4447
4526
|
* so an inlined body calling `createSignal` is never a capture even though
|
|
4448
|
-
* the helper file itself imports it.
|
|
4449
|
-
*
|
|
4527
|
+
* the helper file itself imports it.
|
|
4528
|
+
*
|
|
4529
|
+
* `localTypes`/`importedTypes` are the type-position mirror of `local`/
|
|
4530
|
+
* `imported` (#2350): a factory body's return-type annotations, generic
|
|
4531
|
+
* type arguments, and local variable type annotations reference names too,
|
|
4532
|
+
* and those need the exact same capture-or-re-provision treatment — a type
|
|
4533
|
+
* declared directly in the helper file can't be re-imported (BF112,
|
|
4534
|
+
* folded into `local`'s capture handling since the failure mode is
|
|
4535
|
+
* identical), but a type the helper file itself imports (`import type {
|
|
4536
|
+
* X }` or the per-specifier `import { type X }`) can be re-provisioned as
|
|
4537
|
+
* `import type { X } from '<specifier>'` in the component file.
|
|
4450
4538
|
*/
|
|
4451
4539
|
interface HelperModuleBindings {
|
|
4452
4540
|
/** Declared directly in the helper file — unconditional BF112 capture. */
|
|
@@ -4454,10 +4542,18 @@ interface HelperModuleBindings {
|
|
|
4454
4542
|
/** Named value-import specifiers, keyed by helper-file local name —
|
|
4455
4543
|
* re-provisionable into the component file (#2332). */
|
|
4456
4544
|
imported: Map<string, { source: string; exportedName: string }>
|
|
4545
|
+
/** Type/interface declared directly in the helper file — unconditional
|
|
4546
|
+
* BF112 capture, same as `local` (#2350). */
|
|
4547
|
+
localTypes: Set<string>
|
|
4548
|
+
/** The helper file's own type-only named imports, keyed by local name —
|
|
4549
|
+
* re-provisionable as `import type { X } from '<specifier>'` (#2350). */
|
|
4550
|
+
importedTypes: Map<string, { source: string; exportedName: string }>
|
|
4457
4551
|
}
|
|
4458
4552
|
|
|
4459
4553
|
function collectHelperModuleValueBindings(sf: ts.SourceFile): HelperModuleBindings {
|
|
4460
4554
|
const local = new Set<string>()
|
|
4555
|
+
const localTypes = new Set<string>()
|
|
4556
|
+
const importedTypes = new Map<string, { source: string; exportedName: string }>()
|
|
4461
4557
|
const imported = new Map<string, { source: string; exportedName: string }>()
|
|
4462
4558
|
for (const stmt of sf.statements) {
|
|
4463
4559
|
if (ts.isVariableStatement(stmt)) {
|
|
@@ -4475,35 +4571,43 @@ function collectHelperModuleValueBindings(sf: ts.SourceFile): HelperModuleBindin
|
|
|
4475
4571
|
local.add(stmt.name.text)
|
|
4476
4572
|
continue
|
|
4477
4573
|
}
|
|
4574
|
+
if ((ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) && stmt.name) {
|
|
4575
|
+
localTypes.add(stmt.name.text)
|
|
4576
|
+
continue
|
|
4577
|
+
}
|
|
4478
4578
|
if (ts.isImportDeclaration(stmt)) {
|
|
4479
|
-
if (stmt.importClause?.isTypeOnly) continue
|
|
4480
4579
|
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
4481
4580
|
const src = stmt.moduleSpecifier.text
|
|
4482
4581
|
if (src === '@barefootjs/client' || src === '@barefootjs/client/runtime') continue
|
|
4582
|
+
const wholeTypeOnly = stmt.importClause?.isTypeOnly === true
|
|
4483
4583
|
// Default/namespace imports stay hard BF112 (#2332 scope decision):
|
|
4484
|
-
// no single named export to re-provision under one local name.
|
|
4485
|
-
|
|
4584
|
+
// no single named export to re-provision under one local name. A
|
|
4585
|
+
// type-only default/namespace import has the same problem, so it
|
|
4586
|
+
// goes to `localTypes` rather than `importedTypes`.
|
|
4587
|
+
if (stmt.importClause?.name) (wholeTypeOnly ? localTypes : local).add(stmt.importClause.name.text)
|
|
4486
4588
|
const namedBindings = stmt.importClause?.namedBindings
|
|
4487
4589
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
4488
4590
|
for (const el of namedBindings.elements) {
|
|
4489
|
-
|
|
4490
|
-
|
|
4591
|
+
const entry = { source: src, exportedName: (el.propertyName ?? el.name).text }
|
|
4592
|
+
if (wholeTypeOnly || el.isTypeOnly) importedTypes.set(el.name.text, entry)
|
|
4593
|
+
else imported.set(el.name.text, entry)
|
|
4491
4594
|
}
|
|
4492
4595
|
}
|
|
4493
4596
|
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
4494
|
-
local.add(namedBindings.name.text)
|
|
4597
|
+
(wholeTypeOnly ? localTypes : local).add(namedBindings.name.text)
|
|
4495
4598
|
}
|
|
4496
4599
|
}
|
|
4497
4600
|
}
|
|
4498
|
-
return { local, imported }
|
|
4601
|
+
return { local, imported, localTypes, importedTypes }
|
|
4499
4602
|
}
|
|
4500
4603
|
|
|
4501
4604
|
/**
|
|
4502
4605
|
* Categorized free-identifier references of a reactive-factory body into
|
|
4503
|
-
* its own module scope (#2332): `captured` are
|
|
4504
|
-
* (helper-local bindings — the body would dangle
|
|
4505
|
-
*
|
|
4506
|
-
*
|
|
4606
|
+
* its own module scope (#2332, type positions added #2350): `captured` are
|
|
4607
|
+
* unconditional BF112 hits (helper-local bindings — the body would dangle
|
|
4608
|
+
* if inlined verbatim, value or type alike); `importedRefs`/
|
|
4609
|
+
* `importedTypeRefs` are references to the helper file's own named value/
|
|
4610
|
+
* type imports, which the caller may re-provision into the component file
|
|
4507
4611
|
* instead of declining (§3.4 in the #2332 spec).
|
|
4508
4612
|
*/
|
|
4509
4613
|
interface ModuleCaptureResult {
|
|
@@ -4511,23 +4615,27 @@ interface ModuleCaptureResult {
|
|
|
4511
4615
|
captured: string[]
|
|
4512
4616
|
/** Free refs resolving to the helper's own named value imports, sorted by localName. */
|
|
4513
4617
|
importedRefs: Array<{ localName: string; source: string; exportedName: string }>
|
|
4618
|
+
/** Free type-position refs resolving to the helper's own named type imports, sorted by localName. */
|
|
4619
|
+
importedTypeRefs: Array<{ localName: string; source: string; exportedName: string }>
|
|
4514
4620
|
}
|
|
4515
4621
|
|
|
4516
4622
|
/**
|
|
4517
4623
|
* Free identifiers of a reactive-factory body that resolve to bindings at
|
|
4518
|
-
* its own module scope (#2325 §4h / BF112, #2332
|
|
4519
|
-
* body would silently lose once spliced into the
|
|
4520
|
-
* re-provisioned as an import. Returns `captured`
|
|
4521
|
-
*
|
|
4522
|
-
* diagnostic/
|
|
4624
|
+
* its own module scope (#2325 §4h / BF112, #2332, type positions #2350) —
|
|
4625
|
+
* references the inlined body would silently lose once spliced into the
|
|
4626
|
+
* component file, unless re-provisioned as an import. Returns `captured`
|
|
4627
|
+
* (unconditional BF112) and `importedRefs`/`importedTypeRefs`
|
|
4628
|
+
* (re-provisionable) separately, each sorted for stable diagnostic/
|
|
4629
|
+
* injection text.
|
|
4523
4630
|
*
|
|
4524
4631
|
* Known accepted limitation: `extractFreeIdentifiersFromNode` only scope-
|
|
4525
4632
|
* tracks arrow-function parameters, not nested `function` declarations'
|
|
4526
4633
|
* parameters or nested-block declarations — a body-nested binding that
|
|
4527
4634
|
* happens to shadow a helper-module binding could false-positive into
|
|
4528
|
-
* BF112 or `importedRefs`. Acceptable: the failure
|
|
4529
|
-
* loud build error (BF112/BF113) or a
|
|
4530
|
-
* import, never a silent dangling
|
|
4635
|
+
* BF112 or `importedRefs`/`importedTypeRefs`. Acceptable: the failure
|
|
4636
|
+
* direction is always a loud build error (BF112/BF113) or a
|
|
4637
|
+
* redundant-but-harmless injected import, never a silent dangling
|
|
4638
|
+
* reference.
|
|
4531
4639
|
*/
|
|
4532
4640
|
function moduleCaptureCheck(
|
|
4533
4641
|
fn: ts.FunctionDeclaration,
|
|
@@ -4535,16 +4643,24 @@ function moduleCaptureCheck(
|
|
|
4535
4643
|
moduleBindings: HelperModuleBindings,
|
|
4536
4644
|
selfName: string
|
|
4537
4645
|
): ModuleCaptureResult {
|
|
4538
|
-
if (!fn.body) return { captured: [], importedRefs: [] }
|
|
4646
|
+
if (!fn.body) return { captured: [], importedRefs: [], importedTypeRefs: [] }
|
|
4539
4647
|
const free = extractFreeIdentifiersFromNode(fn.body)
|
|
4648
|
+
const freeTypes = extractFreeTypeIdentifiersFromNode(fn.body)
|
|
4540
4649
|
const exclude = new Set<string>(info.params)
|
|
4541
4650
|
for (const b of info.localBindings) exclude.add(b)
|
|
4542
4651
|
for (const r of info.returnTupleIdentifiers) exclude.add(r)
|
|
4543
4652
|
for (const p of REACTIVE_PRIMITIVES) exclude.add(p)
|
|
4544
4653
|
exclude.add(selfName)
|
|
4654
|
+
// extractFreeTypeIdentifiersFromNode only tracks type parameters declared
|
|
4655
|
+
// BY nodes it walks — it never sees `fn` itself (only `fn.body`), so the
|
|
4656
|
+
// factory's own `<T>` list needs excluding here instead (Copilot review,
|
|
4657
|
+
// PR #2351: `function useThing<Item>(...)` shadowing a module-scope
|
|
4658
|
+
// `Item` type import).
|
|
4659
|
+
if (fn.typeParameters) for (const p of fn.typeParameters) exclude.add(p.name.text)
|
|
4545
4660
|
|
|
4546
4661
|
const captured: string[] = []
|
|
4547
4662
|
const importedRefs: ModuleCaptureResult['importedRefs'] = []
|
|
4663
|
+
const importedTypeRefs: ModuleCaptureResult['importedTypeRefs'] = []
|
|
4548
4664
|
for (const id of free) {
|
|
4549
4665
|
if (exclude.has(id)) continue
|
|
4550
4666
|
if (moduleBindings.local.has(id)) {
|
|
@@ -4554,9 +4670,29 @@ function moduleCaptureCheck(
|
|
|
4554
4670
|
const imp = moduleBindings.imported.get(id)
|
|
4555
4671
|
if (imp) importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName })
|
|
4556
4672
|
}
|
|
4673
|
+
for (const id of freeTypes) {
|
|
4674
|
+
if (exclude.has(id)) continue
|
|
4675
|
+
if (free.has(id)) continue // already resolved above — same name, value position wins
|
|
4676
|
+
if (moduleBindings.localTypes.has(id) || moduleBindings.local.has(id)) {
|
|
4677
|
+
captured.push(id)
|
|
4678
|
+
continue
|
|
4679
|
+
}
|
|
4680
|
+
const typeImp = moduleBindings.importedTypes.get(id)
|
|
4681
|
+
if (typeImp) {
|
|
4682
|
+
importedTypeRefs.push({ localName: id, source: typeImp.source, exportedName: typeImp.exportedName })
|
|
4683
|
+
continue
|
|
4684
|
+
}
|
|
4685
|
+
// A name used ONLY in type position can still resolve to the helper's
|
|
4686
|
+
// own VALUE import (e.g. a class referenced purely as `(): Foo`) —
|
|
4687
|
+
// re-provision it as a normal value import, which brings the type into
|
|
4688
|
+
// scope too, instead of missing it entirely (Copilot review, PR #2351).
|
|
4689
|
+
const valueImp = moduleBindings.imported.get(id)
|
|
4690
|
+
if (valueImp) importedRefs.push({ localName: id, source: valueImp.source, exportedName: valueImp.exportedName })
|
|
4691
|
+
}
|
|
4557
4692
|
captured.sort()
|
|
4558
4693
|
importedRefs.sort((a, b) => (a.localName < b.localName ? -1 : 1))
|
|
4559
|
-
|
|
4694
|
+
importedTypeRefs.sort((a, b) => (a.localName < b.localName ? -1 : 1))
|
|
4695
|
+
return { captured, importedRefs, importedTypeRefs }
|
|
4560
4696
|
}
|
|
4561
4697
|
|
|
4562
4698
|
/**
|
|
@@ -5084,33 +5220,61 @@ function rewriteFactoryCallsInSource(
|
|
|
5084
5220
|
if (edits.length === 0) return source
|
|
5085
5221
|
|
|
5086
5222
|
// #2332 — one deduped import statement per specifier for every inlined
|
|
5087
|
-
// cross-file factory's re-provisioned imports
|
|
5088
|
-
//
|
|
5089
|
-
//
|
|
5223
|
+
// cross-file factory's re-provisioned imports (type-only refs #2350, kept
|
|
5224
|
+
// on separate `import type { ... }` lines since a value and a type
|
|
5225
|
+
// import can't share one specifier list). Injected as a zero-width edit
|
|
5226
|
+
// so the ordinary bottom-to-top splice below applies it; the result is
|
|
5227
|
+
// indistinguishable from a hand-written import for every downstream
|
|
5090
5228
|
// consumer (ctx.imports → SSR templateImports AND client
|
|
5091
5229
|
// collectExternalImports both parse this same rewritten string).
|
|
5092
5230
|
const importsBySpecifier = new Map<string, Map<string, string>>() // specifier -> localName -> exportedName
|
|
5231
|
+
const typeImportsBySpecifier = new Map<string, Map<string, string>>() // specifier -> localName -> exportedName
|
|
5093
5232
|
for (const f of inlinedFactories) {
|
|
5094
5233
|
for (const r of f.requiredImports ?? []) {
|
|
5095
|
-
|
|
5096
|
-
|
|
5234
|
+
const bySpecifier = r.isTypeOnly ? typeImportsBySpecifier : importsBySpecifier
|
|
5235
|
+
let names = bySpecifier.get(r.specifier)
|
|
5236
|
+
if (!names) { names = new Map(); bySpecifier.set(r.specifier, names) }
|
|
5097
5237
|
names.set(r.localName, r.exportedName) // same-key duplicates are identical by prescan construction
|
|
5098
5238
|
}
|
|
5099
5239
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
const
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5240
|
+
// Two different factories can independently need the same (specifier,
|
|
5241
|
+
// localName) — one only in value position, one only in type position
|
|
5242
|
+
// (#2350). The value import already brings the type into scope, so drop
|
|
5243
|
+
// the now-redundant type-only line rather than emit both (which TypeScript
|
|
5244
|
+
// would reject as a duplicate identifier).
|
|
5245
|
+
for (const [spec, typeNames] of typeImportsBySpecifier) {
|
|
5246
|
+
const valueNames = importsBySpecifier.get(spec)
|
|
5247
|
+
if (!valueNames) continue
|
|
5248
|
+
for (const local of [...typeNames.keys()]) {
|
|
5249
|
+
if (valueNames.has(local)) typeNames.delete(local)
|
|
5250
|
+
}
|
|
5251
|
+
if (typeNames.size === 0) typeImportsBySpecifier.delete(spec)
|
|
5252
|
+
}
|
|
5253
|
+
if (importsBySpecifier.size > 0 || typeImportsBySpecifier.size > 0) {
|
|
5254
|
+
// One sorted pass over the UNION of specifiers (value + type-only), not
|
|
5255
|
+
// two separately-sorted lists — the latter would leave the value/
|
|
5256
|
+
// type-only halves each internally sorted but not globally sorted
|
|
5257
|
+
// against each other (a type-only import from 'a' could land after a
|
|
5258
|
+
// value import from 'b'), an unstable-looking order across unrelated
|
|
5259
|
+
// refactors (Copilot review, PR #2351). Within each specifier, named-
|
|
5260
|
+
// import entries are sorted by local name too — `importsBySpecifier`/
|
|
5261
|
+
// `inlinedFactories` iterate in incidental AST-traversal/insertion
|
|
5262
|
+
// order (Copilot review, PR #2338).
|
|
5263
|
+
const buildLine = (names: Map<string, string>, keyword: string, spec: string) => {
|
|
5264
|
+
const specifiers = [...names]
|
|
5265
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
5266
|
+
.map(([local, exported]) => (exported === local ? local : `${exported} as ${local}`))
|
|
5267
|
+
return `import ${keyword}{ ${specifiers.join(', ')} } from '${spec}'`
|
|
5268
|
+
}
|
|
5269
|
+
const allSpecifiers = new Set([...importsBySpecifier.keys(), ...typeImportsBySpecifier.keys()])
|
|
5270
|
+
const lines = [...allSpecifiers].sort().flatMap((spec) => {
|
|
5271
|
+
const out: string[] = []
|
|
5272
|
+
const valueNames = importsBySpecifier.get(spec)
|
|
5273
|
+
if (valueNames) out.push(buildLine(valueNames, '', spec))
|
|
5274
|
+
const typeNames = typeImportsBySpecifier.get(spec)
|
|
5275
|
+
if (typeNames) out.push(buildLine(typeNames, 'type ', spec))
|
|
5276
|
+
return out
|
|
5277
|
+
})
|
|
5114
5278
|
const at = factoryImportInsertionOffset(sourceFile)
|
|
5115
5279
|
edits.push({ start: at, end: at, replacement: at === 0 ? lines.join('\n') + '\n' : '\n' + lines.join('\n') })
|
|
5116
5280
|
}
|
|
@@ -1246,8 +1246,13 @@ export function collectLoopChildBindings(
|
|
|
1246
1246
|
const bindings = emptyLoopChildBindings()
|
|
1247
1247
|
for (const child of children) {
|
|
1248
1248
|
bindings.events.push(...collectLoopChildEventsWithNesting(child))
|
|
1249
|
-
|
|
1250
|
-
|
|
1249
|
+
// stopAtReactiveConditionals=true (#2347): this function always also
|
|
1250
|
+
// collects nested reactive conditionals below via
|
|
1251
|
+
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1252
|
+
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1253
|
+
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1254
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true))
|
|
1255
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1251
1256
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1252
1257
|
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
1253
1258
|
}
|
|
@@ -1342,5 +1347,26 @@ function summarizeLoopChildBranch(
|
|
|
1342
1347
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
1343
1348
|
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1344
1349
|
events: collectConditionalBranchEvents(node),
|
|
1350
|
+
// Loop-param-aware — reuses the flat loop-item collectors scoped to just
|
|
1351
|
+
// this branch's subtree. Both already stop descending into any further
|
|
1352
|
+
// nested reactive conditional (own insert()/arm), so calling them here
|
|
1353
|
+
// on the branch root yields exactly this branch's direct bindings
|
|
1354
|
+
// without re-collecting what a nested arm already owns (#2347).
|
|
1355
|
+
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1356
|
+
// Skip when the branch's ENTIRE content is a single bare `expression`
|
|
1357
|
+
// (no wrapping element) — e.g. a hoisted `renderNode={(n) => <Pill/>}`
|
|
1358
|
+
// callback (#1211/#1213). That value is already fully re-evaluated and
|
|
1359
|
+
// spliced via `__bfSlot` whenever `insert()` (re-)mounts this branch;
|
|
1360
|
+
// an *additional* nested createEffect for the same expression re-calls
|
|
1361
|
+
// it and creates a second, independent live element (a JSX-callback
|
|
1362
|
+
// result isn't idempotent to re-invoke the way a plain signal read is),
|
|
1363
|
+
// and the loop-child arm's `$t()`-based anchor lookup — designed for
|
|
1364
|
+
// text nodes — doesn't cleanly displace an already-mounted Element,
|
|
1365
|
+
// so the second instance lands beside the first instead of replacing
|
|
1366
|
+
// it. A text *nested inside* a static wrapper element in the branch
|
|
1367
|
+
// (the element-descent case) is unaffected and still collected below.
|
|
1368
|
+
reactiveTexts: node.type === 'expression'
|
|
1369
|
+
? []
|
|
1370
|
+
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1345
1371
|
}
|
|
1346
1372
|
}
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
LoopChildBranchSummary,
|
|
12
12
|
LoopChildConditional,
|
|
13
13
|
LoopChildEvent,
|
|
14
|
+
LoopChildReactiveAttr,
|
|
15
|
+
LoopChildReactiveText,
|
|
14
16
|
NestedLoop,
|
|
15
17
|
} from '../../types.ts'
|
|
16
18
|
import type {
|
|
@@ -20,9 +22,10 @@ import type {
|
|
|
20
22
|
IRProp,
|
|
21
23
|
LoopParamBinding,
|
|
22
24
|
} from '../../../types.ts'
|
|
23
|
-
import { AttrValueOf } from '../../../types.ts'
|
|
25
|
+
import { AttrValueOf, pickAttrMeta } from '../../../types.ts'
|
|
24
26
|
import { quotePropName, wrapLoopParamAsAccessor, attrValueToString } from '../../utils.ts'
|
|
25
27
|
import { addCondAttrToTemplate, irChildrenToJsExpr } from '../../html-template.ts'
|
|
28
|
+
import type { ReactiveAttrSlot } from './reactive-effects.ts'
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Apply a string-level expression rewriter (loop-param-accessor wrap, prop
|
|
@@ -381,6 +384,56 @@ export function buildLoopChildConditionalsPlan(
|
|
|
381
384
|
return plans
|
|
382
385
|
}
|
|
383
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Group a branch's reactive attrs by child slot (one qsa per slot),
|
|
389
|
+
* pre-wrapping each expression via the supplied loop-param wrap closure.
|
|
390
|
+
* Shared by every arm builder — outer conditional arms and recursively
|
|
391
|
+
* nested ones alike — so an attr binds inside whichever arm directly owns
|
|
392
|
+
* its element, never a stale outer scope (#2347).
|
|
393
|
+
*/
|
|
394
|
+
export function buildArmAttrsPlan(
|
|
395
|
+
attrs: readonly LoopChildReactiveAttr[] | undefined,
|
|
396
|
+
wrap: (expr: string) => string,
|
|
397
|
+
): readonly ReactiveAttrSlot[] {
|
|
398
|
+
if (!attrs || attrs.length === 0) return []
|
|
399
|
+
const bySlot = new Map<string, LoopChildReactiveAttr[]>()
|
|
400
|
+
for (const attr of attrs) {
|
|
401
|
+
let bucket = bySlot.get(attr.childSlotId)
|
|
402
|
+
if (!bucket) {
|
|
403
|
+
bucket = []
|
|
404
|
+
bySlot.set(attr.childSlotId, bucket)
|
|
405
|
+
}
|
|
406
|
+
bucket.push(attr)
|
|
407
|
+
}
|
|
408
|
+
const slots: ReactiveAttrSlot[] = []
|
|
409
|
+
for (const [slotId, slotAttrs] of bySlot) {
|
|
410
|
+
slots.push({
|
|
411
|
+
slotId,
|
|
412
|
+
attrs: slotAttrs.map(attr => ({
|
|
413
|
+
attrName: attr.attrName,
|
|
414
|
+
wrappedExpression: wrap(attr.expression),
|
|
415
|
+
meta: pickAttrMeta(attr),
|
|
416
|
+
})),
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
return slots
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Pre-wrap a branch's reactive text interpolations via the supplied
|
|
424
|
+
* loop-param wrap closure. Shared by every arm builder (#2347).
|
|
425
|
+
*/
|
|
426
|
+
export function buildArmTextsPlan(
|
|
427
|
+
texts: readonly LoopChildReactiveText[] | undefined,
|
|
428
|
+
wrap: (expr: string) => string,
|
|
429
|
+
): readonly import('./loop-child-arm.ts').LoopChildArmText[] {
|
|
430
|
+
if (!texts || texts.length === 0) return []
|
|
431
|
+
return texts.map(text => ({
|
|
432
|
+
slotId: text.slotId,
|
|
433
|
+
wrappedExpression: wrap(text.expression),
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
|
|
384
437
|
interface BuildLoopChildArmArgs {
|
|
385
438
|
branch: LoopChildBranchSummary
|
|
386
439
|
wrap: (expr: string) => string
|
|
@@ -413,6 +466,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
413
466
|
loopParam,
|
|
414
467
|
loopParamBindings,
|
|
415
468
|
}),
|
|
416
|
-
|
|
469
|
+
attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
|
|
470
|
+
texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
|
|
417
471
|
}
|
|
418
472
|
}
|