@barefootjs/jsx 0.24.1 → 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/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +473 -165
- 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 +29 -5
- 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 +833 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +607 -142
- package/src/errors.ts +4 -0
- 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 +30 -5
package/src/analyzer.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
|
-
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, SourceLocation } from './types.ts'
|
|
10
|
+
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, FactoryRenameSite, SourceLocation } from './types.ts'
|
|
11
11
|
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
12
12
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
13
13
|
import { incrementCounter } from './instrumentation.ts'
|
|
@@ -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[] = []
|
|
@@ -4047,18 +4111,56 @@ function collectEntryBindingNames(sf: ts.SourceFile): Set<string> {
|
|
|
4047
4111
|
return names
|
|
4048
4112
|
}
|
|
4049
4113
|
|
|
4114
|
+
// One `export ... from` hop is followed when resolving a re-exported name
|
|
4115
|
+
// (#2341 BUG-2) — a visited-set guards against cycles regardless, so
|
|
4116
|
+
// raising this later is safe without further changes.
|
|
4117
|
+
const MAX_REEXPORT_HOPS = 1
|
|
4118
|
+
|
|
4119
|
+
/** A helper file's export surface, cached per absolute path for the
|
|
4120
|
+
* lifetime of one `prescanImportedReactiveFactories` call (#2341 BUG-2). */
|
|
4121
|
+
interface HelperFileInfo {
|
|
4122
|
+
sf: ts.SourceFile
|
|
4123
|
+
/** Module-scope function declarations exported under their external name
|
|
4124
|
+
* (own `export function`, or a local `export { f as g }`). */
|
|
4125
|
+
exportedFns: Map<string, ts.FunctionDeclaration>
|
|
4126
|
+
/** `export { a as b } from 'src'` re-exports, keyed by the EXTERNAL name
|
|
4127
|
+
* `b` — a barrel file's whole reason for existing (#2341 BUG-2). */
|
|
4128
|
+
reexports: Map<string, { source: string; innerName: string }>
|
|
4129
|
+
/** Any `export * from '...'` in this file — a named lookup that misses
|
|
4130
|
+
* `exportedFns`/`reexports` might still resolve through one of these, so
|
|
4131
|
+
* it can never be classified `'clean'`. (`export * as ns from` is a
|
|
4132
|
+
* `NamespaceExport` clause and is excluded: a named lookup can never
|
|
4133
|
+
* come through it.) */
|
|
4134
|
+
hasStarReexport: boolean
|
|
4135
|
+
moduleBindings: HelperModuleBindings
|
|
4136
|
+
}
|
|
4137
|
+
/** `'clean'` = read, parsed (or gate-skipped), and proven to define no
|
|
4138
|
+
* reactive factory under any name reachable from it. `null` = unreadable. */
|
|
4139
|
+
type LoadedHelper = HelperFileInfo | 'clean' | null
|
|
4140
|
+
type ExportLookup =
|
|
4141
|
+
| { kind: 'fn'; fn: ts.FunctionDeclaration; file: HelperFileInfo; definingPath: string }
|
|
4142
|
+
| { kind: 'clean' } // proven non-reactive under this name → cleanFactoryImports
|
|
4143
|
+
| { kind: 'unknown' } // cannot prove → leave unclassified so the BF110 name heuristic still fires
|
|
4144
|
+
|
|
4050
4145
|
/**
|
|
4051
4146
|
* Cross-file half of the factory prescan (#2325 round 2): resolve factories
|
|
4052
4147
|
* defined in a relative-imported helper file so `const { count } =
|
|
4053
4148
|
* createCounter(0)` inlines the same way whether `createCounter` lives in
|
|
4054
|
-
* this file or in `./hooks`.
|
|
4149
|
+
* this file or in `./hooks`. Follows one `export ... from` hop so a barrel
|
|
4150
|
+
* `index.ts` re-exporting the real helper resolves to the file that
|
|
4151
|
+
* actually DEFINES it (#2341 BUG-2) — every classification (factory /
|
|
4152
|
+
* declined / reactive-shaped / clean) and every downstream anchor (module-
|
|
4153
|
+
* capture check, helper-import re-provisioning, `sourceFilePath`) is keyed
|
|
4154
|
+
* off that defining file, never the barrel. Mutates `result`'s maps in
|
|
4155
|
+
* place.
|
|
4055
4156
|
*
|
|
4056
4157
|
* Perf: gated on a candidate-callee set collected from the ALREADY-parsed
|
|
4057
4158
|
* entry AST (no regex over source text, per CONTRIBUTING.md's "never parse
|
|
4058
4159
|
* imports with regex" rule) — files with no tuple/object-destructured call
|
|
4059
4160
|
* at all skip every filesystem access below. A second cheap gate (does the
|
|
4060
|
-
* helper file's raw text contain any `REACTIVE_PRIMITIVES` substring
|
|
4061
|
-
*
|
|
4161
|
+
* helper file's raw text contain any `REACTIVE_PRIMITIVES` substring, or
|
|
4162
|
+
* any `export ... from` re-export text) skips the AST parse of helper files
|
|
4163
|
+
* that plainly define no factory and re-export nothing; this is a
|
|
4062
4164
|
* skip-gate over content, not an import parse, so it doesn't run afoul of
|
|
4063
4165
|
* that same rule.
|
|
4064
4166
|
*
|
|
@@ -4127,47 +4229,48 @@ function prescanImportedReactiveFactories(
|
|
|
4127
4229
|
const entryImportIndex = buildEntryImportIndex(entrySourceFile, filePath)
|
|
4128
4230
|
// localName -> planned injection identity; a later factory requiring the
|
|
4129
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.
|
|
4130
4236
|
const plannedInjections = new Map<string, { targetKey: string; exportedName: string }>()
|
|
4131
4237
|
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4238
|
+
// #2341 BUG-2 — one read+parse per helper file per entry file, memoized
|
|
4239
|
+
// across every spec/hop that touches it (a barrel is typically visited
|
|
4240
|
+
// once per re-exported name it satisfies).
|
|
4241
|
+
const helperCache = new Map<string, LoadedHelper>()
|
|
4242
|
+
|
|
4243
|
+
function loadHelperFile(abs: string): LoadedHelper {
|
|
4244
|
+
const cached = helperCache.get(abs)
|
|
4245
|
+
if (cached !== undefined) return cached
|
|
4137
4246
|
|
|
4138
4247
|
let content: string
|
|
4139
4248
|
try {
|
|
4140
|
-
content = fs.readFileSync(
|
|
4249
|
+
content = fs.readFileSync(abs, 'utf8')
|
|
4141
4250
|
} catch {
|
|
4142
|
-
|
|
4251
|
+
helperCache.set(abs, null)
|
|
4252
|
+
return null
|
|
4143
4253
|
}
|
|
4144
4254
|
|
|
4145
|
-
const alreadyKnown = (name: string): boolean =>
|
|
4146
|
-
result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name)
|
|
4147
|
-
|
|
4148
4255
|
// Cheap text-level skip-gate (not an import/JS parse — see docstring):
|
|
4149
|
-
// a
|
|
4150
|
-
// define a reactive factory
|
|
4256
|
+
// a file with no reactive-primitive substring AND no re-export gate
|
|
4257
|
+
// text (`export` ... `from`) can neither define a reactive factory nor
|
|
4258
|
+
// re-export one, so skip parsing it entirely. Substring checks only —
|
|
4259
|
+
// false positives merely cause an AST parse whose outcome is still
|
|
4260
|
+
// correct, they never cause a false 'clean'.
|
|
4151
4261
|
const hasAnyPrimitiveText = [...REACTIVE_PRIMITIVES].some(p => content.includes(p))
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
continue
|
|
4262
|
+
const hasReexportText = content.includes('export') && content.includes('from')
|
|
4263
|
+
if (!hasAnyPrimitiveText && !hasReexportText) {
|
|
4264
|
+
helperCache.set(abs, 'clean')
|
|
4265
|
+
return 'clean'
|
|
4157
4266
|
}
|
|
4158
4267
|
|
|
4159
|
-
const
|
|
4160
|
-
resolved + '.prescan',
|
|
4161
|
-
content,
|
|
4162
|
-
ts.ScriptTarget.Latest,
|
|
4163
|
-
true,
|
|
4164
|
-
ts.ScriptKind.TSX
|
|
4165
|
-
)
|
|
4268
|
+
const sf = ts.createSourceFile(abs + '.prescan', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
4166
4269
|
|
|
4167
4270
|
// Map exported name -> module-scope FunctionDeclaration via the AST.
|
|
4168
4271
|
const localFns = new Map<string, ts.FunctionDeclaration>()
|
|
4169
4272
|
const exportedFns = new Map<string, ts.FunctionDeclaration>()
|
|
4170
|
-
for (const stmt of
|
|
4273
|
+
for (const stmt of sf.statements) {
|
|
4171
4274
|
if (ts.isFunctionDeclaration(stmt) && stmt.name && stmt.body) {
|
|
4172
4275
|
localFns.set(stmt.name.text, stmt)
|
|
4173
4276
|
const hasExportModifier = stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
@@ -4177,20 +4280,37 @@ function prescanImportedReactiveFactories(
|
|
|
4177
4280
|
}
|
|
4178
4281
|
}
|
|
4179
4282
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4283
|
+
|
|
4284
|
+
const reexports = new Map<string, { source: string; innerName: string }>()
|
|
4285
|
+
let hasStarReexport = false
|
|
4286
|
+
for (const stmt of sf.statements) {
|
|
4287
|
+
if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue
|
|
4288
|
+
if (!stmt.moduleSpecifier) {
|
|
4289
|
+
// `export { f }` / `export { f as g }` — keyed by the EXTERNAL export name.
|
|
4290
|
+
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
4291
|
+
for (const el of stmt.exportClause.elements) {
|
|
4292
|
+
if (el.isTypeOnly) continue
|
|
4293
|
+
const fn = localFns.get((el.propertyName ?? el.name).text)
|
|
4294
|
+
if (fn) exportedFns.set(el.name.text, fn)
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
continue
|
|
4298
|
+
}
|
|
4299
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
4300
|
+
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
4301
|
+
// `export { a as b } from 'src'` — keyed by the EXTERNAL name `b`.
|
|
4189
4302
|
for (const el of stmt.exportClause.elements) {
|
|
4190
4303
|
if (el.isTypeOnly) continue
|
|
4191
|
-
|
|
4192
|
-
|
|
4304
|
+
reexports.set(el.name.text, {
|
|
4305
|
+
source: stmt.moduleSpecifier.text,
|
|
4306
|
+
innerName: (el.propertyName ?? el.name).text,
|
|
4307
|
+
})
|
|
4193
4308
|
}
|
|
4309
|
+
} else if (!stmt.exportClause) {
|
|
4310
|
+
// `export * from 'src'`. (`export * as ns from` carries a
|
|
4311
|
+
// NamespaceExport clause here, not `undefined` — excluded on
|
|
4312
|
+
// purpose: a named lookup can never come through it.)
|
|
4313
|
+
hasStarReexport = true
|
|
4194
4314
|
}
|
|
4195
4315
|
}
|
|
4196
4316
|
|
|
@@ -4198,17 +4318,77 @@ function prescanImportedReactiveFactories(
|
|
|
4198
4318
|
// body must not reference any of these (#2325 §4h / BF112), since
|
|
4199
4319
|
// inlining moves the body into the component file where they don't
|
|
4200
4320
|
// exist.
|
|
4201
|
-
const moduleBindings = collectHelperModuleValueBindings(
|
|
4321
|
+
const moduleBindings = collectHelperModuleValueBindings(sf)
|
|
4322
|
+
|
|
4323
|
+
const info: HelperFileInfo = { sf, exportedFns, reexports, hasStarReexport, moduleBindings }
|
|
4324
|
+
helperCache.set(abs, info)
|
|
4325
|
+
return info
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
/**
|
|
4329
|
+
* Resolve `exportedName` from the file at `abs`, following one
|
|
4330
|
+
* `export ... from` hop through a barrel re-export (#2341 BUG-2).
|
|
4331
|
+
* `visited` guards against self/indirect barrel cycles.
|
|
4332
|
+
*/
|
|
4333
|
+
function lookupExportedFactory(
|
|
4334
|
+
abs: string,
|
|
4335
|
+
exportedName: string,
|
|
4336
|
+
visited: Set<string>,
|
|
4337
|
+
hopsLeft: number
|
|
4338
|
+
): ExportLookup {
|
|
4339
|
+
if (visited.has(abs)) return { kind: 'unknown' }
|
|
4340
|
+
visited.add(abs)
|
|
4341
|
+
|
|
4342
|
+
const file = loadHelperFile(abs)
|
|
4343
|
+
if (file === null) return { kind: 'unknown' }
|
|
4344
|
+
if (file === 'clean') return { kind: 'clean' }
|
|
4345
|
+
|
|
4346
|
+
const fn = file.exportedFns.get(exportedName)
|
|
4347
|
+
if (fn) return { kind: 'fn', fn, file, definingPath: abs }
|
|
4348
|
+
|
|
4349
|
+
const re = file.reexports.get(exportedName)
|
|
4350
|
+
if (re) {
|
|
4351
|
+
if (hopsLeft <= 0) return { kind: 'unknown' }
|
|
4352
|
+
// Non-relative re-export specifiers (`export { x } from 'pkg'`)
|
|
4353
|
+
// resolve through bundler/tsconfig-paths configuration this layer
|
|
4354
|
+
// doesn't consume — same restriction as direct imports.
|
|
4355
|
+
if (!re.source.startsWith('./') && !re.source.startsWith('../')) return { kind: 'unknown' }
|
|
4356
|
+
const target = resolveRelativeImportToFile(re.source, abs)
|
|
4357
|
+
if (!target) return { kind: 'unknown' } // unresolvable re-export target: never mark clean
|
|
4358
|
+
return lookupExportedFactory(target, re.innerName, visited, hopsLeft - 1)
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
// `export * from` might still reach this name through a file this
|
|
4362
|
+
// layer doesn't enumerate — never clean. Otherwise the file was fully
|
|
4363
|
+
// inspected and genuinely doesn't define or re-export this name.
|
|
4364
|
+
if (file.hasStarReexport) return { kind: 'unknown' }
|
|
4365
|
+
return { kind: 'clean' }
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
for (const { src, specs } of importsToCheck) {
|
|
4369
|
+
const resolved = resolveRelativeImportToFile(src, filePath)
|
|
4370
|
+
// Unresolvable — left alone here; the name-heuristic BF110 branch in
|
|
4371
|
+
// validateReactiveFactoryCalls handles it at validation time.
|
|
4372
|
+
if (!resolved) continue
|
|
4373
|
+
|
|
4374
|
+
const alreadyKnown = (name: string): boolean =>
|
|
4375
|
+
result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name)
|
|
4202
4376
|
|
|
4203
4377
|
for (const spec of specs) {
|
|
4204
4378
|
if (alreadyKnown(spec.local)) continue
|
|
4205
4379
|
|
|
4206
|
-
const
|
|
4207
|
-
if (
|
|
4380
|
+
const found = lookupExportedFactory(resolved, spec.exported, new Set<string>(), MAX_REEXPORT_HOPS)
|
|
4381
|
+
if (found.kind === 'clean') {
|
|
4208
4382
|
result.cleanFactoryImports.add(spec.local)
|
|
4209
4383
|
continue
|
|
4210
4384
|
}
|
|
4211
|
-
|
|
4385
|
+
if (found.kind === 'unknown') continue
|
|
4386
|
+
|
|
4387
|
+
const { fn, file, definingPath } = found
|
|
4388
|
+
const helperSf = file.sf
|
|
4389
|
+
const moduleBindings = file.moduleBindings
|
|
4390
|
+
|
|
4391
|
+
const det = detectReactiveFactory(fn, helperSf, definingPath)
|
|
4212
4392
|
if (!det) {
|
|
4213
4393
|
result.cleanFactoryImports.add(spec.local)
|
|
4214
4394
|
continue
|
|
@@ -4221,6 +4401,9 @@ function prescanImportedReactiveFactories(
|
|
|
4221
4401
|
result.declined.set(spec.local, det.declined)
|
|
4222
4402
|
break
|
|
4223
4403
|
case 'factory': {
|
|
4404
|
+
// Module-capture check is anchored to the DEFINING file's own
|
|
4405
|
+
// module bindings, not the barrel's (#2341 BUG-2) — the barrel
|
|
4406
|
+
// itself contributes no bindings the inlined body could reference.
|
|
4224
4407
|
const capture = moduleCaptureCheck(fn, det.info, moduleBindings, fn.name!.text)
|
|
4225
4408
|
if (capture.captured.length > 0) {
|
|
4226
4409
|
result.declined.set(spec.local, {
|
|
@@ -4231,7 +4414,9 @@ function prescanImportedReactiveFactories(
|
|
|
4231
4414
|
break
|
|
4232
4415
|
}
|
|
4233
4416
|
// #2332 — re-provision the helper file's own named value imports
|
|
4234
|
-
// 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
|
|
4235
4420
|
// ref resolves to a component-relative specifier (or passes
|
|
4236
4421
|
// through unchanged for bare/npm specifiers); a ref already
|
|
4237
4422
|
// satisfied by an identical top-level import in the component
|
|
@@ -4243,14 +4428,20 @@ function prescanImportedReactiveFactories(
|
|
|
4243
4428
|
const required: RequiredFactoryImport[] = []
|
|
4244
4429
|
const pending: Array<[string, { targetKey: string; exportedName: string }]> = []
|
|
4245
4430
|
let declinedEntry: DeclinedReactiveFactory | null = null
|
|
4246
|
-
|
|
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) {
|
|
4247
4436
|
let specifier: string
|
|
4248
4437
|
let targetKey: string
|
|
4249
4438
|
if (ref.source.startsWith('./') || ref.source.startsWith('../')) {
|
|
4250
|
-
// Resolve from the
|
|
4251
|
-
//
|
|
4439
|
+
// Resolve from the DEFINING file's directory (#2341 BUG-2 —
|
|
4440
|
+
// `definingPath` is the file that actually declares this
|
|
4441
|
+
// import, which may differ from the barrel that was
|
|
4442
|
+
// imported). Unresolvable → same posture as a local
|
|
4252
4443
|
// capture: nothing importable to re-provision (BF112).
|
|
4253
|
-
const abs = resolveRelativeImportToFile(ref.source,
|
|
4444
|
+
const abs = resolveRelativeImportToFile(ref.source, definingPath)
|
|
4254
4445
|
if (!abs) {
|
|
4255
4446
|
declinedEntry = {
|
|
4256
4447
|
code: 'BF112',
|
|
@@ -4267,8 +4458,13 @@ function prescanImportedReactiveFactories(
|
|
|
4267
4458
|
}
|
|
4268
4459
|
// Already satisfied by an identical top-level import in the
|
|
4269
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).
|
|
4270
4465
|
const existing = entryImportIndex.get(ref.localName)
|
|
4271
|
-
if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName
|
|
4466
|
+
if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName &&
|
|
4467
|
+
(ref.isTypeOnly || !existing.isTypeOnly)) {
|
|
4272
4468
|
continue
|
|
4273
4469
|
}
|
|
4274
4470
|
const planned = plannedInjections.get(ref.localName)
|
|
@@ -4285,14 +4481,17 @@ function prescanImportedReactiveFactories(
|
|
|
4285
4481
|
break
|
|
4286
4482
|
}
|
|
4287
4483
|
pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }])
|
|
4288
|
-
required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier })
|
|
4484
|
+
required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier, isTypeOnly: ref.isTypeOnly || undefined })
|
|
4289
4485
|
}
|
|
4290
4486
|
if (declinedEntry) {
|
|
4291
4487
|
result.declined.set(spec.local, declinedEntry)
|
|
4292
4488
|
break
|
|
4293
4489
|
}
|
|
4294
4490
|
for (const [name, id] of pending) plannedInjections.set(name, id)
|
|
4295
|
-
|
|
4491
|
+
// #2341 BUG-2 — anchored to the file that actually defines the
|
|
4492
|
+
// factory, not the (possibly barrel) import path the component
|
|
4493
|
+
// used to reach it.
|
|
4494
|
+
det.info.sourceFilePath = definingPath
|
|
4296
4495
|
if (required.length > 0) det.info.requiredImports = required
|
|
4297
4496
|
result.factories.set(spec.local, det.info)
|
|
4298
4497
|
break
|
|
@@ -4325,8 +4524,17 @@ function prescanImportedReactiveFactories(
|
|
|
4325
4524
|
* came from (`resolveFinalImports` / `detectUsedImports` regex-scan the
|
|
4326
4525
|
* *generated* code, not the consumer's source imports — see #2325 spec C1),
|
|
4327
4526
|
* so an inlined body calling `createSignal` is never a capture even though
|
|
4328
|
-
* the helper file itself imports it.
|
|
4329
|
-
*
|
|
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.
|
|
4330
4538
|
*/
|
|
4331
4539
|
interface HelperModuleBindings {
|
|
4332
4540
|
/** Declared directly in the helper file — unconditional BF112 capture. */
|
|
@@ -4334,10 +4542,18 @@ interface HelperModuleBindings {
|
|
|
4334
4542
|
/** Named value-import specifiers, keyed by helper-file local name —
|
|
4335
4543
|
* re-provisionable into the component file (#2332). */
|
|
4336
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 }>
|
|
4337
4551
|
}
|
|
4338
4552
|
|
|
4339
4553
|
function collectHelperModuleValueBindings(sf: ts.SourceFile): HelperModuleBindings {
|
|
4340
4554
|
const local = new Set<string>()
|
|
4555
|
+
const localTypes = new Set<string>()
|
|
4556
|
+
const importedTypes = new Map<string, { source: string; exportedName: string }>()
|
|
4341
4557
|
const imported = new Map<string, { source: string; exportedName: string }>()
|
|
4342
4558
|
for (const stmt of sf.statements) {
|
|
4343
4559
|
if (ts.isVariableStatement(stmt)) {
|
|
@@ -4355,35 +4571,43 @@ function collectHelperModuleValueBindings(sf: ts.SourceFile): HelperModuleBindin
|
|
|
4355
4571
|
local.add(stmt.name.text)
|
|
4356
4572
|
continue
|
|
4357
4573
|
}
|
|
4574
|
+
if ((ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) && stmt.name) {
|
|
4575
|
+
localTypes.add(stmt.name.text)
|
|
4576
|
+
continue
|
|
4577
|
+
}
|
|
4358
4578
|
if (ts.isImportDeclaration(stmt)) {
|
|
4359
|
-
if (stmt.importClause?.isTypeOnly) continue
|
|
4360
4579
|
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
4361
4580
|
const src = stmt.moduleSpecifier.text
|
|
4362
4581
|
if (src === '@barefootjs/client' || src === '@barefootjs/client/runtime') continue
|
|
4582
|
+
const wholeTypeOnly = stmt.importClause?.isTypeOnly === true
|
|
4363
4583
|
// Default/namespace imports stay hard BF112 (#2332 scope decision):
|
|
4364
|
-
// no single named export to re-provision under one local name.
|
|
4365
|
-
|
|
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)
|
|
4366
4588
|
const namedBindings = stmt.importClause?.namedBindings
|
|
4367
4589
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
4368
4590
|
for (const el of namedBindings.elements) {
|
|
4369
|
-
|
|
4370
|
-
|
|
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)
|
|
4371
4594
|
}
|
|
4372
4595
|
}
|
|
4373
4596
|
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
4374
|
-
local.add(namedBindings.name.text)
|
|
4597
|
+
(wholeTypeOnly ? localTypes : local).add(namedBindings.name.text)
|
|
4375
4598
|
}
|
|
4376
4599
|
}
|
|
4377
4600
|
}
|
|
4378
|
-
return { local, imported }
|
|
4601
|
+
return { local, imported, localTypes, importedTypes }
|
|
4379
4602
|
}
|
|
4380
4603
|
|
|
4381
4604
|
/**
|
|
4382
4605
|
* Categorized free-identifier references of a reactive-factory body into
|
|
4383
|
-
* its own module scope (#2332): `captured` are
|
|
4384
|
-
* (helper-local bindings — the body would dangle
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
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
|
|
4387
4611
|
* instead of declining (§3.4 in the #2332 spec).
|
|
4388
4612
|
*/
|
|
4389
4613
|
interface ModuleCaptureResult {
|
|
@@ -4391,23 +4615,27 @@ interface ModuleCaptureResult {
|
|
|
4391
4615
|
captured: string[]
|
|
4392
4616
|
/** Free refs resolving to the helper's own named value imports, sorted by localName. */
|
|
4393
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 }>
|
|
4394
4620
|
}
|
|
4395
4621
|
|
|
4396
4622
|
/**
|
|
4397
4623
|
* Free identifiers of a reactive-factory body that resolve to bindings at
|
|
4398
|
-
* its own module scope (#2325 §4h / BF112, #2332
|
|
4399
|
-
* body would silently lose once spliced into the
|
|
4400
|
-
* re-provisioned as an import. Returns `captured`
|
|
4401
|
-
*
|
|
4402
|
-
* 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.
|
|
4403
4630
|
*
|
|
4404
4631
|
* Known accepted limitation: `extractFreeIdentifiersFromNode` only scope-
|
|
4405
4632
|
* tracks arrow-function parameters, not nested `function` declarations'
|
|
4406
4633
|
* parameters or nested-block declarations — a body-nested binding that
|
|
4407
4634
|
* happens to shadow a helper-module binding could false-positive into
|
|
4408
|
-
* BF112 or `importedRefs`. Acceptable: the failure
|
|
4409
|
-
* loud build error (BF112/BF113) or a
|
|
4410
|
-
* 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.
|
|
4411
4639
|
*/
|
|
4412
4640
|
function moduleCaptureCheck(
|
|
4413
4641
|
fn: ts.FunctionDeclaration,
|
|
@@ -4415,16 +4643,24 @@ function moduleCaptureCheck(
|
|
|
4415
4643
|
moduleBindings: HelperModuleBindings,
|
|
4416
4644
|
selfName: string
|
|
4417
4645
|
): ModuleCaptureResult {
|
|
4418
|
-
if (!fn.body) return { captured: [], importedRefs: [] }
|
|
4646
|
+
if (!fn.body) return { captured: [], importedRefs: [], importedTypeRefs: [] }
|
|
4419
4647
|
const free = extractFreeIdentifiersFromNode(fn.body)
|
|
4648
|
+
const freeTypes = extractFreeTypeIdentifiersFromNode(fn.body)
|
|
4420
4649
|
const exclude = new Set<string>(info.params)
|
|
4421
4650
|
for (const b of info.localBindings) exclude.add(b)
|
|
4422
4651
|
for (const r of info.returnTupleIdentifiers) exclude.add(r)
|
|
4423
4652
|
for (const p of REACTIVE_PRIMITIVES) exclude.add(p)
|
|
4424
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)
|
|
4425
4660
|
|
|
4426
4661
|
const captured: string[] = []
|
|
4427
4662
|
const importedRefs: ModuleCaptureResult['importedRefs'] = []
|
|
4663
|
+
const importedTypeRefs: ModuleCaptureResult['importedTypeRefs'] = []
|
|
4428
4664
|
for (const id of free) {
|
|
4429
4665
|
if (exclude.has(id)) continue
|
|
4430
4666
|
if (moduleBindings.local.has(id)) {
|
|
@@ -4434,9 +4670,29 @@ function moduleCaptureCheck(
|
|
|
4434
4670
|
const imp = moduleBindings.imported.get(id)
|
|
4435
4671
|
if (imp) importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName })
|
|
4436
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
|
+
}
|
|
4437
4692
|
captured.sort()
|
|
4438
4693
|
importedRefs.sort((a, b) => (a.localName < b.localName ? -1 : 1))
|
|
4439
|
-
|
|
4694
|
+
importedTypeRefs.sort((a, b) => (a.localName < b.localName ? -1 : 1))
|
|
4695
|
+
return { captured, importedRefs, importedTypeRefs }
|
|
4440
4696
|
}
|
|
4441
4697
|
|
|
4442
4698
|
/**
|
|
@@ -4478,6 +4734,27 @@ function detectReactiveFactory(
|
|
|
4478
4734
|
|
|
4479
4735
|
const loc = getSourceLocation(node, sourceFile, filePath)
|
|
4480
4736
|
|
|
4737
|
+
// Count `return`s ANYWHERE in the body, stopping at nested function-like
|
|
4738
|
+
// boundaries (ts.isFunctionLike — functions, arrows, methods, accessors,
|
|
4739
|
+
// and constructors; broader than isMultiReturnJsxFunctionBody's #932
|
|
4740
|
+
// three-kind check, which only needs to catch JSX-returning callbacks and
|
|
4741
|
+
// was never exercised against class/object methods) — a return inside a
|
|
4742
|
+
// nested callback OR a class/object method declared in the factory body
|
|
4743
|
+
// belongs to that inner scope, not this factory (Copilot review, PR
|
|
4744
|
+
// #2342: the narrower check would misclassify a factory with, e.g., a
|
|
4745
|
+
// helper class whose method contains a `return` as having multiple
|
|
4746
|
+
// returns, declining it unnecessarily). A return nested in if/try/loop
|
|
4747
|
+
// blocks DOES count, so guard-clause factories are declassified instead
|
|
4748
|
+
// of splicing an early `return` into the component's init function
|
|
4749
|
+
// (#2341 BUG-3).
|
|
4750
|
+
let totalReturnCount = 0
|
|
4751
|
+
function countReturns(n: ts.Node): void {
|
|
4752
|
+
if (ts.isFunctionLike(n)) return
|
|
4753
|
+
if (ts.isReturnStatement(n)) { totalReturnCount++; return }
|
|
4754
|
+
ts.forEachChild(n, countReturns)
|
|
4755
|
+
}
|
|
4756
|
+
ts.forEachChild(node.body, countReturns)
|
|
4757
|
+
|
|
4481
4758
|
// Require exactly one top-level `return`, whose argument (after unwrapping
|
|
4482
4759
|
// parens / `as const` / type-assertion) is a tuple (array literal) or a
|
|
4483
4760
|
// shorthand-object literal.
|
|
@@ -4494,7 +4771,12 @@ function detectReactiveFactory(
|
|
|
4494
4771
|
if (ts.isTypeAssertionExpression(expr)) expr = expr.expression
|
|
4495
4772
|
returnExpr = expr
|
|
4496
4773
|
}
|
|
4497
|
-
|
|
4774
|
+
// `totalReturnCount === 1 && returnCount === 1` jointly guarantee the
|
|
4775
|
+
// single return is a direct child of `node.body` (top-level returns are a
|
|
4776
|
+
// subset of total) — so a guard-clause / try-catch / loop return
|
|
4777
|
+
// declassifies the factory instead of silently producing an inert
|
|
4778
|
+
// component (#2341 BUG-3).
|
|
4779
|
+
if (totalReturnCount !== 1 || returnCount !== 1 || !returnExpr) return { kind: 'reactive-shaped' }
|
|
4498
4780
|
|
|
4499
4781
|
const returnTupleIdentifiers: string[] = []
|
|
4500
4782
|
let returnKind: 'tuple' | 'object'
|
|
@@ -4530,6 +4812,22 @@ function detectReactiveFactory(
|
|
|
4530
4812
|
return { kind: 'reactive-shaped' }
|
|
4531
4813
|
}
|
|
4532
4814
|
|
|
4815
|
+
// Collect parameter names first — the rename-site walk below needs them
|
|
4816
|
+
// as part of `relevantNames` and to detect param-shadowing declarations
|
|
4817
|
+
// (BF114). Hoisted above local-binding/body-serialization collection
|
|
4818
|
+
// (#2341 BUG-1); it has no dependency on either.
|
|
4819
|
+
const params: string[] = []
|
|
4820
|
+
for (const p of node.parameters) {
|
|
4821
|
+
if (ts.isIdentifier(p.name)) {
|
|
4822
|
+
params.push(p.name.text)
|
|
4823
|
+
continue
|
|
4824
|
+
}
|
|
4825
|
+
// Destructured params are uncommon for this helper shape and out of
|
|
4826
|
+
// initial scope; the factory still wraps a reactive primitive, so
|
|
4827
|
+
// classify it as reactive-shaped rather than silently ignoring it.
|
|
4828
|
+
return { kind: 'reactive-shaped' }
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4533
4831
|
// Collect local bindings in the factory body for identifier hygiene at
|
|
4534
4832
|
// inlining time. Only direct-child declarations of the block are
|
|
4535
4833
|
// considered — good enough for the typical helper shape.
|
|
@@ -4544,24 +4842,146 @@ function detectReactiveFactory(
|
|
|
4544
4842
|
}
|
|
4545
4843
|
}
|
|
4546
4844
|
|
|
4845
|
+
// Names that call-site inlining may rename: params (substituted with
|
|
4846
|
+
// arbitrary argument expressions), local bindings (suffix-renamed for
|
|
4847
|
+
// per-call-site hygiene), and return identifiers (renamed to the
|
|
4848
|
+
// caller's destructure names). Every other identifier in the body is
|
|
4849
|
+
// left untouched — the #2341 BUG-1 fix for the old whole-body regex
|
|
4850
|
+
// renames, which corrupted string/template literals, property keys,
|
|
4851
|
+
// and `.prop` tails that merely happened to share a relevant name.
|
|
4852
|
+
const relevantNames = new Set<string>([...params, ...localBindings, ...returnTupleIdentifiers])
|
|
4853
|
+
|
|
4854
|
+
const renameSites: FactoryRenameSite[] = []
|
|
4855
|
+
let shadowedParam: string | null = null
|
|
4856
|
+
|
|
4857
|
+
/**
|
|
4858
|
+
* Recursive AST walk collecting rename sites for one kept statement.
|
|
4859
|
+
* `toBodyOffset` converts a position in `sourceFile` (this statement's
|
|
4860
|
+
* original source) to an offset in the final joined `bodyStatements`
|
|
4861
|
+
* string — see the join loop below for why this must stay a pure
|
|
4862
|
+
* function of `stmtStart`/`base`.
|
|
4863
|
+
*/
|
|
4864
|
+
function collectRenameSites(root: ts.Node, toBodyOffset: (pos: number) => number): void {
|
|
4865
|
+
function push(id: ts.Identifier, form: 'plain' | 'shorthand'): void {
|
|
4866
|
+
renameSites.push({
|
|
4867
|
+
name: id.text,
|
|
4868
|
+
start: toBodyOffset(id.getStart(sourceFile)),
|
|
4869
|
+
end: toBodyOffset(id.getEnd()),
|
|
4870
|
+
form,
|
|
4871
|
+
})
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
function classify(id: ts.Identifier): void {
|
|
4875
|
+
if (!relevantNames.has(id.text)) return
|
|
4876
|
+
const p = id.parent
|
|
4877
|
+
// Pure-key / non-reference positions — never rename:
|
|
4878
|
+
if (ts.isPropertyAccessExpression(p) && p.name === id) return // obj.name tail (incl. ?. chains)
|
|
4879
|
+
if (ts.isPropertyAssignment(p) && p.name === id) return // { name: v } key
|
|
4880
|
+
if (ts.isBindingElement(p) && p.propertyName === id) return // { name: local } pattern key
|
|
4881
|
+
if ((ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) ||
|
|
4882
|
+
ts.isSetAccessorDeclaration(p) || ts.isPropertyDeclaration(p) ||
|
|
4883
|
+
ts.isEnumMember(p)) && p.name === id) return // member keys
|
|
4884
|
+
if (ts.isJsxAttribute(p) && p.name === id) return // JSX attr name
|
|
4885
|
+
if ((ts.isLabeledStatement(p) && p.label === id) ||
|
|
4886
|
+
((ts.isBreakStatement(p) || ts.isContinueStatement(p)) && p.label === id)) return
|
|
4887
|
+
if ((ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p) || ts.isJsxClosingElement(p)) &&
|
|
4888
|
+
p.tagName === id && /^[a-z]/.test(id.text)) return // intrinsic tag <div>
|
|
4889
|
+
|
|
4890
|
+
// Shorthand dual-role positions → expansion form (renaming must
|
|
4891
|
+
// preserve the implied key): `{ name }` object literal / pattern.
|
|
4892
|
+
if (ts.isShorthandPropertyAssignment(p) && p.name === id) { push(id, 'shorthand'); return }
|
|
4893
|
+
if (ts.isBindingElement(p) && p.name === id && !p.propertyName &&
|
|
4894
|
+
ts.isObjectBindingPattern(p.parent)) {
|
|
4895
|
+
// ArrayBindingPattern trap: a tuple destructure element
|
|
4896
|
+
// (`const [a, b] = ...`) ALSO has propertyName === undefined —
|
|
4897
|
+
// the ts.isObjectBindingPattern(p.parent) check above is what
|
|
4898
|
+
// keeps tuple elements out of the shorthand-expansion path.
|
|
4899
|
+
if (params.includes(id.text)) shadowedParam = id.text // decl of a param name → BF114
|
|
4900
|
+
push(id, 'shorthand')
|
|
4901
|
+
return
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
// Declaration-name positions → plain rename, but a param name
|
|
4905
|
+
// re-declared here is a shadow: params are substituted with
|
|
4906
|
+
// arbitrary argument expressions, so a shadowing declaration would
|
|
4907
|
+
// receive an invalid left-hand side (BF114 declines instead).
|
|
4908
|
+
const isDecl =
|
|
4909
|
+
(ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p) ||
|
|
4910
|
+
ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) ||
|
|
4911
|
+
ts.isClassDeclaration(p) || ts.isClassExpression(p)) && p.name === id
|
|
4912
|
+
if (isDecl && params.includes(id.text)) shadowedParam = id.text
|
|
4913
|
+
|
|
4914
|
+
push(id, 'plain') // genuine value reference or decl
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
function visit(n: ts.Node): void {
|
|
4918
|
+
// Never descend into type-land: annotations are stripped downstream,
|
|
4919
|
+
// and splicing an argument EXPRESSION into a type position would be
|
|
4920
|
+
// invalid JS.
|
|
4921
|
+
if (ts.isTypeNode(n) || ts.isTypeParameterDeclaration(n) ||
|
|
4922
|
+
ts.isTypeAliasDeclaration(n) || ts.isInterfaceDeclaration(n)) return
|
|
4923
|
+
if (ts.isIdentifier(n)) { classify(n); return } // identifiers have no relevant children
|
|
4924
|
+
ts.forEachChild(n, visit)
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
visit(root)
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4547
4930
|
// Serialize the body without the outer braces and without the return
|
|
4548
4931
|
// statement — the return tuple/object is dissolved into caller-named
|
|
4549
|
-
// identifiers.
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4932
|
+
// identifiers. `renameSites` is collected in this SAME loop so its
|
|
4933
|
+
// offsets (relative to the joined `bodyStatements` string) can never
|
|
4934
|
+
// drift from the join logic — this invariant is the single most fragile
|
|
4935
|
+
// part of the rename mechanism (#2341 BUG-1): site collection and
|
|
4936
|
+
// `bodyStatements` construction must use the identical statement filter,
|
|
4937
|
+
// identical `getText` slices, and identical '\n' join.
|
|
4938
|
+
const keptStatements = node.body.statements.filter(s => !ts.isReturnStatement(s))
|
|
4939
|
+
const pieces: string[] = []
|
|
4940
|
+
let base = 0
|
|
4941
|
+
for (const stmt of keptStatements) {
|
|
4942
|
+
const text = stmt.getText(sourceFile)
|
|
4943
|
+
const stmtStart = stmt.getStart(sourceFile)
|
|
4944
|
+
collectRenameSites(stmt, (pos) => pos - stmtStart + base)
|
|
4945
|
+
pieces.push(text)
|
|
4946
|
+
base += text.length + 1 // +1 for the '\n' join — MUST match the join below
|
|
4947
|
+
}
|
|
4948
|
+
const bodyStatements = pieces.join('\n')
|
|
4949
|
+
|
|
4950
|
+
if (shadowedParam !== null) {
|
|
4951
|
+
return {
|
|
4952
|
+
kind: 'declined',
|
|
4953
|
+
declined: {
|
|
4954
|
+
code: 'BF114',
|
|
4955
|
+
detail: `parameter '${shadowedParam}' of '${node.name.text}' is shadowed by a nested declaration inside the factory body`,
|
|
4956
|
+
loc,
|
|
4957
|
+
},
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4554
4960
|
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4961
|
+
// Dev invariant: every collected site's [start, end) range must slice
|
|
4962
|
+
// out exactly the identifier text it was collected for, or the
|
|
4963
|
+
// bottom-to-top splice in inlineFactoryCallAtSite would corrupt
|
|
4964
|
+
// unrelated text. Cheap (bounded by body size) — kept as a permanent
|
|
4965
|
+
// guard against the offset math drifting under a future edit. Declines
|
|
4966
|
+
// rather than throwing (Copilot review, PR #2342): compileJSX does not
|
|
4967
|
+
// catch analyzer errors, so a thrown exception here would crash the
|
|
4968
|
+
// whole compilation instead of failing one factory loudly-but-
|
|
4969
|
+
// gracefully as a diagnostic — this should never trigger in practice,
|
|
4970
|
+
// but the failure mode if it ever does must stay "loud decline," not
|
|
4971
|
+
// "hard crash," matching this feature's whole design philosophy.
|
|
4972
|
+
for (const site of renameSites) {
|
|
4973
|
+
if (bodyStatements.slice(site.start, site.end) !== site.name) {
|
|
4974
|
+
return {
|
|
4975
|
+
kind: 'declined',
|
|
4976
|
+
declined: {
|
|
4977
|
+
code: 'BF111',
|
|
4978
|
+
detail:
|
|
4979
|
+
`internal rename-site offset mismatch for '${site.name}' — this is a compiler bug, ` +
|
|
4980
|
+
`please report it`,
|
|
4981
|
+
loc,
|
|
4982
|
+
},
|
|
4983
|
+
}
|
|
4560
4984
|
}
|
|
4561
|
-
// Destructured params are uncommon for this helper shape and out of
|
|
4562
|
-
// initial scope; the factory still wraps a reactive primitive, so
|
|
4563
|
-
// classify it as reactive-shaped rather than silently ignoring it.
|
|
4564
|
-
return { kind: 'reactive-shaped' }
|
|
4565
4985
|
}
|
|
4566
4986
|
|
|
4567
4987
|
return {
|
|
@@ -4573,6 +4993,7 @@ function detectReactiveFactory(
|
|
|
4573
4993
|
returnKind,
|
|
4574
4994
|
localBindings,
|
|
4575
4995
|
loc,
|
|
4996
|
+
renameSites,
|
|
4576
4997
|
},
|
|
4577
4998
|
}
|
|
4578
4999
|
}
|
|
@@ -4741,30 +5162,49 @@ function rewriteFactoryCallsInSource(
|
|
|
4741
5162
|
const thisCallIndex = callSiteIndex++
|
|
4742
5163
|
const suffix = `_bf${thisCallIndex}`
|
|
4743
5164
|
|
|
4744
|
-
//
|
|
4745
|
-
|
|
4746
|
-
//
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
5165
|
+
// Merged rename map, one entry per old name → replacement text.
|
|
5166
|
+
// Precedence (matches the pre-#2341 sequential-pass outcome): param
|
|
5167
|
+
// substitution > return→caller rename > internal suffix rename — later
|
|
5168
|
+
// `.set()` calls for the same key overwrite earlier ones below.
|
|
5169
|
+
const renames = new Map<string, string>()
|
|
5170
|
+
// 1. Suffix-rename internal bindings not excluded (tuple params/returns,
|
|
5171
|
+
// or object-path destructured names — see excludeFromSuffixRename's
|
|
5172
|
+
// callers).
|
|
5173
|
+
for (const name of factory.localBindings) {
|
|
5174
|
+
if (!excludeFromSuffixRename.has(name)) renames.set(name, name + suffix)
|
|
5175
|
+
}
|
|
5176
|
+
// 2. Return identifiers → caller destructure names (tuple path only).
|
|
5177
|
+
if (renameReturnToCallerNames) {
|
|
5178
|
+
for (const [n, caller] of renameReturnToCallerNames) {
|
|
5179
|
+
if (caller !== n) renames.set(n, caller) // identity rename = no-op, skip
|
|
5180
|
+
}
|
|
4751
5181
|
}
|
|
4752
|
-
//
|
|
5182
|
+
// 3. Parameters → argument expressions. Atomic arguments (bare
|
|
4753
5183
|
// identifiers, numeric literals, string literals) are spliced
|
|
4754
5184
|
// directly; anything more complex is wrapped in parens to preserve
|
|
4755
|
-
// operator precedence at the splice site.
|
|
5185
|
+
// operator precedence at the splice site. Overwrites any same-name
|
|
5186
|
+
// return rename above — param substitution wins, as before #2341.
|
|
4756
5187
|
const atomicArg = /^(?:[\w$.]+|'[^'\\]*'|"[^"\\]*"|-?\d+(?:\.\d+)?)$/
|
|
4757
5188
|
for (let i = 0; i < factory.params.length; i++) {
|
|
4758
5189
|
const p = factory.params[i]
|
|
4759
|
-
const a = argTexts[i] ?? 'undefined'
|
|
4760
|
-
|
|
4761
|
-
body = body.replace(new RegExp(`\\b${escapeRegex(p)}\\b`, 'g'), wrapped)
|
|
5190
|
+
const a = (argTexts[i] ?? 'undefined').trim()
|
|
5191
|
+
renames.set(p, atomicArg.test(a) ? a : `(${a})`)
|
|
4762
5192
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
5193
|
+
|
|
5194
|
+
// Apply the merged renames as a single bottom-to-top position splice
|
|
5195
|
+
// over `bodySource`, using the sites collected once at detection time
|
|
5196
|
+
// (#2341 BUG-1) — never a text search, so string/template-literal
|
|
5197
|
+
// contents, property keys, `.prop` tails, and JSX intrinsic tags are
|
|
5198
|
+
// never touched, and (unlike the old sequential regex passes) an
|
|
5199
|
+
// argument expression already spliced in for an earlier site can never
|
|
5200
|
+
// be re-scanned and corrupted by a later rename.
|
|
5201
|
+
let body = factory.bodySource
|
|
5202
|
+
for (let i = factory.renameSites.length - 1; i >= 0; i--) {
|
|
5203
|
+
const site = factory.renameSites[i]
|
|
5204
|
+
const repl = renames.get(site.name)
|
|
5205
|
+
if (repl === undefined) continue
|
|
5206
|
+
const text = site.form === 'shorthand' ? `${site.name}: ${repl}` : repl
|
|
5207
|
+
body = body.slice(0, site.start) + text + body.slice(site.end)
|
|
4768
5208
|
}
|
|
4769
5209
|
|
|
4770
5210
|
edits.push({
|
|
@@ -4780,33 +5220,61 @@ function rewriteFactoryCallsInSource(
|
|
|
4780
5220
|
if (edits.length === 0) return source
|
|
4781
5221
|
|
|
4782
5222
|
// #2332 — one deduped import statement per specifier for every inlined
|
|
4783
|
-
// cross-file factory's re-provisioned imports
|
|
4784
|
-
//
|
|
4785
|
-
//
|
|
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
|
|
4786
5228
|
// consumer (ctx.imports → SSR templateImports AND client
|
|
4787
5229
|
// collectExternalImports both parse this same rewritten string).
|
|
4788
5230
|
const importsBySpecifier = new Map<string, Map<string, string>>() // specifier -> localName -> exportedName
|
|
5231
|
+
const typeImportsBySpecifier = new Map<string, Map<string, string>>() // specifier -> localName -> exportedName
|
|
4789
5232
|
for (const f of inlinedFactories) {
|
|
4790
5233
|
for (const r of f.requiredImports ?? []) {
|
|
4791
|
-
|
|
4792
|
-
|
|
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) }
|
|
4793
5237
|
names.set(r.localName, r.exportedName) // same-key duplicates are identical by prescan construction
|
|
4794
5238
|
}
|
|
4795
5239
|
}
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
const
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
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
|
+
})
|
|
4810
5278
|
const at = factoryImportInsertionOffset(sourceFile)
|
|
4811
5279
|
edits.push({ start: at, end: at, replacement: at === 0 ? lines.join('\n') + '\n' : '\n' + lines.join('\n') })
|
|
4812
5280
|
}
|
|
@@ -4853,10 +5321,6 @@ function isPascalCaseComponentFn(node: ts.Node): boolean {
|
|
|
4853
5321
|
return false
|
|
4854
5322
|
}
|
|
4855
5323
|
|
|
4856
|
-
function escapeRegex(s: string): string {
|
|
4857
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
4858
|
-
}
|
|
4859
|
-
|
|
4860
5324
|
// =============================================================================
|
|
4861
5325
|
// BF110 diagnostic (#931)
|
|
4862
5326
|
// =============================================================================
|
|
@@ -4895,6 +5359,7 @@ function declinedFactoryErrorCode(code: DeclinedReactiveFactory['code']): ErrorC
|
|
|
4895
5359
|
switch (code) {
|
|
4896
5360
|
case 'BF112': return ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE
|
|
4897
5361
|
case 'BF113': return ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION
|
|
5362
|
+
case 'BF114': return ErrorCodes.REACTIVE_FACTORY_PARAM_SHADOWED
|
|
4898
5363
|
default: return ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED
|
|
4899
5364
|
}
|
|
4900
5365
|
}
|